chore: docs/ aus dem Repository — lebt im Obsidian-Vault (Devcontainer-Mount), Q-05 entschieden; entpackte Deploy-Pakete ignoriert

Die Dateien bleiben in der Historie und im Vault; git rm --cached, nichts
auf der Platte gelöscht.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-10-05 13:30:33 +00:00
parent 2dda9ecc44
commit 984d001380
38 changed files with 6 additions and 9418 deletions

5
.gitignore vendored
View file

@ -76,3 +76,8 @@ config/files/
/RELEASE
/deploy-files.txt
/deploy-*.tgz
/deploy-*/
# Doku lebt im Obsidian-Vault (iCloud), per Devcontainer nach docs/ eingebunden
# — nicht im Repository (Audit Q-05, entschieden 05.10.2026).
/docs/

View file

@ -295,7 +295,7 @@ Grundlage: [`UMSETZUNGSAUDIT-2026-09-29.md`](UMSETZUNGSAUDIT-2026-09-29.md) (4
| 9.4 | A-01, A-02, A-03 | erledigt | Auftragstabelle `archive_pending_jobs` (`ArchivePendingQueue::push()`/`completeWith()`), `archive_pending_at` nur noch Sammelmarker; Nachholer ohne Schlüssel FAILURE; Sweep verschiebt in Quarantäne (30 Tage), Pause per `--pause`/`--resume` bzw. automatisch beim Restore-Abgleich, `archive:reregister` aus `manifest.json` |
| 9.5 | N09, B-04 bis B-07, A-04, A-06 | erledigt | Snapshot versioniert (Bytes- plus Metadaten-Hash, OG-Variante enthalten, `put()` geprüft, alle Fassungen ins Archiv); Bestätigung an Bild-ID und SHA gebunden; Entfernen unter Sperre mit frischem Stand; Korrektur archiviert über Auftrag; `precheck_withheld_at` getrennt von `withheld_at`; Depublikationsstand am Auftrag; Restore-Abgleich löscht Anwendungskopien |
| 9.6 | K-02, R-04, R-05, E9-Schalter | erledigt | Kontingent-/Bildsperre im Scheduler als „blockiert" (Exit 0, einmaliger Verlaufseintrag), KI-Job ohne Retry; nach Kundenbestätigung erneut routen; `X-Pk-Ref` per HMAC signiert, Hard-Sperre nur bei gültiger Signatur; Unmerge setzt nur unveränderte Füllwerte zurück (`company_merge_moves.merged_value`); `NEWSLETTER_SENDING_ENABLED` (Standard aus) |
| 9.7 | Q-01, Q-02, Q-06, P3, OPT | erledigt bis auf Q-05 | Testhelfer nach `tests/Pest.php` (Suite läuft parallel); Larastan voller Scope ohne Meldung; P3-Liste und OPT-1 bis OPT-9 umgesetzt (OPT-4 teilweise, Integritätshash bleibt bewusst); Q-07 war ein Fehlalarm (`IMAGE_PRECHECK_MODEL` steht auskommentiert in `.env.example`). **Q-05 (`docs/` lokal gelöscht, nicht committet) entscheidet der Betreiber.** |
| 9.7 | Q-01, Q-02, Q-06, P3, OPT | erledigt bis auf Q-05 | Testhelfer nach `tests/Pest.php` (Suite läuft parallel); Larastan voller Scope ohne Meldung; P3-Liste und OPT-1 bis OPT-9 umgesetzt (OPT-4 teilweise, Integritätshash bleibt bewusst); Q-07 war ein Fehlalarm (`IMAGE_PRECHECK_MODEL` steht auskommentiert in `.env.example`). Q-05 entschieden am 05.10.2026: `docs/` ist der per Devcontainer eingebundene Obsidian-Vault und kommt aus dem Repository (`.gitignore`), die Historie bleibt. |
Zusätzlich bei der Integration: `press-release-images:sweep-orphan-originals` wöchentlich (So 04:45) im Scheduler, löscht nichts, solange der Archiv-Sweep pausiert ist; `purge-events` von 03:20 auf 03:50; alle `withoutOverlapping()` mit kurzer Sperrdauer (`ScheduleConfigurationTest`); Scheduler-Tabelle im Runbook vollständig. **Im Runbook stand seit `665c94f` (21.07.2026) ein Passwort neben dem KAS-SSH-Zugang** — aus der Arbeitskopie entfernt; in der Git-Historie und auf `origin` bleibt es, deshalb rotieren (Betreiber).

View file

@ -1,206 +0,0 @@
# Decision-Update · Preisstruktur & Veröffentlichungs-Flow (Launch)
**Version:** Juni 2026 **Datum:** 11.06.2026 **Status:** Abgestimmt – bereit zur Integration ins Konzept-/Decision-Log **Scope:** Launch-Preisstruktur, PM-Kontingente, Launch-Credit-Umfang, Veröffentlichungs-Flow. Ersetzt die betroffenen Stellen früherer Tarif-Festlegungen (insb. `konzept/Konzept-Update 1` §8–10 und `user-admin/Presseportal – Konzept für Relaunch` §8–10).
> **IST-Stand 11.06.2026**: Reines Entscheidungs-Dokument, noch nicht
> umgesetzt. Im Code existieren bisher nur der Quota-Stub
> (`users.press_release_quota`, zählt aktuell beim **Einreichen** statt bei
> der Veröffentlichung) und die KI-Klassifikation (Rot/Gelb/Grün, siehe
> `user-admin/Entwicklungsplan KI-Pruefung und Veroeffentlichung.md`).
> Zahlung/Tarife, Submit-Gate hinter Buchung, Slot-Verbrauch bei
> Veröffentlichung, Tageslimit, Einzel-PM und die drei Credit-Posten sind
> offen — siehe Abgleich in `STATUS-ABGLEICH-USER-PANEL.md` §3.5.
---
## 1. Kontext
Dieses Update bündelt die in der Abstimmung getroffenen Entscheidungen zur Preisstruktur und zum Veröffentlichungs-Flow für den Relaunch. Leitlinie blieb durchgängig die Anti-Zombie-Positionierung: keine versteckten Gebühren, keine künstliche Verknappung, kein Bezahlen für Leistungen, die nicht erbracht wurden. Mehrere ältere Festlegungen wurden bewusst überschrieben (siehe Abschnitt 5).
---
## 2. Finalisierte Tarifstruktur (Launch)
|Tier|Monatlich|Jährlich|PMs/Monat|Pro PM|
|---|---|---|---|---|
|**Starter**|29 €|290 €|3|9,67 €|
|**Business**|49 €|490 €|10|4,90 €|
|**Pro**|99 €|990 €|25|3,96 €|
|**Agency**|199 €|1.990 €|60|3,32 €|
**Einzel-PM:** 19 € einmalig – geführt als **separater No-Abo-Block** neben dem Tarif-Raster, nicht als linke/billigste Spalte. Kommunikation über das No-Commitment-Argument („Einmal veröffentlichen, kein Abo, keine Kündigung"), nicht über den Preis.
### 2.1 Klarstellung Preise & Steuern (Einwand 12.06.2026)
**Alle neuen Preise sind Netto-Preise.** Die Umsatzsteuer wird zur
Rechnungsstellung anhand der Rechnungsadresse bestimmt und sauber
ausgewiesen:
- **Deutschland** → grundsätzlich immer mit Steuer (aktuell 19 %).
- **EU-Ausland** → nur mit gültiger USt-ID steuerbefreit (Reverse Charge),
sonst mit Steuer.
- **Drittländer** → grundsätzlich steuerbefreit.
Zum Vergleich Legacy: Dort waren die Beträge **brutto** (z. B. 199 € inkl.
Steuer); steuerbefreite Kunden erhielten den Netto-Ausweis (167,23 €).
Grandfathered-Vereinbarungen werden deshalb auf die Netto-Basis der letzten
Legacy-Rechnung umgerechnet — für deutsche Bestandskunden bleibt der
Bruttobetrag damit unverändert, die Steuer wird künftig nur sauber
ausgewiesen.
**Enterprise:** sichtbar, aber als **dezenter Sales-Hinweis unterhalb der Tabelle** („Größere Mengen oder mehrere Marken? → Kontakt"). Keine eigene Preisspalte, individuelles Pricing.
---
## 3. Mechaniken & Regeln
### 3.1 Jahrespreis-Kommunikation
Die Jahrespreise entsprechen exakt **10 Monatsbeiträgen** → kommuniziert als **„2 Monate gratis"** statt als Prozent-Rabatt. Numerisch identisch zu den Bestandszahlen, nur die Darstellung ändert sich. Der konkrete Hebel zieht psychologisch stärker und passt zur Ehrlichkeits-Linie.
### 3.1.1 Klarstellung Laufzeit & Kündigung (Entscheidung 07.07.2026)
Abonnements verlängern sich jeweils automatisch um die **gewählte Laufzeit**:
- **Monatsabo:** verlängert sich monatlich, **monatlich kündbar**.
- **Jahresabo:** verlängert sich um jeweils **weitere zwölf Monate**, kündbar **zum Ende der jeweiligen Laufzeit**.
Ersetzt die frühere Festlegung aus `legal/Entscheidung.md` E5 (alt), wonach Jahresabos nach der Erstlaufzeit auf monatliche Abrechnung umgestellt wurden. Als Anti-Zombie-Ausgleich bleibt die **Erinnerung vor der Verlängerung** (T8) verpflichtend; die geänderte AGB-Klausel (§ 11 Abs. 2) ist bei der fachanwaltlichen Prüfung erneut vorzulegen.
### 3.2 PM-Kontingent: Verbrauch
**Der PM-Slot zählt ausschließlich bei Veröffentlichung runter, nicht bei der Prüfung.**
- Rot (abgelehnt) → **kein** Slot verbraucht
- Gelb/Grün (veröffentlicht) → Slot zählt runter
Begründung: Wer nichts veröffentlicht bekommt, zahlt keinen Slot. Schützt insbesondere ehrliche Nutzer, deren PM erst nach Nachbesserung durchgeht (relevant ab Phase 2).
### 3.3 Tageslimit (Flut-Schutz)
Schützt die redaktionelle Glaubwürdigkeit des Portals gegen Dumping, nicht den Umsatz. Greift realistisch nur in den oberen Tiers.
|Tier|PMs/Monat|Max./Tag|
|---|---|---|
|Starter|3|–|
|Business|10|2|
|Pro|25|3|
|Agency|60|5|
Das Tageslimit gilt **auch für nachgekaufte Extra-PMs** – sonst würde Extra-PM zum „Spam freikaufen". Höherer Tagesdurchsatz = Enterprise-Fall.
### 3.4 Einzel → Abo-Brücke
Wer als Einzel-Käufer innerhalb von 30 Tagen ein Abo abschließt, bekommt die 19 € auf den ersten Monat angerechnet. Schützt das Einmal-Segment und bietet einen sauberen Upgrade-Pfad.
---
## 4. Launch-Credit-System (klein gehalten)
Zum Launch greifen genau drei Credit-Posten – alle ohne KI-Abhängigkeit, alle mit echtem Nutzen ab Tag 1:
|Posten|Status|Mechanik|
|---|---|---|
|**Extra-PM**|✅ Launch|Monatskontingent voll → einzelne PM nachkaufbar (faire Alternative zum Zwangs-Upgrade)|
|**Boost / Platzierung**|✅ Launch|Nur für **grüne** PMs, nachträglich kaufbar|
|**Veröffentlichungsnachweis (PDF)**|✅ Launch|Kleiner Mitnahme-Posten, PR-Beleg fürs Reporting|
**Credit-Anker:** 1 Credit = 1 € (Listenpreis), Volumenrabatt über Pakete.
**Verlinkung:** Links zur Kundenseite sind Standard-Bestandteil jeder PM, systemseitig als `sponsored`/`nofollow` ausgezeichnet. Hervorhebung der Linkdarstellung ist als Produkt-Feature möglich. **Tabu:** Verkauf von Dofollow-Backlinks und kundenseitige `rel`-Auswahl. Details siehe Decision-Update „Verlinkung & Backlinks".
---
## 5. Veröffentlichungs-Flow (Launch)
```
Schreiben & „Speichern" → buchen → „Speichern & zur Prüfung einreichen"
(frei) (gegated hinter Buchung)
│
KI-Prüfung (Red-Flag)
│
┌──────────────────────────┼──────────────────────────┐
ROT GELB GRÜN
abgelehnt veröffentlicht veröffentlicht
kein Slot weg Slot −1 Slot −1
(Boost nachkaufbar)
```
### 5.0 Klarstellung Gelb-Routing (Entscheidung 12.06.2026)
**Gelb geht zum Launch direkt live — es gibt keine manuelle Prüf-Queue.**
Die Klassifikation kennt nur zwei Ausgänge:
- **Rot** = Inhalte, die rechtlich oder inhaltlich **nicht veröffentlichbar**
sind → Ablehnung mit Meldung/Begründung an den Autor, kein Slot-Verbrauch.
- **Gelb/Grün** = veröffentlichbar → der Beitrag geht in der ersten Phase
direkt online (sofort bzw. zum geplanten Termin), Slot −1.
Gelb bleibt als interne Markierung erhalten (z. B. nicht boostbar, Signal
für den Admin), löst aber **keinen** manuellen Review-Schritt aus. Nach dem
Relaunch sind **Vorabprüfungen** geplant (Phase 2, siehe Abschnitt 7), die
Usern die Möglichkeit geben, ihren Beitrag vor der Einreichung zu
korrigieren.
### 5.1 Zwei-Button-Logik
- **„Speichern"** – immer frei, auch ohne Buchung. Entwürfe schreiben/ablegen reibungslos, auch _vor_ der Buchung (Conversion-Vorteil: fertige PM senkt Kaufhürde).
- **„Speichern & zur Prüfung einreichen"** – sichtbar, aber gegated. Ohne aktive Buchung öffnet das Modal einen Buchungs-Hinweis statt des Prüf-Flows. Der Button konvertiert, er verschwindet nicht.
Begründung für den Gate: Die Prüfung ist die erste kostenpflichtige KI-Ressource. Kein gebuchtes Produkt → kein Ressourcenverbrauch.
### 5.2 Kein Re-Check zum Launch
Einreichen ist zum Launch eine **Einbahnstraße**: Die PM geht durch (gelb/grün → live) oder wird abgelehnt (rot). Es gibt **keinen** „nachbessern und erneut prüfen"-Loop, weil Redigieren/Vorab-Prüfung erst Phase 2 sind.
Konsequenz: Pro PM gibt es genau **eine** Prüfung, untrennbar an die Veröffentlichung gekoppelt. Eine eingereichte PM = eine Prüfung = (bei gelb/grün) eine Veröffentlichung. Damit existiert zum Launch **kein Prüf-Abuse-Vektor** → der komplette Prüfzähler-Mechanismus ist zum Launch nicht nötig.
### 5.3 Keine Gratis-Test-Prüfung
Harter Gate zum Launch. Eine kostenlose Vorab-Prüfung würde den Flow nur verkomplizieren (wenn eine PM durchgeht, wird sie ohnehin veröffentlicht) und einen Abuse-Vektor öffnen (Wegwerf-Accounts). Eine kontrollierte Test-Prüfung (z. B. pro verifizierter Domain) bleibt als spätere Option offen.
---
## 6. Überschriebene Entscheidungen
|Bereich|Alt|Neu|Grund|
|---|---|---|---|
|**PM-Kontingent Pro**|60/Monat|**25/Monat**|60 unrealistisch hoch → Overage-System griff nie, Kontingent wirkte faktisch „unbegrenzt"|
|**PM-Kontingent Agency**|150/Monat|**60/Monat**|dito; Kontingente jetzt an realistischer PR-Frequenz kalibriert|
|**Jahrespreis-Kommunikation**|„ca. 17 % Rabatt"|**„2 Monate gratis"**|gleicher Preis, stärkerer psychologischer Hebel, klarer|
|**Bonus-Credits in Tarif-Tabelle**|12/30/60/120 als Tarif-Argument|**entfernt** (→ Phase 2 als Prüf-Kontingent)|bewarb zum Launch eine Leistung ohne Verbrauchsmöglichkeit|
---
## 7. Auf Phase 2 verschoben
|Punkt|Warum verschoben|
|---|---|
|**Vorab-KI-Prüfung**|erzeugt erst die Situation „prüfen ohne (noch) zu veröffentlichen"|
|**Redigieren / Nachbearbeiten**|setzt Re-Check-Loop voraus|
|**Prüfzähler** (freie Prüfungen/Monat, z. B. 12/30/60/120, eigener Zähler)|erst mit Re-Check relevant; deckelt dann „prüfen ohne veröffentlichen"|
|**Credit-Overflow für Prüfungen**|Prüfzähler leer → weitere Prüfungen ziehen aus echter Credit-Wallet|
|**Klon-/Abuse-Schutz über Account-Monatslimit**|aggregiertes Limit pro Account statt Klon-Erkennung; greift erst bei Re-Check|
|**Score-Feinstufung für Boost**|„nur Geprüft/Hochwertig boostbar" – setzt vollen Content-Score voraus|
|**Tier-gestaffelte Prüf-Versuche**|als „BALD" markiert; zum Launch flach|
**Designprinzip für Phase 2 festgehalten:** Eigener **Prüf-Zähler** (getrennt von der Credit-Wallet), damit „Prüfungen inklusive" ein sauberes Versprechen bleibt und Prüf-Budget nicht versehentlich für Boost/PDF verbraucht wird. Abuse-Schutz über aggregiertes Account-Monatslimit + Prüf-Tageslimit – nicht über Klon-Erkennung.
---
## 8. Offene Stellschrauben (vor Phase-2-Bau zu entscheiden)
- **Boost-Nachkaufpreis** relativ zum inkludierten PM-Preis – klar darüber (treibt Upgrade) oder nur leicht darüber (bequemer, schwächerer Upgrade-Sog).
- **Höhe des Prüf-Kontingents** je Tier final bestätigen, sobald Vorab-Prüfung gebaut wird (Ausgangsvorschlag 12/30/60/120).
- **Credit-Paketliste** auf Konsistenz prüfen (vom Nutzer angekündigt, separat einzubringen).
---
## 9. Anti-Zombie-Check (dieser Stand)
- ✅ Keine versteckten Gebühren – Extra-PM und Boost sind sichtbare, optionale Zukäufe
- ✅ Keine künstliche Verknappung – Kontingente decken den Normalfall bequem; Limits greifen nur bei echtem Power-/Abuse-Verhalten
- ✅ Kein Bezahlen für nicht erbrachte Leistung – rot abgelehnte PM verbraucht keinen Slot
- ✅ Tageslimit als Qualitätsschutz fürs Portal begründet, nicht als Verkaufstrick
- ✅ Kein verkaufter Dofollow-Backlink
- ✅ Free schreiben, Gate erst beim Einreichen – Friktion an der richtigen Stelle

View file

@ -1,253 +0,0 @@
# Entwicklungsplan: Meldeverfahren v2 (Selbstbedienungs-Flow „PM melden")
**Stand:** 22.07.2026
**Grundlage:** `Lösch- und Änderungsanfragen.md` (Entscheidungsachse, Free/Paid-Matrix, §11 Self-Service-Ausbau) und `Pressemitteilung melden.md` (Vier-Spuren-Flow)
**Ziel:** Portal übernimmt den gesamten Post-Publication-Zustand selbst (connektar = write-once). Vier Spuren: Kleine Korrektur (frei), DSGVO-Anonymisierung (frei, Admin-Gate), Depublizierung (kostenpflichtig), Änderung (kostenpflichtig, KI-Freigabelauf).
---
## 0. Ist-Stand: Was bereits existiert (Bauen auf, nicht neu)
Die Codebasis enthält bereits ein **Meldeverfahren v0.1** und fast alle benötigten Bausteine. Der Plan ist ein **Ausbau**, kein Neubau.
| Baustein | Vorhanden | Relevante Dateien |
|---|---|---|
| Melde-Queue (DSGVO / Persönlichkeitsrecht / Meldung) | ✅ v0.1: Intake → Admin-Queue, manuelle Erledigung | `app/Models/LegalRequest.php`, `app/Enums/LegalRequestType.php`, `app/Enums/LegalRequestStatus.php` |
| Öffentliche Meldeformulare (Honeypot, Rate-Limit, DSA-Eingangsbestätigung `M-{id}`) | ✅ `melden/{way}` + PM-gebunden `/release/{slug}/rechtliches` | `app/Http/Controllers/Web/ReportFormController.php`, `app/Http/Controllers/LegalRequestController.php`, `routes/legal.php` |
| Admin-Queue „Recht & Compliance" (`can:compliance:manage`) | ✅ Status-Handling (open/in_progress/resolved/rejected), aber **keine Ausführungs-Aktionen** (kein Anonymisieren/Depublizieren aus der Queue) | `resources/views/livewire/admin/legal-requests/{index,show}.blade.php`, `routes/admin.php` |
| Magic-Link-Infrastruktur (sha256-Token, purpose, payload, TTL, Consume) | ✅ purposes `login`, `press_release_access` | `app/Models/MagicLink.php`, `app/Services/Auth/MagicLinkGenerator.php`, `app/Http/Controllers/Auth/MagicLinkConsumeController.php`, `app/Console/Commands/PurgeMagicLinks.php` |
| Depublizierung + Tombstone | ✅ `depublish()` → Archived + `depublished_at`, Detail-URL liefert **HTTP 410** Tombstone | `app/Services/PressRelease/PressReleaseService.php:394`, `app/Http/Controllers/Web/ReleaseDetailController.php`, `resources/views/web/release-tombstone.blade.php` |
| Bezahlte Depublizierung (Stripe-Checkout + Webhook-Fulfillment) | ✅ aber nur **eingeloggt** (`routes/customer.php:52`) | `app/Services/Billing/StripeCheckoutService.php:452`, `app/Listeners/ProcessStripeWebhook.php:475`, `app/Enums/SinglePurchaseType.php` |
| Credits (Ledger, atomar) | ✅ | `app/Services/Billing/CreditWalletService.php` |
| KI-Prüfung (grün/gelb/rot, Audit) | ✅ Klassifikation + Routing bei Erst-Freigabe; Re-Check nach Edit **ohne** Status-Wirkung | `app/Jobs/ClassifyPressRelease.php`, `PressReleaseService::routeByClassification():176`, `reprocessAfterContentChange():150`, `app/Models/KiAudit.php` |
| Hinterlegte Pressekontaktadresse | ✅ `Contact.email` (Pivot `press_release_contact`), Fallback `Company.email` | `app/Models/Contact.php`, `app/Models/Company.php` |
| Audit/Verlauf | ✅ `ActivityLog::record()` + `PressReleaseStatusLog` (mit `source`) | `app/Services/ActivityLog.php`, `app/Models/PressReleaseStatusLog.php` |
| connektar-Eingang | ✅ Legacy-API (`store()` → Review/KI bzw. Sofort-Publish), Bulk-Import mit `LegacyImportMap`-Skip-Guard | `app/Http/Controllers/Api/Legacy/LegacyPressReleaseController.php`, `app/Services/Import/PressReleaseImporter.php` |
**Fehlende Kernstücke (= dieser Plan):** Re-Sync-Sperre als PM-Flag, Anonymisierungs-Engine (Wortgrenzen-Replace + Diff), Magic-Link-Verifikation für das Meldeverfahren, Self-Service-Portal mit den vier Spuren, Admin-Ausführungsaktionen in der Queue, Gast-Checkout für Depublizierung/Änderung, Reversibilitätsfenster, verschlüsselte Alt-Wert-Snapshots mit Löschfrist.
---
## 1. Phasenübersicht
Reihenfolge nach Nutzen und Abhängigkeit: Zuerst die Bausteine, die den **manuellen Interimsprozess** sofort absichern (Re-Sync-Sperre, Anonymisierung im Admin), dann Verifikation, dann die Self-Service-Spuren.
```
Phase 1 Datenmodell + Re-Sync-Sperre ✅ umgesetzt 22.07.2026
Phase 2 Anonymisierungs-Engine + Admin-Ausführung ✅ umgesetzt 22.07.2026
Phase 3 Magic-Link-Verifikation + Intake-Trichter ✅ umgesetzt 22.07.2026
Phase 4 Spur A — Kleine Korrektur (self-service) ✅ umgesetzt 22.07.2026 (panel-first)
Phase 5 Spur B — DSGVO-Einreichung (self-service → Admin-Gate) ✅ umgesetzt 23.07.2026
Phase 6 Spur C — Depublizierung (Gast-Checkout + Reversibilitätsfenster)
Phase 7 Spur D — Änderung (Checkout + KI-Vorprüfung) ✅ umgesetzt 23.07.2026
Phase 8 Querschnitt-Abschluss: Kommunikation, Auslieferungs-Checks, Doku
```
Jede Phase ist eigenständig deploybar; nach Phase 2 ist der Interims-Triage-Prozess (§4 des Konzepts) technisch vollständig unterstützt.
### Stand 22.07.2026 abends — was noch fehlt (Einstieg für morgen)
**Phasen 1–4 sind umgesetzt und getestet** (Details in den Phasen-Abschnitten). Offen:
1. ~~**Phase 5 — Spur B strukturiert**~~ ✅ **umgesetzt 23.07.** (Details im Phase-5-Abschnitt).
2. **Phase 6 — Spur C:** ✅ **Re-Sync-Sperre nachgerüstet (23.07.):** `PressReleaseService::depublish()` setzt die Sperre jetzt IMMER selbst (kein Aufrufer kann sie vergessen) und invalidiert die Sitemap-Caches; der Admin-Pfad gibt seine Vorgangsnummer als Sperrgrund mit. **Gast-Checkout zurückgestellt (Entscheidung 23.07.):** Magic-Link-Login ins Panel deckt Pressekontakte ab — dort existiert die bezahlte Depublikation bereits. Noch offen aus Phase 6: Reversibilitätsfenster-Hinweis („umkehrbar bis …", `legal.reversal_window_days`) in Panel/Mails.
3. ~~**Phase 7 — Spur D Checkout**~~ ✅ **Panel-Pfad am 23.07. umgesetzt** — mit geändertem Flow (E2 revidiert, siehe Phase-7-Abschnitt): KI-Vorprüfung **vor** der Zahlung im Modal, rot → Ablehnung ohne Zahlung, je PM nur einmal möglich; Obergrenze `legal.change_max_distance_percent` (50 %). Offen: Gast-Pfad für Magic-Link-Verifizierte, Bestätigungsmail nach Ausführung.
4. ~~**Spur A im Web-Pfad**~~ ✅ **erledigt per Entscheidung 23.07.:** Kein eigener Web-Korrektur-Pfad nötig — hinterlegte Pressekontakte loggen sich per bestehendem Magic-Link-Login ins Panel ein und nutzen dort Spur A/D. Bestätigungsmail umgesetzt: `PressReleaseChangeApplied` (Typ `press_release_corrected`, abbestellbar) nach Spur-A-Korrektur (sofort) und Spur-D-Änderung (nach Webhook-Ausführung).
5. **Phase 8 — Querschnitt:** connektar-Template-Stopp in allen Texten prüfen, `noindex` im Tombstone verifizieren, zweistufige Eskalations-Mailtexte (Ablehnung mit Aufsichtsbehörden-Hinweis), Doku-Abgleich.
6. ~~**Ops:** Dev-DB-Migration~~ ✅ **erledigt 23.07.:** Das ALTER war serverseitig fertig (Migration manuell nachregistriert), alle restlichen Migrationen gelaufen — der komplette Flow ist auf pressekonto.test browser-testbar.
7. ✅ **Öffentliche Triage-Seite (23.07.):** `/release/{slug}/rechtliches` ist jetzt zweistufig — Stufe 1 wählt das Anliegen (Absender-Selbstservice / DSGVO / Persönlichkeitsrechte / Meldung), Stufe 2 leitet Absender-Anliegen und DSGVO/Persönlichkeitsrechte primär auf den Magic-Link-Weg (Deep-Link zum `/melden/pm`-Trichter mit vorbefüllter PM-URL); das Kontaktformular ist dort nur der Fallback mit Pflicht-Hinweis auf die persönliche Identifizierung. Meldungen rechtswidriger Inhalte gehen direkt ins Formular (DSA). Der Store-Endpoint akzeptiert nur noch `report`/`dsgvo`/`personal_rights` — Absender-Spuren können nicht mehr als anonyme Queue-Anfrage einlaufen.
8. **Konfig-Feinjustierung (22.07. abends):** `correction_max_distance_percent` auf **5 %** und `corrections_per_week` auf **1** verschärft (statt 15 %/2 aus E5). Dazu im Service die **Mini-Edit-Ausnahme**: Änderungen ≤ 2 Zeichen gelten immer als klein — sonst würde bei kurzen Titeln ein einzelner Tippfehler die Prozent-Grenze reißen.
---
## Phase 1 — Datenmodell + Re-Sync-Sperre ✅ **umgesetzt (22.07.2026)**
**Warum zuerst:** „Jede Mutation setzt die Re-Sync-Sperre" ist strukturell zwingend (§7), sonst überschreibt ein Re-Import jede Anonymisierung — das wäre ein DSGVO-Verstoß nach erfolgter Löschung.
### 1.1 Migrationen ✅
- ✅ `press_releases`: `sync_locked_at` (nullable timestamp) + `sync_lock_reason` (string) — `2026_07_22_151506_add_sync_lock_to_press_releases_table.php`. **Umsetzungshinweis:** Wegen des FULLTEXT-Index kann MySQL hier nicht INSTANT arbeiten (voller Tabellen-Copy, ~175k Zeilen); die Migration nutzt deshalb **ein kombiniertes ALTER** für beide Spalten statt zweier Blueprint-Einzelstatements und verzichtet auf `after()`-Positionierung. Merkregel für spätere Migrationen an `press_releases`: Spalten-Adds immer bündeln.
- ✅ `legal_requests` erweitert — `2026_07_22_151507_add_meldeverfahren_fields_to_legal_requests_table.php`:
- Neue `LegalRequestType`-Cases: `Correction` (Spur A), `Depublication` (Spur C), `Change` (Spur D). `Dsgvo` bleibt Spur B.
- Neue Spalten: `verified_at`, `verification_method` (Enum `LegalRequestVerificationMethod`: `magic_link`/`manual`/`logged_in`), `single_purchase_id` (FK, Spur C/D), `executed_at`.
- Diff-/Schwärzungsdaten kommen in `payload` (json, existiert) — Nutzung ab Phase 4/5.
- ✅ Neue Tabelle `press_release_redaction_snapshots` (`2026_07_22_151507_create_..._table.php`) + Modell `PressReleaseRedactionSnapshot` mit `capture()` (Cast `encrypted:array`, `purge_after` aus `config('legal.snapshot_retention_days')`), Scope `dueForPurge()` und Factory. → Reversibilität ohne Klartext-Altwerte im Log; Test sichert ab, dass der Klartext nie unverschlüsselt in der DB liegt.
### 1.2 Re-Sync-Sperre durchsetzen ✅
- ✅ `PressRelease::lockSync(string $reason)` + `isSyncLocked()` — idempotent (erste Sperre bleibt maßgeblich), schreibt Verlaufs-Eintrag (neuer `ActivityType::SyncLock`, Badge rot). Wird von **jeder** Meldeverfahren-Mutation aufgerufen (Phasen 2, 4, 5, 6, 7).
- ✅ `PressReleaseImporter`: gesperrte PMs werden **auch bei `--force`** übersprungen (Guard `isSyncLocked()` vor `updateOrCreate`, zählt als „Übersprungen").
- ℹ️ `LegacyPressReleaseController`: kein Guard nötig — die Legacy-API hat heute **keinen Update-Pfad** (`store()` legt immer neu an, `show()` liest nur). Sollte je ein Update-Endpunkt entstehen, gehört der `sync_locked_at`-Check (→ `409`/`423`) dorthin.
### 1.3 Tests ✅ (`tests/Feature/Legal/SyncLockTest.php`, 5 Tests grün)
- ✅ `lockSync()`: Felder gesetzt, Verlaufs-Eintrag, idempotent (zweiter Aufruf ändert nichts).
- ✅ Importer überspringt gesperrte PM trotz `--force` (Schwärzung bleibt erhalten); ungesperrte PM wird weiter aktualisiert.
- ✅ Snapshot: Klartext liegt nie unverschlüsselt in der DB, `purge_after` aus Config, `dueForPurge()`-Scope.
- Angrenzende Bestandstests (Importer-Datumslogik, LegalRequest, ActivityLog — 23 Tests) weiter grün; Pint sauber.
---
## Phase 2 — Anonymisierungs-Engine + Admin-Ausführung ✅ **umgesetzt (22.07.2026)**
**Ziel:** Der Admin kann aus der bestehenden Queue (`admin/legal-requests/show`) eine DSGVO-Anfrage tatsächlich **ausführen** statt nur „resolved" zu klicken. Das trägt sofort die Referenzfälle Huraux/Gleue/Rambo.
### 2.1 Service `App\Services\PressRelease\AnonymizationService` ✅
- ✅ `preview(PM, Begriffe, Ersetzungswert)`: Pflicht-Vorschau ohne Mutation — Treffer + Nachher-Wert je Feld (`title`, `subtitle`, `text`, `boilerplate_override`, `keywords`; `teaser_*` sind Integer und entfallen). Genutzt von Admin (Phase 2), wiederverwendbar im Self-Service (Phase 5).
- ✅ Ersetzung über Wortgrenzen: **Unicode-Lookarounds** (`(?<![\pL\pN])…(?![\pL\pN])`) statt `\b`, damit Umlaute korrekt begrenzen — „Max" zerlegt „maximal" nicht, „Mustermanns" bleibt ohne explizite Variante stehen. Case-insensitive, längste Begriffe zuerst. Ersetzungswert default `[entfernt]`, nie Leerstring.
- ✅ `execute()`: Transaktion — verschlüsselter Snapshot (`capture()`: Felder, Kontakt-IDs, Bild-/Anhang-Metadaten, Begriffe) → Replace → Kontakt-Pivot lösen (optional) → markierte **Bilder/Anhänge** soft-löschen, auf public-Disk zusätzlich Dateien inkl. Varianten physisch löschen (sonst per Direkt-URL erreichbar) → `lockSync()` → Sitemap-Cache aller Hosts invalidieren (interne Suche liest live) → `ActivityLog` (neuer Typ `Anonymization`) **ohne Alt-Werte und ohne Schwärzbegriffe**.
- ✅ Kein KI-Gate, kein Credit-Abzug — der Service berührt weder Klassifikations-Routing noch `CreditWalletService`.
### 2.2 Admin-Queue ausgebaut (`admin/legal-requests/show`) ✅
- ✅ Panel „Anonymisierung (Schwärzung)" bei `type=dsgvo`/`personal_rights` mit PM-Bezug und offenem Status: Schwärzbegriffe (ein Begriff/Variante pro Zeile), Ersetzungswert, Checkbox „Kontakt-Zuordnungen lösen", Bild-Auswahl → **Pflicht-Vorschau** (Ausführen-Button erscheint erst danach; Ausführen ohne Vorschau wird serverseitig abgewiesen) → „Anonymisieren ausführen" oder **„Stattdessen depublizieren"** (Matrix-Test §1/§2 menschlich; Vanity-PM → Depublizierung als milderes Restmittel).
- ✅ Depublizieren-Aktion: `PressReleaseService::depublish()` + `lockSync()` + Aktenvermerk über `admin_note`; setzt `executed_at` + `resolved`. Deckt den Fallback „Kanal tot" (Huraux) ab — Admin führt nach manueller Prüfung aus, umkehrbar (Status-Restore, `published_at` bleibt).
- ✅ Detail zeigt `verification_method`/`verified_at` („nicht verifiziert" bis Phase 3) und `executed_at`. Queue-Index-Spalte folgt mit Phase 3, wenn Verifikation real befüllt wird.
### 2.3 Purge-Command ✅
✅ `snapshots:purge` (`PurgeRedactionSnapshots`, Scope `dueForPurge()`), Scheduler täglich 03:10 mit `withoutOverlapping` + Failure-Alert in `routes/console.php`.
### 2.4 Tests ✅ (9 neue Tests grün)
- ✅ `AnonymizationServiceTest`: Wortgrenzen/Varianten/Genitiv, Vorschau mutiert nicht, Snapshot verschlüsselt (Klartext weder im DB-Rohwert noch im Verlauf), Kontakte gelöst, Sperre gesetzt, Bilder soft-gelöscht + public-Dateien entfernt, Purge-Command löscht nur fällige Snapshots.
- ✅ `AdminLegalRequestExecutionTest`: Vorschau→Ausführen-Happy-Path, Ausführen ohne Vorschau abgewiesen, Depublizieren-Alternative, verboten ohne `compliance:manage`.
- Angrenzende Suiten (37 Tests: Legal, Admin-Grenzen, ActivityLog) weiter grün; Pint sauber.
---
## Phase 3 — Magic-Link-Verifikation + Intake-Trichter ✅ **umgesetzt (22.07.2026)**
**Ziel:** Die Meldeseite wird vom Kontaktformular zum **Deflektions-Trichter**: URL einfügen → System schlägt hinterlegten Kontakt nach → Magic-Link an diesen → verifizierter Zugang zur Anliegen-Auswahl.
### 3.1 Intake ✅ (`PressReleaseReportController`, Routen `melden/pm*` in `routes/web.php`)
- ✅ Neue Seite „PM melden/ändern" (`web/melden-pm.blade.php`): URL-Feld → `matchPublishedRelease()` → bei PM-Fund + Pressekontakt-Mail (`PressRelease::pressContactEmail()`: PM-Kontakt via Pivot, Fallback Firmen-Adresse, ohne PortalScope) Magic-Link-Versand. Vier-Anliegen-Tabelle mit Kosten **vorab** auf der Seite (Anti-Zombie). Callout-Einstieg auf der bestehenden `melden`-Übersichtsseite.
- ✅ Adresse **nie angezeigt/übernommen** — nur maskiert („p***@firma.example").
- ✅ Kanal tot: Hinweis mit Link auf das bestehende manuelle Formular (`melden/kontaktdaten`) — dort verifiziert der Admin anhand eines Nachweises (§6-Fallback, Ausführung über Phase 2). *(Bewusst kein neues Formular — der DSA-Weg existiert bereits.)*
- ✅ Rate-Limits: pro IP (5/h) **und** pro PM (`legal.report_links_per_day` = 3/Tag) + Honeypot + `throttle:10,1`.
### 3.2 Magic-Link ✅
- ✅ `MagicLinkGenerator::createReportLink()`: purpose `press_release_report` (**Enum-Migration** `2026_07_22_171134_add_report_purpose_to_magic_links_table.php` — `purpose` ist ein DB-Enum!), payload `{press_release_id, contact_email}`, `user_id` = PM-Owner, TTL `legal.report_link_ttl_hours` = 72; ältere unverbrauchte Report-Links derselben PM werden entwertet.
- ✅ Mailable `PressReleaseReportLink` (queued, `afterCommit`) mit Hinweis „kein Anliegen gestartet? → ignorieren".
- ✅ Consume-Route (`melden/pm/verifizieren/{token}`, throttled): atomares Claim (Muster `MagicLinkConsumeController`) → Session-Marker (`legal.report_session_minutes` = 60) → Anliegen-Auswahl. Kein Login.
- ✅ Eingeloggter Creator: POST `me.press-releases.report` (prüft `accessibleBy`, `verification_method=logged_in`), Button „Ändern/Melden" auf der Kunden-PM-Detailseite (nur published).
### 3.3 Anliegen-Auswahl ✅ (`web/melden-pm-anliegen.blade.php`)
- ✅ Vier Spuren mit Kosten-Labels (kostenlos / kostenlos / 35 € / 10 €) + Beschreibung; Auswahl erzeugt `LegalRequest` mit `type`, `verified_at`, `verification_method`, `requester_email` = Kontaktadresse; Eingangsbestätigung (`LegalRequestReceived`) + Operator-Mail. Bremse „1 offene Anfrage pro PM/Typ" beim Store durchgesetzt.
- ℹ️ **Interim bis Phase 4–7:** Alle vier Spuren landen zunächst als verifizierte Anfragen in der Admin-Queue (DSGVO/Depublizierung dort seit Phase 2 direkt ausführbar). Seiten-Text verspricht ehrlich: kostenpflichtige Anliegen erst nach Zahlungsaufforderung, keine Kosten ohne Bestätigung. Die Self-Service-Ausführung (Spur A sofort, C/D mit Checkout) ersetzt das schrittweise.
- ✅ Lang-Keys `static.pm_report.*` in `lang/de` + `lang/en`.
### 3.4 Tests ✅ (`PressReleaseReportFlowTest`, 10 Tests grün)
Link geht an hinterlegte, nie an Formular-Adresse (maskiert); unbekannte URL abgelehnt; toter Kanal → manueller Fallback; Rate-Limit pro PM; Consume nur einmal; Anliegen landet verifiziert in der Queue; Doppel-Anfrage geblockt; Session-Pflicht; Owner-Einstieg; Fremd-User 403.
---
## Phase 4 — Spur A: Kleine Korrektur (kostenlos, sofort) ✅ **umgesetzt (22.07.2026, panel-first)**
**Bestätigte Entscheidung:** Korrekturen gehen **sofort live** (kein Admin-Gate), landen im Verlauf und sind über den verschlüsselten Snapshot **rückgängig machbar**.
- ✅ `CorrectionService` (`app/Services/PressRelease/CorrectionService.php`):
- `classify()`: relative Änderungsdistanz je Feld (`title`, `subtitle`, `text`, `keywords`), Gate ≤ `legal.correction_max_distance_percent` (15 %, E5). **Hybrid-Metrik:** kurze Felder zeichenbasiert (ein Tippfehler im Titel bleibt „klein"), lange Texte wortbasiertes Levenshtein-DP (PHPs `levenshtein()` ist auf 255 Bytes begrenzt); Vergleich auf `strip_tags`-Basis — reine Markup-Änderungen zählen nicht. Der Nutzer wählt die Einstufung **nie selbst**.
- `apply()`: Snapshot (Rückgängig-Basis) → Felder schreiben (Text durch `PressReleaseHtmlSanitizer`) → `lockSync()` → `LegalRequest` (`correction`, `resolved`+`executed_at`, verifiziert) → Verlauf (`ActivityType::Correction`) → Sitemap-Cache → `reprocessAfterContentChange()` (Re-Score ohne Status-Wirkung, kein Freigabelauf).
- Bremse: `legal.corrections_per_week` = 2 (DB-basiert über ausgeführte Correction-Requests, auditierbar).
- `restoreSnapshot()`: spielt Vorher-Stand zurück (Felder + Kontakte; Bilder nur wenn Dateien noch existieren), Sperre bleibt, Verlaufs-Eintrag.
- ✅ **Panel-Abschnitt „Änderung & Korrektur"** (Kunden-PM-Detail, direkt über Depublikation): ein Einstieg, Formular vorbefüllt → Pflicht-Vorschau zeigt Einstufung + Distanz je Feld → **klein**: „Kostenlos speichern — sofort live"; **groß**: „Als kostenpflichtige Änderung einreichen (10 €)" — Interim bis zum Phase-7-Checkout: erzeugt verifizierte `Change`-Anfrage in der Admin-Queue (Doppel-Bremse), Zahlungsaufforderung folgt.
- ✅ **Admin-Rückgängig**: Button „Rückgängig machen (Snapshot)" in der Queue bei ausgeführten Anfragen mit Snapshot (gilt für Korrektur **und** Anonymisierung, bis zur Löschfrist) — Anfrage geht zurück auf „In Bearbeitung".
- ✅ Gemeinsame Cache-Invalidierung nach `App\Support\WebCaches::forgetSitemaps()` extrahiert (genutzt von Correction + Anonymization).
- ✅ Tests (11 neue, alle grün): Tippfehler klein / Umschreibung groß / Markup-only = keine Änderung, Sofort-Live mit Snapshot+Sperre+Verlauf, Zu-groß-Abwehr, Wochen-Bremse, Restore (Service + Admin-UI), kein Credit-Bezug, Panel-Happy-Paths, Vorschau-Pflicht.
- ℹ️ **Noch offen aus Phase 4:** dieselben Formulare im verifizierten Web-Bereich (Magic-Link-Besucher) — aktuell erzeugt Spur A dort noch eine Queue-Anfrage; die `CorrectionService`-Engine ist dafür wiederverwendbar. Nutzer-Bestätigungsmail nach Korrektur bewusst weggelassen (Owner sieht das Ergebnis sofort im Panel), für den Web-Pfad nachrüsten.
## Phase 5 — Spur B: DSGVO-Einreichung self-service → Admin-Gate ✅ **umgesetzt (23.07.2026)**
- ✅ **Strukturformular** im verifizierten Web-Bereich (`melden-pm-anliegen`, Alpine-Toggle bei Spur-Auswahl „DSGVO"): Schwärzbegriffe zeilenweise (Hinweis auf Varianten/Genitiv), Checkboxen „Pressekontakt-Daten entfernen" und „Bilder mit Personenbezug entfernen" (Bildauswahl trifft der Admin), Freitext-Begründung bleibt Pflicht.
- ✅ **Pflicht-Diff:** Erster POST rendert die Vorschau aus `AnonymizationService::preview()` (Treffer je Feld, geschwärzte Fassung als Klartext) — erst „Verbindlich einreichen" erzeugt die Anfrage. Begriffe ohne Treffer werden mit Schreibweisen-Hinweis abgewiesen; leeres Formular (keine Begriffe, keine Option) ebenso.
- ✅ **Einreichung ≠ Ausführung:** `LegalRequest` `type=dsgvo`, Status `open`, `payload.redaction` = {terms, detach_contacts, remove_images_requested, preview_total_hits}; an der PM ändert sich nichts, keine Credits. Bremse 1 offene Anfrage pro PM/Typ greift weiter.
- ✅ **Admin-Editor vorbefüllt:** `admin.legal-requests.show` übernimmt Begriffe + Kontakt-Option aus `payload.redaction` und zeigt einen Hinweis-Kasten (inkl. Bild-Entfernungswunsch). Admin prüft, ergänzt, führt aus oder depubliziert.
- ✅ **Mails:** Eingangsbestätigung existierte (Phase 3); neu die **Umsetzungsbestätigung** `LegalRequestExecuted` (transaktional, nicht abbestellbar) an die Antragsteller-Adresse nach `executeAnonymization` („anonymized") und `depublishRelease` („depublished").
- ✅ **Wording-Guardrail** eingehalten: überall „Schwärzung/Entfernung", inkl. Hinweis auf Suchmaschinen-/Drittarchiv-Kopien.
- ✅ Tests: Pflicht-Vorschau ohne DB-Write, Treffer-Validierung, bestätigte Einreichung mit strukturiertem Payload, Admin-Prefill, Umsetzungs-Mail.
## Phase 6 — Spur C: Depublizierung (35 €, Gast-Checkout, umkehrbar)
- **Wiederverwendung:** `StripeCheckoutService::forDepublication()` + `ProcessStripeWebhook::fulfillDepublication()` existieren. Neu ist der **Gast-Pfad**: Checkout ohne Login aus dem verifizierten Bereich.
- Variante (empfohlen): `SinglePurchase` ohne `user_id`-Zwang bzw. auf PM-Owner gebucht, `legal_request_id` als Referenz; Stripe-Session mit `customer_email` = verifizierte Kontaktadresse. Webhook-Fulfillment bleibt identisch (idempotent, row-locked).
- Nach Zahlung: `depublish()` → Tombstone (410 + noindex existiert) → `lockSync()` → `LegalRequest` `resolved`/`executed_at` → Bestätigungsmail mit Hinweis auf Suchindex-Trägheit (Cache-Aktualisierung bei Google dauert — Textbaustein).
- **Reversibilitätsfenster:** `depublished_at` + `config('legal.reversal_window_days')` (Standard **14 Tage**, E3): innerhalb der Frist Wiederherstellung über Admin (Snapshot nicht nötig — Status-Restore reicht, `published_at` bleibt). Hinweis im Bestätigungsschritt vor Zahlung („umkehrbar bis …").
- Preis: 35 € live via `config('billing.depublication_price_cents')` (existiert). Die systemische 25-Credit-Variante für eingeloggte Kunden bleibt unberührt — aber Test sichert ab, dass der **DSGVO-Pfad (Spur B) nie** in diesen Checkout mündet.
- Tests: Gast-Checkout-Fulfillment, Reversibilitätsfenster, Tombstone + Sitemap-Ausschluss.
## Phase 7 — Spur D: Änderung (10 €, KI-Vorprüfung) ✅ **Panel-Pfad umgesetzt (23.07.2026)**
**Flow-Entscheidung 23.07. (revidiert E2):** Die KI-Prüfung läuft **VOR der Zahlung** als einmaliger Zwischenschritt im Modal — nicht mehr nach Zahlung auf einem Pending-Payload. Rot → Einreichung wird direkt abgelehnt, es kommt gar nicht erst zur Zahlung (damit entfällt der „kein Verbrauch"-Fall aus E2). Gelb/Grün → Checkbox (§ 14 BGB/AGB, wie Depublikation) + direkter Stripe-Checkout; nach Zahlungseingang geht die Änderung **sofort live**. Der Vorgang ist **je PM nur einmal möglich** (auch nach Ablehnung kein zweiter Anlauf).
- ✅ **Obergrenze:** `legal.change_max_distance_percent` = **50 %** (env `LEGAL_CHANGE_MAX_DISTANCE_PERCENT`) — darüber ist auch die kostenpflichtige Änderung nicht buchbar (sonst verdeckte Selbst-Depublizierung); UI verweist auf Depublikation + Neuveröffentlichung. `CorrectionService::classify()` liefert dafür `exceeds_paid_limit`.
- ✅ **Preis:** `billing.change_price_cents` = 1000 (E1); 0/leer deaktiviert den Weg (wie Depublikation).
- ✅ **Vorprüfung:** `CorrectionService::precheckProposedContent()` klassifiziert eine NICHT gespeicherte PM-Kopie mit den neuen Werten über den `ClassificationManager` (KI-Provider, Fallback deterministisch/Blacklist). Ergebnis + Provider landen in `payload.ki_check` der Change-`LegalRequest` (rot → Status `Rejected`, sonst `Open`).
- ✅ **Checkout:** `CheckoutController::change()` (Route `me.checkout.change`, `checkout/aenderung/{id}`) verlangt die offene Change-Anfrage mit gelb/grünem `ki_check` als Nachweis; `SinglePurchaseType::Change` (`press_release_change`, Enum-Migration), Pending-Wiederverwendung wie Depublikation; Anfrage wird über `single_purchase_id` verknüpft. `StripeCheckoutService::forChange()` (adHoc-Muster).
- ✅ **Fulfillment:** `ProcessStripeWebhook::fulfillChange()` → `CorrectionService::applyPaid()`: Snapshot, Felder schreiben (Text durch Sanitizer), `lockSync()`, Verlauf, Re-Score; Anfrage `Resolved`/`executed_at`, Kauf `Consumed`. Reißt die Obergrenze doch (Payload-Manipulation), bleibt der Kauf unkonsumiert zur Admin-Klärung.
- ✅ **Panel-UI:** Modal `paid-change` mit Zwischenerklärung (einmalige Prüfung, rot/gelb-grün-Folgen), Loading-State, Ablehnungsansicht mit Gründen, Bestätigungs-Switch (`BookingConfirmation` Typ `press_release_change`) + Stripe-Button. Abgebrochene Zahlung: Modal springt direkt wieder zum Zahlungs-Schritt der bereits geprüften Fassung.
- ✅ **Tests:** Panel (grün → Zahlungs-Schritt, rot → Ablehnung + Einmal-Regel, Obergrenze), Service (`applyPaid`, `precheck`, `exceeds_paid_limit`), Checkout (Nachweis-Pflicht, Pending-Reuse, rot blockiert), Webhook (Ausführung + Replay-Idempotenz).
- ✅ **Bestätigungsmail:** `PressReleaseChangeApplied` an den Auftraggeber nach Spur-A-Korrektur und Spur-D-Ausführung (neuer `EmailNotificationType::PressReleaseCorrected`, abbestellbar, erscheint automatisch in den Mail-Präferenzen).
- **Entscheidung 23.07.:** Kein separater Gast-Pfad — hinterlegte Pressekontakte nutzen den bestehenden Magic-Link-Login ins Panel und bearbeiten dort. Der `/melden/pm`-Trichter bleibt für Dritte/DSGVO/Depublikations-Anliegen.
- **Kleinigkeit offen:** KiAudit-Eintrag für die Vorprüfung (Ergebnis liegt bisher nur in `payload.ki_check` der Anfrage).
## Phase 8 — Querschnitt-Abschluss
1. **Template-/Text-Stopp connektar (Sofortmaßnahme §7):** Alle Frontend-/Mail-Texte prüfen (`melden*`-Views, Mail-Templates, `lang/`-Strings): kein Verweis mehr auf „kostenfreier Weg über connektar"; Ersatz: Verweis auf die neue Meldeseite. *(Antwortvorlagen außerhalb des Systems sind organisatorisch zu ändern — nicht Teil des Codes, im Rollout-Termin erwähnen.)*
2. **Auslieferungs-Checkliste (§5) verifizieren:** Tombstone 410 ✅, `noindex` im Tombstone-View prüfen/ergänzen, Sitemap schließt Archived aus (Scope `visibleInArchive` prüfen) + Cache-Invalidierung nach Mutation, interne Suche (FULLTEXT auf `press_releases` — liest live, kein Rebuild nötig; Test ergänzen), **RSS/Feeds: existieren noch nicht, sind aber geplant** (E8) — beim künftigen Feed-Feature zwingend: Archived/anonymisierte PMs ausschließen und Feed-Cache bei Meldeverfahren-Mutationen invalidieren (Anforderung dort einplanen).
3. **Eskalationsstufen-Texte:** Mail-Vorlagen zweistufig — Angebots-/Eingangsmail ohne Aufsichtsbehörden-Hinweis, Ablehnungsmail (aus Admin-Queue `reject()`) mit Art.-12-Pflichthinweis. Textbausteine in `lang/`.
4. **Doku:** kurzer Abschnitt in `docs/konzept/` bzw. Update dieses Ordners; `CLAUDE.md` unberührt.
5. **Gesamt-Testlauf:** `vendor/bin/sail artisan test --compact` + Pint.
---
## 2. Getroffene Entscheidungen (bestätigt 22.07.2026)
| # | Entscheidung | Umsetzung im Plan |
|---|---|---|
| **E1** | Zwei getrennte Preise: **35 €** = komplette Depublizierung einer PM (Spur C), **10 €** = Änderung einer PM (Spur D) | Spur C: `billing.depublication_price_cents` (existiert); Spur D: neuer Config-Wert `billing.change_price_cents` = 1000 → Phase 7 |
| **E2** | ~~Bei KI-„rot" kein Verbrauch~~ **Revidiert 23.07.:** KI-Prüfung läuft **vor** der Zahlung (einmaliger Zwischenschritt im Modal). Rot → Ablehnung ohne Zahlung; Gelb/Grün → Zahlung, danach sofort live. Je PM **nur einmal möglich**. | Umgesetzt in Phase 7 (`precheckProposedContent`, Modal `paid-change`); der „kein Verbrauch"-Fall entfällt, weil vor Zahlung geprüft wird |
| **E3** | Reversibilitätsfenster als **Config-Variable, Standard 14 Tage** | `legal.reversal_window_days` = 14 → Phase 6 |
| **E4** | Snapshot-Löschfrist als **Config-Variable, Standard 30 Tage** | `legal.snapshot_retention_days` = 30; bestimmt `purge_after` → Phasen 1.1 / 2.3 |
| **E5** | Levenshtein-Schwellwert Spur A als Config-Variable; Start 15 %, am 22.07. abends auf **5 %** verschärft (+ Mini-Edit-Ausnahme ≤ 2 Zeichen) | `legal.correction_max_distance_percent` = 5, `legal.corrections_per_week` = 1 → Phase 4 |
| **E6** | Spur B bleibt **admin-gegated**; Automatisierung später bei guter Kalibrierung | Wie geplant → Phasen 2 / 5 |
| **E7** | Magic-Link-TTL/Session: **bestehende System-Mechanik wiederverwenden** | Kein Sondersystem — `MagicLinkGenerator`-Muster mit purpose-spezifischer TTL wie bei `login`/`press_release_access`; TTL des Report-Links als Config-Wert analog der vorhandenen Defaults → Phase 3 |
| **E8** | **RSS/Feeds werden in Zukunft entwickelt** | Anforderung fürs künftige Feed-Feature vorgemerkt: Archived/anonymisierte PMs ausschließen, Feed-Cache bei Meldeverfahren-Mutationen invalidieren → Phase 8 / Abschnitt 3 |
## 3. Nicht-Code-Punkte aus den Konzeptdokumenten (zur Nachverfolgung, außerhalb dieses Plans)
- Anwaltsagenda §10 (Unentgeltlichkeits-Grenze, UWG connektar, Realnamen-Änderung, Art. 19 Syndication, AI-Act-Co-Betreiber).
- Syndication-Vertrag mit connektar (Änderungs-/Löschpropagierung, Art.-19-Zuständigkeit einfordern).
- Externe Antwortvorlagen (Mail-Templates außerhalb des Systems) vom connektar-Verweis befreien.
- **RSS/Feed-Feature** (künftige Entwicklung, E8): muss Archived/anonymisierte PMs ausschließen und die Re-Sync-Sperre bzw. Mutationen des Meldeverfahrens respektieren (Cache-Invalidierung).
## 4. Testing & Qualität (durchgehend)
- Pest-Feature-Tests je Phase (Konventionen: `RefreshDatabase` global, `RolesAndPermissionsSeeder` im `beforeEach`, Factories `PressRelease::factory()->published()->forPortal(...)`; Vorbilder: `tests/Feature/Billing/DepublicationTest.php`, `tests/Feature/LegalRequestTest.php`, `tests/Feature/Admin/AdminPressReleaseActionsTest.php`).
- Kritische Invarianten als eigene Tests: (a) DSGVO-Pfad zieht **nie** Credits/Checkout, (b) jede Mutation setzt `sync_locked_at`, (c) Importer/API respektieren die Sperre, (d) ActivityLog enthält keine Klartext-Altwerte, (e) Magic-Link geht nie an Formular-Adresse.
- `vendor/bin/sail bin pint --dirty` vor jedem Abschluss.
## Änderungshistorie
| Datum | Änderung |
|---|---|
| 22.07.2026 | Erstfassung auf Basis Ist-Stand-Analyse der Codebasis (Meldeverfahren v0.1, Magic-Link-, Depublizierungs- und Billing-Infrastruktur) |
| 22.07.2026 (rev. 2) | Entscheidungen E1–E8 bestätigt und eingearbeitet: Spur D = 10 €, Depublizierung = 35 €; kein Verbrauch bei KI-rot; Reversibilitätsfenster 14 Tage / Snapshot-Frist 30 Tage / Levenshtein 15 % jeweils per Config; Magic-Link nutzt bestehende Mechanik; RSS/Feeds als künftiges Feature vorgemerkt |
| 22.07.2026 (rev. 3) | **Phase 1 umgesetzt:** 3 Migrationen, `lockSync()`/`isSyncLocked()`, Importer-Guard (auch `--force`), `LegalRequestType`-Cases + `LegalRequestVerificationMethod`, Snapshot-Modell mit `capture()`/Factory, `ActivityType::SyncLock`, Config E3–E5 in `config/legal.php`, 5 neue Pest-Tests grün. Erkenntnisse: Legacy-API hat keinen Update-Pfad (API-Guard entfällt vorerst); Spalten-Adds an `press_releases` wegen FULLTEXT-Index immer als ein kombiniertes ALTER (voller Tabellen-Copy, kein INSTANT) |
| 22.07.2026 (rev. 4) | **Phase 2 umgesetzt:** `AnonymizationService` (Unicode-Wortgrenzen-Replace, Pflicht-`preview()`, transaktionales `execute()` mit Snapshot/Kontakt-Detach/Bild-Löschung/Sperre/Sitemap-Invalidierung), Admin-Queue-Panel Anonymisieren/Depublizieren mit Pflicht-Vorschau, `ActivityType::Anonymization`, `snapshots:purge` + Scheduler, 9 neue Pest-Tests grün |
| 22.07.2026 (rev. 5) | Admin-Schwärzungs-Editor: kompletter PM-Inhalt als kopierbare Vorschau (vorher/nachher in einem Block), Ersetzungswert vorbefüllt. **Phase 3 umgesetzt:** Intake `melden/pm` (Kosten-Tabelle vorab, Honeypot, IP+PM-Rate-Limits), `createReportLink` + Enum-Migration `press_release_report`, Mailable, atomare Verifikation mit Session-Marker, Anliegen-Auswahl erzeugt verifizierte Queue-Anfragen (Bremse 1 offene Anfrage/PM/Typ), toter-Kanal-Fallback auf manuelles Formular, Owner-Einstieg aus dem Kunden-Panel, Lang-Keys de/en, 10 neue Pest-Tests grün |
| 22.07.2026 (rev. 6) | **Phase 4 umgesetzt (panel-first, Entscheidung: sofort live + rückgängig machbar):** `CorrectionService` mit Hybrid-Levenshtein-Gate (Zeichen kurz / Wörter lang, strip_tags-Basis), Panel-Abschnitt „Änderung & Korrektur" über der Depublikation (ein Einstieg, System stuft klein/groß ein; groß = Interim-Queue-Anfrage bis Phase-7-Checkout), Wochen-Bremse per Config, Snapshot-Restore im Admin (Korrektur + Anonymisierung), `ActivityType::Correction`, `WebCaches::forgetSitemaps()` extrahiert, 11 neue Pest-Tests grün |
| 22.07.2026 (rev. 7) | Konfig verschärft (5 % / 1 Korrektur pro Woche) + Mini-Edit-Ausnahme ≤ 2 Zeichen im Gate; Abschnitt „Stand & was noch fehlt" für den Wiedereinstieg ergänzt; Tagescommit |
| 23.07.2026 (rev. 8) | Phase 7 Panel-Pfad umgesetzt mit revidiertem E2 (KI-Vorprüfung VOR Zahlung, rot → Ablehnung ohne Zahlung, nur einmal je PM; Obergrenze 50 %). Außerdem: Vergleichsbasis entity-fest (Editor-Normalisierung ≠ Änderung), `toEditorHtml()` in Admin-Edit + Korrektur-Panel (Legacy-Plaintext-Umbrüche), Wochenlimit-Hinweis in Panel-Text und Bremsen-Meldung, Dev-DB vollständig migriert |
| 23.07.2026 (rev. 9) | Entscheidung: kein separater Web-Korrektur-Pfad — Magic-Link-Login ins Panel deckt Pressekontakte ab. Bestätigungsmail `PressReleaseChangeApplied` (Spur A sofort, Spur D nach Webhook) mit neuem abbestellbarem Typ `press_release_corrected`; Phase 7 damit vollständig |
| 23.07.2026 (rev. 10) | Phase 5 umgesetzt: strukturiertes DSGVO-Schwärzungsformular mit Pflicht-Diff im verifizierten Web-Bereich, Admin-Editor-Prefill aus `payload.redaction`, Umsetzungsbestätigung `LegalRequestExecuted` an Antragsteller |
| 23.07.2026 (rev. 11) | Gast-Checkout zurückgestellt (Magic-Link-Login deckt Pressekontakte ab). Re-Sync-Sperre + Sitemap-Invalidierung fest in `depublish()` verankert — schließt die Lücke im bezahlten Webhook-Pfad; Vollsuite 1418 Tests grün |
| 23.07.2026 (rev. 12) | Öffentliche „Rechtliches"-Seite an der PM zur zweistufigen Triage umgebaut: Anliegen-Karten, Magic-Link-Deep-Link mit vorbefüllter URL, Formular nur als Fallback (mit Identifizierungs-Hinweis bei DSGVO/Persönlichkeitsrechten), Meldung direkt; Store-Endpoint auf die drei öffentlichen Typen gehärtet |
| 23.07.2026 (rev. 13) | Pressekontakt-Lücke der Legacy-Bestände geschlossen: 30.422 von 30.433 veröffentlichten PMs haben keine PM↔Kontakt-Verknüpfung (im Altsystem hingen Kontakte an der FIRMA). Detailseite zeigt jetzt Fallback Firmen-Kontakt → Firmendaten; `pressContactEmail()` (Magic-Link-Kanal) nutzt dieselbe Kette PM-Kontakt → Firmen-Kontakt → Firmen-E-Mail |
| 23.07.2026 (rev. 14) | Entscheidung: Die Triage-Buttons führen NICHT in den Web-Melde-Trichter (`/melden/pm`), sondern zum passwortlosen **Portal-Anmeldelink** (`pressekonto /anmeldelink`) — dort werden die PMs der Kontaktadresse im Konto zusammengeführt und alle Selbstservice-Funktionen stehen bereit. Der Web-Trichter bleibt als eigenständiger Weg bestehen (u. a. verifizierte DSGVO-Struktureinreichung) |
| 23.07.2026 (rev. 15) | Panel: „Ändern/Melden"-Button leitet nicht mehr in den Web-Trichter, sondern öffnet ein Anliegen-Modal (4 Spuren). Korrektur/Änderung und Depublizierung springen zu den Panel-Abschnitten; DSGVO hat jetzt das strukturierte Schwärzungsformular (Pflicht-Vorschau, Einreichung ≠ Ausführung, Payload wie Web-Formular → Admin-Editor-Prefill) direkt als Modal im Panel |

View file

@ -1,183 +0,0 @@
# Lösch- und Änderungsanfragen (Meldeverfahren)
**Stand:** 22.07.2026 **Status:** Aktive Arbeitsgrundlage für Live-Portale (Interim) + Vorlage für Systemabbildung (Pfad E/F/G, Magic-Link) **Hinweis:** Interne Einordnung, keine Rechtsberatung. Die mit → _Anwaltsagenda_ markierten Punkte vor Serieneinsatz anwaltlich absichern.
---
## 1. Entscheidungsachse (Kernprinzip)
Nicht das betroffene **Datenfeld** entscheidet über kostenfrei vs. kostenpflichtig, sondern zwei Faktoren zusammen:
1. **Wer ist betroffen** — natürliche Person (DSGVO greift) oder juristische Person (kein Betroffenenrecht)?
2. **Was bleibt nach Anonymisierung übrig** — ein eigenständig zulässiges, werthaltiges Reststück, oder ein wertloses/widersprüchliches Artefakt?
„Personenbezogen" umfasst **nicht nur Kontaktfelder**, sondern auch Name, Autorenschaft und inhaltliche Aussagen einer identifizierbaren Person. Das Entfernen der Kontaktfelder allein erfüllt einen Art.-17-Anspruch nicht, wenn der personenbezogene Kern (z. B. der Realname) bestehen bleibt.
---
## 2. Free/Paid-Matrix
|Konstellation|Reststück nach Anonymisierung|Vorgehen|Kosten|
|---|---|---|---|
|Natürliche Person, PM über/von ihr|wertlos, nicht eigenständig|**Volldepublizierung**|**kostenfrei**|
|Natürliche Person, nur Kontaktdaten (z. B. Ansprechpartner in Firmen-PM)|tragfähige Firmen-PM bleibt|Kontaktdaten raus, Rest bleibt|**kostenfrei**|
|Natürliche Person, Realname-Änderung/-Entfernung|dieselbe PM ohne Realnamen|Änderung umsetzen|**kostenfrei** (Betroffenenrecht-Nähe → _Anwaltsagenda_)|
|Juristische Person, Register-/Firmendaten (§ 9 HGB)|eigenständig zulässig|Kontaktdaten der nat. Person kostenfrei; Volldepublizierung optional|**optional kostenpflichtig**|
**Preis-Überlebensbedingung (kumulativ):** Eine Gebühr ist nur dort tragfähig, wo (a) **kein** Personenbezug einer natürlichen Person besteht **und** (b) ein **tatsächlich funktionierender** kostenfreier Alternativweg existiert. Fehlt eine der beiden Bedingungen, ist die Gebühr angreifbar (Art. 12 Abs. 5 DSGVO bzw. UWG).
> **Revisit-Bedingung:** Der connektar-Weg steht als Gratisweg nicht zur Verfügung (§7). Bedingung (b) wird stattdessen durch den **Magic-Link-Self-Service** (§11) erfüllt: kleine Korrekturen und DSGVO-Anliegen laufen dort kostenfrei. Eine Gebühr (Spur 3) ist damit tragfähig für **größere, diskretionäre** Änderungen und die freiwillige Depublizierung zulässiger Inhalte — nicht für Personenbezug einer natürlichen Person.
---
## 3. Vier Referenzfälle
|Fall|Betroffener|Datenart|Besonderheit|Entscheidung|
|---|---|---|---|---|
|**HWP** (Kaymaz)|juristische Person (GmbH)|Register-/Firmendaten, § 9 HGB|Streumail, ursprünglich an viaductus gerichtet; Frist-Platzhalter nicht ausgefüllt|Persönliche Kontaktdaten kostenfrei raus; Firmeneintrag bleibt; Volldepublizierung **optional 35 €**|
|**Huraux**|natürliche Person|private Adresse, Telefon, Name|Absenderadresse ≠ hinterlegte Adresse; hinterlegter Kanal (Domain) **tot**|Vollstattgabe, kostenfrei, **ohne** formale Verifikation → Aktenvermerk + umkehrbare Depublizierung|
|**Gleue**|natürliche Person|private Kontaktdaten|„Dringend"-Rhetorik (keine Frist); Kanal intakt|Vollstattgabe, kostenfrei, **Magic-Link** an hinterlegte Adresse `mariana@gleue.com`|
|**Rambo**|natürliche Person (Spokesperson in Firmen-PM)|Realname „Matvey Teplitsky" → „Alex"|**connektar verweigert Fremdportal-Änderung** (Holthaus); Legitimation via Firmen-Mail liegt vor|Änderung **kostenfrei** direkt umsetzen; kein connektar-Verweis mehr|
**Achse, die alle vier aufspannen:** _juristisch/natürlich_ × _Kanal funktioniert/tot_ × _Löschung/Änderung_. Deckt die Hauptvarianten des späteren Pfad-E/F/G-Splits ab.
---
## 4. Interims-Triage-Prozess (4 Stufen)
1. **Verifizieren** — Stammt die Anfrage nachweislich vom Betroffenen/Berechtigten? Abgleich Absender ↔ hinterlegte Adresse. Verifikation nur bei **begründeten Zweifeln** (Art. 12 Abs. 6), nie pauschal.
2. **Klassifizieren** — Natürliche oder juristische Person? Personenbezogener Kern oder reine Firmendaten? Löschung oder Änderung?
3. **Entscheiden** nach Free/Paid-Matrix (§2). Bei connektar-Herkunft: parallel Löschung/Änderung an der Quelle anstoßen und Re-Sync sperren.
4. **Antworten** fristwahrend (innerhalb eines Monats ab Eingang).
**Eskalationsstufe-2-Regel (Aufsichtsbehörden-Hinweis):** Der Hinweis „Ihnen steht der Weg zur Aufsichtsbehörde offen" gehört **nicht** in ein Schreiben, das ein kostenfreies Angebot macht und den Ball beim Empfänger lässt — dort wirkt er wie eine Einladung zur Beschwerde. Er gehört erst in den **zweiten** Brief, wenn ein Begehren tatsächlich (teilweise) **abgelehnt** wird; dann ist er Art.-12-Pflichthygiene, keine Provokation. Neutraler Abschluss im Angebotsfall: „Für Rückfragen stehen wir gerne zur Verfügung."
---
## 5. Technische Checkliste bei Umsetzung
Bei DSGVO-Löschung einer natürlichen Person reicht ein bloßer Tombstone (noindex, URL erhalten) **nicht** — die personenbezogenen Daten müssen tatsächlich aus der Auslieferung verschwinden. Umzusetzen sind:
- [ ] Depublizierung / Entfernung der Daten aus dem sichtbaren Inhalt
- [ ] Zugehörige **Bilder und Anhänge**
- [ ] **Interne Suchtreffer**
- [ ] **Feed-/RSS-Ausgaben**
- [ ] **Sitemap-Einträge**
- [ ] `noindex` gesetzt + zur Entfernung aus dem Suchindex eingereicht (Cache-Aktualisierung dauert; kein unmittelbarer Einfluss → im Schreiben erwähnen)
- [ ] **Re-Sync-Sperre**: Eintrag dauerhaft gegen erneutes Einspielen über die Schnittstelle sperren
- [ ] **Art. 19 DSGVO**: connektar über Löschverlangen informieren, Löschung an der Quelle anstoßen; Betroffenen für netzwerkweite Entfernung an connektar verweisen
- [ ] **Kein Credit-Abzug** im DSGVO-Pfad (Art. 12 Abs. 5) — technisch prüfen, dass der DSGVO-Pfad **nicht** die reguläre 25-Credit-Depublizierungslogik zieht → _Anwaltsagenda-nah / technisch kritisch_
- [ ] Bei Handeln ohne formale Verifikation (toter Kanal): **Aktenvermerk** (Art. 5 Abs. 2) + **umkehrbare** Depublizierung + Hinweis auf Wiederherstellbarkeit
---
## 6. Identitätsprüfung (Art. 12 Abs. 6)
- **Grundsatz:** Nachweis nur bei begründeten Zweifeln (z. B. Absenderadresse ≠ hinterlegte Adresse). Löschverlangen ist ein möglicher **Angriffsvektor** — wer die PM liest, kennt Name/Anschrift/Telefon.
- **Regelweg (neues System):** **Magic-Link** an die in der Veröffentlichung hinterlegte Pressekontaktadresse. Wer den Link öffnet, ist strukturell verifiziert — kein Ermessen, kein Ausweis.
- **Nie verlangen:** Ausweiskopie o. ä. (Datenminimierung; holt zusätzliche sensible Daten ins Haus).
- **Fallback „Kanal tot":** Risiko-Asymmetrie kippt Richtung Umsetzen, sobald kein Missbrauchsmotiv erkennbar ist (eigene Privatdaten einer natürlichen Person). Dann: Aktenvermerk statt Verifikation, umkehrbare Depublizierung. → **Systemlücke dokumentieren:** Der Ablauf braucht einen definierten Fallback „hinterlegter Kontaktkanal nicht mehr erreichbar", sonst Lücke für genau diese Konstellation.
- **Rhetorik ignorieren:** „Dringend/Notfall" ändert die Monatsfrist nicht und darf die Prüfung nicht überspringen.
---
## 7. Strukturproblem connektar (KRITISCH)
**Fund (Fall Rambo):** connektar (Holthaus) nimmt nachträgliche Änderungen **ausschließlich auf eigenen Portalen** vor und überträgt sie **nicht** auf Fremdportale. Das widerspricht direkt der bisherigen Standardaussage „connektar aktualisiert das kostenfrei bei uns über die API".
**Präzisierung (bestätigt 22.07.2026):** Die technische **Möglichkeit** einer Propagierung _besteht_ — connektar **nutzt sie nur nicht** bzw. verweigert sie für Fremdportale. Es ist damit **kein technisches Naturgesetz, sondern ein Verhandlungspunkt** für den Syndication-Vertrag. Praktische Konsequenz für den Betrieb bleibt: nicht darauf verlassen.
**Konsequenzen:**
- Der beworbene kostenfreie connektar-Weg **steht für Fremdportal-Änderungen faktisch nicht zur Verfügung**. Kunden landen in der Sackgasse und dann beim Bezahlservice → Trichter-Optik, **UWG-Risiko** (irreführende Angabe) + Anti-Zombie-Widerspruch.
- Betrifft **alle** bisherigen Vorlagen (Änderung _und_ Löschung/Depublizierung).
**Architektur-Entscheidung:** connektar wird als **write-once** behandelt. Das Portal **übernimmt den gesamten Post-Publication-Zustand selbst** (Änderung, Löschung, Depublizierung) — realisiert über den Self-Service-Ausbau (§11) plus Admin. Die **Re-Sync-Sperre** ist damit nicht optional, sondern strukturell zwingend, sonst überschreiben periodische Re-Imports jede Mutation.
**Sofortmaßnahmen:**
1. **Template-Stopp:** „Kostenfreier Weg über connektar" **aus allen Antwortvorlagen entfernen** (auch aus HWP-/Depublizierungs-Texten). Ersatz: Self-Service-Portal (§11) bzw. direkte kostenfreie Umsetzung.
2. **Syndication-Vertrag priorisieren:** Muss Änderungs-/Löschpropagierung und **Art.-19-Zuständigkeit** regeln — und, da die Möglichkeit besteht, deren **Nutzung durch connektar einfordern**. Dieser Fall ist der konkrete Beleg, warum der Vertrag nicht warten kann.
---
## 8. Argumentationsreserve — Vier Säulen (nur juristische Person / Registerdaten)
Für die begründete **Ablehnung** einer Komplettlöschung von Firmen-/Registerdaten (Fall HWP-Typ). Kurzform für den Brief: „Register-/Firmendaten sind nach § 9 HGB öffentlich und betreffen die GmbH als juristische Person, der die genannten Datenschutzrechte nicht zustehen; eine Löschpflicht besteht daher nicht."
1. **Kein Schutzsubjekt:** DSGVO schützt nur natürliche Personen; GmbH-Daten fallen nicht unter Art. 17/21.
2. **Gesetzlich öffentlich:** Handelsregisterdaten nach § 9 HGB für jedermann einsehbar; kein Recht auf „Herauslöschen" rechtmäßiger Registertatsachen aus Drittberichterstattung.
3. **Rechtsgrundlage + Ausnahme:** Art. 6 Abs. 1 lit. f (berechtigtes Interesse), nicht Einwilligung; Art. 17 Abs. 3 lit. a nimmt Meinungs-/Informationsfreiheit vom Löschanspruch aus; bei echter PM zusätzlich Medienprivileg (Art. 85 + LPG).
4. **Widerspruch ≠ automatische Löschung:** Art. 21 löst Abwägung aus; überwiegendes öffentliches Informationsinteresse an Wirtschaftsvorgängen trägt die Weiterverarbeitung. „Ohne meine Zustimmung" läuft leer, weil Einwilligung nie Rechtsgrundlage war.
> **Grenze der Reserve:** Trägt nur Firmen-/Sachdaten. Direkte **persönliche Kontaktdaten** einer natürlichen Person werden dennoch kostenfrei entfernt. Bei **Presskit-/Profilseiten** (aus Aggregat-/Registerdaten erzeugt) ist Säule 3 (Medienprivileg) schwach — Argumentation stützt sich dann auf Säulen 1, 2, 4.
---
## 9. Geschäftsentscheidungen (nicht auf Anwaltsagenda)
- **Tombstone statt Hartlöschung** als Standard für zulässige Depublizierung (noindex, gegen Re-Sync gesperrt) — konsistent mit Systemarchitektur; keine „aus der Datenbank entfernt"-Formulierung mehr.
- **Freiwilliger Depublizierungsservice** (35 €/39 € live; systemisch 25 Credits) nur dort anbieten, wo die Preis-Überlebensbedingung (§2) erfüllt ist.
- **Anti-Zombie-Filter:** Bei natürlicher Person, kaputtem Gratisweg oder Kleinstaufwand nicht auf einer 35–39-€-Gebühr bestehen; Reputations-/UWG-Risiko übersteigt den Ertrag.
- **Keine Löschung/Depublizierung als Zahlungshebel.**
---
## 10. Offene Anwaltsfragen (Agenda-Ergänzungen)
1. **Grenzziehung Unentgeltlichkeit:** Wo verläuft die Grenze zwischen unentgeltlicher Erfüllung nach Art. 17 und einem entgeltlichen **freiwilligen** Depublizierungsservice? Ist die Preisgestaltung tragfähig, wenn das Reststück nach Anonymisierung eigenständig zulässig wäre?
2. **UWG-Risiko connektar-Weg:** Bewertung des als kostenfrei beworbenen connektar-Wegs, der auf Fremdportalen faktisch nicht funktioniert (irreführende Angabe / Trichter in Bezahlservice?).
3. **Namensänderung natürliche Person:** Ab wann ist die Entfernung/Änderung eines Realnamens ein **unentgeltliches** Betroffenenrecht (Art. 16/17) statt eines kostenpflichtigen Edits?
4. **Art. 19 in der Syndication:** Wer trägt die Benachrichtigungspflicht gegenüber weiteren Empfängern — Portal oder Verteiler? Regelung im Syndication-Vertrag.
5. **(bereits offen)** Co-Betreiber-Status nach Art. 50(4) AI Act — separate Agenda.
---
## 11. Self-Service-Ausbau Magic-Link (Ziel-Architektur)
**Ziel:** Anfragevolumen reduzieren, indem Änderungen/Löschungen self-service über den Magic-Link laufen — Portal übernimmt den Post-Publication-Zustand ohnehin selbst (§7).
### Drei Spuren
|Spur|Umfang|Kosten|Ablauf|
|---|---|---|---|
|**1 — Kleine Korrektur**|Tippfehler, Kontaktdaten, Name einer natürlichen Person|kostenlos|self-service, sofort; rate-limited, Levenshtein-gated (= Pfad A/B)|
|**2 — DSGVO-Löschung/Anonymisierung**|Entfernung personenbezogener Daten einer natürlichen Person|**kostenlos, nie Credits** (Art. 12 Abs. 5)|self-service-Einreichung → **Admin-Review** vor Ausführung (= Pfad E/F)|
|**3 — Größere Änderung / freiwillige Depublizierung**|Fließtext umschreiben/ergänzen, Bildtausch, Depublizierung zulässiger (Firmen-)Einträge|kostenpflichtig|self-service Checkout, Credits/Stripe (= Pfad G)|
### Abgrenzungsgrund (kritisch)
**Die Free/Paid-Einstufung darf nie der Antragsteller selbst wählen.** Sonst wählt ein HWP-Typ „DSGVO-Löschung" und erhält die kostenlose Komplett-Depublizierung eines rechtlich zulässigen Firmeneintrags, der eigentlich Spur 3 (bezahlt) wäre → der Bezahlkanal leckt aus.
- Spur 1: Auto-Run vertretbar (geringer Missbrauchswert).
- Spur 2: **admin-gegated** — dort wird der Matrix-Test (§1) menschlich angewandt (natürliche vs. juristische Person; Reststück eigenständig zulässig?). Kein Komfort-, sondern Abgrenzungsgrund.
- Spur 3: kein Gate nötig (wer zahlt, hat kein Trick-Motiv).
### Verifikation
Der Magic-Link geht **immer an die hinterlegte Pressekontaktadresse**, nie an die im Formular eingegebene → selbst bei Fremdantrag ist nur der echte Kontakt handlungsfähig (strukturelle Verifikation, kein Ermessen).
- **Adresse hinterlegt** (Regelfall): Link raus → Self-Service-Portal mit den drei Spuren.
- **Keine Adresse hinterlegt / tot** (Huraux-Fall): kein vertrauenswürdiger Kanal → **vorgeschaltete manuelle Mail-Verifikation** + Admin, umkehrbare Depublizierung, Aktenvermerk (§6-Fallback).
### Webseiten-Intake (Meldeverfahren-Seite)
Vom Kontaktformular zum **Deflektions-Trichter:** URL einfügen → Anliegen wählen → System schlägt hinterlegten Kontakt nach → Magic-Link an diesen; Seite erklärt vorab transparent kostenlos vs. kostenpflichtig (Anti-Zombie).
- **Pflicht:** Formular **rate-limiten** (pro URL/IP), sonst Werkzeug zum Fluten fremder Kontakt-Postfächer mit Magic-Link-Mails.
- Jede Mutation setzt automatisch die **Re-Sync-Sperre** (§5, §7).
### Zu bestätigen (offene Business-Entscheidungen)
1. **Spur 2 admin-gegated?** — Empfehlung: ja (sonst Leck oben). Alternative Auto-Run nur für „eindeutige" Fälle scheitert daran, „eindeutig" maschinell zu bestimmen.
2. **Exakte Grenze Spur 1 ↔ 3.** — Vorschlag: Feld-/Textkorrektur ohne neuen Inhalt = klein/kostenlos; neuer/umgeschriebener Fließtext, Bildtausch, Depublizierung = groß/kostenpflichtig.
3. **Bestätigungs-/Reversibilitätsfenster** für self-service Depublizierung — Empfehlung: ja (Irreversibilität bei einem Klick).
---
## Änderungshistorie
|Datum|Änderung|
|---|---|
|22.07.2026|Erstfassung; vier Referenzfälle (HWP, Huraux, Gleue, Rambo), connektar-Strukturproblem, Free/Paid-Matrix, Triage-Prozess, Anwaltsagenda-Ergänzungen|
|22.07.2026 (rev. 2)|connektar präzisiert (Möglichkeit besteht, ungenutzt → write-once, Portal übernimmt Post-Publication-Zustand); §11 Self-Service-Ausbau Magic-Link (drei Spuren, Abgrenzungsgrund, Verifikation, Intake) ergänzt; §2 Revisit-Bedingung aktualisiert|

View file

@ -1,153 +0,0 @@
# Flow: Pressemitteilung melden
**Stand:** 22.07.2026 **Zweck:** Selbstbedienungs-Flow für Änderungs-, Lösch- und DSGVO-Anliegen zu einer veröffentlichten Pressemitteilung. Reduziert manuelle Anfragen; Portal übernimmt den Post-Publication-Zustand selbst (connektar = write-once). **Hinweis:** Interne Konzeptbeschreibung, keine Rechtsberatung.
---
## Übersicht der vier Anliegen
| Anliegen | Ergebnis | Kosten | Freigabe |
| ------------------------ | ------------------------------------------ | -------------------------- | --------------------------------- |
| **Kleine Korrektur** | Feld-/Textkorrektur ohne neuen Inhalt | kostenlos | automatisch (rate-limited) |
| **DSGVO-Anonymisierung** | Personendaten geschwärzt, PM bleibt online | **kostenlos, nie Credits** | Admin (später ggf. automatisiert) |
| **Depublizierung** | PM offline (Tombstone, umkehrbar) | 35 € | Zahlung |
| **Änderung** | Inhaltsedit + neuer Freigabelauf | 10 € | KI-Prüfung |
**Reframe DSGVO:** DSGVO führt zu **Anonymisierung**, nicht zu Depublizierung. Das ist das mildere, gesetzlich vorgesehene Mittel; der redaktionelle Kern bleibt erhalten (Art. 85). Depublizierung ist der separate, kostenpflichtige Fall.
---
## Diagramm
```mermaid
flowchart TD
Start["PM melden (Website oder Dashboard)"]
QLogin{"Eingeloggter Creator mit zugeordneter PM?"}
PickOwn["PM aus eigenen Beitraegen waehlen"]
URL["PM per URL identifizieren"]
QMail{"Kontaktadresse hinterlegt?"}
ML["Magic-Link an hinterlegte Adresse"]
Click["Link bestaetigt = verifiziert"]
Manual["Hinweis: manuelle Verifizierung, Nachweis an Support"]
Admin0["Admin prueft Berechtigung"]
Anliegen{"Anliegen waehlen"}
KA["Feld-/Textkorrektur ohne neuen Inhalt"]
KDiff["Vorschau / Diff"]
KSave["Sofort speichern, rate-limited"]
DA["Zu schwaerzende Felder + Namensvarianten eingeben"]
DDiff["Pflicht-Vorschau / Diff, Wortgrenzen"]
DAdmin{"Admin-Freigabe: anonymisieren oder depublizieren?"}
DExec["Replace + Assets entfernen + Re-Index"]
PCheck["Checkout 35 EUR"]
PPay["Zahlung"]
PTomb["Tombstone: offline + noindex, umkehrbar"]
CA["Neuen Text eingeben"]
CPrice["Preis beziffern + Checkout"]
CPay["Zahlung"]
CAI{"Neuer Freigabelauf: KI-Pruefung"}
CLive["Live"]
CRej["Abgelehnt, kein Verbrauch"]
Sync["Re-Sync-Sperre setzen"]
Done["Nutzer-Benachrichtigung + Audit-Log ohne Klartext-Altwerte"]
Start --> QLogin
QLogin -- Ja --> PickOwn --> Anliegen
QLogin -- Nein --> URL --> QMail
QMail -- Ja --> ML --> Click --> Anliegen
QMail -- "Nein / tot" --> Manual --> Admin0 --> Anliegen
Anliegen -- "Kleine Korrektur (frei)" --> KA
Anliegen -- "DSGVO (frei)" --> DA
Anliegen -- "Depublizierung (35 EUR)" --> PCheck
Anliegen -- "Aenderung (kostenpflichtig)" --> CA
KA --> KDiff --> KSave --> Sync
DA --> DDiff --> DAdmin
DAdmin -- Anonymisieren --> DExec --> Sync
DAdmin -- "Depublizieren noetig" --> PTomb
PCheck --> PPay --> PTomb --> Sync
CA --> CPrice --> CPay --> CAI
CAI -- "gruen / gelb" --> CLive --> Sync
CAI -- rot --> CRej --> Done
Sync --> Done
```
---
## Schritt-für-Schritt
### 1. Einstieg & Identität
Einstieg über die Website („Pressemitteilung melden") oder das eingeloggte Dashboard.
- **Eingeloggter Creator** mit zugeordneter PM → wählt die PM aus seinen Beiträgen, keine weitere Verifikation nötig.
- **Nicht eingeloggt** → PM per URL identifizieren → Verifikation über Magic-Link.
### 2. Verifikation (wenn nicht eingeloggt)
Der Magic-Link geht **immer an die hinterlegte Firmen-/Pressekontaktadresse**, nie an eine im Formular eingegebene Adresse. Damit ist selbst bei einem Fremdantrag nur der echte Kontakt handlungsfähig — die Verifikation ist strukturell.
- **Adresse hinterlegt** → Link raus, Klick verifiziert, weiter zur Anliegen-Auswahl.
- **Keine Adresse hinterlegt / tot** → Hinweis mit Alternativweg: manuelle Verifizierung (Berechtigungsnachweis an den Support), Admin prüft, Umsetzung umkehrbar + Aktenvermerk (Art. 5 Abs. 2).
### 3. Anliegen wählen
Vier Optionen (siehe Übersicht). Die Free/Paid-Einstufung wird **nicht** vom Nutzer frei gewählt: DSGVO läuft über Admin-Freigabe, damit „DSGVO" nicht als Gratis-Weg für eine eigentlich kostenpflichtige Depublizierung zulässiger Firmeneinträge missbraucht wird.
### Spur A — Kleine Korrektur (kostenlos)
Feld- oder Textkorrektur **ohne neuen Inhalt** (Tippfehler, Kontaktdaten, Name einer natürlichen Person) → Pflicht-Vorschau/Diff → sofortiges Speichern. Rate-limited und auf geringe Änderungsdistanz begrenzt (Levenshtein), damit die Spur nicht zum kostenlosen Voll-Editor wird.
### Spur B — DSGVO-Anonymisierung (kostenlos, nie Credits)
Der Kern des Konzepts. Ablauf:
1. PM auswählen.
2. **Erfassung:** Strukturierte Kontaktfelder werden automatisch zum Leeren angeboten; zusätzlich Freitextfelder, in die der Nutzer die zu schwärzenden Werte einträgt — **inklusive Varianten** (z. B. „Matvey Teplitsky", „Herr Teplitsky", „Teplitskys").
3. **Pflicht-Vorschau/Diff:** System zeigt markiert, was ersetzt wird. Ersetzung über **Wortgrenzen** (`\b`), damit weder Teiltreffer stehenbleiben noch Wörter wie „maximal" bei Name „Max" zerlegt werden.
4. **Admin-Freigabe:** Admin bestätigt und entscheidet auch **anonymisieren vs. depublizieren** — bei Vanity-/Personen-PMs bleibt nach Schwärzung nur ein Rumpf, dann ist Depublizierung das richtige Mittel.
5. **Ausführung:** Text-Replace + separate Entfernung/Ersetzung betroffener **Bilder und Anhänge** (Klarname in Pressebild/PDF!) + Re-Index/Cache-Refresh.
6. **Kein KI-Red-Flag-Gate** — Anonymisierung ist Compliance-Pflicht, darf nicht wie ein normaler Freigabelauf scheitern.
Umsetzung des Ersetzungswerts: neutraler Platzhalter oder Rolle („der Geschäftsführer"), kein Leerstring (zerstört Zitate grammatikalisch).
### Spur C — Depublizierung (35 €)
Checkout → Zahlung → Tombstone (offline + noindex, **umkehrbar** mit Reversibilitätsfenster). Auch Zielpunkt aus Spur B, wenn der Admin depublizieren statt anonymisieren wählt.
### Spur D — Änderung (kostenpflichtig)
Neuen Text eingeben → Preis beziffern + Checkout → Zahlung → **neuer Freigabelauf über die KI-Prüfung**: grün/gelb → live; rot → abgelehnt, kein Verbrauch. Aufwand entspricht faktisch einer Depublizierung (Eingriff + Freigabe), 35 € als plausibler Anker; reine Geschäftsentscheidung.
### 4. Gemeinsame Abschlussschritte
- **Re-Sync-Sperre setzen** (bei jeder Mutation zwingend) — sonst holt connektar beim nächsten Import den alten Stand / Klarnamen zurück.
- **Nutzer-Benachrichtigung + Audit-Log.** Der Log protokolliert „Feld X anonymisiert durch User Y am Z", **nicht** den Alt-Wert im Klartext (sonst leben die Daten im Log weiter). Alt-Wert nur verschlüsselt + mit Löschfrist, falls für Reversibilität nötig.
---
## Querschnitts-Guardrails
- **DSGVO nie Credits** (Art. 12 Abs. 5).
- **Scope-Grenze Spur B:** Das DSGVO-Formular fasst nur **personenbezogene Felder** einer natürlichen Person an. Realname → Rolle/Platzhalter = frei; Realname → anderer echter Name oder Textumbau = Spur D (kostenpflichtig).
- **Begriff:** Was gebaut wird, ist **De-Identifikation / Schwärzung direkter Identifier**, keine „Anonymisierung" im DSGVO-Vollsinn — in der Kundenkommunikation nicht überversprechen.
- **Formular-Rate-Limit** (pro URL/IP), damit der Magic-Link-Versand nicht zum Werkzeug wird, fremde Kontakt-Postfächer zu fluten.
- **Re-Sync-Sperre** ist strukturell, nicht optional (connektar = write-once).
---
## Zu bestätigen
1. Spur B bleibt zunächst **Admin-Freigabe**, Automatisierung später bei guter Kalibrierung.
2. Exakte Grenze Spur A ↔ D (Vorschlag: Feld-/Textkorrektur ohne neuen Inhalt = klein; neuer/umgeschriebener Fließtext, Bildtausch, Depublizierung = groß).
3. Preis Spur D (Vorschlag: 35 € analog Depublizierung).
4. Reversibilitätsfenster-Dauer für Depublizierung/Anonymisierung.

View file

@ -1,219 +0,0 @@
> **Stand der Doku**: 21.05.2026 — diese Liste beschreibt den Zielzustand
> der oeffentlichen Strecke. Welcher Punkt bereits umgesetzt ist, ist
> jeweils mit einer kurzen IST-Notiz markiert.
> **Update 17.06.2026 — Bestands-URL-Mapping (Launch-Vorbereitung).** Die
> komplette URL-Struktur der Altseite (siehe `docs/frontend/Struktur Frontend.md`)
> ist jetzt per 301 auf die neue Struktur abgebildet: englische Alt-Slugs
> (`login`, `contact`, `imprint`, `terms`, `privacypolicy`, `pricelist`,
> `service`) → neue deutsche Pfade bzw. Hub, `/presse/*`-Ratgeber → `/faq`,
> `presskit/{id}/{slug}` → `/newsroom/{slug}` (Feinmapping über
> `companies.legacy_id`, Fallback `/newsrooms`), entfallene Sprachvarianten
> `de-ch`/`de-at` → `/de`. Region-Übersichten (`/region/{slug}`) sind bewusst
> ein späterer Schritt. Implementierung in `routes/web.php`; Test:
> `tests/Feature/Web/LegacyRedirectTest.php`.
> **Update 17.06.2026 — Newsrooms.** Newsroom-Markenseiten pro Unternehmen
> (`/{edition}/newsroom/{slug}`) und das Newsrooms-Verzeichnis
> (`/{edition}/newsrooms`) sind im Editorial-Design mit echten Daten
> umgesetzt (siehe Punkt 4). Neue server-seitige Pagination-Komponente
> `x-web.pagination`. Test: `tests/Feature/Web/NewsroomPageTest.php`.
> **Update 16.06.2026 — Editorial-Relaunch & Ausgaben-Routing.**
> Die oeffentliche Strecke wurde auf ein gemeinsames Editorial-Design
> (Komponenten `x-web.site-header` / `x-web.site-footer`, Design-Tokens in
> `resources/css/web/shared-styles.css`) umgestellt und auf ein
> Ausgaben-Praefix `/{edition}/…` (Sprache `de` | `en`) gehoben.
>
> - **Routing**: Neue Middleware `app/Http/Middleware/SetEdition.php` liest
> die Ausgabe aus dem ersten URL-Segment, setzt Locale + `URL::defaults`.
> `routes/web.php` gruppiert alle oeffentlichen Routen unter `/{edition}`.
> `/` leitet auf die Default-Ausgabe (`/de`) um; Legacy-`.html`-URLs werden
> per 301 auf die neuen Pfade gemappt. Baseline-Default in
> `AppServiceProvider` fuer Route-Generierung ausserhalb von HTTP-Requests.
> - **Neue URL-Schemata**: Detailseite `/{edition}/press-release/{slug}`
> (Route `release.detail`), Kategorie `/{edition}/category/{slug}`
> (Route `kategorie`).
> - **Ausgabe = Sprache**: DE/EN-Umschalter im Header (Region-/CH-/AT-Auswahl
> entfernt). `EditorialClock` (`app/Support/EditorialClock.php`) und die
> Livewire-Komponenten sind sprachdynamisch.
> - **Rubriken-Navigation** statt Uebersichtsseite: Helper
> `app/Support/CategoryNavigation.php` liefert die Top-Rubriken der Ausgabe;
> Header/Footer verlinken direkt auf die Kategorieseiten. Die alte
> `web/kategorien.blade.php` + Route wurden entfernt (Legacy-301).
> - **Tests**: `tests/Feature/Web/` deckt Edition-Routing, Kategorie-Seite,
> Kategorie-Navigation, Detailseite, Veroeffentlichen, Suche und den
> EditorialClock ab.
Das sind die Seiten, die eigene URLs brauchen, weil sie verlinkbar sein müssen, SEO-Wert haben oder direkt von extern angesteuert werden.
#### Inhalts-Seiten (Lese-Erfahrung)
**1. Pressemitteilungs-Detailseite** – `/p/[slug]` oder `/pressemitteilung/[id]` Die wichtigste Seite überhaupt. Jede einzelne PM bekommt eine eigene Seite. Hier landen 90% des Traffics aus Google, Newsletter und Social Shares.
_IST 21.05.2026_: umgesetzt als `resources/views/web/release-detail.blade.php` (Route `release.detail`, URL `/release/{slug}`). Das URL-Schema weicht vom Plan ab, ist aber konsistent über alle Themen.
_IST 16.06.2026_: im Editorial-Design neu aufgebaut, echte Daten (Firma, Kontakte, Bilder, verwandte Meldungen). Neue URL `/{edition}/press-release/{slug}`; Legacy-URLs per 301. Test: `tests/Feature/Web/ReleaseDetailTest.php`.
**2. Branchen-Übersichten** – `/branche/[slug]` Zum Beispiel `/branche/energie-klima`, `/branche/finanzen`. Aggregierte Sicht auf alle PMs einer Branche, mit Sub-Filtern. Das sind deine SEO-Goldgruben (jede Branche eine ranking-fähige Landing Page).
_IST 21.05.2026_: umgesetzt (`web/kategorie.blade.php`, `web/kategorien.blade.php`).
_IST 16.06.2026_: `web/kategorie.blade.php` im Editorial-Design mit echten Daten (Top-Meldung, Feed, Sub-/verwandte Branchen, Stats, Newsrooms). Neue URL `/{edition}/category/{slug}`. Es gibt **keine** Gesamt-Uebersicht mehr — die Header-/Footer-Navigation fuehrt direkt auf die Rubrik (`CategoryNavigation`); `web/kategorien.blade.php` + Route entfernt (Legacy-301). Tests: `CategoryPageTest`, `CategoryNavigationTest`.
**3. Regionen-Übersichten** – `/region/[slug]` `/region/deutschland`, `/region/bayern`, `/region/oesterreich`. Analog zu Branchen, regional gefiltert.
_IST 21.05.2026_: noch nicht umgesetzt.
**4. Newsroom-Seite eines Unternehmens** – `/newsroom/[slug]` Markenseite eines Premium-Publishers mit eigener URL, Logo, allen PMs des Unternehmens. Ist gleichzeitig Verkaufsargument für Pro-/Agency-Tarif und SEO-Vorteil für die Unternehmen.
_IST 21.05.2026_: Layout vorhanden (`web/newsrooms.blade.php`), Daten-Anbindung pro Firma noch offen.
_IST 17.06.2026_: umgesetzt im Editorial-Design mit echten Daten. **Markenseite** `web/newsroom.blade.php` (Route `newsroom`, URL `/{edition}/newsroom/{slug}`): Logo/Initiale, Boilerplate, Website, Branchen der Firma, Kennzahlen (gesamt/30 Tage/heute/Aufrufe/seit Jahr), Top-Meldung und paginierte Meldungsliste (`x-web.pagination`), Sidebar (Meistgelesen, Publisher-CTA). 404 bei unbekanntem Slug oder Firma ohne veröffentlichte Meldung. Zusätzlich **Verzeichnis** `web/newsrooms.blade.php` (Route `newsrooms`, URL `/{edition}/newsrooms`): Karten-Grid aller aktiven Newsrooms mit Meldungs-/Aktivitätszahlen, Namens-Suche (`?q=`) und Pagination. `x-web.active-newsrooms` verlinkt jetzt über `route('newsroom', …)`. Legacy-`.html`-URLs (`/newsrooms.html`, `/newsroom/{slug}.html`) per 301. Test: `tests/Feature/Web/NewsroomPageTest.php`.
**5. Such-Ergebnisseite** – `/suche?q=...` Volltextsuche mit Filtern (Erweiterte Suche schreibt in URL-Parameter, dadurch teilbar/bookmarkbar).
_IST 21.05.2026_: Layout vorhanden (`web/suche.blade.php`), Volltextsuche noch nicht aktiv.
_IST 16.06.2026_: aktive Suche umgesetzt. `web/suche.blade.php` im Editorial-Design + Volt-Komponente `livewire/web/search.blade.php`: Suche ueber Titel/Untertitel/Text/Keywords sowie Firmenname und Rubrik-Name, Rubriken-Filter, Sortierung (Neueste/Aelteste/Meistgelesen), Pagination. URL-Parameter `q`, `category`, `sort` (teilbar/bookmarkbar); portal-/sprachsensitiv. Test: `tests/Feature/Web/SearchPageTest.php`.
**6. Tag-/Themen-Seite** – `/thema/[slug]` _(optional, später)_ Nicht im ersten Release zwingend, aber sehr SEO-wirksam für aktuelle Themen ("Künstliche Intelligenz", "Lieferkettengesetz", "Energiekrise"). Würde ich datengetrieben aus den meistverwendeten Tags generieren lassen.
_IST 21.05.2026_: nicht umgesetzt (bewusst spaeter).
#### Service-/Vertriebs-Seiten
**7. Pressemitteilung einreichen / Veröffentlichen** – `/veroeffentlichen` Die Conversion-Landingpage für neue Publisher. Erklärt Mehrwert, zeigt Tarife, Editor-Vorschau. Dahinter der eigentliche Editor (im User-Bereich).
_IST 21.05.2026_: Landing-Seite vorhanden (`web/veroeffentlichen.blade.php`). Editor-Strecke im User-Bereich ist umgesetzt (siehe Phase 7).
_IST 16.06.2026_: Landing-Seite im Editorial-Design neu aufgebaut, echte Kennzahlen (Archiv-Gesamt, heute veroeffentlicht/in Pruefung, aktive Newsrooms, Beispiel-Mitteilung). Einreichung verweist auf den zentralen Publisher-Bereich. Test: `tests/Feature/Web/VeroeffentlichenPageTest.php`.
**8. Tarife & Preise** – `/preise` _(oder als Modal aus mehreren Stellen aufrufbar)_ Da Tarife auch im Modal aus dem CTA aufgerufen werden, ist die Frage: brauchen wir die Seite? Antwort ja, weil SEO ("Pressemitteilung veröffentlichen Preise" ist eine wichtige Suche) und weil sie verlinkbar sein muss aus AGB, Footer, Mediadaten.
_IST 21.05.2026_: Layout vorhanden (`web/preise.blade.php`), echte Tarife noch nicht hinterlegt (Tarif-Modul siehe `Presseportal – Konzept für Relaunch.md` Abschnitt 8).
_IST 17.06.2026_: Tarife sind jetzt **datengetrieben** aus dem System (`Plan::active()`) auf der **Startseite** als Tarif-Teaser sichtbar (neue Komponente `x-web.pricing-teaser`, Preis/Quota/Tageslimit/Jahrespreis je Tarif, „Beliebt"-Hervorhebung). Der **Preise-Link** ist auf jeder Portalseite präsent: in der Header-Utility-Bar (neu) und im Footer („Tarife & Pakete", bestand bereits). Test: `tests/Feature/Web/PricingTeaserTest.php`. **Offen**: Die eigentliche `/preise`-Seite (`web/preise.blade.php`) ist noch im alten Theme mit Platzhalter-Tarifen — Editorial-Rebuild mit `x-web.pricing-teaser`/echten Plänen ist der nächste Schritt.
**9. Mediadaten / Werbung** – `/mediadaten` oder `/werben` Für Mediaplaner und potentielle Werbekunden: alle buchbaren Slot-Typen (Top-Slot, Highlights, Newsletter, Branchen-Sponsoring), Reichweiten-Daten, Preise, Booking-Kontakt. Pflicht-Seite für jede Plattform mit Anzeigeninventar.
_IST 21.05.2026_: nicht umgesetzt.
**10. Newsletter-Anmeldung als eigene Seite** – `/newsletter` Auch wenn Newsletter im Footer und in einer Sektion auf der Startseite eingebettet ist, brauchst du eine eigene Seite für direkte Anmelde-Links (aus E-Mails, Social, Werbekampagnen).
_IST 21.05.2026_: Layout in den Themes vorhanden, eigene Anmelde-Seite noch nicht.
#### Vertrauens- / Editorial-Seiten
**11. Über uns** – `/ueber-uns` Plattform-Geschichte, Team, redaktionelle Haltung. Kurz und persönlich, kein Marketing-Geschwurbel.
_IST 21.05.2026_: Layout vorhanden (`web/ueber-uns.blade.php`, `web/team.blade.php`).
**12. Redaktion / Redaktionsrichtlinien** – `/redaktion` Wichtige Vertrauensseite: Wer prüft die Inhalte? Wie funktioniert der Content-Score? Was ist der Unterschied zu redaktionell geprüften Anzeigen? Diese Seite differenziert dich von Spam-Portalen.
_IST 21.05.2026_: noch nicht umgesetzt.
**13. Kontakt** – `/kontakt` Klassisch, mit Funktions-E-Mails (presse@, redaktion@, werbung@, support@) und Kontaktformular.
_IST 21.05.2026_: Layout vorhanden (`web/kontakt.blade.php`).
#### Rechtliches
**14. Impressum** – `/impressum` **15. Datenschutz** – `/datenschutz` **16. AGB** – `/agb` **17. Cookie-Einstellungen** – `/cookies` _(oder Modal)_
Diese vier sind Pflicht und nicht zusammenfassbar.
_IST 21.05.2026_: alle vier als Layout vorhanden (`web/impressum.blade.php`, `web/datenschutz.blade.php`, `web/agb.blade.php`, `web/cookies.blade.php`). Inhalte sind teilweise Platzhalter — vor Go-Live durch Anwalt zu pruefen.
#### Technik / Distribution
**18. RSS-Feeds-Übersicht** – `/feeds` Liste aller verfügbaren RSS-Feeds (alle, pro Branche, pro Region). Eine Seite, listet alle Feed-URLs auf.
_IST 21.05.2026_: nicht umgesetzt.
**19. API-Dokumentation** – `/api` Für Distribution-Partner und Pro-/Agency-Kunden mit API-Zugang.
_IST 21.05.2026_: Seite vorhanden (`web/api.blade.php`). Pre-existing `ApiDocumentationTest` ist rot, weil `docs/api/v1.yml` noch fehlt — eigener Track.
#### DSA-/Rechts-Pflichten
**20. PM melden** – `/melden/[id]` Öffentlicher Notice-and-Action-Endpoint, eigener Pfad pro PM (kann auch als Modal von der PM-Detailseite kommen, aber direkter Link für rechtssichere Beschwerden besser).
_IST 21.05.2026_: nicht umgesetzt (Phase 2/3, DSA-Pflicht).
**21. Pressemitteilung verwalten (Magic-Link)** – `/verwalten` Einstiegspunkt für den Pressekontakt-Flow (E-Mail eingeben → Magic Link). Dahinter dann der eingeloggte Verwaltungs-Bereich.
_IST 21.05.2026_: nicht umgesetzt (Phase 2, siehe `Presseportal – Konzept für Relaunch.md` Abschnitt 6).
---
### Was als Modal/Overlay läuft (keine eigene Seite)
Das sind die Sachen, die man oft in einer separaten Seite versteckt sieht, aber besser inline gelöst werden – kein Kontext-Verlust für den User.
- **Erweiterte Suche** → Modal mit Filtern (URL-Parameter werden trotzdem gesetzt für Teilbarkeit)
- **Tarife-Übersicht aus CTAs** → Modal (neben der eigenen `/preise`-Seite)
- **Whitepaper-Download mit Lead-Capture** → Modal mit Name/E-Mail-Feldern
- **Newsletter-Anmeldung aus Sektion** → inline ohne Seitenwechsel
- **PM melden aus Detailseite** → Modal (mit Fallback auf eigene URL)
- **Cookie-Einstellungen** → Modal (mit Fallback auf eigene URL für Rechtssicherheit)
- **Login** → Modal (Anmelden-Button öffnet Modal, kein Seitenwechsel; eigene Seite nur als Fallback `/login`)
- **Bild-Lightbox** auf PM-Detailseite → Overlay
- **Teilen-Funktionen** auf PM-Detailseite → Modal mit Plattform-Auswahl und vorgenerierten Texten
- **Tarif-Wechsel im User-Bereich** → Modal
- **Credit-Aufladung** → Mini-Checkout-Modal (war im Konzept schon so geplant)
---
### Was im eingeloggten User-Bereich liegt
Hier ist wichtig: **alles unter einer einzigen Dashboard-URL**, nicht 15 Untermenüs. Ein Bereich, mehrere Tabs/Sektionen.
**Publisher-Dashboard** – `/dashboard`
> **IST-Stand 21.05.2026**: Im Code heisst der Customer-Bereich `/admin/me`
> (Routen-Namen `me.*`); das Admin-Backend liegt unter `/dashboard` und ist
> Editoren/Admins vorbehalten. Die Bereiche im User-Backend sind als
> eigene Pages mit `wire:navigate` (kein vollst. Seitenwechsel)
> umgesetzt und ueber die Sidebar navigierbar. Eine echte
> Tab-Komponente innerhalb einer einzigen URL gibt es nicht — der
> Mehrwert ist gleich.
Mit folgenden Bereichen als Tabs oder Sidebar-Navigation (kein Seitenwechsel zwischen den Tabs, oder URL-Tabs wie `/dashboard/meldungen`):
- **Übersicht** – Stats, Credit-Stand, letzte Aktivitäten _(umgesetzt als `customer/dashboard`)_
- **Meine Pressemitteilungen** – Liste mit Status, Bearbeiten, Korrektur, Update _(umgesetzt als `customer/press-releases/{index,show,create,edit}`)_
- **Editor** – Neue PM erstellen / bestehende bearbeiten (eigene Unter-URL `/editor` oder `/editor/[id]`) _(umgesetzt als Teil von `press-releases.{create,edit}`)_
- **Newsroom** – Markenseite konfigurieren (für Pro/Agency) _(nicht umgesetzt — Phase 2)_
- **Statistiken** – Detail-Auswertungen pro PM _(nicht umgesetzt — Phase 2)_
- **Credits & Rechnungen** – Stand, Verlauf, Pakete kaufen, Rechnungen herunterladen _(nur Rechnungen umgesetzt; Credits sind Phase 2)_
- **Tarif & Account** – Tarif-Verwaltung, Rechnungsdaten, Team-Mitglieder (für Agency) _(Profil + Rechnungsadresse umgesetzt; Tarif/Team Phase 2)_
- **Boost & Platzierungen** – Slot-Buchungen, Verlauf, neue buchen _(als Stub vorhanden `customer/bookings`)_
**Pressekontakt-Bereich** (Magic-Link) – `/verwalten/[token]`
Vereinfachte Version des Dashboards für nicht-registrierte Pressekontakte:
- Liste der PMs mit dieser E-Mail
- Änderungs-Wizard (Pfade A–G)
- Optional: Account-Anlage für späteren direkten Zugriff
**Admin-Bereich** – `/admin` _(intern, nicht öffentlich)_
Eigene Anwendung im Grunde, aber URL-mäßig unter Hauptdomain:
- Review-Queue (Gelb-PMs, Beschwerden, Persönlichkeitsrecht-Pfad F)
- User-Verwaltung
- Inventar-Management (welche Slots sind gebucht)
- Editorial-Picks setzen
- Reports / Statistiken
---
### Strukturelle Faustregeln, die ich anwenden würde
**1. Maximal zwei Klicks ab Startseite zu jeder Funktion.** Aus Startseite → Branchenseite → PM-Detail. Aus Startseite → Veröffentlichen → Tarif-Auswahl. Wenn etwas drei Klicks braucht, ist es falsch verortet.
**2. Footer ist die Sitemap.** Alle Service- und Rechts-Seiten leben _nur_ im Footer. Keine Mega-Menüs im Header. Die Hauptnavigation oben ist ausschließlich Branchen-Navigation plus Veröffentlichen-CTA.
**3. URL-Schemata konsistent.** Singular für Detailseiten (`/branche/...`, `/newsroom/...`), Verben für Aktionen (`/veroeffentlichen`, `/melden`, `/verwalten`). Keine kryptischen IDs in URLs, wenn vermeidbar – Slugs für SEO.
**4. Modals statt Seiten, wenn möglich.** Aber: jeder Modal hat einen Fallback-URL-Endpoint, falls jemand direkt verlinkt oder einen Bookmark setzt. Beispiel: Tarife-Modal → `/preise` als eigene Seite existiert weiterhin.
**5. Dashboard ist EIN Bereich.** Nicht "Meine PMs" als eigene Seite, "Stats" als andere, "Credits" als dritte – alles unter `/dashboard` mit Tabs. Reduziert kognitive Last und Navigation.
---
### Zusammenfassung als Liste zum Abhaken
**Öffentliche Inhalts-Seiten (6):** Detailseite, Branche, Region, Newsroom, Suche, Thema
**Service-/Vertriebs-Seiten (4):** Veröffentlichen, Preise, Mediadaten, Newsletter
**Vertrauen/Editorial (3):** Über uns, Redaktion, Kontakt
**Rechtliches (4):** Impressum, Datenschutz, AGB, Cookies
**Technik/Distribution (2):** Feeds, API-Doku
**DSA-Pflicht (2):** Melden, Verwalten (Magic-Link-Einstieg)
**Eingeloggte Bereiche (3):** Dashboard, Pressekontakt-Bereich, Admin
**Macht insgesamt 24 echte Seiten/Bereiche** – das ist für eine Plattform dieser Tiefe sehr schlank. Vergleichswert: presseportal.de hat über 80 Seiten in der Sitemap.

View file

@ -1,51 +0,0 @@
# KI- und Entwickler-Workflow (Pressekonto)
Kurzanleitung für Arbeit im Dev-Container: Dokumentation (Obsidian), Issues (Forgejo/`tea`), Git.
## 1. Dokumentation und Kontext (Obsidian)
- **Speicherort:** Markdown-Dateien liegen im Projekt unter **`docs/`** (im Container: `/var/www/html/docs/`). Dieser Ordner ist per Dev-Container mit dem Obsidian-Vault verbunden; Änderungen synchronisieren mit dem lokalen Vault.
- **Bei Fragen nach „Kontext“, „Briefing“ oder „Plan“:** Zuerst **`docs/`** nach passenden `.md`-Dateien durchsuchen (Dateinamen und Überschriften).
- **Neue Inhalte:** Konzepte, Pläne, Briefings und Spezifikationen **immer als `.md` in `docs/`** ablegen. Sinnvolle, stabile Dateinamen wählen (z. B. `feature-stripe-briefing.md`).
## 2. Issue-Management (Forgejo mit `tea`)
**Nur das `tea`-CLI im Terminal verwenden** — keine direkten HTTP/cURL-Aufrufe auf die Forgejo-API für Issue-Operationen.
| Aktion | Befehl |
|--------|--------|
| Issues auflisten | `tea issue list` |
| Issue anzeigen | `tea issue view <id>` |
| Neues Issue (Beschreibung aus Datei) | `tea issue create --title "Kurztitel" --description "$(cat docs/beispiel.md)"` |
| Issue aktualisieren | `tea issue edit <id> --description "$(cat docs/update.md)"` |
**Hinweise:**
- Nach `postCreateCommand` liegt `tea` typischerweise unter `~/.local/bin`; Shell neu starten oder `export PATH="$HOME/.local/bin:$PATH"` setzen, falls `tea` nicht gefunden wird.
- Remote/Ziel prüfen bei Bedarf mit `tea repos` bzw. der in `postCreateCommand` konfigurierten Login-Instanz **`gitmedia`** (`https://git.adametz.media`).
- **`tea` ist im Setup bereits authentifiziert** — keine Zugangsdaten oder Tokens im Chat abfragen oder in Repos committen. Token-Datei nur read-only gemountet (`/tmp/.forgejo_token`).
**Weitere Unterbefehle:** `tea issue --help`, `tea issue create --help`.
## 3. Git und Commits (Issue-Schließen)
1. **Conventional Commits:** Präfixe wie `feat:`, `fix:`, `refactor:`, `docs:`, `chore:` verwenden.
2. **Issue-Verknüpfung zum automatischen Schließen:** Im Commit-Text die Issue-Nummer mit **`closes`** (alternativ in vielen Gitea/Forgejo-Setups auch `fix(es)`, `resolve(s)`) angeben.
**Beispiel:**
```text
feat: Stripe-Zahlung implementiert, closes #12
```
3. **Branching:** Projekt-Branches und MR/PR-Regeln wie im Team vereinbart beibehalten.
## 4. Arbeitsweise (KI / Agent)
- Kontext zuerst aus **`docs/`**, dann Code und bestehende Projektregeln (z. B. `AGENTS.md`, `.cursorrules`).
- Terminal-Befehle direkt ausführen, wo sinnvoll; Antworten **prägnant**, Entscheidungen und Änderungen **nachvollziehbar** dokumentieren (bei Bedarf kurz in `docs/` oder im Issue).
- Keine manuelle Nachfrage nach Forgejo-Zugang **sofern `tea login` für `gitmedia` gesetzt ist**; bei `tea`-Fehlern Umgebung/Remote prüfen, nicht nach Passwörtern fragen.
---
*Letzte inhaltliche Ausrichtung: Dev-Container `workspaceFolder` `/var/www/html`, Vault-Bind nach `docs/`.*

View file

@ -1,612 +0,0 @@
# Phase 8 · User-Panel-Konsolidierung & Pressemitteilungs-Lifecycle
Stand: 2026-05-29 — **abgeschlossen** (alle Päckchen 8A–8K umgesetzt).
Vorgänger: Phase 7 (Press-Release-Form-Refactor — abgeschlossen)
Abgleich-Doku: [`docs/STATUS-ABGLEICH-USER-PANEL.md`](./STATUS-ABGLEICH-USER-PANEL.md)
Roadmap-Abschluss: [`dev/frontend/hub-flux/20-PHASE-8-USER-PANEL.md`](../dev/frontend/hub-flux/20-PHASE-8-USER-PANEL.md)
> **Status 29.05.2026**: Alle Sub-Päckchen abgeschlossen. Bewusste
> Abweichung in 8H: Upload-Control bleibt `flux:input type=file` statt
> `flux:file-upload` (Stabilität); die Lizenz-Pflichtfelder sind vollständig
> umgesetzt. Der Rechtstext im Veröffentlichungs-Modal (8I) ist ein
> Platzhalter und vor Go-Live anwaltlich zu prüfen.
---
## 0. Worum es geht
Phase 8 bündelt drei thematisch zusammenhängende Bündel:
1. **User-Panel-Konsolidierung** — Lücken aus Phase 7 schließen, Firmen-Liste
auf das Mockup-Niveau heben (`dev/frontend/tailwind_v3/User Firmen presseportale.html`).
2. **Pressemitteilungs-Titelbild & SVG-Platzhalter** — jede PM bekommt ein
sichtbares Hero-Bild, entweder eigener Upload oder ein farbiger
SVG-Platzhalter aus einem definierten Set.
3. **Veröffentlichungs-Modal mit rechtlichem Hinweis + Quota-Vorbereitung**
— Pressemitteilungen werden nur über ein bewusstes Modal eingereicht;
in dem Modal steht ein rechtlicher Hinweis und die Information, dass
ein PM-Kontingent verbraucht wird.
Alle Änderungen, die das User-Panel betreffen, werden **konsistent ins
Admin-Panel übertragen**, sobald sie inhaltlich passen (z. B.
Show-Pages, Listen-Indikatoren, Bild-Manager-Wrapper).
---
## 1. Sub-Päckchen-Übersicht
| ID | Thema | Größe | Risiko |
|---|---|---|---|
| **8A** | Show-Page-Lücken schließen (subtitle, scheduling, embargo, boilerplate_override) — Customer + Admin | S | gering |
| **8B** | Listen-Indikatoren für Scheduling/Embargo — Customer + Admin | S | gering |
| **8C** | Pressekontakt-Warnung in Sidebar-Card (Customer + Admin) + Tests | XS | gering |
| **8D** | Doku-Pflege: Phase-7-Schlussfeinheiten in `19-PHASE-7-…md` + Konzept-Anpassungen aus Abgleich | S | keine |
| **8E** | Firmen-Liste auf Mockup-Niveau (Counter-Strip, Saved-Views, Filter-Chips, Card/List-Toggle, Rollen-Legende) | L | mittel |
| **8F** | SVG-Platzhalter-Set extrahieren + auswählbar machen (Customer-Modal) | M | mittel |
| **8G** | PressRelease-Titelbild — Schema, Default-Platzhalter, Vorschau im Form | M | mittel |
| **8H** | FluxUI `flux:file-upload` im Image-Manager + Pflichtfelder Urheber/Lizenz/Rechte | M | mittel |
| **8I** | Veröffentlichungs-Modal mit Rechts-Hinweisen + Quota-Anzeige (Customer) + Hook | M | mittel |
| **8J** | Quota-Stub: Demo-Counter im Datenmodell + Decrement-Hook im PressReleaseService | M | hoch (Datenmodell) |
| **8K** | Tests + Pint + Build + Roadmap-Update + Abschluss-Eintrag in `PROGRESS.md` | S | gering |
**Abkürzungen**: XS = < 1 h, S = 1–3 h, M = 3–8 h, L = 8–16 h.
Reihenfolge entspricht dem geplanten Ablauf. Nach jedem Päckchen ist ein
Review-Stopp mit dem User vorgesehen, bevor das nächste startet.
---
## 2. Päckchen im Detail
### 8A · Show-Page-Lücken schließen
**Ziel**: Customer-Show und Admin-Show zeigen die Phase-7-Felder.
**Anpassungen**:
- `resources/views/livewire/customer/press-releases/show.blade.php`
- Untertitel direkt unter H1 als kleinere Headline anzeigen
- „Geplante Veröffentlichung"-Card mit `scheduled_at`, falls gesetzt
- „Embargo bis"-Card mit `embargo_at`, falls gesetzt
- „Boilerplate (Override)"-Card, falls `boilerplate_override` befüllt
- `resources/views/livewire/admin/press-releases/show.blade.php`
- Untertitel
- Boilerplate-Override (Scheduling/Embargo sind bereits da)
**Tests**: bestehende Show-Tests erweitern um Assertions für die neuen
Sichtbarkeiten.
**Akzeptanz**: Customer- und Admin-Show stellen exakt dieselben PM-Felder
dar, die in den Forms gepflegt werden können (außer Anhänge — deaktiviert).
---
### 8B · Listen-Indikatoren für Scheduling/Embargo
**Ziel**: In beiden PM-Listen sieht man auf einen Blick, welche PMs
geplant sind oder unter Embargo stehen.
**Anpassungen**:
- `customer/press-releases/index.blade.php`:
- In der Datums-Spalte: zusätzliches Sub-Label „geplant · 21.05. 14:00"
bzw. „Embargo bis 21.05."
- `admin/press-releases/index.blade.php`:
- Analog
**UI-Pattern**: Mono-Sub-Zeile unter dem Hauptdatum, wenn `status =
review` UND `scheduled_at`/`embargo_at` gesetzt.
**Tests**: ein neuer Pest-Test pro Liste, der ein PM mit
`scheduled_at`/`embargo_at` erstellt und die Sub-Zeile assertet.
---
### 8C · Pressekontakt-Warnung im Form-Sidebar
**Ziel**: Wenn keine Pressekontakt in einer PM gewählt ist, zeigt die
Sidebar-Card eine dezente Warn-Box (analog zur „Telefonnummer fehlt"-
Warnung), damit klar ist, dass die PM zwar speicherbar ist, aber
einen Kontakt empfehlen sollte.
**Anpassungen**:
- `customer/press-releases/create.blade.php` + `edit.blade.php`:
- Im Pressekontakt-Sidebar-Card:
```blade
@if (! $contactId)
<div class="inline-callout warn">
<flux:icon.exclamation-triangle class="size-4" />
Es wurde noch kein Pressekontakt ausgewählt. Empfohlen, aber nicht zwingend.
</div>
@endif
```
- `admin/press-releases/create.blade.php` + `edit.blade.php`: identisch
**Tests**: Form-Render-Test mit ohne-Kontakt-Setup, das den Warn-String
assertet.
---
### 8D · Doku-Pflege
**Ziel**: `19-PHASE-7-PRESS-RELEASE-FORM.md` und Konzept-Dokumente an
den IST-Stand anpassen, damit zukünftige Phasen auf einer sauberen
Basis aufsetzen.
**Konkret**:
- `dev/frontend/hub-flux/19-PHASE-7-PRESS-RELEASE-FORM.md`:
- Block ergänzen: „Anhänge deaktiviert; Tests skipped; siehe Security-Review"
- Block ergänzen: „Pressekontakt nullable; Warnung im Sidebar"
- Block ergänzen: „Sidebar-Reihenfolge: Status → Kategorie → Portal
(Pill) → Pressekontakt → Themen-Tags → Veröffentlichung → Weitere
Felder → Phase-2-Footer"
- `docs/user-admin/Admin-User.md`: Aktualisierungen aus
[`STATUS-ABGLEICH-USER-PANEL.md` Abschnitt 6.1](./STATUS-ABGLEICH-USER-PANEL.md#61-sofort-ohne-risiko-machbar).
- `docs/user-admin/checkliste-user-backend.md`: neuen Phase-7-Block
hinzufügen.
**Akzeptanz**: Wer die `docs/`-Hauptdokumente liest, bekommt einen
zutreffenden Eindruck vom IST-Stand.
---
### 8E · Firmen-Liste auf Mockup-Niveau
**Status**: ✅ abgeschlossen (21.05.2026)
**Was umgesetzt wurde**
- `resources/views/livewire/customer/press-kits/index.blade.php` komplett
überarbeitet (Volt + Hub-Tokens).
- Neue Hub-Tokens in `resources/css/shared/hub-components.css`: `.firm-card`,
`.add-tile`, `.seg-toggle`, `.role-pill`, `.mini-logo`, `.lg-*`-Logo-Varianten,
`.menu-trigger`, `.card-action`, `.page-btn`.
- Counter-Strip mit `Firmen · aktiv · PMs gesamt · Pressekontakte hinterlegt`.
- Saved-View-Tabs `Alle/Aktiv/In Anlage/Inaktiv/Mit mir geteilt`, mit
Live-Counts. „In Anlage" ist bewusst noch leer (Phase-2-Heuristik).
- Filter-Chips (Portal, Rolle) via FluxUI-Dropdown + URL-Sync (`?view=…&portal=…&role=…&mode=…`).
- View-Toggle Karten / Liste, persistiert in der URL als `?mode=list`.
- Karten- und Listen-Ansicht mit Hub-Look, deterministische Logo-Varianten,
Status-Badge, Portal-Pills, Rolle-Pill, KPIs (PMs / Kontakte / letzte PM),
Aktionen „Firma öffnen" und „Neue PM".
- Add-Tile auf der letzten Seite (CTA: `Firma anlegen anfragen` → Profil).
- Empty-States: 3-Schritt-Onboarding (keine Firmen) und Reset-CTA (Filter
ohne Treffer).
- Rollen-Legende als `panel-warm` mit Owner / Verantwortlich / Mitglied.
- `tests/Feature/CustomerPressKitIndexPhase8eTest.php` mit 14 Tests
(Counter, Saved-Views, Filter, View-Mode, Empty-States, Add-Tile,
Rollen-Legende).
**Ziel**: `customer/press-kits/index.blade.php` entspricht dem Mockup
`dev/frontend/tailwind_v3/User Firmen presseportale.html`.
**Mockup-Komponenten** (alle CSS-Klassen aus
`resources/css/shared/hub-components.css` bereits vorhanden oder leicht
ergänzbar):
- **Counter-Strip** (`.counter-strip`): `X Firmen · X aktiv · X PMs gesamt · X Pressekontakte`
- **Saved-View-Tabs** (`.view-tabs`): `Alle / Aktiv / In Anlage / Inaktiv / Mit mir geteilt`
- „In Anlage" = neue Firma, die noch keine Stammdaten hat (heuristisch: kein Logo + keine PMs)
- „Mit mir geteilt" = `company_user.role IN (member, responsible)` ohne `owner_user_id = me`
- **Filter-Chips** (`.filter-chip`):
- Status (Aktiv/Inaktiv/Alle)
- Portal (presseecho/businessportal24/Alle)
- Rolle (Admin/Redakteur/Beobachter/Alle) — „Admin"-Begriff für Owner,
„Redakteur" für `responsible`, „Beobachter" für `member`
- Branche (Auswahl aus vorhandenen `Industry`-Werten)
- **Seg-Toggle** (`.seg-toggle`): Karten- vs. Listen-Ansicht (Default Karten)
- **Karten** (`.firm-card`):
- Logo (Hub-Token-Box oder echtes Logo)
- Status-Badge
- Portal-Pills (eine oder zwei, je nach Firma-Portal)
- Rolle-Pill (`.role-pill.admin` für Owner)
- KPIs: PMs, Pressekontakte, Datum letzte PM
- Aktionen: „Bearbeiten" (zur Firma) + „Neue PM" (Editor mit Firma vorgewählt)
- `is-self`-Highlight für die aktive Firma aus dem Context-Switcher
- **Add-Tile**: „Neue Firma anlegen" — derzeit nur Anfrage-Link auf Profil
(Self-Service-Anlage ist Phase-2-Thema)
- **Empty-States**:
- Keine Firma: 3-Schritt-Onboarding (Stammdaten → Boilerplate → Pressekontakte) — derzeit nur Anfrage-CTA
- Filter ohne Treffer: Reset-CTA
- **Rollen-Legende** am Ende als `panel-warm`
**Volt-Anpassungen**:
- Computed Properties: `statusCounts`, `portalOptions`, `roleOptions`,
`industryOptions`
- Neue Properties: `$statusFilter`, `$portalFilter`, `$roleFilter`,
`$industryFilter`, `$viewMode` (cards|list)
- Methoden: `setView($status)`, `resetFilters()`, `toggleViewMode()`
**Was bewusst NICHT in 8E kommt**:
- Echte Self-Service-Firma-Anlage (Phase 2)
- Statistik-Tab in Firmen-Detail (Phase 2)
- Abrechnung pro Firma (Phase 2)
**Tests**:
- Komponente rendert mit 0/1/N Firmen
- Filter-Kombinationen
- View-Mode-Toggle
- Counter-Strip-Zahlen stimmen mit den Filtern
**Admin-Spillover**: Die `admin/companies/index.blade.php` hat einen
ähnlichen, älteren Mockup-Stand. Wenn Zeit übrig: Counter-Strip und
Saved-Views auch dort einbauen (separater Patch im selben Päckchen,
nur wenn ohne Mehraufwand machbar — sonst Phase 9).
---
### 8F · SVG-Platzhalter-Set
**Ziel**: Ein wiederverwendbares Set von Hero-Platzhaltern für PMs ohne
echtes Titelbild.
**Quelle**: Bestehende inline SVGs in den Landing-Page-Komponenten:
- `resources/views/components/web/focus-hero.blade.php` (760×500, Punkt-Pattern + Kreise)
- `resources/views/components/web/feed-top-item.blade.php` (240×160, Linien + Punkte)
- ggf. weitere aus `industry-spotlight`, `quality-summary`, `live-ticker`
**Neue Struktur**:
```
resources/views/components/portal/press-release-placeholder.blade.php
public/images/press-release-placeholders/
01-grid-blue.svg
02-grid-green.svg
03-grid-amber.svg
04-lines-blue.svg
05-lines-green.svg
06-lines-amber.svg
07-dots-blue.svg
08-dots-green.svg
09-dots-amber.svg
```
Jede SVG ist 1600×900 (Hero-Aspect-Ratio 16:9), entspricht dem Bildformat
des `large`-Variant aus `ImageService`.
**Komponente**:
```blade
<x-portal.press-release-placeholder
:variant="$pressRelease->placeholder_variant ?? '01-grid-blue'"
:title="$pressRelease->title"
class="aspect-[16/9] w-full" />
```
**Modal-Auswahl** (Volt-Sub-Komponente):
```
resources/views/livewire/components/press-release-placeholder-picker.blade.php
```
- Grid 3×3 mit Vorschau aller Varianten
- Wire-Event `placeholderSelected($variant)` → Parent setzt
`placeholder_variant` und schließt das Modal
**Tests**: Komponente rendert mit jeder Variante; Picker emittiert
korrektes Event.
---
### 8G · Titelbild-Schema & Default-Logik
**Ziel**: Jede PM hat **immer** ein Hero-Bild — entweder echtes Bild
oder Platzhalter.
**Schema-Änderung**:
Migration: `add_placeholder_variant_to_press_releases.php`
```php
$table->string('placeholder_variant', 32)->nullable()->after('boilerplate_override');
```
**Default-Logik**:
- `PressRelease::booted` → bei `creating`: wenn `placeholder_variant`
leer, würfle eine Variante aus dem Set
- alternativ in `customer/press-releases/create.blade.php`: nach
`mount()` Default setzen
**Cover-Image-Resolver** (Service):
```
app/Services/PressRelease/PressReleaseCoverImage.php
```
Public-Methoden:
- `coverUrl(PressRelease $pr, string $variant = 'large'): string`
- wenn echtes Preview-Bild da → `variantUrl($variant)`
- sonst → `asset('images/press-release-placeholders/'.$pr->placeholder_variant.'.svg')`
- `coverIsPlaceholder(PressRelease $pr): bool`
**Verwendung**:
- Hero in Customer-Show, Admin-Show, Public-Detail-Page
- Thumb in beiden Listen
- Vorschau im Form
**Tests**: Resolver-Unit-Test für beide Fälle.
---
### 8H · FluxUI File-Upload + Lizenzfelder
**Ziel**: Image-Manager nutzt `flux:file-upload`, erfasst die rechtlich
nötigen Felder, eckende UI passt zum Mockup.
**Anpassungen** in `resources/views/livewire/components/press-release-images-manager.blade.php`:
- `<flux:file-upload accept="image/jpeg,image/png,image/webp" wire:model="newImage" />`
(Dropzone-Style aus FluxUI)
- Zusätzliche Felder als FluxUI-Inputs:
- **Urheber/Fotograf** (`flux:input` required)
- **Lizenztyp** (`flux:select` mit Enum-Werten):
- Eigene Aufnahme
- CC-Lizenz (Lizenz-URL Pflicht)
- Kommerzielle Lizenz erworben (Lizenz-URL Pflicht)
- Einwilligung des Urhebers
- Sonstiges
- **Lizenz-URL** (`flux:input` conditional required)
- **Personen-Einwilligung** (`flux:checkbox` optional)
- **Rechte-Bestätigung** (`flux:checkbox` required, mit AGB-Text aus
`Presseportal – Konzept für Relaunch.md` Abschnitt 2)
**Schema**:
Migration: `add_license_fields_to_press_release_images.php`
```php
$table->string('author')->nullable();
$table->string('license_type', 32)->nullable();
$table->string('license_url')->nullable();
$table->boolean('persons_consent')->default(false);
$table->timestamp('rights_confirmed_at')->nullable();
```
Enum: `App\Enums\ImageLicenseType` (PHP-Enum mit Labels).
**Tests**:
- Upload ohne Urheber → Validierungs-Fehler
- Upload mit `license_type = cc` ohne `license_url` → Fehler
- Upload mit allen Pflichtfeldern → erfolgreich, `rights_confirmed_at` gesetzt
**UI-Skizze** (Form-Reihenfolge):
```
┌─────────────────────────────────────┐
│ [flux:file-upload Dropzone] │
├─────────────────────────────────────┤
│ Urheber: [input] * │
│ Lizenztyp: [select] * │
│ Lizenz-URL: [input] (*) │
│ [ ] Personen-Einwilligung │
│ [ ] Ich bestätige die Rechte * │
│ │
│ [Hochladen] │
└─────────────────────────────────────┘
```
---
### 8I · Veröffentlichungs-Modal
**Ziel**: Customer kann eine PM nur über ein explizites Modal mit
rechtlichem Hinweis und Quota-Information einreichen.
**Anpassungen** in `customer/press-releases/edit.blade.php`:
- „Zur Prüfung einreichen"-Button löst Modal-Open aus statt direkt
`submitForReview` zu rufen
- Modal-Inhalt:
- Eyebrow „Veröffentlichung"
- H3 „Pressemitteilung zur Prüfung einreichen"
- Block „Rechtliche Hinweise" (aus Konzept-Abschnitt 5 zur
DSGVO-Position + Abschnitt 2 zu Bildrechten, plus AGB-Verweis)
- Block „Kontingent" — Anzeige `Ihre verbrauchten PMs in diesem Monat: X / Y`
- Block „Bestätigungen":
- [ ] Inhalt entspricht den AGB
- [ ] Bildrechte sind geklärt
- [ ] Pressekontakt-Daten korrekt
- Footer: „Abbrechen" (sekundär) + „Veröffentlichung anfordern" (primär,
disabled bis alle 3 Checkboxen gesetzt)
- Confirm-Button ruft `submitForReview()` (existiert bereits)
**Schreibweise** (juristisch sicherer Ankertext):
```
Mit dem Einreichen dieser Pressemitteilung versichern Sie:
- Sie sind befugt, den Inhalt zu veröffentlichen.
- Alle verwendeten Bilder, Logos und Zitate liegen in Ihrer Nutzungsbefugnis.
- Personenbezogene Daten sind nur in dem für die Berichterstattung
zwingend erforderlichen Umfang enthalten.
- Aussagen entsprechen Ihrem Wissensstand und sind sachlich richtig.
Sie stellen [Plattform] von Ansprüchen Dritter frei, die aus einer
unberechtigten Nutzung von Inhalten resultieren. Die endgültige
Veröffentlichung erfolgt nach redaktioneller Prüfung.
```
(Exakte Formulierung muss vor Go-Live durch einen Anwalt geprüft werden —
für die Bauphase reicht der Platzhalter.)
**Quota-Anzeige**:
In Phase 8I noch ohne echte Tarif-Logik: Anzeige eines **Demo-Counters**,
der aus einer einfachen Aggregation (`User::pressReleasesPublishedThisMonth()`)
kommt — und einer hartcodierten Obergrenze (z. B. 3 als Starter-Default).
Das echte Tarif-System kommt in einer späteren Phase.
**Tests**:
- Modal öffnet sich bei Klick
- Submit-Button bleibt disabled bis alle 3 Checkboxen gesetzt
- Nach Bestätigung: PM-Status ist `review`
- Toast-Bestätigung wird angezeigt
**Admin-Spillover**: Im Admin-Editor reicht der Admin direkt ein
(`publish`), kein Modal nötig — der Hinweis ist auf den Customer-Flow
zugeschnitten.
---
### 8J · Quota-Stub
**Ziel**: Die UI-Anzeige in 8I hängt am echten Datenmodell, auch wenn
das vollständige Tarif-/Credit-System erst später kommt.
**Datenmodell** (minimal):
Migration: `create_user_quota_table.php` oder als JSON in `profiles`?
> **Entscheidung**: Wir machen es als Migration-light auf `users`:
>
> ```php
> $table->unsignedInteger('press_release_quota')->default(3)->after('settings');
> $table->unsignedInteger('press_release_quota_used_this_month')->default(0)->after('press_release_quota');
> ```
>
> Diese Spalten sind temporär, das echte Tarif-Modell überschreibt sie
> oder ersetzt sie durch eigene Tabellen.
**Service** in `PressReleaseService::submitForReview`:
```php
$user->increment('press_release_quota_used_this_month');
```
(Reset des Counters per Scheduled-Command monatlich → eigener kleiner
Befehl `ResetMonthlyPressReleaseQuota`.)
**API für die View** (z. B. via `User`-Method):
```php
public function pressReleaseQuotaRemaining(): int
{
return max(0, $this->press_release_quota - $this->press_release_quota_used_this_month);
}
```
**Akzeptanz**: Veröffentlichungs-Modal zeigt sinnvolle Zahlen, der
Counter erhöht sich nach Submit, der Scheduler-Command resettet ihn
zum 1. des Monats. Tarif-Anbindung folgt später.
**Tests**:
- Counter inkrementiert bei `submitForReview`
- Counter resettet via Scheduled-Command (Unit-Test)
---
### 8K · Tests, Pint, Build, Roadmap
**Ziel**: Saubere Übergabe.
- `vendor/bin/sail artisan test --compact` muss durchlaufen (außer
pre-existing `ApiDocumentationTest`).
- `vendor/bin/sail bin pint --dirty --format agent` clean.
- `vendor/bin/sail npm run build:portal` clean.
- `dev/frontend/hub-flux/20-PHASE-8-USER-PANEL.md` als neue
Roadmap-Doku (analog zu `19-…`).
- Eintrag in `dev/frontend/hub-flux/PROGRESS.md` mit allen Sub-Päckchen.
- `docs/user-admin/checkliste-user-backend.md` um Phase-8-Block ergänzen.
---
## 3. Was außerhalb von Phase 8 bleibt
Bewusst nicht in Phase 8:
- **Magic-Link-Flow für Pressekontakte** → Phase 9 oder Phase 2 lt. Konzept
- **Statistik-Tab in Firmen-Detail** → Phase 9
- **Self-Service-Firmen-Anlage** → Phase 9
- **Notice-and-Action für externe Meldungen** → Phase 2/3
- **KI-Vorprüfung** → Phase 2/3
- **Korrektur-/Update-Hinweis-System** → Phase 2/3
- **Echtes Tarif-/Credit-System mit Stripe** → eigene Phase
- **Trust-Score / Score-System** → Phase 3
- **Anhänge-Reaktivierung** → eigener Sicherheits-Audit-Track
---
## 4. Reihenfolge & Review-Punkte
Vorgeschlagene Reihenfolge der Päckchen:
```
8D (Doku zuerst — bewegt sich nichts am Code)
→ 8A (Show-Page-Lücken)
→ 8B (Listen-Indikatoren)
→ 8C (Pressekontakt-Warnung)
→ Review-Stopp mit User
→ 8E (Firmen-Liste auf Mockup) — größtes Päckchen, Review davor
→ Review-Stopp
→ 8F (SVG-Platzhalter extrahieren)
→ 8G (Titelbild-Schema + Default)
→ 8H (FluxUI File-Upload + Lizenzfelder)
→ Review-Stopp
→ 8J (Quota-Stub im Datenmodell zuerst, damit 8I darauf aufsetzen kann)
→ 8I (Veröffentlichungs-Modal)
→ 8K (Abschluss)
```
**Begründung**: Doku zuerst, weil der Abgleich sonst veraltet. Dann
kleine UX-Lücken (8A–8C), die schnelle Wins sind. Anschließend die
größere Firmen-Liste (8E), die ein eigenes Päckchen ist. Bild- und
Veröffentlichungs-Block am Ende, weil sie thematisch zusammengehören
und Schema-Änderungen mitbringen.
---
## 5. Risiken & Annahmen
- **Annahme**: FluxUI `flux:file-upload` ist in der aktuellen Version
voll funktional und kompatibel mit `WithFileUploads` von Livewire.
Fallback: bestehender Standard-Upload bleibt erhalten.
- **Annahme**: SVG-Platzhalter (1600×900) sind klein genug, dass wir
sie direkt aus `public/images/...` ausliefern — kein CDN-Setup nötig.
- **Risiko**: Schema-Änderungen in 8G + 8H + 8J berühren produktive
Tabellen (`press_releases`, `press_release_images`, `users`). Alle
Migrations sind additive (nullable + default), Rollback-fähig.
- **Risiko**: Quota-Stub in 8J wird vom echten Tarif-System abgelöst
— Code-Schnittstelle (`pressReleaseQuotaRemaining()`) muss stabil bleiben,
damit das Veröffentlichungs-Modal nicht neu gebaut werden muss.
- **Risiko**: Rechtstext im Veröffentlichungs-Modal ist Platzhalter.
Vor Go-Live durch Anwalt zu prüfen.
---
## 6. Akzeptanzkriterien Phase 8 gesamt
- [x] Customer-Show + Admin-Show stellen alle Phase-7-Felder dar
- [x] PM-Listen markieren Scheduling und Embargo
- [x] Pressekontakt-Sidebar warnt bei leerer Auswahl
- [x] `docs/user-admin/*` ist mit dem Code synchron
- [x] Firmen-Liste entspricht dem Mockup zu ≥ 90 %
- [x] Jede PM hat ein sichtbares Hero-Bild (echtes oder Platzhalter)
- [x] Image-Upload erfasst Urheber + Lizenz-Typ + Rechte-Bestätigung
- [x] „Zur Prüfung einreichen" erfordert eine bewusste Modal-Bestätigung
- [x] Quota-Counter inkrementiert pro Einreichung, resettet monatlich
- [x] Tests grün (375 passed, 4 skipped — inkl. pre-existing `ApiDocumentationTest`)
- [x] Pint clean, Build clean
- [x] Roadmap-Eintrag und `PROGRESS.md`-Block geschrieben
---
## 7. Nächster Schritt
Mit **8D (Doku)** starten, weil das ohne Code-Änderungen funktioniert
und den Boden für die folgenden Päckchen ebnet. Direkt im Anschluss
**8A–8C** als Block, weil sie zusammen die Phase-7-Lücken schließen.
Danach Review-Stopp für Phase 8E (Firmen-Liste) — das ist das
sichtbarste Päckchen für den User und sollte mit klarem Mockup-Vergleich
abgenommen werden.

View file

@ -1,289 +0,0 @@
# Phase 9 · Veröffentlichungs-Flow (Launch) & Tarif-Modul
Stand: 2026-06-12 — **Block 1 (9A–9C) abgeschlossen**; Review-Stopp vor
Block 2 (9D–9J, Tarif-Modul). Suite nach Block 1: 451 passed, 4 skipped.
Vorgänger: Phase 8 (User-Panel-Konsolidierung) + KI-Prüf-Pipeline (beide abgeschlossen).
Verbindliche Entscheidungen: [`docs/Decision-Update Preisstruktur & Veröffentlichungs-Flow.md`](./Decision-Update%20Preisstruktur%20&%20Ver%C3%B6ffentlichungs-Flow.md)
Abgleich-Doku: [`docs/STATUS-ABGLEICH-USER-PANEL.md`](./STATUS-ABGLEICH-USER-PANEL.md)
---
## 0. Worum es geht
Phase 9 setzt das Decision-Update vom 11./12.06.2026 um — in zwei Blöcken:
1. **Block 1 — Veröffentlichungs-Flow (9A–9C)**: Die Flow-Regeln, die
unabhängig vom Tarif-Modul gelten und auf denen das Tarif-Modul aufsetzt.
Funktioniert vollständig mit dem vorhandenen Quota-Stub.
2. **Block 2 — Tarif-Modul (9D–9I)**: Zahlung, Tarife, Einzel-PM, Tageslimit
und die drei Launch-Credit-Posten. Löst den Quota-Stub ab.
**Leitplanken aus dem Decision-Update:**
- Gelb geht zum Launch **direkt live** wie Grün — keine manuelle Prüf-Queue.
Nur Rot wird abgelehnt (Meldung mit Begründung an den Autor).
- Der PM-Slot zählt **bei Veröffentlichung** runter, nicht bei der Prüfung.
Rot verbraucht keinen Slot.
- „Speichern" bleibt immer frei; „Speichern & zur Prüfung einreichen" ist
hinter eine aktive Buchung gegated (der Button konvertiert, er verschwindet
nicht).
- Kein Re-Check zum Launch: eine Einreichung = eine Prüfung = (bei Gelb/Grün)
eine Veröffentlichung. Vorab-Prüfung/Redigieren sind Phase 2.
---
## 1. Sub-Päckchen-Übersicht
| ID | Thema | Größe | Risiko |
|---|---|---|---|
| **9A** ✅ | Gelb-Routing auf Direkt-Live umstellen (Routing, Scheduler, Tests) | S | gering |
| **9B** ✅ | Slot-Verbrauch von Einreichung auf Veröffentlichung umstellen (Rot = kein Slot) | M | mittel (Idempotenz) |
| **9C** ✅ | Submit-Gate-Schnittstelle (`hasActiveBooking()`-Stub, Modal-Hinweis, Server-Guard) + Fix: Create-Form lief am Funnel vorbei | M | gering |
| — | **Review-Stopp mit User** | | |
| **9D** ✅ | Tarif-Datenmodell: Pläne, Einzelkäufe, Cashier, Rechnungskreise STR-/MAN-, MAN-Fälligkeitslauf (Stub-Ablösung folgt mit 9E) | L | hoch (Datenmodell) |
| **9E** ✅ | Stripe-Anbindung: Produkt-Sync (Tarife + Einzel-PM), Webhook-Verarbeitung (STR-Spiegelung, Einmalkauf-Erfüllung, Endpoint registriert), Checkout-Flows (Backend), Slot-Logik auf Plan-Kontingent (Grandfathered = unbegrenzt), Stripe Tax | L | mittel |
| **9F** ✅ | Tarif-Seite + Checkout-UI: Buchungs-Seite mit echtem 4-Tier-Raster (Monat/Jahr-Toggle, „2 Monate gratis"), Einzel-PM-Block, Bestandstarif-Anzeige, „Abo verwalten" (Stripe Billing Portal), Enterprise-Hinweis | M | gering |
| **9G** | Tageslimit je Tier (Business 2 / Pro 3 / Agency 5; gilt auch für Extra-PMs) | S | gering |
| **9H** | Einzel-PM-Kauf (19 €) + Einzel→Abo-Brücke (Anrechnung 30 Tage) | M | mittel |
| **9I** | Launch-Credits: Extra-PM, Boost (nur Grün), Veröffentlichungsnachweis-PDF | L | mittel |
| **9J** | Abschluss: Tests, Pint, Build, Doku-Sync, PROGRESS-Eintrag | S | keine |
Nach jedem Päckchen Review-Stopp mit dem User; vor 9D ein größerer
(Datenmodell-Entscheidungen + Cashier-Freigabe).
---
## 2. Block 1 — Veröffentlichungs-Flow
### 9A · Gelb-Routing auf Direkt-Live
**Entscheidung (12.06.2026)**: Rot = nicht veröffentlichbar (rechtlich/
inhaltlich) → Ablehnung mit Meldung. Gelb/Grün = veröffentlichbar → geht in
der ersten Phase direkt online. Gelb bleibt als interne Markierung erhalten
(nicht boostbar, Admin-Signal), löst aber keine manuelle Prüfung aus.
**Anpassungen:**
- `PressReleaseService::routeByClassification()`: Gelb durchläuft denselben
Auto-Publish-Pfad wie Grün (`autoPublishGreen()` → generalisiert zu
`autoPublishApproved()`); Verzögerungsfenster
(`scoring.classification.green_delay_minutes`) gilt für beide.
- `PublishScheduledPressReleases`: Kandidaten-Query von
`classification = green` auf `classification IN (green, yellow)`.
- Admin-Review-Queue bleibt als **Fallback** bestehen: unklassifizierte PMs
(Job noch nicht gelaufen / KI-Ausfall ohne Fallback-Ergebnis) bleiben in
`review` und sind manuell behandelbar. KI-Badge und Klassifikations-Filter
im Admin bleiben unverändert.
**Tests:** `PressReleaseClassificationJobTest` (Gelb-sofort → published,
Gelb-geplant → bleibt review bis Termin), `PressReleaseSchedulingTest`
(gelbe fällige PM wird publiziert).
### 9B · Slot-Verbrauch bei Veröffentlichung
**Regel:** Der Slot zählt genau einmal pro PM, beim **ersten** Übergang zu
`published`. Rot abgelehnte PMs verbrauchen nichts.
**Anpassungen:**
- `submitForReview()`: Increment von `press_release_quota_used_this_month`
**entfernen**. Stattdessen Guard: Einreichen erfordert
`pressReleaseQuotaRemaining() > 0` (sonst würde eine grüne PM ohne
verfügbaren Slot veröffentlicht).
- `publish()`: Increment beim Statuswechsel auf `published`, idempotent —
nur wenn die PM zuvor noch nie veröffentlicht war (Prüfung über
`press_release_status_logs`, kein neues Schema-Feld). Zählt auf den
PM-Eigentümer (`user_id`).
- Veröffentlichungs-Modal: Text von „wird bei Einreichung verbraucht" auf
„wird bei Veröffentlichung verbraucht; abgelehnte PMs kosten keinen Slot".
**Tests:** Submit verbraucht keinen Slot; Publish (Admin, Auto-Publish,
Scheduler) verbraucht genau einen; Rot → kein Verbrauch; Archivieren +
erneutes Publizieren zählt nicht doppelt; Submit bei 0 Rest-Slots blockiert.
### 9C · Submit-Gate-Schnittstelle
**Ziel:** Das Gate aus dem Decision-Update §5.1, gebaut gegen eine schmale
Schnittstelle, die zunächst ein Stub bedient und in 9D/9E vom Tarif-Modul
implementiert wird — Modal und Service müssen dann nicht mehr angefasst werden.
**Anpassungen:**
- `User::hasActiveBooking(): bool` — Launch-Schnittstelle. Stub-Verhalten
über `config/billing.php` (`billing.enforce_booking`, Default `false`):
solange das Tarif-Modul fehlt, gibt die Methode `true` zurück; mit
aktiviertem Flag (und später echter Subscription-Prüfung) greift das Gate.
- Einreichungs-Modal (`press-release-submit-modal`): ohne aktive Buchung
zeigt das Modal statt des Prüf-Flows einen Buchungs-Hinweis mit CTA
(„Buchung erforderlich" → Tarif-Seite). Der Button bleibt sichtbar.
- Server-Guard: `submitForReview()` wirft ohne aktive Buchung eine Exception
(UI allein reicht nicht); API-Submit-Route antwortet mit **402**.
**Tests:** Gate aus (Default) → Verhalten unverändert; Gate an →
Modal-Hinweis statt Checkboxen, `submitForReview` wirft, API gibt 402.
---
## 3. Block 2 — Tarif-Modul (nach Review-Stopp)
### Entscheidung 12.06.2026 — Hybride Rechnungsarchitektur
Alle **neuen** Abschlüsse und Zahlungen laufen über **Stripe**. Die Umsetzung
ist hybrid mit zwei getrennten Rechnungskreisen (plus Altbestand):
| Kreis | Präfix | Inhalt |
|---|---|---|
| **Stripe-Shop** | `STR-` | Alles Neue (Abos, Einzel-PM, Credits) — komplette Abwicklung über Stripe, fortlaufende Nummer im STR-Kreis |
| **Manuell/Legacy** | `MAN-` | Laufende, noch aktive Alt-Zahlungen ab Relaunch: Fälligkeit wird im Hintergrund geprüft, Rechnung wie im Legacy-System ausgestellt |
| Alt-Archiv | — | Die importierten Alt-Rechnungen (`legacy_invoices`, 864 Stück) bleiben unverändert bestehen |
### 9D · Tarif-Datenmodell — ✅ umgesetzt (12.06.2026)
- **Cashier installiert** (`laravel/cashier` ^16.5, freigegeben); `User` ist
`Billable`, Cashier-Tabellen (`subscriptions`, `subscription_items`,
Customer-Spalten) migriert. Die lokale `invoices()`-Relation überschreibt
bewusst die Cashier-Methode.
- **`plans`**: Starter/Business/Pro/Agency mit Monats-/Jahrespreis
(Jahres = 10 × Monat), PM-Kontingent, Tageslimit, Stripe-IDs (nullable,
werden in 9E gepflegt). `PlanSeeder` idempotent.
- **`single_purchases`**: Einzel-PM, Extra-PM, Boost, PDF-Nachweis mit
Status Pending/Paid/Consumed/Refunded und Stripe-Checkout-Referenzen.
- **`invoice_number_sequences` + `InvoiceNumberGenerator`**: atomare,
fortlaufende Nummern pro Kreis (`STR-00001`, `MAN-00001`; Padding
konfigurierbar in `config/billing.php`).
- **MAN-Kreis**: `ManualInvoiceService` + Command
`billing:generate-manual-invoices` (Scheduler täglich 04:30) — findet
aktive/grandfathered `user_payment_options` **ohne**
`stripe_subscription_id` mit erreichtem `current_period_end`, friert die
Rechnungsadresse als Snapshot ein, stellt eine MAN-Rechnung aus
(Zahlungsziel `billing.manual_due_days`) und schaltet die Periode weiter.
Konditions-Overrides pro Vereinbarung über `legacy_conditions`
(`amount_cents`/`tax_cents`/`total_cents`/`interval`); ohne Override
Netto-Preis der `payment_option` + `billing.vat_rate`. Nicht abrechenbare
Fälle (fehlende Rechnungsadresse) werden geloggt und erneut versucht.
- **`User::hasActiveBooking()`** prüft jetzt echt (hinter
`billing.enforce_booking`): Cashier-Abo ∨ bezahlter Einzel-/Extra-PM-Kauf
∨ aktive/grandfathered Legacy-Vereinbarung (MAN-Kreis).
- **USt-Behandlung (Einwand 12.06.)**: Alle neuen Preise sind **netto**.
`VatResolver` bestimmt die Steuer pro Rechnung aus der Rechnungsadresse:
DE immer mit Steuer, EU nur mit (formal plausibler) USt-ID befreit
(Reverse Charge inkl. Pflichthinweis in `invoices.tax_note`), Drittland
befreit. `vat_id` an `billing_addresses` + Snapshot, gepflegt über das
bestehende USt-ID-Feld im Profil. Grandfathered-Vereinbarungen rechnen
auf der Netto-Basis der letzten Legacy-Rechnung (`net_cents`, brutto ÷
1,19 bzw. Netto-Ausweis direkt). **Offen**: echte VIES-Validierung der
USt-ID (aktuell Formatprüfung) — Folgeschritt, vor Gate-Aktivierung
empfohlen.
- **Legacy-Migration (12.06.)**: `legacy:grandfather-subscriptions` leitet
die aktiven, jährlich wiederkehrenden Vereinbarungen aus dem
Rechnungsarchiv ab und schreibt sie als `grandfathered` in
`user_payment_options` (Replay-fähig — die Kern-Migration läuft kurz
vor dem Relaunch erneut). Details:
`dev/migration 2026/05-DATABASE-MERGE.md` §5.6 + `MIGRATION-STEPS.md`.
- ~~Noch offen in 9D~~ → mit 9E erledigt: Slot-Logik auf Plan-Kontingent
umgestellt, Stub-Spalte entfernt.
### 9E · Stripe (Laravel Cashier) ✅ (12.06.2026)
- ✅ Produkt-Sync: `billing:sync-stripe-plans` legt Tarife (Monats-/Jahres-
preise) und das Einzel-PM-Produkt als Netto-Preise in Stripe an
(Test-Mode gelaufen; IDs in `plans` bzw. `STRIPE_PRICE_SINGLE_PM`).
- ✅ Webhooks: `ProcessStripeWebhook` spiegelt bezahlte Stripe-Rechnungen
mit STR-Nummer in `invoices` und erfüllt Einmalkäufe; Endpoint
`https://pressekonto.com/stripe/webhook` registriert, Secret gesetzt.
- ✅ Checkout-Flows (Backend): `me.checkout.subscription` +
`me.checkout.single-pm` (CheckoutController → StripeCheckoutService);
Stripe Tax via `Cashier::calculateTaxes()` (Netto-Preise). UI-Anbindung
der Buttons folgt in 9F.
- ✅ Slot-Logik: Plan-Kontingent + Einmalkauf-Verbrauch statt Stub;
**Grandfathered = unbegrenzt** (Entscheidung 12.06.2026, Bestandsschutz).
Details: `docs/user-admin/Billing-und-Rechnungskreise.md` §2.
- Offen → §7 der Billing-Doku: Stripe Tax im Dashboard aktivieren,
Live-Mode-Sync vor Relaunch.
### 9F · Tarif-Seite + Checkout-UI ✅ (12.06.2026)
- ✅ „Buchungen & Add-ons" zeigt das echte 4-Tier-Raster aus `plans`
(Monat/Jahr-Toggle, Jahrespreis als „2 Monate gratis") mit
Checkout-Buttons auf `me.checkout.subscription`; Einzel-PM als
separater No-Abo-Block (`me.checkout.single-pm`); Enterprise als
dezenter Hinweis unter dem Raster. Der Credit-Konzept-Mock ist
abgelöst (Credits → 9I bzw. Phase 2).
- ✅ Aktueller Tarif real: Abo (Preis, Kontingent, Kündigungsstatus),
Bestandstarif (unbegrenzt, nächste MAN-Rechnung) oder offene
Einzelkäufe; Kontingent-Kachel (`Unbegrenzt` bei Bestandsschutz).
- ✅ „Abo verwalten" → Stripe Billing Portal (`me.checkout.billing-portal`:
Zahlungsmethode, Rechnungen, Kündigung).
- ✅ Aktive Buchungen + Verlauf aus echten Daten (Abo, Legacy-Vereinbarung,
offene/eingelöste Einzelkäufe mit PM-Verknüpfung).
- Einstieg aus dem Submit-Gate-Hinweis (9C) führt bereits hierher.
### 9G · Tageslimit
- `plans.daily_limit` (Starter ohne Limit); Prüfung beim Veröffentlichen
(nicht beim Einreichen), zählt veröffentlichte PMs des Users pro Kalendertag
(Europe/Berlin); gilt auch für Extra-PMs. Überschreitung → PM bleibt in
`review` mit Hinweis, Veröffentlichung am Folgetag durch den Scheduler.
### 9H · Einzel-PM + Abo-Brücke
- Einzel-PM-Kauf 19 € → genau eine Einreichung/Veröffentlichung.
- Brücke: Abo-Abschluss innerhalb 30 Tagen rechnet 19 € auf den ersten
Monat an (Stripe-Coupon oder Rabatt-Position).
### 9I · Launch-Credits
- Credit-Wallet (1 Credit = 1 € Listenpreis, Pakete mit Volumenrabatt).
- Posten: **Extra-PM** (Kontingent voll → einzelne PM nachkaufen),
**Boost** (nur für grün klassifizierte PMs, nachträglich),
**Veröffentlichungsnachweis-PDF**.
- Kein Dofollow-Backlink-Verkauf (bewusst ausgeschlossen).
### 9J · Abschluss
- Volle Suite grün, Pint clean, `npm run build` clean.
- Doku-Sync: `STATUS-ABGLEICH`, `checkliste-user-backend.md`, dieses Dokument,
`PROGRESS.md`-Eintrag.
---
## 4. Was außerhalb von Phase 9 bleibt
- Vorab-KI-Prüfung, Redigieren/Re-Check-Loop, Prüfzähler, Credit-Overflow
(Decision-Update §7 — Phase 2)
- Score-Feinstufung für Boost („nur Geprüft/Hochwertig boostbar")
- Magic-Link-Flow, Statistik-/Abrechnungs-Tabs, Anhänge-Reaktivierung,
Trust-Score, Notice-and-Action
---
## 5. Risiken & Annahmen
- **Idempotenz Slot-Verbrauch (9B)**: Prüfung über Status-Logs statt neuem
Feld — bei Alt-Daten mit unvollständigen Logs schlimmstenfalls ein
doppelter Zähler; akzeptabel für den Stub, wird mit 9D-Periodenzähler
sauber.
- **Gate-Stub (9C)**: `enforce_booking=false` als Default hält das System
bis zum Tarif-Modul voll funktionsfähig; das Flag erlaubt Tests und
frühe Aktivierung.
- **9D/9E Datenmodell + Cashier**: größter Block, eigener Review-Stopp davor;
Stub-Ablösung (`press_release_quota`-Spalten entfernen) erst nach
verifizierter Migration.
- **Rechtstexte** (Einreichungs-Modal) sind weiterhin Platzhalter —
anwaltliche Prüfung läuft parallel, unabhängig von Phase 9.
- **Betrieb**: Queue-Worker für `classification` in Produktion bleibt
Go-Live-Voraussetzung (unabhängig von Phase 9).
---
## 6. Akzeptanzkriterien Phase 9 gesamt
- [ ] Gelb klassifizierte PMs gehen ohne manuelle Prüfung live (sofort/Termin)
- [ ] Rot verbraucht keinen Slot; Slot zählt genau einmal, bei Veröffentlichung
- [ ] Einreichen ohne aktive Buchung zeigt Buchungs-Hinweis (UI) und wird
serverseitig abgelehnt (Gate aktiviert)
- [ ] Tarife buchbar (4 Tiers, monatlich/jährlich), Einzel-PM kaufbar
- [ ] Tageslimit greift je Tier, auch für Extra-PMs
- [ ] Extra-PM, Boost (nur Grün) und PDF-Nachweis als Credits kaufbar
- [ ] Quota-Stub vollständig abgelöst, `pressReleaseQuotaRemaining()` stabil
- [ ] Tests grün, Pint clean, Build clean, Doku synchron

View file

@ -1,96 +0,0 @@
# `docs/` — Konzept- und Status-Dokumente
**Historischer Stand (12.06.2026, vor dem Go-Live am 20.07.2026).** Die
Statusangaben in diesem Ordner beschreiben den Planungsstand der Konzeptphase;
was seitdem umgesetzt wurde, steht nicht hier. Aktueller Einstieg für
Betrieb, Backlog und Deploy: [`dev/README.md`](../dev/README.md) mit
[`dev/roadmap.md`](../dev/roadmap.md) und `dev/2026-09-01/`.
Ursprünglicher Stand 12.06.2026 — Phase 8, die KI-Prüf-Pipeline (Phasen 0–5)
sowie aus Phase 9 die Stripe-Anbindung (9E), die Tarif-Seite (9F), das
Admin-Zahlungsmodul, die User-Panel-Restarbeiten und das
Verlinkungs-Decision-Update sind abgeschlossen. Damals nächste Blöcke: die
zwei Decision-Updates in `weiteres/` (Phase-2-Funktionen & Magic-Link,
Duplicate-Content), dazu 9G Tageslimit und der Login-/Registrierungs-Flow.
Diese README ist der schnellste Einstieg in den `docs/`-Ordner.
Sie verlinkt die zentralen Dokumente und sortiert sie nach „Was ist der aktuelle Stand?" vs. „Was ist konzeptueller Zielzustand?".
## Schneller Einstieg
| Frage | Doku |
|---|---|
| Was ist im Code, was ist Konzept, was fehlt? | [`STATUS-ABGLEICH-USER-PANEL.md`](./STATUS-ABGLEICH-USER-PANEL.md) |
| Was gilt für Preise, Kontingente und den Veröffentlichungs-Flow zum Launch? | [`Decision-Update Preisstruktur & Veröffentlichungs-Flow.md`](./Decision-Update%20Preisstruktur%20&%20Ver%C3%B6ffentlichungs-Flow.md) |
| Wie wird das umgesetzt (aktueller Plan)? | [`PHASE-9-FLOW-UND-TARIFE-PLAN.md`](./PHASE-9-FLOW-UND-TARIFE-PLAN.md) |
| Wie funktioniert die KI-Prüfung (Klassifikation, Score, Audit)? | [`user-admin/Entwicklungsplan KI-Pruefung und Veroeffentlichung.md`](./user-admin/Entwicklungsplan%20KI-Pruefung%20und%20Veroeffentlichung.md) |
| Was ist pro Phase erledigt, was offen? | [`user-admin/checkliste-user-backend.md`](./user-admin/checkliste-user-backend.md) |
| Welche Hub-Flux-Phasen sind durch? | [`../dev/archive/frontend/hub-flux/PROGRESS.md`](../dev/archive/frontend/hub-flux/PROGRESS.md) (Archiv) |
## Aufbau
### Top-Level — Status & Entscheidungen
- [`STATUS-ABGLEICH-USER-PANEL.md`](./STATUS-ABGLEICH-USER-PANEL.md) — Konzept-vs-Code-Vergleich pro Page. **Erste Anlaufstelle.**
- [`Decision-Update Preisstruktur & Veröffentlichungs-Flow.md`](./Decision-Update%20Preisstruktur%20&%20Ver%C3%B6ffentlichungs-Flow.md) — **Verbindlicher Launch-Stand** für Tarife, PM-Kontingente, Credits und Veröffentlichungs-Flow. Überschreibt die Tarif-Abschnitte (§8–10) in `Konzept-Update 1` und im Relaunch-Konzept.
- [`PHASE-9-FLOW-UND-TARIFE-PLAN.md`](./PHASE-9-FLOW-UND-TARIFE-PLAN.md) — **Aktueller Umsetzungsplan**: Veröffentlichungs-Flow (9A–9C) + Tarif-Modul (9D–9J).
- [`PHASE-8-USER-PANEL-PLAN.md`](./PHASE-8-USER-PANEL-PLAN.md) — Plan der Phase 8 (abgeschlossen 29.05.2026, als Referenz erhalten).
- [`Echte öffentliche Unterseiten.md`](./Echte%20%C3%B6ffentliche%20Unterseiten.md) — Sitemap-Konzept, jede Seite mit IST-Notiz.
- [`KI-UND-ENTWICKLER-WORKFLOW.md`](./KI-UND-ENTWICKLER-WORKFLOW.md) — Workflow für KI-/Entwickler-Sessions.
### `weiteres/` — Abgestimmte Decision-Updates (nächste Umsetzungsblöcke)
- [`Decision-Update Phase-2-Funktionen & Magic-Link-Änderungsprozess.md`](./weiteres/Decision-Update%20Phase-2-Funktionen%20&%20Magic-Link-%C3%84nderungsprozess.md) — Boost + Veröffentlichungsnachweis (Launch), Magic-Link-Zugangs-/Änderungsprozess, Phase-2-Funktionen (Vorab-Prüfung, Prüfzähler, kostenpflichtige Änderungspfade). **Noch nicht umgesetzt.**
- Das Decision-Update „Duplicate-Content & Duplicate-Checking" (12.06.2026, „nichts bauen, nur Canonical-Hygiene") ist überholt und entfernt (18.09.2026): Die API reicht dieselbe Meldung auf beiden Portalen ein, deshalb gibt es seit Anhang 2 der Roadmap (`dev/2026-09-01/roadmap-anhang2-2026.md`) Textprüfsumme, kanonische Zuordnung je Kategorie und Quellenauskunft.
- [`Detailplan Umsetzung Launch-Slice (Magic-Link, Compliance, Bestandsschutz, Auth).md`](./weiteres/Detailplan%20Umsetzung%20Launch-Slice%20%28Magic-Link%2C%20Compliance%2C%20Bestandsschutz%2C%20Auth%29.md) — konkreter Umsetzungsplan (WS-1 bis WS-8) aus den beiden Decision-Updates + Merkliste; verbindliche Entscheidungen vom 15.06.2026 (Legacy-Posten bleiben, Pflicht-E-Mail-Verifizierung, Socialite freigegeben). **In Umsetzung** (WS-1, WS-6 ohne Google, WS-2 erledigt).
- [`Sicherheit & Deployment-Hinweise (Auth, Rollen, Verifizierung).md`](./weiteres/Sicherheit%20%26%20Deployment-Hinweise%20%28Auth%2C%20Rollen%2C%20Verifizierung%29.md) — **deployment-kritisch:** Reihenfolge der Migrationen (Verifizierungs-Backfill, Legacy-Rollen-Downgrade), der Sicherheits-Fix der Editor-Überberechtigung, Firmen-Scope-Verhaltensänderung und der Magic-Link-Kontaktzugang. Feed für die Live-Deployment-Checkliste (WS-7).
- [`Decision-Update Verlinkung & Backlinks in Pressemitteilungen.md`](./weiteres/Decision-Update%20Verlinkung%20&%20Backlinks%20in%20Pressemitteilungen.md) — systemseitige `rel`-Auszeichnung (`sponsored nofollow`), kein Verkauf von Dofollow-Backlinks. **Umgesetzt** (`PressReleaseLinkPolicy`, 12.06.2026); Kurzfassung in §4 des Preisstruktur-Decision-Updates. Als Referenz wieder abgelegt (18.09.2026), weil die Transparenzseite „kostenlos veröffentlichen" die Link-Policy erklärt.
### `user-admin/` — User-/Admin-Backend
Konzept und Status-Dokumentation für das User- und Admin-Backend.
- [`Admin-User.md`](./user-admin/Admin-User.md) — Hauptdokument zum User-/Admin-Backend (Navigation, Firmen-Detail, Rollen).
- [`Billing-und-Rechnungskreise.md`](./user-admin/Billing-und-Rechnungskreise.md) — **Zentrale Billing-Referenz**: hybride Rechnungskreise (STR-/MAN-/Archiv), Tarif-Datenmodell, USt-Regeln, Befehle, Konfiguration.
- [`checkliste-user-backend.md`](./user-admin/checkliste-user-backend.md) — Erledigt/Offen-Liste pro Phase (1, 7, 8, KI-Pipeline).
- [`Entwicklungsplan KI-Pruefung und Veroeffentlichung.md`](./user-admin/Entwicklungsplan%20KI-Pruefung%20und%20Veroeffentlichung.md) — KI-Klassifikation (Rot/Gelb/Grün), Content-Score, Audit-Log; Phasen 0–5 umgesetzt (11.06.2026).
- [`Umsetzung Pressemitteilung Bearbeitung Titelbild Veroeffentlichung.md`](./user-admin/Umsetzung%20Pressemitteilung%20Bearbeitung%20Titelbild%20Veroeffentlichung.md) — Umsetzungs-Notiz (11.06.2026): Titelbild/Cover, Lizenzformular, Zeitzonen-Handling, vereinfachte Veröffentlichungs-Box.
- [`Lizenztyp Bildupload.md`](./user-admin/Lizenztyp%20Bildupload.md) — Fachvorgabe für die Lizenz-/Rechtefelder beim Bildupload (umgesetzt).
- [`user-zusammenhaenge.md`](./user-admin/user-zusammenhaenge.md) — Datenmodell-Mapping, Models, Services und Commands.
- [`Presseportal – Konzept für Relaunch.md`](./user-admin/Presseportal%20%E2%80%93%20Konzept%20f%C3%BCr%20Relaunch.md) — Zielzustand der Plattform (KI-Workflow, Bilder, Notice-and-Action, DSGVO, Magic-Link, Tarife, Korrektur-Modell). Jeder Abschnitt hat eine **IST-Stand-Box**; die Tarif-Abschnitte sind durch das Decision-Update überschrieben.
### `konzept/` — Strategie & Marke
Strategische Konzepte und Updates. Sie beschreiben Themen, die teilweise oder noch gar nicht gebaut sind. Jedes Update hat oben einen IST-Stand-Hinweis.
- [`Konzept Presseportal – Marktposition & Hebel.md`](./konzept/Konzept%20Presseportal%20%E2%80%93%20Marktposition%20&%20Hebel.md) — Marktanalyse und Positionierung (Anti-Zombie-Linie).
- [`Entwicklungs-Konzept - Frontend-Komponenten Multi-Brand.md`](./konzept/Entwicklungs-Konzept%20-%20Frontend-Komponenten%20Multi-Brand.md) — Multi-Brand-Architektur (umgesetzt).
- [`Konzept-Update 1 – Überarbeitete Abschnitte.md`](./konzept/Konzept-Update%201%20%E2%80%93%20%C3%9Cberarbeitete%20Abschnitte.md) — Tarife/Credits (§8–10 **überschrieben durch Decision-Update**), Score-Architektur §15 (Klassifikation + Content-Score umgesetzt, Trust-Score offen), Boost §16, Tool-Loop §17.
- [`Konzept-Update 2 – Score-Stufen-System.md`](./konzept/Konzept-Update%202%20%E2%80%93%20Score-Stufen-System.md) — Drei-Stufen-Score (Backend umgesetzt; öffentliche Badges im Web-Frontend offen).
- [`Konzept-Update 3 – Multi-Brand-Architektur (Hub & Spoke).md`](./konzept/Konzept-Update%203%20%E2%80%93%20Multi-Brand-Architektur%20(Hub%20&%20Spoke).md) — Hub-&-Spoke-Markenarchitektur.
- [`Konzept-Update 4 - Positionierung + Markenversprechen.md`](./konzept/Konzept-Update%204%20-%20Positionierung%20+%20%20Markenversprechen.md) — Positionierung und Markenversprechen.
- [`Konzept-X - Brand-Landing.md`](./konzept/Konzept-X%20-%20Brand-Landing.md) — Brand-Landing-Konzept.
## Lesehilfe
In den überarbeiteten Dokumenten finden sich folgende Markierungen:
| Marker | Bedeutung |
|---|---|
| **IST-Stand JJJJ-MM-TT** | Kompakte Notiz oben am Abschnitt, was im Code tatsächlich umgesetzt ist. |
| Phase 1 / Phase 7 / Phase 8 | Abgeschlossene Roadmap-Blöcke: Grund-User-Backend, PM-Form-Refactor, User-Panel-Konsolidierung. |
| KI-Pipeline (Phasen 0–5) | Klassifikation, Routing, Content-Score — abgeschlossen 11.06.2026, siehe Entwicklungsplan. |
| Hub-Flux | Visuelle Migrationsphase des User Backends, gepflegt in `dev/archive/frontend/hub-flux/` (Archiv). |
| Phase 2 / Phase 3 | Später — Magic-Link-Flow, Re-Check/Redigieren, Trust-Score, Notice-and-Action, Statistik. |
| Launch-Block (offen) | Zahlung/Tarife, Submit-Gate, Slot-Verbrauch bei Veröffentlichung — siehe Decision-Update. |
## Wie pflegen wir die Doku?
- Wenn sich der Code so weit ändert, dass ein Konzept-Abschnitt nicht mehr stimmt, kommt eine **IST-Stand-Box** an den Abschnitt, statt den Konzept-Text zu löschen. So bleibt die ursprüngliche Zielvorstellung lesbar.
- Entscheidungen, die frühere Konzept-Festlegungen ersetzen, kommen als eigenes **Decision-Update** auf Top-Level; die überschriebenen Abschnitte bekommen einen Verweis darauf.
- Jeder Phasen-Abschluss aktualisiert
- `user-admin/checkliste-user-backend.md` (Erledigt-Block),
- `STATUS-ABGLEICH-USER-PANEL.md` (Abgleich),
- `dev/archive/frontend/hub-flux/PROGRESS.md` (Tagebuch, Archiv),
- und ggf. die Detail-Doku in `dev/archive/frontend/hub-flux/`.
- Neue große Themen bekommen ein eigenes Plan-Dokument auf `docs/`-Top-Level (z. B. `PHASE-8-USER-PANEL-PLAN.md`).

View file

@ -1,342 +0,0 @@
# Status-Abgleich · User Panel
Stand: 2026-06-11 (Phase 8 vollständig abgeschlossen; KI-Prüf-Pipeline
Phasen 0–5 umgesetzt; Titelbild-/Lizenz-/Zeitzonen-Umbau vom 10./11.06.
eingearbeitet. Preise & Veröffentlichungs-Flow: siehe
[`Decision-Update Preisstruktur & Veröffentlichungs-Flow.md`](./Decision-Update%20Preisstruktur%20&%20Ver%C3%B6ffentlichungs-Flow.md))
> Dieses Dokument vergleicht die Konzept-Dokumente im Ordner `docs/` mit dem
> tatsächlichen Code-Stand. Es dient als Single Source of Truth für die
> Entscheidung, welche Konzept-Abschnitte aktualisiert werden müssen und wo
> weiterhin Lücken bestehen.
>
> **Methode**: Code-Inspektion aller Customer-Komponenten in
> `resources/views/livewire/customer/`, Models, Migrationen und Services
> gegen die Inhalte der Doku in `docs/user-admin/` und `docs/konzept/`.
>
> **Lese-Hilfe**:
>
> - ✅ **Doku stimmt mit Code überein** — kein Handlungsbedarf
> - 🔄 **Doku ist überholt** — Code ist schon weiter, Doku muss nachgezogen werden
> - 📝 **Code ist hinter Doku** — Konzept beschreibt etwas, das noch nicht
> gebaut ist
> - ⚠️ **Inkonsistent** — Doku und Code widersprechen sich
> - ❓ **Nicht im Konzept** — im Code da, aber nirgends dokumentiert
---
## 1. Globale Architektur des User Backends
| Konzept-Aussage | Quelle | IST im Code | Status |
|---|---|---|---|
| User/Admin getrennt, technisch gemeinsames Backend, Trennung über Rollen/Policies | `Admin-User.md` | Umgesetzt: `PressReleasePolicy`, Spatie-Rollen, Customer-/Admin-Routen | ✅ |
| Navigation in „Mein Bereich · Finanzen · Konto" | `Admin-User.md`, `checkliste-user-backend.md` | `components/layouts/app/sidebar.blade.php` setzt die drei Gruppen | ✅ |
| Topbar oben rechts mit Firmen-Kontext-Switcher | `Admin-User.md` | `customer/company-switcher.blade.php` + Layout-Integration | ✅ |
| „Pressemappen" terminologisch auf „Firmen" umbenannt | `checkliste-user-backend.md` | Im UI durchgehend „Firmen" / „Meine Firmen"; Routen heißen aus Legacy-Gründen weiterhin `me.press-kits.*` | 🔄 (Doku-Nacharbeit: Hinweis ergänzen, dass die internen Route-Namen weiterhin `press-kits` heißen) |
| Phase 1 funktional abgeschlossen | `checkliste-user-backend.md` Z. 71 | Trifft zu — alle in „Erledigt" markierten Punkte sind im Code verifizierbar | ✅ |
---
## 2. Customer-Pages — IST-Stand pro Page
### Dashboard (`customer/dashboard.blade.php`)
| Konzept-Aussage | IST | Status |
|---|---|---|
| Datenqualitäts-Hinweise (Profil, Rechnungsadresse, Pressekontakte, PMs ohne Firma) | umgesetzt mit `<x-portal.hint-card>` | ✅ |
| KPI-Reihe Pressemitteilungen | umgesetzt mit `<x-portal.stat-card>`, Trend-Slot mit `pub/prüf/entwurf` | ✅ |
| Filter-Reaktion auf Firmen-Kontext | `recent` und `companies` queries respektieren `selectedCompany` | ✅ |
### Pressemitteilungen-Liste (`customer/press-releases/index.blade.php`)
| Konzept-Aussage | IST | Status |
|---|---|---|
| Status-Tabs (Alle/Veröffentlicht/Entwürfe/Prüfung/Abgelehnt/Archiv) | umgesetzt als `view-tabs` mit Counter-Pillen | ✅ |
| Filter ohne Firma, Status, Portal | `statusFilter`, `portalFilter`, `companyFilter` aktiv | ✅ |
| Filter-Presets (`user_filter_presets`) | **fehlt** | 📝 (in Phase 2 lt. Doku — bleibt pending) |
| PM-Detail Tab „Verlauf" aus `press_release_status_logs` | als „Status & Verlauf"-Card eingebaut, nicht als eigener Tab | 🔄 (Doku-Anpassung: Card statt Tab; funktional gleichwertig) |
| Hinweis Scheduling/Embargo in der Liste | umgesetzt (Sub-Label „geplant · …" / „Embargo bis …" in der Datums-Spalte, `index.blade.php` Z. 505–514) | ✅ (Phase 8B) |
### Pressemitteilungs-Forms
| Konzept-Aussage | IST | Status |
|---|---|---|
| Einfacher Editor mit Absätzen, fett + kursiv | Flux-Editor mit `heading | bold italic | bullet ordered blockquote | link` — mehr als minimal | 🔄 (Konzept-Update: Aktuelle Toolbar ist bewusst etwas größer als „nur fett + kursiv") |
| Pflichtfeld `company_id` für Customer | Validation `required` | ✅ |
| Portal aus Firma abgeleitet (Customer) | `updatedCompanyId()` setzt `portal` aus `company->portal` | ✅ |
| `subtitle`-Feld | seit Phase 7 da | ❓ (im Konzept nicht erwähnt, aber sinnvoll) |
| `scheduled_at`, `embargo_at`-Felder im Form | `scheduled_at` da (Datum via `flux:date-picker` + Uhrzeit via `flux:time-picker`, Eingabe/Anzeige in Europe/Berlin, Speicherung UTC). **Embargo aus der Form-UI entfernt** (11.06.) — `embargo_at` bleibt im Schema, wird beim Speichern auf `null` geführt | 🔄 (bewusste Vereinfachung; Doku: `Umsetzung Pressemitteilung Bearbeitung Titelbild Veroeffentlichung.md`) |
| Titelbild pro PM | ein einzelnes Cover-Bild (1280×580) oder SVG-Platzhalter; Upload-Form einklappbar, Platzhalter-Picker | ✅ (Phase 8F/8G + Umbau 11.06.) |
| Einreichungs-Modal in allen Customer-Ansichten | `confirm-submit-review`-Modal in Show, Create **und** Edit (KI-Plan Phase 0) | ✅ |
| HTML-Sanitizer auf Save | `PressReleaseHtmlSanitizer` (mews/purifier) | ❓ (Konzept-Punkt 2/Bilder nennt KI-Check, aber keinen HTML-Sanitizer — sollte dokumentiert werden) |
| Boilerplate-Override pro PM | seit Phase 7 als optionaler Override-Text | ❓ (im Konzept nicht erwähnt) |
| Pressekontakt-Zuordnung Single-Select (1 pro PM, n:m beibehalten) | seit Phase 7, jetzt optional; Warn-Box in der Sidebar-Card, wenn kein Kontakt gewaehlt (`create`/`edit.blade.php`) | ✅ (Phase 8C; Konzept-Punkt: war ursprünglich „mehrere möglich", jetzt 1 pro PM optional) |
| Attachment-Manager | **temporär deaktiviert wegen Security-Review** | ⚠️ (Konzept beschreibt Anhänge, Code hat es auskommentiert) |
### Pressemitteilungs-Detail (Customer Show)
| Konzept-Aussage | IST | Status |
|---|---|---|
| Status & Verlauf inkl. Logs | umgesetzt | ✅ |
| Zugeordnete Pressekontakte | umgesetzt | ✅ |
| Rejection-Begründung sichtbar | umgesetzt | ✅ |
| Vorschau-Link für externe Reviewer | `generateShareLink` via Magic-Link-Token | ✅ |
| Anzeige Subtitle / `scheduled_at` / `embargo_at` / `boilerplate_override` / `no_export` | umgesetzt — Subtitle unter H1, Scheduling/Embargo als Cards im „Status & Verlauf"-Block, Boilerplate-Override als eigene Card (`customer/press-releases/show.blade.php`) | ✅ (Phase 8A; Admin-Show analog) |
### Firmen-Liste (`customer/press-kits/index.blade.php`)
| Konzept-Aussage / Mockup | IST | Status |
|---|---|---|
| Karten-Grid pro Firma mit Logo, Status, Portal, Rolle, KPIs | umgesetzt auf Mockup-Niveau (`.firm-card`, deterministische Logo-Varianten, Status-Badge, Portal-Pills, Rolle-Pill, KPIs) | ✅ (Phase 8E) |
| Counter-Strip (X Firmen, X aktiv, X PMs total, X Kontakte) | umgesetzt | ✅ (Phase 8E) |
| Saved-View-Tabs (Alle / Aktiv / In Anlage / Inaktiv / Mit mir geteilt) | umgesetzt mit Live-Counts; „In Anlage" bewusst noch leer (Phase-2-Heuristik) | ✅ (Phase 8E) |
| Filter-Chips (Status / Portal / Rolle / Branche) | Portal + Rolle via FluxUI-Dropdown + URL-Sync umgesetzt | ✅ (Phase 8E) |
| Seg-Toggle Karten/Liste | umgesetzt (`?mode=list`) | ✅ (Phase 8E) |
| Empty-States (keine Firma / Filter ohne Treffer) | umgesetzt (3-Schritt-Onboarding + Reset-CTA) | ✅ (Phase 8E) |
| Rollen-Legende (Admin / Redakteur / Beobachter) | umgesetzt als `panel-warm` | ✅ (Phase 8E) |
| „Firma anlegen"-CTA | zeigt „Firma anlegen anfragen" → Profil (Add-Tile); Self-Service-Anlage bleibt Phase-2-Thema | 🔄 (bewusst, Firma-Self-Service ist Phase 2) |
### Firmen-Detail (`customer/press-kits/show.blade.php`)
| Konzept-Aussage | IST | Status |
|---|---|---|
| Tabs Übersicht / Stammdaten / Pressekontakte / PMs / Statistik / Abrechnung | **eine lange Seite mit Quick-Nav-Anker, keine echten Tabs** | 🔄 (Doku-Update: Quick-Nav statt Tabs; visuell gleichwertig) |
| Stammdaten inkl. Logo bearbeitbar | umgesetzt | ✅ |
| Pressekontakte verwalten | umgesetzt | ✅ |
| Eigentümer-Anzeige konsolidiert aus `owner_user_id` + `company_user.role` | umgesetzt | ✅ |
| Statistik-Tab | nur Stub („In Vorbereitung") | 📝 (Phase 2) |
| Abrechnung-Tab | nur Stub | 📝 (Phase 2) |
| Magic-Link aktiv/inaktiv-Badge pro Kontakt | **fehlt** | 📝 |
| Anzahl PMs pro Kontakt aus `press_release_contact` | **fehlt** | 📝 |
| „+ Neuer Pressekontakt" mit Magic-Link-Berechtigung-Toggle | nur Basis-Form, kein Magic-Link-Toggle | 📝 (gehört zu Magic-Link-Flow → Phase 2) |
### Settings (`settings/*`, `customer/profile`, `customer/security`)
| Konzept-Aussage | IST | Status |
|---|---|---|
| Profil + Rechnungsadresse + Sicherheit + Newsletter + API-Tokens | alles vorhanden | ✅ |
| Magic-Link-Verlauf in Sicherheit | **fehlt** | 📝 (Phase 2) |
| API-Nutzungs-Log | **fehlt** | 📝 (Phase 2) |
| Team-Tab (Agency-Tarif) | **fehlt** | 📝 (Phase 2) |
### Finanzen
Zentrale Billing-Referenz: [`user-admin/Billing-und-Rechnungskreise.md`](./user-admin/Billing-und-Rechnungskreise.md).
| Konzept-Aussage | IST | Status |
|---|---|---|
| Rechnungen mit Legacy-Archiv | umgesetzt | ✅ |
| Hybride Rechnungskreise STR-/MAN- (Decision 12.06.) | umgesetzt (Phase 9D) — Nummern-Generator, MAN-Fälligkeitslauf, Grandfather-Migration, USt-Logik (`VatResolver`) | ✅ |
| Tarif-Datenmodell + Cashier | umgesetzt (Phase 9D) — `plans`, `single_purchases`, `User` ist Billable | ✅ |
| Stripe-Checkout/Webhooks + STR-Spiegelung | umgesetzt (Phase 9E) — Produkt-Sync, Webhook-Verarbeitung, Checkout-Backend, Plan-Kontingent | ✅ |
| Buchungen & Add-ons (UI) | umgesetzt (Phase 9F) — Tarif-Raster, Einzel-PM-Block, Bestandstarife, Billing Portal | ✅ |
| Zahlungsmethoden firmenscharf | **fehlt** | 📝 (Phase 2) |
---
## 3. Großthemen aus dem Konzept — Status
### 3.1 KI-Freigabe-Workflow
**Konzept-Stand**: `Presseportal – Konzept für Relaunch.md` Abschnitt 1 + §15.
**Umsetzungs-Doku**: `user-admin/Entwicklungsplan KI-Pruefung und Veroeffentlichung.md` (Phasen 0–5 ✅, 11.06.2026).
| Punkt | Code-Stand |
|---|---|
| KI-Prüfung mit JSON-Antwort | **umgesetzt** — `ClassifyPressRelease`-Job (Queue `classification`), OpenAI-Treiber + deterministischer Fallback, provider-agnostische Architektur unter `app/Services/PressRelease/Classification/` |
| Drei-Stufen-Ergebnis grün/gelb/rot | **umgesetzt** — `press_releases.classification`; Routing (seit Phase 9A, Entscheidung 12.06.2026): Rot → `rejected` + Mail, **Gelb/Grün → Auto-Publish** (sofort/zum Termin); unklassifizierte PMs bleiben als Fallback in der manuellen Queue |
| Logging der KI-Antworten | **umgesetzt** — `ki_audits`-Tabelle (append-only, inkl. Provider/Modell/Begründung/Raw-Response) |
| Content-Score 0–100 → Stufe | **umgesetzt** — `content_score`/`content_tier` (`ScorePressRelease`-Job), Editor-Panel, Admin-Badges, öffentliches Stufen-Badge in Customer-Show |
| Re-Klassifikation bei Änderung | **umgesetzt** — `reclassifyIfClassified()`/`rescoreIfScored()` bei Titel-/Text-Änderung (Customer, Admin, API) |
| Admin-On-Demand-Prüfung | **umgesetzt** — „Prüfung"-Button + Modal im Admin-Editor (Re-Check ohne Statusänderung) |
| API-Absicherung | **umgesetzt** — API kann `status` nicht mehr setzen; eigene Submit-Route läuft durch denselben Funnel |
| Trust-Score | **fehlt** (Phase 3 / KI-Plan Phase 6) |
| Blacklist-Wort-Check | **vorhanden** als synchroner Hard-Filter vor der KI-Klassifikation |
**Bewertung**: ✅ — Kern-Pipeline produktionsbereit. ⚠️ Betriebs-Voraussetzung:
Queue-Worker für `classification` in Produktion (Test-Drain:
`php artisan classification:work`). 📝 verbleibend: Trust-Score, Live-Update
des Ergebnisses in der UI (aktuell erst nach Reload sichtbar), Stufen-Badges
im öffentlichen Web-Frontend.
### 3.2 Bilder & Lizenzen
**Konzept-Stand**: `Presseportal – Konzept für Relaunch.md` Abschnitt 2 + `Admin-User.md` Punkt 4.
| Punkt | Code-Stand |
|---|---|
| Upload-Workflow (Eigenes / Stock / KI) | Nur „Eigenes Bild" via `press-release-images-manager`; auf **ein Titelbild pro PM** begrenzt, Cover-Variante 1280×580, Original wird nach Variantenerzeugung gelöscht (Stock/KI weiterhin offen) |
| Pflichtfelder (Urheber, Lizenztyp, Lizenz-URL, Personen-Einwilligung, Rechte-Bestätigung) | umgesetzt (Phase 8H, erweitert 10./11.06. nach `Lizenztyp Bildupload.md`) — `author`, `license_type` (7 Typen, `App\Enums\ImageLicenseType`), `license_detail`, `license_url` (bedingt Pflicht), `source_url`, `people_rights_status`, `property_rights_status`, `rights_notes`, `rights_confirmed_at`; Risikohinweise bei unklaren Lizenzfällen |
| KI-Wasserzeichen-Check | **fehlt** |
| Unsplash/Pexels-API | **fehlt** |
| KI-Bildgenerierung | **fehlt** |
| `is_preview`-Flag für Titelbild | im Modell vorhanden, im Manager toggelbar |
| Bild-Varianten (thumb/medium/large) | `ImageService::PRESS_RELEASE_IMAGE_VARIANTS` generiert sie automatisch |
| SVG-Platzhalter, falls keine Bilder | umgesetzt (Phase 8F/8G) — zentrales Set in `public/images/press-release-placeholders/`, `App\Enums\PressReleasePlaceholder`, `PressReleaseCoverImage`-Resolver, Hero in Customer-/Admin-Show |
**Bewertung**: ✅ für Lizenzfelder + SVG-Platzhalter (Phase 8). 📝 verbleibend: Stock-/KI-Quellen, Wasserzeichen-Check (Phase 2/3).
### 3.3 Notice-and-Action (Meldung durch Dritte)
**Konzept-Stand**: `Presseportal – Konzept für Relaunch.md` Abschnitt 3.
| Punkt | Code-Stand |
|---|---|
| Öffentliches Melden-Formular | **fehlt** |
| Ticketsystem mit Kategorien (Urheberrecht, Persönlichkeitsrecht, …) | **fehlt** |
| KI-Triage | **fehlt** |
| Quarantäne-Flow | **fehlt** |
**Bewertung**: 📝 — Phase 2/3-Thema, im Konzept gut beschrieben.
### 3.4 Magic-Link-Flow für Pressekontakte
**Konzept-Stand**: `Presseportal – Konzept für Relaunch.md` Abschnitt 6.
| Punkt | Code-Stand |
|---|---|
| `magic_links`-Tabelle | **vorhanden** |
| Magic-Link-Generator | `MagicLinkGenerator` existiert (wird für PM-Vorschau-Links genutzt) |
| Magic-Link für Pressekontakt-Zugang | **fehlt** als eigener Flow |
| Token-Tabelle `press_release_access_requests` o. ä. | **fehlt** |
| Änderungs-Wizard (Tippfehler/Daten/Korrektur/Update/DSGVO) | **fehlt** |
**Bewertung**: 📝 — Phase 2-Thema, vollständig im Konzept beschrieben.
### 3.5 Pricing / Tarife / Credits
**Verbindlicher Konzept-Stand**: [`Decision-Update Preisstruktur & Veröffentlichungs-Flow.md`](./Decision-Update%20Preisstruktur%20&%20Ver%C3%B6ffentlichungs-Flow.md)
(11.06.2026 — überschreibt §8–10 in `Konzept-Update 1` und im Relaunch-Konzept).
| Punkt (laut Decision-Update) | Code-Stand |
|---|---|
| Tarif-Raster Starter/Business/Pro/Agency (29/49/99/199 €, 3/10/25/60 PMs) | **nicht im Datenmodell** |
| Einzel-PM 19 € (No-Abo-Block) + Einzel→Abo-Brücke | **fehlt** |
| Zahlung/Checkout (Stripe) | **Backend umgesetzt** (Phase 9E) — Checkout-Routen, Webhooks, STR-Spiegelung; UI folgt mit 9F |
| Slot-Verbrauch **bei Veröffentlichung** (Rot = kein Slot) | **umgesetzt** (Phase 9B) — zählt idempotent beim ersten `published`-Übergang; Einreichen erfordert freien Slot, verbraucht aber keinen |
| Submit-Gate: „Zur Prüfung einreichen" gegated hinter Buchung | **vorbereitet** (Phase 9C) — `User::hasActiveBooking()`-Stub hinter `billing.enforce_booking` (Default aus), Modal-Hinweis + Server-Guard + API 402; echte Buchungs-Prüfung seit 9D/9E (Abo ∨ Einmalkauf ∨ Legacy-Vereinbarung) |
| Tageslimit (Business 2 / Pro 3 / Agency 5) | **fehlt** |
| Launch-Credits: Extra-PM, Boost (nur grün), Veröffentlichungsnachweis-PDF | **fehlt** |
| Jahrespreis als „2 Monate gratis" | Kommunikations-Regel, greift mit Tarif-UI |
| `user_payment_options`-Tabelle | **vorhanden** (Pivot zu Companies da, aber kein aktiver Flow) |
**Bewertung**: 📝 — der **Launch-Block** und damit das größte ungebaute Feature.
Vorhandene Anschlusspunkte: Plan-Kontingent (`pressReleaseQuotaRemaining()`,
null = unbegrenzt/Bestandsschutz), Veröffentlichungs-Modal (zeigt Kontingent),
KI-Klassifikation (liefert das Rot/Gelb/Grün für den Slot-Verbrauch).
Bewusst **nicht** zum Launch: Re-Check-Loop, Vorab-Prüfung, Prüfzähler
(alles Phase 2, siehe Decision-Update §7).
### 3.6 Korrektur-Modell & Tombstones
**Konzept-Stand**: `Presseportal – Konzept für Relaunch.md` Abschnitt 4.
| Punkt | Code-Stand |
|---|---|
| Korrektur-Hinweis | **fehlt** |
| Update-Hinweis (am Ende anhängen) | **fehlt** |
| Anonymisierung (DSGVO) | **fehlt** |
| Tombstone statt Hard-Delete | `PressReleaseService::deleteFromAdmin()` setzt veröffentlichte PMs auf „archiviert" mit Ersatztext — **rudimentär da** |
| Textvorlagen admin-pflegbar | **fehlt** |
**Bewertung**: 🔄 — Tombstone-Variante existiert minimal; Konzept-Doku sollte den Ist-Stand notieren, der Rest ist Phase 2.
### 3.7 Score / Trust-Score (Konzept-Update 2)
**Konzept-Stand**: `Konzept-Update 2 – Score-Stufen-System.md`.
| Punkt | Code-Stand |
|---|---|
| Content-Score 0–100 → Stufe (Standard/Geprüft/Hochwertig) | **umgesetzt** (KI-Plan Phase 5) — `content_score`/`content_tier`, Schwellen kalibrierbar in `config/scoring.php` (Geprüft ≥ 60, Hochwertig ≥ 80) |
| Stufen-Badges im Backend + Customer-Ansicht | **umgesetzt** (Standard wird laut Konzept nicht beworben) |
| Stufen-Badges im öffentlichen Web-Frontend | **fehlt** (Folgearbeit) |
| Trust-Score (User-/Firmen-Ebene) | **fehlt** (Phase 3 / KI-Plan Phase 6) |
| Auto-Publishing in Abhängigkeit vom Score | Klassifikations-basiert umgesetzt (Grün → Auto-Publish); Trust-Score-Lockerung fehlt |
| Boost-Eligibilität nach Stufe | **fehlt** — zum Launch gilt laut Decision-Update: Boost nur für **grüne** PMs, Score-Feinstufung ist Phase 2 |
**Bewertung**: ✅ Content-Score-Kern da; 📝 Trust-Score + öffentliche Badges + Boost-Stufung.
---
## 4. Was im Code da ist, aber im Konzept nicht / nur am Rande steht
| Feature | Wo im Code | Doku-Nacharbeit |
|---|---|---|
| Phase-7-Schema-Erweiterungen (`press_releases.subtitle`, `scheduled_at`, `embargo_at`, `boilerplate_override`, `no_export`) | Migrationen `2026_05_20_*` | Im Konzept ergänzen, dass PMs Untertitel + Scheduling/Embargo unterstützen |
| `mews/purifier` für HTML-Sanitization | `PressReleaseHtmlSanitizer` | Im Konzept-Abschnitt zu Editor erwähnen |
| `press_release_attachments`-Tabelle + Model | Migration `2026_05_20_143424_*` | UI auskommentiert, Tabelle bleibt → Doku-Anker für spätere Reaktivierung |
| Background-Job für scheduled publishing | `app/Console/Commands/PublishScheduledPressReleases.php`, alle 5 Min via Scheduler; publiziert seit der KI-Anbindung nur noch **grün klassifizierte** fällige PMs | Im Konzept als „automatische Veröffentlichung zum geplanten Termin" hinzufügen |
| Zeitzonen-Handling für geplante Termine | `PressRelease::DISPLAY_TIMEZONE` (Europe/Berlin), `scheduledAtLocal()`/`embargoAtLocal()`; Eingabe Berlin, Speicherung UTC | dokumentiert in `Umsetzung Pressemitteilung Bearbeitung Titelbild Veroeffentlichung.md`; `published_at`/`created_at` weiterhin UTC-Anzeige (Folgeschritt) |
| Monatlicher Quota-Reset | `press-releases:reset-monthly-quota` (Scheduler, 1. des Monats) | Setzt den Plan-Kontingent-Zähler zurück (seit 9E) |
| FluxUI Toast für UX-Feedback | `Flux::toast()` durchgehend in Customer-Forms | Konzept-übergreifend, kein Konzept-Update nötig |
| Smooth-Scroll zu Validation-Errors | `resources/js/portal-form-hooks.js` | UX-Detail, keine Konzept-Doku |
| Pre-Submit-Check-Liste in PM-Forms | computed `presubmitChecks` | Im Konzept als „Pre-Submit-Check senkt Support-Aufwand" ergänzen |
| Hub-Design-System (Tokens + Komponenten) | `dev/frontend/hub-flux/` (Phase 0–7) | Eigene Roadmap-Doku, nicht teil von `docs/` |
| Theme-Override pro Domain | `ThemeServiceProvider` + `config/domains.php` | In `Echte öffentliche Unterseiten.md` ergänzen |
| Public-Detail-Page (`web/release-detail.blade.php`) | umgesetzt | In `Echte öffentliche Unterseiten.md` als „existiert" markieren |
---
## 5. Offene Punkte aus dem letzten Code-Review
Diese Punkte habe ich beim Review der Phase-7-Forms gefunden, sie sind weder
in den Konzept-Dokumenten erfasst noch in einem Plan:
| Lücke | Betroffene Dateien | Status |
|---|---|---|
| Customer-Show zeigt weder `subtitle` noch `scheduled_at`/`embargo_at`/`boilerplate_override` | `customer/press-releases/show.blade.php` | ✅ erledigt (8A) |
| Admin-Show zeigt weder `subtitle` noch `boilerplate_override` | `admin/press-releases/show.blade.php` | ✅ erledigt (8A) |
| Liste-Indikator für Scheduling/Embargo | `customer/press-releases/index.blade.php`, `admin/press-releases/index.blade.php` | ✅ erledigt (8B) |
| Pressekontakt-Sidebar zeigt keine Warn-Box, wenn kein Kontakt gewählt | `customer/press-releases/create.blade.php`, `edit.blade.php` | ✅ erledigt (8C) |
| Anhang-Tests laufen ins Leere | `tests/Feature/PressReleaseAttachmentsManagerTest.php`, Teile von `PressReleasePhase7SchemaTest.php` | ✅ via `->skip(...)` mit Verweis auf Security-Review |
| Roadmap-Doku `19-PHASE-7-PRESS-RELEASE-FORM.md` ist nicht mehr aktuell | Letzte 3 große Änderungen fehlen | offen → wird im Phase-8-Abschluss (8K) als `20-PHASE-8-…md` dokumentiert |
---
## 6. Empfehlungen zur Pflege der Doku
### 6.1 Sofort ohne Risiko machbar
1. In `Admin-User.md` ergänzen: „PMs unterstützen Untertitel, Scheduling und
Embargo seit Phase 7".
2. In `Presseportal – Konzept für Relaunch.md` Abschnitt 1: aktuellen
Blacklist-Stand notieren („KI-Vorprüfung folgt; aktuell wird per
Blacklist gegen offensichtliche Verstöße geprüft").
3. In `Presseportal – Konzept für Relaunch.md` Abschnitt 2: hinzufügen,
dass Bilder aktuell nur als „Eigenes Bild" hochgeladen werden können,
Stock- und KI-Quellen folgen.
4. In `Presseportal – Konzept für Relaunch.md` Abschnitt 4: notieren, dass
Tombstone-Variante rudimentär da ist (`deleteFromAdmin`-Ersatztext),
die Korrektur-/Update-Hinweise aber noch fehlen.
5. In `checkliste-user-backend.md` neuen Block „Phase 7" hinzufügen mit
Verweis auf `dev/frontend/hub-flux/19-PHASE-7-PRESS-RELEASE-FORM.md`.
### 6.2 Mit Phase 8 ergänzen
6. Neuer Abschnitt im `Admin-User.md`: „Titelbild & SVG-Platzhalter".
7. Neuer Abschnitt im `Presseportal – Konzept für Relaunch.md`: „Veröffentlichungs-Modal & Quota-Kommunikation".
8. Aktualisierung der Firmen-Liste-Doku im `Admin-User.md` mit den
neuen UI-Bausteinen (Counter-Strip, Saved-Views, Filter-Chips,
Card/List-Toggle, Rollen-Legende).
### 6.3 Längerfristig (Phase 2/3)
9. Magic-Link-Flow für Pressekontakte → eigenes Doku-Kapitel, sobald
gebaut.
10. Tarif-/Credit-System → eigener Architektur-Block (Datenmodell,
Stripe-Integration, Quota-Counter-Implementierung).
---
## 7. Quellen-Übersicht für die nächsten Schritte
| Frage | Quelle |
|---|---|
| Was ist konzeptuell der User-Backend-Aufbau? | `docs/user-admin/Admin-User.md` |
| Was ist bereits umgesetzt, was offen? | `docs/user-admin/checkliste-user-backend.md` (Phase 1 ✅) |
| Datenmodell-Übersicht? | `docs/user-admin/user-zusammenhaenge.md` |
| Großthemen-Konzept (KI, Bilder, Tombstones, Magic-Link, Pricing)? | `docs/user-admin/Presseportal – Konzept für Relaunch.md` |
| Brand- & Design-System? | `docs/konzept/Entwicklungs-Konzept - Frontend-Komponenten Multi-Brand.md`, `dev/frontend/hub-flux/*` |
| Score-System? | `docs/konzept/Konzept-Update 2 – Score-Stufen-System.md` |
| Aktuelle Phase 7 (PM-Form-Refactor)? | `dev/frontend/hub-flux/19-PHASE-7-PRESS-RELEASE-FORM.md` |
| Nächste Phase 8? | `docs/PHASE-8-USER-PANEL-PLAN.md` (neu, neben diesem Dokument) |

View file

@ -1,313 +0,0 @@
openapi: 3.1.0
info:
title: pressekonto Publisher API
version: "1.0"
description: |
REST-API des Publisher-Hubs (pressekonto) für presseecho.de und
businessportal24.com. Pressemitteilungen werden als Entwurf angelegt,
optional mit Bildern versehen und über die Submit-Route zur
Inhaltsprüfung eingereicht — eine direkte Veröffentlichung über die
API ist bewusst nicht möglich.
Authentifizierung über einen persönlichen API-Token (Bearer). Jeder
Token trägt Berechtigungen (Abilities), die den Zugriff auf die
Endpunkte steuern. Tokens werden im Publisher-Konto unter „API-Tokens"
selbst erstellt und widerrufen (Voraussetzung: aktive Buchung).
Rate-Limit: 60 Anfragen pro Minute je Token (Header X-RateLimit-Limit /
X-RateLimit-Remaining, bei 429 zusätzlich Retry-After).
Migration: Bestehende API-Schlüssel der Altportale wurden übernommen
und bleiben gültig — auch in der alten Übergabeform (X-ApiKey-Header)
und auf den dokumentierten /service/…-Pfaden. Unbekannte Alt-Schlüssel
erhalten 410 mit Migrationshinweis. Neue Integrationen nutzen bitte
Bearer-Tokens auf /api/v1.
servers:
- url: https://pressekonto.de/api/v1
security:
- bearerToken: []
tags:
- name: press-releases
description: "Eigene Pressemitteilungen (Ability: press-releases:read bzw. press-releases:write)"
- name: images
description: "Bilder einer Pressemitteilung (Ability: press-release-images:write zum Schreiben)"
- name: companies
description: "Eigene Firmenprofile/Newsrooms (Ability: companies:read)"
- name: categories
description: "Aktive Rubriken (Ability: press-releases:read)"
- name: newsletter
description: "Newsletter-Anmeldungen (Ability: newsletter:subscribe)"
paths:
/press-releases:
get:
tags: [press-releases]
summary: Eigene Pressemitteilungen auflisten
parameters:
- name: status
in: query
description: Nach Status filtern (draft, review, published, rejected, archived)
schema: { type: string }
- name: per_page
in: query
description: Seitengröße, Standard 25, max. 100
schema: { type: integer, maximum: 100 }
- name: page
in: query
schema: { type: integer }
responses:
"200":
description: Paginierte Liste
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/PressRelease" }
post:
tags: [press-releases]
summary: Pressemitteilung anlegen (immer als Entwurf)
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/PressReleaseInput" }
responses:
"201":
description: Entwurf angelegt
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/PressRelease" }
"403": { description: "Ability fehlt oder Firma gehört nicht zum Konto" }
"422": { description: "Validierungsfehler" }
/press-releases/{id}:
parameters:
- name: id
in: path
required: true
schema: { type: integer }
get:
tags: [press-releases]
summary: Pressemitteilung abrufen
responses:
"200":
description: Mitteilung inkl. Firma, Rubrik und Bildern
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/PressRelease" }
"403": { description: "Nicht gefunden oder fremdes Konto" }
patch:
tags: [press-releases]
summary: Pressemitteilung ändern (nur draft/rejected)
requestBody:
content:
application/json:
schema: { $ref: "#/components/schemas/PressReleaseInput" }
responses:
"200": { description: "Aktualisierte Mitteilung" }
"409": { description: "Nur Entwürfe und abgelehnte Mitteilungen sind änderbar" }
delete:
tags: [press-releases]
summary: Pressemitteilung löschen (nicht bei published)
responses:
"204": { description: "Gelöscht" }
"409": { description: "Veröffentlichte Mitteilungen können nicht per API gelöscht werden" }
/press-releases/{id}/submit:
post:
tags: [press-releases]
summary: Zur Inhaltsprüfung einreichen
description: |
Der einzige API-Weg Richtung Veröffentlichung. Durchläuft denselben
Funnel wie das Web-Formular (Richtlinien-Filter, Kontingent, Status-Log).
parameters:
- name: id
in: path
required: true
schema: { type: integer }
responses:
"200": { description: "Eingereicht (Status review)" }
"402": { description: "Aktive Buchung erforderlich (Tarif oder Einzel-PM)" }
"409": { description: "Nur draft/rejected kann eingereicht werden" }
"422": { description: "Kontingent erschöpft oder Richtlinienverstoß" }
/press-releases/{id}/images:
parameters:
- name: id
in: path
required: true
schema: { type: integer }
get:
tags: [images]
summary: Bilder einer Mitteilung auflisten
responses:
"200":
description: Bilderliste
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/PressReleaseImage" }
post:
tags: [images]
summary: Bild hochladen (nur bei draft/rejected)
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [image]
properties:
image:
type: string
format: binary
description: JPG, PNG oder WebP, max. 8 MB
title: { type: string, maxLength: 120 }
description: { type: string, maxLength: 500 }
copyright: { type: string, maxLength: 255 }
is_preview:
type: boolean
description: Als Vorschaubild setzen (ersetzt ein vorhandenes)
responses:
"201": { description: "Bild gespeichert" }
"409": { description: "Mitteilung ist nicht mehr änderbar" }
"422": { description: "Ungültige Datei" }
/press-release-images/{imageId}:
delete:
tags: [images]
summary: Bild entfernen (nur bei draft/rejected)
parameters:
- name: imageId
in: path
required: true
schema: { type: integer }
responses:
"204": { description: "Gelöscht" }
"409": { description: "Mitteilung ist nicht mehr änderbar" }
/companies:
get:
tags: [companies]
summary: Eigene Firmenprofile auflisten
parameters:
- name: per_page
in: query
schema: { type: integer, maximum: 100 }
responses:
"200": { description: "Paginierte Liste eigener Firmen" }
/companies/{id}:
get:
tags: [companies]
summary: Firmenprofil abrufen
parameters:
- name: id
in: path
required: true
schema: { type: integer }
responses:
"200": { description: "Firmenprofil" }
"403": { description: "Nicht gefunden oder fremdes Konto" }
/categories:
get:
tags: [categories]
summary: Aktive Rubriken auflisten
responses:
"200": { description: "Rubriken inkl. Übersetzungen" }
/newsletter/subscribe:
post:
tags: [newsletter]
summary: Newsletter-Anmeldung anlegen (Double-Opt-in folgt per Mail)
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [portal, email]
properties:
portal:
type: string
enum: [presseecho, businessportal24]
email: { type: string, format: email, maxLength: 190 }
salutation_key: { type: string, maxLength: 20 }
first_name: { type: string, maxLength: 80 }
last_name: { type: string, maxLength: 80 }
responses:
"201": { description: "Anmeldung angelegt (unbestätigt)" }
components:
securitySchemes:
bearerToken:
type: http
scheme: bearer
description: "Persönlicher API-Token aus dem Publisher-Konto (Selfservice unter „API-Tokens")."
schemas:
PressReleaseInput:
type: object
required: [company_id, category_id, language, title, text]
properties:
company_id:
type: integer
description: ID eines eigenen Firmenprofils — bestimmt Newsroom und Portal
category_id: { type: integer }
language:
type: string
minLength: 2
maxLength: 2
description: de oder en
title: { type: string, maxLength: 255 }
text:
type: string
description: "Erlaubtes HTML: p, br, h2, h3, strong, em, ul, ol, li, blockquote, a"
backlink_url: { type: string, format: uri, maxLength: 255 }
keywords: { type: string, maxLength: 255 }
teaser_begin: { type: integer, minimum: 0 }
teaser_end: { type: integer, minimum: 0 }
no_export: { type: boolean }
PressRelease:
type: object
properties:
id: { type: integer }
uuid: { type: string, format: uuid }
portal: { type: string }
language: { type: string }
title: { type: string }
slug: { type: string }
text: { type: string }
backlink_url: { type: [string, "null"] }
keywords: { type: [string, "null"] }
status:
type: string
enum: [draft, review, published, rejected, archived]
hits: { type: integer }
teaser:
type: object
properties:
begin: { type: [integer, "null"] }
end: { type: [integer, "null"] }
no_export: { type: boolean }
published_at: { type: [string, "null"], format: date-time }
company: { type: object }
category: { type: object }
images:
type: array
items: { $ref: "#/components/schemas/PressReleaseImage" }
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
PressReleaseImage:
type: object
properties:
id: { type: integer }
title: { type: [string, "null"] }
description: { type: [string, "null"] }
copyright: { type: [string, "null"] }
is_preview: { type: boolean }
width: { type: integer }
height: { type: integer }
mime: { type: string }

View file

@ -1,150 +0,0 @@
## 1. Hauptnavigation (rechts oben)
| Seite | Link | Dahinter | Relaunch |
| ------------- | ----------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------- |
| Home | `/` | Startseite | **Portal** |
| Registrierung | `/de/registration.html` | Account-Anlage | → **Hub** (pressekonto) |
| Login | `/de/login.html` | Anmeldung | → **Hub** (Sanctum, kein eigener Login am Portal) |
| Preisliste | `/de/pricelist.html` | Tarife | → **Hub** (neue Tarifstruktur Einzel/Starter/…/Agency) |
| Service | `/de/service.html` | Leistungsübersicht / Verkaufsseite | wird zu veroeffentlichen |
| Kontakt | `/de/contact.html` | Kontakt | **Portal** + Hub (auch hier muss deutlich werden, dass der Hub das ganze Backend System ist) |
## 2. Region/Sprache
| Variante | Link | Relaunch |
| ------------ | -------- | ---------- |
| Deutsch | `/` | **Portal** |
| Deutsch (CH) | `/de-ch` | raus |
| Deutsch (AT) | `/de-at` | raus |
| English | `/en` | Portal |
## 3. „Presse"-Dropdown — Ratgeber/SEO-Content (12 Seiten)
Das ist editorial wertvollster Teil: statische Ratgeber-Seiten, die wahrscheinlich organischen Long-Tail-Traffic ziehen.
|Seite|Link|
|---|---|
|Pressemitteilung schreiben|`/de/presse/pressemitteilung-schreiben.html`|
|Aufbau Pressemitteilung|`/de/presse/pressemitteilung-aufbau.html`|
|Pressemitteilung Nutzen & Einsatz|`/de/presse/pressemitteilung-nutzen.html`|
|Pressemappe|`/de/presse/pressemappe.html`|
|Redakteure & Journalisten|`/de/presse/redakteure-journalisten.html`|
|Pressemitteilung Beispiel|`/de/presse/pressemitteilung-beispiel.html`|
|Pressekonferenz|`/de/presse/pressekonferenz.html`|
|Deutsche Presse Agentur|`/de/presse/deutsche-presseagentur.html`|
|Redaktion|`/de/presse/redaktion.html`|
|Pressestelle|`/de/presse/pressestelle.html`|
|Pressesprecher|`/de/presse/pressesprecher.html`|
|Presseausweis|`/de/presse/presseausweis.html`|
**Relaunch:** Diese URLs unbedingt 1:1 erhalten oder sauber 301-redirecten — hier steckt der SEO-Wert. Editorial passen sie zu beiden Portalen, ggf. sogar besser zu presseecho (Kontexttiefe). Entscheidung offen, wo sie inhaltlich angesiedelt werden.
Hier würde ich eine FAQ Seite machen, die alle Themen beinhaltet und sauber 301-redirectet
## 4. Kategorien (14 Stück)
`/de/category/{slug}.html` — Listenseiten je Themenrubrik:
Handel · IT · Finanzen · Reisen · Kultur · Politik · Medizin · Sport · Motor/Verkehr · Haus · Mode · Sonstiges · Wissenschaft/Forschung · Recht
**Relaunch:** Diese Kategoriestruktur ist die Backbone für Migration + Boost-Logik (Kategorieseiten sind Teil deiner Boost-Platzierung). Beim Relaunch zu prüfen: Übernimmst du alle 14 unverändert, oder konsolidierst du? „Sonstiges" als Auffangbecken ist für deine Content-Score-/Trust-Logik suboptimal.
## 5. Content-/Detailseiten (die eigentlichen Templates)
|Typ|URL-Muster|Funktion|Relaunch|
|---|---|---|---|
|PM-Detailseite|`/de/{slug}.html`|Einzelne Pressemitteilung|**Portal** — Kern-Template|
|Kategorie-Liste|`/de/category/{slug}.html`|PMs je Rubrik, paginiert|**Portal**|
|Pressemappe (Presskit)|`/de/presskit/{id}/{slug}.html`|Firmenprofil + alle PMs der Firma|**Portal** = deine „Firmen-Detailseite"|
|Stream + Paginierung|`/de?page=N`|732 Seiten Archiv|**Portal** — aber Paginierungs-/Indexierungsstrategie überdenken|
Die Presskit-Seite entspricht funktional exakt deiner geplanten **Firmen-Detailseite** (Header, Stammdaten, Boilerplate, Kontakt, gefilterte PM-Liste). Gutes Mapping — du baust das also nicht neu, sondern modernisierst ein bestehendes Konstrukt.
## 6. Footer / Legal
| Seite | Link | Relaunch |
| ----------- | ------------------------ | -------------------------------------------------- |
| Impressum | `/de/imprint.html` | **Portal** + Hub (pflicht, pro Domain) |
| AGB | `/de/terms.html` | **Hub** (Vertragsbeziehung läuft über pressekonto) |
| Datenschutz | `/de/privacypolicy.html` | **Portal** + Hub |
---
## Einordnung für den Relaunch
Wenn ich das gegen deine Hub-and-Spoke-Architektur lege, fällt die Seitenmenge in drei klare Eimer:
**Bleibt am Portal (businessportal24.com & presseecho.de = öffentlicher Spoke):** Home/Stream, PM-Detail, Kategorie-Listen, Pressemappe/Firmen-Detail, Ratgeber-Seiten, Impressum, Datenschutz, Kontakt.
**Wandert in den Hub (pressekonto):** Registrierung, Login, Preisliste/Tarife, AGB, und alles dahinter (PM erstellen, Pressemappe anlegen, Account). Das deckt sich mit deinem Prinzip „Portale haben keine eigenen Login-Systeme".
---
## Relaunch-Mapping der Bestands-URLs (IST 17.06.2026)
Alle Alt-URLs werden per **301** auf die neue Struktur geleitet (Implementierung
in `routes/web.php`, Closure `$htmlRedirect` + Regions-Routen; Test:
`tests/Feature/Web/LegacyRedirectTest.php`). Die neue Struktur liegt unter dem
Ausgaben-Präfix `/{de|en}/…`.
| Alt-URL (Altsystem) | Ziel (301) | Status |
| ------------------------------------ | ---------------------------------- | ------ |
| `/` | `/de` (Default-Ausgabe) | ✅ |
| `/de/{slug}.html` (PM) | `/de/press-release/{slug}` | ✅ |
| `/de/category/{slug}.html` | `/de/category/{slug}` | ✅ |
| `/de/presskit/{id}/{slug}.html` | `/de/newsroom/{slug}` *(Lookup über `companies.legacy_id`; Fallback `/newsrooms`)* | ✅ |
| `/de/presse/*.html` (12 Ratgeber) | `/de/faq` (konsolidiert) | ✅ |
| `/de/pricelist.html` | `/de/preise` | ✅ |
| `/de/service.html` | `/de/veroeffentlichen` | ✅ |
| `/de/contact.html` | `/de/kontakt` | ✅ |
| `/de/imprint.html` | `/de/impressum` | ✅ |
| `/de/terms.html` | `/de/agb` | ✅ |
| `/de/privacypolicy.html` | `/de/datenschutz` | ✅ |
| `/de/registration.html` | Hub `pressekonto/register` | ✅ |
| `/de/login.html` | Hub `pressekonto/login` | ✅ |
| `/de/kategorien.html` (Übersicht) | `/de` (Übersicht entfällt) | ✅ |
| `/de-ch`, `/de-at` (+ Subpfade) | `/de` | ✅ |
**Bewusst auf später verschoben (eigene Slice):**
- **Regionen-Übersichten** `/region/{slug}` (Punkt 3 der öffentlichen Strecke) –
aktuell leiten nur die entfallenen Sprachvarianten `de-ch`/`de-at` auf `/de`.
- **Tarife/AGB im Hub**: `pricelist` zeigt vorerst auf die öffentliche
SEO-Seite `/preise`; die buchbare Tarifstruktur (Checkout) lebt im Hub.
**Erledigtes Feinmapping:**
- **Presskit→Newsroom**: `presskit/{id}/{slug}` löst über den Unique-Index
`companies.(legacy_portal, legacy_id)` den heutigen Newsroom auf und leitet
301 direkt auf `/newsroom/{slug}` (nur bei aktiver Firma mit ≥1 veröffentlichter
Meldung; sonst Fallback `/newsrooms`).
- **Preise datengetrieben & überall sichtbar**: Tarif-Teaser auf der Startseite
aus `Plan::active()` (`x-web.pricing-teaser`); Preise-Link in Header-Utility-Bar
und Footer. Test: `tests/Feature/Web/PricingTeaserTest.php`.
- **Statische Seiten im Editorial-Design** (Rebuild abgeschlossen): Pflicht-
seiten `impressum`, `datenschutz`, `agb`, `cookies` (Prosa über `.pm-body`)
sowie `faq` (natives `<details>`-Akkordeon), `hilfe` (Support-Landing),
`kontakt` (Kontaktwege; Formular wartet auf POST-Handler — als TODO markiert),
`ueber-uns` (Kennzahlen/Mission/Werte/Team/Timeline) und `api`
(Features/Code-Beispiel/Integrationen). Tote Doku-/Anker-Links durch echte
Ziele bzw. Hinweise ersetzt. Gemeinsames Gerüst: `x-web.static-page`.
- **Header/Footer-Linkpflege**: `site-header`/`site-footer` ohne tote `#`-Links —
FAQ/Hilfe/Cookies verlinkt, „Anmelden" führt in den Hub
(`{domain_portal_url}/login`), nicht verlinkte Platzhalter entfernt.
Test: `tests/Feature/Web/NavigationLinksTest.php`.
Gemeinsames Gerüst: wiederverwendbare Komponente `x-web.static-page`
(site-header, Breadcrumb, Masthead, site-footer). Altes Theme
(`livewire:web.header`, `gradient-hero`) entfernt. Rechtstexte sind weiterhin
Platzhalter (vor Go-Live ersetzen). Test: `tests/Feature/Web/StaticPagesTest.php`.
- **`/preise`-Seite im Editorial-Design** (Rebuild abgeschlossen): Reihenfolge
Abo-Tarife → Einzel-PM → Add-ons. Datengetrieben über `Plan::active()`,
`config('billing.single_pm_price_cents')` und `config('credits.*')`; Route
`$preisePage`-Closure. Texte ausgelagert in `lang/{de,en}/pricing.php`
(Schlüssel `pricing.*`), damit Portal/Hub einheitlich ziehen können. Add-on-
Lineup (Magic-Link-Pfade) ist mit TODO-Kommentaren als „noch im Rework"
markiert. Test: `tests/Feature/Web/PreisePageTest.php`.

View file

@ -1,509 +0,0 @@
**Datum:** 12. Mai 2026 **Status:** Technisches Implementierungs-Konzept **Tech-Stack:** Laravel 12+, Livewire 4 / Volt, Tailwind CSS (v4), Alpine.js (über Livewire) **Bezug:** Konzept-Update 3 (Multi-Brand-Architektur), Konzept-Update 4 (Positionierung), Brand-Landing-Konzept businessportal24
> **IST-Stand 21.05.2026**: Multi-Brand-Architektur ist umgesetzt
> (`config/domains.php`, `ThemeServiceProvider`, getrennte Vite-Builds
> `portal` + `web`). Die Hub-Migration des User Backends ist als
> eigene Roadmap in `dev/frontend/hub-flux/` dokumentiert (Phasen 0–7
> abgeschlossen, Phase 8 in Planung). Der hier beschriebene Brand-Context
> wird ueber `View::share()` global aufgeloest.
---
## 1. Leitprinzipien
Vier Regeln, an denen sich jede technische Entscheidung in diesem Dokument messen muss:
1. **Ein Codebase, viele Brands.** Kein Branch pro Portal, keine duplizierten Views. Differenzierung über Konfiguration, CSS-Variablen und gezielte View-Overrides.
2. **Brand-Awareness zentral aufgelöst, nicht in Komponenten verteilt.** Eine Komponente fragt nicht „bin ich auf businessportal24?". Sie konsumiert eine `$brand`-Context-Variable und rendert entsprechend.
3. **Livewire/Volt nur wo nötig.** Statische Komponenten bleiben pures Blade. Reaktivität ist ein Kostenfaktor (Server-Roundtrips, Hydration, State-Management) – sie muss verdient werden.
4. **Solo-tauglich heißt: jede Entscheidung muss in 6 Monaten noch verständlich sein.** Lieber explizit als clever.
## 2. Brand-Auflösung (Multi-Tenant-Pattern)
### Brand-Resolution-Pipeline
```
Request → Middleware → BrandResolver → Brand-Context im Container
→ View-Pfad-Override
→ Config-Override
→ Layout-Auswahl
```
**Schritt 1: Domain-Mapping**
Die `brands`-Tabelle aus Update 3 enthält pro Brand mindestens:
- `slug` (z.B. `businessportal24`, `presseecho`, `hub`)
- `primary_domain` (z.B. `businessportal24.com`)
- `theme_key` (z.B. `bp24`, `pe`) – Verweis auf CSS-Token-Set
- `config_path` (z.B. `brands/businessportal24.php`)
- `is_publisher_hub` (boolean)
-ist zu prüfen, teils schon im System angelegt!
**Schritt 2: Middleware**
```php
// app/Http/Middleware/ResolveBrand.php
public function handle(Request $request, Closure $next): Response
{
$brand = Cache::rememberForever(
"brand.domain.{$request->getHost()}",
fn() => Brand::query()
->where('primary_domain', $request->getHost())
->orWhereJsonContains('aliases', $request->getHost())
->firstOrFail()
);
app()->instance(Brand::class, $brand);
View::share('brand', $brand);
Config::set('brand', $brand->config());
return $next($request);
}
```
Cache ist hier wichtig – die Domain-zu-Brand-Auflösung passiert bei jedem Request. `rememberForever` mit explizitem Cache-Bust beim Brand-Update.
-ist zu prüfen, teils schon im System angelegt!
**Schritt 3: Brand im Container**
Jede Klasse kann via Dependency Injection auf die aktuelle Brand zugreifen:
```php
public function __construct(private Brand $brand) {}
```
In Blade-Templates ist `$brand` durch `View::share()` direkt verfügbar.
### Lokale Entwicklung
Lokal arbeiten mit `.test`-Domains in Docker (devserver) auf dem Server via Treafik:
- `businessportal24.test`
- `presseecho.test`
- `pressekonto.test`
Alle zeigen auf dieselbe Codebase, die Middleware löst per Hostname auf. Keine Subdomains, keine Port-Tricks – schmerzfreies lokales Multi-Brand-Setup.
## 3. Theming-System (Tailwind v4 + CSS Custom Properties)
### Empfehlung: Tailwind v4
Falls die Migration auf v4 noch offen ist: **jetzt machen**. Die `@theme`-Direktive in v4 macht Multi-Brand-Theming dramatisch einfacher als das v3-Config-Konstrukt. Native CSS-Variablen, keine PostCSS-Akrobatik mehr.
### Token-Architektur
Drei Ebenen (Beispiel ):
```css
/* Ebene 1: Globale Design-Tokens (markenneutral) */
@theme {
--font-serif: 'Source Serif 4', Georgia, serif;
--font-sans: 'Inter', system-ui, sans-serif;
--spacing-section: 5rem;
--spacing-section-tight: 3rem;
--radius-card: 2px; /* fast keine Rundungen, editorial */
}
/* Ebene 2: Semantische Tokens (markenneutral, aber rollenbasiert) */
:root {
--color-text-primary: var(--brand-text);
--color-text-muted: var(--brand-text-muted);
--color-surface: var(--brand-surface);
--color-accent: var(--brand-accent);
--color-cta-bg: var(--brand-cta-bg);
--color-cta-fg: var(--brand-cta-fg);
--color-hub-transition: var(--brand-hub-bg);
}
/* Ebene 3: Brand-spezifische Werte */
[data-brand="businessportal24"] {
--brand-text: #1a1a1a;
--brand-text-muted: #6b6b6b;
--brand-surface: #fafaf7; /* warmer off-white */
--brand-accent: #d94e1f; /* gedämpftes Orange */
--brand-cta-bg: #d94e1f;
--brand-cta-fg: #ffffff;
--brand-hub-bg: #1a2540; /* dunkelblau, Störer */
}
[data-brand="presseecho"] {
--brand-text: #f0f0e8;
--brand-text-muted: #a0a098;
--brand-surface: #1f2620; /* dunkelgrün-anthrazit */
--brand-accent: #5a8a6b; /* gedämpftes Grün */
--brand-cta-bg: #5a8a6b;
--brand-cta-fg: #ffffff;
--brand-hub-bg: #1a2540; /* Hub-Farbe bleibt konstant! */
}
```
### Brand-Aktivierung im Layout
```blade
{{-- resources/views/layouts/brand.blade.php --}}
<!DOCTYPE html>
<html lang="de" data-brand="{{ $brand->slug }}">
<head>
@vite(['resources/css/app.css', 'resources/js/app.js'])
{{-- Brand-CSS wird im app.css via @import geladen, oder optional separat: --}}
@if($brand->has_custom_css)
<link rel="stylesheet" href="{{ asset("themes/{$brand->slug}.css") }}">
@endif
</head>
<body class="bg-[var(--color-surface)] text-[var(--color-text-primary)]">
{{ $slot }}
</body>
</html>
```
**Wichtig:** Der `data-brand`-Attribut auf `<html>` ist der einzige Hebel, der den gesamten Look umschaltet. Alle Tailwind-Utilities, die brand-spezifische Werte nutzen, greifen über CSS-Variablen darauf zu.
### Pragmatische Tailwind-Nutzung
Die Komponenten schreiben **nicht** `bg-orange-600` (das wäre brand-spezifisch im Markup festgenagelt). Stattdessen:
```blade
<button class="bg-[var(--color-cta-bg)] text-[var(--color-cta-fg)] hover:opacity-90">
Mitteilung einreichen
</button>
```
Oder noch sauberer mit eigenen Tailwind-Utility-Klassen, die in `app.css` definiert werden:
```css
@layer components {
.btn-cta {
@apply bg-[var(--color-cta-bg)] text-[var(--color-cta-fg)]
px-6 py-3 rounded-sm font-medium hover:opacity-90 transition;
}
.btn-hub {
@apply bg-[var(--color-hub-transition)] text-white
px-8 py-6 block;
}
}
```
So bleibt das Markup brand-agnostisch und die Stilfragen zentralisiert.
## 4. Komponenten-Hierarchie und Engine-Wahl
### Drei Render-Modi, drei Anwendungsbereiche
|Modus|Wann verwenden|Performance|Beispiele|
|---|---|---|---|
|**Blade Component**|Statisches Markup, keine Interaktion|⚡⚡⚡|TopBar, Footer, PressItem, StatsRow|
|**Volt (Single-File)**|Lokaler State, einfache Reaktivität, Lifecycle einfach|⚡⚡|AdHocTicker, HeroSlider, Search|
|**Klassisches Livewire**|Komplexe Komponenten mit Services, Events, mehrere Methoden|⚡|PressEditor, NewsroomDashboard|
### Volt: konkrete Empfehlung
Volt ist für dieses Projekt **die richtige Wahl als Default für reaktive Komponenten** – aber nicht für statische. Die Gründe:
**Pro Volt:**
- Single-File-Komponenten: PHP-Logik + Blade-Template + Tailwind-Klassen in einer Datei. Solo-Entwickler-Freundlichkeit ist hoch.
- Funktionale API ist deutlich weniger Boilerplate als klassische Livewire-Klassen.
- Volt-Komponenten lassen sich genau wie Livewire-Komponenten lazy-laden (`<livewire:lazy ...>`), was für Above-the-fold-Performance wichtig ist.
**Kontra Volt:**
- Für reine Display-Komponenten ist Volt overkill. Eine `<x-press-item>` ohne State soll keine Livewire-Komponente sein – Hydration und Wire-Tracking sind unnötige Kosten.
- Wenn eine Komponente Services injiziert, ein eigenes Test-Setup braucht oder mehr als ~150 Zeilen wächst, ist eine klassische Livewire-Klasse besser strukturierbar.
**Faustregel:**
> Renderst du HTML ohne Server-Interaktion? → Blade Component. Brauchst du `wire:model`, `wire:click`, Polling oder reaktiven State? → Volt. Wird die Komponente komplex, hat Services, eigene Tests? → Klassisches Livewire.
### Konkrete Komponenten-Inventur aus den Screens
Aufgeschlüsselt nach Engine. Das ist die direkte Übersetzung der Screens in technische Bausteine:
#### Blade-Komponenten (statisch, hochfrequent wiederverwendet)
```
<x-brand.top-bar> -- Wirtschafts-Ticker, Sprachen, Newsletter/RSS
<x-brand.header> -- Logo, Suche, CTAs
<x-brand.rubriken-nav> -- Hauptnavigation (Wirtschaft, Tech, Finanzen...)
<x-brand.footer> -- Footer mit Cross-Brand-Hinweis
<x-press.item> -- Standard-Listen-Eintrag mit Slots für Varianten
<x-press.item-hero> -- Große Hero-Variante
<x-press.item-sidebar> -- Kompakte Sidebar-Variante (mit Nummerierung)
<x-press.byline> -- Quelle · Zeit · Lesezeit (wiederverwendbar)
<x-ui.section-header> -- "§ 01" + Label + H2 (das Editorial-Pattern)
<x-ui.stats-row> -- Drei-/Vier-Spalten-Statistik
<x-ui.button> -- CTA-Button mit Varianten (primary, secondary, hub)
<x-ui.badge> -- Branchen-Marker, "Geprüft"-Label
<x-hub.transition-block> -- DER dunkelblaue Störer (siehe Briefing)
<x-hub.cta> -- Inline Hub-CTA (Variante des Störers)
<x-quality.standard-footer> -- "Alle Pressemitteilungen werden geprüft..."
```
#### Volt-Komponenten (reaktiv, isolierter State)
```
<livewire:ad-hoc-ticker /> -- Auto-refresh alle 30s, Polling
<livewire:hero-slider /> -- 3 Top-Meldungen, auto-rotate mit Alpine
<livewire:press-search /> -- Suche im Header
<livewire:press-list-filter /> -- "Alle · Heute · Diese Woche" Tabs
<livewire:newsroom-list /> -- Newsroom-Sidebar mit "heute aktiv" Polling
<livewire:branchen-index /> -- Live-Werte mit ± Indikatoren
<livewire:termine-week /> -- Termine-Karussell mit Wochen-Navigation
```
#### Klassisches Livewire (komplex, services)
```
PressSubmissionForm -- Mehrstufige Einreichung (auf Hub)
NewsroomManager -- Profil-Verwaltung (auf Hub)
AdminReviewQueue -- Redaktions-Tool (auf Hub)
```
Auffällig: die **Brand-Portale brauchen kaum klassisches Livewire**. Das ist konsistent zur Architektur aus Update 3 – Brand-Portale sind primär Lese-Oberflächen, der State liegt im Hub.
## 5. Brand-Differenzierung in Komponenten
Drei Mechanismen, in aufsteigender Eingriffstiefe:
### 5a. Konfiguration über Brand-Config
Der einfachste Fall: eine Komponente verhält sich anders je nach Brand-Konfiguration. (Beispiel)
siehe: config/domains.php
```php
return [
'name' => 'businessportal24',
'tagline' => 'Pressemitteilungen · DACH',
'press_item_layout' => 'timeline', // vs. 'topic'
'show_market_ticker' => true,
'show_branchen_index' => true,
'hero_variant' => 'top-meldung', // vs. 'topic-cluster'
'rubriken' => ['Wirtschaft', 'Technologie', /* ... */],
];
return [
'name' => 'presseecho',
'tagline' => 'Branchen-Pressearchiv',
'press_item_layout' => 'topic',
'show_market_ticker' => false,
'show_branchen_index' => false,
'hero_variant' => 'topic-cluster',
'rubriken' => [/* andere Reihenfolge, andere Schwerpunkte */],
];
```
Komponenten lesen daraus:
```blade
@if($brand->config('show_market_ticker'))
<x-brand.market-ticker />
@endif
<x-press.item :layout="$brand->config('press_item_layout')" :item="$item" />
```
**Das ist die häufigste Form der Differenzierung** – und sie reicht für ~80 % aller Fälle.
### 5b. Slots und Defaults in Komponenten
Wenn eine Komponente strukturell gleich ist, aber Inhalte/Sprache abweichen:
```blade
{{-- resources/views/components/hub/transition-block.blade.php --}}
@props([
'title' => $brand->config('hub_cta.title') ?? 'Pressemitteilung einreichen',
'description' => $brand->config('hub_cta.description'),
'buttonText' => $brand->config('hub_cta.button') ?? 'Zum Publisher-Bereich',
])
<aside class="btn-hub flex flex-col gap-4">
<h3 class="text-xl font-medium">{{ $title }}</h3>
<p class="text-white/80">
{{ $description ?? $slot }}
</p>
<a href="{{ route('hub.submit', ['brand' => $brand->slug]) }}"
class="btn-cta inline-flex items-center gap-2 w-fit">
{{ $buttonText }}
<span aria-hidden="true">↗</span>
</a>
</aside>
```
Brand-Texte stehen in Config, Komponente bleibt eine.
### 5c. View-Override pro Brand (Eskalations-Pfad)
Für die seltenen Fälle, in denen eine Brand wirklich ein anderes Markup braucht: Laravel kann View-Pfade brand-spezifisch erweitern.
```php
// app/Providers/BrandServiceProvider.php
public function boot(): void
{
$this->app['view']->prependLocation(
resource_path("views/themes/{$brand->slug}")
);
}
```
Dann sucht Laravel View-Dateien zuerst unter `resources/views/themes/presseecho/components/press/item.blade.php`, dann unter dem Standard-Pfad. **Nur** für die Komponenten, die wirklich anders sein müssen, wird eine Override-Datei angelegt.
> **Disziplin-Regel:** View-Overrides sind die letzte Eskalationsstufe. Erst versuchen, mit Config + Slots auszukommen. Override-Dateien verdoppeln Wartungsaufwand – jeder Bugfix muss mehrfach gemacht werden.
## 6. Datei-Struktur (Beispiel, siehe akutelle Struktur und optimiere falls nötig )
```
app/
├── Brand/
│ ├── Brand.php # Eloquent Model
│ ├── BrandManager.php # Service, im Container
│ └── BrandResolver.php # Domain → Brand
├── Http/
│ └── Middleware/
│ └── ResolveBrand.php
├── Livewire/
│ ├── Brand/ # Brand-Portal-spezifisch
│ │ ├── AdHocTicker.php
│ │ ├── HeroSlider.php
│ │ └── PressSearch.php
│ └── Hub/ # Hub-spezifisch
│ ├── PressSubmissionForm.php
│ └── NewsroomManager.php
├── View/
│ └── Components/
│ ├── Brand/
│ ├── Press/
│ ├── Hub/
│ ├── Ui/
│ └── Quality/
└── Providers/
└── BrandServiceProvider.php
config/
└── brands/
├── businessportal24.php
├── presseecho.php
└── hub.php
resources/
├── css/
│ ├── app.css # Tailwind base + semantische Tokens
│ └── themes/
│ ├── businessportal24.css # Brand-Tokens (optional separat)
│ └── presseecho.css
├── js/
│ └── app.js
└── views/
├── layouts/
│ ├── brand.blade.php # Brand-Portal-Layout
│ └── hub.blade.php # Hub-Layout
├── components/ # Standard-Komponenten
│ ├── brand/
│ ├── press/
│ ├── hub/
│ ├── ui/
│ └── quality/
├── livewire/ # Volt-Komponenten
│ ├── ad-hoc-ticker.blade.php
│ ├── hero-slider.blade.php
│ └── press-search.blade.php
├── pages/ # Konkrete Seiten-Templates
│ ├── home.blade.php
│ └── veroeffentlichen.blade.php
└── themes/ # NUR Brand-Overrides
└── presseecho/
└── components/
└── ... # nur was wirklich anders ist
```
## 7. Performance-Strategie
Vier konkrete Hebel, die in dieser Reihenfolge ausgeschöpft werden:
**1. Aggressive View-Caching für statisches Markup.** Press-Listen, Newsroom-Sidebars, Statistik-Zeilen können mit Tag-basiertem Cache gepuffert werden. Neue Mitteilung → relevante Tags invalidieren.
```php
Cache::tags(['press_list', "brand.{$brand->slug}"])
->remember('home.aktuelle-meldungen', now()->addMinutes(5), fn() => /* ... */);
```
**2. Volt-Komponenten lazy laden, wo sinnvoll.** Below-the-fold-Komponenten (Branchen-Index, Termine, Newsroom-Liste) als `lazy`:
```blade
<livewire:branchen-index lazy />
```
Sie laden erst beim Scroll, blockieren nicht das initiale Render.
**3. Asset-Pipeline: ein Bundle, alle Brands.** Über die CSS-Variablen-Strategie ist kein Per-Brand-Build nötig. Ein Vite-Build, der für alle Brands gilt. Spart Komplexität und Cache-Invalidierung.
**4. Brand-Resolution cachen.** Die Domain-zu-Brand-Auflösung ist `rememberForever` (siehe Middleware). Cache-Bust nur beim Brand-Update über Model-Observer.
## 8. Migration der Bestands-Inhalte
Quer zu allem oben: die ~100.000 Bestandsmitteilungen sind im neuem System migriert! Drei Punkte, die das Komponenten-Design beeinflussen:
- **`<x-press.item>` muss tolerant gegenüber unvollständigen Daten sein.** Alte Mitteilungen haben evtl. keine Lesezeit-Schätzung, keine Branchen-Zuordnung, keine sauberen Bilder. Komponente rendert auch dann sauber.
- **Permalink-Stabilität.** Die alte URL-Struktur muss erhalten bleiben (Strategie-Dokument: Tombstone-Modell). Das ist ein Routing-Thema, kein Komponenten-Thema – aber die Komponenten dürfen keine URLs hardcoden, sondern nur `route()`-Helpers nutzen.
- **Brand-Zuordnung der Bestände.** Wie in Update 3 festgelegt: am Start ist jede Mitteilung beiden Brands zugewiesen. Komponenten brauchen dafür keine Sonderlogik – sie filtern nach Brand-Kontext, und der Pool ist eben (am Anfang) für beide Brands derselbe.
## 9. Entwicklungs-Reihenfolge (Empfehlung)
Konkrete Bauplan-Sequenz, die früh nutzbare Ergebnisse liefert:
**Sprint 1 – Fundament**
- Brand-Model, Middleware, Resolver
- Theming-Setup (Tailwind v4, CSS-Variablen, zwei Brand-Themes)
- Layout `brand.blade.php`
- Grundlegende UI-Komponenten (`button`, `badge`, `section-header`)
**Sprint 2 – Statisches Markup für businessportal24**
- TopBar, Header, RubrikenNav, Footer
- `<x-press.item>` und seine Varianten
- StatsRow, QualityStandardFooter
- Statische Version der Veröffentlichen-Landing (ohne Reaktivität)
**Sprint 3 – Reaktive Komponenten**
- AdHocTicker (Volt + Polling)
- HeroSlider (Volt + Alpine)
- PressList mit Filter-Tabs
- Hub-Transition-Block mit Cross-Domain-Auth-Übergabe
**Sprint 4 – Hub-Anbindung**
- Sanctum-Setup für Cross-Domain
- Hub-Routing für `?brand=businessportal24`-Parameter
- Einreichungs-Flow im Hub (klassisches Livewire)
**Sprint 5 – Zweite Brand aufschalten**
- `presseecho.test` lokal
- Brand-Config für presseecho
- Erste Override-Komponente: `topic-cluster`-Hero
- Testen: was funktioniert ohne Override, was braucht eines?
Ab Sprint 5 wird die eigentliche Stresstest-Frage beantwortet: **Hält die Architektur, wenn die zweite Brand wirklich anders aussehen soll?** Wenn an Sprint 5 viele Overrides nötig werden, ist die Config-Schicht zu dünn – dann iterieren.
## 10. technische Punkte
- **Tailwind v4:** wenn das Projekt noch nicht migriert ist, sollte das _vor_ Sprint 1 entschieden werden. v4 macht das CSS-Variablen-Setup deutlich eleganter.
- **Sanctum-Cookie-Domain für Cross-Domain-Auth:** Detail aus Update 3, muss vor Sprint 4 final geklärt sein. Same-Site-Strategie, SPA-Mode oder klassischer Token-Flow?
- **CDN/Asset-Hosting:** Brand-Bilder, Press-Item-Fotos – kommen vom Hub
- **Translations:** DACH-Sprachschalter auf der Startseite –(de (ohne parameter) / de-at / de-ch / en) ist eine reine Inhalts-Filterung. Bei Mehrsprachigkeit de/en: i18n-Setup
- **Polling-Frequenzen:** AdHocTicker, Newsroom-Liste – wie oft refreshen, ohne dass die Server-Last bei wachsendem Traffic problematisch wird? Anfangswerte: Ticker 30s, Newsroom 60s, Branchen-Index 5 min.
---
_Dieses Konzept ist die technische Brücke zwischen Architektur (Update 3), Positionierung (Update 4) und Implementation. Es legt fest, wie Komponenten strukturiert werden, damit die Brand-Differenzierung skaliert – ohne in eine Codebase-Duplikation zu kippen. Anpassungen sollten dokumentiert und mit den Update-Dokumenten abgeglichen werden._

View file

@ -1,292 +0,0 @@
**Stand:** Mai 2026 **Portale:** presseecho.de, businessportal24.de **Zweck:** Strategisches Leitkonzept zur Differenzierung gegenüber Wettbewerbern und zur schrittweisen Reaktivierung der beiden übernommenen Portale.
---
## 1. Ausgangslage
Zwei übernommene Pressekonto, über 10 Jahre alt, mit zusammen rund 100.000 archivierten Pressemitteilungen. Frontend nicht responsive, Backend technisch veraltet. Aktive Bestandskunden vorhanden, größerer inaktiver Stamm. Aktuell ca. 50 % des Traffics über connektar.de als Distribution-Partner.
Relaunch in Umsetzung: Laravel-Backend mit 1:1-Datenmigration, neues Tailwind-basiertes Frontend, schrittweise Markendifferenzierung der beiden Portale.
**Entwicklung:** Solo, neben Hauptberuf, mit KI-gestützter Entwicklung. **Laufende Kosten:** Minimal (Hetzner-Server ~10 €/Monat). **Existenzdruck:** Keiner. Projekt ist optionales Nebeneinkommen.
---
## 2. Marktposition: bewusste Nische statt Konkurrenz
### Ehrliche Einschätzung des Marktes
Klassische Pressekonto verlieren redaktionelle Relevanz, behalten aber SEO-Funktion. Käufer buchen Pressemitteilungen heute nicht mehr für Journalistenanrufe, sondern für:
1. Backlinks und SEO-Substanz
2. Google-Sichtbarkeit zu konkreten Suchbegriffen
3. Digitale Sichtbarkeit (Investoren, Partner, Bewerber finden aktuelle PMs)
4. Inhalte für eigene Newsroom-Seiten
Wettbewerber wie openPR und Pressebox kommunizieren weiterhin das alte Versprechen („Reichweite zu Journalisten") und sind in Lock-in-Modellen, Vertriebsstrukturen und Distributions-Verträgen gefangen.
### Eigene Positionierung
**„Pressemitteilungen für digitale Sichtbarkeit – ehrlich, fair, ohne Lock-in."**
Keine Massenabdeckung. Keine aggressive Skalierung. Eine ruhige, durchdachte Alternative, die genau die Käufer anspricht, die mit dem klassischen Modell unzufrieden sind.
### Strukturelle Vorteile gegenüber Wettbewerbern
- Niedrige Fixkosten → keine Notwendigkeit für aggressive Tarifmodelle
- Solo-Entwicklung → schnelle, ehrliche Produktentscheidungen ohne Vertriebs-Druck
- 100.000 PMs Archiv + gewachsene Domain-Autorität → nicht reproduzierbares Asset
- Kein Investorendruck → kann bewusst klein, nachhaltig und langfristig wachsen
---
## 3. Realistische Erfolgsdefinition
**Erfolg heißt nicht:** Verdrängung der Marktführer, hohe Wachstumsraten, große Userzahlen.
**Erfolg heißt:**
- Stabiler Betrieb mit minimalen Fixkosten
- Wachsender Stamm zufriedener Bestandskunden
- Planbares monatliches Einkommen (mittelfristig 3.000–8.000 €)
- Mit dem Alter: lukratives Nebeneinkommen
- Verkaufbar, falls gewünscht
**Zeithorizont:** 3–5 Jahre für stabile Einkommensbasis.
---
## 4. Was wir bewusst NICHT machen
Diese Liste ist wichtiger als jede Feature-Roadmap. Sie schützt vor Entscheidungen in schwachen Momenten.
- **Keine Lock-in-Verträge** mit 12-Monats-Mindestlaufzeit
- **Keine kostenlosen PMs** (zieht falsche Zielgruppe an)
- **Keine Fake-Urgency** im Marketing („Nur noch 2 Plätze frei")
- **Keine versteckten Gebühren** bei Standard-Korrekturen
- **Keine Vermarktung als „Reichweite zu X Journalisten"** – ehrliches Versprechen statt PR-Floskeln
- **Keine aggressiven Pop-ups** beim Verlassen
- **Keine Newsletter-Pflichtanmeldung** im Bestellprozess versteckt
- **Keine „Auf Anfrage"-Preisverschleierung** – klare Preise auf der Verkaufsseite
---
## 5. Differenzierungs-Hebel
### Hebel 1: Friction-freier Einstieg (sofort wirksam)
**Botschaft auf der Verkaufsseite:**
> „Kein Abo-Zwang. Monatstarife monatlich kündbar. Faire Preise."
_(Aktualisiert 07.07.2026: Jahrestarife verlängern sich um jeweils zwölf Monate und sind zum Laufzeitende kündbar — die Botschaft „monatlich kündbar" gilt nur für Monatstarife, siehe `Decision-Update Preisstruktur` §3.1.1.)_
Direkter Konter gegen openPR (12 Monate Mindestlaufzeit, jährliche Vorauszahlung) und Pressebox (ähnliches Modell). Stärkster Hebel ohne Entwicklungsaufwand – reine Marketingbotschaft.
### Hebel 2: SEO-Substanz und Vertrauen herausstellen
Auf der Verkaufsseite kommunizieren:
- „Online seit 2008"
- „Über 100.000 Pressemitteilungen archiviert"
- Domain-Autorität als Trust-Signal
- Echte Aufrufzahlen pro PM (statt nur „Reichweite versprochen")
Wirkt bei B2B-Käufern stark, weil Vertrauen signalisiert wird – etwas, das KI-Tool-Plattformen mit 2 Jahren Marktpräsenz nicht liefern können.
### Hebel 3: Faires Korrektur- und Änderungsmodell
**Was Wettbewerber machen:** openPR berechnet 10 € pro Änderung, auch bei Tippfehlern.
**Was wir machen:**
- Tippfehler / Kontaktdaten-Korrekturen: kostenfrei (Self-Service)
- Inhaltliche Korrektur: mit transparentem Korrekturhinweis
- Updates: als Anhang an Original
- Tombstone bei Löschung statt Hard-Delete (SEO-Erhalt)
- Anonymisierung bei DSGVO-Anfragen ohne Diskussion
Adressiert echten Schmerz von Bestandskunden anderer Portale.
### Hebel 4: Smarte Add-ons mit Konversionslogik
Credit-System ermöglicht margenstarke Zusatzumsätze nach der Erstbuchung:
- Highlight-Buchung im Anschluss („7 Tage prominent platziert")
- KI-Bildgenerierung
- Cross-Post zwischen presseecho.de und businessportal24.de
- KI-Quality-Check / Stilverbesserung
- Newsletter-Erwähnung (wenn Newsletter aufgebaut)
### Hebel 5: Distribution über die richtigen Kanäle
**Falsch wäre:** Eigene Social-Media-Accounts der Portale aufbauen → kostet Zeit, baut keine Reichweite auf, kann Reputation schaden.
**Richtig ist:**
- **Share-Funktionen für Kunden:** Nach Veröffentlichung vorgefertigte Share-Texte für LinkedIn, X, Facebook, WhatsApp
- **LinkedIn priorisieren:** B2B-Distribution läuft 2026 dort, nicht auf X
- **Open-Graph- und Twitter-Card-Tags** sauber konfigurieren, damit geteilte Links gut aussehen
- **Optional Phase 3:** kuratierter „Best of"-Channel mit harter KI-Score-Schwelle (>85), nur wenn Zeit dafür da ist
### Hebel 6: Brand-Schutz absichern
- DPMA-Marken für presseecho.de und businessportal24.de prüfen, ggf. anmelden (~290 € pro Wortmarke)
- Google Ads Brand-Schutz-Kampagnen für eigene Portalnamen (~20–50 €/Monat)
- Verhindert, dass Wettbewerber bei Markensuchen oben stehen
---
## 6. Preismodell (bewusst gegen den Markt)
|Tier|Preis|Pressemitteilungen|Inklusive|
|---|---|---|---|
|**Einzel**|29 € / Stück|1|Free-Stock, KI-Quali-Check|
|**Starter**|19 €/Mo. oder 190 €/Jahr|3/Monat, weitere à 15 €|Free-Stock, KI-Quali|
|**Business**|59 €/Mo. oder 590 €/Jahr|10/Monat|+ 1 Highlight, 5 Premium-Stock, 10 KI-Bilder|
|**Pro**|119 €/Mo. oder 1.190 €/Jahr|unbegrenzt (Fair Use)|+ 3 Highlights, größere Kontingente, Priority|
|**Agency**|249 €/Mo. oder 2.490 €/Jahr|unbegrenzt für bis zu 5 Marken|alles aus Pro × Marken, je weitere Marke 39 €/Mo.|
**Eckpunkte:**
- Monatstarife monatlich kündbar, Jahrestarife zum Ende der Zwölfmonats-Laufzeit
- Jahrespreise mit ~15 % Rabatt
- Credit-System für flexible Add-ons
- Mengenrabatte ab 3 PMs in 30 Tagen (z. B. 20 % auf nächste PM)
- Reaktivierungs-Gutschein für Bestandskunden zum Relaunch (z. B. 50 % auf nächste PM)
**Launch-Aktion:** „Erste PM für 9 € statt 29 €" – einmalig pro Account, neue Accounts auf gleiche Firma/E-Mail werden erkannt.
---
## 7. Akquise-Strategie
### Phase 1: Bestandskundenreaktivierung (sofort)
- Persönliche Kontaktaufnahme zu aktiven Bestandskunden (20 Telefonate vor Relaunch wertvoller als jedes Marketingkonzept)
- Reaktivierungs-Mailing an inaktiven Stamm mit konkretem Gutschein
- Realistischer Erwartungswert: 5–15 % Reaktivierung bei persönlicher Ansprache, 1–3 % bei Massenmail
### Phase 2: Organische Sichtbarkeit (mittelfristig)
- Saubere Migration ohne SEO-Verlust (saubere Redirects, Tombstone-Modell, schnelle Ladezeiten)
- Bestandsarchiv als Long-Tail-Magnet pflegen
- Content-Marketing über das Portal selbst (redaktionelle Übersichten, Branchen-Specials)
### Phase 3: Empfehlungsmarketing (langfristig)
- Empfehlungs-System: Bestandskunden bekommen Rabatt für gebrachte Neukunden
- Funktioniert in B2B-Nischen besser als bezahltes Marketing, weil Empfehlungen das Vertrauensthema lösen
### Bezahlte Werbung – realistisch eingesetzt
- **Brand-Schutz** (Pflicht): Eigene Brandnames bei Google Ads buchen (~20–50 €/Monat)
- **Long-Tail-Branchenkeywords** (Test): „Pressemitteilung [Branche] veröffentlichen" mit kleinem Budget (200–300 €/Monat) testen
- **Nicht** auf Hauptkeywords wie „Pressemitteilung veröffentlichen" bieten – CPCs zu hoch, ROI bei Solo-Setup nicht gegeben
---
## 8. Connektar.de als Distribution-Partner
50 % des aktuellen Traffics läuft über connektar.de. Risiko und Asset zugleich.
**Strategie:**
- Weiterlaufen lassen, aber Abhängigkeit transparent machen
- Eigenständige Akquise parallel aufbauen
- Datenmodell vorbereiten: `source: distribution_partner`, separate Statistiken
- Qualitäts-SLA tracken (KI-Ablehnungsrate)
- Bei Bedarf in Zukunft: eigenes Kündigungsrecht bei Qualitätsproblemen verhandeln
---
## 9. Technisches Setup
- **Backend:** Laravel
- **Frontend:** Tailwind, responsive
- **Hosting:** Hetzner (~10 €/Monat)
- **Deployment:** Ploi
- **Tracking:** Umami self-hosted auf separater VM (DSGVO-konform, kein Cookie-Banner, kein Adblocker-Problem)
- **Zusätzlich:** Serverseitiger Aufruf-Counter in Laravel für verlässliche PM-Statistiken
---
## 10. Phasen-Roadmap
### Phase 1 – MVP / Relaunch
- Backend-Migration auf Laravel
- Responsive Frontend mit Tailwind
- 1:1-Datenmigration der Bestands-PMs
- Magic-Link-Login für Self-Service-Änderungen
- Korrektur- und Tombstone-Modell
- KI-Vorprüfung neuer PMs
- Credit-System mit Tarifstruktur
- Share-Buttons mit Open-Graph-Tags
- Marketingbotschaft „Keine Mindestlaufzeit" prominent
- Brand-Schutz-Kampagnen aktivieren
- Bestandskundenreaktivierung
### Phase 2 – Konsolidierung (3–6 Monate nach Launch)
- Cross-Post zwischen den beiden Portalen
- Highlight-Buchungen mit eleganter UX
- KI-Score-Anzeige für Kunden (Transparenz)
- Statistik-Dashboard für Kunden (eigene Aufrufzahlen)
- Empfehlungsmarketing-System
### Phase 3 – Differenzierung (6–12 Monate)
- Schärfere Markentrennung presseecho.de vs. businessportal24.de
- LinkedIn-Auto-Post für Pro/Agency-Tier
- Optional: kuratierter Best-of-Social-Channel mit hoher Schwelle
- Newsletter-Aufbau, Newsletter-Erwähnung als Add-on
### Phase 4 – Wachstum & Optimierung (laufend)
- Quartals-Rhythmus für kontinuierliche Verbesserungen
- Bestandskunden regelmäßig befragen
- Tarifmodell datenbasiert nachschärfen
- Bei stabiler Basis: ggf. weitere Add-ons / Premium-Features
---
## 11. Erfolgskontrollen und Stopps
Klare Stopps definieren, um nicht im Zombie-Modus zu landen:
**Nach 9 Monaten Live-Betrieb prüfen:**
- Wachsender Bestandskundenstamm?
- Planbarer monatlicher Umsatz erkennbar?
- Connektar-Anteil sinkend (Eigenakquise zieht)?
- Reaktivierungsrate alter Kunden im Erwartungsbereich?
**Falls keine positive Entwicklung sichtbar:**
- Konsolidierung auf eine Marke prüfen
- Verkaufsoption prüfen
- Reines Archiv-Modell mit minimaler Pflege erwägen
- Kein „weiter so" aus Trägheit
**Anti-Zombie-Regel:** Mindestens einmal pro Quartal ein halber Tag „Was muss verbessert werden". Ohne Ausnahme.
---
## 12. Strategische Leitlinien zusammengefasst
1. **Nische besetzen, nicht konkurrieren.**
2. **Modernes Verständnis von Pressemitteilungen** kommunizieren – digitale Sichtbarkeit, SEO, ehrliche Versprechen.
3. **Friction-frei sein**, wo Wettbewerber Friction aufbauen (Mindestlaufzeit, Korrekturgebühren, intransparente Preise).
4. **100.000 PMs als Asset** kommunizieren – nicht reproduzierbar.
5. **Empfehlungsmarketing in B2B-Nische** ist langfristig stärker als bezahltes Marketing.
6. **Geduld als Wettbewerbsvorteil** – kein Investorendruck heißt: organisch wachsen können.
7. **Konsistenz schlägt Spektakel** – kleine kontinuierliche Verbesserungen, alle 6–12 Monate ein nennenswertes Feature.
8. **Erfolg = stabiles Nebeneinkommen**, nicht Marktführerschaft.
9. **Bewusste Selbstverpflichtung** auf das, was man nicht macht.
10. **Bei jeder Feature-Entscheidung fragen:** Passt das zu einem ruhigen, fairen, durchdachten Portal – oder zu einem aggressiven Wachstums-Tool?
---
_Diese Strategie ist ein lebendes Dokument. Sie sollte mindestens einmal jährlich überprüft und an die tatsächliche Marktentwicklung angepasst werden._

View file

@ -1,450 +0,0 @@
Stand: Mai 2026 Zweck: Ersatz bzw. Ergänzung der Abschnitte 8, 9, 10 sowie neue Abschnitte zur Score-Architektur, Boost-Eligibilität und zum Tool-Loop. Datenmodell-Ergänzungen am Ende.
> **IST-Stand 11.06.2026**:
>
> - **§8–10 (Tarife, Credits, Preisliste) sind ueberschrieben** durch das
> [`Decision-Update Preisstruktur & Veröffentlichungs-Flow`](../Decision-Update%20Preisstruktur%20&%20Ver%C3%B6ffentlichungs-Flow.md)
> (11.06.2026): neue Kontingente (Pro 25 statt 60, Agency 60 statt 150),
> Jahrespreis als „2 Monate gratis", Bonus-Credits aus der Tarif-Tabelle
> entfernt, Launch-Credits auf Extra-PM/Boost/PDF-Nachweis reduziert.
> Die Abschnitte unten bleiben als urspruengliche Zielvorstellung lesbar.
> - **§15.1 (Klassifikations-Score) und §15.2 (Content-Score) sind umgesetzt**
> (11.06.2026, siehe `docs/user-admin/Entwicklungsplan KI-Pruefung und
> Veroeffentlichung.md`). §15.3 (Trust-Score), §16 (Boost) und §17
> (Tool-Loop) sind weiterhin offen.
> - Zahlung/Stripe, Tarif-Datenmodell, Credit-Pakete und Auto-Refill sind
> **nicht** umgesetzt. Im Code existiert nur der Phase-8-**Quota-Stub**
> (`users.press_release_quota`, zaehlt beim Einreichen — laut
> Decision-Update kuenftig bei Veroeffentlichung).
---
## 8. Preismodell – Tarife (überarbeitet)
### Grundlogik
Alle Tarife enthalten ein Kontingent an Pressemitteilungen sowie monatlich ausgeschüttete Bonus-Credits für Tools und Add-ons. Bonus-Credits aus Abos verfallen monatlich, gekaufte Credits bleiben 24 Monate gültig. So bleibt das Abo aktivierungsstark, ohne dass der Nutzer eigenes Geld verliert.
### Tier-Struktur
| Tier | Preis | PMs | Bonus-Credits/Mo. | Effektiver PM-Preis | Besonderheiten |
| ------------ | ------------------------ | ----------------------- | ------------------------ | ------------------- | --------------------------------------------------------------- |
| **Einzel** | 19 € / Stück | 1 | 4 (verfallend nach 30 T) | 19,00 € | Pay-as-you-go |
| **Starter** | 29 €/Mo. (290 €/Jahr) | 3 | 12 | 9,67 € | Free-Stock, KI-Quality-Check |
| **Business** | 49 €/Mo. (490 €/Jahr) | 10 | 30 | 4,90 € | Erweiterte Statistiken, optionaler Newsroom |
| **Pro** | 99 €/Mo. (990 €/Jahr) | unbegrenzt (Fair Use) | 60 | < 2 € | Eigener Newsroom, Priority, volles Statistik-Dashboard |
| **Agency** | 199 €/Mo. (1.990 €/Jahr) | unbegrenzt für 5 Marken | 120 | < 1 € | Multi-Redakteur-Workflow, API-Zugang, je weitere Marke 29 €/Mo. |
Jahrespreise mit ca. 17 % Rabatt eingebaut. Fair Use im Pro-Tarif: Soft-Cap 50 PMs/Monat.
### Mehrwerte im Vergleich
|Feature|Einzel|Starter|Business|Pro|Agency|
|---|---|---|---|---|---|
|Pressemitteilungen|1|3/Mo.|10/Mo.|unbegr.|unbegr. (5 Marken)|
|Bonus-Credits|4 einmalig|12/Mo.|30/Mo.|60/Mo.|120/Mo.|
|Free-Stock-Bilder|✓|✓|✓|✓|✓|
|KI-Quality-Check|✓|✓|✓|✓|✓|
|Erweiterte Statistiken|–|–|✓|✓|✓|
|Eigener Newsroom|–|–|optional|inkl.|inkl.|
|Priority-Support|–|–|–|✓|✓|
|Multi-Redakteur-Workflow|–|–|–|–|✓|
|API-Zugang|–|–|–|–|✓|
### Kommunikation
Die inkludierten Bonus-Credits sind Teil des Pakets, nicht zusätzliche Kosten. Reicht das Kontingent nicht (z. B. weil mehrere PMs mit aufwändigem Tooling veröffentlicht werden), kauft der Nutzer Credits nach – diese bleiben 24 Monate erhalten und schaffen langfristige Bindung an die Plattform.
### Bestandskunden
Aktive Jahresabos behalten Preis bis zum nächsten Verlängerungstermin. Loyalty-Bonus 10–20 % im ersten Verlängerungsjahr. Downgrade-Pfad anbieten.
### Einstiegsstrategie
In der Anfangsphase (erste 6–12 Monate nach Relaunch) bewusst günstiger einsteigen, um User-Base aufzubauen. Preise sind kalkuliert mit Spielraum für spätere Anpassung. Wichtig: Bestandskunden behalten ihre Konditionen.
---
## 9. Credit-System (überarbeitet)
### Grundregel
**1 Credit = 1 €** als Listenpreis-Anker. Alle Service-Preise werden in ganzen Credits ausgewiesen. Wer größere Pakete kauft, zahlt effektiv weniger pro Credit (Volumenrabatt), aber der Listenpreis bleibt stabil. So entfällt jede Kopfrechen-Übung im UI.
### Credit-Pakete
|Paket|Credits|Preis|Effektiv pro Credit|Ersparnis|
|---|---|---|---|---|
|Test|10|10 €|1,00 €|–|
|Standard|50|45 €|0,90 €|10 %|
|Plus|150|120 €|0,80 €|20 %|
|Pro|500|375 €|0,75 €|25 %|
|Business|1.500|1.050 €|0,70 €|30 %|
Ganzzahlige Beträge, keine Bruchteile im UI. Intern kann auf Cent-Ebene abgerechnet werden, aber nach außen sieht der Nutzer nur ganze Credits.
### Auto-Refill
Standardmäßig nach erstem Kauf aktiviert (mit Opt-Out):
- Trigger: bei < 10 Credits Restguthaben
- Aufladung: zuletzt gekauftes Paket (Default Standard, 50 Credits)
- Eindeutige Bestätigungs-Mail nach jeder automatischen Aufladung
### Gültigkeit
- Gekaufte Credits: 24 Monate ab Kauf
- Bonus-Credits aus Abos: monatlich verfallend
- Willkommens-Bonus (5 Credits einmalig bei Account-Anlage): 90 Tage
### Mini-Checkout (kontextuell)
1. User klickt z. B. „KI-Bild generieren"
2. Modal: _„Kostet 4 Credits. Du hast 2 Credits."_
3. Optionen:
- „Schnell aufladen: Standard-Paket (50 Credits, 45 €)" – 1-Klick mit Saved Payment Method
- „Anderes Paket wählen"
- „Abbrechen"
4. Nach Aufladung wird Aktion automatisch ausgeführt
### Erstkauf
Stripe Checkout mit `setup_future_usage` für Saved Payment Method. Danach 1-Klick-Aufladung.
### Dashboard
- Credit-Stand oben rechts immer sichtbar
- Trennung sichtbar: Bonus-Credits (verfallend) vs. gekaufte Credits (24 Monate)
- Verlauf einsehbar (was wofür verbraucht)
- Rechnungs-PDFs für jede Aufladung
### Buchhaltung & Recht
- Credits = Vorauszahlung, bilanziell als Verbindlichkeit
- MwSt-Behandlung mit Steuerberater abstimmen (Kauf vs. Verbrauch)
- Verfall in AGB sauber dokumentieren
- Keine Auszahlung in Geld (sonst PSD2-Lizenzthema)
- EU-Auslandskunden: Reverse-Charge bei B2B mit USt-ID
---
## 10. Preisliste in Credits (überarbeitet)
Alle Preise in ganzen Credits (1 Credit = 1 €). Anker-Werte für die Startphase, iterativ anpassbar.
### Veröffentlichung
|Service|Credits|
|---|---|
|Standard-PM (Pay-as-you-go)|19|
|PM-Korrektur (Pfad C)|8|
|PM-Update (Pfad D)|4 _(im ersten Jahr ggf. kostenlos)_|
|Depublizierung (Pfad G)|19–25|
### Bilder
|Service|Credits|
|---|---|
|Free-Stock (Unsplash, Pexels)|0|
|Premium-Stock (Adobe, Shutterstock)|8|
|KI-Bild generieren|4|
|KI-Bild Re-Generation|2|
### KI-Textservices
|Service|Credits|
|---|---|
|Quality-Check (Stil/Pressestil)|3|
|Lektorat|8|
|Pressetext-Optimierung (Headlines, SEO)|15|
|Headline-Booster (nur Headlines)|5|
|PM aus Stichworten generieren|25|
|Übersetzung (DE↔EN)|12|
### Platzierungen
|Service|Credits|
|---|---|
|Highlight Kategorie (3 Tage)|15|
|Highlight Kategorie (7 Tage)|30|
|Startseite-Highlight (24 h)|39|
|Startseite-Highlight (3 Tage)|89|
|Top-Slot Startseite (24 h)|119|
|Newsletter-Erwähnung|59|
|Social-Share (offizieller Kanal)|25|
Voraussetzung für alle Platzierungen: Mindest-Content-Score erreicht (siehe Abschnitt „Boost-Eligibilität").
### Distribution
|Service|Credits|
|---|---|
|PDF-Export mit Branding|2|
|Social-Snippet-Generierung|3|
|Verteiler-Versand (klein, branchenspezifisch)|39|
|Verteiler-Versand (mittel)|99|
|Verteiler-Versand (groß, branchenübergreifend)|199|
### Account / Profil
|Service|Credits|
|---|---|
|Verifiziertes Firmenprofil (einmalig)|79|
|Custom Subdomain (pro Jahr)|49|
|Erweiterte Statistiken (pro Monat)|15|
### Goodies (kostenlos, fördern Aktivität)
- PM-Updates kostenfrei im ersten Jahr (besseres Archiv)
- 3 Free-Stock-Bilder pro PM
- Erster KI-Quality-Check pro PM kostenfrei
- 5 Credits Willkommens-Bonus bei Account-Anlage (90 Tage gültig)
- Headline-Vorschlag (1 Variante) kostenfrei pro PM
---
## NEU – Abschnitt 15: Score-Architektur
Die Plattform arbeitet mit drei voneinander unabhängigen Scores. Sie haben unterschiedliche Funktionen, werden unterschiedlich berechnet und an unterschiedlichen Stellen wirksam. Die Trennung ist zentral, weil sie unterschiedliche Datenmodelle und Update-Logiken betrifft.
### 15.1 Klassifikations-Score (Eintritts-Filter)
**Funktion:** Entscheidet, ob eine Pressemitteilung überhaupt veröffentlicht wird.
**Bereich:** Grün / Gelb / Rot (kategorial)
**Faktoren:**
- Werbung statt Pressemitteilung
- Beleidigend / diskriminierend
- Rechtlich heikel
- Spam-Muster
- Unseriöse Versprechen
**Auswirkung:**
- Grün: Direkte Veröffentlichung (optional 5–10 Min Verzögerung)
- Gelb: Manuelle Review-Queue
- Rot: Zurück an User mit Begründung
**Aktualisierung:** Einmalig bei Einreichung. Bei Änderung der PM (Pfad C/D) wird neu klassifiziert.
**Speicherung:** `press_releases.classification` plus vollständiges Audit-Log in `ki_audits`.
### 15.2 Content-Score (Qualitäts-Indikator)
**Funktion:** Misst die handwerkliche Qualität einer Pressemitteilung. Bestimmt organische Sichtbarkeit und Boost-Berechtigung.
**Bereich:** 0–100 Punkte
**Faktoren (Vorschlag, iterativ verfeinerbar):**
|Kategorie|Gewichtung|Was zählt|
|---|---|---|
|Pressestil|20 %|Tonalität (informativ vs. werblich), passive vs. aktive Konstruktion, Zitate vorhanden|
|Struktur|15 %|Lead-Absatz vorhanden, sinnvolle Absatzstruktur, Pyramidaler Aufbau|
|Lesbarkeit|10 %|Flesch-Index für Deutsch, Satzlängen, Fachsprache angemessen|
|Vollständigkeit|15 %|Pressekontakt, Unternehmensinfo, Datum, Branche, Region|
|Bildmaterial|10 %|Mindestens 1 Bild, Auflösung, Alt-Text, Bildunterschrift|
|Quellen / Belege|10 %|Verlinkungen, Studien-Referenzen, Datenquellen|
|Headline-Stärke|10 %|Länge, Keyword-Relevanz, Klarheit|
|Originalität|10 %|Kein Boilerplate, kein Duplicate-Content, individueller Ton|
**Auswirkung:**
- **Organische Sichtbarkeit:** Listing-Position, Top-Story-Kandidat, Newsletter-Aufnahme, Trending in Branche
- **Boost-Berechtigung:** Schwellenwerte für kostenpflichtige Slots (siehe Abschnitt 16)
- **User-Feedback:** Sichtbar im Editor-Dashboard mit konkreten Verbesserungsvorschlägen
**Aktualisierung:** Bei Einreichung berechnet, bei jeder Änderung der PM neu berechnet. History pro PM in `content_scores`.
**Speicherung:** `press_releases.content_score` (aktueller Wert), `content_scores` (History mit Faktor-Breakdown).
### 15.3 Trust-Score (Reputations-Indikator)
**Funktion:** Bewertet die Zuverlässigkeit eines Publishers über Zeit. Reduziert Moderationslast und kann öffentliche Anerkennung bringen.
**Bereich:** 0–100 oder Stufen (Bronze / Silber / Gold / Verifiziert)
**Faktoren:**
- Anzahl problemfrei veröffentlichter PMs
- Durchschnittlicher Content-Score über alle PMs
- Beschwerderate (Reports, Korrekturen, Depublizierungen)
- Account-Alter
- Verifikations-Status (verifiziertes Firmenprofil)
**Auswirkung:**
- **Moderation:** Lockerung der KI-Freigabe-Schwelle (mehr „Grün" automatisch)
- **Sichtbarkeit (optional):** öffentliches Verifizierungs-Badge auf Newsroom und PM-Seiten
- **Bevorzugung in Branchen-Übersichten** bei gleichem Content-Score
- **Bei Trust-Verlust:** Rückfall in strengere Moderation (auch nach Beschwerden, häufigen Korrekturen, Depublizierungen)
**Aktualisierung:** Rollierend, z. B. nächtlicher Cron-Job über die letzten 90 Tage Aktivität.
**Speicherung:** `accounts.trust_score`, `accounts.trust_tier` (Bronze/Silber/Gold/Verifiziert), History in `trust_score_log`.
### Offene Detail-Entscheidungen
- Trust auf User- oder auf Firmen-Ebene? (Empfehlung: Firmen-Ebene, weil Mitarbeiter wechseln)
- Trust-Verlust: ab welchen Schwellen?
- Verifizierungs-Badge: nur über kostenpflichtigen Verifizierungs-Prozess oder auch durch Trust-Score erreichbar?
---
## NEU – Abschnitt 16: Boost-Eligibilität
Die Verbindung zwischen Score-System und kostenpflichtigen Sichtbarkeits-Slots. Grundprinzip: **Schlechter Content kann nicht in den Top-Slot gekauft werden.** Das schützt die redaktionelle Glaubwürdigkeit der Plattform und schafft den Anreiz, in Qualität zu investieren.
### Schwellenwerte je Slot-Typ
|Slot|Klassifikation|Min. Content-Score|
|---|---|---|
|Highlight Kategorie|Grün|50|
|Startseite-Highlight (24h / 3 T)|Grün|65|
|Top-Slot Startseite|Grün|75|
|Newsletter-Erwähnung|Grün|70|
|Social-Share (offizieller Kanal)|Grün|70|
|Verteiler-Versand (extern)|Grün|80|
PMs mit Klassifikation Gelb können nicht boostbar werden, auch nicht nach manueller Freigabe – sie bleiben in regulärer Sichtbarkeit. PMs mit Klassifikation Rot werden nicht veröffentlicht und sind damit irrelevant.
### UI-Logik
Wenn ein User einen Boost-Slot bucht, dessen Schwelle seine PM nicht erreicht, sieht er statt des Buchungsformulars:
> _„Diese Pressemitteilung erreicht aktuell einen Content-Score von 60/100. Für den Top-Slot Startseite empfehlen wir mindestens 75 Punkte. So kannst du deinen Score verbessern:"_
>
> _[Pressetext-Optimierung – 15 Credits → +15–20 Punkte]_ _[Headline-Booster – 5 Credits → +3–7 Punkte]_ _[Bild hinzufügen – 4 Credits → +5–10 Punkte]_
Nach Tool-Anwendung wird der Score neu berechnet, der Slot kann dann gebucht werden.
### Effekt
Drei gewollte Konsequenzen:
1. **Plattform-Qualität bleibt hoch:** Premium-Slots zeigen nur qualitativ hochwertige Inhalte.
2. **Tools werden indirekt verkauft:** Wer den Slot will, muss in Qualität investieren – entweder selbst oder über kostenpflichtige Tools.
3. **Glaubwürdigkeit für Leser bleibt erhalten:** Leser und Journalisten erkennen schnell, dass sichtbar platzierte Inhalte tatsächlich relevant sind.
### Sonderfall: Editorial-Pick
Unabhängig vom Boost-System kann die Redaktion (intern) PMs als „Empfehlung der Redaktion" hervorheben. Das ist ein redaktionelles Instrument, kein kommerzielles, und nicht buchbar. Wirkt als Vertrauensanker auf der Startseite.
---
## NEU – Abschnitt 17: Tool-zu-Algorithmus-Loop
Der strategische Kern der Monetarisierungslogik. Der Loop verbindet drei Plattform-Ziele in einem geschlossenen System:
### Die drei Ziele
1. **Plattform-Qualität:** Hohe durchschnittliche Inhaltsqualität, damit Leser, Journalisten und Mediaplaner die Plattform als seriös wahrnehmen.
2. **Monetarisierung:** Umsatz aus Tools, Tarifen und Boost-Slots.
3. **Anreiz für Publisher:** Sichtbar gute Platzierungen für gute Inhalte als motivierender Faktor.
### Der Loop
```
Publisher schreibt PM
↓
Content-Score wird berechnet (z. B. 55/100)
↓
Publisher will Top-Slot buchen (Schwelle 75)
↓
System empfiehlt: Pressetext-Optimierung (15 Credits)
↓
Tool wird angewendet, Score steigt auf 78
↓
Top-Slot wird gebucht (119 Credits)
↓
PM erscheint prominent auf Startseite
↓
Hohe Reichweite, gute Statistiken
↓
Publisher sieht Wert, kommt wieder
↓
Plattform-Durchschnittsqualität steigt
↓
Mehr Leser, mehr Wert für nächsten Boost-Käufer
```
### Voraussetzungen für Funktionieren
- **Tools müssen tatsächlich gut sein.** Wenn das KI-Lektorat schlechter ist als das, was der Publisher selbst zustande bringt, kollabiert der Loop. → Tool-Qualität ist Wettbewerbsvorteil, hier wird investiert.
- **Score-Verbesserung muss spürbar und nachvollziehbar sein.** Der Publisher muss verstehen, was sein Tool-Einsatz konkret gebracht hat. → Score-Breakdown sichtbar, Vorher-Nachher-Vergleich.
- **Reichweite muss real sein.** Ein gekaufter Top-Slot muss tatsächlich Reichweite bringen. → Leser-Seite (Newsletter, SEO, Social) muss aktiv aufgebaut werden.
- **Boost-Schwellen dürfen nicht zu hoch sein.** Sonst wird der Loop frustrierend statt motivierend. → Schwellen iterativ kalibrieren auf Basis realer Score-Verteilung.
### Was das für den Build bedeutet
- **Tools haben strategische Priorität.** KI-Lektorat, Pressetext-Optimierung, Headline-Booster sind nicht nur Add-ons, sondern das Herzstück der Wertschöpfung.
- **Score-Anzeige muss früh implementiert werden.** Ohne sichtbaren Score kein Loop.
- **Statistik-Dashboard ist Pflicht für mittlere Tarife.** Ohne sichtbare Reichweiten-Daten erkennen Publisher den Wert ihres Investments nicht.
---
## 13. Datenmodell-Skizze – Ergänzungen
Zusätzlich zu den bestehenden Tabellen aus dem Hauptkonzept:
```
content_scores
- id, press_release_id
- score (0-100), version (bei Neuberechnung)
- factors (JSON: pressestil, struktur, lesbarkeit, vollstaendigkeit,
bildmaterial, quellen, headline, originalitaet)
- calculated_at, calculation_reason (initial/edit/tool_applied)
placements
- id, press_release_id, account_id
- slot_type (kategorie_highlight, startseite_highlight, top_slot,
newsletter, social_share, verteiler_klein/mittel/gross)
- starts_at, ends_at
- credits_spent
- status (scheduled, active, completed, cancelled)
- eligibility_check_passed (bool, snapshot bei Buchung)
- eligibility_score_snapshot (Content-Score zum Zeitpunkt der Buchung)
- created_at
placement_inventory
- id, slot_type
- max_concurrent (z.B. 1 für Top-Slot, 3 für Startseite-Highlight)
- duration_options (JSON: [24h, 72h])
- min_content_score (75)
- min_classification ('green')
trust_score_log
- id, account_id
- score (0-100), tier (bronze/silber/gold/verifiziert)
- factors (JSON: pm_count, avg_content_score, complaints,
account_age_days)
- calculated_at
accounts (Ergänzungen)
- + trust_score (int, 0-100)
- + trust_tier (enum)
- + verified_business_profile (bool)
- + verified_at
```
Wichtige Logiken:
- **placement_inventory** definiert, wie viele Slots welcher Art parallel verfügbar sind. Bei Buchung wird geprüft: ist ein Slot für das gewünschte Zeitfenster frei? Wenn nicht: nächstmöglicher Termin anbieten oder ablehnen.
- **eligibility_score_snapshot** auf Placement-Ebene: damit nachvollziehbar bleibt, mit welchem Score eine PM zum Buchungszeitpunkt qualifiziert war. Wenn der Score später sinkt (etwa durch Korrektur), bleibt der gebuchte Slot bestehen, aber bei Verlängerung wird neu geprüft.
- **content_scores** mit Versionierung erlaubt nachträglich Auswertung: Welche Tools haben welchen Score-Effekt gehabt? Daten für Tool-Optimierung.
---
## Offene Punkte / nächste Entscheidungen (Update)
Zusätzlich zu den bereits dokumentierten Punkten:
- **Content-Score-Faktoren feinjustieren:** Welche Gewichtung passt für deutsche Pressemitteilungen? Iterativ kalibrieren mit echten Daten.
- **Boost-Schwellen kalibrieren:** Erst nach 100–200 echten PMs sehen, wo die Score-Verteilung liegt. Schwellen ggf. anpassen.
- **Trust-Score: User vs. Firma:** Empfehlung Firma, aber Detail-Logik bei Mitarbeiterwechsel klären.
- **Tool-Qualität:** KI-Prompts für Lektorat und Pressetext-Optimierung müssen sehr sauber gebaut werden. Eigene Test-Suite mit Vorher/Nachher-PMs.
- **Slot-Inventory:** Wie viele Top-Slots parallel? Empfehlung 1 (sonst verliert er an Wert), Startseite-Highlight 3, Kategorie-Highlight 5–10 je Branche.
- **Editorial-Picks:** Wer wählt aus? Anfangs du selbst, später ggf. Redaktions-Account mit Frontend-Tool.

View file

@ -1,203 +0,0 @@
Stand: Mai 2026 Zweck: Ersetzt die Außenkommunikation des Content-Scores durch ein dreistufiges System. Aktualisiert Abschnitt 15.2 (Content-Score) und Abschnitt 16 (Boost-Eligibilität) aus dem ersten Konzept-Update.
> **IST-Stand 11.06.2026**: Der Content-Score mit Stufen-System ist
> **umgesetzt** (siehe `docs/user-admin/Entwicklungsplan KI-Pruefung und
> Veroeffentlichung.md`, Phase 5): `content_score`/`content_tier` auf
> `press_releases`, Schwellen Geprueft ≥ 60 / Hochwertig ≥ 80 kalibrierbar
> in `config/scoring.php`, Editor-Score-Panel, Admin-Badges und
> oeffentliches Stufen-Badge in der Customer-Ansicht (Standard ohne Badge).
> Offen: Stufen-Badges im oeffentlichen Web-Frontend, Boost-Eligibilitaet
> nach Stufe (zum Launch laut Decision-Update: Boost nur fuer gruene PMs)
> sowie User-/Firmen-Trust-Score (Phase 3).
---
## Hintergrund
Der Content-Score (0–100) bleibt als plattform-internes Steuerungsinstrument bestehen. Für die Außenkommunikation gegenüber Lesern wird die konkrete Punktzahl jedoch durch ein dreistufiges Stufen-System ersetzt. Gründe:
- Konkrete Punktzahlen wirken meta-fetischistisch und lenken vom Inhalt ab
- Vergleichbarkeit zwischen PMs ("warum nur 67?") erzeugt Konflikte ohne Mehrwert
- Goodhart's Law: Publisher würden auf die Zahl optimieren statt auf Qualität
- Stufen sind kulturell etabliert (Nutri-Score, Stiftung Warentest) und sofort verständlich
Die Punktzahl bleibt dort erhalten, wo sie produktiv ist: im Editor während des Schreibens, im Publisher-Dashboard und in Boost-Buchungs-Dialogen.
---
## 15.2 Content-Score (überarbeitet)
### Score und Stufen
Der Content-Score (0–100) wird Plattform-intern unverändert berechnet. Für alle nach außen gerichteten Anzeigen gilt folgendes Mapping:
|Stufe|Score-Bereich|Bedeutung|
|---|---|---|
|**Standard**|30 – 59|Mindestqualität erreicht, regulär veröffentlicht|
|**Geprüft**|60 – 79|Solide Pressemitteilung, gute Substanz, redaktionelle Standards eingehalten|
|**Hochwertig**|80 – 100|Top-Qualität, redaktioneller Maßstab|
PMs unterhalb von Score 30 werden vom Klassifikations-Score (Grün/Gelb/Rot) abgefangen und entweder in die manuelle Review-Queue gegeben oder zurück an den Autor verwiesen. Auf der öffentlichen Plattform sind ausschließlich PMs ab Stufe Standard sichtbar.
Schwellenwerte sind als Anker zu verstehen und werden nach 100–200 echten PMs anhand der tatsächlichen Score-Verteilung kalibriert.
### Sichtbarkeit pro Stufe
Nicht jede Stufe wird gleich behandelt. Standard wird nicht beworben – weder als Badge noch als Label. Erst ab Stufe Geprüft erscheint sichtbar ein Vertrauensindikator. So wirkt das System nicht wie ein Stigma für Standard-PMs, sondern wie eine Auszeichnung für die besseren.
|Stufe|Auf Detailseite|In Listen|Im Newsletter-Filter|
|---|---|---|---|
|Standard|nichts angezeigt|nichts angezeigt|enthalten in "alle Meldungen"|
|Geprüft|Häkchen-Icon mit Tooltip "Geprüfte Pressemitteilung"|optional kleines Häkchen-Icon|"Geprüfte Pressemitteilungen"|
|Hochwertig|Label "Hochwertig" mit Stern-Icon|Stern-Icon neben Headline|"Hochwertige Pressemitteilungen"|
### Wo die Punktzahl sichtbar bleibt
Die genaue Punktzahl bleibt produktiv im geschützten Bereich:
**Im Editor während des Schreibens:**
> _"Aktueller Score: 67/100 – Stufe: Geprüft. Noch 13 Punkte bis 'Hochwertig'. So verbessern Sie Ihre Pressemitteilung:_ _• Bild hinzufügen (+5–10 Punkte)_ _• Zitat einbauen (+3–5 Punkte)_ _• Lead-Absatz präziser fassen (+2–4 Punkte)"_
**Im Publisher-Dashboard:**
- Score pro PM mit Stufenanzeige
- Durchschnittsscore über alle PMs
- Trend über Zeit
- Score-Breakdown nach Faktoren
**In Boost-Buchungs-Dialogen:**
> _"Der Top-Slot Startseite erfordert Stufe 'Hochwertig' (Score 80+). Ihre Pressemitteilung erreicht aktuell Stufe 'Geprüft' (Score 67). So erreichen Sie 'Hochwertig':_ _[Pressetext-Optimierung – 15 Credits, +15–20 Punkte]_ _[Headline-Booster – 5 Credits, +3–7 Punkte]"_
---
## 16. Boost-Eligibilität (überarbeitet)
Boost-Schwellen werden von konkreten Punktzahlen auf Stufen umgestellt. Die Schwellen werden Plattform-intern weiterhin auf Score-Ebene geprüft, kommuniziert wird gegenüber Publishern aber die Stufe.
### Schwellenwerte je Slot-Typ
|Slot|Klassifikation|Mindeststufe|Entspricht Score|
|---|---|---|---|
|Highlight Kategorie|Grün|Standard|≥ 30|
|Startseite-Highlight (24h / 3T)|Grün|Geprüft|≥ 60|
|Top-Slot Startseite|Grün|Hochwertig|≥ 80|
|Newsletter-Erwähnung|Grün|Geprüft|≥ 60|
|Social-Share (offizieller Kanal)|Grün|Geprüft|≥ 60|
|Verteiler-Versand (extern)|Grün|Hochwertig|≥ 80|
PMs mit Klassifikation Gelb können nicht boostbar werden, auch nicht nach manueller Freigabe – sie bleiben in regulärer Sichtbarkeit. PMs mit Klassifikation Rot werden nicht veröffentlicht.
### UI-Logik beim Buchen
Wenn ein User einen Boost-Slot bucht, dessen Schwelle seine PM nicht erreicht, sieht er statt des Buchungsformulars die Tool-Empfehlungen aus dem Editor-Dialog. Der konkrete Score wird hier sichtbar, weil der Publisher die Distanz zur nächsten Stufe verstehen muss, um eine wirtschaftliche Entscheidung zu treffen (Tool kaufen oder anders boosten).
---
## Differenzierung: Hochwertig-Stufe vs. Editorial-Pick
Beide sind Vertrauenssignale, aber konzeptionell unterschiedlich. Sie müssen visuell und sprachlich klar unterscheidbar sein.
||Hochwertig-Stufe|Editorial-Pick|
|---|---|---|
|**Vergabe**|algorithmisch (Score ≥ 80)|manuell durch Redaktion|
|**Voraussetzung**|Content-Score|freie redaktionelle Auswahl|
|**Häufigkeit**|viele PMs|wenige, ausgewählte PMs|
|**Bezeichnung**|"Hochwertig"|"Auswahl der Redaktion"|
|**Symbol**|Stern-Icon|orange Auszeichnung (wie auf Startseite)|
|**Funktion**|Qualitätsindikator|redaktionelle Empfehlung|
Eine PM kann beide Auszeichnungen gleichzeitig tragen. In der Regel werden Editorial-Picks aus dem Pool der Hochwertig-PMs gewählt, aber das ist nicht zwingend – die Redaktion hat freie Hand.
---
## Außenkommunikation – Konkrete Labels und Texte
Damit das System Plattform-weit konsistent kommuniziert wird, hier die verbindlichen Texte und Labels:
### Detailseite – Metadaten-Zeile
Statt bisher "Qualität: Sehr hoch (94)":
- **Standard:** kein Label
- **Geprüft:** Häkchen-Icon ✓ mit Tooltip "Geprüfte Pressemitteilung – redaktionelle Standards eingehalten"
- **Hochwertig:** Stern-Icon ★ mit Label "Hochwertig" und Tooltip "Pressemitteilung mit besonders hoher Qualität"
### Newsletter-Block
Statt "Nur Meldungen ab Content-Score 80":
- **Tageszusammenfassung:** "Alle wichtigen Meldungen aus DACH"
- **Wochenrückblick:** "Die wichtigsten Meldungen der Woche"
- **Branchen-Alerts:** "Höchstens 2 Mails pro Woche. Nur hochwertige Pressemitteilungen aus dieser Branche."
### Erweiterte Suche – Filter
Filter-Optionen:
- "Alle Pressemitteilungen"
- "Nur geprüfte Pressemitteilungen"
- "Nur hochwertige Pressemitteilungen"
- "Nur Auswahl der Redaktion"
### Vertrauens-Sektion auf der Plattform
Eine Erklärseite (z.B. /redaktion oder /qualitaet) erläutert das System öffentlich:
> _"Jede Pressemitteilung auf businessportal24 wird automatisch auf Qualität geprüft. Wir unterscheiden drei Stufen:_
>
> _• **Standard** – erfüllt unsere Mindestanforderungen für Pressemitteilungen_ _• **Geprüft** – solide Pressemitteilung mit guter Substanz_ _• **Hochwertig** – Pressemitteilung in redaktioneller Spitzenqualität_
>
> _Zusätzlich vergibt unsere Redaktion das Sigel 'Auswahl der Redaktion' für Pressemitteilungen, die wir besonders empfehlen."_
---
## Auswirkungen auf das Datenmodell
Geringe Änderungen, da Score weiterhin intern als Zahl gespeichert wird:
```
press_releases (Ergänzung)
- + content_tier (enum: standard, gepruft, hochwertig)
– wird automatisch aus content_score abgeleitet
– kann als generated column oder per Trigger gepflegt werden
- + editorial_pick (bool, default false)
- + editorial_pick_at, editorial_pick_by (für Audit)
```
Die Stufen-Schwellen werden in einer zentralen Konfiguration gepflegt (z.B. `config/scoring.php` in Laravel), damit sie bei späterer Kalibrierung an einer Stelle anpassbar sind, ohne Code-Änderungen.
---
## Was unverändert bleibt
- **Content-Score (0–100)** wird intern weiterhin berechnet und gespeichert
- **Score-Faktoren und Gewichtungen** bleiben wie in Abschnitt 15.2 des ersten Updates definiert
- **Klassifikations-Score (Grün/Gelb/Rot)** bleibt unverändert als Eintritts-Filter
- **Trust-Score** auf Account-Ebene bleibt unverändert
- **Tool-zu-Algorithmus-Loop** funktioniert identisch, nur mit Stufen statt Punktzahlen in der Außenkommunikation
- **Datenmodell** für `content_scores`, `placements`, `placement_inventory` bleibt unverändert
---
## Migrationsschritt für bestehendes Mockup
Die Detailseite-Mockup-Version vom 7. Mai zeigt aktuell "Qualität: Sehr hoch (94)" in der Metadaten-Zeile. Anpassung:
- Text "Qualität: Sehr hoch (94)" entfernen
- Stattdessen: Stern-Icon ★ + Label "Hochwertig" (für Stufe Hochwertig)
- Tooltip beim Hover über das Label
- Konsistenz prüfen mit Newsletter-Sidebar-Block (auch dort Score-Zahl entfernen, durch "hochwertige Pressemitteilungen" ersetzen)
---
## Offene Punkte / nächste Entscheidungen
- **Schwellenwerte kalibrieren:** Erst nach 100–200 echten PMs sehen, wo die Score-Verteilung liegt. Schwellen ggf. anpassen, sodass Standard ca. 40–50 %, Geprüft ca. 35–45 %, Hochwertig ca. 10–20 % der PMs umfasst (Anhaltswert).
- **Visuelle Symbole final wählen:** Stern für Hochwertig, Häkchen für Geprüft – Alternativen prüfen (z.B. Medaille, Auszeichnungs-Symbol). Konsistenz mit bestehendem Icon-Set wahren.
- **Editorial-Pick-Symbol final festlegen:** Orange Auszeichnung wurde auf Startseite genutzt – muss klar unterscheidbar bleiben vom Hochwertig-Symbol.
- **Tooltip-Texte feinjustieren:** kurze, prägnante Erklärung pro Stufe, übersetzungsfähig.
- **Erklärseite /redaktion oder /qualitaet:** als Vertrauens-Anker für Leser und Suchmaschinen erstellen.

View file

@ -1,327 +0,0 @@
Stand: Mai 2026 Zweck: Festlegung der Plattform-Architektur mit zentralem Hub (presseportale.com) und mehreren öffentlichen Brand-Portalen. Dokumentation der strategischen, technischen und Branding-Entscheidungen für die Entwicklung.
---
## Architektur-Entscheidung
Die Plattform folgt einem **Hub-and-Spoke-Modell**:
- **Ein zentraler Hub** für alle Verwaltungs-, Veröffentlichungs- und Abrechnungs-Funktionen
- **Mehrere öffentliche Brand-Portale**, die unabhängige redaktionelle Marken mit eigener Zielgruppe und eigener Editorial-Anmutung darstellen
- **Eine gemeinsame Codebase und Datenbank** im Hintergrund
Das Modell entspricht der Architektur, mit der etablierte Verlagsgruppen (Axel Springer, Hubert Burda Media) ihre Markenportfolios technisch organisieren: ein zentrales Redaktions- und Tool-System, viele Frontend-Marken.
### Strategische Vorteile
- **Effiziente Entwicklung:** Eine Codebase für alle Portale, Updates an Tools/Editor/Credit-System wirken portal-übergreifend
- **Multi-Portal-Publishing:** Publisher können Pressemitteilungen mit einem Klick auf mehreren Portalen veröffentlichen (verkaufbares Premium-Feature)
- **Datenkonsistenz:** Ein Account, ein Credit-Stand, eine Statistik-Aggregation über alle Portale
- **Skalierbarkeit:** Weitere Portale (z.B. branchenspezifisch) können als reine Frontend- und Konfigurations-Erweiterung hinzugefügt werden
- **Saubere Trennung Marke vs. Funktion:** Brand-Portale optimieren für Editorial-Glaubwürdigkeit, Hub für Funktion und Effizienz
---
## Domain- und Verantwortungs-Aufteilung
### presseportale.com – Hub (Publisher Hub)
**Marke:** Publisher Hub (mit Subline "Mein Pressekonto")
**Funktion:**
- User-Panel: Editor, Dashboard, Statistiken, Credit-Verwaltung, Newsroom-Konfiguration
- Admin-Panel: Review-Queue, Moderation, Inventory-Verwaltung, Editorial-Picks, User-Verwaltung
- Magic-Link-Bereich für Pressekontakte
- Account-Setup und Tarif-Auswahl (zentraler Veröffentlichungs-Funnel)
- Stripe-Integration, Rechnungen, Buchhaltung
- Alle KI-Tools (Lektorat, Pressetext-Optimierung, Bildgenerierung, etc.)
**Branding:** Neutrale, eigenständige Marke. Sauberes Tool-Design (aktuell Flux UI). Funktionsfokussiert. Brand-Kontext der Portale wird über Banner/Hinweise vermittelt, nicht über Farbadaption.
**Zielgruppe:** Publisher (Unternehmen, PR-Agenturen, Pressekontakte), Admins.
---
### businessportal24.com – Brand-Portal
**Marke:** businessportal24
**Funktion:**
- Öffentliche Pressemitteilungs-Plattform
- Editorial-Anmutung mit Fokus auf Wirtschaft, B2B, Branchen-Tiefe
- Kein eigener User-Login-Bereich
- Reichweiten-Generierung über Newsletter, SEO, Branchenseiten
**Branding:** Eigenständige Editorial-Identität (Anthrazit + Orange-Akzent). Wirkt wie ein Wirtschaftsmedium, nicht wie ein Pressedienst.
**Zielgruppe:** Wirtschaftsjournalisten, Mediaplaner, B2B-Entscheider, Branchen-Experten.
---
### presseecho.de – Brand-Portal
**Marke:** presseecho
**Funktion:** Wie businessportal24, aber mit anderer Markenidentität und potentiell anderer Zielgruppen-Ausrichtung. Konzeption folgt in eigener Iteration.
**Branding:** Eigenständig, klar unterscheidbar von businessportal24 (eigener Akzentton, eventuell anderer Editorial-Schwerpunkt).
**Zielgruppe:** noch zu definieren / komplementär zu businessportal24.
---
## Login- und Session-Mechanik
### Grundregel
**Alle Authentifizierung läuft ausschließlich über presseportale.com.** Brand-Portale haben keine eigenen Login-Formulare oder Auth-Bereiche.
### Verhalten in den Brand-Portalen
- "Anmelden"-Button im Header eines Brand-Portals → Redirect auf presseportale.com (Login-Seite mit Rück-Redirect nach erfolgreicher Anmeldung)
- "Veröffentlichen"-Button → kurzer Brand-spezifischer Landing-Inhalt, dann Übergang auf presseportale.com für Account-Setup und Veröffentlichungs-Funnel
- Wenn der User über die Brand-Domain eingeloggt erkannt wird (über Cross-Domain-Mechanismus, siehe unten), erscheint im Header ein "Mein Account"-Element, das auf presseportale.com führt
### Cross-Domain-Session
Da die Brand-Portale und der Hub auf unterschiedlichen Top-Level-Domains laufen (.com / .de), ist Cookie-basiertes Session-Sharing nicht möglich. Notwendige Lösung:
**Variante A (empfohlen):** Token-basierter Auth-Mechanismus über Laravel Sanctum
- Hub gibt nach Login ein API-Token aus
- Brand-Portale prüfen über API gegen den Hub den Login-Status
- Vorteil: sauber, standardkonform, skalierbar
**Variante B:** Lightweight-Cookie-Sync über Cross-Domain-Redirect bei Pageload (analog zu wie Single-Sign-On-Lösungen es machen)
- Komplexer, fehleranfälliger
- Nur zu empfehlen, falls Variante A nicht umsetzbar
Empfehlung: Variante A, da sie zum Laravel-Stack ohnehin passt und auch für die API-Anbindung von Distribution-Partnern (connektar etc.) wiederverwendbar ist.
### Magic-Link-Flow
Magic-Links für Pressekontakte zeigen _immer_ auf presseportale.com mit Brand-Kontext-Hinweis:
> _„Sie verwalten eine Pressemitteilung, die auf businessportal24 veröffentlicht wurde."_
Brand-Logo und -Name werden im Hub-Header angezeigt, damit der Pressekontakt versteht, wo seine PM erscheint. Funktional bleibt der Flow zentral.
---
## E-Mail-Strategie
Saubere Trennung nach Funktion:
### System-Mails (von presseportale.com)
- Login, Password-Reset, Account-Bestätigung
- Magic-Link für Pressekontakte
- Stripe-Zahlungs-Bestätigungen, Rechnungen
- Credit-Aufladung-Bestätigungen, Auto-Refill-Hinweise
- Wartungs-/System-Benachrichtigungen
**Absender:** `noreply@presseportale.com` oder `support@presseportale.com`
### Editorial- und Brand-Mails (von der jeweiligen Brand-Domain)
- "Ihre Pressemitteilung wurde auf businessportal24 veröffentlicht"
- Branchen-Newsletter (Tageszusammenfassung, Wochenrückblick, Branchen-Alerts)
- Newsroom-Update-Benachrichtigungen
- Reichweiten-Statistiken pro PM
**Absender:** `redaktion@businessportal24.com`, `newsletter@businessportal24.com`, analog für presseecho.de
### Begründung
Diese Trennung stärkt das Branding der Portale (Editorial-Mails kommen "vom Magazin"), während Verwaltungs-Funktionen klar dem zentralen Hub zugeordnet bleiben. Funktional sind beide Domains technisch identisch hinterlegt (gleicher Mail-Service, gleiche Templates), nur die Absender-Konfiguration unterscheidet sich.
---
## Datenmodell-Implikationen
Die Hub-and-Spoke-Architektur erfordert eine zentrale Tabelle für die Brand-/Portal-Zugehörigkeit:
```
brands (oder portals)
- id, slug, name, domain
- primary_color, accent_color
- logo_url
- editorial_email, newsletter_email
- is_active, created_at
press_releases (Ergänzung)
- + brand_id (FK auf brands)
- + cross_published_to (JSON array of brand_ids, für Multi-Portal-Veröffentlichung)
placements (Ergänzung)
- + brand_id (FK auf brands)
- Placements gelten pro Portal, da Inventory portal-spezifisch ist
newsletters (Ergänzung)
- + brand_id
accounts
- Bleiben portal-übergreifend
- Eine Person hat einen Account, kann auf allen Portalen veröffentlichen
- Sub-Berechtigungen pro Brand möglich (für Agency-Tarif: Marke X nur auf Portal Y)
credit_accounts
- Bleiben portal-übergreifend, ein Credit-Pool für alles
```
### Wichtige Logiken
- Pressemitteilungen sind immer einem Brand zugeordnet (`brand_id`), können aber zusätzlich auf andere Brands "cross-published" werden
- Branchen, Kategorien und Tags können entweder portal-spezifisch oder portal-übergreifend definiert sein – Empfehlung: portal-übergreifender Stamm, mit Möglichkeit zur portal-spezifischen Untergliederung
- Placements/Boost-Slots sind grundsätzlich portal-spezifisch (jeder Top-Slot auf businessportal24 ist ein anderer Slot als auf presseecho)
- Aggregierte Statistiken im Dashboard zeigen alle Brands eines Publishers zusammen, mit Filter-Möglichkeit pro Brand
---
## Brand-übergreifende Features
### Aktuell (Migrations-Phase)
Im Dashboard werden Pressemitteilungen aktuell **gefiltert nach Portal** angezeigt. Hintergrund: Die beiden Portale kommen aus separater Legacy-Entwicklung, eine getrennte Sicht erleichtert die Migration.
### Mittelfristig
**Cross-Sichtbarkeit als Standard:**
- Dashboard zeigt alle PMs eines Publishers portal-übergreifend
- Filter-Möglichkeit pro Portal vorhanden, aber nicht Standard
- Statistiken aggregieren über alle Portale
**Multi-Portal-Veröffentlichung als Premium-Feature:**
- Beim Einreichen einer PM kann der Publisher auswählen, auf welchen Portalen sie erscheinen soll
- Standard: Hauptportal des Publishers
- Optional: Zusatz-Portale (kostet zusätzliche Credits oder ist in höheren Tarifen inkludiert)
- Wettbewerber können das nicht – starkes Verkaufsargument
**Portal-übergreifender Newsroom:**
- Premium-Publisher haben einen Newsroom auf jedem Portal, gepflegt aus einem zentralen Profil im Hub
- Logo, Beschreibung, Kontakt einmal pflegen – auf allen Portalen aktuell
---
## Implikationen für die Veröffentlichen-Landingpage
Aus der Architektur folgt die Aufteilung der Veröffentlichen-Seite in zwei Ebenen:
### Auf businessportal24.com/veroeffentlichen (Brand-Landing)
- Hero mit _brand-spezifischem_ Wertversprechen (Wirtschafts-Fokus, B2B-Reichweite, Branchen-Tiefe)
- Konkrete Reichweiten-Zahlen _für businessportal24_ (Newsletter-Abos, Branchen-Traffic, ISIN-Coverage)
- Differenzierungs-Punkte speziell für dieses Portal
- Tarif-Teaser ("Ab 19 € pro Pressemitteilung", mit Link auf volle Übersicht)
- Soziale Beweise (Logos von Publishern, die businessportal24 nutzen)
- Direkter CTA "Jetzt veröffentlichen →" → zur zentralen Plattform auf presseportale.com
### Auf presseportale.com/veroeffentlichen (Zentrale Funnel-Seite)
- Volle Tarif-Tabelle (Einzel / Starter / Business / Pro / Agency)
- Tool-Showcase (Lektorat, KI-Bilder, etc.)
- Erklärung des Multi-Portal-Konzepts ("Ein Account, mehrere Portale")
- Portal-Auswahl im Anmelde-Prozess ("Auf welchen Portalen möchten Sie veröffentlichen? businessportal24 / presseecho / beide")
- FAQ
- Account-Setup-Funnel
### Analoge Struktur für presseecho.de
presseecho.de/veroeffentlichen folgt demselben Pattern, mit brand-spezifischem Inhalt und Übergang zum zentralen Funnel.
---
## Footer-Verlinkung zwischen Hub und Brand-Portalen
### Auf Brand-Portalen (Footer)
Im Footer-Bereich von businessportal24 und presseecho ein dezenter Link:
> **Für Publisher** → Publisher Hub | Pressemitteilung einreichen | Tarife & Pakete
Bestehende Kunden steigen so direkt in den Hub ein, ohne sich die Hub-Domain merken zu müssen.
### Auf dem Hub (Footer)
Im Footer von presseportale.com Verlinkung auf alle Brand-Portale:
> **Unsere Portale** → businessportal24 (Wirtschaft & Branchen) | presseecho (...)
Schafft Transparenz und Vertrauen – sichtbar, dass es eine Plattform-Familie ist.
---
## Branding-Strategie pro Bereich
|Bereich|Primärfarbe|Anmutung|Charakter|
|---|---|---|---|
|**presseportale.com**|neutral (z.B. Anthrazit + zurückhaltender Akzent)|Tool / SaaS|sachlich, funktional, effizient|
|**businessportal24.com**|Anthrazit + Orange|Editorial / Wirtschaftsmedium|seriös, datendicht, B2B|
|**presseecho.de**|eigene Palette (z.B. Anthrazit + Blau oder Bordeaux)|Editorial / anderer Schwerpunkt|klar differenziert von businessportal24|
Wichtig: Die Brand-Portale müssen optisch klar unterscheidbar sein, damit Leser nicht den Eindruck gewinnen, es sei "dasselbe in zwei Versionen". Empfehlung: gemeinsame typografische Sprache (Editorial-Serif für Headlines, gleiche Sans für UI), aber klar unterschiedliche Akzentfarben und sekundäre visuelle Elemente.
Der Hub bekommt eine _eigene_ visuelle Sprache, die sich von beiden Brand-Portalen abhebt. Tendenziell: weniger Farbe, mehr Whitespace, Tool-orientierte Komponenten (Tabellen, Dashboards, Form-Bausteine). Aktuell auf Flux UI basierend – soll noch eigene Identität bekommen, ohne den Funktions-Charakter zu verlieren.
---
## Migration und Roadmap
### Phase 1 (laufend)
- Backend-Migration zu Laravel
- Zentrale `brands`-Tabelle als Grundlage anlegen
- Bestehende PMs der beiden Portale werden mit `brand_id` versehen
- Dashboard zeigt portal-getrennt (Migrations-Komfort)
### Phase 2
- Hub auf presseportale.com mit User-Panel produktiv
- Magic-Link-Flow zentral aufgesetzt
- Erste Brand-Portal-Iteration (businessportal24) im neuen Design live
- Cross-Domain-Auth über Sanctum
### Phase 3
- Zweites Brand-Portal (presseecho) im neuen Design live
- Cross-Sichtbarkeit im Dashboard
- Multi-Portal-Veröffentlichung als Feature aktiv
### Phase 4 (mittelfristig)
- Aggregierte Statistiken portal-übergreifend
- Portal-übergreifender Newsroom
- Vorbereitung für drittes Portal (falls relevant)
---
## Skalierungs-Argument
Die Architektur ist explizit auf Wachstum ausgelegt. Wenn in 12–24 Monaten ein drittes Portal sinnvoll wird (z.B. ein branchenspezifisches wie "energieportal.com" oder ein regional fokussiertes wie "presseportal-bayern.de"), bedeutet das technisch:
- Keine neue Codebase
- Keine neue Datenbank
- Kein neuer Auth-Mechanismus
- Kein neues Credit-System
- Nur: Frontend, Konfiguration in `brands`-Tabelle, Editorial-Setup
Damit wird die Investition in den Hub heute strukturell verteidigt – jede Stunde, die in den Hub investiert wird, zahlt auf jedes zukünftige Portal mit ein.
---
## Offene Punkte / nächste Entscheidungen
- **Cross-Domain-Auth final festlegen:** Sanctum-Implementierung details, Token-Lebensdauer, Refresh-Strategie
- **presseecho-Konzept:** Markenidentität, Zielgruppe, Differenzierung zu businessportal24 klären
- **Multi-Portal-Veröffentlichung – Pricing:** wie wird Cross-Publishing verrechnet? Pro zusätzlichem Portal X Credits? In höheren Tarifen inkludiert? Wie sehen die Tier-Grenzen aus?
- **Hub-Design eigenständig schärfen:** aktuelles Flux-UI-Setup soll mehr Eigenständigkeit bekommen, ohne den funktionalen Charakter zu verlieren – eigenes Mini-Briefing nötig
- **Footer-Verlinkungen zwischen den Portalen:** konkrete Texte und Platzierungen finalisieren
- **Übergang in den Brand-Portalen markieren:** wenn ein User von businessportal24 auf den Hub springt – sichtbarer Übergangs-Indikator (Banner oben "Sie sind im Publisher Hub" mit Rück-Link)? Oder unauffällig? UX-Entscheidung steht aus

View file

@ -1,178 +0,0 @@
**Datum:** 12. Mai 2026 **Status:** Strategie-Dokument **Vorgänger:** Konzept-Updates 1 (Score-Architektur), 2 (Score-Stufen), 3 (Multi-Brand-Architektur) **Bezug:** Presseportal-Strategie (lebendes Strategiepapier, Mai 2026)
---
## 1. Ausgangslage
Die Multi-Brand-Architektur (Update 3) hat die technische Trennung von Hub und Brand-Portalen geklärt. Offen blieb die inhaltliche Frage: **Wofür stehen die beiden Marken konkret – und was dürfen sie versprechen?**
Frühere Positionierungs-Entwürfe haben Versprechen formuliert, die in einem Solo-Entwicklungs-Setup nicht eingehalten werden können:
- „Exklusive Analysen, Interviews, kuratierte Nischen-Informationen" für presseecho.de – setzt redaktionelle Arbeit voraus, die nicht geleistet wird.
- „Breite öffentliche Wahrnehmung und Lead-Generierung" für businessportal24.com – ist ein Reichweiten-Versprechen, mit dem große Player mit ganz anderen Budgets vermarkten.
Beide Formulierungen folgen dem Muster „großes Presseportal kopiert" – genau das Anti-Pattern, das die übergeordnete Strategie ausschließt. Die Positionierung muss aus dem entstehen, was tatsächlich anders gemacht wird: Archiv-Stabilität, Qualitätsschwelle, faire Konditionen, Themen-Tiefe statt Timeline-Hype.
**Zusätzliche Einschränkung:** Beide Portale ziehen am Start aus demselben Migrations-Pool. Eine inhaltliche Differenzierung über getrennte Redaktionen oder exklusive Inhalte ist am Start nicht ehrlich darstellbar.
## 2. Leitidee: Differenzierung über Leseparadigma
Da die Inhalte zu Beginn identisch sind, wird die Differenzierung **nicht über das Was, sondern über das Wie** aufgebaut:
||businessportal24|presseecho|
|---|---|---|
|**Leitfrage**|„Was ist aktuell bei Unternehmen los?"|„Was läuft in dieser Branche / zu diesem Thema?"|
|**Logik**|Aktualitäts-/Zeitachsen-Logik|Themen-/Cluster-/Archiv-Logik|
|**Lesemodus**|„Was ist neu?"|„Was gibt es dazu?"|
|**Wert für Leser**|Übersicht über aktuelle Geschehnisse|Tiefe und Kontext zu einem Thema|
|**Wert für Publisher**|Sichtbarkeit _jetzt_|Auffindbarkeit _dauerhaft_|
Diese Differenzierung ist:
- **Solo-tauglich**: eine Codebasis, ein Inhalts-Pool, zwei Präsentations-Logiken
- **Ehrlich**: beide Portale liefern echten Mehrwert, nur für unterschiedliche Lesebedürfnisse
- **Selbstverstärkend**: mit der Zeit entscheiden Publisher selbst, welche Brand zu ihrem Inhalt passt – die Differenzierung wächst organisch
## 3. businessportal24.com – Positionierung
### Kern-USP
> _„Die Wirtschaftspresse für den deutschen Mittelstand – aktuell, transparent, ohne Reichweiten-Marketing. Was hier veröffentlicht wird, ist nach Qualität geprüft und bleibt dauerhaft auffindbar."_
### Zielgruppen
**Publisher-Seite:** Mittelständische Unternehmen, Selbstständige, kleinere PR-Agenturen, regionale Akteure. Charakteristisch: Sie suchen _eine_ zuverlässige Veröffentlichungs-Adresse, nicht zehn Streuverteiler.
**Leser-Seite:** Journalisten und Multiplikatoren, die wissen wollen, was bei KMU gerade passiert; Wirtschaftsinteressierte; lokale/regionale Recherche.
### Thematischer Schwerpunkt
Unternehmensmeldungen, neue Produkte, Personalia, Standorte, Aufträge, Auszeichnungen, Wirtschaftsthemen aus der Breite des Mittelstands.
### Tonalität
Aktiv, klar, wirtschaftsnah, zugänglich. Nicht hip, aber lebendig. Erwachsen, nicht laut.
### Farbwelt
Energetisches Orange/Rot als Akzent auf neutraler Basis. Passt zum Profil „aktuell, lebhaft, KMU-aktiv". Wichtig: **keine SaaS-Landingpage-Gradienten**, sondern zurückhaltend eingesetzte Akzente. Das Wirtschafts-Charakter braucht typografische Dichte, keine Lifestyle-Optik.
### Was hier _nicht_ versprochen wird
- Reichweite oder Reichweiten-Garantien
- Lead-Generierung
- „Virale Verbreitung"
- SEO-Versprechen
- Exklusive Inhalte
## 4. presseecho.de – Positionierung
### Kern-USP
> _„Das Branchenportal mit Themen-Gedächtnis – wo Pressemitteilungen nicht in der Timeline verschwinden, sondern dauerhaft in Themen- und Branchenkontexten zugänglich bleiben."_
### Zielgruppen
**Publisher-Seite:** Unternehmen mit fachlich-spezifischer Kommunikation, B2B-Anbieter, Branchen-Akteure. Charakteristisch: Sie wollen, dass ihre Meldung auch in zwei Jahren noch im Themen-Kontext gefunden wird – nicht nur 48 Stunden im Strom mitschwimmen.
**Leser-Seite:** Fachjournalisten, Branchenrecherche, Analysten, Studierende – alle, die zu einem Thema _in die Tiefe_ gehen wollen statt nur „was war heute?".
### Thematischer Schwerpunkt
Am Start identisch zum gemeinsamen Pool. Differenzierung über die **Präsentation**: Themen- und Branchen-Cluster, sichtbare Archivtiefe, „dazu erschien auch …", längere Lese-Pfade, Themen-Historie.
### Tonalität
Ruhig, sachlich, expertenorientiert – aber nicht steif. Eher „Fachbuchhandlung" als „Lifestyle-Magazin".
### Farbwelt
Dunkelgrün. Seriös, beständig, „Bibliotheks-Charakter". Idealer Kontrast zur aktiven Orange-Welt von businessportal24.
### Der ehrliche Mehrwert
Das ~10 Jahre alte Archiv mit knapp 100.000 Mitteilungen wird hier zum _Feature_, nicht zur Altlast. presseecho.de ist faktisch die Marke, in der die **Archivtiefe sichtbar** wird – und damit das Asset, das in der Strategie als wichtigster Differenzierungs-Vorteil identifiziert wurde.
### Was hier _nicht_ versprochen wird
- Exklusive redaktionelle Inhalte
- Eigene Interviews oder Analysen
- Kuratierte Auswahl durch eine Redaktion
- Branchen-Newsletter mit Mehrwert über die Pressemitteilungen hinaus
- Experten-Bewertungen / -Rankings
Wenn diese Versprechen später eingelöst werden sollen, müssen sie _vorher_ operativ verfügbar sein – nicht umgekehrt.
## 5. Markenarchitektur im Verhältnis zueinander
Die beiden Brands sind nicht Konkurrenz, sondern **komplementär**. Beim Veröffentlichen erhält der Publisher eine ehrliche Entscheidungshilfe:
- _„Mein Thema ist aktuell, ich will Sichtbarkeit jetzt"_ → businessportal24
- _„Mein Thema ist fachlich, ich will dauerhafte Auffindbarkeit im Kontext"_ → presseecho
- _„Beides"_ → Cross-Publishing (gegen Aufpreis im Credit-System)
Das erfüllt drei Funktionen gleichzeitig:
1. **Ehrliche Beratung** gegenüber dem Kunden – keine künstliche Verknappung
2. **Verkaufslogik** für Cross-Publishing als Premium-Option
3. **Selbstverstärkende Differenzierung** – Publisher sortieren ihre Inhalte selbst zur passenden Marke
## 6. Migrationspfad der Differenzierung
Die Differenzierung wird **nicht zum Start vollständig** geliefert. Sie wächst in drei Phasen, im Einklang mit der Anti-Zombie-Regel „nichts versprechen, was am Start nicht verfügbar ist".
### Phase 1 – Migration (Monate 1–6)
Beide Portale zeigen praktisch dieselben Inhalte. Differenzierung lebt nur über:
- **Marken-Identität**: Logo, Farbwelt, Tagline, Tonalität der Marketing-Texte
- **Navigations-Logik**: Timeline-first (businessportal24) vs. Themen-first (presseecho)
- **Landing-Page-Sprache**: unterschiedliche Wertversprechen, gleiche Backend-Mechanik
Mehr ist in dieser Phase nicht ehrlich darstellbar.
### Phase 2 – Aufbau (Monate 6–18)
- Cross-Publishing wird kostenpflichtig
- Default-Publishing geht zu _einer_ Brand – Publisher entscheiden bewusst, wo es passt
- Erste organische Inhalts-Drift entsteht (regional/aktuell → businessportal24; fachlich/dauerhaft → presseecho)
- Brand-spezifische Navigations-Features werden ausgebaut (Themen-Cluster bei presseecho, Aktualitäts-Filter bei businessportal24)
### Phase 3 – Reife (ab Monat 18+)
- presseecho.de hat erkennbare Themenwelten mit Archivtiefe
- businessportal24.com hat erkennbares „Was-ist-neu"-Profil
- Eventuell brand-spezifische Kleinfeatures: Themen-Newsletter bei presseecho, Branchen-Filter bei businessportal24
- Erst hier können erweiterte Markenversprechen formuliert werden – und nur, wenn sie operativ tatsächlich abgedeckt sind
## 7. Verbindung zur Gesamtstrategie
Diese Positionierung folgt konsequent den Leitlinien aus dem Strategiepapier:
- **Bewusste Nische statt Konkurrenz**: kein Wettlauf mit Pressebox/openPR um Reichweite
- **Asset-Wert konservieren**: das Archiv wird über presseecho.de sichtbar gemacht statt versteckt
- **Geduld als Wettbewerbsvorteil**: Differenzierung wachsen lassen, statt am Start zu erfinden
- **Ehrliche Kommunikation**: nur versprechen, was operativ verfügbar ist
- **Konsistenz schlägt Spektakel**: ein klarer, langsam wachsender Markencharakter pro Portal
## 8. Implikationen für die nächsten Schritte
Daraus ergeben sich konkrete Aufgaben für die kommende Arbeitsphase:
- **Brand-Landing businessportal24.com/veroeffentlichen** – Wertversprechen aus diesem Update, kurzer Vertriebs-Touchpoint, Übergabe in den Hub-Funnel
- **Brand-Landing presseecho.de/veroeffentlichen** – Wertversprechen aus diesem Update, betont Archiv und Themen-Kontext
- **Frontend-Navigationskonzept** – Timeline-first vs. Themen-first als sichtbarer Unterschied
- **Tonalitäts-Leitfaden** für Marketing-Texte je Brand (kann später wachsen, am Start reicht eine knappe Tabelle)
- **Cross-Publishing-Preislogik** im Credit-System (offener Punkt aus Update 3)
## 9. Offene Punkte
- Hub-Design-Sprache (offener Punkt aus Update 3) – sollte sich farblich/atmosphärisch klar von beiden Brand-Welten unterscheiden, neutral-funktional bleiben
- Wann genau wechselt das Default-Publishing-Modell von „zu beiden Brands" auf „zu einer Brand" (Phase-1- vs. Phase-2-Übergang)
- Soll es eine sichtbare Verbindung der beiden Marken nach außen geben (z.B. „ein Service der Pressekonto-Familie") – oder bleiben sie öffentlich strikt getrennt?
---
_Dieses Update ist Teil der Reihe lebender Konzept-Dokumente. Es legt fest, was die beiden Marken am Start versprechen dürfen – und damit gleichzeitig, was sie noch nicht versprechen. Änderungen an dieser Positionierung sollten dokumentiert und mit dem Strategiepapier abgeglichen werden._

View file

@ -1,166 +0,0 @@
# businessportal24.com/veroeffentlichen
**Datum:** 12. Mai 2026 **Status:** Implementierungs-Konzept **Bezug:** Konzept-Update 3 (Multi-Brand-Architektur), Konzept-Update 4 (Positionierung & Markenversprechen)
---
## 1. Strategische Verortung
Diese Landing ist **kein Funnel** im klassischen SaaS-Sinn. Sie ist ein **kurzer Vertriebs-Touchpoint**, der drei Dinge leistet:
1. Den Markencharakter von businessportal24 vermitteln (aktuell, KMU-nah, erwachsen)
2. Vertrauen für die Veröffentlichungs-Entscheidung aufbauen
3. Sauber in den **Hub-Funnel** auf pressekonto.de übergeben, wo Account, Credits, Abrechnung und Tools liegen
Was hier _nicht_ hingehört: ausführliche Preistabellen, mehrstufige Onboarding-Flows, Account-Verwaltung, Dashboard-Vorschauen. Das ist Hub-Territorium.
**Tonalität:** wirtschaftsnah, klar, sachlich-lebendig. Nicht Marketing-Sprech. Nicht „SaaS-Begeisterung". Eher Tageszeitungs-Wirtschaftsteil als Stripe-Landingpage.
**Visuelle Linie:** typografische Dichte statt Whitespace-Verschwendung. Orange/Rot als **Akzent**, nicht als flächiger Gradient. Editorial-Anmutung: schmale Spalten, klare Hierarchie, sachliche Bildwelt (echte Pressefotos / Unternehmensfotos, keine generischen Stock-Illustrationen).
## 2. Seitenstruktur
### Above the fold
**Hero-Block**
- **H1** (eine der Varianten):
- „Pressemitteilung veröffentlichen. Geprüft. Dauerhaft auffindbar."
- „Die Wirtschaftspresse für den deutschen Mittelstand."
- „Veröffentlichen, wo Mitteilungen nicht verschwinden."
- **Sub** (USP-Kern, max. 2 Sätze): _„businessportal24 ist die Presseplattform für mittelständische Unternehmen, Selbstständige und PR-Agenturen. Jede Veröffentlichung wird auf Qualität geprüft und bleibt dauerhaft im Archiv auffindbar."_
- **Primary CTA**: „Jetzt veröffentlichen" → führt in den Hub-Funnel (pressekonto.de), Cross-Domain-Auth via Sanctum
- **Secondary CTA**: „So funktioniert's" → Scroll-Anker zum „Ablauf"-Abschnitt
**Was bewusst fehlt im Hero:**
- Kein „Jetzt 14 Tage kostenlos testen"
- Kein „Reichweite für Ihre Marke"-Versprechen
- Kein Hero-Bild mit lächelnden Stockfoto-Menschen
- Kein riesiger orangener Gradient-Banner (die SaaS-Optik, die im aktuellen Redesign-Feedback bemängelt wurde)
### Trust-Block: Was uns ausmacht
Vier knappe Kacheln, jeweils mit Headline + 1–2 Sätzen. Keine großen Icons (oder nur sehr zurückhaltend, monochrom). Editorial-Look statt Marketing-Look.
1. **Geprüfte Qualität** _„Jede Mitteilung durchläuft eine Qualitätsprüfung, bevor sie online geht. Keine SEO-Spam-Texte, keine reinen Werbeanzeigen."_
2. **Dauerhaft auffindbar** _„Pressemitteilungen bleiben im Archiv – auch nach Jahren. Über 100.000 Mitteilungen aus mehr als einem Jahrzehnt sind weiterhin abrufbar."_
3. **Faire Konditionen** _„Transparente Preise, kein Abo-Zwang, keine Vertragsfallen. Sie zahlen, was Sie veröffentlichen."_
4. **Korrektur statt Löschung** _„Fehler in einer Mitteilung? Wir korrigieren statt zu löschen – damit Verweise und Verlinkungen bestehen bleiben."_
### Ablauf: So funktioniert's
Drei Schritte, knapp gehalten. Bewusst entmystifiziert – keine „Magic"-Sprache.
1. **Konto anlegen** – mit E-Mail-Adresse. Kein Passwort nötig, Login per Magic-Link.
2. **Mitteilung einreichen** – Text, Bild, Ansprechpartner. Eine Qualitätsprüfung läuft automatisch und durch eine kurze redaktionelle Sichtung.
3. **Veröffentlichung** – nach Freigabe online und im Archiv. Bei Bedarf jederzeit korrigierbar.
Hinweis am Ende des Blocks (klein, sachlich): _„Die Veröffentlichung erfolgt über den zentralen Publisher-Bereich auf pressekonto.de."_ – das setzt die Hub-Architektur transparent.
### Für wen das richtig ist
Kurzer Abschnitt mit konkreten Anlässen statt abstrakter Zielgruppen-Beschreibung. Macht die Eignung selbst-evident.
_„Typische Pressemitteilungen auf businessportal24:_
- _Neue Produkte oder Dienstleistungen_
- _Personalien und Geschäftsleitungs-Wechsel_
- _Auszeichnungen und Zertifizierungen_
- _Standort-Eröffnungen, Expansionen, Aufträge_
- _Veranstaltungs-Ankündigungen_
- _Studien und Marktanalysen aus Unternehmenshand"_
Anschließend ein Satz zur Eingrenzung – das ist wichtig für die Qualitätsschwelle:
_„Nicht geeignet sind reine Werbeanzeigen, SEO-Linkbuilding-Texte oder Inhalte ohne nachvollziehbaren Pressewert."_
### Preise (kurz, mit Verweis)
Hier **kein** vollständiges Preismodell. Stattdessen:
- Ein Satz zur Logik: _„Sie kaufen Credits, die für Veröffentlichungen eingesetzt werden. Keine Mindestlaufzeit, keine versteckten Gebühren."_
- Ein Preis-Anker, damit der Besucher nicht ratlos bleibt: _„Eine Veröffentlichung ab X € – Mengenrabatte verfügbar."_ (Konkreter Wert nachträglich)
- Link zum Hub: _„Vollständige Preisübersicht im Publisher-Bereich →"_
### Kurz-FAQ
Maximal 4–5 Fragen. Direkt, ohne Marketing-Worte.
- _„Wie schnell wird meine Mitteilung veröffentlicht?"_ – Werktags üblicherweise innerhalb von 24 Stunden nach Einreichung.
- _„Bleibt meine Mitteilung dauerhaft online?"_ – Ja. Mitteilungen werden nicht gelöscht. Korrekturen sind jederzeit möglich.
- _„Kann ich auch auf presseecho.de veröffentlichen?"_ – Ja, über den zentralen Publisher-Bereich. Cross-Publishing ist optional verfügbar.
- _„Brauche ich ein Abo?"_ – Nein. Sie kaufen Credits nach Bedarf, ohne Vertragsbindung.
- _„Was passiert bei einem Fehler in der Mitteilung?"_ – Korrektur statt Löschung: Inhalte werden aktualisiert, die URL bleibt erhalten.
### Final CTA
Schlicht. Kein „Letzte Chance"-Pathos.
- **H2**: „Bereit zu veröffentlichen?"
- **Button**: „Konto anlegen" → Hub
- **Klein darunter**: „Oder zuerst Beispiele ansehen →" (Link zur Startseite des Portals)
### Footer
Standard, aber mit zwei brand-spezifischen Elementen:
- Hinweis auf den Betreiber: _„businessportal24 ist ein Service der Pressekonto-Gruppe."_ (oder ähnlich – siehe offener Punkt aus Update 4)
- Cross-Link zu presseecho.de (dezent, eine Zeile): _„Für fachlich-spezifische Themen: presseecho.de"_
## 3. Visuelle Leitlinien
### Was diese Seite tun soll
- **Editorial wirken**, nicht promotional. Vorbild: Wirtschaftsteil einer überregionalen Zeitung, nicht Stripe/Linear/Notion.
- **Typografisch arbeiten**: klare Hierarchien, ausreichend Dichte, lesbare Spaltenbreiten (~65–75 Zeichen).
- **Farben sparsam**: Orange/Rot als Akzent (Links, primärer CTA, dünne Trennlinien, gelegentliche Rubriken-Marker). Keine flächigen Gradienten, keine farbigen Cards als Hintergrund.
- **Sachlichkeit ausstrahlen**: Pressemitteilungen sind ein seriöses Produkt. Optik muss das transportieren.
### Was diese Seite _nicht_ tun darf
- **Keine** Hero-Gradienten in Orange (siehe Feedback zum ersten Redesign-Versuch)
- **Keine** generischen Briefkasten-/Megafon-Icons
- **Keine** „lifestyle"-Bildwelt mit lächelnden Menschen am Laptop
- **Kein** SaaS-Vokabular („Plattform", „Solution", „Empower your communications")
- **Keine** Pseudo-Trust-Elemente wie „4.9 Sterne", „10.000 zufriedene Kunden" – wenn es echte Zahlen gibt, dann nüchtern darstellen
### Layout-Empfehlung
- **Maximalbreite** ~1100–1200px, zentriert, mit ausreichend Seitenrand
- **Hero** kompakt, nicht ganzseitig hoch – Pressekontext heißt: Information schnell sichtbar
- **Sektions-Trenner** über Typografie und schmale Linien, nicht über farbige Background-Bänder
- **Mobile**: einspaltig, gleiche Reihenfolge, Hero noch kompakter
## 4. Technische Hinweise
- **Routing**: `businessportal24.com/veroeffentlichen` rendert eine eigene Brand-Landing-Route, die das geteilte Backend nutzt aber brand-spezifische Templates lädt
- **CTA-Übergabe**: Klick auf „Jetzt veröffentlichen" / „Konto anlegen" leitet zum Hub (`pressekonto.de/publisher/start?brand=businessportal24`), damit der Hub weiß, welche Brand der Einstiegspunkt war (für Default-Brand-Zuordnung und ggf. Tracking)
- **Cross-Domain-Auth**: Wie in Update 3 festgelegt via Sanctum. Wenn der Besucher bereits eingeloggt ist, springt er direkt ins Dashboard, sonst in den Onboarding-Flow
- **Tracking**: Brand-Landing trackt eigenen Funnel-Eintritt, der Hub übernimmt ab dort. So bleibt nachvollziehbar, welche Brand-Landing welche Conversion-Rate hat
## 5. Copy-Hinweise zur weiteren Verwendung
Die Copy-Vorschläge in Abschnitt 2 sind **Ausgangsmaterial**, nicht final. Sie verkörpern aber die Tonalität, die in Update 4 festgelegt wurde:
- aktiv, klar, wirtschaftsnah, zugänglich
- keine Reichweiten- oder Lead-Versprechen
- konkrete Beispiele statt abstrakte Zielgruppen-Floskeln
- ehrliche Eingrenzung (das „Nicht geeignet"-Statement)
Beim Schreiben weiterer Texte gilt: Im Zweifel **eine Aussage weniger** statt eine Aussage mehr. Pressecharakter heißt zurückhaltend, nicht prahlend.
## 6. Offene Punkte
- Konkrete Preis-Anker (X € pro Veröffentlichung) – sobald die Preislogik aus Update 3 finalisiert ist
- Beispiel-Mitteilungen zum Anzeigen auf der Landing – sinnvoll oder unnötig? (Die Startseite tut das ohnehin)
- Soll der Footer-Hinweis auf presseecho.de prominent oder eher unauffällig sein? Hängt an der Grundsatzentscheidung aus Update 4 (sichtbare Markenfamilie vs. strikt getrennt)
- A/B-Test-Strategie für die drei H1-Varianten – sinnvoll erst, wenn Mindest-Traffic erreicht ist
---
_Dieses Konzept ist eine konkrete Implementierungs-Vorlage. Es schöpft die Positionierung aus Update 4 in seitenstrukturelle Form und benennt explizit, was diese Landing nicht leisten soll – um die Anti-Zombie-Regel auf operativer Ebene umzusetzen._

View file

@ -1,431 +0,0 @@
# User Backend und Admin Backend
> **Stand der Doku**: 11.06.2026 — Phase 1, Phase 7 (PM-Form-Refactor), Phase 8 (User-Panel-Konsolidierung) und die KI-Pruef-Pipeline (Klassifikation + Content-Score) sind abgeschlossen.
> Aktueller Code-vs-Konzept-Abgleich: [`docs/STATUS-ABGLEICH-USER-PANEL.md`](../STATUS-ABGLEICH-USER-PANEL.md).
> Naechster Block (Zahlung/Tarife, Veroeffentlichungs-Flow): [`docs/Decision-Update Preisstruktur & Veröffentlichungs-Flow.md`](../Decision-Update%20Preisstruktur%20&%20Ver%C3%B6ffentlichungs-Flow.md).
Dieses Konzept beschreibt das gemeinsame Backend aus zwei Perspektiven:
- **User Backend**: Self-Service-Bereich für Kunden/User zur Pflege eigener Firmen, Kontakte, Pressemitteilungen, Rechnungen, API-Tokens und Einstellungen. „Pressemappe" bleibt als öffentlicher/PR-bezogener Kontext erhalten, nicht als Hauptnavigation im User Backend.
- **Admin Backend**: Verwaltungsbereich für interne Admins/Editoren. Die bestehende Admin-Oberfläche bleibt in Phase 1 unverändert.
Beide Bereiche laufen technisch im gleichen Backend. Die sichtbaren Menüs, Aktionen und Daten werden über Rollen, Policies und Berechtigungen getrennt. Admins können sich über Impersonation als User einloggen, um dessen User-Backend-Sicht nachzuvollziehen und Inhalte zu prüfen oder zu korrigieren.
> Hinweis Routen-Namen: In der UI heißen die Firmen ueberall „Firmen". Aus
> historischen Gruenden tragen die zugehoerigen Routen weiterhin den
> Praefix `me.press-kits.*` (z. B. `me.press-kits.show`). Das ist nur
> ein Routen-Name, fachlich sind es Firmen. Eine Umbenennung der Routen
> ist nicht geplant, weil sie nur intern relevant ist.
Vorab siehe hierzu folgende Mechanik für Untermenüs https://pressekonto.test/settings/profile
# Aktualisierte Navigation
## Phasen-Farbcode
Für die weitere Planung werden Features farblich/phasenbasiert getrennt:
- **Grün / Phase 1**: auf dem bestehenden Datenmodell kurzfristig umsetzbar.
- **Gelb / Phase 2**: braucht kleinere neue Tabellen, Policies oder Services.
- **Rot / Später**: strategische Produkt-/Monetarisierungsthemen mit größerem Datenmodell- oder Rechtsaufwand.
## Umsetzungsstand Phase 1
Bereits umgesetzt:
- Firmen-Kontext-Switcher im User Backend mit „Alle Firmen" und Einzelfirma, platziert rechts in der Topbar.
- User-Backend-Navigation gegliedert in „Mein Bereich", „Finanzen" und „Konto".
- „Buchungen & Add-ons" ist als vorbereiteter Bereich eingebunden; Statistiken, Credits/Tarif, Zahlungsarten und Benachrichtigungen bleiben markierte spätere Punkte.
- Dashboard und PM-Liste reagieren auf den aktiven Firmen-Kontext.
- Firmen-Liste und Firmen-Detail auf Basis der bestehenden `companies`, `contacts` und `press_releases`.
- Zugriff auf Firmen ist auf eigene bzw. zugeordnete Firmen begrenzt.
- Öffnen einer Firma setzt die aktive Firma für den weiteren User-Backend-Kontext.
- Kontaktverwaltung innerhalb der Firma für Owner und Verantwortliche; Mitglieder bleiben lesend.
- Neue Pressemitteilungen übernehmen die aktive Firma als Vorauswahl.
- PM-Detail zeigt zugeordnete Pressekontakte sowie Status-/Verlaufsdaten.
- Rechnungen sind im User Backend in einer eigenen Finanznavigation eingeordnet; Legacy bleibt als Archivhinweis im Inhalt sichtbar.
- Rechnungen zeigen einen Hinweisblock; Rechnungsadresse wird im Profil als eigener Bereich gepflegt.
- Firmen-Stammdaten werden sichtbar in der Firma gepflegt; die Profilseite verweist nur noch auf die jeweilige Firma.
- Dashboard zeigt erste Datenqualitäts-Hinweise aus bestehenden Tabellen: Profil, Rechnungsadresse, Pressekontakte und Legacy-PMs ohne Firma.
Noch offen in Phase 1:
- Keine offenen Punkte aus der ersten grünen User-Backend-Ausbaustufe.
## Umsetzungsstand Phase 7 (PM-Form-Refactor)
Phase 7 ist mit Stand 21.05.2026 abgeschlossen. Sie hat das Form-Erlebnis für Pressemitteilungen vereinheitlicht und das Datenmodell um mehrere Felder erweitert. Details: `dev/frontend/hub-flux/19-PHASE-7-PRESS-RELEASE-FORM.md`.
Zusammenfassung:
- **Neue PM-Felder**: `subtitle`, `scheduled_at`, `embargo_at`, `boilerplate_override`, `no_export`. Migrationen liegen in `database/migrations/2026_05_20_*`.
- **HTML-Sanitizer**: Inhalt wird serverseitig durch `mews/purifier` gereinigt (`App\Services\PressRelease\PressReleaseHtmlSanitizer`).
- **Sidebar-Aufbau** in Customer- und Admin-Forms identisch (Status & Absenden, Kategorie, Portal-Pill, Pressekontakt, Themen-Tags, Veröffentlichung, Weitere Felder, Phase-2-Footer).
- **Pressekontakt-Pflichtfeld** aufgehoben — Auswahl bleibt empfohlen, ist aber technisch nullable. Eine Warn-Box im Sidebar-Card (Phase 8) macht das transparent.
- **Anhänge-/Downloads-UI deaktiviert** wegen ausstehendem Security-Review. Tabelle `press_release_attachments` und Manager-Komponente bleiben erhalten.
- **Background-Job** `php artisan press-releases:publish-scheduled` veröffentlicht geplante PMs (alle 5 Minuten via Scheduler).
- **UX**: `Flux::toast()` für alle Erfolg/Fehler-Meldungen, Smooth-Scroll zum ersten Validation-Fehler nach Save, `presubmitChecks` als kompakte Pflichtfeld-Übersicht im Sidebar.
## Phase 8 (abgeschlossen 29.05.2026)
Plan-Doku: `docs/PHASE-8-USER-PANEL-PLAN.md`. Schwerpunkte:
1. ✅ Show-Page-Lücken aus Phase 7 schließen (Subtitle, Scheduling, Embargo, Boilerplate-Override) — Customer + Admin (8A).
2. ✅ Listen-Indikatoren für geplante Veröffentlichung und Embargo (8B).
3. ✅ Pressekontakt-Warn-Box in der Form-Sidebar, wenn kein Kontakt gewählt (8C).
4. ✅ Firmen-Liste auf Mockup-Niveau (Counter-Strip, Saved-Views, Filter-Chips, Card/List-Toggle, Rollen-Legende) (8E).
5. ✅ SVG-Platzhalter-Set für PM-Titelbilder + Auswahl-Modal + Cover-Resolver (8F/8G).
6. ✅ Image-Manager mit Lizenz-Pflichtfeldern (Urheber/Lizenztyp/Lizenz-URL/Rechte-Bestätigung) (8H).
7. ✅ Veröffentlichungs-Modal mit rechtlichen Hinweisen und Kontingent-Stub (8I/8J; das echte Tarif-System kommt später).
## KI-Prüfung & Veröffentlichung (abgeschlossen 11.06.2026)
Detail-Doku: `Entwicklungsplan KI-Pruefung und Veroeffentlichung.md`. Kurzfassung:
- Jede Einreichung (Customer-Form **und** API) läuft durch denselben Funnel: Blacklist-Hard-Filter → asynchrone KI-Klassifikation (Rot/Gelb/Grün, OpenAI mit deterministischem Fallback) → Status-Routing.
- Rot → abgelehnt + Begründung per Mail; Gelb → manuelle Admin-Review-Queue; Grün → Auto-Publish (sofort oder zum geplanten Termin).
- Jede KI-Entscheidung wird in `ki_audits` protokolliert; Admin sieht Badge, Begründung und kann nach Klassifikation filtern. On-Demand-Prüfung über den „Prüfung"-Button im Admin-Editor.
- Zusätzlich Content-Score 0–100 → Stufe Standard/Geprüft/Hochwertig mit Editor-Panel und Badges.
- Parallel umgesetzt (10./11.06.): ein Titelbild pro PM (Cover 1280×580), erweitertes Lizenz-/Rechteformular, Termine in Europe/Berlin (Speicherung UTC), Embargo aus der Form-UI entfernt — siehe `Umsetzung Pressemitteilung Bearbeitung Titelbild Veroeffentlichung.md`.
## Topbar
Oben rechts über dem Content:
**Firmen-Kontext-Switcher** Dropdown „Aktive Firma: [bma.cc ▼]" mit drei Optionen. Vorteil: Die Sidebar bleibt schlank und einklappbar, ohne den aktiven Firmenkontext zu verlieren.
- Einzelne Firma wählen → filtert Dashboard, PMs, Kontakte, Statistiken auf diese Firma
- „Alle Firmen" → aggregierte Sicht
- „Firma anlegen" am Ende der Liste
Da die User-Firmen-Beziehung n:m mit Rollen ist (`member`, `responsible`, `owner`), zeige ich pro Eintrag ein dezentes Rollen-Icon. Das hilft dem User zu verstehen, wo er was darf.
Wichtig: Da das Portal über die Firma abgeleitet wird, ist der Switcher implizit auch der Portal-Switcher. Sauber gelöst, ohne zweites Konzept.
---
## Hauptnavigation (überarbeitet)
### Gruppe „Mein Bereich"
**1. Dashboard** Zusätzlich zu vorher genannten Elementen jetzt mit **Datenqualitäts-Hinweisen**, weil das Datenmodell zeigt, dass viele Pflichtfelder optional sind:
- „Rechnungsadresse fehlt – Rechnungen können nicht erstellt werden"
- „3 Pressemitteilungen ohne Firmenzuordnung (Legacy)"
- „Profil unvollständig – ergänzen für Verifizierung"
Diese Hinweise sind dismissible und verschwinden bei Erledigung. Das senkt deinen Support-Aufwand erheblich, weil User ihre eigenen Datenlücken sehen.
Phase: **Grün**, wenn die Hinweise auf vorhandene Tabellen beschränkt bleiben (`profile`, `billing_addresses`, `company_user`, `companies`, `press_releases`).
**2. Pressemitteilungen** Erweiterungen aus dem Datenmodell:
- **PM-Detailansicht** zeigt einen „Status & Verlauf"-Block (aus `press_release_status_logs` mit Status-Wechseln, Grund, Quelle, Zeitpunkt) — als Card auf der Show-Page, nicht als eigener Tab.
- Filter „PMs ohne Firma" (für Legacy-Migration)
- Filter „PMs mit Portalabweichung" (falls du das den Usern zeigen willst – ich würde es eher in den Admin-Bereich legen)
- **Filter-Presets** (aus `user_filter_presets`): User kann seine eigenen Filter speichern, „Meine Entwürfe der letzten 30 Tage" etc.
- In der PM-Detail: zugeordnete `press_release_contact`-Kontakte als eigene Sektion
Stand 21.05.2026:
- **PM-Felder** umfassen jetzt zusaetzlich `subtitle`, `scheduled_at`, `embargo_at`, `boilerplate_override`, `no_export` (Phase 7).
- **Editor** ist `flux:editor` mit Absaetzen, fett, kursiv, Listen, Zitat, Links und Headings. Der Inhalt wird beim Speichern serverseitig durch HTMLPurifier (`mews/purifier`, gekapselt in `PressReleaseHtmlSanitizer`) bereinigt.
- **Pressekontakt-Auswahl** ist Single-Select aus den Kontakten der Firma, **optional** und mit Warn-Box, wenn leer. Das Pivot `press_release_contact` bleibt n:m, fuer den Customer-Flow wird aber maximal ein Kontakt pro PM gespeichert.
- **Anhaenge** sind im UI deaktiviert (Security-Review). Tabelle `press_release_attachments` und Service `PressReleaseAttachmentStorage` bleiben erhalten.
- **Filter-Presets** sind weiterhin **Gelb** (Tabelle existiert, UI noch nicht aktiv).
Phase: **Gruen** fuer Liste, Detail, Statusverlauf, Firmenpflicht, Untertitel, Scheduling und Boilerplate-Override. **Umgesetzt (11.06.)**: KI-Pruefung bei Einreichung (Klassifikation + Content-Score, siehe `Entwicklungsplan KI-Pruefung und Veroeffentlichung.md`); Embargo wurde aus der Form-UI entfernt. **Gelb** fuer Filter-Presets. **Rot/Spaeter** fuer Vorab-KI-Pruefung ohne Einreichung, Notice-and-Action und Korrektur-/Update-Hinweis-System (siehe `Presseportal – Konzept für Relaunch.md`).
**3. Firmen** Klar strukturierter Detailbereich pro Firma, weil hier am meisten dranhängt:
- Tab **Stammdaten** (Firma, Logo, Branche, Footer-Code-Flag)
- Tab **Pressekontakte** – wichtig: Kontakte hängen an der Firma, nicht direkt am User. Hier verwaltet der User die Liste der hinterlegten Pressekontakte. Direkte `contact_user`-Pivots würde ich für den User unsichtbar lassen, da sie eher technisches Artefakt sind.
- Tab **Pressemitteilungen** der Firma
- Tab **Statistik** der Firma
- Tab **Abrechnung** – falls Zahlungsoptionen über `user_payment_option_company` an Firmen gehängt sind, hier sichtbar
- Eigentümer-Anzeige: konsolidiert aus `owner_user_id` UND `company_user.role = owner`. Falls beides existiert und divergiert → Datenqualitätshinweis (eher Admin-Thema, aber User sollte zumindest wissen, wer Owner ist)
Phase: **Grün** für Stammdaten, Kontakte, PMs und Eigentümeranzeige auf bestehendem Modell. **Gelb** für Team-Einladungen, Owner-Übertragung und firmenscharfe Abrechnung.
**4. Medien** Wie zuvor (Eigene / Stock / KI), aber Bilder kommen aus `press_release_images`. Konzeptionell sollte die Bibliothek über alle PMs/Firmen aggregieren. Wenn das Datenmodell aktuell nur 1:n PM→Bild ist, müsste das später auf eine eigenständige `media_library` mit polymorpher Verwendung in PMs umgebaut werden. Aber das ist Phase 2 – fürs Konzept reicht erstmal die Aggregations-Sicht.
Phase: **Gelb** für eine aggregierte Ansicht vorhandener `press_release_images`; **Rot/Später** für eine echte wiederverwendbare Medienbibliothek.
**5. Statistiken** Reichweite/Performance aggregiert oder nach Switcher-Auswahl gefiltert.
Phase: **Grün**, soweit nur vorhandene `hits`, PM-Status und Zeiträume genutzt werden. Erweiterte Quellen, Verweildauer oder Demografie sind **Rot/Später**.
---
### Gruppe „Buchen & Bezahlen"
**6. Buchungen & Add-ons** _(neu als eigener Punkt)_ Zentraler Marktplatz für alles Verbrauchsbasierte:
- Highlights (Kategorie / Startseite / Top-Slot)
- KI-Services (Lektorat, Quality-Check, Übersetzung, Bildgenerierung)
- Premium-Stock
- Newsletter-Erwähnung
- Verteiler-Versand
- Verifiziertes Firmenprofil
- Custom Domain
Mit Tabs:
- **Verfügbar** (Marktplatz, alle buchbaren Services)
- **Aktive Buchungen** (was läuft gerade, wann endet es)
- **Verlauf** (was wurde wann gebucht)
Zusätzlich: Aus dem PM-Editor heraus immer noch der direkte „Highlight buchen"-Button als kontextueller Einstieg. Beide Wege koexistieren.
**7. Credits & Tarif** Wie zuvor, zwei Tabs.
**8. Rechnungen** Wie zuvor: aktuelle + Legacy als Archiv-Tab.
---
### Gruppe „Konto"
**9. Einstellungen** Tabs strukturiert auf Basis des Datenmodells:
- **Profil** (`profiles`-Daten: Anrede, Titel, Adresse, Geburtsdatum, Backlink, Statistik-/Footer-Code-Flags)
- **Rechnungsadresse** (`billing_addresses` – getrennt von Profil, weil eigene Tabelle und oft abweichend)
- **Sicherheit** – hier zeigt das Datenmodell Möglichkeiten, die du dem User geben solltest:
- Passwort & 2FA
- Aktive Sessions (`sessions`)
- **Magic-Link-Verlauf** (`magic_links` – Zweck, Zeitpunkt, IP) – wertvoll für Transparenz und Sicherheit
- Login-Verlauf
- **Benachrichtigungen** – verbunden mit `newsletter_subscriptions`: User sieht hier seine Newsletter-Abos pro Portal, kann steuern
- **Zahlungsmethoden** (`user_payment_options` – inkl. Verknüpfung zu Firmen, falls vorhanden)
- **Team** (für Agency-Tarif: `company_user`-Pivots verwalten, Rollen vergeben)
- **API & Integrationen**:
- Tokens (`personal_access_tokens` mit Berechtigungen, letzter Zugriff)
- **API-Nutzungs-Log** (`api_usage_logs` – Methode, Pfad, Status, Dauer) als eigener Sub-Tab. Das ist Gold für API-User und entlastet deinen Support enorm.
- Webhooks
Phase: **Grün** für Profil, Rechnungsadresse, Sicherheit, Newsletter und API-Tokens. **Gelb** für Magic-Link-/Token-Request-Historie und API-Nutzungs-Log. **Rot/Später** für Webhooks.
Hinweis unten bei dem Namen ist ein Menü, wo auch noch einmal Settings verknüpft sind https://pressekonto.test/settings/profile
---
### Gruppe „Hilfe"
**10. Hilfe & Support** wie zuvor.
---
# Was sich konkret durch das Datenmodell geändert hat
|Feature|Wo verankert|Datenquelle|
|---|---|---|
|Datenqualitäts-Hinweise auf Dashboard|Dashboard|`profile`, `billing_address`, PM-`company_id`-Null-Checks|
|PM-Statusverlauf|PM-Detail, Tab „Verlauf"|`press_release_status_logs`|
|Filter-Presets|PM-Liste|`user_filter_presets`|
|Magic-Link-Historie|Einstellungen → Sicherheit|`magic_links`|
|API-Nutzungs-Log|Einstellungen → API|`api_usage_logs`|
|Newsletter-Abos|Einstellungen → Benachrichtigungen|`newsletter_subscriptions`|
|Pressekontakte je Firma|Firma → Bereich „Pressekontakte"|`contacts` via `company_id`|
|Eigentümer-Anzeige|Firma → Stammdaten|`owner_user_id` + `company_user.role`|
|Zahlungsoptionen pro Firma|Firma → Bereich „Abrechnung"|`user_payment_option_company`|
---
# Zwei strategische Punkte aus deinem Datenmodell, die ich aufwerfen würde
**1. Direkte `contact_user`-Pivots im User-UI verstecken** Das Datenmodell erlaubt, Kontakte direkt an User zu hängen (zusätzlich zur Pflicht-Zuordnung an eine Firma). Für den User-UI würde ich das **nicht** sichtbar machen – das verwirrt. Kontakte werden über die Firma verwaltet. Punkt. Die direkte Pivot-Zuordnung kann technisch bleiben (z. B. „User darf diesen Kontakt sehen" über alle Firmen hinweg), aber UI-seitig bleibt es bei „Firma → Kontakte".
**2. PMs ohne Firma** Das Datenmodell erlaubt `company_id = null`. Im User-UI würde ich diese Fälle:
- Auf dem Dashboard als Hinweis listen („3 PMs ohne Firmenzuordnung – jetzt zuordnen")
- In der PM-Liste als eigenen Filter
- Im PM-Editor als Pflichtfeld erzwingen (auch wenn DB es zulässt)
So drehst du die Datenqualität schrittweise sauber, ohne harte Migration.
---
# Firmen-Detail (User-Sicht)
> **IST-Stand 21.05.2026**: Die Firmen-Detailseite ist umgesetzt als
> **eine lange Seite mit Quick-Nav-Ankern** statt mit echten Tab-Wechseln.
> Die im folgenden beschriebene Tab-Struktur ist konzeptuell gleichwertig
> und kann bei Bedarf in eine echte Tab-Komponente umgezogen werden,
> ohne den Funktionsumfang zu aendern.
>
> **Route**: `/admin/me/press-kits/{company}` mit dem Routen-Namen
> `me.press-kits.show`. In der UI heisst der Bereich „Firmen".
## Aufruf
Drei Wege führen hierher:
- Klick auf einen Eintrag in der Firmen-Liste
- Klick auf den Firmennamen im Firmen-Kontext-Switcher (→ aktive Firma + Sprung in Detail)
- Tiefenlinks aus PM-Detail („zur Firma"), Statistik („Firma im Detail")
URL-Struktur: `/firmen/{id}` im User Backend (konzeptueller Zielzustand). IST: `/admin/me/press-kits/{id}` (siehe Routen-Name oben). Öffentliche Pressemappe bleibt ein separater PR-Kontext.
---
## Header (über allen Tabs sichtbar)
Kompakte Header-Karte mit:
- **Logo** (links, klickbar zum Ändern wenn berechtigt)
- **Firmenname** + dezenter `slug`-Hinweis
- **Status-Badges nebeneinander**:
- Portal (welches der beiden Portale)
- Verifizierungs-Status (Häkchen oder „Nicht verifiziert")
- Aktiv/Inaktiv
- Deine Rolle: Owner / Verantwortlich / Mitglied
- **Aktions-Menü** rechts:
- Primär: „Neue Pressemitteilung" (führt direkt in Editor mit dieser Firma vorausgewählt)
- Sekundär (Dropdown): „Verifizierung beantragen", „Custom Domain einrichten", „Als inaktiv markieren", „Firma übertragen"
Der Header bleibt beim Tab-Wechsel stehen, sodass Kontext (welche Firma, welche Rolle) nie verloren geht.
---
## Tab-Struktur (6 Tabs)
### Tab 1: Übersicht (Default)
Eine Mini-Dashboard-Sicht für genau diese Firma. Gibt dem User sofort das Gefühl „hier passiert was" beim Reinklicken.
Inhalte:
- **KPI-Reihe**: Anzahl PMs gesamt, Veröffentlicht in den letzten 30 Tagen, Aktive Highlights, Reichweite (30 Tage)
- **Letzte 5 Pressemitteilungen** dieser Firma mit Status und Datum
- **Pressekontakte-Block**: kompakte Liste, „X Kontakte hinterlegt", Sprung in Tab 3
- **Datenqualitäts-Hinweise** firmenspezifisch:
- „Logo fehlt"
- „Keine Pressekontakte hinterlegt – Änderungs-Workflow nicht möglich"
- „Owner-Konflikt: `owner_user_id` und `company_user.role=owner` divergieren" (eher Admin-Hinweis, aber wenn es User betrifft, transparent zeigen)
- „Branche nicht gesetzt – beeinträchtigt Auffindbarkeit"
- **Quick Actions**: „Neue PM", „Pressekontakt hinzufügen", „Highlight buchen"
### Tab 2: Stammdaten
Bearbeitbare Firmendaten. Felder gemäß `companies`-Tabelle:
- Firmenname *
- Logo (Upload, mit Preview)
- Kurzbeschreibung (1–2 Sätze für Listing-Ansichten)
- Lange Beschreibung (für die Firmenseite)
- Branche/Kategorie *
- Adresse: Straße, PLZ, Ort, Land
- Website-URL
- Footer-Code-Flag (mit kurzer Erklärung was es bewirkt)
- Aktivstatus (Toggle, mit Warnhinweis was passiert)
**Eigentümer-Block** (read-only für Nicht-Owner):
- Anzeige des konsolidierten Eigentümers
- Bei Divergenz zwischen `owner_user_id` und `company_user.role=owner`: gelber Warnhinweis mit „An Support melden"-Link
**Portal-Anzeige**:
- Read-only mit Tooltip: „Das Portal wird durch die Firma festgelegt und kann nicht im Self-Service geändert werden. Bei Bedarf bitte Support kontaktieren."
**Verifizierung**:
- Status anzeigen
- Wenn nicht verifiziert: CTA „Verifizierung beantragen" → führt zu Buchungen & Add-ons mit vorausgewähltem Service
### Tab 3: Pressekontakte
Verwaltung der `contacts` dieser Firma. Direkte `contact_user`-Pivots werden hier nicht angezeigt – Kontakte gehören zur Firma, Punkt.
Liste mit:
- Name, Position, E-Mail, Telefon
- Status-Badge: „Magic-Link aktiv" / „Magic-Link inaktiv"
- Anzahl PMs, in denen dieser Kontakt referenziert ist (aus `press_release_contact`)
- Aktionen: Bearbeiten / Löschen / Test-Magic-Link senden
Oben: „+ Neuer Pressekontakt" mit Formular (Name, Position, E-Mail, Telefon, Magic-Link-Berechtigung ja/nein).
**Wichtiger Erklärungsblock** über der Liste (einmalig dismissible):
> Pressekontakte sind die offiziellen Ansprechpartner zu dieser Firma. Sie können sich per Magic-Link einloggen, um Pressemitteilungen zu korrigieren, zu aktualisieren oder DSGVO-Anfragen zu stellen. Hinterlegen Sie alle relevanten Kontakte, um den autorisierten Änderungs-Workflow zu ermöglichen.
Beim Löschen eines Kontakts: Warnung, falls dieser noch in PMs referenziert ist („In 12 PMs hinterlegt – diese verlieren den Kontakt").
### Tab 4: Pressemitteilungen
Gefilterte PM-Liste auf `company_id` dieser Firma.
- Standard-Filter (Alle / Veröffentlicht / In Prüfung / Entwürfe / Depubliziert / Korrekturen)
- Volltextsuche
- „+ Neue Pressemitteilung" mit dieser Firma vorausgewählt
- Pro Eintrag: Titel, Status, Datum, zugeordnete Pressekontakte, Reichweite, Aktionen
Bulk-Aktionen für Power-User: Mehrere PMs auswählen → „Pressekontakte bulk zuordnen", „Exportieren als PDF".
### Tab 5: Statistik
Reichweite und Performance dieser Firma:
- Views, Klicks, Verweildauer im Zeitverlauf (30/90/365 Tage)
- Top-PMs nach Reichweite
- Verteilung nach Quelle (organisch, Newsletter, Distribution-Partner)
- Kategorien-Heatmap (welche Themen performen)
- Aktive Highlights & Buchungen, die dieser Firma zugeordnet sind
- Im Pro-Tarif zusätzlich: Demografie, Geräte, Suchbegriffe
Export-Button (CSV/PDF) – sinnvoll für Reportings, die User intern an Marketing/Geschäftsführung weiterleiten.
### Tab 6: Abrechnung
Hier wird's etwas heikel, weil Abrechnung hauptsächlich am User hängt, aber `user_payment_option_company` einen firmenscharfen Bezug erlaubt.
Inhalte:
- **Zahlungsmethoden für diese Firma** – Liste der `user_payment_options`, die per Pivot mit dieser Firma verknüpft sind
- „Zahlungsmethode dieser Firma zuordnen" (aus den vorhandenen User-Zahlungsmethoden auswählen)
- **Rechnungen mit Firmenbezug** – PMs/Buchungen, die diese Firma betreffen, mit den entsprechenden Rechnungen
- **Klarer Erklärtext oben**:
> Rechnungen werden grundsätzlich auf Ihren User-Account ausgestellt. Hier sehen Sie Zahlungsmethoden und Buchungen, die speziell dieser Firma zugeordnet sind. Eine vollständige Übersicht aller Rechnungen finden Sie unter „Rechnungen".
Dieser Tab ist nur für Owner sichtbar – Member und Verantwortliche haben hier nichts verloren.
---
## Rollen-Logik (aus `company_user.role`)
Klare Sichtbarkeits- und Bearbeitungsregeln:
**Owner**: Alle Tabs, alle Aktionen. Kann Firma deaktivieren, übertragen, Pressekontakte verwalten, Stammdaten ändern, Abrechnung sehen.
**Verantwortlich**: Übersicht, Stammdaten (read-only), Pressekontakte (verwalten), PMs (verwalten + erstellen), Statistik. Kein Tab Abrechnung. Stammdaten-Änderungen mit Hinweis „Nur Owner kann ändern".
**Mitglied**: Übersicht, Stammdaten (read-only), Pressekontakte (read-only), PMs (eigene erstellen, nur eigene bearbeiten), Statistik. Kein Tab Abrechnung.
Im Header die Rolle als Badge zeigen, damit der User immer weiß, was er darf, ohne dass er es durch Klicken herausfindet.
---
## Verknüpfungen zu anderen Bereichen
- **Switcher** (Topbar rechts): Auswahl einer Firma scrollt globale Filter auf diese Firma, der direkte Sprung ins Detail bleibt aber ein expliziter Klick
- **PM-Editor**: PMs werden mit `company_id` erstellt, das Feld ist Pflicht (auch wenn DB nullable). Aus dem Firmen-Detail ist es vorausgewählt.
- **Buchungen & Add-ons**: Highlights, KI-Services, Verifizierung etc. werden in der Buchungs-Sektion abgewickelt, aber aus dem Firmen-Detail kontextuell verlinkt
- **Statistiken (Hauptpunkt)**: Aggregierte Sicht über alle Firmen vs. firmenspezifische Sicht hier im Tab. Beide Wege koexistieren.
- **Einstellungen → Team**: Beim Agency-Tarif können User andere User zur Firma einladen (`company_user`-Pivot mit Rolle setzen). Verlinkung von hier aus sinnvoll.
---
## Offene Designentscheidungen
**1. Firmenwechsel-Bestätigung** Wenn ein User im Firmen-Detail arbeitet und über den Switcher die Firma wechselt – sofort wechseln oder Warnung „ungespeicherte Änderungen"? Ich würde Standard-Browser-Verhalten beibehalten (`beforeunload` bei dirty forms), kein eigener Dialog.
#Ja Ohne Dialog.
**2. Firma deaktivieren vs. löschen** Im Datenmodell ist Aktiv/Inaktiv vorhanden. Echtes Löschen ist heikel wegen verknüpfter PMs, Rechnungen, Kontakte. Ich würde dem User **nur** Deaktivieren anbieten – echtes Löschen läuft über Support-Anfrage. Senkt deine Risiken bei DSGVO-Konflikten.
#Ja Firmen nur dekativieren
**3. Owner-Übertragung** „Firma übertragen" ist ein sensibler Vorgang. Ich würde einen eigenen Wizard mit E-Mail-Bestätigung beim neuen Owner verlangen (ähnlich GitHub-Repo-Transfer). Macht den Punkt komplexer, aber sauber.
#ja genau so soll es gemacht werden, wird allerdings oft nach den Relaunch verschoben.
**4. Pressekontakt-Zuordnung beim PM-Erstellen** Beim Anlegen einer neuen PM: Sollen alle Pressekontakte der Firma automatisch zugeordnet werden, oder muss der User explizit auswählen? Ich tendiere zu „alle vorausgewählt, abwählbar" – gibt dem User eine Voreinstellung, die in 90 % der Fälle stimmt.
Ja Würde ich so machen die meisten haben eh nur einen oder keine Pressekontakt.
---

View file

@ -1,263 +0,0 @@
# Billing & Rechnungskreise (hybrides Modell)
Stand: 12.06.2026 — Datenmodell, MAN-Kreis, USt-Behandlung (Phase 9D) sowie
Stripe-Sync, Webhook-Verarbeitung, Checkout-Flows, Plan-Kontingent
(Phase 9E) und Tarif-Seite/Checkout-UI (Phase 9F) umgesetzt.
Dieses Dokument ist die zentrale Referenz für das Abrechnungssystem:
Rechnungskreise, Tarif-Datenmodell, Steuerlogik, Befehle und Konfiguration.
Verwandte Dokumente:
- [`docs/Decision-Update Preisstruktur & Veröffentlichungs-Flow.md`](../Decision-Update%20Preisstruktur%20&%20Ver%C3%B6ffentlichungs-Flow.md) — verbindliche Launch-Entscheidungen (Tarife, Kontingente, Flow, Netto-Preise).
- [`docs/PHASE-9-FLOW-UND-TARIFE-PLAN.md`](../PHASE-9-FLOW-UND-TARIFE-PLAN.md) — Umsetzungsplan mit Päckchen-Status.
- `dev/migration 2026/05-DATABASE-MERGE.md` §5.5/§5.6 — Rechnungsarchiv (D-12) und Grandfathering (D-13).
---
## 1. Die drei Rechnungswelten
| Welt | Präfix | Tabelle | Inhalt |
|---|---|---|---|
| **Stripe-Shop** | `STR-` | `invoices` | Alle **neuen** Abschlüsse (Abos, Einzel-PM, Credits). Abwicklung komplett über Stripe; Rechnungen werden per Webhook in `invoices` gespiegelt und erhalten eine fortlaufende STR-Nummer. *(Spiegelung: Phase 9E)* |
| **Manuell/Legacy** | `MAN-` | `invoices` | Laufende, noch aktive Alt-Zahlungsvereinbarungen ab Relaunch. Fälligkeit wird täglich geprüft, Rechnung wie im Altsystem ausgestellt. |
| **Alt-Archiv** | — | `legacy_invoices` | Read-only Archiv aller importierten Legacy-Rechnungen (D-12). Wird nie verändert; PDFs werden on-demand aus den Archivdaten erzeugt. |
**Rechnungsnummern**: `InvoiceNumberGenerator` vergibt atomar (Row-Lock auf
`invoice_number_sequences`) fortlaufende, lückenlose Nummern pro Kreis:
`STR-00001`, `MAN-00001`, … (Padding: `billing.invoice_number_padding`).
---
## 2. Tarif-Datenmodell
| Tabelle | Zweck |
|---|---|
| `plans` | Tarif-Katalog (Starter/Business/Pro/Agency): Monats-/Jahrespreis **netto**, PM-Kontingent/Monat, Tageslimit, Stripe-Produkt-/Preis-IDs. Seeder: `PlanSeeder` (idempotent). |
| `subscriptions`, `subscription_items` | Laravel-Cashier-Tabellen — Zustand der Stripe-Abos. `User` ist `Billable`. |
| `single_purchases` | Einmalkäufe: Einzel-PM (19 €), Extra-PM, Boost, Veröffentlichungsnachweis-PDF. Status: pending → paid → consumed (oder refunded). |
| `payment_options` / `user_payment_options` | Legacy-Zahlungsvereinbarungen. Grandfathered-Einträge tragen die Netto-Vertragsbasis in `legacy_conditions`; versteckte Katalog-Platzhalter `LEGACY-{PE\|BP}-{Artikel}`. |
| `invoice_number_sequences` | Fortlaufende Nummern pro Rechnungskreis. |
**Submit-Gate** (`User::hasActiveBooking()`, hinter `billing.enforce_booking`):
Eine aktive Buchung ist ein Cashier-Abo **oder** ein bezahlter, noch nicht
eingelöster Einzel-/Extra-PM-Kauf **oder** eine aktive/grandfathered
Legacy-Vereinbarung. Bestandskunden behalten damit nach Gate-Aktivierung
volle Einreichungsrechte.
**PM-Kontingent** (`User::pressReleaseQuotaRemaining()`, null = unbegrenzt):
| Wer | Kontingent |
|---|---|
| Launch-Schalter `billing.enforce_booking` aus | unbegrenzt (Vor-Launch-Zustand) |
| Bestandskunde (aktive/grandfathered Legacy-Vereinbarung) | **unbegrenzt** — Entscheidung 12.06.2026: Bestandsschutz gilt, das Alt-Produkt sah unbegrenzte PMs vor; eine Migration auf neue Tarife kommt ggf. später |
| Abonnent | Monatskontingent des Tarifs (`plans.press_release_quota`) plus offene Einmalkäufe |
| Nur Einmalkäufe | Anzahl bezahlter, noch nicht eingelöster Einzel-/Extra-PM-Käufe |
**Slot-Verbrauch** (erst bei Veröffentlichung, Decision-Update §3.2; Ablehnung
kostet nichts; Re-Publish nach Archivierung zählt nicht doppelt): zuerst das
Plan-Monatskontingent (Zähler `users.press_release_quota_used_this_month`,
monatlicher Reset), danach wird der älteste bezahlte Einmalkauf eingelöst
(`single_purchases.status → consumed`, verknüpft mit der PM). Die frühere
Stub-Spalte `users.press_release_quota` ist entfernt.
**Checkout-Einstiege** (Phase 9E/9F — verdrahtet auf der Buchungs-Seite):
| Route | Zweck |
|---|---|
| `me.checkout.subscription` (`/admin/me/checkout/abo/{slug}/{monthly\|yearly}`) | Stripe-Checkout für ein Tarif-Abo |
| `me.checkout.single-pm` (`/admin/me/checkout/einzel-pm`) | Stripe-Checkout Einzel-PM (legt `single_purchases`-Eintrag `pending` an; Webhook setzt `paid`) |
| `me.checkout.billing-portal` (`/admin/me/checkout/abo-verwalten`) | Stripe Billing Portal (Zahlungsmethode, Rechnungen, Kündigung) |
**Buchungs-Voraussetzung** (12.06.2026): Jeder Checkout erfordert eine
vollständige Rechnungsadresse (`User::hasCompleteBillingAddress()`,
Pflicht: Nachname, Straße, PLZ, Ort, Land) — sonst Redirect aufs Profil
mit Hinweis. Lokale Adresse (`User::stripeAddress()`) und USt-ID
(`StripeCheckoutService::syncTaxIdFromBillingAddress`, Typ `eu_vat`)
werden an den Stripe-Customer übergeben.
Erfolg/Abbruch landen auf der Buchungs-Seite (`?checkout=erfolg|abbruch`).
Die Steuer ergänzt **Stripe Tax** automatisch (`Cashier::calculateTaxes()`
im AppServiceProvider, Netto-Preise mit `tax_behavior: exclusive`) — nach
denselben Regeln wie der VatResolver im MAN-Kreis, inkl. USt-ID-Abfrage im
Checkout.
---
## 3. MAN-Kreis: Fälligkeitslauf für Legacy-Zahlungen
Täglicher Scheduler-Lauf (04:30): `billing:generate-manual-invoices`
1. Findet `user_payment_options` mit Status `active`/`grandfathered`,
**ohne** `stripe_subscription_id`, deren `current_period_end` erreicht ist.
2. Friert die Rechnungsadresse als Snapshot ein (`invoice_billing_addresses`,
inkl. `vat_id`).
3. Stellt die Rechnung aus: Netto-Basis × USt-Regel (Abschnitt 4),
MAN-Nummer, Zahlungsziel `billing.manual_due_days` (Default 14 Tage).
4. Schaltet die Periode weiter (`monthly`/`yearly` aus `legacy_conditions`
bzw. `payment_options.interval`).
Nicht abrechenbare Fälle (fehlende Rechnungsadresse, kein Intervall) werden
geloggt, **die Periode bleibt stehen** — der nächste Lauf versucht es erneut.
Solche Vereinbarungen erscheinen in `/admin/payments` im Panel **„Blockierte
Vereinbarungen"** (Grund + Link ins User-Profil, „Jetzt abrechnen" nach dem
Nachtragen der Adresse). Optionen: `--dry-run`, `--limit=50`.
Jede MAN-Rechnung trägt `user_payment_option_id` (Link zur Vereinbarung) und
ggf. `replaces_legacy_invoice_number`, wenn sie eine offene Alt-Rechnung aus
dem Archiv ersetzt (siehe 3a).
### 3a. Bearbeitung nach der Ausstellung (Überweisungskunden, 16.09.2026)
MAN-Kunden zahlen per Überweisung. Der Weg nach dem 04:30-Lauf ist ein
Knopfdruck-Workflow in `/admin/payments` → „Rechnungsausgang", Filter
„Offene MAN-Rechnungen" (Default). Logik: `ManualInvoiceDunningService`,
jede Aktion mit Eintrag im Aktivitäts-Verlauf (`ActivityType::ManualInvoice`).
| Aktion | Wirkung |
|---|---|
| **Rechnung senden** | Mail `ManualInvoiceIssued` mit PDF-Anhang (`ManualInvoicePdfRenderer`), Bankverbindung aus `billing.issuer`, Hinweis auf Bestandsschutz-Ende. Setzt `sent_at` und **`due_date = heute + manual_due_days`** — die Frist zählt ab Versand. Verweigert bei Adresse auf der Mail-Sperrliste. |
| **Erinnerung** | Gleiche Mail im Mahn-Ton (`isReminder`), `reminded_at`/`reminder_count`. Verstrichene Frist → Nachfrist `billing.reminder_due_days` (7) ab heute, sonst bleibt `due_date`. |
| **Bezahlt** | `status = paid`, `paid_at` = Datum laut Kontoauszug. |
| **Bestandsschutz beenden** | Rechnung `void` (`voided_at`, `void_reason`), weitere offene MAN-Rechnungen des Kunden ebenfalls `void`, `LegacySubscriptionService::cancelBundleFor()`; optional Kundenmail `GrandfatheringTerminated`. Rechnungen werden nie gelöscht (Nummernfolge, GoBD). |
| **Entfernen** (Panel „Blockierte Vereinbarungen") | Nicht abrechenbare Vereinbarung ohne Rechnung beenden (`abandonAgreement`): Bündel gekündigt, offene MAN-Rechnungen `void`, offene Alt-Rechnung im Archiv `uncollectible`. Keine Kundenmail. |
PDF-Routen: `admin.invoices.pdf` (Admin) und `me.invoices.manual-pdf`
(Kunde, `InvoicePolicy::downloadPdf`); STR-Rechnungen liefern 404 — dort
bleibt Stripe der Beleg.
### 3b. Altlast aus unbezahlten Legacy-Rechnungen (`legacy:settle-unpaid`)
Legacy-Status `hold` = nie bezahlt, ersatzlos erledigt (Einordnung
16.09.2026). `pending`/`reminder` = offene Vorgänge. Der Befehl
`legacy:settle-unpaid` (`--dry-run`, `--no-invoice`) räumt auf:
- Bestandsschutz-Posten mit `hold`-Quellrechnung → Bündel gekündigt, offene
MAN-Rechnungen storniert. Die Archivzeilen bleiben erhalten und werden im
Admin-Archiv sowie im Kundenkonto standardmäßig ausgeblendet
(`LegacyInvoice::scopeWithoutHold`).
- `pending`/`reminder` (recurring) → Periode auf den Leistungsbeginn der
offenen Rechnung zurückgedreht (`legacy_conditions.open_legacy_invoice`),
sofort MAN-Rechnung ausgestellt; die Alt-Rechnung erhält Status
`superseded` (+ `raw_snapshot.superseded_by_invoice_number`).
- `pending`/`reminder` (single) → `uncollectible`.
`legacy:grandfather-subscriptions` wertet den Zahlungsstatus seit 16.09.2026
selbst aus (hold → übersprungen, offen → Periode auf Leistungsbeginn).
**Befüllung**: `legacy:grandfather-subscriptions` (Migrations-Runbook, nach
`legacy:archive-invoices`) leitet die aktiven jährlichen Vereinbarungen aus
dem Rechnungsarchiv ab — Replay-fähig für den Lauf kurz vor Relaunch.
Details: `dev/migration 2026/05-DATABASE-MERGE.md` §5.6.
---
## 4. USt-Behandlung (Entscheidung 12.06.2026)
**Alle neuen Preise sind Netto-Preise.** Die Steuer wird zur
Rechnungsstellung über `App\Services\Billing\VatResolver` aus der
Rechnungsadresse bestimmt:
| Fall | Behandlung | Rechnung |
|---|---|---|
| Deutschland | immer mit Steuer (`billing.vat_rate`, Default 19 %) | Netto + USt ausgewiesen |
| EU mit gültiger USt-ID | befreit (Reverse Charge) | `is_netto`, Pflichthinweis in `tax_note` |
| EU ohne USt-ID | mit Steuer | Netto + USt ausgewiesen |
| Drittland | grundsätzlich befreit | `is_netto`, Hinweis „nicht im Inland steuerbar" |
- Die USt-ID wird im Profil gepflegt (bestehendes Feld) und zusätzlich an
der Rechnungsadresse (`billing_addresses.vat_id`) gespeichert; jede
Rechnung friert sie im Adress-Snapshot ein.
- „Gültig" = vorhanden + formal plausibel (Länder-Präfix, EL für
Griechenland). **Offen: echte VIES-Validierung** — vor Aktivierung von
Gate/Checkout umsetzen.
- **Legacy-Umrechnung**: Das Altsystem fakturierte brutto (199 € inkl.
Steuer; Befreite mit Netto-Ausweis 167,23 €). Die Grandfather-Migration
leitet daraus die Netto-Basis ab (`legacy_conditions.net_cents`) — für
deutsche Bestandskunden bleibt der Bruttobetrag unverändert, die Steuer
wird künftig nur sauber ausgewiesen.
---
## 5. Befehle & Scheduler
| Befehl | Zweck | Scheduler |
|---|---|---|
| `billing:generate-manual-invoices` | MAN-Fälligkeitslauf (Abschnitt 3) | täglich 04:30 |
| `legacy:settle-unpaid` | Altlast unbezahlter Legacy-Rechnungen auflösen (Abschnitt 3b) | manuell, wiederholbar |
| `billing:sync-stripe-plans` | Tarife + Einzel-PM als Netto-Produkte/Preise nach Stripe synchronisieren (idempotent; `--dry-run`) | manuell |
| — Admin-UI: `/admin/payments/plans` | Tarif-Pflege (Preise, Kontingent, Tageslimit, aktiv/inaktiv) mit Sofort-Sync nach Stripe (`StripePlanSyncService`): Preisänderung legt ein neues Price-Objekt an und deaktiviert das alte; Bestandsabos behalten ihren Preis | — |
| `legacy:grandfather-subscriptions` | Aktive Legacy-Abos aus dem Archiv migrieren | manuell (Migrations-Runbook) |
| `press-releases:reset-monthly-quota` | Monatlicher Reset des Plan-Kontingent-Zählers (`press_release_quota_used_this_month`) | monatlich, 1. um 00:05 |
---
## 6. Konfiguration
`config/billing.php`:
| Schlüssel | ENV | Default | Bedeutung |
|---|---|---|---|
| `enforce_booking` | `BILLING_ENFORCE_BOOKING` | `false` | Submit-Gate scharf schalten (Launch-Schalter) |
| `invoice_number_padding` | — | `5` | Stellen der laufenden Nummer |
| `manual_due_days` | `BILLING_MANUAL_DUE_DAYS` | `14` | Zahlungsziel MAN-Rechnungen (ab Versand) |
| `reminder_due_days` | `BILLING_REMINDER_DUE_DAYS` | `7` | Nachfrist einer Zahlungserinnerung |
| `issuer.*` | `BILLING_ISSUER_*` | adametz.media | Aussteller, Bankverbindung, Steuernummer auf MAN-Rechnungen und in der Zahlungsmail |
| `vat_rate` | `BILLING_VAT_RATE` | `0.19` | USt-Satz für steuerpflichtige Fälle |
| `eu_country_codes` | — | EU-27 ohne DE | Basis der Drittland-/EU-Unterscheidung |
Stripe/Cashier:
| ENV | Bedeutung |
|---|---|
| `STRIPE_KEY` / `STRIPE_SECRET` | Publishable/Secret Key (Test-Keys gesetzt) |
| `STRIPE_WEBHOOK_SECRET` | Signatur-Prüfung des Webhook-Endpoints (gesetzt; Endpoint `https://pressekonto.com/stripe/webhook` im Dashboard registriert, 12.06.2026) |
| `STRIPE_PRICE_SINGLE_PM` | Stripe-Price-ID der Einzel-PM (legt `billing:sync-stripe-plans` an; ohne sie ist der Einzel-PM-Checkout deaktiviert) |
| `BILLING_OWN_VAT_ID` | Eigene deutsche USt-ID des Betreibers — schaltet die eVatR-Online-Bestätigung ausländischer EU-USt-IDs frei (BZSt-REST-API, `VatIdValidationService`); ohne sie bleibt es bei der Formatprüfung |
| `CASHIER_CURRENCY` / `CASHIER_CURRENCY_LOCALE` | `eur` / `de_DE` (gesetzt) |
**Benötigte Webhook-Events** am Stripe-Endpoint: `invoice.payment_succeeded`
(STR-Spiegelung), `checkout.session.completed` (Einmalkauf-Erfüllung) sowie
die Cashier-Standardevents `customer.subscription.created/updated/deleted`,
`customer.updated`, `customer.deleted` (Abo-Zustand).
**Lokal testen** (der registrierte Endpoint zeigt auf die Live-Domain und
läuft bis zum Relaunch ins Leere — das ist unkritisch, Stripe versucht die
Zustellung nur erneut): Stripe CLI verwenden —
`stripe listen --forward-to pressekonto.test/stripe/webhook` und das von der
CLI ausgegebene `whsec_…` temporär als `STRIPE_WEBHOOK_SECRET` in die `.env`.
---
## 7. Offene Punkte (Stand 12.06.2026, nach Phase 9E)
0. **9E erledigt**: Stripe-Sync (Tarife + Einzel-PM als Netto-Produkte,
Test-Mode), Webhook-Verarbeitung (STR-Spiegelung, Einmalkauf-Erfüllung,
Endpoint + Secret eingerichtet), Checkout-Flows (Abo + Einzel-PM,
Routen siehe Abschnitt 2), Slot-Logik auf Plan-Kontingent umgestellt
(Grandfathered = unbegrenzt, Entscheidung 12.06.2026), Stub-Spalte
entfernt, Stripe Tax aktiviert (`Cashier::calculateTaxes()`).
1. **Phase 9F erledigt** (12.06.2026): Die Buchungs-Seite zeigt das echte
Tarif-Raster (Monat/Jahr-Toggle), den Einzel-PM-Block, Bestandstarife
und „Abo verwalten" (Stripe Billing Portal, `me.checkout.billing-portal`).
1b. **Admin-Zahlungsmodul erledigt** (12.06.2026): `/admin/payments` zeigt
KPIs (aktive Abos, MRR netto, Umsatz 30 Tage, offene Einzel-PMs) plus
Abo-, Einmalkauf- und Rechnungstabellen (STR/MAN) mit User-Suche;
`/admin/payments/plans` pflegt die Tarife mit Sofort-Sync nach Stripe
(Abschnitt 5). Refund-Workflow direkt aus dem Admin bleibt offen
(vorerst über das Stripe-Dashboard).
2. **Stripe Tax**: im Dashboard aktiviert (12.06.2026, Produkt-Steuercode
„SaaS – business use", Steuer nicht im Preis enthalten — passt zu den
Netto-Preisen). Vor Relaunch im **Live-Mode** wiederholen; dort auch
`billing:sync-stripe-plans` erneut ausführen (Live-Produkt-IDs).
3. **VIES-Validierung** der USt-ID (aktuell Formatprüfung; Stripe prüft
die im Checkout erfasste USt-ID asynchron selbst).
4. **PDF-Erzeugung**: MAN-Rechnungen erledigt (16.09.2026,
`ManualInvoicePdfRenderer`, inkl. `tax_note`); STR-Belege kommen weiter
von Stripe.
5. **`ExpireGrandfatheredSubscriptions`**: Benachrichtigung zur Umstellung
auf neue Tarife am `grandfathered_until` (D-13-Rest); erst dann wird
das unbegrenzte Bestandskontingent ggf. migriert.
6. **Steuerberater-Abnahme** der USt-Regeln und Rechnungstexte vor dem
ersten produktiven MAN-/STR-Lauf.
7. **Tageslimit** (`plans.daily_limit`) durchsetzen — Phase 9G.

View file

@ -1,524 +0,0 @@
# Entwicklungsplan: KI-Prüfung & Veröffentlichungs-Pipeline
Stand: 11.06.2026 — **Phasen 0–5 abgeschlossen**, Phase 6 (Trust-Score) offen.
Dieser Plan definiert die schrittweise Umsetzung der automatisierten Prüfung
und Veröffentlichung von Pressemitteilungen (PM). Er ist so geschnitten, dass
jede Phase einzeln umgesetzt, getestet und ausgeliefert werden kann.
> **Abgleich mit dem Decision-Update (11.06.2026)**: Das
> [`Decision-Update Preisstruktur & Veröffentlichungs-Flow`](../Decision-Update%20Preisstruktur%20&%20Ver%C3%B6ffentlichungs-Flow.md)
> setzt auf dieser Pipeline auf und ergänzt zum Launch drei noch offene
> Flow-Regeln, die **nicht** Teil dieses Plans waren:
>
> 1. **Submit-Gate**: „Zur Prüfung einreichen" wird hinter eine aktive
> Buchung gelegt (das Modal zeigt ohne Buchung einen Buchungs-Hinweis).
> 2. **Slot-Verbrauch bei Veröffentlichung** statt bei Einreichung —
> rot abgelehnte PMs verbrauchen keinen Slot. Der aktuelle Quota-Stub
> zählt noch beim Einreichen (`submitForReview`) und muss umgestellt werden.
> 3. **Kein Re-Check zum Launch**: eine Einreichung = eine Prüfung;
> Nachbessern + erneut prüfen kommt erst in Phase 2.
> 4. **Gelb-Routing geändert (Entscheidung 12.06.2026)**: Gelb geht zum
> Launch **direkt live** wie Grün — keine manuelle Review-Queue mehr.
> Nur Rot wird abgelehnt (mit Begründung an den Autor). Phase 4 unten
> beschreibt das ursprünglich gebaute Verhalten (Gelb → manuelle Queue);
> die Umstellung erfolgt im Phase-9-Plan
> (`docs/PHASE-9-FLOW-UND-TARIFE-PLAN.md`, Päckchen 9A).
## Ziel & Leitprinzip
Jede eingehende PM wird **automatisch von einer KI geprüft**. Nur in
**äußersten Fällen** erfolgt eine manuelle redaktionelle Prüfung. Zwei
Einreichungsstellen müssen denselben Prüf-Pfad durchlaufen:
1. **Web-Formular** (Customer- und Admin-Editor)
2. **API** (`/api/v1/press-releases`)
Es gibt zwei voneinander unabhängige Bewertungen (Konzept-Update 1, Abschnitt 15):
- **Klassifikations-Score (Grün/Gelb/Rot)** — der „Red Flag": entscheidet, ob
überhaupt veröffentlicht wird. **Jetzt umzusetzen.**
- **Content-Score (0–100) → Stufe (Standard/Geprüft/Hochwertig)** — die
Qualitätsbewertung („Scoring"). **Spätere Phase.**
## Konzept-Grundlage
- `docs/konzept/Konzept-Update 1 – Überarbeitete Abschnitte.md`, §15.1
(Klassifikations-Score) und §15.2 (Content-Score).
- `docs/konzept/Konzept-Update 2 – Score-Stufen-System.md` (Stufen-Mapping,
`content_tier`, Außenkommunikation).
Klassifikations-Score laut Konzept §15.1:
| Klassifikation | Bedeutung | Auswirkung |
|---|---|---|
| **Grün** | unauffällig | direkte Veröffentlichung (optional 5–10 Min. Verzögerung) |
| **Gelb** | unklar/grenzwertig | manuelle Review-Queue (nicht boostbar) |
| **Rot** | unzulässig | zurück an Autor mit Begründung, keine Veröffentlichung |
Faktoren (Red Flags): Werbung statt PM, beleidigend/diskriminierend, rechtlich
heikel, Spam-Muster, unseriöse Versprechen. Speicherung laut Konzept:
`press_releases.classification` plus Audit-Log `ki_audits`.
## Ist-Zustand (Bestandsaufnahme)
- **Statuswerte** (`App\Enums\PressReleaseStatus`): `draft`, `review`,
`published`, `rejected`, `archived`.
- **Web-Einreichung**: `App\Services\PressRelease\PressReleaseService::submitForReview()`
prüft nur eine wortbasierte Blacklist (`config/blacklist.php` via
`BlacklistService`), setzt sonst Status `review`, erhöht das Quota und
schreibt ein `PressReleaseStatusLog`.
- **Veröffentlichung**: `PressReleaseService::publish()` (Admin-Aktion) und der
Cron `App\Console\Commands\PublishScheduledPressReleases` (publiziert
`review`-PMs mit fälligem `scheduled_at`). Beide prüfen erneut nur die
Blacklist.
- **API**: `App\Http\Controllers\Api\V1\PressReleaseController::store()` /
`update()` schreiben `status` direkt aus dem Request (erlaubt: `draft`,
`review`) und rufen `submitForReview` **nicht** auf. Eine API-PM mit
`status=review` landet damit **ohne** Blacklist-/Quota-/Log-Prüfung in der
Queue.
- **UI-Einreichung (Prozess-Start)**:
- Detailansicht ([show.blade.php]): vollständiges Modal `confirm-submit-review`
mit rechtlichen Hinweisen, Quota-Anzeige und Bestätigungs-Checkboxen →
ruft `submitForReview`.
- Bearbeiten ([edit.blade.php]): Button „Speichern & zur Prüfung" mit nur
`wire:confirm` (Browser-Dialog), **kein** Modal.
- Erstellen ([create.blade.php]): Button „Zur Prüfung senden" ohne Modal.
- **Kein** Datenmodell für Klassifikation/Score: keine Spalten
`classification`, `content_score`, `content_tier`, keine Tabelle `ki_audits`.
## Lücken & Risiken
- **L1 — API-Bypass**: Einreichung über die API umgeht jede Prüfung.
- **L2 — Keine echte Inhaltsprüfung**: nur eine triviale Wort-Blacklist; keine
Erkennung von Werbung, Spam, rechtlich heiklen oder unseriösen Inhalten.
- **L3 — Auto-Publish ohne Klassifikation**: geplante PMs werden vom Cron
veröffentlicht, ohne dass eine inhaltliche Bewertung stattgefunden hat.
- **L4 — Uneinheitlicher Prozess-Start**: das Bestätigungs-Modal existiert nur
in der Detailansicht, nicht beim Bearbeiten/Erstellen.
- **L5 — Kein Audit**: KI-Entscheidungen wären ohne `ki_audits` nicht
nachvollziehbar (DSGVO / Nachweispflicht).
## Zielarchitektur
```
Einreichung (Formular ODER API)
│
▼
SubmissionService.submit() ← ein einziger Funnel
│
├─ Hard-Filter: Blacklist (synchron, deterministisch)
▼
ClassificationService.classify() ← KI (Claude), mit Fallback
│
├─ Rot → status=rejected, Begründung an Autor
├─ Gelb → status=review (manuelle Queue, „äußerste Fälle")
└─ Grün → Veröffentlichungspfad (sofort / geplant)
│
▼
ki_audits (vollständiges Audit-Log jeder KI-Entscheidung)
+
press_releases.classification / classified_at
+
(später) content_score / content_tier
```
Kernregeln:
- Formular **und** API rufen ausschließlich `SubmissionService.submit()` auf.
Die API darf `status` nicht mehr frei setzen; `published` ist über die API
nie erreichbar.
- Re-Klassifikation bei jeder Änderung einer PM (Konzept §15.1: „Bei Änderung
der PM wird neu klassifiziert").
- Schwellen/Verhalten sind konfigurierbar (`config/scoring.php`), damit sie
ohne Code-Änderung kalibriert werden können.
---
## Entwicklungsschritte
### Phase 0 — Prozess-Start im UI vereinheitlichen — ✅ erledigt (11.06.2026)
**Ziel:** Das bestehende Einreichungs-Modal erscheint überall dort, wo eine PM
eingereicht wird — auch beim Bearbeiten (Button „Speichern & zur Prüfung") und
beim Erstellen (Button „Zur Prüfung senden"). Reiner UI-Schritt, kein Backend.
**Umsetzung:** Das Modal `confirm-submit-review` (rechtliche Hinweise, Quota,
Bestätigungs-Checkboxen) wird in Customer-Show, -Create **und** -Edit über
`flux:modal.trigger` geöffnet; bestätigt ruft es wie geplant
`submitForReview` bzw. `saveAndSubmit`/`save('review')`.
**Umfang:**
- Modal `confirm-submit-review` aus `show.blade.php` in eine wiederverwendbare
Blade-/Volt-Komponente extrahieren (z. B.
`resources/views/livewire/components/press-release-submit-modal.blade.php`).
- In `edit.blade.php` den `wire:confirm`-Button durch einen
`flux:modal.trigger` ersetzen; bei Bestätigung wird wie bisher
`saveAndSubmit` ausgeführt (erst speichern, dann einreichen).
- In `create.blade.php` denselben Modal-Trigger vor `save('review')` schalten.
- Texte/Checkboxen identisch zur Detailansicht halten (rechtliche Hinweise,
Quota, Bestätigungen).
**Betroffene Dateien:** `resources/views/livewire/customer/press-releases/{show,edit,create}.blade.php`,
neue Komponente unter `resources/views/livewire/components/`.
**Done:** In allen drei Ansichten (Customer: show/edit/create) öffnet derselbe
Bestätigungsdialog; Tests für Edit/Create-Submit grün.
**Admin-Editor (`/admin/press-releases/`) — bewusst ausgenommen:** Der
Admin-Editor behält sein bisheriges Verhalten (`wire:confirm`). Begründung: Wenn
eine PM beim Admin landet, hat die vorgelagerte User-Prüfung (Einreichungs-Modal
im Customer-Flow) bereits stattgefunden. Der Admin braucht hier keinen erneuten
Bestätigungsdialog. Stattdessen erhält der Admin-Editor in einer späteren Phase
einen zusätzlichen **„Prüfung"-Button** (siehe Phase 4: On-Demand-KI-Prüfung).
**Tests:** Volt-Tests, die das Öffnen des Modals und den Submit-Pfad
(`saveAndSubmit` / `save('review')`) abdecken.
### Phase 1 — Einreichungs-Funnel & API-Absicherung — ✅ erledigt (11.06.2026)
**Ziel:** Beide Einreichungsstellen laufen durch einen Pfad; die API-Lücke (L1)
wird geschlossen. Noch ohne KI — nur Vereinheitlichung.
**Umsetzung:**
- `PressReleaseService::submitForReview()` ist der alleinige Einreichungs-Einstieg
(Web-Formular **und** API rufen dieselbe Methode). Auf eine separate
`SubmissionService`-Fassade wurde bewusst verzichtet — `submitForReview` ist
bereits die stabile Schnittstelle, in die Phase 3 die KI-Klassifikation
einhängt.
- API: `status` aus den Validierungsregeln von `StorePressReleaseRequest` und
`UpdatePressReleaseRequest` entfernt (inkl. ungenutzter Imports). `store()`
erzeugt jetzt **immer** `PressReleaseStatus::Draft`; ein übergebenes `status`
wird ignoriert. `update()` kann den Status nicht mehr setzen.
- Neue explizite Route `POST /api/v1/press-releases/{pressRelease}/submit`
(`press-releases.submit`) → `PressReleaseController::submit()`. Diese prüft
`press-releases:write`, Ownership und erlaubt nur `draft`/`rejected`
(sonst 409); ruft `submitForReview()`; eine `BlacklistViolationException`
wird als **422** mit Begründung zurückgegeben. Damit greifen Blacklist-,
Quota- und Status-Log-Behandlung auch für API-Einreichungen.
- `published` ist über die API weiterhin nie erreichbar (nur Admin-Aktion/Cron).
**Betroffene Dateien:** `app/Http/Controllers/Api/V1/PressReleaseController.php`,
`app/Http/Requests/Api/V1/{Store,Update}PressReleaseRequest.php`,
`routes/api.php`. `PressReleaseService` blieb unverändert (Schnittstelle
ausreichend).
**Done:** API kann keine PM mehr ungeprüft in `review` heben; eine PM-Einreichung
verhält sich über API und Formular identisch.
**Tests:** `tests/Feature/Api/V1/PressReleaseSubmitApiTest.php` (Create erzeugt
immer Draft & ignoriert `status`; Submit-Route hebt nach `review`, zählt Quota,
schreibt Log; Blacklist → 422 + `rejected`; fehlende Schreibrechte → 403;
bereits in `review` → 409; fremde PM → 403). Alle grün.
### Phase 2 — Datenmodell & Audit — ✅ erledigt (11.06.2026)
**Ziel:** Persistenz für Klassifikation und vollständiges KI-Audit. Noch ohne
Verhaltensänderung (alle Felder nullable).
**Umsetzung:**
- Migration `add_classification_to_press_releases`: Spalten `classification`
(string(16), nullable, nach `status`) und `classified_at` (timestamp,
nullable) plus Index auf `classification`. `content_score`/`content_tier`
bewusst **erst in Phase 5** (siehe Datenmodell-Anhang).
- Migration `create_ki_audits_table`: `press_release_id` (FK, cascade),
`type`, `provider` (nullable), `model` (nullable), `result` (nullable),
`reason` (text, nullable), `raw_response` (json, nullable),
`created_at` (useCurrent), Index `(press_release_id, type)`. Kein
`updated_at` (append-only Log).
- Model `App\Models\KiAudit` (`$timestamps = false`, Cast `raw_response` →
array, Konstanten `TYPE_CLASSIFICATION`/`TYPE_CONTENT_SCORE`, Relation
`pressRelease()`), Relation `PressRelease::kiAudits()` (neueste zuerst).
- Enum `App\Enums\PressReleaseClassification` (Green/Yellow/Red + `label()`),
in `PressRelease::casts()` für `classification` registriert.
- `config/scoring.php`: Anbieter/Modell-Auswahl (`CLASSIFICATION_PROVIDER`,
Default `deterministic`, `CLASSIFICATION_MODEL`), Timeout, Grün-Verzögerung
(Minuten), Gelb→manuelle-Queue-Flag sowie Content-Score-Stufen-Schwellen
(Phase 5).
- `KiAuditFactory` mit States `classification()` / `contentScore()`.
**Betroffene Dateien:** zwei neue Migrationen unter `database/migrations/`,
`app/Models/PressRelease.php`, `app/Models/KiAudit.php`,
`app/Enums/PressReleaseClassification.php`, `config/scoring.php`,
`database/factories/KiAuditFactory.php`.
**Done:** Migrationen laufen; Modelle/Casts/Relation vorhanden; keine bestehende
Funktionalität verändert (alle Felder nullable).
**Tests:** `tests/Feature/PressReleaseClassificationModelTest.php` (Enum-/
Datetime-Cast, Default null, `kiAudits()`-Reihenfolge, `raw_response`-Array-Cast
+ Relation, Cascade-Delete). Alle grün.
### Phase 3 — KI-Klassifikation (Red Flag) — ✅ erledigt (11.06.2026)
**Ziel:** Echte inhaltliche Prüfung jeder Einreichung; Ergebnis asynchron als
Klassifikation gespeichert und auditiert.
**Entscheidungen (11.06.2026):** Erster aktiver Anbieter ist **OpenAI** (Key/
Budget vorhanden); Anthropic/Gemini folgen über dieselbe Treiber-Schnittstelle.
Klassifikation läuft **asynchron über die Queue** (synchron wäre später nicht
handelbar). Zum Testen ohne Dauer-Worker gibt es einen Drain-Befehl.
**Umsetzung:**
- **Provider-agnostische Treiber-Architektur** unter
`app/Services/PressRelease/Classification/`:
- Interface `Contracts\ClassificationDriver::classify(PressRelease): ClassificationResult`.
- `ClassificationResult` (Value Object: Enum-Klassifikation, `reasons[]`,
`provider`, `model`, `rawResponse`, `reasonText()`).
- `Drivers\OpenAiClassificationDriver` — OpenAI Chat-Completions via
`Http`-Client, liest `config/services.openai` (Key/URL/Modell/Timeout),
erzwingt `response_format: json_object` und parst
`{classification, reasons[]}`. Wirft bei fehlendem Key / HTTP-Fehler /
ungültigem JSON.
- `Drivers\DeterministicClassificationDriver` — Blacklist → Rot/Grün
(nie Gelb), als Fallback ohne externe API.
- `ClassificationManager` (Laravel-Manager) löst den Treiber aus
`config('scoring.classification.provider')` auf
(`createOpenaiDriver`/`createDeterministicDriver`).
- **Asynchroner Job** `app/Jobs/ClassifyPressRelease` (Queue `classification`,
`tries=3`): klassifiziert über den aktiven Treiber, bei Ausfall **Fallback**
auf den deterministischen Treiber (mit `Log::warning`), schreibt
`press_releases.classification`/`classified_at` und einen `ki_audits`-Eintrag
(inkl. `provider`/`model`/`reason`/`raw_response`).
- **Einbindung in den Funnel:** `PressReleaseService::submitForReview()` stößt
nach dem synchronen Blacklist-Hard-Filter und dem Statuswechsel den Job an
(`ClassifyPressRelease::dispatch(...)->onQueue('classification')`). Greift für
Formular **und** API (gemeinsamer Einstieg aus Phase 1).
- **Drain-Befehl** `php artisan classification:work` (Option `--once`): arbeitet
die Queue einmalig ab und beendet sich (`queue:work --stop-when-empty`) — zum
Testen ohne permanenten Worker.
- **Konfig:** `config/scoring.php` Default-Provider auf `openai` gesetzt
(`CLASSIFICATION_PROVIDER`); Modell leer ⇒ `config('services.openai.model')`.
- **Test-Isolation:** `phpunit.xml` erzwingt `CLASSIFICATION_PROVIDER=deterministic`
und leeren `OPENAI_API_KEY`, damit die Suite keine echten OpenAI-Calls macht;
der OpenAI-Pfad wird gezielt mit `Http::fake()` getestet.
**Noch offen (bewusst):** Re-Klassifikation bei jeder PM-Änderung (Update über
Formular/API) ist noch **nicht** verdrahtet — Phase 3 klassifiziert beim
Einreichen. Nachzuziehen, wenn das Status-Routing (Phase 4) steht. Anthropic-/
Gemini-Treiber + SDK folgen separat.
**Betroffene Dateien:** `app/Services/PressRelease/Classification/*`,
`app/Jobs/ClassifyPressRelease.php`,
`app/Console/Commands/RunClassificationQueue.php`,
`app/Services/PressRelease/PressReleaseService.php`, `config/scoring.php`,
`phpunit.xml`.
**Done:** Jede Einreichung (Formular + API) stößt asynchron eine Klassifikation
an, erzeugt einen `ki_audits`-Eintrag; bei KI-Ausfall greift der deterministische
Fallback nachvollziehbar. Status-Routing folgt in Phase 4.
**Tests:** `tests/Feature/PressReleaseClassificationJobTest.php` (OpenAI grün/gelb
mit `Http::fake`, Fallback bei HTTP-500, deterministisch Rot bei Blacklist,
Dispatch auf Queue `classification` via `Queue::fake`). Alle grün; volle Suite
416 grün (2 vorbestehende WIP-Failures unverändert).
### Phase 4 — Routing, Auto-Publish & Review-Queue — ✅ erledigt (11.06.2026)
**Ziel:** Die Klassifikation steuert den Status. Manuelle Prüfung nur noch bei
Gelb.
**Umsetzung:**
- **Routing im Job** über `PressReleaseService::routeByClassification()`
(vom `ClassifyPressRelease`-Job nach dem Klassifizieren aufgerufen):
- **Rot** → `reject(..., source: 'ki')`: `status=rejected`, KI-Begründung per
Mail an den Autor (`PressReleaseRejected`, wie bei Blacklist).
- **Gelb** → keine Aktion, bleibt `review` (manuelle Admin-Queue).
- **Grün** → `autoPublishGreen()`: ohne Termin sofort veröffentlichen,
optional mit Sicherheitsfenster `scoring.classification.green_delay_minutes`
(über `published_at`-Override); mit zukünftigem `scheduled_at` bleibt die PM
in `review` und der Scheduler publiziert zum Termin.
- Greift nur, solange die PM noch `review` ist (manuelle Admin-Eingriffe haben
Vorrang). `publish()` erhielt einen `?Carbon $publishedAtOverride`-Parameter,
`reject()` einen `string $source`-Parameter.
- **Scheduler** `PublishScheduledPressReleases`: Kandidaten-Query um
`where('classification', 'green')` erweitert — nur **grüne** fällige PMs
werden automatisch publiziert; gelbe warten immer auf den Admin. Geplante
Termine werden weiterhin respektiert.
- **Admin-Review-Queue:** Index- und Show-Ansicht zeigen ein KI-Klassifikations-
Badge (grün/gelb/rot); der Index hat einen **Klassifikations-Filter**
(`classificationFilter`, inkl. URL-Param, Active-Chip, Reset) — damit „nur
Gelb" filterbar. Die Show-Ansicht blendet im Review-Block den **KI-Hinweis**
(Begründung aus dem jüngsten `ki_audits`-Eintrag) ein.
**Test-Isolation (wichtig):** Da Tests mit `sync`-Queue den Job inline ausführen,
wurde der Klassifikations-Job in den „submit→review"-Tests via `Queue::fake()`
entkoppelt (Workflow, PublishModal, API-Submit). Die Scheduler-Tests setzen jetzt
`classification = green` für Publish-Kandidaten; neuer Test: fällige **gelbe** PM
bleibt `review`.
**Betroffene Dateien:** `app/Services/PressRelease/PressReleaseService.php`,
`app/Jobs/ClassifyPressRelease.php`,
`app/Console/Commands/PublishScheduledPressReleases.php`, Admin-Views
`resources/views/livewire/admin/press-releases/{index,show}.blade.php`.
**Done:** Grüne PMs gehen automatisch live (sofort/zum Termin), rote werden
abgelehnt + Autor benachrichtigt, nur gelbe landen in der manuellen Queue; Admin
sieht Klassifikation + KI-Begründung und kann nach Gelb filtern.
**Tests:** Routing in `PressReleaseClassificationJobTest` (Rot→rejected+Mail,
Grün-sofort→published+Mail, Grün-geplant→bleibt review, Gelb→bleibt review);
Scheduler in `PressReleaseSchedulingTest` (grün fällig→published, gelb
fällig→review); Admin-UI in `PressReleaseIndexPhase8bTest` (KI-Badge,
Klassifikations-Filter). Volle Suite 423 grün (2 vorbestehende WIP-Failures).
**Admin „Prüfung"-Button (On-Demand-KI-Prüfung)** — ✅ erledigt (11.06.2026):
- Im Admin-Editor gibt es oben den Button **„Prüfung"**, der ein Modal
`admin-ki-check` öffnet: auswählbare Klassifikation (Content-Score als
„in Vorbereitung" deaktiviert) und ein **Anbieter-Override** (Konfiguriert /
OpenAI / Deterministisch).
- `runKiCheck()` dispatcht `ClassifyPressRelease` auf der Queue `classification`
**mit `route: false`** und optionalem `providerOverride`. Das ist eine
nachgelagerte Re-Check-Prüfung: sie aktualisiert nur `classification` +
`ki_audits`, **ohne** den Status zu ändern (kein Auto-Publish/Reject) — die
Entscheidung bleibt beim Admin (Ergebnis sichtbar in der Detailansicht).
- Dafür erhielt `ClassifyPressRelease` die Parameter `bool $route = true` und
`?string $providerOverride = null`.
**Tests:** `tests/Feature/Admin/AdminKiCheckTest.php` (Button/Modal sichtbar;
Dispatch mit `route=false` + Provider-Override; Abbruch ohne Auswahl;
Re-Check-Job aktualisiert Bewertung, lässt Status unverändert).
**Re-Klassifikation bei Änderung** (Konzept §15.1) — ✅ erledigt (11.06.2026):
- Neue Service-Methode `reclassifyIfClassified()`: dispatcht – nur wenn die PM
bereits klassifiziert ist – `ClassifyPressRelease` mit `route: false`
(Re-Check ohne Statusänderung).
- Eingehängt überall dort, wo Inhalt geändert wird, und nur bei tatsächlicher
Änderung von Titel/Text (`wasChanged(['title', 'text'])`):
Customer-Editor `save()`, Admin-Editor `save()`, API `update()`.
Beim Einreichen übernimmt weiterhin `submitForReview` die (routende)
Klassifikation.
**Tests:** `tests/Feature/PressReleaseReclassifyTest.php` (Service dispatcht nur
bei vorhandener Klassifikation; API-Update klassifiziert neu bei Text-Änderung,
nicht bei reiner Keyword-Änderung).
**Noch offen / Folgearbeiten:**
- **Live-Aktualisierung der Ansicht** nach Abschluss des Hintergrund-Jobs
(Polling/Event) wäre ein optionales UX-Upgrade; aktuell erscheint das Ergebnis
nach Reload/Navigation.
- **Content-Score-Option** im Prüfungs-Modal — ✅ mit Phase 5 aktiviert (s. u.).
### Phase 5 — Content-Score & Stufen — ✅ erledigt (11.06.2026)
**Ziel:** Qualitätsbewertung 0–100 → Stufe Standard/Geprüft/Hochwertig
(Konzept-Update 2).
**Umsetzung:**
- **Datenmodell:** Migration `add_content_score_to_press_releases` —
`content_score` (tinyint, nullable), `content_tier` (string, nullable, Index),
`scored_at`. Enum `App\Enums\PressReleaseContentTier`
(Standard/Geprueft/Hochwertig) mit `fromScore()` (Schwellen aus
`config/scoring.php`), `label()` und `isPubliclyBadged()` (Standard wird laut
Update 2 nicht beworben). In `PressRelease` als Cast registriert.
- **Schwellen** (`config/scoring.php`): Geprüft ≥ 60, Hochwertig ≥ 80 (Update 2),
kalibrierbar; plus Anbieter/Modell/Timeout für den Score.
- **Treiber-Architektur** unter `app/Services/PressRelease/ContentScore/`
analog zur Klassifikation: `Contracts\ContentScoreDriver`, `ContentScoreResult`,
`Drivers\OpenAiContentScoreDriver` (gewichtete Faktoren §15.2 als JSON
`{score, breakdown}`), `Drivers\DeterministicContentScoreDriver`
(regelbasierte Heuristik: Länge, Bild, Quelle, Headline, Vollständigkeit),
`ContentScoreManager`.
- **Job** `app/Jobs/ScorePressRelease` (Queue `classification`, Fallback auf
deterministisch): schreibt `content_score` + abgeleitete `content_tier` +
`scored_at` und `ki_audits` (type=content_score). Optionaler
`providerOverride`.
- **Berechnung** bei Einreichung (`submitForReview` dispatcht Klassifikation
**und** Score) und bei Inhaltsänderung (`rescoreIfScored()` in Customer-/
Admin-Editor und API-`update()`, analog zur Re-Klassifikation).
- **Anzeige:**
- Customer-Editor: Score-Panel (Punktzahl, Stufe, „noch X Punkte bis zur
nächsten Stufe") — der produktive Editor-Score laut Update 2.
- Admin-Index & -Show: Stufen-/Score-Badge (intern inkl. Punktzahl).
- Customer-Detailansicht: öffentliches Stufen-Badge (✓ Geprüft / ★ Hochwertig;
Standard ohne Badge).
- **Admin-Prüfungs-Modal:** Content-Score-Option aktiviert; `runKiCheck()`
dispatcht zusätzlich `ScorePressRelease`.
**Done:** Score wird bei Einreichung/Änderung berechnet, Stufe abgeleitet,
auditiert und überall sichtbar.
**Tests:** `tests/Feature/PressReleaseContentScoreTest.php` (Tier-Mapping,
öffentliche Badges, OpenAI-Score→Tier+Audit, Fallback, Dispatch bei Submit,
Re-Score nur wenn bereits bewertet); Editor-Panel in
`CustomerPressReleaseEditPhase7Test`; Stufen-Badge in `PressReleaseIndexPhase8bTest`;
Content-Score-Dispatch in `AdminKiCheckTest`. Volle Suite 440 grün.
**Noch offen / Folgearbeiten:**
- **Public Web-Frontend** (presseecho/businessportal24): Stufen-Badges in den
öffentlichen Listen/Detailseiten gemäß Update 2 ergänzen (bisher nur im
Portal/Backend und der Customer-Ansicht).
- **Score-History & Breakdown-Ansicht** (Publisher-Dashboard) und Boost-
Eligibilität (Abschnitt 16) sind eigene spätere Ausbaustufen.
### Phase 6 — Trust-Score (später)
Account-/Firmen-Ebene (Konzept §15.3): lockert die KI-Freigabe-Schwelle für
zuverlässige Publisher. Eigene spätere Ausbaustufe; hier nur als Ausblick
vermerkt.
---
## Datenmodell-Anhang (Zielzustand)
```
press_releases (Ergänzungen)
+ classification enum(green,yellow,red) NULL
+ classified_at timestamp NULL
+ content_score tinyint NULL (Phase 5)
+ content_tier enum(standard,gepruft,hochwertig) NULL (Phase 5)
ki_audits (neu)
- id
- press_release_id FK
- type enum(classification,content_score)
- provider string (z. B. anthropic)
- model string (z. B. claude-opus-4-8)
- result string/json
- reason text NULL
- raw_response json/longtext
- created_at timestamp
```
## Offene Entscheidungen
- **Anbieter & Modell** — ✅ entschieden (11.06.2026): Erster aktiver Anbieter
ist **OpenAI** (`CLASSIFICATION_PROVIDER=openai`, Modell aus
`config/services.openai`). Architektur provider-agnostisch; Anthropic/Gemini
folgen. Offen bleibt, ob später mehrere Anbieter parallel (Primär + Fallback
jenseits des deterministischen) laufen sollen.
- **Synchron vs. Queue** — ✅ entschieden (11.06.2026): **Queue** (asynchron,
Queue-Name `classification`). Drain zum Testen: `php artisan classification:work`.
- **Dependency**: OpenAI-Treiber nutzt den nativen `Http`-Client (kein neues
Composer-Paket). Anthropic PHP-SDK (`anthropic-ai/sdk`) ist **freigegeben**;
für Gemini je ein offizielles/etabliertes SDK oder HTTP-Client bei Umsetzung
des Treibers.
- **Grün-Verzögerung**: 0 Min. (sofort) oder 5–10 Min. (Konzept-Option) als
Sicherheitsfenster — konfigurierbar über
`scoring.classification.green_delay_minutes`, Default noch festzulegen.
- **Gelb-Verhalten**: ausschließlich manuelle Queue, oder zusätzlich
automatische Benachrichtigung des Autors.
- **DSGVO**: Aufbewahrung/Anonymisierung der `raw_response` in `ki_audits`.
## Nächste Schritte
Phasen 0–5 sind umgesetzt (Suite grün). Es folgen:
1. **Launch-Block aus dem Decision-Update** (siehe Abgleich-Box oben):
Submit-Gate hinter Buchung, Slot-Verbrauch bei Veröffentlichung,
Tarif-/Zahlungs-Modul.
2. **Betrieb**: Queue-Worker für `classification` im Produktions-Setup
(Test-Drain: `php artisan classification:work`).
3. **Folgearbeiten**: Live-Aktualisierung des KI-Ergebnisses in der UI,
Stufen-Badges im öffentlichen Web-Frontend, Anthropic-/Gemini-Treiber.
4. **Phase 6**: Trust-Score (eigene Ausbaustufe).

View file

@ -1,207 +0,0 @@
Ich würde das Formular inhaltlich klarer machen und stärker gegen typische Bildrechts-Probleme absichern. Der aktuelle Ansatz ist gut, aber bei Presseportalen würde ich etwas präziser zwischen **Urheberrecht**, **Nutzungsrecht**, **Personenrechten** und **Quelle/Nachweis** unterscheiden.
## **1. Lizenztyp-Auswahl überarbeiten**
Aktuell hast du:
- Eigene Aufnahme
- CC-Lizenz
- Kommerzielle Lizenz erworben
- Einwilligung des Urhebers
- Sonstiges
Ich würde daraus eher machen:
### **Empfohlene Lizenztypen**
|**Option**|**Wann verwenden?**|
|---|---|
|**Eigene Aufnahme**|Der Uploadende hat das Bild selbst erstellt|
|**Vom Urheber / Fotografen freigegeben**|Direkte schriftliche Erlaubnis liegt vor|
|**Agentur-/Stockbild-Lizenz**|Bild wurde z. B. über Adobe Stock, Shutterstock, Getty etc. lizenziert|
|**Creative-Commons-Lizenz**|Bild steht unter CC BY, CC BY-SA, CC0 etc.|
|**Presse-/PR-Bild mit Nutzungsfreigabe**|Bild wurde z. B. von Unternehmen, Veranstaltern, Agenturen oder Pressestellen bereitgestellt|
|**Gemeinfrei / Public Domain / CC0**|Keine oder sehr weitgehende Nutzungsbeschränkungen|
|**Sonstige Lizenz / Sondervereinbarung**|Freitext erforderlich|
„Einwilligung des Urhebers“ würde ich nicht als eigenen Lizenztyp stehen lassen, sondern eher als **„Vom Urheber/Fotografen freigegeben“** formulieren. Das ist verständlicher.
## **2. Bei Creative Commons zusätzliche Felder anzeigen**
Wenn jemand **CC-Lizenz** auswählt, sollte nicht nur „CC-Lizenz“ gespeichert werden. Du brauchst genauer:
- CC0
- CC BY
- CC BY-SA
- CC BY-ND
- CC BY-NC
- CC BY-NC-SA
- CC BY-NC-ND
Wichtig: **NC** bedeutet „nicht-kommerziell“ und kann für ein Presseportal problematisch sein, besonders wenn die Seite werbefinanziert ist oder kommerziell betrieben wird. **ND** erlaubt keine Bearbeitung, also eventuell auch keinen Beschnitt als Titelbild.
Daher würde ich bei CC-Lizenzen automatisch Hinweise anzeigen, zum Beispiel:
Diese Lizenz kann Einschränkungen enthalten. Bitte prüfen, ob kommerzielle Nutzung, Bearbeitung und Veröffentlichung als Titelbild erlaubt sind.
## **3. „Urheber / Fotograf“ verpflichtender machen**
Das Feld **Urheber / Fotograf** sollte in den meisten Fällen Pflicht sein, außer vielleicht bei eigener Aufnahme, wenn der Name des Uploadenden automatisch hinterlegt wird.
Besser wäre:
**Urheber / Fotograf / Rechteinhaber**
Denn nicht immer ist der Fotograf auch der Rechteinhaber. Bei Agenturen oder Unternehmen können die Rechte woanders liegen.
## **4. „Copyright / Quelle“ klarer benennen**
Das Feld „Copyright / Quelle“ ist etwas gemischt. Ich würde es aufteilen oder klarer formulieren:
- **Copyright-Hinweis / Bildnachweis**
- **Quelle des Bildes**
- **Lizenz- oder Nachweis-URL**
Beispiel:
**Bildnachweis, wie er angezeigt werden soll**
`Foto: Max Mustermann / Beispiel GmbH`
**Quelle / Fundstelle**
`https://...`
**Lizenz-URL / Nachweis-URL**
`https://creativecommons.org/licenses/by/4.0/` oder Link zur Stocklizenz / Presseseite
So vermeidest du, dass jemand nur „Internet“ oder „Google“ einträgt.
## **5. Datei-Upload um Pflicht-Hinweise ergänzen**
Beim Upload würde ich neben Dateityp und Größe noch einen kurzen Warnhinweis ergänzen:
Bitte laden Sie nur Bilder hoch, für die Sie die erforderlichen Nutzungsrechte besitzen. Bilder aus Google, Social Media, Messenger-Gruppen oder fremden Websites dürfen nicht ohne ausdrückliche Erlaubnis verwendet werden.
Das ist sehr hilfreich, weil genau dort viele Fehler passieren.
## **6. Personenrechte besser abfragen**
Dein Feld „Einwilligung abgebildeter Personen liegt vor“ ist gut, aber ich würde es differenzierter machen.
Statt nur einem Schalter:
**Sind Personen auf dem Bild erkennbar?**
- Nein
- Ja, und die Einwilligung liegt vor
- Ja, aber es handelt sich um eine öffentliche Veranstaltung / redaktionelle Berichterstattung
- Unsicher
Wenn „Ja“ oder „Unsicher“ gewählt wird, kannst du einen Hinweis anzeigen:
Bei erkennbaren Personen können zusätzlich Persönlichkeits- oder Datenschutzrechte betroffen sein. Bitte stellen Sie sicher, dass eine Veröffentlichung zulässig ist.
Der aktuelle Toggle „Einwilligung liegt vor“ ist gut, aber er setzt voraus, dass der Nutzer selbst erkennt, ob Personenrechte relevant sind.
## **7. Property Rights / Marken / Kunstwerke ergänzen**
Neben Personen sind auch diese Fälle kritisch:
- Logos und Marken
- Kunstwerke
- private Innenräume
- Gebäude, Architektur, Museen
- Fahrzeuge mit Kennzeichen
- Veranstaltungsplakate oder Screenshots
Ich würde daher ergänzen:
**Enthält das Bild erkennbare Marken, Kunstwerke, geschützte Werke oder private Orte?**
- Nein
- Ja, Rechte/Nutzung sind geklärt
- Unsicher
Das schützt besonders bei PR-, Event- und Pressebildern.
## **8. Rechtebestätigung präziser formulieren**
Aktuell:
Ich bestätige, dass ich zur Nutzung dieses Bildes berechtigt bin und alle Rechte geklärt sind.
Ich würde es ausführlicher und rechtlich klarer machen:
Ich bestätige, dass ich über die erforderlichen Rechte zur Veröffentlichung dieses Bildes verfüge. Dies umfasst insbesondere Urheberrechte, Nutzungsrechte, Persönlichkeitsrechte abgebildeter Personen sowie gegebenenfalls Marken-, Eigentums- oder sonstige Rechte Dritter. Ich bin berechtigt, das Bild auf diesem Presseportal veröffentlichen zu lassen.
Optional zusätzlich:
Mir ist bewusst, dass ich für fehlerhafte oder unvollständige Angaben verantwortlich bin.
Je nach Portal kannst du das etwas freundlicher formulieren, aber inhaltlich sollte es klar sein.
## **9. Lizenznachweis als Datei-Upload ermöglichen**
Sehr sinnvoll wäre ein optionaler Upload:
**Nachweis / Freigabe hochladen**
Zum Beispiel:
- Lizenzbestätigung
- E-Mail-Freigabe
- Model Release
- Vertrag
- Screenshot der Lizenzseite
- Pressefreigabe
Das muss nicht öffentlich sichtbar sein, aber intern gespeichert werden.
## **10. Sichtbarer Bildnachweis im Frontend**
Ich würde im Formular klar anzeigen:
**Dieser Bildnachweis wird öffentlich angezeigt:**
`Foto: Max Mustermann / Beispiel GmbH`
Viele Nutzer verstehen besser, was sie eintragen müssen, wenn sie sehen, wo es später erscheint.
## **11. Meine empfohlene Struktur**
So würde ich das Formular aufbauen:
1. **Bild hochladen**
2. **Titel / Alt-Text**
3. **Bildnachweis, öffentlich sichtbar**
4. **Urheber / Fotograf / Rechteinhaber**
5. **Lizenztyp**
6. **Lizenzdetails je nach Auswahl**
7. **Quelle / Lizenz-URL**
8. **Personen auf dem Bild?**
9. **Marken, Kunstwerke oder private Orte sichtbar?**
10. **Nachweis/Freigabe hochladen**
11. **Rechte bestätigen**
Zusätzlich würde ich **Alt-Text** ergänzen. Nicht primär wegen Recht, sondern wegen Barrierefreiheit und SEO.
## **Konkrete Anpassung deiner Felder**
Aus deinem Screenshot würde ich ändern:
**„Copyright / Quelle“**→ **„Öffentlicher Bildnachweis“**
**„Urheber / Fotograf“**→ **„Urheber / Fotograf / Rechteinhaber“**
**„Lizenz-URL optional“**→ **„Quelle oder Lizenznachweis-URL“**
**„Einwilligung abgebildeter Personen liegt vor“**
→ ersetzen durch Auswahl:
**„Sind erkennbare Personen abgebildet?“**
**„Bildrechte bestätigt“**
→ Pflicht-Checkbox mit ausführlicher Bestätigung.
## **Besonders wichtig**
Ich würde **„Sonstiges“ nie ohne Pflicht-Freitext erlauben**. Wenn jemand „Sonstiges“ wählt, sollte zwingend erklärt werden müssen, warum die Nutzung erlaubt ist.
Außerdem würde ich bei riskanten Angaben wie **CC-NC**, **CC-ND**, **Unsicher**, **Sonstiges** oder fehlender Lizenz-URL entweder eine Warnung anzeigen oder eine manuelle Prüfung durch die Redaktion auslösen.

File diff suppressed because it is too large Load diff

View file

@ -1,277 +0,0 @@
# Umsetzung Pressemitteilung bearbeiten: Titelbild, Rechte, Veröffentlichung
Stand: 11.06.2026
Diese Notiz dokumentiert die zuletzt umgesetzten Anpassungen an der Bearbeitung von Pressemitteilungen im User Panel und an den parallel genutzten Admin-Formularen.
## Betroffener Bereich
- Customer Create/Edit: `resources/views/livewire/customer/press-releases/create.blade.php`
- Customer Edit: `resources/views/livewire/customer/press-releases/edit.blade.php`
- Admin Create/Edit: `resources/views/livewire/admin/press-releases/create.blade.php`, `resources/views/livewire/admin/press-releases/edit.blade.php`
- Show/Index (Customer + Admin) für die Termin-Anzeige: `resources/views/livewire/customer/press-releases/{show,index}.blade.php`, `resources/views/livewire/admin/press-releases/{show,index}.blade.php`
- Model (Zeitzonen-Konstante + Accessoren): `app/Models/PressRelease.php`
- Titelbild-Manager: `resources/views/livewire/components/press-release-images-manager.blade.php`
- Platzhalter-Auswahl: `resources/views/livewire/components/press-release-placeholder-picker.blade.php`
- Platzhalter-Dateien: `public/images/press-release-placeholders`
- Layout-CSS: `resources/css/shared/hub-components.css`
## Titelbild-Platzhalter
Die Platzhalter für Pressemitteilungs-Titelbilder wurden erweitert.
- Die Varianten werden über `App\Enums\PressReleasePlaceholder` verwaltet.
- Die SVG-Dateien liegen lokal unter `public/images/press-release-placeholders`.
- Der Picker lädt die lokalen Varianten in `resources/views/livewire/components/press-release-placeholder-picker.blade.php`.
- Das Modal wurde für mehr Varianten verbreitert und mit einer scrollbaren Grid-Darstellung versehen.
Ziel: Wenn noch kein eigenes Titelbild vorhanden ist, kann ein optisch passender Platzhalter gewählt werden.
## Titelbild-Upload
Der Upload wurde auf ein einzelnes Titelbild begrenzt.
- Es kann vorerst nur ein Titelbild pro Pressemitteilung hochgeladen werden.
- Wenn ein Titelbild vorhanden ist, wird die Platzhalter-Card ausgeblendet.
- Das Upload-Formular wird ebenfalls ausgeblendet, solange ein Titelbild existiert.
- Das vorhandene Titelbild wird in einer eigenen Bild-Card angezeigt.
- In der Bild-Card werden Titel, Größe und Bildnachweis/Copyright angezeigt.
- Das Titelbild kann gelöscht werden; danach erscheinen Platzhalter und Upload-Formular wieder.
Das Upload-Formular ist einklappbar:
- Im Ausgangszustand erscheint nur der Hinweis, dass ein Titelbild fehlt.
- Über „Eigenes Titelbild hochladen" wird das Formular geöffnet.
- Über „Abbrechen" wird das Formular wieder geschlossen und zurückgesetzt.
## Bildverarbeitung und Speicherverhalten
Die Bildverarbeitung wurde auf die Titelbild-Nutzung optimiert.
- Erlaubte Formate: JPG, PNG, WebP.
- Maximale Dateigröße: 16 MB.
- Nicht previewfähige Dateien wie TIFF lösen keine Livewire-Preview-Exception mehr aus; sie werden über die Validierung abgefangen.
- Für Pressemitteilungsbilder wird eine Cover-Variante erzeugt: 1280 x 580 px.
- Der kanonische Bildpfad zeigt auf die Cover-Variante.
- Das Original wird nach der Variantenerzeugung gelöscht, um Speicherplatz zu sparen.
- Breite und Höhe in der Oberfläche beziehen sich auf die gespeicherte Cover-Version, nicht auf das Original.
Relevante Datei: `app/Services/Image/ImageService.php`.
## Lizenz- und Rechteformular
Das Formular wurde an die Vorgaben aus `docs/user-admin/Lizenztyp Bildupload.md` angepasst.
Erfasste Felder:
- Titel / Alt-Text über das bestehende `title`-Feld.
- Öffentlicher Bildnachweis über das bestehende `copyright`-Feld.
- Urheber / Fotograf / Rechteinhaber über `author`.
- Lizenztyp über `license_type`.
- Lizenzdetails über `license_detail`.
- Lizenz-URL über `license_url`.
- Quelle / Fundstelle über `source_url`.
- Personenrechte über `people_rights_status`.
- Marken, Kunstwerke, geschützte Werke oder private Orte über `property_rights_status`.
- Interne Notizen über `rights_notes`.
- Rechtebestätigung über `rights_confirmed_at`.
Lizenztypen:
- Eigene Aufnahme
- Creative-Commons-Lizenz
- Agentur-/Stockbild-Lizenz
- Vom Urheber / Fotografen freigegeben
- Presse-/PR-Bild mit Nutzungsfreigabe
- Gemeinfrei / Public Domain / CC0
- Sonstige Lizenz / Sondervereinbarung
Wichtige Regeln:
- „Bitte wählen" ist der Ausgangszustand.
- „Unsicher" wurde aus den Auswahlmöglichkeiten entfernt.
- Am Ende muss der Uploadende die Verantwortung für die Rechte bestätigen.
- Creative-Commons-Lizenzen erfassen zusätzlich die konkrete CC-Variante.
- CC-, Stock-/Agentur- und Presse-/PR-Lizenzen verlangen eine Lizenz- oder Nachweis-URL.
- „Sonstige Lizenz / Sondervereinbarung" verlangt einen Pflicht-Freitext.
- Risikohinweise werden bei eingeschränkten oder unklaren Lizenzfällen angezeigt.
Relevante Dateien:
- `app/Enums/ImageLicenseType.php`
- `app/Models/PressReleaseImage.php`
- `database/migrations/2026_06_10_154249_add_rights_detail_fields_to_press_release_images_table.php`
- `resources/views/livewire/components/press-release-images-manager.blade.php`
## Veröffentlichung
Die Veröffentlichungs-Box wurde vereinfacht.
Sichtbar sind nur noch:
- Sofort nach Freigabe
- Geplanter Termin
Embargo / Sperrfrist wurde in den Formularen aus der Oberfläche entfernt, weil es aktuell noch keine sinnvolle Anwendung im User-Flow gibt.
Technisches Verhalten:
- `scheduled_at` bleibt erhalten und wird weiterhin gespeichert.
- `embargo_at` wird in den betroffenen Formularen nicht mehr gesetzt und beim Speichern auf `null` geführt.
- Für den geplanten Termin wird `flux:date-picker` für das Datum verwendet.
- Für die Uhrzeit wird `flux:time-picker` verwendet.
- Intern werden Datum und Uhrzeit wieder zu `scheduledAt` kombiniert.
- Der geplante Termin muss mindestens 5 Minuten in der Zukunft liegen.
- Bei zu frühem Termin wird direkt ein Fehler gesetzt; beim Speichern greift die Validierung ebenfalls.
Betroffene Properties:
- `scheduledDate`
- `scheduledTime`
- `scheduledAt`
## Zeitzonen-Handling für geplante Veröffentlichung
Stand: 11.06.2026 (nachgezogen)
Die Anwendung läuft serverseitig in UTC (`config/app.php` → `timezone = 'UTC'`).
Geplante Termine werden aber von Redaktion und Kunden in **deutscher Zeit**
gedacht. Vorher wurde die im Formular eingegebene Uhrzeit naiv als UTC
interpretiert, wodurch die Veröffentlichung um den Berlin-Offset (im Sommer
+2 h) verschoben stattfand. Das ist behoben.
Grundprinzip:
- Eingabe und Anzeige erfolgen in **Europe/Berlin**.
- Gespeichert wird weiterhin **UTC**.
- Wichtig: Laravel konvertiert beim Speichern **nicht** automatisch nach UTC.
Deshalb wird der eingegebene Wert beim Parsen explizit als Berlin
interpretiert und mit `->utc()` umgewandelt; beim Laden wird umgekehrt von
UTC nach Berlin gewandelt.
Zentrale Stelle:
- `App\Models\PressRelease::DISPLAY_TIMEZONE` (`'Europe/Berlin'`) ist die
Single Source of Truth.
- `PressRelease::scheduledAtLocal()` und `PressRelease::embargoAtLocal()`
liefern die Termine in der Anzeige-Zeitzone für alle Views.
Verhalten in den Formularen (Customer **und** Admin, Create **und** Edit):
- Helper `scheduledAtUtc()`: parst die naiven Eingabefelder als Berlin und
gibt den UTC-Zeitpunkt für die Speicherung zurück.
- Die „mind. 5 Minuten in der Zukunft"-Prüfung läuft jetzt über eine
zeitzonenbewusste Closure-Regel statt über die naive `after:`-Regel.
- Beim Bearbeiten (`mount`) wird `scheduled_at` von UTC nach Berlin gewandelt,
bevor Datum/Uhrzeit in die Eingabefelder gefüllt werden.
Anzeige (lokalisiert auf Berlin):
- Customer-Show/-Index und Admin-Show/-Index: geplanter Termin und Embargo
werden über `scheduledAtLocal()` / `embargoAtLocal()` ausgegeben.
Bewusst (noch) nicht umgestellt:
- `published_at`, `created_at` und die Status-Log-Zeitstempel werden weiterhin
in UTC angezeigt. Eine vollständige Anzeige-Lokalisierung dieser Felder ist
als Folgeschritt vorgesehen.
## Aufräumung: Scheduling-Logik und Queries
Im Zuge der Zeitzonen-Umstellung wurden zwei Altlasten in allen vier
PM-Formularen bereinigt:
- **Doppelte Termin-Synchronisierung entfernt:** Die Termin-Logik lief vorher
sowohl im generischen `updated()`-Hook als auch in den spezifischen
`updated{PublishMode,ScheduledDate,ScheduledTime}`-Hooks – also doppelt.
`updated()` enthält jetzt nur noch die generische Re-Validierung bereits
fehlerhafter Felder; die Synchronisierung liegt ausschließlich in den
spezifischen Hooks.
- **Redundante Queries reduziert:** Im Customer-Edit wird die geladene
Pressemitteilung pro Request memoisiert (`mount()`, `with()` und `save()`
greifen sonst jeweils mit einer eigenen Query auf dieselbe PM zu).
## Responsive Layout
Das Layout der Pressemitteilungsformulare wurde entkoppelt von der globalen Sidebar-Logik.
Die globale Flux-Sidebar bleibt im stabilen Standardzustand:
- `flux:sidebar sticky stashable`
- Header- und Toggle-Sichtbarkeit weiter über `lg:hidden`
Das eigentliche Formularlayout wird über eigene Klassen gesteuert:
- `.pr-editor-layout`
- `.pr-editor-side`
Regel in `resources/css/shared/hub-components.css`:
```css
@media (min-width: 1180px) {
.pr-editor-layout {
grid-template-columns: minmax(0, 1fr) 360px;
}
.pr-editor-side {
position: sticky;
top: 1rem;
}
}
```
Verhalten:
- Unter 1180 px steht die rechte Formularbox unterhalb des Hauptinhalts.
- Ab 1180 px steht die rechte Formularbox wieder rechts.
- Die rechte Box wird ab 1180 px sticky.
- Die globale linke Navigation bleibt davon unberührt.
## Button-Varianten
Sekundäre `variant="ghost"`-Buttons in Blade-Views wurden breit auf `variant="filled"` umgestellt, weil die Ghost-Buttons optisch zu wenig als Buttons erkennbar waren.
Umfang:
- Alle Blade-Views unter `resources/views` wurden auf verbleibende `variant="ghost"` geprüft.
- Markdown-Dokumentation wurde dabei nicht als UI geändert.
## Tests und Verifikation
Ergänzte bzw. angepasste Tests:
- `tests/Feature/PressReleasePlaceholderTest.php`
- `tests/Feature/PressReleaseImageLicenseTest.php`
- `tests/Feature/CustomerPressReleaseSchedulingFormTest.php`
- `tests/Feature/Admin/AdminPressReleaseSchedulingTest.php`
Für die Zeitzonen-Umstellung zusätzlich angepasst (Assertions auf
Berlin-Werte umgestellt):
- `tests/Feature/PressReleaseShowPhase8aTest.php`
- `tests/Feature/Admin/AdminPressReleaseShowTest.php`
Zuletzt erfolgreich ausgeführte Checks:
- `php artisan test --compact tests/Feature/CustomerPressReleaseSchedulingFormTest.php tests/Feature/Admin/AdminPressReleaseSchedulingTest.php tests/Feature/Admin/AdminPressReleaseShowTest.php tests/Feature/PressReleaseShowPhase8aTest.php tests/Feature/PressReleaseIndexPhase8bTest.php`
- `vendor/bin/pint --dirty --format agent`
- Volle Suite: 400 passed (die zwei roten Tests `PressReleaseImageApiTest` und
`CustomerPressReleaseCreatePhase7Test` sind vorbestehende, unabhängige WIP-
Failures – per Stash-Test verifiziert, dass sie auch ohne diese Änderungen
scheitern).
Vorher zusätzlich grün gelaufen:
- `php artisan test --compact tests/Feature/PressReleasePlaceholderTest.php tests/Feature/PressReleaseImageLicenseTest.php tests/Feature/CustomerPressReleaseEditPhase7Test.php`
- Phase-8-nahe Show-/Index-/Attachment-/Admin-Tests.
## Bewusst noch nicht umgesetzt
- Optionaler Upload von Lizenznachweisen oder Freigabe-Dokumenten.
- Reaktivierung des separaten Anhang-Managers.
- Manuelle redaktionelle Prüf-Workflows für riskante Lizenzfälle.
- Vollständige Medienbibliothek statt einzelnes Titelbild.
- Frontend-Ausgabe des Bildnachweises außerhalb der Bearbeitungsoberfläche, sofern noch nicht separat angebunden.
- Anzeige-Lokalisierung von `published_at`, `created_at` und Status-Log-Zeitstempeln (aktuell weiterhin UTC).
- Klärung/Anpassung der Auto-Publish-Policy: geplante PMs (Status `review` + fälliger `scheduled_at`) werden vom Scheduler-Command automatisch veröffentlicht – ohne separate redaktionelle Freigabe. Wird in einem eigenen Schritt geprüft.

View file

@ -1,34 +0,0 @@
1. Pressemitteilungen anlegen ohne Firma hier würde ich das Create Formular nicht aufrufen lassen, wenn keine Firma existiert, sondern es muss eine Meldung erscheinen, dass ohne eine Firma keine Pressemitteilung angelegt werden kann.. Hintergrund öffnet man. Eine neue Pressemitteilung kann man keine Firma angeben und kann auch nicht speichern man muss wieder zurückspringen eine Firma anlegen und dann kann man erst entsprechend ein Presse Beitrag erstellen.
Profil
1. https://pressekonto.test/admin/me/profile die Darstellung ist noch nicht wirklich sauber hier kann noch etwas mehr Struktur rein. Grundsätzlich würde ich ab sofort Rechnungsadresse als Pflicht machen, wenn ein Paket gebucht wird und diese Rechnungsdaten gleich mit übergeben. Profil Einstellungen. Hier gibt es auch schon so etwas wie Anrede Vorname Telefon Risse etc. dieses doppelt sich mit der Rechnungsadresse zumindest in Teilen, denn die Rechnungsadresse ist technisch eigentlich gar nicht richtig vollständig hier viel zum Beispiel Firmenname Vorname Nachname Anrede.
2. Validierung der Rechnungsadresse gebe ich etwas ein und drücke ich auf. Absenden bekomme ich die Fehlermeldung zweimal einmal direkt unter dem Feld und einmal unten.
3. Umsatzsteuer ID. Ich würde hier gerne schon mal eine Validierung der Umsatzsteuer ID haben, ob diese gültig ist oder macht das an dieser Stelle gar keinen Sinn, weil Strip das komplett übernimmt? Ich glaube es wär aber gut, wenn Firmen aus EU Land hier schon eine Steuernummer hinterlegen können und diese zumindest schon einmal während der Eingabe validiert wird, ob diese richtig ist. Diese kann dann ja Stripe übergeben werden. https://api.evatr.vies.bzst.de/app/v1/abfrage
4. Unten gibt es noch Einstellungen Statistiken anzeigen und Footer code deaktivieren haben die aktuell eine Funktion?
Firmen:
Wenn ich auf die Firmenübersicht gehe und eine Firma ein Logo hat, soll das auch in der Übersicht erscheinen und angezeigt werden. Jetzt ist da dann immer der Kürzel der Firma
Bei der Firma ist zusätzlich angegeben, letzte PM ihr fehlt Jahreszahl
Einheitlichkeit.
Statt Checkboxen, bitte die Switcher benutzen von Flux UI.
---
## Status (Claude, 12.06.2026)
| # | Punkt | Status |
|---|---|---|
| PM 1 | PM-Anlage ohne Firma | ✅ Das Formular erscheint nicht mehr; stattdessen Meldung „Ohne Firma kann keine Pressemitteilung angelegt werden" mit Button „Firma anlegen". |
| Profil 1 | Struktur + Rechnungsadresse vollständig | ✅ Seite neu gegliedert (Persönliche Daten / Konto & Sicherheit / Rechnungsadresse / Einstellungen). Rechnungsadresse hat jetzt Anrede, Vorname, Nachname, Firmenname (optional) + Anschrift, mit Unterabschnitten Empfänger/Anschrift/Steuern und Button „Persönliche Daten übernehmen" gegen die Doppel-Eingabe. Rechnungsadresse ist ab sofort **Pflicht für jede Buchung** (Checkout leitet sonst mit Hinweis aufs Profil); Adresse und USt-ID werden an Stripe übergeben. |
| Profil 2 | Doppelte Fehlermeldung | ✅ Statt einer generischen Sammelmeldung + Feldmeldung werden fehlende Pflichtfelder jetzt einzeln und genau einmal unter dem jeweiligen Feld gemeldet. |
| Profil 3 | USt-ID-Validierung | ✅ Zweistufig: Formatprüfung sofort bei der Eingabe (hartes Gate beim Speichern), Online-Bestätigung über die BZSt-eVatR-REST-API (`api.evatr.vies.bzst.de`) live während der Eingabe. **Wichtig:** eVatR kann nur ausländische EU-IDs bestätigen und braucht unsere eigene deutsche USt-ID → ENV `BILLING_OWN_VAT_ID` setzen. Deutsche IDs werden nur formatgeprüft. Antwort auf die Frage: Stripe validiert die im Checkout erfasste ID zusätzlich selbst — unsere Prüfung verbessert die Eingabe-UX und sichert den manuellen MAN-Rechnungskreis ab, ersetzt Stripe also nicht, ergänzt es. Die lokal gepflegte USt-ID wird beim Checkout als Stripe-Tax-ID übergeben. |
| Profil 4 | Schalter „Statistiken anzeigen" / „Footer-Code deaktivieren" | ⚠️ Befund: Beide Flags werden gespeichert (auch aus dem Legacy-Import), aber **noch nirgends ausgewertet** — die konsumierenden Features (öffentliche PM-Seiten mit Footer-Codes/Statistiken) kommen mit dem Web-Relaunch. Die Beschreibungen weisen jetzt darauf hin („Greift mit dem Relaunch der Portal-Seiten"). Entscheidung offen: behalten (empfohlen, Daten sind da) oder bis zum Relaunch ausblenden. |
| Firmen 1 | Logo in der Übersicht | ✅ Die Übersicht nutzt jetzt dieselbe Logo-Auflösung wie die Detailseite (inkl. migrierter Legacy-Pfade) — vorher fielen alle Legacy-Firmen auf die Initialen zurück. |
| Firmen 2 | „Letzte PM" ohne Jahr | ✅ Kartenansicht zeigt jetzt `d.m.Y`. |
| Einheitlichkeit | Checkboxen → Flux-Switches | ✅ Umgestellt (Boilerplate-Override in PM-Create/-Edit, Footer-Code in Firma-Anlage/-Detail). Ausnahme: die Mehrfachauswahl der API-Token-Berechtigungen bleibt eine Checkbox-Gruppe — dort ist die Checkbox die semantisch richtige Flux-Komponente. |

View file

@ -1,164 +0,0 @@
# Checkliste User Backend
Stand: 11.06.2026 (Phase 7 + Phase 8 + KI-Pruef-Pipeline abgeschlossen; naechster Block: Zahlung/Tarife + Veroeffentlichungs-Flow laut Decision-Update)
Diese Checkliste fasst den aktuellen Stand des User Backends zusammen und trennt erledigte Punkte von den naechsten sinnvollen Umsetzungsschritten.
Begleitende Dokumente:
- `docs/STATUS-ABGLEICH-USER-PANEL.md` — Konzept-vs-Code-Abgleich pro Page.
- `docs/Decision-Update Preisstruktur & Veröffentlichungs-Flow.md` — verbindliche Launch-Entscheidungen zu Tarifen, Kontingenten und Flow.
- `docs/user-admin/Entwicklungsplan KI-Pruefung und Veroeffentlichung.md` — KI-Klassifikation + Content-Score (Phasen 0–5 erledigt).
- `docs/user-admin/Umsetzung Pressemitteilung Bearbeitung Titelbild Veroeffentlichung.md` — Titelbild/Lizenzformular/Zeitzonen-Umbau (10./11.06.).
- `docs/PHASE-8-USER-PANEL-PLAN.md` — Phase-8-Plan (abgeschlossen).
- `dev/frontend/hub-flux/19-PHASE-7-PRESS-RELEASE-FORM.md` — Phase-7-Abschluss.
- `dev/frontend/hub-flux/PROGRESS.md` — Tagebuch der Hub-Migration.
## Erledigt
- [x] User Backend und Admin Backend konzeptionell getrennt, technisch weiter im gemeinsamen Backend mit rollenbasierter Sicht.
- [x] User-Backend-Navigation in die Bereiche „Mein Bereich", „Finanzen" und „Konto" gegliedert.
- [x] Firmen-Kontext-Switcher aus der Sidebar in eine Topbar rechts ueber dem Content verschoben.
- [x] Topbar technisch in `<flux:main>` integriert, damit sie nicht mit Sidebar oder Content kollidiert.
- [x] Topbar visuell vom Content abgesetzt.
- [x] Firmen-Kontext mit „Alle Firmen" und Einzelfirma als globale User-Backend-Auswahl umgesetzt.
- [x] Dashboard und Pressemitteilungs-Liste reagieren auf den aktiven Firmen-Kontext.
- [x] Firmen-Liste und Firmen-Detail im User Backend umgesetzt.
- [x] Terminologie in der Navigation von „Pressemappen" auf „Firmen" vereinheitlicht.
- [x] Legacy-URLs fuer `pressemappen` auf neue `firmen`-Routen weitergeleitet.
- [x] Firmen-Stammdaten im Firmen-Detail bearbeitbar gemacht, inklusive Logo.
- [x] Legacy-Company-Logos bevorzugt lokal aufgeloest, um 403-Fehler durch externe Alt-URLs zu vermeiden.
- [x] Firmen-Logos in Admin- und User-Ansichten auf sinnvolle Groessen begrenzt.
- [x] Kontaktverwaltung innerhalb einer Firma umgesetzt.
- [x] Rechte fuer Firmen- und Kontaktverwaltung getrennt: Owner/Verantwortliche duerfen bearbeiten, Mitglieder bleiben lesend.
- [x] Neue Pressemitteilungen uebernehmen aktive Firma als Vorauswahl.
- [x] Portal der Pressemitteilung wird aus der Firma abgeleitet statt manuell gewaehlt.
- [x] Pressemitteilungs-Detail zeigt zugeordnete Pressekontakte.
- [x] Pressemitteilungs-Detail zeigt Status- und Verlaufsdaten.
- [x] Profilseite auf persoenliches Profil reduziert; Firmendaten werden in der jeweiligen Firma gepflegt.
- [x] Rechnungsadresse als eigener sichtbarer Bereich im Profil aufgenommen.
- [x] Rechnungen in die Finanznavigation verschoben.
- [x] Rechnungsseite mit Hinweisblock zu Legacy-Rechnungen und Link zur Rechnungsadresse versehen.
- [x] „Buchungen & Add-ons" als vorbereiteter Bereich eingebunden.
- [x] API-Tokens unter „Konto" als „API & Integrationen" eingeordnet.
- [x] Dashboard zeigt erste Datenqualitaets-Hinweise aus bestehenden Tabellen.
- [x] Phase-1-Dokumentation in `Admin-User.md` aktualisiert.
- [x] Regressionstests fuer Firmen-Kontext, Navigation, Profil, Firmen und Sicherheitsgrenzen ergaenzt.
## Naechste sinnvolle Schritte
- [x] Topbar fachlich abrunden: links optional Seitentitel/Kontext aufnehmen, rechts Firmen-Switcher und spaeter weitere kompakte Tools.
- [x] Mobile Darstellung der Topbar pruefen und bei Bedarf vereinfachen, damit der Switcher auf kleinen Displays nicht zu breit wird.
- [x] Firmen-Switcher um direkten Link „Firma oeffnen" oder „Firma verwalten" fuer die aktive Firma erweitern.
- [x] Leere und fehlerhafte Zustaende in Firmen, Kontakte, Pressemitteilungen und Rechnungen visuell vereinheitlichen.
- [x] Dashboard-Hinweise klickbar machen, sodass User direkt zur passenden Korrekturstelle springen.
- [x] Pressemitteilungs-Liste um Filter fuer „ohne Firma", Status und aktive Firma sauber abrunden.
- [x] Firmen-Detail in klare Tabs oder Sektionen aufteilen: Stammdaten, Pressekontakte, Pressemitteilungen, Abrechnung, Statistik.
- [x] Rechnungsadresse validieren und klarer anzeigen, welche Daten fuer kuenftige Rechnungen fehlen.
- [x] Sicherheit/Konto-Bereich weiter ausbauen: Passwort, 2FA, Sessions und Login-Hinweise konsolidieren.
- [x] UI-Texte und Begriffe final durchgehen: Firma, Pressemappe, Pressemitteilung, User Backend, Admin Backend.
- [x] Breitere visuelle QA im User Backend: Tabellenabstaende, Karten, Header, responsive Verhalten.
## Phase 7 — Pressemitteilungs-Form-Refactor (abgeschlossen)
- [x] Pressemitteilungen unterstuetzen einen Untertitel (`press_releases.subtitle`).
- [x] Pressemitteilungen unterstuetzen geplante Veroeffentlichung (`scheduled_at`) und Embargo (`embargo_at`).
- [x] Pressemitteilungen unterstuetzen ein optionales Boilerplate-Override (`boilerplate_override`).
- [x] PM-Inhalt wird vor dem Speichern serverseitig sanitiert (HTMLPurifier via `mews/purifier`, gekapselt in `PressReleaseHtmlSanitizer`).
- [x] Customer- und Admin-Forms verwenden den gleichen Sidebar-Aufbau:
Status & Absenden, Kategorie, Portal (als farbiger Pill), Pressekontakt, Themen-Tags, Veroeffentlichung, Weitere Felder, Phase-2-Footer.
- [x] Pressekontakt-Pflichtfeld aufgehoben — Auswahl bleibt empfohlen, ist aber technisch nullable.
- [x] Anhaenge-/Downloads-UI ist im Customer- und Admin-Editor deaktiviert (Tabelle `press_release_attachments` und Manager-Komponente bleiben fuer einen spaeteren Security-Review erhalten).
- [x] Hintergrund-Command `php artisan press-releases:publish-scheduled` veroeffentlicht geplante PMs (Scheduler-Eintrag in `routes/console.php`, Intervall 5 Min).
- [x] FluxUI Toast (`Flux::toast()`) wird konsistent fuer Erfolg, Fehler und Validation-Probleme in PM-Forms und Status-Aktionen verwendet.
- [x] Smooth-Scrolling zum ersten Validation-Fehler nach Save (`resources/js/portal-form-hooks.js`).
- [x] Pre-Submit-Check-Liste (`@computed presubmitChecks`) zeigt vor dem Einreichen offene Pflichtfelder und Empfehlungen.
## Phase 8 — User-Panel-Konsolidierung (abgeschlossen)
Vollstaendiger Plan: `docs/PHASE-8-USER-PANEL-PLAN.md`. Roadmap-Abschluss:
`dev/frontend/hub-flux/20-PHASE-8-USER-PANEL.md`.
- [x] Show-Page-Luecken schliessen (Subtitle, Scheduling, Embargo, Boilerplate-Override) — Customer + Admin (8A).
- [x] Listen-Indikatoren fuer geplante Veroeffentlichung und Embargo (8B).
- [x] Pressekontakt-Warn-Box in Sidebar-Card, wenn kein Kontakt gewaehlt (8C).
- [x] Doku-Pflege: `docs/user-admin/*` an IST-Stand ziehen (8D, dieses Dokument).
- [x] Firmen-Liste auf Mockup-Niveau (Counter-Strip, Saved-Views, Filter-Chips, Card/List-Toggle, Rollen-Legende) (8E) — Tests: `CustomerPressKitIndexPhase8eTest`, `CustomerPressKitCreatePhase8eTest`.
- [x] Set wiederverwendbarer SVG-Platzhalter fuer PM-Titelbilder + Auswahl-Modal (8F) — `App\Enums\PressReleasePlaceholder`, `<x-portal.press-release-placeholder>`, Picker `components.press-release-placeholder-picker`.
- [x] Titelbild-Schema in `press_releases` (`placeholder_variant`, deterministischer Default, `PressReleaseCoverImage`-Resolver, Hero in Customer-/Admin-Show) (8G).
- [x] Bild-Upload mit Lizenz-Pflichtfeldern (Urheber, Lizenz-Typ, Lizenz-URL bedingt, Personen-Einwilligung, Rechte-Bestaetigung) im Image-Manager (8H). Hinweis: Upload-Control bleibt `flux:input type=file` statt `flux:file-upload` (Stabilitaet); Lizenzerfassung vollstaendig.
- [x] Veroeffentlichungs-Modal mit rechtlichen Hinweisen (Platzhalter, anwaltlich zu pruefen) + Kontingent-Anzeige (Customer-Show) (8I).
- [x] Kontingent-Stub im Datenmodell (`users.press_release_quota` + `..._used_this_month`, Decrement in `submitForReview`, monatlicher `press-releases:reset-monthly-quota`-Command) (8J).
- [x] Tests, Pint, Build, Roadmap-Update (8K).
## KI-Pruef-Pipeline — Klassifikation & Content-Score (abgeschlossen 11.06.2026)
Vollstaendiger Plan mit Phasen-Details: `docs/user-admin/Entwicklungsplan KI-Pruefung und Veroeffentlichung.md`.
- [x] Einreichungs-Modal vereinheitlicht: `confirm-submit-review` in Customer-Show, -Create und -Edit (Phase 0).
- [x] API-Absicherung: `status` nicht mehr per API setzbar, eigene Submit-Route durch denselben Funnel (Phase 1).
- [x] Datenmodell: `press_releases.classification`/`classified_at`, `ki_audits`-Audit-Tabelle (Phase 2).
- [x] KI-Klassifikation Rot/Gelb/Gruen, asynchron ueber Queue `classification`, OpenAI-Treiber + deterministischer Fallback (Phase 3).
- [x] Status-Routing: Rot → abgelehnt + Mail, Gelb → manuelle Admin-Queue, Gruen → Auto-Publish (sofort/zum Termin); Scheduler publiziert nur gruene PMs (Phase 4).
- [x] Admin: KI-Badge + Klassifikations-Filter im Index, KI-Begruendung in der Show, On-Demand-„Pruefung"-Button mit Anbieter-Override.
- [x] Re-Klassifikation und Re-Score bei Titel-/Text-Aenderung (Customer, Admin, API).
- [x] Content-Score 0–100 → Stufe Standard/Geprueft/Hochwertig inkl. Editor-Panel und Badges (Phase 5).
## Titelbild, Lizenzen & Termin-Handling (abgeschlossen 10./11.06.2026)
Details: `docs/user-admin/Umsetzung Pressemitteilung Bearbeitung Titelbild Veroeffentlichung.md`.
- [x] Upload auf ein Titelbild pro PM begrenzt; Cover-Variante 1280×580, Original wird nach Verarbeitung geloescht.
- [x] Lizenz-/Rechteformular nach `Lizenztyp Bildupload.md` erweitert (7 Lizenztypen, Personen-/Sachrechte-Status, Quelle, Notizen, Risikohinweise).
- [x] Veroeffentlichungs-Box vereinfacht: nur „Sofort nach Freigabe" und „Geplanter Termin"; Embargo aus der Form-UI entfernt (`embargo_at` bleibt im Schema).
- [x] Zeitzonen-Handling: Eingabe/Anzeige Europe/Berlin, Speicherung UTC (`PressRelease::DISPLAY_TIMEZONE`, `scheduledAtLocal()`).
- [x] PM-Editor-Layout responsive entkoppelt (`.pr-editor-layout`); Ghost-Buttons auf `filled` umgestellt.
## Naechster Block — Zahlung, Tarife & Veroeffentlichungs-Flow (Launch)
Verbindliche Entscheidungen: `docs/Decision-Update Preisstruktur & Veröffentlichungs-Flow.md`. Umsetzungsplan: `docs/PHASE-9-FLOW-UND-TARIFE-PLAN.md`.
- [x] Gelb-Routing auf Direkt-Live umstellen (Entscheidung 12.06.: Gelb geht wie Gruen online, keine manuelle Queue; nur Rot wird abgelehnt) — Phase 9A.
- [x] Slot-Verbrauch von Einreichung auf **Veroeffentlichung** umstellen (Rot = kein Slot-Verbrauch, idempotent ueber Status-Logs; Submit-Guard bei 0 Rest-Slots) — Phase 9B.
- [x] Submit-Gate vorbereitet: `User::hasActiveBooking()`-Stub (`billing.enforce_booking`, Default aus), Buchungs-Hinweis im Modal, Server-Guard + API 402 — Phase 9C. Echte Buchungs-Pruefung kommt mit dem Tarif-Modul.
- [x] Funnel-Luecke geschlossen: Create-Form legte PMs direkt mit Status `review` an (ohne Blacklist/Quota/KI/Status-Log) — laeuft jetzt ueber `submitForReview` (9C).
- [x] Tarif-Datenmodell (Phase 9D, 12.06.): `plans` (4 Tiers + Seeder), `single_purchases` (Einzel-PM/Extra-PM/Boost/PDF), Laravel Cashier installiert (`User` ist Billable), `hasActiveBooking()` prueft hybrid (Stripe-Abo / Einmalkauf / Legacy-Vereinbarung).
- [x] Hybride Rechnungskreise (Entscheidung 12.06.): fortlaufende Nummern via `InvoiceNumberGenerator` — **STR-** fuer den neuen Stripe-Shop, **MAN-** fuer laufende Legacy-Zahlungen; Alt-Archiv (`legacy_invoices`) bleibt unveraendert.
- [x] MAN-Faelligkeitslauf: `billing:generate-manual-invoices` (taeglich 04:30) prueft `user_payment_options` ohne Stripe-Subscription auf erreichtes Periodenende, stellt Rechnung mit Adress-Snapshot aus und schaltet die Periode weiter (Konditions-Overrides via `legacy_conditions`).
- [x] Aktive Legacy-Zahlungen migrieren (12.06.): `legacy:grandfather-subscriptions` leitet aus dem Rechnungsarchiv die aktiven jaehrlichen Vereinbarungen ab (22 im Test-Snapshot) und schreibt sie als `grandfathered` nach `user_payment_options` — Replay-faehig fuer den Lauf kurz vor Relaunch. Doku: `dev/migration 2026/05-DATABASE-MERGE.md` §5.6.
- [x] USt-Behandlung (12.06.): alle neuen Preise netto; `VatResolver` (DE immer Steuer, EU nur mit USt-ID befreit/Reverse Charge, Drittland befreit), `vat_id` an Rechnungsadresse + Rechnungs-Snapshot, `tax_note` auf Rechnungen; Grandfathered rechnen auf Netto-Basis der letzten Legacy-Rechnung (Brutto bleibt fuer DE-Bestandskunden gleich).
- [ ] VIES-Validierung der USt-ID (aktuell Formatpruefung) — vor Gate-/Checkout-Aktivierung.
- [x] Stripe-Checkout + Webhooks (Phase 9E, 12.06.): Produkt-Sync nach Stripe (Tarife + Einzel-PM, netto, Stripe Tax), STR-Rechnungsspiegelung + Einmalkauf-Erfuellung per Webhook (Endpoint registriert), Checkout-Flows als Backend (`me.checkout.subscription`/`me.checkout.single-pm`), Slot-Logik auf Plan-Kontingent umgestellt (Grandfathered = unbegrenzt, Bestandsschutz), Quota-Stub-Spalte entfernt. UI-Anbindung folgt in 9F. Doku: `docs/user-admin/Billing-und-Rechnungskreise.md`.
- [x] Tarif-Seite + Checkout-UI (Phase 9F, 12.06.): Buchungs-Seite mit echtem 4-Tier-Raster (Monat/Jahr-Toggle, "2 Monate gratis"), Einzel-PM-Block, Bestandstarif-Anzeige (unbegrenzt), "Abo verwalten" via Stripe Billing Portal; Credit-Mock abgeloest (Credits → 9I/Phase 2).
- [ ] Tageslimit je Tier (Business 2 / Pro 3 / Agency 5), gilt auch fuer Extra-PMs.
- [ ] Launch-Credits: Extra-PM, Boost (nur gruene PMs), Veroeffentlichungsnachweis-PDF; Credit-Anker 1 Credit = 1 €.
- [ ] Einzel→Abo-Bruecke (19 € Anrechnung innerhalb 30 Tagen).
- [ ] Rechtstexte im Einreichungs-Modal anwaltlich pruefen lassen (Platzhalter, Go-Live-Blocker).
- [ ] Queue-Worker fuer `classification` im Produktions-Setup verankern.
## Phase 2 / spaeter
- [ ] Vorab-KI-Pruefung, Redigieren/Nachbessern + Re-Check-Loop, Pruefzaehler und Credit-Overflow (Decision-Update §7).
- [ ] Magic-Link-Zugriff fuer Firmen-E-Mail-Adressen konzipieren und umsetzen.
- [ ] Separate `token_requests`-Tabelle fuer nicht-userbasierte Zugriffe anlegen.
- [ ] Zugriff per Firmen-E-Mail so begrenzen, dass nur passende Firmen und Pressemitteilungen sichtbar werden.
- [ ] Trust Score fuer User/Firmen konzipieren und im Admin Backend justierbar machen (KI-Plan Phase 6).
- [ ] Moderationslogik an Trust Score anbinden (Klassifikations-Routing existiert bereits).
- [ ] Stufen-Badges (Geprueft/Hochwertig) im oeffentlichen Web-Frontend ausgeben.
- [ ] Aufbewahrungsfristen fuer Magic Links, Token Requests, API Logs, Statuslogs und `ki_audits`-Raw-Responses (DSGVO) definieren und technisch absichern.
- [ ] Admin-editierbare Textvorlagen fuer neutrale Tombstone-/Entfernungs- und Systemtexte einbauen.
- [ ] API-Nutzungs-Log im User Backend sichtbar machen.
- [ ] Benachrichtigungen und Newsletter-Abos im Konto-Bereich ausbauen.
- [ ] Zahlungsarten und firmenbezogene Zahlungsoptionen im User Backend aktivieren.
- [ ] Statistikbereich fuer Firmen und Pressemitteilungen umsetzen.
- [ ] Medienbereich aus vorhandenen Pressemitteilungsbildern ableiten; spaeter echte Medienbibliothek pruefen.
- [ ] Team-/Rollenverwaltung fuer Firmen im User Backend ergaenzen.
- [ ] Anzeige-Lokalisierung von `published_at`, `created_at` und Status-Log-Zeitstempeln (aktuell UTC).
## Hinweise
- Phase 1, Phase 7 (PM-Form-Refactor), Phase 8 (User-Panel-Konsolidierung) und die KI-Pruef-Pipeline (Phasen 0–5) sind abgeschlossen — siehe Plan-Dokus oben.
- Fuer Preise, Kontingente und den Veroeffentlichungs-Flow gilt ausschliesslich das Decision-Update vom 11.06.2026; aeltere Tarif-Tabellen in `Konzept-Update 1` und im Relaunch-Konzept sind ueberschrieben.
- Das PM-Kontingent kommt aus dem Tarif (`plans.press_release_quota`) plus Einmalkaeufen; Bestandskunden (Grandfathered) sind unbegrenzt. Solange `billing.enforce_booking` aus ist, gilt kein Kontingent (Launch-Schalter).
- Die KI-Klassifikation laeuft asynchron — in Produktion wird ein Queue-Worker fuer die Queue `classification` benoetigt (Test-Drain: `php artisan classification:work`).
- Anhaenge sind aktuell aus Sicherheitsgruenden deaktiviert, Tabelle und Komponente bleiben aber erhalten und werden in einem separaten Audit-Track reaktiviert.

View file

@ -1,329 +0,0 @@
# User-Admin: Zusammenhänge und relevante Daten
Stand: 2026-06-11 (aktualisiert nach Phase 8 + KI-Pipeline)
Diese Notiz beschreibt den User als fachlichen Mittelpunkt für die weitere Konzeption des Admin-User-Bereichs. Grundlage sind die aktuellen Models und Migrationen im Laravel-Projekt.
> **Was seit Phase 8 + KI-Pipeline dazugekommen ist (29.05.–11.06.2026)**:
>
> - `press_releases`: `placeholder_variant` (SVG-Titelbild-Platzhalter, 8G),
> `classification`/`classified_at` (KI-Klassifikation Rot/Gelb/Grün),
> `content_score`/`content_tier`/`scored_at` (Content-Score).
> - `press_release_images`: Lizenz-/Rechtefelder `author`, `license_type`,
> `license_detail`, `license_url`, `source_url`, `people_rights_status`,
> `property_rights_status`, `rights_notes`, `persons_consent`,
> `rights_confirmed_at` (8H + Erweiterung 10.06.).
> - `users`: Quota-Stub `press_release_quota` +
> `press_release_quota_used_this_month` (8J; wird vom Tarif-Modul abgelöst).
> - Neue Tabelle `ki_audits` (Modell `KiAudit`, append-only Audit-Log jeder
> KI-Entscheidung) mit Relation `PressRelease::kiAudits()`.
> - Neue Enums: `PressReleasePlaceholder`, `ImageLicenseType`,
> `PressReleaseClassification`, `PressReleaseContentTier`.
> - Neue Services/Jobs: `PressReleaseCoverImage` (Cover-Resolver),
> Treiber-Architektur unter `Services/PressRelease/Classification/` und
> `…/ContentScore/`, Jobs `ClassifyPressRelease`/`ScorePressRelease`
> (Queue `classification`), Konfiguration in `config/scoring.php`.
> - Neue Commands: `press-releases:reset-monthly-quota` (Scheduler, 1. des
> Monats), `classification:work` (Queue-Drain zum Testen).
> - Zeitzonen-Konstante `PressRelease::DISPLAY_TIMEZONE` (Europe/Berlin) mit
> `scheduledAtLocal()`/`embargoAtLocal()` für alle Termin-Anzeigen.
> **Was sich seit dem ursprünglichen Stand (2026-05-05) geändert hat**:
>
> - Pressemitteilungen haben zusätzliche Felder: `subtitle`, `scheduled_at`,
> `embargo_at`, `boilerplate_override`, `no_export` (Phase 7).
> - Neue Tabelle `press_release_attachments` (Modell `PressReleaseAttachment`).
> UI ist temporär deaktiviert (Security-Review), Tabelle und Service
> bleiben aber erhalten.
> - Neuer Service-Layer für PMs: `PressReleaseService` (Status-Übergänge,
> Scheduling-Auflösung), `PressReleaseHtmlSanitizer` (HTMLPurifier-Wrapper),
> `PressReleaseAttachmentStorage`.
> - Neuer Console-Command `php artisan press-releases:publish-scheduled`
> (Scheduler-Intervall 5 Min) veröffentlicht geplante PMs automatisch.
> - Pivot `press_release_contact` bleibt n:m, der Customer-Flow speichert
> aber nur einen Kontakt pro PM und das Feld ist nullable.
## Zentraler Ausgangspunkt
Ein `User` ist nicht nur ein Login-Konto, sondern bündelt im System mehrere fachliche Bereiche:
- Zugang und Status: Name, E-Mail, Verifikation, Passwort, 2FA, Aktiv/Inaktiv, Super-Admin-Flag, Rollen/Berechtigungen.
- Portal- und Legacy-Zuordnung: `portal`, `registration_type`, `language`, `legacy_portal`, `legacy_id`.
- CRM-Zuordnung: Firmen, Firmenrollen, Kontakte.
- Content-Zuordnung: Pressemitteilungen, Bilder, Statusverlauf.
- Abrechnung und Verträge: Rechnungsadresse, Zahlungsoptionen, Zahlungen, Rechnungen, Legacy-Rechnungen.
- API und Sicherheit: Sanctum-Tokens, API-Nutzung, Magic-Links.
- Kommunikation und Arbeitskomfort: Newsletter-Abos, Filter-Presets.
## Datenmodell im Überblick
```mermaid
erDiagram
users ||--o| profiles : "hat"
users ||--o| billing_addresses : "hat"
users ||--o{ companies : "owner_user_id"
users }o--o{ companies : "company_user.role"
users }o--o{ contacts : "contact_user"
companies ||--o{ contacts : "hat"
users ||--o{ press_releases : "autor"
companies ||--o{ press_releases : "zugeordnet"
contacts }o--o{ press_releases : "press_release_contact"
press_releases ||--o{ press_release_images : "hat"
press_releases ||--o{ press_release_status_logs : "hat"
users ||--o{ press_release_status_logs : "changed_by_user_id"
users ||--o{ user_payment_options : "hat"
user_payment_options }o--o{ companies : "user_payment_option_company"
user_payment_options ||--o{ user_payments : "hat"
user_payments ||--o{ invoices : "erzeugt"
users ||--o{ invoices : "hat"
users ||--o{ legacy_invoices : "hat"
users ||--o{ newsletter_subscriptions : "hat"
users ||--o{ magic_links : "hat"
users ||--o{ user_filter_presets : "hat"
users ||--o{ api_usage_logs : "hat"
```
## Direkte Relationen am User
`profile`
- Kardinalität: 1:0..1.
- Tabelle: `profiles`, Foreign Key `user_id` ist eindeutig.
- Inhalt: persönliche Profildaten, Anrede, Titel, Vor-/Nachname, Telefon, Adresse, Land, Geburtsdatum, Backlink, Statistik-/Footer-Code-Flags, Validierungs-/Vertragsdaten, Steuerdaten.
- Relevanz im Admin: Datenqualität, Legacy-Profil, persönliche Stammdaten, Vertrag/Validierung.
`billingAddress`
- Kardinalität: 1:0..1.
- Tabelle: `billing_addresses`, Foreign Key `user_id` ist eindeutig.
- Inhalt: Rechnungsname, Adresse, PLZ, Ort, Land.
- Relevanz im Admin: Rechnungsfähigkeit, fehlende Abrechnungsdaten, Kundendatenpflege.
`ownedCompanies`
- Kardinalität: 1:n.
- Tabelle: `companies`, Foreign Key `owner_user_id`.
- Bedeutung: Der User ist fachlicher Eigentümer einer Firma, auch ohne Pivot-Eintrag in `company_user`.
- Relevanz im Admin: wichtig für Rechte im Customer-Profil und für klare Verantwortlichkeit.
`companies`
- Kardinalität: n:m.
- Pivot: `company_user` mit `company_id`, `user_id`, `role`.
- Rollen: `member`, `responsible`, `owner`.
- Bedeutung: Ein User kann mehrere Firmen haben; eine Firma kann mehreren Usern zugeordnet sein.
- Relevanz im Admin: Hauptstruktur für Kundenkonto, Berechtigungen in der Firmenpflege und spätere User-Detailansicht.
`contacts`
- Kardinalität: n:m.
- Pivot: `contact_user` mit `contact_id`, `user_id`.
- Zusätzlich gehört jeder Kontakt zwingend zu genau einer Firma über `contacts.company_id`.
- Bedeutung: Kontakte können direkt Usern zugeordnet sein, fachlich sind sie aber an Firmen aufgehängt.
- Relevanz im Admin: Ansprechpartner und Zuordnungsqualität.
`pressReleases`
- Kardinalität: 1:n.
- Tabelle: `press_releases`, Foreign Key `user_id`.
- Zusätzlich: optionale Firmenzuordnung über `company_id` und Pflichtkategorie über `category_id`.
- Wichtig: Datenbankseitig ist `company_id` nullable, fachlich sollte eine Customer-PM einer Firma zugeordnet sein. Der Customer-Flow erzwingt aktuell eine eigene Firma und leitet daraus das Portal ab.
- Relevanz im Admin: Content-Historie, Status, Veröffentlichungen, Qualität, Freigabeprozess.
`newsletterSubscriptions`
- Kardinalität: 1:n.
- Tabelle: `newsletter_subscriptions`, Foreign Key `user_id`.
- Inhalt: Portal, Name, E-Mail, IP, Bestätigung, Subscribe/Unsubscribe, Legacy-Zuordnung.
- Relevanz im Admin: Kommunikationsstatus und Opt-in/Opt-out-Historie.
`billing / payments`
- `userPaymentOptions`: 1:n über `user_payment_options.user_id`.
- `invoices`: 1:n über `invoices.user_id`.
- `legacyInvoices`: 1:n über `legacy_invoices.user_id`.
- Indirekt: `user_payment_options` hat n:m zu Firmen über `user_payment_option_company`; `user_payments` hängt an `user_payment_options`; `invoices` können zusätzlich an `user_payments` und `invoice_billing_addresses` hängen.
- Relevanz im Admin: Vertrags-/Abo-Status, aktive API-Freischaltung, Rechnungen, Legacy-Archiv.
`tokens`
- Kommt über Laravel Sanctum `HasApiTokens`.
- Tabelle: `personal_access_tokens` mit polymorphem `tokenable_type` / `tokenable_id`.
- Im Customer-Bereich werden Tokens für API-Zugriffe verwaltet.
- Relevanz im Admin: API-Zugang, genutzte Berechtigungen, letzter Zugriff.
`magicLinks`
- Kardinalität: 1:n.
- Tabelle: `magic_links`, Foreign Key `user_id`.
- Inhalt: Token-Hash, Zweck, Payload, Ablauf, Verbrauch, IPs.
- Relevanz im Admin: Auth-/Support-Kontext, Magic-Login-Historie.
`filterPresets`
- Kardinalität: 1:n.
- Tabelle: `user_filter_presets`, Foreign Key `user_id`.
- Inhalt: Seite, Name, Default-Flag, letzte Nutzung, Filter-JSON.
- Relevanz im Admin: eher Arbeitskomfort/Personalisierung, nicht Kern-Kundendaten.
`apiUsageLogs`
- Kardinalität: faktisch 1:n über `api_usage_logs.user_id`, aber aktuell keine Relation im `User`-Model.
- Inhalt: Token, Methode, Pfad, Route, Status, IP, User-Agent, Dauer, Zeitpunkt.
- Relevanz im Admin: API-Aktivität, Support und Missbrauchsanalyse.
`roles` und `permissions`
- Kommen über Spatie `HasRoles`.
- Tabellen u. a. `roles`, `permissions`, `model_has_roles`, `model_has_permissions`, `role_has_permissions`.
- Relevanz im Admin: Trennung zwischen Admin, Editor, Customer, Super-Admin und feineren Berechtigungen wie `press-releases:publish` oder `users:manage`.
## Firmen, Kontakte und Pressemitteilungen
Die fachliche CRM-/Content-Struktur läuft im Kern so:
- User hat mehrere Firmen über `company_user`.
- User kann zusätzlich direkter Eigentümer einer Firma über `companies.owner_user_id` sein.
- Firma hat mehrere Kontakte über `contacts.company_id`.
- Firma hat mehrere Pressemitteilungen über `press_releases.company_id`.
- Pressemitteilung hat genau einen Autor/User über `press_releases.user_id`.
- Pressemitteilung kann mehrere Kontakte über `press_release_contact` referenzieren.
- Kontakt kann mehreren Usern direkt zugeordnet sein über `contact_user`, bleibt aber immer an genau eine Firma gebunden.
Daraus ergibt sich für den User-Admin eine wichtige Sicht:
- User-Zentrum: Wer ist der Account?
- Firmen-Zentrum: Welche Firmen gehören dazu, in welcher Rolle?
- Kontakt-Zentrum: Welche Ansprechpartner hängen an diesen Firmen und/oder am User?
- Content-Zentrum: Welche Pressemitteilungen wurden vom User erstellt und welcher Firma sind sie zugeordnet?
- Abrechnungs-Zentrum: Welche Zahloptionen, Rechnungen und Legacy-Rechnungen hängen am User und ggf. an Firmen?
## Pressemitteilungen im Detail
Eine Pressemitteilung (`press_releases`) enthält:
- Identifikation: `id`, `uuid`, `slug`, `legacy_portal`, `legacy_id`.
- Zuordnung: `portal`, `language`, `user_id`, `company_id`, `category_id`.
- Inhalt: `title`, `subtitle`, `text`, `backlink_url`, `keywords`, `boilerplate_override`.
- Status/Publikation: `status`, `published_at`, `scheduled_at`, `embargo_at`, `no_export`, `hits`, `teaser_begin`, `teaser_end`.
- Medien: Bilder über `press_release_images`, Anhänge über `press_release_attachments` (UI deaktiviert).
- Kontakte: Pivot `press_release_contact`.
- Verlauf: Statuslogs über `press_release_status_logs`, inklusive `changed_by_user_id`, Statuswechsel, Grund und Quelle.
Service-Layer rund um PMs:
- `App\Services\PressRelease\PressReleaseService` — kapselt die Übergänge `submitForReview`, `publish`, `reject`, `archive`, `deleteFromAdmin` sowie das Auflösen von `scheduled_at`/`embargo_at` für die echte Veröffentlichung.
- `App\Services\PressRelease\PressReleaseHtmlSanitizer` — Wrapper um `mews/purifier` mit definierter Tag-/Attribut-Whitelist.
- `App\Services\PressRelease\PressReleaseAttachmentStorage` — kapselt Datei-Operationen auf dem `public`-Disk (UI aktuell deaktiviert).
- `App\Console\Commands\PublishScheduledPressReleases` — wird per Scheduler (alle 5 Min) ausgeführt und veröffentlicht freigegebene PMs zum geplanten Zeitpunkt, respektiert Embargo.
Fachliche Konsequenz:
- Für Kunden sollte `company_id` praktisch Pflicht sein, obwohl die DB es nullable erlaubt.
- Für Admins kann `company_id = null` als Migrations-/Altfall auftauchen und sollte im User-Admin sichtbar markiert werden.
- `portal` sollte mit der Firma konsistent sein. Im Customer-Flow wird das Portal aus der Firma abgeleitet; im Admin-Bereich sollte eine bewusste Korrektur möglich sein.
## User-relevante Tabellen
Kernkonto:
- `users`
- `profiles`
- `billing_addresses`
- `sessions`
- `password_reset_tokens`
- `personal_access_tokens`
- `magic_links`
- Spatie Permission-Tabellen
CRM:
- `companies`
- `company_user`
- `contacts`
- `contact_user`
Content:
- `press_releases`
- `press_release_images`
- `press_release_attachments` (UI temporaer deaktiviert)
- `press_release_contact`
- `press_release_status_logs`
- `categories`
- `category_translations`
Billing:
- `billing_addresses`
- `user_payment_options`
- `user_payment_option_company`
- `user_payments`
- `payment_options`
- `payment_option_translations`
- `invoices`
- `invoice_billing_addresses`
- `legacy_invoices`
Kommunikation/API/Arbeitskomfort:
- `newsletter_subscriptions`
- `api_usage_logs`
- `user_filter_presets`
## Relevante Inhalte für den Admin-User-Bereich
Für eine optimierte Admin-User-Ansicht sollten diese Blöcke geplant werden:
- Account: Status, Portal, Sprache, Registrierungstyp, Rollen, Berechtigungen, Super-Admin, letzte Aktivität, letzte IP, E-Mail-Verifikation, 2FA-Status.
- Datenqualität: Profil vorhanden, Rechnungsadresse vorhanden, Firma vorhanden, Kontakte vorhanden, veröffentlichte PMs vorhanden, Legacy-Zuordnung vorhanden.
- Firmen: Firmenliste mit Rolle, Eigentümerstatus, Portal, Aktivstatus, Logo, Footer-Code-Flag, Kontakt-/PM-Anzahl.
- Kontakte: Kontakte je Firma plus direkte User-Kontakte aus `contact_user`.
- Pressemitteilungen: Gesamtzahl, Statusverteilung, letzte PMs, veröffentlichte PMs, PMs ohne Firma, PMs mit Portalabweichung.
- Abrechnung: Rechnungsadresse, aktive Zahlungsoptionen, verknüpfte Firmen zu Zahlungsoptionen, Zahlungen, Rechnungen, Legacy-Rechnungen.
- API: Token-Anzahl, Berechtigungen, letzte Nutzung, API-Usage-Logs.
- Support/Admin-Aktionen: User bearbeiten, Rollen/Berechtigungen ändern, Impersonation, Magic-Link-Kontext, ggf. Account deaktivieren.
## Offene Punkte für die weitere Konzeption
- `User` hat keine direkte `apiUsageLogs()`-Relation, obwohl `api_usage_logs.user_id` existiert. Für Admin-Detailansichten wäre eine Relation sinnvoll.
- `press_releases.company_id` ist nullable. Fachlich sollte geklärt werden, ob das nur Legacy-/Admin-Fälle erlaubt oder langfristig verboten werden soll.
- `contacts` gehören immer zu einer Firma, können aber zusätzlich direkt Usern zugeordnet werden. Für die Oberfläche muss klar sein, ob "User-Kontakte" nur direkte Pivot-Kontakte meint oder alle Kontakte seiner Firmen.
- Eine Firma kann über `owner_user_id` und zusätzlich über `company_user.role = owner` eine Eigentümerinformation haben. Das sollte im Admin eindeutig dargestellt und ggf. harmonisiert werden.
- Billing hängt primär am User, Zahlungsoptionen können aber über Pivot auch Firmen betreffen. Für den Admin sollte klar werden, ob Rechnungen immer User-Rechnungen sind oder später firmenscharf betrachtet werden sollen.
- Portal-Scope ist auf Firmen, Kontakte, Pressemitteilungen und Newsletter aktiv. Admin-Auswertungen nutzen teils `withoutGlobalScopes()`. In der User-Admin-Konzeption muss entschieden werden, wann portalübergreifend gesucht und angezeigt wird.
## Quellen im Code
Modelle:
- `app/Models/User.php`
- `app/Models/Company.php`
- `app/Models/Contact.php`
- `app/Models/PressRelease.php`
- `app/Models/PressReleaseImage.php`
- `app/Models/PressReleaseAttachment.php`
- `app/Models/PressReleaseStatusLog.php`
- `app/Models/Profile.php`
- `app/Models/BillingAddress.php`
- `app/Models/Invoice.php`
- `app/Models/LegacyInvoice.php`
- `app/Models/UserPaymentOption.php`
- `app/Models/UserPayment.php`
- `app/Models/NewsletterSubscription.php`
- `app/Models/MagicLink.php`
- `app/Models/UserFilterPreset.php`
- `app/Models/ApiUsageLog.php`
Services / Commands:
- `app/Services/PressRelease/PressReleaseService.php`
- `app/Services/PressRelease/PressReleaseHtmlSanitizer.php`
- `app/Services/PressRelease/PressReleaseAttachmentStorage.php`
- `app/Services/Image/ImageService.php`
- `app/Console/Commands/PublishScheduledPressReleases.php`
- `routes/console.php` (Scheduler-Eintraege)
Migrationen:
- `database/migrations/*`

View file

@ -1,254 +0,0 @@
**Version:** Juni 2026 (Rev. 2) **Datum:** 12.06.2026 **Status:** Abgestimmt – bereit zur Integration ins Konzept-/Decision-Log **Scope:** Definition von Boost und Veröffentlichungsnachweis (Launch), des Magic-Link-Zugangs- und Änderungsprozesses sowie der Phase-2-Funktionen (Vorab-Prüfung/Redigieren, Prüfzähler, höheres Prüfkontingent, kostenpflichtige Änderungspfade). **Änderungen Rev. 2:** KI bei E/F entfernt (Admin-Panel statt KI-Check); A nutzt Algorithmus-Diff statt KI; Bremsen-/Limit-Spalte für alle Pfade ergänzt. **Änderungen Rev. 3:** A & B als Launch bestätigt; Boost-Preisstaffel (12/20/35) und PDF-Preis (3 Credits) festgelegt; G Depublizieren = 25 Credits. **Änderungen Rev. 4:** Extra-PM ergänzt – tier-gestaffelt (19/15/12/10/8 Credits); Cross-Portal-Veröffentlichung als kein Thema vermerkt (Portale getrennt).
---
## 1. Kontext
Dieses Update definiert die credit-nahen Funktionen und den Änderungsprozess für bestehende Pressemitteilungen. Leitlinie: kleiner, ehrlicher Launch-Umfang; alles, was an die volle Credit-/Prüf-Mechanik gekoppelt ist, wandert kontrolliert in Phase 2. Gesetzlich verpflichtende Pfade bleiben unabhängig davon ab Launch erreichbar.
**Grundprinzip Kostenkontrolle:** Jeder Vorgang, der einen KI-Call oder Admin-Arbeit auslöst, braucht ein Limit. Daraus folgen zwei Strategien:
1. **Kosten vermeiden statt deckeln** – wo ein billiger Algorithmus die Arbeit erledigt (z. B. Tippfehler-Diff), wird kein KI-Call ausgelöst. Das Limit schützt dann nur noch gegen Missbrauch, nicht gegen Kosten.
2. **Bezahlung als Selbstbremse** – kostenpflichtige Pfade (C/D/G) sind selbstbegrenzend: Wer zahlt, missbraucht nicht. Harte Limits braucht es nur bei den kostenlosen Pfaden (A/B/E/F).
---
## 2. Launch-Funktionen
### 2.1 Extra-Pressemitteilung (Kontingent-Nachkauf)
Die Brücke zwischen Tarif-Kontingent und Credit-Wallet: Ist das monatliche PM-Kontingent voll, kann eine einzelne weitere PM über die Wallet nachgekauft werden – ohne Zwang zum nächsthöheren Tarif. Das ist die faire Alternative zum erzwungenen Upgrade und der genau gewünschte Fall „ich brauche _einmalig_ eine PM mehr".
**Preis ist tier-gestaffelt** (Treuevorteil: höheres Abo = günstigere Extra-PM):
|Situation|Extra-PM|Inkl. PM-Preis|Logik|
|---|---|---|---|
|Kein Abo (Einzel)|19 Credits|19,00 €|voller Satz = Einzel-PM-Preis|
|Starter|15 Credits|9,67 €|günstiger als Einzel, mild über inkludiert|
|Business|12 Credits|4,90 €|–|
|Pro|10 Credits|3,96 €|–|
|Agency|8 Credits|3,32 €|–|
**Mechanik:** Der Extra-PM-Preis wird **zur Kaufzeit aus dem aktiven Abo abgeleitet**, nicht statisch gespeichert. Reicht das Wallet-Guthaben nicht, greift der kontextuelle Mini-Checkout („Kostet 15 Credits, du hast 8" → Paket nachladen). Der Volumenrabatt steckt bereits in den Credit-Paketen – **keine** separaten Extra-PM-Bündel nötig (eine Wallet, ein tier-abhängiger Preis).
**Design-Entscheidung (bewusst):** Der prozentuale Aufschlag ggü. dem inkludierten PM-Preis ist bei Starter am mildesten (+55 %), bei den oberen Tiers höher (~140–150 %). Das ist gewollt: Die gelegentliche 4. PM eines Starter-Kunden wird _nicht_ bestraft, um künstlichen Upgrade-Druck zu erzeugen – passt zur „fair bleiben, kein Zwang"-Linie. Wer dauerhaft viel veröffentlicht, wandert ohnehin in höhere Tiers.
**Tageslimit gilt auch hier:** Nachgekaufte Extra-PMs unterliegen demselben Tageslimit wie das Kontingent (kein „Spam freikaufen"; höherer Tagesdurchsatz = Enterprise-Fall).
### 2.2 Boost (Platzierung)
- **Was:** bezahlte Hervorhebung einer PM – Platzierung auf Startseite und Branchen-/Kategorieseite
- **Gate:** nur **grüne** PMs sind boostbar; gelb/rot nicht
- **Mechanik:** nachträglich kaufbar, sobald die PM live und grün ist
- **Umfang:** ein Boost = Featured auf Startseite **und** Branchenseite (eine Stufe, nur die Dauer variiert – bewusst kein getrenntes Pricing pro Platzierung)
- **Bezahlung:** über die Credit-Wallet (kleine Wallet ist Launch-Bestandteil)
**Preisstaffel (1 Credit = 1 €):**
|Dauer|Credits|Pro Tag|
|---|---|---|
|7 Tage|12|1,71 €|
|14 Tage|20|1,43 €|
|30 Tage|35|1,17 €|
Moderate Staffel: Pro-Tag-Preis sinkt mit der Dauer, der Einstieg (7 Tage / 12) bleibt unter dem PM-Preis von 19 – ein Boost wirkt nie teurer als das Veröffentlichen selbst. Passt zur „Nische besetzen statt abschöpfen"-Linie (Volumen statt Einzelmarge).
### 2.3 Veröffentlichungsnachweis / PDF
- **Was:** generiertes PDF „PM XY wurde am … auf … veröffentlicht" inkl. URL, Datum, Vorschau
- **Zweck:** Reporting an Vorgesetzte/Kunden – klassische PR-Mitnahme
- **Preis:** **3 Credits**, pauschal pro PM – Impulskauf, keine Abwägung (kostet die Plattform praktisch nichts: keine KI, on-demand aus vorhandenen Daten)
- **Generierung:** on-demand aus vorhandenen PM-Daten
- **Phase-2-Option:** in Business/Pro/Agency später ggf. **inklusive** als kleiner Tarif-Perk; zum Launch einheitlich 3 Credits
---
## 3. Magic-Link: Zugang & Änderungsprozess
### 3.1 Zugangsmodell – eine Verwaltung, zwei Eintrittswege
**Grundsatz:** Die Verwaltung bestehender Pressemitteilungen ist **ein und dieselbe** Funktion im pressekonto. Es gibt nur zwei Wege hinein:
1. **Registrierter Account** → normaler Login → verwaltet seine PMs direkt im pressekonto.
2. **Pressekontakt ohne Account** → **Magic-Link** → loggt sich zu den PMs ein, die mit seiner hinterlegten E-Mail verknüpft sind, und nimmt dort Änderungen vor.
Der Magic-Link ist also **kein eigenes System**, sondern die Zugangsbrücke für Pressekontakte bzw. Firmen, deren E-Mail-Adressen im System hinterlegt sind, die aber noch nicht direkt registriert sind. Die dahinterliegende Verwaltungsoberfläche und die Änderungspfade sind identisch zum eingeloggten Account.
**Account-Konvertierung:** Aus dem Magic-Link-Zugang heraus kann der Pressekontakt jederzeit „Permanenten Account anlegen" wählen (Passwort vergeben). Danach läuft der Zugang über den regulären Login – der Magic-Link wird für ihn überflüssig.
**Authentifizierung (Sicherheit):**
- Auf jeder PM dezenter Link „Sie sind als Pressekontakt hinterlegt? Pressemitteilung verwalten →"
- E-Mail-Eingabe + Captcha; **identische Antwort unabhängig vom Match** (verhindert User-Enumeration)
- Bei Match: Magic-Link-Mail mit 30-Min-Token → authentifizierte Session
- Dashboard listet alle PMs mit dieser E-Mail als Pressekontakt (über mehrere Firmen/Jahre)
### 3.2 Änderungs- & Lösch-Pfade (A–G)
Kein wahlloses Ändern/Löschen – Friction nach Anliegen. Alle Pfade laufen über dieselbe Verwaltung (Login **oder** Magic-Link).
|Pfad|Anliegen|KI / Admin|Kosten|Bremse|Phase|Public Hint|
|---|---|---|---|---|---|---|
|**A**|Tippfehler/Grammatik|Algorithmus-Diff, KI nur im Graubereich|kostenfrei|1 Einreichung/PM/24 h, gebündelt + Account-Tages-Cap|**Launch**|nein|
|**B**|Pressekontakt-Daten ändern|keine|kostenfrei|normales Rate-Limit|**Launch**|nein|
|**C**|Inhaltliche Korrektur (sachlicher Fehler)|KI|kostenpflichtig|Bezahlung = Bremse|**Phase 2**|ja|
|**D**|Update/Ergänzung (neue Information)|KI|kostenpflichtig|Bezahlung = Bremse|**Phase 2**|ja|
|**E**|DSGVO-Anonymisierung|**Admin-Panel** (keine KI)|kostenfrei|1 offene Anfrage/PM gleichzeitig|**Launch (Pflicht)**|nein|
|**F**|Persönlichkeitsrechtsverletzung|**Admin-Panel** (keine KI)|kostenfrei|1 offene Anfrage/PM gleichzeitig|**Launch (Pflicht)**|je nach Outcome|
|**G**|Depublizieren|KI|25 Credits + Bedenkzeit|Bezahlung + 24–48 h Bedenkzeit|**Phase 2**|Tombstone|
A & B sind als Launch-Bestandteil bestätigt: kostenfrei, credit-unabhängig und sie senken sofort das unautorisierte Mail-Aufkommen. C/D/G folgen mit dem Credit-/Prüf-System in Phase 2.
**Pfad-Details (Kurzfassung):**
- **A Tippfehler:** Inline-Editor mit Diff. **Kein KI-Call im Normalfall** – ein lokaler Zeichen-Diff (Levenshtein) entscheidet: Änderung klein **und** keine Zahlen/Namen/Eigennamen berührt → automatisch übernommen (0 KI, 0 Admin). Nur im Graubereich (größere Änderung oder sensible Tokens berührt) → KI-Check **oder** Umleitung zu Pfad C. Nutzer korrigiert idealerweise alle Tippfehler gebündelt und reicht **einmal** ein = ein Vorgang.
- **B Kontaktdaten:** Formular, direkt übernommen, Versionierung im Hintergrund
- **C Korrektur:** Editor + Pflichtfeld „Was war falsch / was ist korrekt?"; KI erlaubt Fakt-Korrektur (Zahl/Datum/Name), blockiert Umschreibung der Aussage; PM erhält Korrektur-Hinweis
- **D Update:** Ergänzung wird unten mit Datum angehängt, Original bleibt unverändert; KI-Check auf Spam/Werbung
- **E DSGVO:** Aufklärung (Art. 85 DSGVO, Medienprivileg → keine Volllöschung, aber kostenfreie Entfernung personenbezogener Daten); Checkbox-Auswahl (Name, Durchwahl, persönliche E-Mail, Freitext). **Keine KI** – die Anfrage geht als Benachrichtigung ins Admin-Panel, manuelle Sichtung. In Phase 2/3 automatisierbar.
- **F Persönlichkeitsrecht:** Pflichtfelder (betroffene Stelle, Art, Begründung, Belege). **Keine KI** – Benachrichtigung ins Admin-Panel, manuelle Sichtung; Outcomes: Anonymisierung, Anpassung, Tombstone, Ablehnung mit Begründung. In Phase 2/3 automatisierbar.
- **G Depublizieren:** Aufklärungsseite → Begründungspflicht (KI lenkt „veraltet" → D, „falsch/peinlich" → C) → kostenpflichtige Bestätigung → 24–48 h Bedenkzeit mit Widerrufslink → Tombstone (`noindex`, raus aus Listen/Suche, URL bleibt)
### 3.3 Compliance-Minimum zum Launch (E & F)
**E und F können nicht auf Phase 2 warten.** Sobald PMs live sind, besteht ein gesetzliches Recht auf Anonymisierung personenbezogener Daten (E) und auf Meldung von Persönlichkeitsrechtsverletzungen (F). Beide müssen ab Tag 1 erreichbar sein – zur Not als einfaches Formular/manueller Prozess statt vollem Wizard. Beide sind **kostenfrei** (eine legitime Rechtsanfrage darf nie hinter einer Gebühr stehen) und damit unabhängig vom Phase-2-Credit-Build.
**Abwicklung über Admin-Panel (keine KI):** E und F lösen keinen KI-Call aus. Die Anfrage erzeugt eine Benachrichtigung im Admin-Panel, die Sichtung erfolgt manuell. Das spart KI-Kosten und gibt bei Rechtsthemen die bessere Kontrolle. Eine Automatisierung ist für Phase 2/3 vorgesehen.
**Limit – nicht „einmalig", sondern „eine offene Anfrage pro PM":** Eine gesetzliche Anfrage darf nie hart gesperrt werden (jemand kann legitim ein zweites Mal eine Anonymisierung brauchen, etwa wenn über ein Update neue personenbezogene Daten hinzukommen). Die saubere Bremse: Solange für eine PM eine Anfrage offen ist, kann keine neue gestellt werden (verhindert Spam-Duplikate); nach Abschluss geht wieder eine. Das deckelt Missbrauch ohne Rechtsrisiko.
### 3.4 Missbrauchsschutz, Edge Cases & Standard-Antwort
- **Pfad A (Tippfehler):** Algorithmus-Diff spart im Normalfall jeden KI-Call → Limit schützt nur gegen Missbrauch, nicht gegen Kosten. Bremse: 1 Korrektur-Einreichung pro PM / 24 h (gebündelt), plus Account-Tages-Cap gegen den pathologischen Fall (ein Account bearbeitet hunderte PMs)
- **Rate-Limit** auf Magic-Link-Anfragen pro E-Mail/IP; Cooldown nach Depublizierung (Widerruf-Fenster); Audit-Log mit IP/User-Agent je Edit-Aktion
- **Keine valide E-Mail** (alte connektar-PMs): Fallback „Verifikation per Domain-Inhaberschaft / Impressums-Match", manuelle Prüfung
- **E-Mail geändert / Person verlässt Firma:** manuelle Anfrage mit Bestätigung über `info@`/Impressum
- **Massenanträge:** Bulk möglich, Friction (Gebühr, Bedenkzeit) wird **pro PM** angewendet
- **Standard-Antwort auf unautorisierte Lösch-Mails:** „Änderungen an Pressemitteilungen sind ausschließlich über das Verwaltungs-Portal der jeweiligen PM möglich." → beendet das wahllose „löscht das mal eben"-Mailaufkommen ab Launch
### 3.5 Abgrenzung zum öffentlichen „Melden"-Button
||„Melden" (öffentlich)|Änderungs-Flow (autorisiert)|
|---|---|---|
|Wer|jeder Dritte|nur Pressekontakt (Login/Magic-Link)|
|Auth|keine|Login oder Magic-Link|
|Use Case|Urheberrecht, Verleumdung, Spam|eigene PM ändern|
|Outcome|Quarantäne, Prüfung|siehe Pfade A–G|
Treffpunkt: Eine über „Melden" angezeigte Persönlichkeitsrechtsverletzung läuft in dieselbe Queue wie Pfad F.
---
## 4. Phase 2 (nachgelagert)
Diese Funktionen entstehen gemeinsam, weil sie alle die Re-Check-/Credit-Mechanik voraussetzen.
### 4.1 Vorab-Prüfung / Redigieren
Erzeugt erst die Situation „prüfen, ohne (noch) zu veröffentlichen" und den Edit→Neu-Prüfen-Loop. Damit werden auch die kostenpflichtigen Pfade C/D technisch real.
### 4.2 Prüfzähler
- **Eigener Zähler**, getrennt von der Credit-Wallet → „Prüfungen inklusive" bleibt sauberes Versprechen
- **Aggregiert pro Account/Monat** gedeckelt (nicht pro PM) → löst Klon-Abuse ohne Klon-Erkennung
- **Prüf-Tageslimit** als Burst-Schutz (~10/Tag)
- **Overflow:** Zähler leer → weitere Prüfungen ziehen 1 Credit/Prüfung aus der Wallet
### 4.3 Höheres Prüfkontingent (Tier-gestaffelt)
|Tier|Freie Prüfungen/Monat|
|---|---|
|Einzel|4|
|Starter|12|
|Business|30|
|Pro|60|
|Agency|120|
Schnitt ~3–4 Prüfungen pro inkludierter PM; ehrlicher Normalfall (≈2 Prüfungen) stößt nie an. Dies sind die früheren „Bonus-Credit"-Zahlen, umgewidmet zum Prüf-Kontingent.
### 4.4 Kostenpflichtige Magic-Link-Pfade (C / D / G)
Kosten-Anker bei 1 Credit = 1 € (zu bestätigen):
|Pfad|Aktion|Anker|
|---|---|---|
|C|Inhaltliche Korrektur|≈ 8 Credits (zu bestätigen)|
|D|Update/Ergänzung|≈ 4 Credits (zu bestätigen)|
|G|Depublizieren|**35 Credits** + 24–48 h Bedenkzeit (festgelegt)|
Depublizieren bewusst am teuersten und mit Bedenkzeit, weil irreversibelste Aktion.
---
## 5. Anti-Zombie-Check (dieser Stand)
- ✅ Gesetzliche Anfragen (E/F) immer kostenfrei, ab Launch erreichbar und nie hart gesperrt (1 offene Anfrage/PM statt „einmalig")
- ✅ Kosten nur bei echtem Mehraufwand (Korrektur, Update, Depublizierung), nicht bei Pflicht-Rechten
- ✅ Kostenlose Pfade bleiben echt kostenlos: Tippfehler laufen über Algorithmus-Diff statt KI, Limits schützen gegen Missbrauch – nicht als versteckte Kostenbremse
- ✅ Eine Verwaltung, zwei Eintrittswege – keine künstliche Trennung registrierter/unregistrierter Nutzer
- ✅ Prüf-Kontingent großzügig genug, dass der Normalfall nie ansteht
- ✅ Depublizierung mit Aufklärung + Bedenkzeit statt Hard-Delete – schützt den Kunden vor sich selbst
---
**Bewusst nicht im Scope – Cross-Portal-Veröffentlichung:** Die beiden Portale (presseecho.de, businessportal24.com) sind optisch, inhaltlich und systemisch vollständig getrennt; im Relaunch laufen sie lediglich auf einem gemeinsamen Backend zusammen. Eine PM auf beiden Portalen gleichzeitig zu veröffentlichen ist **kein Feature** – wer in beiden präsent sein will, bucht zwei getrennte Einträge (zwei Slots / zwei Extra-PMs). Das vermeidet Duplicate-Content über die eigenen Domains und passt zur getrennten Zielgruppen-Logik. Kein Phase-2-Punkt, sondern bewusst ausgeschlossen.
Noch mal ein wichtiger Hintergrund, der noch dokumentiert werden muss. Das sollte auch zukünftig gegebenenfalls geprüft werden.
**In Rev. 3 abgeschlossen:** A/B-Launch ✓ · Boost-Staffel 12/20/35 ✓ · PDF 3 Credits ✓ · G Depublizieren 35 Credits ✓ **In Rev. 4 abgeschlossen:** Extra-PM tier-gestaffelt (19/15/12/10/8) ✓ · Cross-Portal ausgeschlossen ✓
---
## 6. Umsetzungsstand (Stand 17.06.2026)
Die credit-nahe Mechanik ist technisch umgesetzt. Single Source of Truth für alle Preise/Staffeln: `config/credits.php` (1 Credit = 1 €). Ableitung zentral im `CreditPricingService`.
**Fundament**
- **Credit-Wallet + Ledger:** `credit_wallets` (denormalisierter Saldo) + `credit_transactions` (append-only). Einziger Schreibpfad: `CreditWalletService` (atomar, Row-Lock). Zu wenig Guthaben → `InsufficientCreditsException` mit `required/available/shortfall` (treibt den kontextuellen Mini-Checkout).
- **Tier:** `Tier`-Enum + `User::currentTier()` (kein Abo = `Einzel`).
- **Wallet-Topup:** Credit-Pakete mit Bonus-Staffel (10→10, 25→27, 50→55, 100→115). Stripe-Ad-hoc-Charge (`checkoutCharge`, **keine** Stripe-Preis-IDs nötig); Gutschrift idempotent im Webhook über `credit_topup_id`.
**Launch-Funktionen**
- **§2.1 Extra-PM:** `ExtraPmPurchaseService` zieht den tier-abgeleiteten Preis aus der Wallet und verbucht einen `SinglePurchase(ExtraPm, paid)` → greift in die bestehende Kontingent-/Slot-Mechanik.
- **§2.2 Boost:** `BoostService` + `boosts`-Tabelle. Gate: nur veröffentlichte, grüne PMs. Mehrfachkauf verlängert. `PressRelease::scopeBoosted()` als Basis für die Featured-Platzierung (Frontend-Anbindung offen).
- **§2.3 Veröffentlichungsnachweis:** `ProofPdfService` (3 Credits, einmalig je PM) + `ProofPdfRenderer` (on-demand PDF), Download unter `/admin/me/press-releases/{id}/nachweis` (Policy + Kauf-Gate).
- **UI:** Customer-Bereich „Buchungen & Add-ons" (Wallet-Hub, Paket-Topup, Extra-PM, Preisliste, USt.-Bestätigungs-Modal vor Stripe) und Add-ons direkt an der veröffentlichten PM (Boost/Nachweis).
- **Submit-Gate (§5.1):** `BILLING_ENFORCE_BOOKING`. Ohne Buchung erscheint im Entwurf/Submit-Modal der Hinweis „Noch keine Buchung" mit Link zur Buchungsseite — der Entwurf wird dabei **immer zuerst gespeichert** (kein Datenverlust beim Sprung).
**Phase 2 (Mechanik vorbereitet, UI bewusst noch nicht aktiv)**
- **Prüfzähler/Prüfkontingent (§4.2/§4.3):** `review_checks` + `ReviewCheckService` vollständig (Tageslimit hart → Monats-Freikontingent tier-gestaffelt 4/12/30/60/120 → Credit-Overflow). Anzeige im Kundenbereich ausgeblendet, bis die Vorab-Prüfung (§4.1) steht.
- **Kostenpflichtige Magic-Link-Pfade C/D/G:** Preise liegen in `config/credits.php` (`paths.correction` 8, `paths.update` 4, `depublish` 25); Flows folgen mit dem Re-Check-/Vorab-Prüfungs-System.
**Noch offen / nächste Schritte**
- Featured-Platzierung geboosteter PMs im öffentlichen Web-Feed.
- Vorab-Prüfung/Redigieren (§4.1) als Voraussetzung für Prüfzähler-UI und Pfade C/D/G.
## 7. Launch-Vorbereitung (Stand 18.06.2026)
Aufsetzend auf dem Credit-Stand aus §6 wurden weitere Launch-Punkte umgesetzt:
**Status-Widget (immer verfügbarer Buchungsüberblick)**
- Header-Dropdown oben rechts (Desktop) bzw. im Mobile-Header, für alle Kunden-Panel-User: aktuelle Buchung/Tarif (+ Intervall), verbleibendes PM-Kontingent (oder ∞), Credit-Guthaben. Ohne aktive Buchung Hinweis + Link zur Buchungsseite.
- `CustomerStatusSummary` bündelt die Anzeige-Werte über die öffentliche `User`-API (Gating bleibt Single Source of Truth im Modell); Anzeige via `x-customer.status-menu`. Kompakte Topbar (`min-h-0`), kein Leerraum.
**WS-8 Code-Optimierung**
- `User::currentPlan()` und `hasUnlimitedPressReleaseQuota()` pro Request memoisiert (Status-Widget fragt sie auf jeder Seite ab); `fresh()` setzt den Cache zurück.
- Detailseite: `getMyPR()` war nicht memoisiert und lief bis zu 7× pro Request mit voller Eager-Load-Query → memoisiert; Firmen-Kennzahlen per `withCount()` statt nachträglichem `loadCount()`.
- Dedup: `PressRelease::scopeAccessibleBy(User)` (Kunden-PM-Zugriff), `Contact::scopeForCompany()`, `BillingAddress::REQUIRED_FIELDS`/`completenessPercent()`, Trait `InteractsWithPressReleaseTags` (create/edit).
**Paket-Vorteile (dynamisch, admin-pflegbar)**
- `plans.benefits` (JSON-Spalte, Cast, `Plan::benefitsList()`). Im Admin „Tarife & Pakete" zeilenweise pflegbar; angezeigt auf der In-App-Buchungsseite UND der öffentlichen Preise-Seite (eine Datenquelle). DB-Vorteile sind admin-redaktioneller Klartext (unter `/en` nicht auto-übersetzt — bewusst).
**i18n (Internationalisierungs-Readiness)**
- Default-Locale auf `de` (`APP_LOCALE`/`APP_FALLBACK_LOCALE`): Backend rendert Deutsch nativ als Schlüssel; `en` ist der spätere Umschalter. Frontend nutzt bereits URL-Prefix `/de`–`/en` (`SetEdition`) + strukturierte `lang/de|en/*.php`.
- Alle Backend-Views sind `__()`-gewrappt (Auth-Strecke als letzter Rest nachgezogen). Leeres `lang/en.json` als Gerüst.
- **Bewusst offen:** Backend-Sprachumschalter (Middleware liest `user->language`) und die EN-Übersetzungen (~2743 Strings) — erst beim tatsächlichen EN-Launch.
**Bekannter Bugfix**
- Legacy-/importierte PMs (Plaintext mit `\n`) verloren beim Laden in den HTML-Editor die Umbrüche → `PressReleaseHtmlSanitizer::toEditorHtml()` wandelt sie beim Bearbeiten in `<p>`/`<br>`.
**Noch offen / nächster Schritt**
- End-to-End-Flow-Durchlauf mit Test­buchungen (Detail-/Optikkorrekturen, Stripe-Testmodus) — QA als letzter Launch-Schritt.

View file

@ -1,145 +0,0 @@
**Version:** Juni 2026 **Datum:** 11.06.2026 **Status:** Abgestimmt – bereit zur Integration ins Konzept-/Decision-Log **Scope:** Behandlung von Links in Presseartikeln, `rel`-Auszeichnung, Linkangabe und Linktyp-Auswahl im Editor, Abgrenzung zum verkauften Dofollow-Backlink. Präzisiert den entsprechenden Punkt im Decision-Update „Preisstruktur & Veröffentlichungs-Flow".
---
## 1. Kontext & Klarstellung
In der Preis-Abstimmung war der Punkt „verkaufte Dofollow-Backlinks ausgeschlossen" zu pauschal formuliert und konnte als „keine Links" missverstanden werden. Das ist falsch. Links in Presseartikeln gehören selbstverständlich dazu. Die Frage ist nicht _ob_, sondern _wie ausgezeichnet_ und _wie verkauft_.
Es geht um zwei verschiedene Dinge, die beide „Backlink" heißen:
1. **Der Link im Presseartikel zur Kundenseite** – normaler, erwarteter Bestandteil jeder PM. Erlaubt, darf hervorgehoben werden. Muss technisch korrekt ausgezeichnet sein.
2. **Den „Dofollow-Backlink für SEO-Wert" als bezahltes Upsell verkaufen** – das ist die Falle und bleibt ausgeschlossen.
---
## 2. Grundregel (Google-konform)
**Externe Links in Pressemitteilungen werden systemseitig mit `rel="sponsored"` bzw. `rel="nofollow"` ausgezeichnet.**
Hintergrund: Google behandelt Links in Pressemitteilungen als Kategorie, die nofollow gehört – der Kunde setzt den Link selbst, es ist keine redaktionelle Empfehlung des Portals. Jeder über Bezahlung oder kommerzielle Vereinbarung entstandene Link muss entsprechend qualifiziert sein; eine fehlende Kennzeichnung von Paid-Placements ist selbst der Richtlinienverstoß. Für kombinierte Fälle (bezahlte Platzierung + kein Endorsement) ist `rel="sponsored nofollow"` zulässig.
**Konsequenz fürs Produkt:** Die `rel`-Auszeichnung ist **nicht kundenseitig wählbar**. Sie wird vom System anhand der Platzierungs-Kategorie automatisch gesetzt. Damit ist die Domain dauerhaft sauber und es gibt keinen Hebel, über den ein Kunde „dofollow" erzwingen könnte.
---
## 3. Wichtig: nofollow ≠ wertlos
Die alte Logik „nofollow = nutzlos" gilt seit 2019 nicht mehr. Google behandelt `nofollow`/`sponsored`/`ugc` seitdem als **Hinweis**, nicht als absolute Anweisung. Praktischer Wert für den Kunden bleibt:
- **Referral-Traffic** – direkte Klicks vom Presseartikel auf die Kundenseite
- **Marken-Assoziation & Kontext** – die Nennung selbst zählt
- **AI-/LLM-Sichtbarkeit** – Markennennungen aus Presseartikeln fließen zunehmend in Google AI Overviews und LLM-Antworten ein, wo Sichtbarkeit von Autorität abhängt, nicht nur von klassischen Backlinks
**Das ist das ehrliche, zukunftsfeste Verkaufsargument:** Sichtbarkeit, Reichweite und Auffindbarkeit – nicht „PageRank kaufen".
---
## 4. Linktypen im Überblick
|Element|Funktion|`rel` (systemseitig)|
|---|---|---|
|Firmen-Link im PM-Fließtext|Standard, immer möglich|`sponsored` / `nofollow`|
|**Unternehmens-CTA-Box** (Website, Newsroom, Kontakt) am PM-Ende|Hervorhebung, optisch abgesetzt|`sponsored` / `nofollow`|
|Link zur **Landingpage / Produktseite** (extern)|vom Kunden gewählt|`sponsored` / `nofollow`|
|Link zum **Unternehmensprofil auf dem Portal** (intern)|redaktionelle interne Verlinkung|**follow**|
**Der unterschätzte Punkt – die interne Verlinkung:** Ein follow-Link auf das **portaleigene** Unternehmensprofil ist regelkonform (interner, redaktioneller Link) und stärkt die **eigene** Domain-Architektur. Der Kunde bekommt eine sichtbare „Über das Unternehmen"-Verlinkung; das Portal baut gleichzeitig seine interne Linkstruktur auf, statt fremde Autorität zu verschenken.
---
## 5. Linkangabe & Linktyp-Auswahl im Editor
**Anforderung:** Beim Erstellen einer PM gibt der Kunde Links an und wählt einen Linktyp.
**Sauberes Design (Google-konform):** Der Kunde wählt **Ziel und Zweck** des Links – das System leitet daraus die korrekte `rel`-Auszeichnung ab. Die Begriffe „follow/nofollow" tauchen im Kunden-UI **nicht** auf.
Editor-Flow beim Hinzufügen eines Links:
1. **Ziel-URL** eingeben
2. **Link-Art** wählen (das ist die kundenseitige „Linktyp"-Auswahl):
|Link-Art (Auswahl im Editor)|Beispiel|Systemseitige Auszeichnung|
|---|---|---|
|Unternehmens-Website|startseite.de|extern → `sponsored`/`nofollow`|
|Spezifische Landingpage / Produktseite|aktion.startseite.de|extern → `sponsored`/`nofollow`|
|Newsroom / Unternehmensprofil auf dem Portal|(interner Profil-Link)|intern → `follow`|
|Kontakt / Social-Profil|LinkedIn etc.|extern → `sponsored`/`nofollow`|
3. **Darstellung** (optional, ggf. tarif-/boost-abhängig): Inline-Textlink **oder** hervorgehobene CTA-Box. Das betrifft die _Präsentation_, nicht den Link-Typ.
**Entscheidung festgehalten:** Die Linktyp-Auswahl steuert **Ziel und Darstellung**, nie das `rel`-Attribut. Das `rel` ist systemgesteuert. Damit hat der Kunde maximale Freiheit bei Ziel und Hervorhebung, ohne dass die Domain-Sicherheit kippt.
---
## 6. Was Produkt ist – und was tabu bleibt
||Status|
|---|---|
|Links in der PM (extern, korrekt ausgezeichnet)|✅ Standard, kostenlos Teil der Veröffentlichung|
|Hervorgehobene CTA-Box / Linkdarstellung|✅ als Hervorhebungs-/Boost-Feature verkaufbar|
|follow-Link auf portaleigenes Unternehmensprofil|✅ regelkonform, stärkt eigene Domain|
|Sichtbarkeit / Reichweite / Platzierung|✅ das eigentliche Verkaufsargument|
|„Dofollow-Backlink" als bezahltes Upsell|❌ Link-Scheme-Verstoß, Domain-Risiko, gegen Anti-Zombie|
|Kundenseitiger follow/nofollow-Schalter|❌ nicht im UI, `rel` bleibt systemgesteuert|
---
## 7. Anti-Zombie-Check (dieser Stand)
- ✅ Keine versteckten SEO-Versprechen – verkauft wird Sichtbarkeit, nicht PageRank
- ✅ Domain-Glaubwürdigkeit langfristig geschützt (korrekte `rel`-Auszeichnung systemseitig)
- ✅ Kunde behält volle Freiheit bei Linkziel und Hervorhebung
- ✅ Regelkonform gegenüber Google-Spam-Richtlinien, kein Graubereich
- ✅ Interne Verlinkung baut eigenes Asset statt fremde Autorität zu verschenken
---
## 8. Offene / spätere Punkte
- **Darstellungs-Stufung:** Soll die hervorgehobene CTA-Box ein eigenes Boost-/Tarif-Feature sein oder Standard? (Hängt am Boost-Konzept.)
- **Anzahl Links pro PM:** Sinnvolle Obergrenze definieren (gegen Link-Spam in der PM selbst, z. B. Profil + Website + 1–2 Kontextlinks).
- **Anchor-Text-Regeln:** Optional späterer Hinweis/Soft-Check gegen übermäßige Exact-Match-Anchors (Spam-Signal), eher Phase 2.
---
## 9. Korrektur am vorherigen Decision-Update
Im Dokument „Preisstruktur & Veröffentlichungs-Flow", Abschnitt 4, wird der Eintrag
> „Bewusst ausgeschlossen: Verkaufte Dofollow-Backlinks."
ersetzt durch:
> „Verlinkung: Links zur Kundenseite sind Standard-Bestandteil jeder PM, systemseitig als `sponsored`/`nofollow` ausgezeichnet. Hervorhebung der Linkdarstellung ist als Produkt-Feature möglich. **Tabu:** Verkauf von Dofollow-Backlinks und kundenseitige `rel`-Auswahl. Details siehe Decision-Update „Verlinkung & Backlinks"."
---
_SEO-/Richtlinien-Stand: Google-Spam-Policies inkl. Link-Spam-Enforcement 2024–2026; `nofollow`/`sponsored`/`ugc` als Hinweise seit September 2019. Vor produktiver Umsetzung der `rel`-Logik einmal gegen die dann aktuelle Google-Search-Central-Doku gegenprüfen._
---
## 10. Umsetzungsstand (12.06.2026)
**Umgesetzt:**
- `PressReleaseLinkPolicy` (app/Services/PressRelease/): systemseitige
`rel`-Auszeichnung beim Rendern — extern → `sponsored nofollow noopener`
+ `target="_blank"`; portalintern (konfigurierte Domains aus
`config/domains.php` inkl. www-Variante, relative Pfade) → **follow**;
`mailto:`/`tel:` → ohne `rel`/`target`. Autoren-`rel` wird immer
überschrieben (kein kundenseitiger Hebel). Greift in
`PressReleaseHtmlSanitizer::render()` und wirkt damit rückwirkend auf
alle gespeicherten Inhalte und auf jede Ausgabe (Panel-Vorschau heute,
Web-Detailseiten beim Relaunch).
- Editor-Hinweis (PM anlegen/bearbeiten): Links erwünscht, Auszeichnung
automatisch — bewusst ohne follow/nofollow-Auswahl im UI.
- §9-Korrektur im Decision-Update „Preisstruktur & Veröffentlichungs-Flow"
(Abschnitt 4) eingearbeitet.
**Offen (wie in §8 vorgesehen):** CTA-Box/Darstellungs-Stufung (hängt am
Boost-Konzept, 9I), Link-Obergrenze pro PM, Anchor-Text-Soft-Check
(Phase 2). Die Linktyp-Auswahl im Editor ist fürs `rel` nicht nötig
(systemseitig aus der Ziel-URL abgeleitet) — sie wird relevant, sobald die
CTA-Box als Darstellungs-Option kommt.

View file

@ -1,148 +0,0 @@
**Version:** Juni 2026 · **Datum:** 15.06.2026 · **Status:** Abgestimmt – in Umsetzung
**Scope:** Detaillierter Umsetzungsplan für die nächste Phase. Bündelt die Launch-Anteile der beiden Decision-Updates (Phase-2/Magic-Link, Duplicate-Content) mit den offenen Punkten aus `weiteres/Merkliste.md` sowie den Rest-Zweigen Login-Flow und Code-Optimierung.
---
## 0. Ausgangslage – was bereits im Code existiert
Die Kartierung der betroffenen Subsysteme hat ergeben, dass einiges bereits halb gebaut ist. Der reale Restaufwand ist dadurch kleiner als die Konzept-Dokumente vermuten lassen – vieles ist „verifizieren + scharf schalten + UI" statt „von null bauen".
| Thema | Ist-Stand |
|---|---|
| Magic-Link | Basis vorhanden (`app/Models/MagicLink.php`, `MagicLinkGenerator`, `MagicLinkConsumeController`) – aber nur **Login registrierter User** + **Read-only-PM-Vorschau**. Pressekontakt-per-E-Mail-Zugang fehlt. |
| Abo-/Bestandsschutz-Gate | **Bereits implementiert:** `User::hasActiveBooking()` zählt `grandfathered` mit; `app/Services/PressRelease/PressReleaseService.php` wirft `BookingRequiredException`. Aber hinter Launch-Schalter `billing.enforce_booking=false` deaktiviert. |
| Bestandsschutz | Abgebildet als `UserPaymentOption.status='grandfathered'` + `legacy_conditions` (JSON). Einmal-Import `GrandfatherLegacySubscriptions` + täglicher MAN-Worker `GenerateManualInvoices` (04:30) existieren. |
| Boost / Proof-PDF / Extra-PM | Enum-Cases existieren (`SinglePurchaseType`), aber **keine Credit-Wallet** → bleibt Phase 2. |
| Canonical-Tags | Fehlen in `resources/views/web/layouts/web-master.blade.php` (`<head>` hat nur `meta_description`). |
| „Melden"-Button | Platzhalter (`href="#"`) in `resources/views/web/release-detail.blade.php`, kein Backend. |
| Google-Login / Socialite | **Nicht installiert** – Greenfield (hiermit freigegeben). |
| E-Mail-Verifizierung | Fortify-Feature `emailVerification()` aktuell **nicht aktiv**; Registrierung setzt weder `is_active` noch Rolle. |
---
## 1. Verbindliche Entscheidungen (15.06.2026)
1. **Legacy-Posten bleiben unverändert.** Bestandsschutz-Posten werden **nicht** auf ein Tarif-Abo (z. B. Agentur) gemappt. Die Positionen bleiben als eigene Legacy-Konditionen erhalten, so wie sie sind.
2. **Registrierung mit Pflicht-Verifizierung.** Neu registrierte Benutzer müssen sich **einmalig per E-Mail verifizieren**. Erst nach Verifizierung: `customer`-Rolle + `is_active=true`. **Ausnahme:** Magic-Link, Google-Login & vergleichbare Wege – dort findet die Verifizierung über den Login-Kanal selbst statt (keine zusätzliche E-Mail-Verifizierung nötig).
3. **Laravel Socialite freigegeben** als neue Abhängigkeit (für Google-Login, später ggf. weitere Provider).
---
## 2. Bewusst NICHT in dieser Phase (Phase 2, credit-abhängig)
Der Großteil des Phase-2/Magic-Link-Dokuments hängt an der **Credit-Wallet**, die noch nicht existiert. Diese Punkte werden bewusst zurückgestellt, bis die Wallet gebaut ist:
- Pfade **C** (Inhaltliche Korrektur), **D** (Update/Ergänzung), **G** (Depublizieren)
- **Boost** (Platzierung), **Veröffentlichungsnachweis/PDF**, **Extra-PM** (Kontingent-Nachkauf)
- **Prüfzähler** & höheres Prüfkontingent, Vorab-Prüfung/Redigieren
Vom **Duplicate-Content-Dokument** ist die Entscheidung ohnehin „nichts bauen"; einziger umsetzbarer Launch-Punkt dort: **Canonical-Tags** (siehe WS-1). Hash-/Shingle-Vergleich bleibt Phase-3-Option.
---
## 3. Arbeitspakete
### WS-1 — SEO Canonical-Tags (Duplicate-Doc §5) · klein, isoliert
**Scope:** Jede öffentliche PM bekommt ihre kanonische URL; pro Portal getrennt (kein Cross-Portal → kein System-Duplicate).
- `<link rel="canonical">` + OpenGraph-Tags (og:title/description/image) in `web-master.blade.php` via `@yield`/`@stack`.
- `release-detail.blade.php` setzt die kanonische URL der PM; Listen-/Übersichtsseiten Self-Canonical.
- **Tests:** Feature-Test prüft `rel="canonical"` mit korrekter URL auf veröffentlichter PM.
- **Aufwand:** ~½ Tag. Keine Migration, kein Risiko.
### WS-2 — Magic-Link-Pressekontaktzugang (Phase-2-Doc §3.1) · Fundament
**Scope:** Zugangsbrücke „eine Verwaltung, zwei Eintrittswege" für unregistrierte Pressekontakte.
- Neuer Magic-Link-`purpose` = `contact_access` im bestehenden `MagicLinkGenerator` (TTL 30 Min).
- Dezenter Link auf jeder öffentlichen PM: „Sie sind als Pressekontakt hinterlegt? → verwalten".
- E-Mail-Eingabe + Captcha; **enumeration-sichere identische Antwort** unabhängig vom Match.
- Bei Match → Mail mit Token → authentifizierte Session, die alle PMs zu dieser E-Mail listet (`Contact::where('email',…)->pressReleases()`, firmen-/jahresübergreifend).
- Rate-Limit pro E-Mail/IP; Audit-Log (IP/User-Agent) je Aktion.
- **Account-Konvertierung:** „Permanenten Account anlegen" aus der Magic-Link-Session (Passwort vergeben) → Verifizierung gilt über den Magic-Link-Kanal als erfolgt (siehe Entscheidung 2).
- **Tests:** Token-Lifecycle, Enumeration-Sicherheit, abgelaufener/verbrauchter Token, PM-Liste nur eigene E-Mail.
- **Aufwand:** ~2–3 Tage. **Fundament für WS-3.**
### WS-3 — Änderungs-/Lösch-Pfade A · B · E · F (Phase-2-Doc §3.2/3.3) · Launch-Pflicht
**Status (16.06.): WS-3 launch-fertig.**
- **E · F + Melden + Admin-Queue – erledigt** (Commit `95007da`): Tabelle `legal_requests`, öffentliches Formular `/release/{slug}/rechtliches` (DSGVO/Persönlichkeitsrecht/Meldung, enumeration-neutral, zwei Honeypots, Rate-Limit, Bremse „1 offene Anfrage/PM+Typ", typabhängiger Hinweistext), „Melden"-/E&F-Einstieg auf der PM-Detailseite, Admin-Bereich „Recht & Compliance" (Nav-Badge mit Offen-Zähler, Queue index/show mit erledigt/abgelehnt/wieder öffnen + interner Notiz). Routen entkoppelt in `routes/legal.php`. Tests grün. **Hinweis:** Regeltexte je Typ sind Entwurf (TODO im Formular) – vor Launch mit Recht/Legal finalisieren.
- **B — Kontaktdaten ändern – abgedeckt** (über WS-2-Firmen-Scope): `responsible`-Pressekontakte bearbeiten die Kontakt-Datensätze im Firmen-Bereich; die öffentliche PM-Detailseite lädt Kontakte aus der Relation, daher spiegeln sich Änderungen auf veröffentlichten PMs. Keine separate Hintergrund-Versionierung (direkte Übernahme).
- **A — Tippfehler/Grammatik – bewusst zurückgestellt (Phase 2 / Pfad C).** Veröffentlichte PMs sind im Kundenbereich nicht editierbar (nur `draft`/`rejected`). A würde veröffentlichte Inhalte automatisch ändern (Integritätsrisiko), ist nicht launch-blockierend und überlappt mit Pfad C. Entscheidung 16.06.: nach Phase 2.
Alle hinter WS-2 **oder** regulärem Login.
- **B — Kontaktdaten ändern** (einfachster Pfad zuerst): Formular, direkt übernommen, Versionierung im Hintergrund. Kostenfrei.
- **A — Tippfehler/Grammatik:** Inline-Editor mit **Levenshtein-Diff** (kein KI-Call). Kleine Änderung ohne Zahlen/Namen → automatisch; Graubereich → Hinweis „bitte als Korrektur (Pfad C, Phase 2) einreichen". Bremse: 1 Einreichung/PM/24 h + Account-Tages-Cap.
- **E — DSGVO-Anonymisierung** & **F — Persönlichkeitsrecht:** **rechtlich zwingend ab Launch.** Formular → Eintrag in neuer Tabelle `legal_requests` → **Admin-Queue** (keine KI). Bremse: „1 offene Anfrage pro PM" (nicht hart gesperrt).
- **Öffentlicher „Melden"-Button** (§3.5) verdrahten → speist dieselbe Queue wie F.
- **Neuer Admin-Bereich „Recht & Compliance":** Nav in `components/layouts/app/sidebar.blade.php`, Routen in `routes/admin.php`, Volt-Komponenten unter `livewire/admin/legal-requests/` nach Muster `admin/contacts/index`.
- **Migration:** `legal_requests` (press_release_id, type[dsgvo/personal_rights/report], status, payload, requester_email, resolved_at, …).
- **Tests:** je Pfad ein Feature-Test; „1 offene Anfrage/PM"-Bremse; Admin-Queue-Sichtbarkeit; Melden→F-Queue.
- **Aufwand:** ~4–5 Tage (E/F + Queue der Großteil).
### WS-4 — Bestandsschutz-Abrechnung & -Übersicht (Merkliste 1·2·3·5)
**Scope:** Vorhandene Mechanik (`GrandfatherLegacySubscriptions`, `GenerateManualInvoices`) verifizieren + Lücken schließen. **Legacy-Posten bleiben unverändert (Entscheidung 1) – kein Tarif-Mapping.**
**Status (16.06.):**
- **M1 (portalübergreifend) – verifiziert:** `hasActiveBooking()` / `duePaymentOptions()` filtern nicht nach `legacy_portal`; Beweis-Test ergänzt (grandfathered Posten zählt unabhängig vom aktiven Portal-Kontext).
- **M5 (Import + 04:30-Worker) – verifiziert:** `GenerateManualInvoices` läuft täglich 04:30 (`routes/console.php`), berücksichtigt Active + Grandfathered ohne Stripe.
- **M2 (gemeinsam abrechnen, keine Einzelkündigung) – umgesetzt (Entscheidung 16.06.: getrennte Rechnungen, gekoppelte Kündigung):** kein Rechnungs-Schema-Umbau; jede Kondition behält ihre eigene MAN-Rechnung. Neuer `LegacySubscriptionService::cancelBundleFor()` kündigt das gesamte Bestandsschutz-Bündel eines Kunden **gemeinsam** – es gibt bewusst keine Einzelkündigung.
- **M3 (Admin-Sichtbarkeit) – umgesetzt:** Admin-Payments-Ansicht zeigt eine eigene „Bestandsschutz-Posten"-Tabelle (Badge, Portal, Netto, nächste Fälligkeit, „Bündel (N)"-Indikator) inkl. Bündel-Kündigung mit Bestätigung.
- Tests: `tests/Feature/Billing/LegacyBundleTest.php` (M1/M2/M3). **Offen optional:** kombinierte Sammel-Rechnung (eine Rechnung mit Positionen) wurde bewusst nicht gebaut.
- **M1 — Beide Portale ohne Unterscheidung:** Bestandsschutz gilt portalübergreifend, auch wenn nur für ein Portal gebucht → sicherstellen, dass `legacy_portal` im Import **kein** Filter ist.
- **M2 — Zwei Posten zusammen abrechnen, nicht einzeln kündbar:** Zwei Legacy-Posten desselben Kunden (je Portal, z. B. connektar 2×1000 € netto) werden gemeinsam abgerechnet und gelten zusammen (Gesamtsumme 2000 € netto bleibt). Die einzelnen Positionen bleiben als eigene Legacy-Konditionen erhalten – **keine** Zusammenführung zu einem Tarif, **keine** Einzelkündigung.
- **M5 — Einmal-Import + täglicher Worker:** Verifizieren, dass alle aktiven Abrechnungen erfasst werden und der 04:30-Worker fällige Rechnungen korrekt erzeugt; ggf. Gesamtübersicht ergänzen.
- **M3 — Ansicht deutlicher:** Im Admin-Payments-Bereich Bestandsschutz-Posten optisch klar kennzeichnen/zuordnen (Badge „Bestandsschutz", Portal-Zuordnung, gebündelte Posten sichtbar).
- **Tests:** gemeinsame Abrechnung zweier Posten; portalübergreifende Geltung; Worker erzeugt Rechnung zum Periodenende.
- **Aufwand:** ~2–3 Tage. Parallel zu WS-1/2 möglich.
### WS-5 — Abo-/Bestandsschutz-Gate scharf schalten (Merkliste 6) — ✅ erledigt 16.06.2026
**Scope:** „User ohne aktives Abo oder Bestandsschutz dürfen keine PM schreiben."
- Mechanik existiert (`hasActiveBooking()` inkl. `grandfathered`, `BookingRequiredException`) und ist mit `enforce_booking=true` voll getestet:
- **Backend-Gate:** `PressReleaseService::submitForReview()` wirft `BookingRequiredException`/`QuotaExceededException`.
- **API:** `POST /api/v1/press-releases/{id}/submit` → `402` (keine Buchung) bzw. `422` (Kontingent erschöpft).
- **Block-UX am Einreichpunkt:** `press-release-submit-modal.blade.php` zeigt ohne Buchung statt des Prüf-Flows den Hinweis „Buchung erforderlich" + Button zur Buchungsseite; alle Volt-Caller (show/edit/create/index) fangen die Exceptions und zeigen einen Toast.
- **Proaktiver Hinweis:** Kunden-Dashboard zeigt bei scharfem Gate ohne aktive Buchung einen Banner „Buchung erforderlich, um zu veröffentlichen" → `me.bookings.index`. Bestandskunden (grandfathered) sehen ihn nicht.
- **Launch-Flip:** Kein Code-Default ändern — der Schalter bleibt in `config/billing.php` auf `false` (Dev-Default). Scharfschalten ausschließlich per Prod-Env `BILLING_ENFORCE_BOOKING=true` (→ WS-7-Checkliste).
- **Tests:** `HasActiveBookingTest`, `PressReleaseSubmitApiTest` (402/422), `DashboardTest` (Banner an/aus + grandfathered-Ausnahme), `LegacyBundleTest` (grandfathered zählt portalübergreifend).
- **Aufwand:** erledigt. **Abhängigkeit:** WS-4 (erledigt).
### WS-6 — Login/Registrierung: Flow + E-Mail-Verifizierung + Google-Login (Merkliste 7 + Zweig 3) — ✅ erledigt 17.06.2026
**Scope nach Entscheidung 2 & 3 — vollständig umgesetzt (Audit 17.06., 65 Auth-Tests grün):**
- **E-Mail-Verifizierung scharf:** `User implements MustVerifyEmail`; `verified`-Middleware greift real auf den Panel-Routen (`routes/admin.php`, `routes/customer.php`). Eigener Volt-Flow statt Fortify-Built-in (`Features::emailVerification()` bewusst aus, wie `registration()`/`resetPasswords()`): Notice-Volt + `VerifyEmailController` + Resend-Route. Lockout der Bestands-User durch Backfill-Migration verhindert.
- **Rolle + Aktivierung erst nach Bestätigung:** `CreateNewUser` legt inaktiv & rollenlos an; `App\Listeners\ActivateUserAfterVerification` (auf `Verified`-Event) setzt `is_active=true` + `customer`.
- **Ausnahme-Kanäle:** Magic-Link & Google gelten als verifiziert → `SocialAuthService`/Magic-Link setzen `email_verified_at` + Rolle direkt.
- **Google-Login:** `laravel/socialite ^5.27`, `config/services.php`, `GoogleController` (redirect/callback), `oauth_provider`/`oauth_provider_id`-Migration, De-Dup über E-Mail.
- **Flow-Audit:** Login (2FA-Bypass behoben), Passwort-Reset, Verify-Email, 2FA-Challenge, Magic-Link – alle als Volt-Komponenten, gehärtet (Security-Review 16.06.).
- **Tests:** `tests/Feature/Auth/` (10 Dateien): Registrierung→Verifizierung→Aktivierung, unverifiziert blockiert, Socialite gemockt, Account-Verknüpfung, Magic-Link/Google ohne E-Mail-Schritt.
- **Operativ offen → WS-7:** Google-Keys + Redirect-URI in der Prod-`.env`.
- **Doku:** [Sicherheit & Deployment-Hinweise (Auth, Rollen, Verifizierung).md](Sicherheit%20&%20Deployment-Hinweise%20(Auth,%20Rollen,%20Verifizierung).md).
### WS-7 — Live-Deployment-Checkliste (Merkliste 4) — ✅ erledigt 17.06.2026
- Kompakte, abarbeitbare Go-Live-Checkliste erstellt: [Live-Deployment-Checkliste (WS-7).md](Live-Deployment-Checkliste%20(WS-7).md). Deckt ab: Live-`.env` (Domains, MySQL + Legacy-DBs, Session/Cookies, `BILLING_ENFORCE_BOOKING=true`, `BILLING_OWN_VAT_ID`, Stripe-Live-Keys, Google-/OpenAI-Keys, echter Mailer), Code/Assets-Build, Migrationen (Reihenfolge), einmalige Setups (`billing:sync-stripe-plans`, `legacy:grandfather-subscriptions` für `net_cents`, `domains:generate-favicons`), Stripe-Webhook, Cache-Warmup, Scheduler-Cron + Queue-Worker, Post-Deploy-Kontrollen.
- **Aufwand:** erledigt (reine Doku).
### WS-8 — Code-Optimierung (Zweig 4)
- Nach Abschluss der Features: gezielte Optimierung (N+1 in Listen, doppelte Query-Logik, Service-Extraktion). Bewusst zuletzt, wenn der Funktionsumfang steht.
---
## 4. Empfohlene Reihenfolge
```
1. WS-1 Canonical-Tags (Quick-Win, isoliert)
2. WS-2 Magic-Link-Kontaktzugang (Fundament)
3. WS-3 Pfade B → A → E/F + Queue (Launch-Pflicht, auf WS-2)
4. WS-4 Bestandsschutz-Abrechnung (parallel möglich)
5. WS-5 Abo-Gate scharf schalten (nach WS-4)
6. WS-6 Login-Flow + E-Mail-Verifizierung + Google
7. WS-7 Deployment-Checkliste
8. WS-8 Code-Optimierung (zuletzt)
── Phase 2 (später): Wallet, C/D/G, Boost, Proof-PDF, Extra-PM, Prüfzähler ──
```
---
## 5. Anti-Zombie-Check
- ✅ Phase-2-Funktionen (Wallet-abhängig) bewusst zurückgestellt – nichts auf Vorrat gebaut.
- ✅ Legacy-Posten bleiben unangetastet – kein riskantes Tarif-Mapping bestehender Konditionen.
- ✅ Rechtlich zwingende Pfade E/F ab Launch erreichbar, kostenfrei, nie hart gesperrt.
- ✅ Vorhandene Mechanik (Abo-Gate, Bestandsschutz-Worker, Magic-Link-Basis) wird verifiziert/erweitert statt neu gebaut.
- ✅ Duplicate-Checking nicht gebaut – nur Canonical-Hygiene, wie im Decision-Update entschieden.

View file

@ -1,135 +0,0 @@
# Decision-Update: KI-Kennzeichnung (Art. 50 KI-VO)
**Status:** Entschieden (MVP-ready) **Datum:** 2026-07-08 **Betrifft:** Submit-Flow, Veröffentlichungsansicht, AGB + Veröffentlichungsrichtlinien, Audit-Log **Rechtsrahmen:** Art. 50 Abs. 4 KI-VO · Leitlinienentwurf 08.05.2026 · Code of Practice 10.06.2026 · Stichtag 02.08.2026
---
## Getroffene Entscheidungen
1. **Public-Interest-Frage wird bewusst ignoriert.** Gelabelt wird allein nach dem deklarierten KI-Grad, nicht danach, ob der Inhalt „öffentliches Interesse" berührt. Bewusste Über-Erfüllung, um das Fehlklassifikationsproblem zu eliminieren. Anti-Zombie-konform (Transparenz statt Grauzone).
2. **Selbst-Deklaration ist Pflicht in jedem Veröffentlichungsprozess.** Kein optionales Feld – ohne vollständige Deklaration kein Submit.
3. **Deklaration wird versioniert im PM-Log gespeichert** (Audit-Trail als eigentliche Abs.-50-Verteidigung).
4. **Öffentliches Label spricht in Einreicher-Stimme, nicht in Portalstimme.** Keine Behauptung einer redaktionellen Prüfung durch das Portal. Neutrales ℹ-Icon, kein grünes ✓.
5. **Keine Nicht-KI-Zertifizierung in Portalstimme.** Abwesenheit von KI wird nicht als Portal-Aussage garantiert.
---
## Eingabemaske (Submit-Flow)
**Text** — Pflichtauswahl (eine Option):
- ⭕ Ohne generative KI erstellt
- ⭕ Mit KI als Formulierungshilfe erstellt und redaktionell überarbeitet
- ⭕ Überwiegend mit generativer KI erstellt und anschließend geprüft
- ⭕ Vollständig KI-generiert (ohne wesentliche menschliche Bearbeitung)
**Bilder** — Pflichtauswahl je Bild:
- ⭕ Originalfoto
- ⭕ KI-generiertes Bild
- ⭕ KI-bearbeitetes Bild
**Verbindliche Bestätigung** (Pflicht-Checkbox, blockiert Submit):
- ☑ Ich bestätige, dass meine Angaben zum Einsatz generativer KI vollständig und wahrheitsgemäß sind.
---
## Mapping: Deklaration → öffentliches Label
### Text
|Auswahl|Label?|Öffentlicher Wortlaut|
|---|---|---|
|Ohne generative KI|Nein|_(kein Label; alternativ optional, neutral: siehe Hinweis)_|
|KI als Formulierungshilfe, redaktionell überarbeitet|Nein|_(entspricht dem Entlastungsfall; kein Pflicht-Label)_|
|Überwiegend KI-generiert, anschließend geprüft|**Ja**|ℹ Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung überwiegend mit generativer KI erstellt.|
|Vollständig KI-generiert|**Ja**|ℹ Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung vollständig mit generativer KI erstellt.|
> **Hinweis zur „Ohne KI"-Option:** Standardmäßig **kein** Label (keine Zertifizierung in Portalstimme). Falls doch ein sichtbarer Positiv-Hinweis gewünscht ist, ausschließlich attribuiert: „ℹ Nach Angaben des Einreichers ohne generative KI erstellt." — nie als Portal-Aussage.
### Bilder
|Auswahl|Label?|Öffentlicher Wortlaut|
|---|---|---|
|Originalfoto|Nein|_(kein Label)_|
|KI-generiertes Bild|**Ja**|Bildhinweis: KI-generierte Illustration.|
|KI-bearbeitetes Bild|**Ja**|Bildhinweis: KI-bearbeitetes Bild.|
> **Bewusste Über-Erfüllung:** Der gesetzliche Deepfake-Trigger greift nur bei fotorealistischer Darstellung, die für echt gehalten werden könnte. Wir labeln pauschal jedes deklarierte KI-Bild (auch stilisiert/abstrakt), weil das für ein Presseportal markenkonform und einfacher ist. Kein Präzisionsanspruch — bewusste Vereinfachung.
**Label-Optik:** Neutrales ℹ-Icon (kein grünes ✓, das als Freigabe/Prüfung gelesen würde). Textlabel am Anfang des Inhalts, Bildhinweis direkt am jeweiligen Bild. Optische Ausrichtung am offiziellen EU-Symbolsatz (Code of Practice, 10.06.2026).
---
## Audit-Log (pro PM-Version)
Unveränderlich an die konkrete Veröffentlichung gebunden speichern:
- gewählte Text-Option
- gewählte Bild-Option(en) je Bild
- Bestätigungshäkchen (true) + Timestamp
- User-ID / Firmenprofil-ID
- PM-Version (bei Re-Publish oder substanzieller Änderung ab 02.08.2026 neuer Trigger → neuer Log-Eintrag)
Zweck: belastbarer Nachweis „Kunde hat am Datum X Option Y deklariert und bestätigt". Ohne Persistenz ist die Klausel wertlos.
---
## Vertragliche Verankerung
Platzierung in **AGB _und_ Veröffentlichungsrichtlinien** (Single-Source, Section-Flags, per Domain gerendert). Die Gewährleistung + Sperr-/Entfernungsrecht brauchen den bindenden AGB-Anker; Richtlinien allein tragen schwächer.
**Klausel „Einsatz generativer KI":**
- Nutzer ist verpflichtet, den Einsatz generativer KI bei Erstellung oder wesentlicher Bearbeitung von Texten, Bildern oder sonstigen Inhalten wahrheitsgemäß anzugeben.
- Nutzer versichert, dass sämtliche Inhalte geltendes Recht sowie Rechte Dritter (Urheber-, Marken-, Persönlichkeits-, Datenschutzrecht) nicht verletzen.
- KI-generierte oder KI-bearbeitete Bilder sind entsprechend zu kennzeichnen. _(Der konditionale Nachsatz „sofern im Veröffentlichungsprozess vorgesehen" wird gestrichen — die Deklarationspflicht gilt unbedingt, nur der Mechanismus liegt beim Betreiber.)_
- Der Betreiber ist berechtigt, unzutreffend gekennzeichnete oder rechtswidrige Inhalte zu sperren, zu kennzeichnen oder zu entfernen sowie bei wiederholten Verstößen den Zugang einzuschränken.
---
## Offene Anwaltsfrage (nicht durch dieses Update gelöst)
**Betreiber-Rolle nach Abs. 4:** Hält die vertragliche Verantwortungszuweisung das Portal tatsächlich aus der Mit-Betreiber-Rolle heraus? Klausel = richtiger Baustein, aber „Klausel vorhanden" ≠ „Frage geklärt". Bleibt Punkt 1 der Anwaltsagenda.
Antwort:
Szenario 1 – Das klassische Presseportal (aus meiner Sicht dein Fall)
Der Nutzer:
* erstellt oder lädt die Pressemitteilung hoch,
* entscheidet über den Inhalt,
* gibt an, ob KI verwendet wurde,
* veröffentlicht im eigenen Namen.
Du als Portal:
* stellst lediglich die Plattform bereit,
* speicherst die Inhalte,
* veröffentlichst sie technisch.
Hier sehe ich gute Argumente dafür, dass ihr nicht Betreiber des KI-Systems im Sinne des AI Acts seid, sondern lediglich Hosting- bzw. Plattformanbieter. Die KI wurde vom Nutzer verwendet (z. B. ChatGPT, Claude oder Gemini), nicht von euch.
Szenario 2 – Ihr bietet eigene KI-Funktionen an
Beispielsweise:
* “Pressemitteilung automatisch schreiben”
* “Titel mit KI generieren”
* “Text verbessern”
* “SEO automatisch erstellen”
Dann wird die Sache deutlich komplizierter.
Je nachdem, wie diese Funktion ausgestaltet ist, könnt ihr hinsichtlich dieser KI-Funktion selbst als Betreiber (Deployer) auftreten oder zumindest eigene Pflichten aus dem AI Act auslösen.
---
## Revisit-Bedingungen
Dieses Update neu bewerten, wenn:
- der Code of Practice / die Kommissions-Leitlinien final im Amtsblatt konkretere Label-Vorgaben bringen,
- die Abs.-2-Frist-Verschiebung (maschinenlesbare Markierung, voraussichtlich 02.12.2026) final publiziert wird und Metadaten-Pflichten für Betreiber berührt,
- eine nationale Durchführungsregelung (DE) über Art. 50 hinausgeht,
- Betriebsdaten zeigen, dass Kunden systematisch falsch deklarieren (→ ggf. Stichproben-/Nachschärfungsprozess).

View file

@ -1,206 +0,0 @@
**Version:** Juni 2026 · **Datum:** 17.06.2026 · **Status:** Go-Live-Referenz (WS-7)
**Scope:** Eine kompakte, abarbeitbare Liste aller Commands & Live-Settings für den Go-Live – damit nichts in langen Dokumenten gesucht werden muss. Die Begründungen stehen in [Sicherheit & Deployment-Hinweise (Auth, Rollen, Verifizierung)](Sicherheit%20&%20Deployment-Hinweise%20(Auth,%20Rollen,%20Verifizierung).md) und im [Detailplan Launch-Slice](Detailplan%20Umsetzung%20Launch-Slice%20(Magic-Link,%20Compliance,%20Bestandsschutz,%20Auth).md); hier nur das Was/Womit.
> **Reihenfolge zählt:** erst Code & Assets, dann Migrationen (vor echtem Traffic auf geschützte Routen), dann einmalige Setups, zuletzt Caches + Worker/Scheduler. Befehle laufen auf dem Live-Host via Sail bzw. dem dortigen PHP – Platzhalter `php artisan …` ggf. durch `vendor/bin/sail artisan …` ersetzen.
---
## 0. Vor dem Deploy besorgen (externe Keys/Settings)
- [ ] **Stripe Live-Keys**: `STRIPE_KEY`, `STRIPE_SECRET` (Live-Modus), Webhook-Signing-Secret (`STRIPE_WEBHOOK_SECRET`, erst nach Schritt 4 final).
- [ ] **Google Cloud OAuth**: Client-ID/Secret; Redirect-URI in der Google Console **exakt** = `https://pressekonto.com/auth/google/callback` (Portal-Host!).
- [ ] **OpenAI**: `OPENAI_API_KEY` für KI-Klassifikation/Content-Score.
- [ ] **Mail-Provider** (echter Versand statt `log`): Zugangsdaten/Domain (SPF/DKIM gesetzt).
- [ ] **DNS/SSL**: alle Domains zeigen auf den Host, gültige Zertifikate (Portal + presseecho + businessportal24).
- [ ] **Umami-Webanalyse einschalten**: `UMAMI_ENABLED=true` (Snippet je Brand, sys.adametz.media; bis dahin bewusst aus — die Datenschutzerklärung Abschnitt 5 beschreibt den Einsatz bereits).
- [ ] **Domain final bestätigen:** Standing-Wert ist `pressekonto.com` (so in `.env.example`). ⚠️ `CLAUDE.md` nennt `pressekonto.de` – vor Go-Live verbindlich klären und überall konsistent setzen.
---
## 1. Live-`.env` — kritische Settings
```env
APP_ENV=production
APP_DEBUG=false
APP_URL=https://pressekonto.com
APP_PROTOCOL=https
# Sprache: Backend/Hub rendert Deutsch als Default (Schlüssel = deutscher Text);
# 'en' ist der Umschalter. Frontend nutzt zusätzlich URL-Prefix /de|/en (SetEdition).
APP_LOCALE=de
APP_FALLBACK_LOCALE=de
# Domains (ThemeServiceProvider matcht hierüber)
APP_PORTAL_NAME=pressekonto.com
APP_PORTAL_URL=https://pressekonto.com
APP_PRESSEECHO_NAME=presseecho.de
APP_PRESSEECHO_URL=https://presseecho.de
APP_BUSINESSPORTAL_NAME=businessportal24.de
APP_BUSINESSPORTAL_URL=https://businessportal24.de
# Datenbank (Prod = MySQL) – Haupt-DB + zwei Legacy-Quellen für den Import
DB_CONNECTION=mysql
DB_HOST=… DB_DATABASE=… DB_USERNAME=… DB_PASSWORD=…
DB_DATABASE_PRESSEECHO=… DB_HOST_PRESSEECHO=… DB_USERNAME_PRESSEECHO=… DB_PASSWORD_PRESSEECHO=…
DB_DATABASE_BUSINESSPORTAL=… DB_HOST_BUSINESSPORTAL=… DB_USERNAME_BUSINESSPORTAL=… DB_PASSWORD_BUSINESSPORTAL=…
# Session/Cookies über die Live-Domain absichern
SESSION_DRIVER=database
SESSION_DOMAIN=.pressekonto.com
SESSION_SECURE_COOKIE=true
SANCTUM_STATEFUL_DOMAINS=pressekonto.com
# Queue & Cache (DB-getrieben, kein Redis-Zwang)
QUEUE_CONNECTION=database
CACHE_STORE=database
# Mail – echter Versand statt log!
MAIL_MAILER=smtp
MAIL_HOST=… MAIL_PORT=… MAIL_USERNAME=… MAIL_PASSWORD=… MAIL_SCHEME=…
MAIL_FROM_ADDRESS="…@pressekonto.com"
MAIL_FROM_NAME="Pressekonto"
# Abo-/Bestandsschutz-Gate SCHARF (WS-5)
BILLING_ENFORCE_BOOKING=true
BILLING_OWN_VAT_ID=DE… # Pflicht für eVatR-Prüfung ausländischer USt-IDs
BILLING_VAT_RATE=0.19
# Stripe (Live)
STRIPE_KEY=pk_live_…
STRIPE_SECRET=sk_live_…
STRIPE_WEBHOOK_SECRET=whsec_… # aus Schritt 4
STRIPE_PRICE_SINGLE_PM=price_… # aus billing:sync-stripe-plans (Schritt 3)
# Google-Login
GOOGLE_CLIENT_ID=…
GOOGLE_CLIENT_SECRET=…
GOOGLE_REDIRECT_URI=https://pressekonto.com/auth/google/callback
# KI-Klassifikation / Content-Score
OPENAI_API_KEY=…
```
> Nach jeder `.env`-Änderung im laufenden Betrieb: `php artisan config:cache` erneuern (Schritt 5).
---
## 2. Code & Assets
```bash
composer install --no-dev --optimize-autoloader # zieht u.a. laravel/socialite
npm ci
npm run build # baut Portal- UND Web-Assets (public/build/portal + /web)
php artisan storage:link
```
---
## 3. Migrationen + einmalige Setups
```bash
php artisan migrate --force
```
Diese Phase-Migrationen MÜSSEN durchlaufen (Details im Sicherheits-Doc §7):
1. `backfill_email_verified_at_for_existing_users` — verhindert Lockout aller Bestands-User.
2. `downgrade_legacy_editor_users_to_customer` — schließt die Admin-Überberechtigung.
3. `add_oauth_provider_columns_to_users` — Google-Verknüpfung.
4. `create_legal_requests_table` — Compliance-Queue (WS-3).
5. **Credit-Ökonomie** (Decision-Update Rev. 4): `create_credit_wallets_table`, `create_credit_transactions_table` (Wallet + Ledger), `create_review_checks_table` (Prüfzähler), `create_boosts_table` (Platzierung), `create_credit_topups_table` (Stripe-Aufladung). Ohne sie schlagen Wallet-, Boost-, Nachweis- und Topup-Flows fehl.
6. `add_benefits_to_plans_table` — Tarif-Vorteile (admin-pflegbare Liste, In-App- und Preise-Seite). Nullable; ohne sie greift der Plan-Vorteils-Fallback. Vorteilstexte kommen aus dem Seeder bzw. dem Admin („Tarife & Pakete").
Einmalige Setups (idempotent, in dieser Reihenfolge):
- [ ] **Stripe Tax im LIVE-Account aktivieren** (QA-Befund 07.07.): Dashboard → Einstellungen → Steuer — Origin-Adresse (In der Lake 4, 33739 Bielefeld, DE) hinterlegen, Standard-Steuerverhalten „exklusiv", **Registrierung Deutschland (Standard)** anlegen. Alle Checkout-Sessions laufen mit `automatic_tax` — ohne aktives Stripe Tax lehnt Stripe jede Session ab (kein Checkout möglich). Im Testmode am 07.07. per API eingerichtet; im Live-Account manuell wiederholen.
```bash
# Stripe: Tarife als Netto-Produkte/Preise anlegen → liefert STRIPE_PRICE_SINGLE_PM
php artisan billing:sync-stripe-plans
# Hinweis: Credit-Pakete (Wallet-Topup) brauchen KEINE Stripe-Preis-IDs — sie
# laufen als Ad-hoc-Charge mit price_data (tax_behavior=exclusive). Preise/
# Staffeln stehen in config/credits.php (Pakete, Extra-PM-Tiers, Boost,
# Nachweis, Prüf-Overflow).
# Bestandsschutz aus dem Rechnungsarchiv neu nach user_payment_options migrieren.
# WICHTIG: auf Prod erneut laufen lassen, damit net_cents befüllt ist (vorher nur Dry-Run prüfen).
php artisan legacy:grandfather-subscriptions --dry-run
php artisan legacy:grandfather-subscriptions
# Rechnungsadressen der Bestandskunden aus dem Archiv übernehmen (Befund
# 06.07.: ohne Adresse überspringt der MAN-Rechnungslauf die Vereinbarung und
# der Bestandsschutz läuft am Periodenende aus). Läuft NACH dem Grandfathering.
# Der Report listet manuell nachzupflegende Kunden (leere/unparsebare Alt-Adressen).
php artisan legacy:import-billing-addresses --dry-run
php artisan legacy:import-billing-addresses
# Favicons für alle Domains generieren
php artisan domains:generate-favicons
# Rechtstext-Versionen (E3) + Ablehnungskategorien K1–K6 anlegen — ohne sie
# fehlen AGB-Versionsangabe/Protokoll-Referenz und die /richtlinien-Kategorien.
php artisan db:seed --class=LegalDocumentSeeder --force
php artisan db:seed --class=RejectionCategorySeeder --force
# Legacy-API-Schlüssel als Sanctum-Tokens übernehmen (bestehende
# Kunden-Integrationen bleiben gültig; erst Dry-Run prüfen). Läuft NACH dem
# Grandfathering, da die Berechtigung am aktiven Zahlungsstatus hängt.
php artisan legacy:import-api-keys --dry-run
php artisan legacy:import-api-keys
```
---
## 4. Stripe-Webhook
```bash
php artisan cashier:webhook # registriert den Webhook-Endpoint bei Stripe
```
Danach das im Stripe-Dashboard angezeigte **Signing-Secret** als `STRIPE_WEBHOOK_SECRET` in die `.env` eintragen und `config:cache` erneuern.
> Der Webhook (`ProcessStripeWebhook`) erfüllt bei `checkout.session.completed` zwei Fälle über die Session-Metadaten: `single_purchase_id` (Einzel-/Extra-PM bezahlt) und `credit_topup_id` (Wallet-Gutschrift). Beide sind idempotent (Pending-Gate bzw. `credited_at`). Ohne funktionierenden Webhook bleibt aufgeladenes Guthaben aus.
---
## 5. Caches scharf stellen
```bash
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
php artisan icons:cache
```
(oder gebündelt `php artisan optimize`). Bei jeder späteren `.env`-/Code-Änderung erneut ausführen.
---
## 6. Scheduler & Queue-Worker (Dauerbetrieb)
**Cron** (einmal pro Minute, treibt alle geplanten Tasks):
```cron
* * * * * cd /pfad/zur/app && php artisan schedule:run >> /dev/null 2>&1
```
Aktive geplante Tasks: `magic-links:purge` (täglich 03:00), `press-releases:purge-drafts` (So 04:00), `press-releases:publish-scheduled` (alle 5 min), `billing:generate-manual-invoices` (täglich 04:30), `press-releases:reset-monthly-quota` (Monats-1. 00:05).
**Queue-Worker** (Pflicht – sonst bleiben KI-Klassifikation & Content-Score liegen):
```bash
php artisan queue:work --queue=default --tries=3 --max-time=3600
```
Verarbeitet die `ShouldQueue`-Jobs `ClassifyPressRelease` & `ScorePressRelease` (DB-Queue). Über Supervisor/systemd persistent halten. Manueller Einmal-Lauf als Fallback: `php artisan classification:work`.
---
## 7. Nach dem Deploy prüfen
- [ ] **Keine Lockouts:** Stichprobe aktiver Legacy-User → Login + Panel erreichbar.
- [ ] **`editor`-Rolle bereinigt:** `Role::findByName('editor')->users()->whereNotNull('legacy_portal')->count()` == 0.
- [ ] **Admins legitim:** Die 4 verbliebenen Legacy-Admins (Alt-Gruppe 1) sind echtes Personal.
- [ ] **Bestandsschutz korrekt:** `user_payment_options` (grandfathered) hat befülltes `net_cents`; Bestandskunden sehen den Buchungs-Hinweis NICHT (WS-5).
- [ ] **Gate scharf:** Einreichen ohne Buchung → Hinweis „Noch keine Buchung" mit Link zur Buchungsseite (Entwurf bleibt erhalten, kein Datenverlust); Bestandskunde/Abo → durchgelassen.
- [ ] **Credit-Wallet:** Paket-Topup-Checkout → Webhook → Guthaben gutgeschrieben (genau einmal). Extra-PM/Boost/Nachweis aus Guthaben buchbar; bei zu wenig Guthaben Mini-Checkout-Hinweis (kein Absturz).
- [ ] **Auth-Kanäle:** Registrierung→Verifizierungsmail→Aktivierung; Magic-Link-Login; Google-Round-Trip (live mit echter https-Domain).
- [ ] **Stripe:** Test-Checkout + Webhook-Event kommt an (Cashier verarbeitet).
- [ ] **KI-Queue:** eine PM einreichen → Worker klassifiziert (kein Stau in `jobs`/`failed_jobs`).
- [ ] **Mail:** echte Zustellung (nicht `log`), Absender/Domain stimmen.

View file

@ -1,16 +0,0 @@
Bestandsschutz gilt für beide Portale Auch wenn es nur für ein Portal gebucht ist, hier machen wir in der Migration keine Unterscheidung.
Hinweis, wer für jeweils ein Portal ein Bestandsschutz hat, d.h. zwei laufende Posten, Werden diese im Bestandsschutz auch zusammen abgerechnet und sind nicht einzeln kündbar. Heißt zum Beispiel bei connektar Gibt es zwei Bestandschutz mit 1000 € netto jeweils für ein Portal diese fallen zusammen und gelten als Bestandsschutz für beide Portale. Heißt die Gesamtrechnungsumme von 2000 € netto bleibt erhalten. Das entspricht in etwa dem Agentur Abo dass es noch mal zu prüfen.
In der Ansicht muss noch mal deutlich zugeordnet werden und optisch besser erkennbar werden. Ist sehr einfach Gehalten.
Wir brauchen eine Live Deployment Liste, welche wichtigen Command und Dinge für die Live Einstellung beachtet werden müssen, damit diese nicht aus den Langen Dokumenten gesucht werden müssen.
Für den Bestandsschutz muss einmalig alle aktiven Abrechnungen in die Tabelle übernommen werden, damit wir eine Gesamtübersicht haben, was noch aktiv ist. Hier drauf läuft im Hintergrund der Worker, der täglich schaut, ob eine Rechnung erstellt werden muss für einen Bestandsschutz.
Es ist zu prüfen, dass User ohne aktives Abo oder Bestandsschutz keine Pressemitteilung schreiben dürfen.
Login und Registrierung
Zusätzlich Integration von Google Login, gegebenenfalls weitere.

View file

@ -1 +0,0 @@
Wichtig ist, dass Ereignisse an einer Pressemitteilung auch gelockt werden, d.h. so etwas, wie wenn es über die Sharing Buttons oder Aufrufe geht

View file

@ -1,134 +0,0 @@
**Version:** Juni 2026 · **Datum:** 16.06.2026 · **Status:** Umgesetzt
**Scope:** Sicherheits- und deployment-kritische Punkte aus der Auth-/Rollen-/Verifizierungs-Phase (WS-6 & WS-2). Dieses Dokument bündelt, was beim Go-Live zwingend beachtet werden muss. Feed für die spätere Live-Deployment-Checkliste (WS-7).
---
## 1. ⚠️ SICHERHEIT — Legacy-Rollen-Überberechtigung (behoben)
**Befund:** Der Legacy-Import (`app/Services/Import/UserImporter.php`) mappte die Alt-Gruppe 2 auf die neue Rolle `editor`. Die Alt-Gruppe 2 war jedoch die normale **Self-Publisher-Masse** (65.950 von 66.151 Usern) – keine Redaktions-Mitarbeiter. Im neuen Rollenmodell gewährt `editor` **Admin-Panel-Zugriff** (`User::canAccessAdmin()` prüft `hasRole(['admin','editor'])`).
**Auswirkung:** Praktisch jeder Legacy-Kunde konnte sich – insbesondere über den Magic-Link-Login – ins **komplette Admin-Panel** einloggen.
**Fix:**
- Mapping korrigiert: Alt-Gruppe 2 → `customer` (statt `editor`). `editor` wird ab jetzt ausschließlich manuell an echtes Personal vergeben.
- Daten-Migration `2026_06_16_080913_downgrade_legacy_editor_users_to_customer` stuft die bestehenden Legacy-`editor` (alle mit `legacy_portal`) auf `customer` herab. Chunked (Placeholder-Limit), nicht umkehrbar.
- Ergebnis nach Migration: `editor = 0`, `customer ≈ 65.952`.
**Deployment / Nachkontrolle:**
- Migration MUSS auf Prod laufen.
- Es bleiben **4 Legacy-Admins** (Alt-Gruppe 1) – **manuell prüfen**, ob das wirklich Personal ist.
- Kontrolle nach Deploy: `Spatie\Permission\Models\Role::findByName('editor')->users()->whereNotNull('legacy_portal')->count()` muss 0 sein.
---
## 2. ⚠️ DEPLOYMENT-KRITISCH — E-Mail-Verifizierung scharf
`User` implementiert jetzt `MustVerifyEmail`; die `verified`-Middleware auf den Panel-Routen greift damit real (war vorher wirkungslos).
**Lockout-Gefahr:** Ohne Vorsorge würden ALLE Bestands-User mit `email_verified_at = NULL` (alle 66.151, davon ~59.403 aktiv) ausgesperrt.
**Absicherung:** Migration `2026_06_15_101337_backfill_email_verified_at_for_existing_users` setzt `email_verified_at` für alle bestehenden User.
**Deployment:** Diese Migration MUSS mit dem Deploy laufen, **bevor** echter Traffic auf die geschützten Routen kommt. Neue Registrierungen (nach Deploy) bleiben absichtlich `NULL`, bis verifiziert.
**Registrierungs-Verhalten (Entscheidung 15.06.):**
- Selbst-Registrierung legt ein **inaktives, rollenloses** Konto an → Danke-/Notice-Seite → Verifizierungsmail.
- Bestätigter Link → `is_active = true` + `customer`-Rolle (`App\Listeners\ActivateUserAfterVerification`).
- **Ausnahme-Kanäle** (gelten als verifiziert, kein E-Mail-Schritt): Magic-Link, Kontaktzugang (WS-2), Google-Login (geplant).
---
## 3. VERHALTENSÄNDERUNG — Firmen-Scope für Pressemitteilungen
PM-Zugriff war hart an `user_id` (Autor) gebunden. Jetzt **additiv**: Zugriff = **Autor ODER Mitglied der zugeordneten Firma** (Owner via `owner_user_id` oder `company_user`-Pivot).
- Betroffen: `PressReleasePolicy::canManage()` (view/update/submitForReview/delete) sowie die Queries der Kunden-Komponenten (Liste/Show/Edit) und `User::accessibleCompanyIds()`.
- **Solo-Owner:** keine sichtbare Änderung (sie sind Autor ihrer PMs).
- **Firmen mit mehreren Mitgliedern:** Mitglieder sehen/bearbeiten jetzt **alle** PMs der Firma (gewollt – „gesamte Firma").
---
## 4. Anmeldung per E-Mail-Link – zusammengeführt (WS-2)
Eine einzige öffentliche Seite `/anmeldelink` (`auth.magic-link`) deckt **beide** Magic-Link-Fälle ab; das frühere Login-Modal und die separate Pressekontakt-Seite sind aufgelöst (war nicht selbsterklärend, zwei fast identische Wege). Die alte URL `/pressekontakt-zugang` leitet per Redirect auf `/anmeldelink`.
- **Enumeration-sicher** (neutrale Antwort unabhängig vom Treffer), **Honeypot** + **Rate-Limit** (pro E-Mail+IP 3/h, pro IP 15/h).
- Einheitlicher Service-Eintritt `ContactAccessService::requestMagicAccess()`. Berücksichtigt **jede** Stelle, an der eine E-Mail hinterlegt ist – an einer **Firma** (`companies.email`) **und** an einem **Pressekontakt** (`contacts.email`), **portalübergreifend** (Abfragen ohne `PortalScope`):
- **Bestehendes aktives Konto** → Login-Magic-Link (auch ohne Hinterlegung – ersetzt das alte Login-Modal).
- **Hinterlegte Firma/Kontakt(e) ohne Konto** → **lazy** angelegter, per E-Mail **de-duplizierter** `customer`-Account (aktiv, verifiziert).
- **Mehrfach hinterlegt** → **genau ein** Konto je E-Mail, dem **alle** zugehörigen Firmen und Pressekontakte zugeordnet werden – auch über Portalgrenzen hinweg.
- **Pivot-Rolle `responsible`** („Verantwortlich"): Wer seine E-Mail an einer Firma/einem Pressekontakt hinterlegt hat, erhält **Schreibzugriff** auf die gesamte Firma (Stammdaten, Pressekontakte, Pressemitteilungen inkl. Terminierung/„Kalender") – siehe `CompanyPolicy::update()`. Owner (`owner_user_id`) bleiben unangetastet; ein bestehender reiner Lese-Pivot (`member`) wird beim nächsten Login/Magic-Access auf `responsible` hochgestuft. Greift über den Login-Re-Sync auf **allen** Anmeldewegen.
- **Beides zugleich** → Konto wiederverwenden, Scope ergänzen, Link.
- **Deaktiviertes Bestandskonto / unbekannte E-Mail** → keine Aktion, gleiche neutrale Meldung.
- Versand über den **bestehenden Login-Magic-Link** (`MagicLinkGenerator` + `MagicLinkConsumeController`) – keine Schema-Änderung.
**Design-Nuance (bewusst):** Der Pressekontakt-Account wird beim **Absenden der Anfrage** angelegt (nicht erst beim Klick), um eine fragile `magic_links`-Schema-Änderung (ENUM/FK über SQLite-Tests + MySQL-Prod) zu vermeiden. Er ist „lazy" (nur für real existierende Kontakt-Mails) und ohne den per Mail versendeten Token wertlos. Die ältere Methode `requestAccess()` (nur Pressekontakte) bleibt für gezielte Aufrufe/Tests erhalten.
**Re-Sync bei jedem Login:** `App\Listeners\SyncCompanyMembershipsOnLogin` hört auf das `Login`-Event und ruft `ContactAccessService::syncMembershipsForEmail()` – greift damit bei **allen** Anmeldewegen (Magic-Link, Passwort, Google). Nachträglich (z. B. per API) hinzugekommene Firmen/Kontakte mit derselben E-Mail werden so beim nächsten Login automatisch zugeordnet, unabhängig vom Login-Weg. Rein additiv (kein Konto, keine Mail), bestehende Mitgliedschaften/Owner bleiben unangetastet.
- **Passwort-Login für Magic-Link-Accounts:** Lazy angelegte Accounts haben `password = null` und können sich (korrekt) nicht per Passwort anmelden, bis sie über **„Passwort vergessen"** eins setzen. Danach ist auch der reguläre Passwort-Login möglich.
- **Performance-Hinweis:** Der Re-Sync fragt `companies`/`contacts` per `LOWER(email)` ab (case-insensitive, SQLite-Tests + MySQL-Prod). Bei sehr großen Tabellen pro Login besser über einen lowercased/funktionalen Index auf `email` absichern (Follow-up, noch nicht angelegt).
---
## 5. Guest-Redirect rollenbewusst (403-Sackgasse behoben)
`bootstrap/app.php` → `redirectUsersTo`: eingeloggte User auf Gast-Routen (`/login`, `/register`) werden rollen-/verifizierungsbewusst geleitet (Admin → `/dashboard`, Customer → `/admin/me`, unverifiziert → Notice). Verhindert, dass ein Customer auf das feste `/dashboard` (403) läuft und festsitzt.
Ergänzend: Magic-Link-Consume nutzt einen rollensicheren Redirect **ohne** `intended()` (eine als Gast besuchte Admin-URL darf einen Customer nicht in den 403-Bereich schicken).
---
## 6. Login-/Magic-Link-Härtung (Security-Review 16.06.)
Aus einer gezielten Auth-Prüfung umgesetzt:
- **Magic-Link-Versand rate-limited** (vorher ungedrosselt): pro E-Mail+IP (3/h) und zusätzlich pro IP (15/h) – verhindert Mail-Fluten aktiver Accounts und das laufende Entwerten alter Links. Der Pressekontakt-Zugang war bereits limitiert.
- **Inaktive (aber verifizierte) User werden beim Passwort-Login zentral blockiert** (`Auth::logout()` + Fehler), analog zum Magic-Link-Consume. Schließt nur-`auth`+`verified`-Routen ohne zusätzlichen `is_active`-Check ab.
- **Rollensicherer Login-Redirect**: eine gemerkte `intended`-Admin-URL (z. B. `/admin/users`) schickt einen Customer nicht mehr in den 403 – inaktzessible Admin-Pfade fallen auf das rollengerechte Ziel zurück.
- **ContactAccess mutiert keine deaktivierten Bestands-Accounts** mehr: der `is_active`-Check läuft vor jeder Firmenzuordnung/jedem Linkversand.
- **Magic-Link-Verbrauch ist atomar** (konditionales `UPDATE … whereNull(consumed_at)`): Single-Use auch bei parallelen Requests garantiert.
**Captcha (dokumentiert, vorerst NICHT eingebaut):** Beim Pressekontakt-Zugang greifen aktuell Honeypot + Rate-Limit. Ein echtes Captcha (Cloudflare Turnstile / hCaptcha) ist sinnvoll, wird aber erst eingebaut, falls im Livebetrieb zu viel Missbrauch auftritt (Entscheidung 16.06.) – braucht Provider-Entscheidung, Keys und ein Paket.
## 6b. 2FA-Bypass behoben & Login-Pfade konsolidiert (Review 16.06., Teil 2)
**Befund:** Der sichtbare Volt-Login machte direkt `Auth::attempt()` und umging damit Fortifys 2FA-Pipeline (`RedirectIfTwoFactorAuthenticatable`) – für Accounts mit aktivem 2FA faktisch ein **2FA-Bypass**. Zusätzlich existierte der Fortify-POST `/login` parallel mit schwächeren Post-Login-Regeln (kein `is_active`-Block, `resolveTarget` neutralisierte nur `/` und `/dashboard`).
**Fix (Volt-nativ):**
- Der Volt-Login prüft die Zugangsdaten jetzt OHNE sofortiges Einloggen; bei aktivem 2FA (`hasEnabledTwoFactorAuthentication()`) wird der Session-Vertrag `login.id`/`login.remember` gesetzt und auf eine neue **Volt-2FA-Challenge-Seite** (`/two-factor-challenge`) geleitet. Diese postet an Fortifys bestehenden `two-factor-challenge`-Controller (TOTP + Recovery-Code).
- Gemeinsame Post-Login-Logik in `App\Support\LoginRedirect` (rollengerechtes Home + 403-sicherer intended-Redirect), genutzt von Volt-Login UND der Fortify-Response.
- `RoleAwareLoginResponse` implementiert jetzt **beide** Contracts (`LoginResponse` **und** `TwoFactorLoginResponse`) und erzwingt einheitlich: unverifiziert → Notice, verifiziert-inaktiv → Logout+Fehler, sonst 403-sicherer Redirect. Damit ist auch der direkte Fortify-POST-Pfad gehärtet.
**Hinweis:** `fortify.views => false` bleibt; die Challenge wird vom Volt-Frontend bereitgestellt, die Verifizierung übernimmt Fortify.
**Nachschärfung (Review 16.06., Teil 3):** Der `wantsJson()`-Kurzschluss in `RoleAwareLoginResponse` lief zuvor VOR den Sicherheitschecks – ein XHR/JSON-Login eines verifiziert-inaktiven Accounts erhielt so eine Session ohne Logout. Jetzt laufen die Prüfungen zuerst: verifiziert-inaktiv → `Auth::logout()` + Session-Invalidate + **403** (JSON) bzw. Login mit Fehler (HTML); unverifiziert → 204 (JSON) bzw. Notice (HTML). Erst danach der 204-/Redirect-Erfolgsfall.
---
## 6c. Google-Login (Socialite, WS-6)
`/auth/google/redirect` → Google, `/auth/google/callback` → `GoogleController`. Auflösung in `App\Services\Auth\SocialAuthService`:
- **De-Dup über E-Mail:** bestehende E-Mail wird wiederverwendet (keine Dubletten), nur `oauth_provider`/`oauth_provider_id` werden verknüpft.
- **Neuer User:** aktiv + verifiziert + `customer` (Verifizierung gilt über den Google-Kanal als erfüllt).
- **Offener Selbst-Registrierer (unverifiziert):** wird über Google onboardet (verifiziert + aktiv + customer).
- **Deaktivierter, verifizierter Account:** wird **nicht** reaktiviert → Login blockiert.
- Abschluss über dieselbe `LoginRedirect`-Logik (rollengerecht, 403-sicher).
**Deployment-Voraussetzungen:**
- `composer install` (neue Abhängigkeit `laravel/socialite`).
- Migration `add_oauth_provider_columns_to_users` (siehe unten).
- ENV: `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REDIRECT_URI`. Die Redirect-URI muss in der Google Cloud Console **exakt** der Callback-URL der **Portal-Domain** entsprechen – Prod: `https://pressekonto.com/auth/google/callback` (der Login läuft auf dem Portal-Host, nicht zwingend `APP_URL`).
- **Lokales Testen:** Google akzeptiert keine `.test`-Domains als Redirect-URI (nur `localhost`/`127.0.0.1` oder echte https-Domains). Der echte Google-Round-Trip wird daher auf **Staging/Live** mit der echten https-Domain getestet; lokal ist die Login-Logik über `tests/Feature/Auth/GoogleLoginTest.php` (gemockter Socialite-Provider) abgesichert. Wer lokal end-to-end testen will: gesamten Flow auf `http://localhost` legen (Portal-Host temporär auf localhost) oder einen https-Tunnel als Portal-Host verwenden – sonst bricht die host-gebundene Session beim Wechsel `.test`→`localhost`.
## 7. Deployment-Reihenfolge (Migrationen dieser Phase)
1. `2026_06_15_101337_backfill_email_verified_at_for_existing_users` — verhindert Lockout.
2. `2026_06_16_080913_downgrade_legacy_editor_users_to_customer` — schließt die Admin-Überberechtigung.
3. `2026_06_16_103238_add_oauth_provider_columns_to_users` — Provider-Verknüpfung für Google-Login.
4. `2026_06_16_134458_create_legal_requests_table` — Queue für Recht & Compliance (WS-3: DSGVO/Persönlichkeitsrecht/Meldungen).
**Nach Deploy prüfen:**
- `editor`-Rolle hat keine Legacy-User mehr.
- Niemand ungewollt ausgesperrt (Stichprobe aktiver Legacy-User → Login/Panel erreichbar).
- Die 4 verbliebenen Legacy-Admins sind legitimes Personal.