Commit graph

268 commits

Author SHA1 Message Date
299837d2ba security: Host-Whitelist & Legacy-API-Key-Übergabe härten (H5, M11)
- trustHosts erlaubt nur die konfigurierten Domains (plus localhost/IP
  für Health-Checks) und verwirft fremde Host-Header, sodass
  X-Forwarded-Host keine URL-Generierung mehr vergiften kann (H5).
- Legacy-API-Keys nur noch per Header (Bearer/X-ApiKey); die alte
  Query-/Body-Übergabe wird nicht mehr zur Auth akzeptiert, erhält aber
  weiterhin den 410-Migrationshinweis statt eines opaken 401 (M11).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 13:48:28 +00:00
000ae93e3b media: PM-Medien privat ausliefern + Vorschau-Signatur (M4, M5, Fix#4/#5)
- Neue PM-Bilder/Anhänge liegen auf der privaten local-Disk; Auslieferung
  über kontrollierte Media-Routen mit gemeinsamer Zugriffslogik
  (AuthorizesPressReleaseMediaAccess). Öffentliche Medien werden aus den
  geladenen Modellen ermittelt (weniger Queries) und länger gecacht.
- Anonyme Token-Vorschau lädt Bilder über temporär signierte URLs (Fix#5).
- API-Resource baut Variant-URLs über die Media-Route statt /storage
  (Fix#4). Storage-Schicht prüft zusätzlich den serverseitigen MIME-Type (M5).
- press-releases:privatize-media migriert Bestandsmedien.

Deployment: einmalig `php artisan press-releases:privatize-media` ausführen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 13:48:06 +00:00
0b35c3b04d press-releases: Publish-Rechte, Quota-Races & Slug vereinheitlichen
- 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>
2026-07-14 13:47:51 +00:00
7043ea8ad5 billing: Nebenläufigkeit & Idempotenz härten (H4, Stripe, Boost-Slots)
- 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>
2026-07-14 13:47:21 +00:00
7b3a4d7de8 PM-Detailseite: Inhaltspruefung (Gruen/Gelb/Rot) + Qualitaets-Score sichtbar
- Neue Kachel "Inhaltspruefung" in Status & Verlauf (Badge je Ergebnis,
  "Ausstehend" ohne Pruefung); bei Gelb neutraler Hinweis, dass die
  Boost-Platzierung gruen gepruefte Meldungen voraussetzt
- Content-Score-Panel des Editors in gemeinsame Komponente
  x-content-score-panel extrahiert und auf der Detailseite wiederverwendet
  (Score/100, Stufe, Abstand zur naechsten Schwelle)
- PressReleaseClassification::badgeClass() (ok/warn/err)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 14:59:33 +00:00
8657311944 PM-Uebersicht: Rubrik unter Portal, Boost-moeglich-Hinweis, gruener Haken
- Rubrik (lokalisiert) direkt unter den Portal-Pills; Kategorie wird in
  accessiblePageFor() eager geladen (kein N+1)
- Status-Spalte: laeuft kein Boost, wirbt eine veroeffentlichte gruene PM
  mit "Boost moeglich ->" (Link zur PM-Seite mit den Add-ons)
- Gruen geprueft: kleiner gruener Haken neben dem Titel (Tooltip
  "Inhaltspruefung: Gruen")

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:23:07 +00:00
92d1ac246f Add-on-Panel zeigt freie Boost-Plaetze je Stufe
slotAvailability() liefert zusaetzlich free/limit (knappste Ausgabe, ohne
den eigenen gehaltenen Slot); solange buchbar, steht unter den Kauf-Buttons
"Noch X von Y Plaetzen frei." — bei laufendem Boost weiterhin nur der
Verlaengerungs-Hinweis.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 13:12:05 +00:00
4853d48b83 Boost-Slot-Grenzen (5/3) + Kategorie-Boost zum halben Satz
Entscheidung 13.07.: verkauft wird nur, was sichtbar ist — max. 5
gleichzeitige Boosts je Startseite, 3 je Branchenseite (= die sichtbaren
Plaetze; Hero-Slider damit vertagt). Neue Stufe Kategorie-Boost (nur
Branchenseite, 6/10/18 Credits = 50 % aufgerundet); der Startseiten-Boost
(12/20/35) erscheint weiter auf Start- UND Branchenseite.

- boosts.placement (home/category, Bestand = home) + BoostPlacement-Enum;
  Preise/Slots in config/credits.php (boost_category, boost_slots)
- BoostService::slotAvailability() je Ausgabe (Portal x Sprache, Kategorie
  zusaetzlich je Rubrik); ausgebucht -> soldOut-Exception mit fruehestem
  freien Termin; Verlaengerungen halten ihren Slot, Stufen verlaengern
  unabhaengig; Kategorie-Boost invalidiert nur Kategorie-Caches
- Startseite zeigt nur Home-Boosts; Branchenseite: Kategorie-Boosts mit
  Vorrang (ihre 3 Slots sind die Garantie), Home-Boosts fuellen den Rest
- Add-on-Panel der PM-Seite: zwei Boost-Angebote, bei vollem Inventar
  "Derzeit ausgebucht — frei ab" statt Kauf-Buttons (serverseitig gegated);
  Buchungsseiten-Preisliste um die Kategorie-Staffel ergaenzt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 12:28:03 +00:00
9d6ef50e62 Boost-Buchung invalidiert gecachte Start- und Branchenseiten sofort
Statt bis zu 300 s Cache-TTL zu warten, verwirft der BoostService nach
der Buchung die Home-/Kategorie-Caches der betroffenen Ausgaben (inkl.
Eltern-Rubrik) — die bezahlte Platzierung ist direkt sichtbar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:22:24 +00:00
6e1267f00f Add-on-Modal: Pflicht-Schalter + Verlaengerungs-Hinweis bei laufendem Boost
Kauf-Button bleibt deaktiviert, bis der Bestaetigungs-Schalter aktiv ist
(serverseitig abgesichert). Laeuft bereits ein Boost, warnt das Modal,
dass die Buchung die Laufzeit verlaengert, mit altem und neuem Enddatum.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:17:32 +00:00
51da374173 E-Mail-Praeferenzen: Opt-out je Mail-Typ + One-Click-Unsubscribe ohne Login
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 17:15:05 +00:00
06d80b255b KI-Klassifikation: Substanzpruefung gegen Gruen-Durchrutscher
Gruen setzt jetzt einen erkennbaren Nachrichtenkern voraus; Test-/
Platzhalter-Texte sind explizit K6-Verstoss (rot), sehr duenne Inhalte
mindestens gelb. Behebt Befund, dass inhaltsleere Test-PMs (Score 4)
nach der Prompt-Ueberarbeitung gruen durchliefen.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 17:14:12 +00:00
927523ca63 PM-Liste: Hinweis auf laufende Boost-Platzierung unter dem Status
Badge "Boost bis TT.MM.JJJJ" in der Status-Spalte; Boost-Enden der
Seite in einer Query aggregiert statt isBoosted() je Zeile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:11:38 +00:00
bbcdc72e2f Web: Boost-Platzierung auf Start- und Branchenseite ausspielen
Geboostete PMs besetzen Lead- und Top-Slots vor der Chronologie
(HomePageData/CategoryPageData ueber boosted()-Scope) und tragen das
Badge "Top-Platzierung". Abgelaufene Boosts wirken nicht.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:05:10 +00:00
744e87d008 Werbeplaetze reaktiviert: Eigenwerbe-Slots oeffnen das Kontakt-Widget
Die im Spoke-Frontend-Audit entfernten Anzeige-Flaechen (focus-ad,
feed-ad, Inline-Ad) kehren als dezente Eigenwerbung zurueck:

- neue Komponente x-web.ad-slot im frueheren "ANZEIGE"-Stil mit Hinweis
  "Hier koennte Ihre Werbung stehen" (DE/EN)
- eingebaut an den drei frueheren Stellen: Hero-Sidebar der Startseiten,
  Feed nach der dritten Meldung, PM-Detailseite nach der
  Unternehmensbeschreibung (nicht in der privaten Vorschau)
- Klick oeffnet das schwebende Kontakt-Widget mit vorgewaehltem neuen
  Thema "Werbung buchen" (ContactMessageTopic::Advertising); Anfragen
  landen wie gewohnt in der Admin-Queue "Kontakt"

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 16:53:02 +00:00
dc2a3443c1 KI-Kennzeichnung: 2 Optionen im Submit-Modal + Labels auf Detailseite
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>
2026-07-10 16:51:23 +00:00
c8836564e9 Add-ons: Kaufbestaetigung vor Credit-Abbuchung + deutlichere Buttons
Boost-Laufzeiten und Nachweis oeffnen jetzt ein Bestaetigungs-Modal mit
Preis, Guthaben und Restsaldo; erst "Kostenpflichtig buchen" bucht ab.
Boost-Buttons primary mit explizitem Kauf-Label.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:44:05 +00:00
3ae57d9dba Admin-PM-Liste: KI-Pruefungs-Modal + geschaerfte KI-Prompts
- Spalten umsortiert (Aktionen, Datum, KI-Pruefung zuerst); Pruefen-Button
  nur noch Icon, oeffnet Detail-Modal mit Klassifikation, Begruendung,
  Content-Score inkl. Faktor-Breakdown und Re-Check (wire:poll bis Queue durch)
- Content-Score: Bild-Punkte (10) systemseitig statt KI (nur ein Upload-Bild
  moeglich), Quellen 10->5, Pressestil 20->25; Breakdown wandert ins Audit-Log
- Klassifikations-Prompt prueft gegen aktive Ablehnungskategorien (K1-K6,
  rejection_categories): klarer Verstoss => direkt rot mit Kategorie-Code

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 16:37:47 +00:00
de532feec2 API: nicht-numerische Routen-IDs als 404 statt TypeError/500
Ein vergessener Postman-Platzhalter (PM_ID) lief als String in die
int-typisierten Controller-Parameter. Numerische where-Constraints auf
den v1- und /service-Gruppen; Legacy faellt in den 410-Catch-all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:20:01 +00:00
2820385651 S4: legacy:seed-api-testdata — API-Testkunde nach connektar-Muster
Idempotenter Command (nur local/staging): aktiver Legacy-API-User mit
Grandfathering, importiertem X-ApiKey, api:immediate-publication,
Firma + Pressekontakt; gibt Key + curl-Sammlung des /service-Flows aus.
Live gegen die lokale App verifiziert (Firma -> Kontakt -> PM sofort
published -> GET 200 im Alt-Format).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 14:22:58 +00:00
a3fd361ece Legacy-API-Paritaet S1+S2: connektar-Flow verhaltensgleich + KI-Schalter
Eigene Compat-Controller (Api/Legacy) fuer die 5 real genutzten
/service-Endpoints (Prod-Log-Beleg): Company-/Kontakt-Create (liefen
410!), PM-Create, Bild-Upload, published-only-GET — verschachtelte
Alt-Params, 201+Location+{}, Alt-Fehlerformat, Hit-Zaehlung,
Legacy-Land-Mapping.

- Anlegen = Einreichen (Alt-Verhalten); Sofort-Publish via neuer
  Permission api:immediate-publication (WS-7: connektar zuweisen)
- Admin-Schalter 'KI-Pruefung Legacy-API' (app_settings-Tabelle,
  Toggle auf /admin/press-releases): an = review + KI-Routing
- Admin-PM-Liste: KI-Spalte (Ampel-Badge) + manueller Pruef-Button
  ohne Routing fuer ungepruefte PMs
- Mails bei API-Einlieferern an Pressekontakt, sonst Firmen-Mail,
  Fallback Einlieferer (User-Entscheidung 10.07.)
- Tests: LegacyServiceParityTest (connektar-Flow end-to-end);
  Suite 1182 gruen

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 14:09:40 +00:00
5c91e2e281 API: KI-Deklaration als v1-Pflichtfeld, Legacy-Default fuer /service (Entscheidung 10.07.)
Befund: v1 hatte keine KI-Deklarationsfelder — Submit lief seit der
KI-Kennzeichnungspflicht (09.07.) immer auf 422 und haette alle
Legacy-API-Kunden blockiert (Suite uebersah es: Factory deklariert).

- v1-Submit verlangt ai_text_declaration; der Call gilt dokumentiert
  als verbindliche Bestaetigung (Art. 50 KI-VO) und schliesst die
  Archiv-Kenntnisnahme (AGB par. 13) ein
- Bild-Upload verlangt ai_declaration (original/ai_generated/ai_edited)
- Legacy-/service-Aliasse defaulten auf none/original — das
  API-Kunden-Mailing (R6.4) weist auf die Erklaerungswirkung hin
- OpenAPI v1.yml + lang-Hinweis aktualisiert; Tests erweitert
  (Submit/Image/LegacyCompatibility)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 12:41:24 +00:00
813cbd2e18 R4.3: Karten-Metadaten-Logik als PressRelease-Praesentations-Helper
localizedCategoryName/cardImageUrl/teaserText/wordCount/readTimeMinutes/
compactPublishedLabel ersetzen die 5x kopierte @php-Logik der
Kartenkomponenten; Lesezeit-Formel vereinheitlicht (ceil -> round in
HomePageData-Spotlight). Tests: Web/PressReleaseCardHelpersTest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:57:26 +00:00
2ef41a4718 R2.5: legal-request-Hinweistexte final (Wortlaut aus Meldeverfahren v0.1)
- Meldung/DSGVO/Persoenlichkeitsrecht auf die am 06.07. freigegebenen
  /melden-Texte gezogen: kostenlose manuelle Pruefung, unverzuegliche
  Eingangsbestaetigung mit Vorgangsnummer (sofern E-Mail), Entscheidung
  mit Begruendung; DSGVO ohne pauschale 30-Tage-Zusage, stattdessen
  Bestaetigungslink/Nachweis wie im Meldeverfahren
- ENTWURF-TODO entfernt, Querverweis auf /melden ergaenzt
- Render-Test erweitert (Vorgangsnummer, kostenlos, melden-Link)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:11:28 +00:00
644f860223 PM-Tracking: Ereignis-Log fuer Aufrufe + Shares (press_release_events)
- press_release_events (type view/share, channel, ohne Personenbezug):
  Model/Enums/Factory, Cascade-Delete an der PM
- Detailaufruf loggt View-Event zusaetzlich zum Legacy-hits-Zaehler
  (bleibt bewusst einfacher Page-Request-Zaehler, User-Vorgabe 10.07.)
- POST press-release/{slug}/share (throttle 30/min, nur archiv-sichtbare
  PMs des Portals) loggt Teilen-Klicks je Kanal (LinkedIn/X/E-Mail/Copy)
- Kundenpanel-PM-Seite: neue Kachel Geteilt mit Kanal-Aufschluesselung
- lang: web.detail.views (de/en)
- Tests: Web/PressReleaseTrackingTest (7)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 09:56:42 +00:00
72e6a3c210 Backend-WIP: Kontakt-Queue, Page-Cache, Validierungs-Feedback, i18n
Gebuendelt, da ueber gemeinsame lang-/Service-Dateien verschraenkt:
- Kontakt-Nachrichten: contact_messages-Tabelle + Model/Enums/Factory,
  Admin-Queue (livewire/admin/contact-messages), ContactController
  persistiert das Anliegen zusaetzlich zur Brand-Mail
- Page-Data-Cache (Services/Web/*PageData) + web-archive-Indizes auf
  press_releases
- Validierungs-Feedback + Company-Portal-Restriction (Feature-Tests)
- i18n-Auslagerung lang/de|en (auth, passwords, validation, static)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:38:11 +00:00
1ad2e544a9 Monitoring (R5.3): Betriebs-Alerts fuer Scheduler-/Queue-Ausfaelle
OperationalAlert loggt critical + Best-Effort-Mail an MONITORING_ALERT_EMAIL
(Fallback LEGAL_CONTACT_EMAIL, feature-flagged). Alle 6 Scheduler-Tasks mit
onFailure, Queue::failing-Listener; .env.example: LOG_STACK=daily als Prod-Soll.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:34:40 +00:00
c2ec18be5f Buchungsseite: Buchungs- und Verlaengerungsdatum des Abos aus Stripe
Cashier spiegelt die Periode nicht lokal; neuer SubscriptionPeriodResolver
holt start_date + current_period_end (Item-Ebene) gecacht, Fallback null.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:34:28 +00:00
75d7baab2a Depublikation: Credit-Preis auf 35 (O6), Preis-Tests nachgezogen
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:34:28 +00:00
2882bc44b2 PM-Editor UX: Kategorie in die Schreibfläche, Bild-Upload als Modal-Flow
Kategorie-Pflichtfeld direkt unter die Firma statt in die Sidebar.
Titelbild-Upload öffnet ein Modal: erst Bild wählen, dann erscheinen
die gestrafften Schritte (Herkunft & Bildnachweis mit Vorschlag aus
dem Urheber, Personen & Rechte, Bestätigung), Optionales eingeklappt.
Bildrechte-Checkbox im Einreichen-Modal nur noch mit eigenem Titelbild.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 13:55:21 +00:00
c387048d7e KI-Kennzeichnung (Art. 50 KI-VO): Pflicht-Deklaration für Text & Bilder
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>
2026-07-09 13:54:43 +00:00
6c4fd0cc17 USt-ID vor jeder Buchung per eVatR gegenprüfen, Stripe-Tax-IDs abgleichen
USt-IDs laufen ab — Kunden bestellten anderswo über veraltete IDs
steuerfrei. Vor jedem Checkout wird die Profil-USt-ID jetzt einmal per
eVatR (BZSt) geprüft (Ergebnis 6 h gecacht): Eine definitiv abgelehnte
ID geht nicht an Stripe, nicht mehr passende eu_vat-Einträge am
Stripe-Customer werden entfernt — Stripe Tax rechnet dann wieder mit
Steuer statt Reverse Charge. Fail-open bei API-Ausfall und deutschen
IDs (eVatR prüft nur ausländische EU-IDs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 13:28:56 +00:00
f8e8f3eb1a Buchungsseite: Verlauf mit Kauftyp, Erfolgsmeldungen grün
„Eingelöste Käufe" nennt jetzt Kauftyp und PM-Titel (z. B.
„Depublikation: <Titel>") über das neue kompakte shortLabel() am
SinglePurchaseType. Die Checkout-Erfolgsbanner (Buchung, Tarifwechsel,
vorgemerkter Wechsel) und die Extra-PM-Bestätigung nutzen das
etablierte grüne Success-Muster (ok-soft) statt Hub-Blau.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 11:25:24 +00:00
723d51c21e Depublikation: Kauf als eingelöst markieren, Rechnung & Bestätigungs-Mail
Drei Lücken im bezahlten Depublikations-Flow (T12) geschlossen:

- Webhook setzt den Kauf jetzt auf status=consumed (vorher nur
  consumed_at) — erst damit erscheint er unter „Eingelöste Käufe".
- Einmalkäufe (Depublikation, Einzel-PM, Credit-Topup) aktivieren
  invoice_creation im Stripe-Checkout: Ohne das Flag erzeugt Stripe im
  Payment-Mode keine Rechnung und die STR-Spiegelung lief ins Leere.
  Abo-Checkouts bleiben unverändert (Parameter dort unzulässig).
- Neue Bestätigungs-Mail PressReleaseDepublished an den Käufer
  (nicht zwingend der PM-Autor) nach vollzogener Depublikation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 11:25:12 +00:00
26ce2544f0 UI: Agentur-Angaben im Profil, Firmen-Typ-Auswahl entfernt
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>
2026-07-08 09:44:17 +00:00
0b3c6ccac9 companies:retire-agency-type: Firmen-Typ „Agentur" abschaffen
A4 des Umbaus Agentur-Attribution → User-Profil: Agentur-Firmen mit
PMs oder Pressekontakten werden zu type=company umtypisiert (sie sind
faktisch Newsrooms), leere Agentur-Firmen soft-gelöscht. Geschützt vor
Löschung (nur Umtypisierung): Mitgliedschaften Dritter in company_user
oder Verknüpfungen in user_payment_option_company. --dry-run +
JSON-Report; im Deploy-Runbook nach legacy:migrate-agency-names
eingereiht.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:33:39 +00:00
56fe5ea6f4 Attribution: Übermittler aus users.agency_name statt Agentur-Firma
A3 des Umbaus Agentur-Attribution → User-Profil: Eager Loading der
Detailseite/Vorschau lädt user:id,agency_name; die Legacy-Relation
PressRelease::submittingAgency() (Pseudo-Firma type=agency via
owner_user_id) entfällt. Tests decken Verteiler-Fall, Namensgleichheit
und PM ohne Übermittler ab.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:30:41 +00:00
621fc0569f legacy:migrate-agency-names: Agentur-Namen → users.agency_name
Übernimmt je Owner den Namen seiner Legacy-Agentur-Firma(en) ins neue
Profilfeld (A2 des Umbaus Agentur-Attribution → User-Profil). Regeln:
zuletzt aktualisierter Datensatz gewinnt bei Mehrfach-Namen (alle
Varianten im Report), bereits gesetzte Werte werden nie überschrieben,
Owner-lose Agentur-Firmen werden nur reportet. --dry-run + JSON-Report
nach storage/app/private/migration/; im Deploy-Runbook (Erst-Einrichtung)
nach dem Firmen-/User-Import eingereiht.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:25:04 +00:00
1ef1e13c19 Depublikation: abgebrochener Checkout blockiert nicht mehr
Der Klick auf "Kostenpflichtig depublizieren" legte sofort einen
Pending-Kauf an; wer bei Stripe abbrach, sah danach dauerhaft "liegt
bereits ein Depublikations-Vorgang vor". Jetzt sperrt nur ein bezahlter
Vorgang — ein offener Pending-Kauf wird beim nächsten Klick
wiederverwendet (kein Duplikat), und die Detailseite zeigt das Angebot
weiterhin an.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:07:32 +00:00
9b55e11159 PM-Detailseite: Direktlinks zur öffentlichen Seite je Portal
Veröffentlichte PMs zeigten im Backend nur den Status-Badge, ohne Weg
zur öffentlichen Seite. Im Seitenkopf stehen jetzt Direktlinks — bei
Portal::Both einer je Portal (QA-Befund 07.07.).

Die URL-Logik ist als PressRelease::publicPortalLinks() zentralisiert
(vorher doppelt in der Published-Mail und fehlerhaft im Nachweis-PDF:
dort fehlte das /de/press-release/-Segment, die dokumentierte URL lief
ins Leere; bei Both nennt der Nachweis jetzt beide URLs). Edition folgt
der PM-Sprache statt hart „de".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:00:40 +00:00
a21d635ed6 PM-Detailseite: Depublikations-Sektion ans Seitenende verschoben
Der seltene Ausnahme-Service stand zwischen Add-ons und Inhalt; jetzt
steht er als letzte Sektion unter dem Inhalt (QA-Befund 07.07.). Dazu
zwei UI-Tests: Sektion + Unternehmer-Bestätigungs-Protokoll auf der
Detailseite, Ausblenden ohne Preis bzw. bei laufendem Vorgang.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:35:42 +00:00
dfb8de9e45 Add-ons: Modal statt Toast bei fehlendem Credit-Guthaben
Scheiterte ein Boost- oder Nachweis-Kauf am Wallet-Guthaben, gab es nur
einen flüchtigen Toast ohne Weg zur Lösung. Jetzt öffnet sich ein Modal
mit Kosten, Guthaben und fehlender Differenz sowie einem Direktlink
"Credits aufladen" zur Wallet-Sektion der Buchungsseite (neuer
#credits-Anker).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:30:57 +00:00
2a46aed1a9 Einreichen-Modal: Panel-Struktur und fixierter Kontingent-Badge
Das Modal war ein unstrukturierter Stapel aus zwei Boxen und losen
Switches; der Kontingent-Badge wurde vom Flex-Layout zusammengedrückt
und brach um ("3 /" / "3"). Jetzt: drei beschriftete Panels (Rechtliche
Hinweise, PM-Kontingent, Ihre Bestätigungen) im portalweiten
panel/panel-head-Stil, die Switches als getrennte Zeilen mit
Fortschrittszähler (x / 4) im Panel-Kopf, Badge mit shrink-0 und
whitespace-nowrap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:16:56 +00:00
fcfbbd4918 PM-Editor: Ablehnungsbegründung direkt auf der Bearbeiten-Seite anzeigen
Bei abgelehnten Pressemitteilungen stand die Begründung bisher nur auf
der Detailseite. Der Editor zeigt jetzt dasselbe Hinweis-Panel (Begründung
+ Ablehnungsdatum) über der Schreibfläche, damit der Kunde beim
Überarbeiten sieht, was zu ändern ist.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:09:42 +00:00
c040f394e3 Rechnungs-Seite: Archiv-Filter entfernt, Altsystem-Archiv nur für Bestandskunden
QA-Befund 07.07.: Die Filter-/Suchzeile unter den aktuellen Rechnungen
entfällt; das Rechnungsarchiv (Altsystem) erscheint nur noch, wenn der
User dort tatsächlich importierte Rechnungen hat — Neukunden sehen es
nicht mehr. Seitenbeschreibung passt sich entsprechend an.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 13:53:05 +00:00
ef2b4fe2ad Downgrades ohne Gutschrift: Tarifwechsel nach unten erst zum Periodenende
QA-Entscheidung 07.07.: Eine Buchung ist nicht rückabwickelbar — es darf
nie eine negative Proration-Rechnung oder ein Stripe-Guthaben entstehen.

- StripeCheckoutService::isDowngrade: sofortiger Wechsel nur, wenn der
  Periodenpreis des Ziel-Tarifs den aktuellen erreicht (Wechsel-Rechnung
  damit garantiert >= 0); sonst Downgrade
- scheduleDowngrade: Stripe Subscription Schedule stellt das Abo zum Ende
  der bezahlten Periode um (proration_behavior none), hebt eine
  vorgemerkte Kündigung auf und spiegelt die Vormerkung lokal
  (neue Spalten scheduled_stripe_price/scheduled_change_at/
  stripe_schedule_id an subscriptions)
- Sofort-Upgrade löst eine bestehende Vormerkung vorher auf (release)
- Webhook customer.subscription.updated räumt die Vormerkung nach
  Vollzug ab
- Buchungs-Modal unterscheidet Upgrade (sofort, Differenz-Rechnung) und
  Downgrade (Vormerkung, keine Zahlung heute); Banner + Hinweis auf der
  Paket-Karte zeigen den vorgemerkten Wechsel mit Datum
- Kontingent-Sperre aus 4b7f6d3 entfernt — ohne Gutschrift gegenstandslos

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 13:38:57 +00:00
4b7f6d3f0f Downgrade-Vorkehrungen: Kontingent-Sperre beim Tarifwechsel, Gutschriften nicht spiegeln
QA-Befund 07.07.: Der Wechsel zurück in einen kleineren Tarif erzeugte
eine Proration-Gutschrift (negative Stripe-Rechnung) — die STR-Spiegelung
crashte an den unsigned-Spalten (Webhook-500), und der Wechsel war trotz
verbrauchtem Monatskontingent möglich.

- ProcessStripeWebhook: Belege ohne positiven Rechnungsbetrag
  (Gutschrift/Null-Rechnung) werden geloggt übersprungen statt gespiegelt
- CheckoutController::swap: Wechsel gesperrt, wenn der Monatsverbrauch
  das Kontingent des Ziel-Tarifs übersteigt (möglich ab Monatserstem)
- Buchungs-Modal: erklärt die Sperre inkl. Datum statt Bestätigungs-
  Button; Proration-Hinweis deckt jetzt auch die Guthaben-Richtung ab

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 13:22:30 +00:00
090a893cc6 PM-Vorschau: echtes Portal-Design, Direkt-Button und Portal-Switch
Der Vorschau-Link (/pm-vorschau/{token}) renderte eine schlichte
Standalone-Seite, die dem spaeteren Erscheinungsbild nicht entsprach.
Jetzt rendert er die echte Detailseite (web.release-detail) im Theme
des Zielportals:

- Controller wendet Domain-/Theme-Konfig des Zielportals an (Sprache
  der PM, Links zeigen auf die Portal-Domain), kein Hit-Zaehler
- Gelbes Vorschau-Banner mit Status und Link-Gueltigkeit; noindex per
  Meta-Tag + X-Robots-Tag-Header, Canonical entfaellt bei noindex
  (neue robots-Section im Web-Layout)
- Teilen-Buttons teilen den Vorschau-Link selbst (oeffentliche URL
  existiert vor Veroeffentlichung noch nicht)
- PMs fuer beide Portale: ?portal=... waehlt die Ansicht, Umschalter
  im Banner; im Kundenpanel je Portal ein Vorschau-Button, sonst ein
  einzelner "Vorschau oeffnen"-Button (Link-Feld jetzt kopierbar)
- Alte press-release-preview-View entfernt

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 13:11:57 +00:00
9a95a6937d Suche: Volltext-Index + Fix fuer doppelte Alpine-Instanz
Archiv-Suche lief auf grossen Datenbestaenden langsam und war im Browser
komplett tot: app.js startete ein eigenes Alpine, Livewire injizierte
zusaetzlich seins - der DOM-Morph brach mit "Alpine.transaction is not a
function" ab (keine Rubrik klickbar, Suchbegriff nicht aenderbar).

- app.js buendelt jetzt Livewire + Alpine aus livewire.esm,
  @livewireScriptConfig im Web-Layout stoppt die Auto-Injektion
- MySQL-Volltext-Index (Boolean-Mode, +wort*) statt LIKE ueber alles;
  LIKE-Fallback fuer SQLite-Tests und kurze Tokens
- Firmen-/Rubrik-Treffer als ID-Listen statt orWhereHas (OR + EXISTS
  hebelte den Volltext-Index aus)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 13:03:14 +00:00
9e76e24014 Tarifwechsel im Buchungs-Modal: Abo-Swap mit anteiliger Verrechnung
Tarif-Karten bieten bei bestehendem Abo jetzt 'Zu diesem Tarif
wechseln' (pro Intervall, aktueller Tarif deaktiviert). Der Wechsel
laeuft ueber das bekannte Bestaetigungs-Modal (T1neu-Checkbox) als
POST auf die neue checkout.swap-Route: Cashier swapAndInvoice stellt
sofort um, verrechnet anteilig und hebt eine vorgemerkte Kuendigung
auf. SCA-Faelle leiten auf die Cashier-Bezahlseite. QA-Befund 07.07.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 12:58:32 +00:00