Ein FULLTEXT-Index zwingt MySQL bei JEDEM ADD COLUMN auf ALGORITHM=COPY. Auf
press_releases (>1 GB) bedeutet das einen vollständigen Rebuild, während dessen
kein Schreibzugriff möglich ist — im August hat genau das die Seite zum Stehen
gebracht, weil die wartenden Requests PHP-FPM-Worker und DB-Verbindungen
belegten.
Die durchsuchbaren Felder liegen jetzt gespiegelt in `press_release_search`,
die die Indizes trägt. Ihr Schema ist per Konstruktion stabil und wird nie
erweitert; neue Felder gehören nach press_releases, wo sie ohne Volltext-Index
Millisekunden kosten. Synchron hält sie ein Observer (alle Schreibzugriffe
laufen über Eloquent, auch der Importer), reparieren lässt sie sich mit
press-releases:reindex-search.
Der Umstieg ist über mehrere Migrationen geteilt und schaltet erst um, wenn
press_releases seine Indizes verloren hat — bis dahin sucht die Anwendung
unverändert weiter. Die Drop-Migration verweigert den Dienst, solange der
Suchindex unvollständig ist, und verwirft die gecachte Umschalt-Prüfung: Stünde
dort noch der alte Stand, setzte der Rückfallpfad sein MATCH() auf einen
soeben entfernten Index ab (MySQL-Fehler 1191) und jede Suche endete im 500er.
Dazu die betriebliche Absicherung:
- DB_LOCK_WAIT_TIMEOUT (Default 10 s) statt MySQLs einem Jahr. Requests
scheitern bei einer laufenden Schemaänderung nach Sekunden, statt sich
aufzustauen, bis keine Verbindung mehr frei ist.
- Sessions und Cache nach Redis, damit eine blockierte MySQL-Tabelle nicht die
gesamte Anwendung mitreißt.
- db:online-alter als Notausgang (pt-online-schema-change), falls eine der
kleinen FULLTEXT-Tabellen doch einmal groß wird. Percona-Toolkit ist
bewusst nicht Teil des Standard-Setups.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 7 (Spur D, E2 revidiert): KI-Vorprüfung VOR der Zahlung im Modal —
rot lehnt ohne Zahlung ab, gelb/grün führt zum Stripe-Checkout
(SinglePurchaseType::Change, 10 € netto), nach Zahlungseingang übernimmt
der Webhook die geprüfte Fassung sofort (applyPaid: Snapshot, Sperre,
Verlauf). Nur einmal je PM; Obergrenze legal.change_max_distance_percent
(50 %) verhindert die verdeckte Selbst-Depublizierung.
Phase 5 (Spur B): strukturiertes Schwärzungsformular mit Pflicht-Vorschau
im verifizierten Web-Bereich UND als Modal im Kunden-Panel; Admin-Editor
wird aus payload.redaction vorbefüllt. Umsetzungsmail LegalRequestExecuted
an Antragsteller, Bestätigungsmail PressReleaseChangeApplied (Spur A/D,
neuer abbestellbarer Typ press_release_corrected).
Außerdem:
- Re-Sync-Sperre + Sitemap-Invalidierung fest in depublish() verankert
(Lücke im bezahlten Webhook-Pfad geschlossen)
- Öffentliche „Rechtliches"-Seite als zweistufige Triage: primär der
Portal-Anmeldelink, Formular nur Fallback mit Identifizierungs-Hinweis;
Store auf report/dsgvo/personal_rights gehärtet
- Panel-Button „Ändern/Melden" öffnet Anliegen-Modal (4 Spuren) statt
des Web-Formulars
- Pressekontakt-Fallback für Legacy-Bestände (30k PMs ohne Verknüpfung):
Detailseite + pressContactEmail() nutzen PM-Kontakt → Firmen-Kontakt →
Firmendaten
- Korrektur-Gate entity-fest (Editor-Normalisierung zählt nicht als
Änderung), toEditorHtml() in Admin-Edit + Korrektur-Panel,
Wochenlimit-Hinweis in Text und Bremsen-Meldung
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Unterseite /admin/newsletter/abmeldungen listet Newsletter-Opt-outs mit
Zeitpunkt, Suche und Link zur User-Detailseite
- Kampagnen-Filter: Empfänger einer früheren Kampagne ausschließen
(Abgleich per User-ID und E-Mail-Snapshot) für Follow-up-Mailings
- Login-Filter kann Monats-Scheiben (von–bis) abgrenzen, um große
Listen Monat für Monat abzuarbeiten
- Versand läuft in Wellen (Standard 50 Mails / 15 Min, per .env
justierbar) gegen SMTP-Limits und Blocklisten; Hinweis im Senden-Modal
- web: Firmen-Website im Newsroom & Co. als absolute URL verlinkt
(Company::websiteUrl), statt relativ auf /newsroom/www.example.de
- kunde: API-Doku-Button öffnet die /de/api-Seite; OpenAPI-Spezifikation
als eigener, benannter Download (pressekonto-api-v1.yml)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Neuer Button in der Aktionsspalte und im Detail-Modal führt zu
admin/users/{id}; bestehende Aktions-Buttons bekommen Tooltips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Neues Panel "Guthaben & Kontingent" auf der Benutzer-Detailseite
(users:manage): Credits gehen als Grant-Buchung ins Wallet-Ledger,
freie Pressemitteilungen als bezahlte 0-€-Einzelkäufe, die das
Buchungs-Gate erfüllen und beim Veröffentlichen regulär eingelöst
werden — Stripe bleibt unberührt. Jede Gutschrift landet mit Vergeber
und Grund als neuer Typ "Gutschrift" im Aktivitäts-Verlauf.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Neuer ActivityType "Mail-Abmeldung": Der Unsubscribe-Controller
protokolliert Abbestellen (Link-Klick und RFC-8058-One-Click) sowie
das Wieder-Aktivieren — nur bei echter Zustandsänderung, wiederholte
Klicks erzeugen keine Doppel-Einträge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Neues Modul unter Admin ▸ Newsletter (nur users:manage): Kampagnen mit
Betreff + leicht formatiertem Text (Purifier-Profil newsletter: fett/
kursiv, Listen, Links) im zentralen Mail-Layout. Empfänger-Filter auf
der User-Datenbank: Portal, Legacy-Vertrag (Bestandsschutz), Login
innerhalb/außerhalb X Monaten, Sortierung nach Login/Erstellung — mit
Live-Zähler und Vorschau im Editor.
Versand friert die Empfängerliste als Protokoll ein (newsletter_
campaign_recipients) und stellt pro Empfänger einen Queue-Job ein;
der Opt-out wird im Worker erneut geprüft. Jede Mail trägt den
signierten One-Click-Abmelde-Link (neuer EmailNotificationType::
Newsletter, bestehende Unsubscribe-Infrastruktur inkl. RFC 8058);
die Abmeldung greift sofort für alle künftigen Auswahlen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Neuer Listener RecordMailActivity am MessageSent-Event erfasst zentral
alle Versandwege (Mailables, Notifications, Fortify, Queue-Mails beim
realen Versand im Worker). Neuer ActivityType "E-Mail" mit Empfänger,
Betreff und Mailable-Klasse in den Metadaten; bekannte Empfänger werden
mit ihrem User verknüpft.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Firma und Autor sind im Header und Status-Grid klickbar
(admin/companies/{id} bzw. admin/users/{id}). Das Kontakte-Panel heißt
jetzt "Firma & Pressekontakte" und zeigt die Firmendaten (Adresse,
E-Mail, Telefon, Website) über den Pressekontakten. Neu: "Benutzer mit
Zugriff (über Firma)" listet alle Firmen-Mitglieder mit Rolle und
Autor-Markierung — mehrere User hängen nur über die Firma an einer PM,
Magic-Links sind reine Login-Tokens.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chronologisches Protokoll der Systemereignisse als neue Admin-Seite
(/admin/reports/activity) mit Filter nach Ereignistyp, Zeitraum und
Suche. Erfasst ab jetzt fortlaufend:
- Registrierungen inkl. Weg (Formular, Google, Magic-Link)
- Anmeldungen inkl. Weg (Passwort, Google, Magic-Link, Impersonation)
- PM-Statuswechsel mit Ergebnis/Begründung (zentral im Status-Log-Hook;
KI-/Scheduler-Wechsel erscheinen als „System")
- Zahlungseingänge (Rechnungsspiegelung) und Credit-Aufladungen
- Formular-Eingänge: Kontakt, Rechtsanfrage, Widerruf, Kündigung,
Newsletter-Anmeldung (Observer auf den Modellen)
Schreiben ist Best-Effort über den ActivityLog-Service — ein
fehlgeschlagener Verlaufseintrag bricht nie den fachlichen Ablauf.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Neue Reports-Seite (settings:manage) zeigt den Queue-Zustand (ältester
wartender Job, Worker aktiv/gestoppt, fehlgeschlagene Jobs) und
Deployment-Checks als Ampel: Permissions ausgerollt, APP_DEBUG,
Session-Secure-Cookie, ausstehende Medien-Privatisierung. Damit ist die
für Magic-Link-Login und fristrelevante Mails kritische Queue-Abhängigkeit
aus dem Admin sichtbar.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
page_url aus dem anonymen Kontaktformular wird auf url:http,https
validiert; das Admin-Backend rendert nur http(s)-Links klickbar, sonst
als Text. Verhindert javascript:-URIs, die beim Admin-Klick im
Admin-Kontext liefen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- users:manage/roles:manage auf allen User-/Rollen-Masken erzwungen;
is_super_admin aus $fillable entfernt und nur per autorisiertem
forceFill setzbar; zentraler Gate::before-Super-Admin-Kurzschluss (K1/M8).
- Nicht-Super-Admins können Super-Admin-Konten nicht mehr bearbeiten (Fix#2);
Impersonation mit Rangprüfung, keine Token-Erstellung während Impersonation (H1).
- Destruktive/verwaltende Admin-Aktionen (CRM/Billing/Compliance/Struktur)
hängen an neuen admin-only Permissions; Editor behält reine Content-Rechte (H2).
- Kanonischer Permission-Katalog (App\Authorization\Permission) als Single
Source of Truth für Seeder und Monitoring.
Deployment: `php artisan db:seed --class=RolesAndPermissionsSeeder` auf allen
Umgebungen ausführen (neue Permissions).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Publish/Reject/Archive und changeStatus erzwingen konsistent
press-releases:publish; changeStatus schloss die Lücke, über die das
Status-Modal die Prüfung umging (Fix#1/H2).
- publish()/changeStatusFromAdmin sperren PM- und User-Zeile
transaktional; QuotaExceededException wird in Admin-Publish und im
Auto-Publish-Job abgefangen (Warnung statt 500 / bleibt in Review, Fix#3/M2).
- Slug-Erzeugung nur noch über HasUniqueSlug (generateUniqueSlug +
saveWithUniqueSlugRetry), redundanter Controller-Helper entfernt (M3).
- API-/Legacy-Text wird vor der Persistenz sanitisiert (M6);
Scheduler/Legacy-Tracking ignorieren soft-gelöschte PMs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CreditTopup-Gutschrift, MAN-Rechnung, Proof-PDF und Stripe-Webhook-
Spiegelung sperren die betroffene Zeile innerhalb der Transaktion und
prüfen die Idempotenz erst danach (verhindert Doppelgutschrift bei
at-least-once-Webhooks, H4).
- Gemeinsamer LocksModelForUpdate-Trait für die häufige Lock-by-Key-Form
(CreditTopup, ManualInvoice).
- Unique-Index auf invoices.stripe_invoice_id; Boost-Inventar über
boost_slot_locks serialisiert, verwaiste Kategorie-Locks werden beim
Löschen der Rubrik entfernt.
Deployment: vor migrate prüfen, ob invoices.stripe_invoice_id bereits
Duplikate enthält (die Unique-Migration bricht sonst bewusst ab).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Submit-Modal auf zwei Auswahloptionen reduziert (kein Label vs.
"Ganz oder ueberwiegend mit generativer KI erstellt"); alle vier
Enum-Werte bleiben fuer API/Bestand gueltig und mappen per formOption()
auf die zwei Gruppen. Oeffentliche Detailseite rendert jetzt Text-Label
("KI-generiert: Nach Angaben ...") und Bild-Badge (KI-generiert/
KI-bearbeitet, abgedunkelter Hintergrund) — fehlte dort bisher komplett.
Bildnachweis-Vorschlag ohne "Foto:"-Praefix (stand sonst doppelt).
Einreich-Toast + FAQ: automatische Pruefung in Minuten statt 24 h.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Selbstdeklaration des KI-Einsatzes je PM (Text) und je Bild inkl.
Submit-Gate im PressReleaseService, versioniertem Audit-Trail
(press_release_ai_declarations) und öffentlichem KI-Label in
Einreicher-Stimme. Bestätigung im Submit-Modal ist Pflicht.
Co-authored-by: Cursor <cursoragent@cursor.com>
A5 des Umbaus Agentur-Attribution → User-Profil: Kunden-Profil erhält
den Abschnitt „Agentur-Angaben" (Feld Agentur-/Verteilername, öffentlich
als Übermittler sichtbar). Firma-Anlegen (Kunde + Admin) und
Admin-Firma-Bearbeiten kommen ohne Typ-Feld aus — Neuanlagen sind immer
type=company. Firmen-Übersicht zeigt kein Typ-Label mehr. Admin-User-
Formulare pflegen agency_name; die Detailseite zeigt den Übermittler
als Badge. CompanyFactory erzeugt keine zufälligen Agentur-Typen mehr.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Frontend-Umbau (Kevin, 06.07.2026), Ergebnis des businessportal24-Audits:
- Fake-Anzeigen und Mock-/Fallback-Daten entfernt (focus-ad, feed-ad,
highlight-card, press-release-card, featured-releases, press-releases-grid);
Sektionen rendern nur noch mit echten Daten oder gar nicht
- Newsletter-Strip und Newsletter-Box zurueckgebaut (Feature nachgelagert,
vor Reaktivierung DOI-Flow noetig)
- Echte Boersendaten: MarketDataService (Yahoo-Finance-Quotes, config/market.php,
Cache) + market:refresh alle 15 Min. im Scheduler; live-ticker blendet sich
ohne Daten aus
- EN-Lokalisierung der Kern-Seiten via lang/de/web.php + lang/en/web.php;
Carbon-Locale folgt app()->getLocale()
- Web-Tests auf den neuen Frontend-Stand nachgezogen (Suite: 993 gruen)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Zwei eigenstaendige Formulare unter /melden/rechtswidrig (DSA Art. 16:
URL, Name, E-Mail, Begruendung, Wahrheitsversicherung) und
/melden/kontaktdaten (DSGVO-Antrag) auf allen drei Domains. Gemeldete
URLs werden automatisch der passenden veroeffentlichten PM zugeordnet;
ohne Treffer bleibt die URL im payload. Jede Meldung erhaelt eine
Vorgangsnummer (M-{id}, analog K-{id} der Kuendigung), die meldende
Person unverzueglich eine Eingangsbestaetigung (DSA Art. 16 Abs. 4) und
das Melde-Postfach einen internen Hinweis — auch im bestehenden
Per-PM-Flow. Admin-Queue zeigt Vorgangsnummer, Name und gemeldete URL;
/melden verlinkt die neuen Formulare als ersten Weg.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Security-Header-Middleware (nosniff, X-Frame-Options, Referrer-/
Permissions-Policy) global, proxy-unabhaengig; CSP bewusst separat.
- Firmenlogo-Upload im Create-Formular durch den ImageService-Pfad
ersetzt (Mime-Whitelist ohne SVG, Re-Encode, UUID-Name) - vorher
rohe store()-Ablage ohne Server-Validierung (SVG/XSS moeglich).
- Rate-Limits ergaenzt: Passwort-Reset (3/E-Mail + 15/IP pro Std.),
Registrierung (10/IP pro Std.), throttle auf magic-login- und
pm-vorschau-Consume-Routen (Defense-in-Depth).
- EnsureUserIsCustomer als persistente Livewire-Middleware
(symmetrisch zum Admin-Gate, greift auch bei Folge-Requests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Oeffentliche Bestaetigungsseite /de/kuendigung ohne Login: Art der
Kuendigung (Grund nur bei ausserordentlich), Zuordnungsdaten,
Beendigungszeitpunkt; Honeypot + Rate-Limit wie beim Widerruf.
Sofortige automatische Zugangsbestaetigung per E-Mail (Par. 312k
Abs. 4 BGB) + Betreiber-Mail. Kuendigungen laufen in der
Recht-&-Compliance-Queue unter den Widerrufen auf (K-Vorgaenge,
eigener Filter, Erledigt/Wieder-oeffnen, vierte KPI-Karte).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eigene Widerrufe-Sektion unter der Anfragen-Tabelle in
/admin/legal-requests: Vorgangsnummer, Zuordnungsdaten, Statusfilter
(offen/alle/erledigt, eigener Paginator), Erledigt/Wieder-oeffnen-Aktion
mit resolved_at; KPI-Karte "Offene Widerrufe". Dazu
WithdrawalRequest-Factory (+resolved-State) fuer die Tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bisher wurde der NullNewsletterSyncClient unbedingt gebunden -
NEWSLETTER_SYNC_ENABLED=true mit echtem Provider haette stumm nichts
getan. Jetzt: Platzhalter-Provider -> No-Op-Client, aktivierter aber
nicht implementierter Provider -> RuntimeException (fail fast). Das
Admin-UI weist beim Test-Sync auf den Platzhalter hin.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
role- und billing-Feldgrids aus create+edit in <x-portal.user-role-fields> und <x-portal.user-billing-fields> gezogen (nur das innere Grid; Panel-/Card-Wrapper bleiben pro Seite). Nebenbei-Bugfix: create zeigt jetzt den billing.name-Validierungsfehler (Feld wurde via userBillingRules() schon validiert, Fehler aber nie gerendert). company-link-picker + contact-fields bleiben bewusst inline (divergiert bzw. nur in edit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Status-Badge-Farblogik (ok/warn/err/muted) aus 7 divergenten match-Bloecken in eine Enum-Methode gezogen. Archiviert/Entwurf jetzt konsistent 'muted' (grau) auch auf Detail-/Edit-/Log-Seiten (vorher dort 'hub'-Akzent). Toter statusColor-Block (Flux-Farben, nie referenziert) in customer/show entfernt. flux:badge :color in admin/users bleibt bewusst separat (eigenes Farbvokabular, Einzel-Call-Site).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Analyse der beiden ~40/160-Zeilen-save()-Methoden (users/create vs edit): sie
teilen nur den User-Attribut-Payload und den byte-identischen Firmen-Sync-Loop
(Bearbeiten hat zusaetzlich Profil/Kontakte/volle Billing-CRUD). Ein voller
UserUpsertService waere Over-Engineering (ueberwiegend Branches) — daher nur
der wirklich gemeinsame Persistenz-Kern:
User::syncCompaniesWithRoles(array $companyIds, array $roles) baut das
Pivot-Payload (fehlende Rolle => 'member') und synct. Beide Formulare nutzen
es; im Bearbeiten kommen die linkedCompanyIds nun direkt aus $validated.
Test: UserSyncCompaniesTest (Rollen-Zuordnung, member-Default, Detach).
29 User-Tests gruen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Zweite Struktur-Component: buendelt den empty-stage-Block (runder Icon-Rahmen
+ Titel + Subtext + optionale Aktionen). Props: title, subtitle, variant
(default|warm|err); Slots :icon und :actions.
Erst-Umstellung: die zwei Leerzustaende der Admin-PM-Liste (gefiltert = warm
mit 'Filter zuruecksetzen', leer = Standard mit 'Neue PM anlegen'). Weitere
Vorkommen (customer/press-releases, customer/press-kits) folgen nach Sicht-
pruefung. 36 Tests gruen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Neun weitere Standard-Header (md) auf <x-portal.page-header>:
- roles/index (bedingter Anlegen-Button), roles/create (Guard-Badge via
:meta), roles/edit (ID + Guard + bedingte Systemrolle via :meta)
- footer-codes/index, footer-codes/create, footer-codes/edit (ID + Global +
Aktiv/Inaktiv via :meta, Titel als Beschreibung)
- presets/index, presets/create, presets/edit (ID via :meta)
Roles-/FooterCode-/Preset-Tests (23) gruen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sieben weitere Header auf <x-portal.page-header>:
- users (index), users/create, contacts/index, contacts/create (Standard;
contacts/create mit bedingtem 'Firma vorausgewaehlt'-Badge via :meta)
- users/show, users/edit (ID + Portal + Aktiv/Inaktiv-Badges via :meta,
dynamischer Titel/Beschreibung aus User-Daten)
- contacts/edit (ID + Portal-Label via :meta, ohne Beschreibung)
Component: break-words am H1 ergaenzt (fuer lange Namen/E-Mails in show/edit;
bei kurzen Titeln ein No-op). User-/Contact-Tests (129) gruen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Erste Seiten-Struktur-Component fuer das Admin-Backend: buendelt den 35-fach
kopierten page-header-Block (Hub-Badge + Eyebrow + H1 + Beschreibung, dazu
Slots :meta und :actions fuer die variablen Teile). Muster wie <x-portal.
stat-card>.
Bewusst nur EINE Seite (categories/create) umgestellt, damit die Optik
einzeln im Browser gegengeprueft werden kann, bevor die restlichen 34 Header
folgen. AdminCategoryManagement-Tests gruen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In companies/index, companies/show und contacts/index war die private
Methode portalBadgeColor() definiert, aber nirgends aufgerufen (kein
Aufruf im Blade-Markup oder PHP). Entfernt; in companies/show wird damit
auch der Portal-Import ungenutzt und faellt weg.
Klaert den im Wiedereinstieg-Plan offenen Punkt: keine Portal::badgeColor()-
Zentralisierung noetig, da es keine echten Call-Sites gab.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Name aus der locale-gefiltert geladenen Uebersetzung mit #id-Fallback als
Model-Methode. Ersetzt die identische Inline-Logik an 4 Stellen (Footer-Codes
und Kategorien jeweils create/edit).
Test: CategoryLocalizedNameTest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Anzeigename eines Kontakts (Vor-/Nachname mit Fallback) als Model-Methode.
Ersetzt die identische Inline-Logik an 6 eindeutigen Relation-$contact-Stellen
(Admin: Benutzer-/PM-/Firmen-Detail; Kunde: Press-Kit-/PM-Detail).
Bewusst noch offen: Select-/toBase-Varianten ($contactOption/$contactResult/
$lookupContact, stdClass-Risiko) sowie web/release-detail (User-Frontend).
Test: ContactDisplayNameTest.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Die zwischen Benutzer-Anlegen und -Bearbeiten identischen Validierungsbloecke
liegen jetzt in ProvidesUserFormRules: userIdentityRules(?int $ignore) (E-Mail-
Unique ignoriert beim Bearbeiten den eigenen Datensatz), userRolesAndCompaniesRules()
und userBillingRules(). Beide Formulare nutzen die Array-Spreads; die nur im
Bearbeiten genutzten Profil-/Kontakt-Regeln bleiben dort inline. Ungenutzten
Rule-Import aus dem Anlegen-Formular entfernt.
Offen (mit User abzustimmen, UI-/Divergenz-lastig): gemeinsamer Save-Service und
die Blade-Feld-Components.
31 User-Tests gruen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Den in allen vier PM-Editoren identischen, rein praesentationalen
Pre-Submit-Checklisten-Block (~34 Zeilen je Datei) in eine
Blade-Component ausgelagert, die $this->presubmitChecks als Prop nimmt.
Render-Test ergaenzt.
Bewusst konservativ: Nur dieser wire:model-freie, presentational Block
wurde extrahiert. Die interaktiven Markup-Panels (Tag-Input, Publish-
Schedule, Kontakt-/Firmen-Select) bleiben in den Editoren, da ihre
wire:model-/Alpine-Bindungen ohne visuelle QA nicht risikofrei in
Components zu ueberfuehren sind und die Tests gerendertes Markup nur
begrenzt abdecken.
Tests: volle Suite gruen (800 passed, 1 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Den ~15x duplizierten Admin-Lookup PressRelease::withoutGlobalScopes()
->findOrFail($id) in einen findAdminPR()-Helfer (Concern) zentralisiert;
index/edit/show nutzen ihn. Die eager-load-Query in show::with() bleibt
unberuehrt (anderer Zweck).
Bewusst NICHT vereinheitlicht: das Status-Badge-Mapping divergiert real
(index: archived/draft -> muted; edit/show: -> hub via default) -> waere
eine Design-Entscheidung ueber kanonische Badge-Farben, keine mechanische
Dedup.
Tests: volle Suite gruen (799 passed, 1 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Die in admin/create und admin/edit identische Inline-Firmensuche
(Volltext-oder-LIKE) inkl. des 2x duplizierten supportsFullTextSearch()-
Treiберchecks in einen Company::scopeSearchByTerm() zentralisiert; beide
Editoren nutzen ihn jetzt (Verhalten unveraendert). Ungenutzte DB-Importe
entfernt. Customer nutzt bereits CustomerCompanyContext (eigene
join-/user-gescopte Query, daher nicht zusammengelegt).
Tests: neuer Scope-Test; volle Suite gruen (799 passed, 1 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Den 3x identischen contentChanged->fresh->reclassifyIfClassified->
rescoreIfScored-Block (admin/edit 1x, customer/edit 2x) in
PressReleaseService::reprocessAfterContentChange() zentralisiert.
Hinweis: Eine vollstaendige createDraft()/updateContent()-Verlagerung
wurde bewusst NICHT erzwungen — die Attributsaetze und der umgebende
Validierungs-/Fehlerfluss von Admin (no_export, direkter review-Status)
und Customer (placeholder_variant, Titel-Fallback, immer Draft) divergieren
real; eine gemeinsame Persistenzmethode waere fragil bei grosser Testflaeche.
Tests: 2 neue Faelle; volle Suite gruen (796 passed, 1 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Erkenntnis: Die Admin-Inline-Query (Contact::withoutGlobalScopes()->where
->orderBy) ist exakt der Contact::forCompany()-Scope -> alle vier
Kontakt-Helfer waren funktional identisch. companyContacts/companyContact/
defaultContactIdFor in neuen Concern InteractsWithPressReleaseContacts
(ueber forCompany) ausgelagert. tagSuggestionsFor() (4x identisch) in
InteractsWithPressReleaseTags verschoben. Ungenutzte Collection-Importe
in den Customer-Editoren entfernt; selectedCompany() bleibt je Kontext
(Admin: withoutGlobalScopes find; Customer: CustomerCompanyContext).
Tests: volle Suite gruen (794 passed, 1 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>