diff --git a/docs/weiteres/Decision-Update Phase-2-Funktionen & Magic-Link-Änderungsprozess.md b/docs/weiteres/Decision-Update Phase-2-Funktionen & Magic-Link-Änderungsprozess.md new file mode 100644 index 0000000..f06ed10 --- /dev/null +++ b/docs/weiteres/Decision-Update Phase-2-Funktionen & Magic-Link-Änderungsprozess.md @@ -0,0 +1,159 @@ + + +**Version:** Juni 2026 **Datum:** 11.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). + +--- + +## 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. + +--- + +## 2. Launch-Funktionen + +### 2.1 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 +- **Dauer:** Featured-Zeitraum in Stufen (z. B. 7 / 14 / 30 Tage), Preis gestaffelt +- **Bezahlung:** über die Credit-Wallet (kleine Wallet ist Launch-Bestandteil) + +### 2.2 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 +- **Bezahlung:** kleiner Credit-Betrag, keine KI nötig +- **Generierung:** on-demand aus vorhandenen PM-Daten + +--- + +## 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|Kosten|Phase|Public Hint| +|---|---|---|---|---| +|**A**|Tippfehler/Grammatik|kostenfrei|Launch*|nein| +|**B**|Pressekontakt-Daten ändern|kostenfrei|Launch*|nein| +|**C**|Inhaltliche Korrektur (sachlicher Fehler)|kostenpflichtig|**Phase 2**|ja| +|**D**|Update/Ergänzung (neue Information)|kostenpflichtig|**Phase 2**|ja| +|**E**|DSGVO-Anonymisierung|kostenfrei|**Launch (Pflicht)**|nein| +|**F**|Persönlichkeitsrechtsverletzung|kostenfrei|**Launch (Pflicht)**|je nach Outcome| +|**G**|Depublizieren|kostenpflichtig + Bedenkzeit|**Phase 2**|Tombstone| + +* A/B sind kostenfrei und credit-unabhängig – technisch Launch-fähig, aber kein Muss (siehe offene Punkte). + +**Pfad-Details (Kurzfassung):** + +- **A Tippfehler:** Inline-Editor mit Diff; KI prüft, ob nur kosmetisch → ja: übernommen ohne Hinweis; nein: Umleitung zu C +- **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); KI-Plausibilitätscheck; sofort umgesetzt +- **F Persönlichkeitsrecht:** Pflichtfelder (betroffene Stelle, Art, Begründung, Belege) → Review-Queue mit KI-Vorklassifikation; Outcomes: Anonymisierung, Anpassung, Tombstone, Ablehnung mit Begründung +- **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. + +### 3.4 Missbrauchsschutz, Edge Cases & Standard-Antwort + +- **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| +|D|Update/Ergänzung|≈ 4 Credits| +|G|Depublizieren|≈ 19–29 Credits + 24–48 h Bedenkzeit| + +Depublizieren bewusst am teuersten und mit Bedenkzeit, weil irreversibelste Aktion. + +--- + +## 5. Anti-Zombie-Check (dieser Stand) + +- ✅ Gesetzliche Anfragen (E/F) immer kostenfrei und ab Launch erreichbar +- ✅ Kosten nur bei echtem Mehraufwand (Korrektur, Update, Depublizierung), nicht bei Pflicht-Rechten +- ✅ 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 + +--- + +## 6. Offene Punkte + +- **A/B im Launch?** Kostenfrei und credit-unabhängig → könnten den Mail-Aufwand sofort senken. Entscheidung: A/B mit in den Launch nehmen oder kompletten Änderungs-Wizard (inkl. A/B) erst in Phase 2, zum Launch nur E/F als Pflicht-Minimum. +- **Kosten-Anker C/D/G** final bestätigen, sobald das Credit-/Prüf-System gebaut wird. +- **Boost-Preisstaffel** (7/14/30 Tage) in Credits festlegen. +- **PDF-Preis** in Credits festlegen. \ No newline at end of file diff --git a/resources/views/livewire/components/press-release-images-manager.blade.php b/resources/views/livewire/components/press-release-images-manager.blade.php index a94b90b..2504c24 100644 --- a/resources/views/livewire/components/press-release-images-manager.blade.php +++ b/resources/views/livewire/components/press-release-images-manager.blade.php @@ -59,6 +59,17 @@ new class extends Component { $this->isUploadFormOpen = true; } + /** + * Beim Wechsel des Lizenztyps das Detail-Feld leeren — sonst klebt + * z. B. der zuvor gewählte CC-Wert (cc_by) im Freitextfeld + * „Lizenzdetails / Begründung" von „Sonstiges". + */ + public function updatedNewLicenseType(): void + { + $this->newLicenseDetail = ''; + $this->resetErrorBag('newLicenseDetail'); + } + public function closeUploadForm(): void { $this->resetUploadForm(); @@ -92,7 +103,7 @@ new class extends Component { [ 'newImage' => ['required', 'image', 'mimes:jpeg,jpg,png,webp', 'max:' . (int) (ImageService::MAX_PRESS_RELEASE_IMAGE_BYTES / 1024)], 'newTitle' => ['nullable', 'string', 'max:120'], - 'newCopyright' => ['nullable', 'string', 'max:255'], + 'newCopyright' => ['required', 'string', 'max:255'], 'newAuthor' => ['required', 'string', 'max:255'], 'newLicenseType' => ['required', Rule::enum(ImageLicenseType::class)], 'newLicenseDetail' => [$requiresLicenseDetail ? 'required' : 'nullable', 'string', 'max:120'], @@ -104,6 +115,7 @@ new class extends Component { 'newRightsConfirmed' => ['accepted'], ], [ + 'newCopyright.required' => __('Bitte einen öffentlichen Bildnachweis angeben, z. B. Foto: Max Mustermann / Beispiel GmbH.'), 'newAuthor.required' => __('Bitte Urheber, Fotograf oder Rechteinhaber angeben.'), 'newLicenseType.required' => __('Bitte einen Lizenztyp wählen.'), 'newLicenseDetail.required' => __('Bitte die Lizenz genauer angeben.'), @@ -355,169 +367,190 @@ new class extends Component { @else -
- {{ __('Titelbild hochladen') }} + + {{ __('Titelbild hochladen') }} - {{-- ===== Schritt 1 · Bild auswählen ===== --}} -
-
- 1 - {{ __('Bild auswählen') }} -
- - @if (! $newImage) - - - - -
- {{ __('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.') }} + {{-- ===== Schritt 1 · Bild auswählen ===== --}} +
+
+ 1 + {{ __('Bild auswählen') }}
- @else - {{-- Große Vorschau im Titelbild-Format, damit das Motiv + + @if (!$newImage) + + + + +
+ {{ __('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.') }} +
+ @else + {{-- Große Vorschau im Titelbild-Format, damit das Motiv vor dem Hochladen wirklich beurteilbar ist. --}} -
-
- @if ($this->newImagePreviewUrl()) - {{ __('Vorschau des gewählten Titelbilds') }} - @else -
- -
- @endif -
-
-
- {{ $newImage->getClientOriginalName() }} - · - {{ number_format($newImage->getSize() / 1048576, 2, ',', '.') }} MB +
+
+ @if ($this->newImagePreviewUrl()) + {{ __('Vorschau des gewählten Titelbilds') }} + @else +
+ +
+ @endif +
+
+
+ {{ $newImage->getClientOriginalName() }} + · + {{ number_format($newImage->getSize() / 1048576, 2, ',', '.') }} MB +
+ + {{ __('Anderes Bild wählen') }} +
- - {{ __('Anderes Bild wählen') }} -
+ @endif + +
+ + {{-- ===== Schritt 2 · Öffentliche Bildinfos ===== --}} +
+
+ 2 + {{ __('Bildinformationen (öffentlich sichtbar)') }}
- @endif - -
- - {{-- ===== Schritt 2 · Öffentliche Bildinfos ===== --}} -
-
- 2 - {{ __('Bildinformationen (öffentlich sichtbar)') }} -
-
- - -
-
- - {{-- ===== Schritt 3 · Herkunft & Lizenz ===== --}} -
-
- 3 - {{ __('Herkunft & Lizenz') }} -
-
- - - {{ __('Bitte wählen…') }} - @foreach ($licenseTypeOptions as $value => $label) - {{ $label }} - @endforeach - -
- - @if ($newLicenseType === \App\Enums\ImageLicenseType::CreativeCommons->value) - - {{ __('Bitte wählen…') }} - @foreach ($ccLicenseOptions as $value => $label) - {{ $label }} - @endforeach - - -
- {{ __('Creative-Commons-Lizenzen können Einschränkungen enthalten. Bitte prüfen Sie, ob kommerzielle Nutzung, Bearbeitung und Veröffentlichung als Titelbild erlaubt sind.') }} +
+ +
- @elseif($licenseDetailRequired) - - @endif +
-
- - -
-
- - {{-- ===== Schritt 4 · Personen & Rechte Dritter ===== --}} -
-
- 4 - {{ __('Personen & Rechte Dritter') }} -
-
- - @foreach ($peopleRightsOptions as $value => $label) - - @endforeach - - - - @foreach ($propertyRightsOptions as $value => $label) - - @endforeach - -
- - @if (in_array($newPeopleRightsStatus, ['consent', 'public_event'], true)) -
- {{ __('Bei erkennbaren Personen können zusätzlich Persönlichkeits- oder Datenschutzrechte betroffen sein. Bitte stellen Sie sicher, dass die Veröffentlichung zulässig ist.') }} + {{-- ===== Schritt 3 · Herkunft & Lizenz ===== --}} +
+
+ 3 + {{ __('Herkunft & Lizenz') }}
- @endif - - @if ($showsRightsWarning) -
- {{ __('Diese Auswahl kann Einschränkungen enthalten. Bitte laden Sie das Bild nur hoch, wenn die Nutzung, Bearbeitung und Veröffentlichung als Titelbild erlaubt ist.') }} +
+ + + {{ __('Bitte wählen…') }} + @foreach ($licenseTypeOptions as $value => $label) + {{ $label }} + @endforeach +
- @endif -
- {{-- ===== Schritt 5 · Bestätigung ===== --}} -
-
- 5 - {{ __('Bestätigung') }} -
+ @if ($newLicenseType === \App\Enums\ImageLicenseType::CreativeCommons->value) + + {{ __('Bitte wählen…') }} + @foreach ($ccLicenseOptions as $value => $label) + {{ $label }} + @endforeach + - +
+ {{ __('Creative-Commons-Lizenzen können Einschränkungen enthalten. Bitte prüfen Sie, ob kommerzielle Nutzung, Bearbeitung und Veröffentlichung als Titelbild erlaubt sind.') }} +
+ @elseif($licenseDetailRequired) + + @endif - +
+ + +
+
-
- {{ __('Abbrechen') }} - {{ __('Hochladen') }} -
-
-
+ {{-- ===== Schritt 4 · Personen & Rechte Dritter ===== --}} +
+
+ 4 + {{ __('Personen & Rechte Dritter') }} +
+
+ + @foreach ($peopleRightsOptions as $value => $label) + + @endforeach + + + + @foreach ($propertyRightsOptions as $value => $label) + + @endforeach + +
+ + @if (in_array($newPeopleRightsStatus, ['consent', 'public_event'], true)) +
+ {{ __('Bei erkennbaren Personen können zusätzlich Persönlichkeits- oder Datenschutzrechte betroffen sein. Bitte stellen Sie sicher, dass die Veröffentlichung zulässig ist.') }} +
+ @endif + + @if ($showsRightsWarning) +
+ {{ __('Diese Auswahl kann Einschränkungen enthalten. Bitte laden Sie das Bild nur hoch, wenn die Nutzung, Bearbeitung und Veröffentlichung als Titelbild erlaubt ist.') }} +
+ @endif +
+ + {{-- ===== Schritt 5 · Bestätigung ===== --}} +
+
+ 5 + {{ __('Bestätigung') }} +
+ + + + + +
+ + {{ __('Abbrechen') }} + {{ __('Hochladen') }} + +
+
+ @endif @else
diff --git a/tests/Feature/PressReleaseImageLicenseTest.php b/tests/Feature/PressReleaseImageLicenseTest.php index 8e93008..4f5647f 100644 --- a/tests/Feature/PressReleaseImageLicenseTest.php +++ b/tests/Feature/PressReleaseImageLicenseTest.php @@ -226,6 +226,7 @@ test('valid cc upload stores license detail and license url', function () { LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id]) ->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800)) ->set('newAuthor', 'Jane Doe') + ->set('newCopyright', 'Foto: Jane Doe (CC BY 4.0)') ->set('newLicenseType', ImageLicenseType::CreativeCommons->value) ->set('newLicenseDetail', 'cc_by') ->set('newLicenseUrl', 'https://creativecommons.org/licenses/by/4.0/') @@ -243,6 +244,34 @@ test('valid cc upload stores license detail and license url', function () { expect($image->license_url)->toBe('https://creativecommons.org/licenses/by/4.0/'); }); +test('the public image credit is required', function () { + /** @var TestCase $this */ + ['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner(); + $this->actingAs($owner); + + LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id]) + ->set('newImage', UploadedFile::fake()->image('foto.jpg', 1200, 800)) + ->set('newAuthor', 'Jane Doe') + ->set('newLicenseType', ImageLicenseType::Own->value) + ->set('newPeopleRightsStatus', 'none') + ->set('newPropertyRightsStatus', 'none') + ->set('newRightsConfirmed', true) + ->call('saveImage') + ->assertHasErrors(['newCopyright']); +}); + +test('switching the license type clears the stale license detail', function () { + /** @var TestCase $this */ + ['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner(); + $this->actingAs($owner); + + LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id]) + ->set('newLicenseType', ImageLicenseType::CreativeCommons->value) + ->set('newLicenseDetail', 'cc_by') + ->set('newLicenseType', ImageLicenseType::Other->value) + ->assertSet('newLicenseDetail', ''); +}); + test('existing title image hides upload form and can be removed', function () { /** @var TestCase $this */ ['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();