Commit graph

134 commits

Author SHA1 Message Date
f1648a05b0 web: Erstquelle-Absatz in der Veröffentlichungsmail, vierte Transparenzseite „kostenlos veröffentlichen" (Anhang 1: 1d, 2.4, E7)
Meldungen, die über die API eingegangen sind, bekommen in der
Veröffentlichungsmail einen Absatz zum Unterschied Erstquelle/Kopie —
technische Tatsache, ohne Preis und Angebot; direkt eingereichte nicht.

/pressemitteilung-kostenlos-veroeffentlichen beantwortet die Suche nach
„kostenlos" ehrlich: kein kostenloser Weg (E7), was kostenlose Portale
leisten, warum Links aus Presseportalen kaum tragen (auch bei uns
sponsored nofollow), warum Annahme ohne Gegenwert und Prüfung sich
ausschließen, wer am Ende zahlt. Die Liste kostenloser Portale ist in
config/transparency.php vorbereitet und bleibt leer, bis geprüfte
Einträge mit Prüfdatum vorliegen.

Unter-Navigation, Hilfe, Sitemaps, llms.txt und die Modelltabelle der
Kostenseite verlinken die neue Seite. Anhang 1 und Protokoll Teil 6.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 14:22:05 +00:00
b6e4bc5dc0 web: Transparenzseiten — was kostet, ohne Abo, loeschen/aendern (Roadmap 2.4)
Drei Seiten mit unterschiedlicher Suchintention, DE/EN, Hub + Spokes:
- /was-kostet-eine-pressemitteilung: kurze Antwort im ersten Bildschirm,
  vier Abrechnungsmodelle ohne Anbieternamen und fremde Preise, sechs
  Prueffragen fuer Folgekosten, "Was bei uns gilt" (sieben Zusagen mit
  Einloesung), Rechenbeispiel Einzelabrechnung gegen Starter-Monate mit
  Zeilen fuer beide Seiten.
- /pressemitteilung-veroeffentlichen-ohne-abo: Ablauf, enthalten / nicht
  enthalten, nichts verfaellt, wann sich ein Tarif lohnt.
- /pressemitteilung-loeschen-oder-aendern-lassen: fuenf Wege als
  Tabelle (Korrektur, Aenderung, DSGVO, Depublikation, DSA), Antragsweg
  ueber /melden/pm, Korrektur statt Loeschung, Absatz fuer Meldungen auf
  fremden Portalen ohne Zusagen ueber Dritte.

PricingTransparencyData liefert alle Zahlen aus config/billing, credits,
legal und dem Starter-Tarif; TransparencyPageController leitet die Marke
aus dem Host ab. Tabellen-Typografie fuer .pm-body. Verlinkt aus
Preisuebersicht, Hilfe-Center, Sitemaps und llms.txt.

Test TransparencyPagesTest (9) erzwingt u. a., dass im Rechenbeispiel
beide Modelle Zeilen gewinnen. Protokoll Teil 5, Roadmap 2.4 abgehakt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 12:00:28 +00:00
84ea617107 web: Jahresarchiv je Branche, Kategoriebeschreibungen, Portalname im Titel (Roadmap 2.3)
- /de/category/{slug}/archiv/{jahr} (CategoryArchivePageData,
  CategoryArchiveController, View kategorie-archiv): 30 Meldungen je
  Seite, neueste zuerst, Jahresleiste mit Zahlen; ohne Jahr das juengste
  Jahr mit Meldungen. Canonical mit ?page=N auf Folgeseiten, hreflang
  nur auf Seite 1, CollectionPage + BreadcrumbList. Gecacht bis Seite 3.
  Leere Jahre, unbekannte Kategorien, leere Folgeseiten: 404.
- Branchenseite: Jahresleiste unter dem Feed (archiveYears, eine
  gruppierte Query); Sitemap listet alle Jahresarchive.
- categories:seed-descriptions {--dry-run} {--force}: fuellt leere DE/EN
  Beschreibungen aus database/seeders/data/category-descriptions.php
  (14 Kategorien, sachlich, ohne Werbeaussagen), leert den Seiten-Cache.
- Branchenseite: Titel trug config('app.name') ("– pressekonto") statt
  des Portalnamens — jetzt presseecho/businessportal24.
- Protokoll Teil 5 (2.3 mit Befund: 0 von 14 Beschreibungen, Sonstiges =
  28 % des Bestands), Roadmap 2.3 abgehakt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 10:25:29 +00:00
b74f881ac8 web: Entitaeten im JSON-LD verknuepft, News-Sitemap und llms.txt (Roadmap 2.1, 2.2, 2.7)
- App\Support\StructuredData: Portal-Organization als Publisher mit @id
  und Logo (ImageObject), Newsroom-@id; NewsArticle.author auf der
  Meldung und Organization auf der Newsroom-Seite beschreiben jetzt
  dieselbe Entitaet. Startseiten nutzen denselben Publisher.
- /sitemap-news.xml je Spoke (NewsSitemapController): im Archiv
  sichtbare Meldungen der letzten 48 h, max. 1.000, news:publication
  mit Portalname und Sprache; 15 min gecacht, mit den anderen Sitemaps
  geleert; in robots.txt der Spokes. Hub liefert leere Liste.
- /llms.txt je Host (LlmsTxtController): sachliche Beschreibung,
  URL-Aufbau, aktive Kategorien, Seiten fuer Unternehmen, Rechtliches;
  Hub verweist auf beide Portale. Keine Aussagen ueber KI-Nennungen.
- Protokoll Teil 5 (Block 2) inkl. Betreiber-Schritten fuer das
  Publisher Center und Befund zu 2.5/E5; Roadmap 2.1/2.7 abgehakt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 10:15:28 +00:00
713024be37 companies: Befehl zum Deaktivieren der Legacy-Linkspam-Registrierungen
4.391 aktive Firmen (2010-2022) tragen bbcode-/HTML-Linkspam in den
Stammdaten und haben nie eine Meldung veroeffentlicht. Sie werden
deaktiviert, nicht geloescht; IDs landen in storage/app, Besitzerkonten
bleiben unangetastet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 09:56:44 +00:00
99ffd012d4 companies: Links aus Firmennamen normalisieren — API-Eingang und Bestandsbefehl
38 oeffentliche Firmen tragen eine URL im Namen ("Firma - https://..."),
laufend ueber die API. Nicht ablehnen (traefe die Meldung), sondern
normalisieren: URL raus, Rest bleibt Name, URL ins leere Website-Feld;
besteht der Name nur aus der URL, wird der Host zum Namen. Am API-Eingang
vor der Zuordnung; im Bestand per companies:strip-links-from-names mit
Dry-Run, Slugs bleiben.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 09:07:49 +00:00
87ca9082a9 companies: Kurzfelder des Profils ohne Links, Adressen und Spam (NoPromotionalText)
Firmenname, Adresse und Pressekontakt erscheinen oeffentlich bzw. in
Mails, wurden aber nicht geprueft — nur die Beschreibung lief durch die
Rot-Ebene. Neue deterministische Regel: keine Links, keine E-Mail-
Adressen, keine Zeichen-/Emoji-Ketten, kein Blacklist-Treffer; nackte
Domains ohne Pfad bleiben erlaubt (Marke). Adresse zusaetzlich durch die
Rot-Ebene wie die Beschreibung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 07:55:07 +00:00
4fd1db7825 press-releases: Mail "wird persoenlich geprueft" beim Block, Blade-Guard fuer @php-Bloecke (Roadmap 1d, Nachtrag)
Der Autor bekommt beim Block nach dem letzten Rot (oder Gelb mit
eingeschalteter Queue) eine kurze Mail statt Stillstand: was passiert ist,
dass nichts zu tun ist, und dass das Ergebnis per Mail kommt. Laeuft ueber
notifyAuthor() wie Veroeffentlichung und Ablehnung (API-Regel, Sperrliste,
adressbezogene Abmeldung, Bounce-Referenz); Opt-out am Typ "abgelehnt".
Neue ConversionSource mail_review fuer den Link ins Konto.

BladePhpBlockGuardTest wendet das Compiler-Muster auf alle Views an und
faengt inline @php( vor einem spaeteren @endphp ab — fand den eigenen
Erklaer-Kommentar im Modal, der nur zufaellig funktionierte. Regel in
CLAUDE.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 15:05:47 +00:00
35db30e019 mail: adressbezogene Abmeldung fuer Empfaenger ohne Konto (Roadmap 1d.3)
Der Abmeldelink in API-Benachrichtigungen zeigte auf das Konto des
Einlieferers - ein Klick des Pressekontakts haette connektar abgemeldet.
Jetzt traegt der Link die Empfaengeradresse (signiert), der Zustand liegt
als Unsubscribed in der Sperrliste. Anmeldelinks, die die Person selbst
anfordert, gehen weiter durch; harte Bounces sperren alles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 14:35:47 +00:00
0692d892e0 press-releases: begrenzter Pruefkreislauf mit Admin-Queue (Roadmap 1d.4, E3)
Automatische Pruefung je Meldung hoechstens max_automatic_attempts (3);
das letzte Rot blockt statt abzulehnen: Meldung bleibt in review, Admin
sieht sie unter "Pruefung noetig" mit allen KI-Begruendungen und
entscheidet (Freischalten / Mit Kommentar zurueck). Danach ist die
Automatik fuer diese Meldung aus - jede Freigabe geht direkt zum Menschen.

Gelb: yellow_to_manual_queue wird jetzt ausgewertet (Vorgabe false).
Kundenseite: "Zur Pruefung freigeben" mit Erklaerung der drei Schritte,
Versuchszaehler an der Ablehnung, Hinweis auf menschliche Pruefung.

Blade: inline @php(...) vor einem @php...@endphp-Block wird als Rohblock
gelesen - vier Stellen auf Blockform umgestellt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 14:30:26 +00:00
09ec866aac customer: Sichtbarkeit belegen - Aufrufe im Verlauf, Newsroom-Uebersicht (Roadmap 1c)
- VisibilityStats: Firmen-Kennzahlen ueber alle Meldungen, Aufrufe je
  Meldung (30 Tage), lueckenlose Tagesreihe (Tage vor der Zaehlung sind
  null, nicht 0), Sitemap-seit und Google-Suchlink (Indexierung Variante B).
- Firmenseite: alle Meldungen der Firma statt nur eigene (Beansprucher
  sahen eine leere Liste), Aufrufe-Spalten, echtes Statistik-Panel.
- Meldungsseite: Panel Sichtbarkeit mit SVG-Tagesbalken 30/90 Tage.
- Claim aus der Veroeffentlichungsmail landet auf der Meldung
  (payload press_release_id), Mail-Hinweis erweitert, ein Element (E4).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 11:53:09 +00:00
36691a5b13 billing: MAN-Rechnungen fuer Ueberweisungskunden bearbeiten, hold-Altlast bereinigen
Bestandskunden aus dem Altsystem zahlen per Ueberweisung; nach dem 04:30-Lauf
fehlte bisher der gesamte Bearbeitungsweg (kein PDF, keine Mail, kein
Zahlungseingang, kein Bestandsschutz-Ende). Plan und Befund:
dev/lagacy-rechnungen/entwicklungsplan-2026-09-16.md.

Bereinigung (legacy:settle-unpaid, wiederholbar):
- Legacy-Status hold = nie bezahlt, ersatzlos erledigt. 9 daraus abgeleitete
  Bestandsschutz-Buendel werden gekuendigt, MAN-00001/00002 storniert.
  Archivzeilen bleiben erhalten, sind aber in Admin-Archiv und Kundenkonto
  standardmaessig ausgeblendet.
- pending/reminder (recurring) = offene Vorgaenge: Periode auf den
  Leistungsbeginn der unbezahlten Rechnung zurueckgedreht und als MAN-Rechnung
  neu ausgestellt (Archiv-Status superseded, replaces_legacy_invoice_number).
- pending/reminder (single) = abgeschrieben (uncollectible).
- legacy:grandfather-subscriptions wertet den Zahlungsstatus jetzt selbst aus.

Workflow in /admin/payments (ManualInvoiceDunningService, je Aktion mit
Eintrag im Aktivitaets-Verlauf):
- Rechnung senden: Mail mit PDF-Anhang, Bankverbindung aus billing.issuer,
  Hinweis auf Bestandsschutz-Ende; Zahlungsziel zaehlt ab Versand.
- Erinnerung: gleiche Mail im Mahn-Ton, Nachfrist bei verstrichener Frist.
- Bezahlt: Zahlungseingang laut Kontoauszug verbuchen.
- Bestandsschutz beenden: Rechnung storniert (nie geloescht), Buendel
  gekuendigt, weitere offene MAN-Rechnungen des Kunden storniert, optionale
  Kundenmail.
- Panel "Blockierte Vereinbarungen": faellige Posten ohne Rechnung (meist
  fehlende Rechnungsadresse) mit "Jetzt abrechnen" oder "Entfernen".

Schema: invoices bekommt user_payment_option_id, sent_at, reminded_at,
reminder_count, voided_at, void_reason, replaces_legacy_invoice_number
(inkl. Backfill der bestehenden MAN-Rechnungen). PDF-Primitiven des
Archiv-Renderers in den Trait DrawsPdf ausgelagert; MAN-PDF on-demand ueber
admin.invoices.pdf / me.invoices.manual-pdf (InvoicePolicy).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 10:56:29 +00:00
ada41cb067 mail: Kontakterreichbarkeit - Formpruefung, Domainpruefung, Einlieferer-Bericht
Vier Punkte aus kontakterreichbarkeit-mx-bounces-2026-09-15.md, ohne
Veroeffentlichungssperre:

- PlausibleEmailDomain: Domain ohne Endung oder mit Tippfehler-TLD ist
  ein 422 an der Legacy-API, deterministisch ohne DNS.
- email_domain_checks + MailDomainChecker ueber DnsResolver-Schnittstelle
  (RFC 5321 impliziter MX, RFC 7505 Null MX, Unresolved ist kein Befund),
  Observer an Company/Contact, Job nach Commit, taeglicher Refresh,
  mail:check-domains fuer den Bestand. Nie eine Adresssperre aus DNS.
- Admin > Reports > Einlieferer-Bericht: tote Adressen aus Sperrliste
  und Domainbefund, automatisch abgelehnte Meldungen mit Grund,
  Text zum Kopieren und CSV. Versendet nichts.
- EmailBounceKind::Policy: 5.7.x wird protokolliert, zaehlt aber nicht
  auf den Schwellwert. Statuscode entscheidet vor Diagnosetext.

Messung vorab: 6 von 877 API-Domains ohne Zustellweg, eine davon fuer
66 der 151 Ruecklaeufer verantwortlich.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 09:22:39 +00:00
fddfd8a61c mail: Bounce-Abruf liest RFC 6533, All-Inkl-Freitext und ganze INBOX
Befund aus 151 echten Rücklaeufern: 2 SmarterMail-DSNs
(message/global-delivery-status) und 2 Freitext-Hinweise des eigenen
Relays (X-KASMail-Error, formal auto-replied) fielen durch. Policy- und
Quota-Codes zaehlen weich statt hart. Abruf ohne UNSEEN-Filter, weil
das Verschieben die Zustandsmarke ist.

Dazu Schutz in tests/TestCase.php: Suite bricht ab, wenn ein Config-Cache
sie auf die Dev-Datenbank umlenkt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 15:27:09 +00:00
71d9639491 deps: Composer- und npm-Pakete innerhalb der Constraints aktualisiert
Laravel 12.69, Livewire 4.4, Flux 2.20, Cashier 16.8 (Stripe-SDK 21),
Larastan 3.12; Vite 6.4.3, Tailwind 4.3, Alpine 3.17 - npm audit 0.
Drei neue Larastan-Befunde behoben, drei tote Baseline-Einträge entfernt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 15:04:52 +00:00
e84c2772ed admin: Mail-Sperrliste, Markierung nicht erreichbarer Newsrooms (Roadmap 1b.6, Schritt 4)
Admin ▸ Mail-Sperrliste mit Freigabe, manueller Sperre, nicht lesbaren
Rückläufern und Breaker-Zustand. Firmenseite markiert gesperrte
Firmen-E-Mail und Pressekontakte. Die Claim-Seite zählt gesperrte
Adressen nicht mehr als Empfänger und erklärt, warum kein Link geht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 14:01:07 +00:00
e80006589c mail: Rückläufer aus dem Bounce-Postfach lesen (Roadmap 1b.6, Schritt 3)
webklex/laravel-imap als Transport, eigener DSN-Parser nach RFC 3464 auf
der rohen Nachricht: Final-Recipient, Action, Status, Diagnostic-Code und
unser X-Pk-Ref aus der Original-Nachricht. Maßgeblich ist Action —
Zustellbestätigungen sperren niemanden, Freitext-Rückläufer wandern nach
Unparsed vor einen Menschen, Abwesenheitsnotizen nach Ignored.
mail:fetch-bounces alle zehn Minuten; ohne Konto tut es nichts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 13:55:30 +00:00
0a658bb34b mail: zentrale Bounce-Suppression mit Versand-Gate und Circuit Breaker (Roadmap 1b.6, Schritte 1–2)
email_suppressions/email_bounces als Zustand und Ereignis-Log; ein
MessageSending-Listener streicht gesperrte Empfänger aus JEDER ausgehenden
Mail und setzt den Return-Path. Zweiter Aufnahmepfad: SMTP-Ablehnungen im
Worker — 5.1.x sperrt den Empfänger, Relay-Ablehnungen zählen für den
Circuit Breaker, der den Versand anhält statt gesunde Adressen zu sperren.
X-Pk-Ref-Header auf den Mails, API-Meldungen fallen auf die Firmen-E-Mail
zurück, der Bounce-Import schreibt in die zentrale Liste.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 13:48:56 +00:00
87f95fb7a7 newsroom: Beschreibung pflegen, durch die Rot-Ebene geprüft (Roadmap 1b.3)
Die Firmenseite im Kundenbereich bekommt den Beschreibungstext (max.
2 000 Zeichen, reiner Text). Er läuft durch dieselbe Klassifikation wie
eine Meldung (CompanyProfileCheck, KI mit deterministischem Fallback);
Rot wird abgewiesen. Links im Profiltext gibt es nicht — die Website ist
das eine Link-Feld. Bilder zurückgestellt (BALD-Platzhalter), bis die
Bild-Kennzeichnung angepasst ist.

Stufe 3 der Konversionsleiter (newsroom_edited) wird hier geschrieben,
einmal je Firma, mit der Herkunft des zuletzt eingelösten Magic Links.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 13:04:12 +00:00
01014887cf newsroom: Hinweis „Diesen Newsroom verwalten“ auf der Seite und in der Mail (Roadmap 1b.4, E4)
E4 (15.09.2026): Die Veröffentlichungsmail trägt den Hinweis am Ende, als
Hinweis, nicht als Angebot. Auf der Newsroom-Seite eine Zeile unter dem
Profil. Der Mail-Link ist mit src=mail_publish getaggt; die Claim-Seite
reicht die Herkunft bis auf das Token durch, sodass die Leiter „aus der
Mail beansprucht“ von „auf der Seite entdeckt“ unterscheidet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 11:05:53 +00:00
9143cc4a54 newsroom: Verwaltungszugang „Diesen Newsroom verwalten" (Roadmap 1b.2)
Neue Seite /newsroom/{slug}/verwalten auf den Web-Portalen: schickt den
Anmeldelink an die hinterlegte Firmen-E-Mail (sonst Pressekontakte), zeigt
sie nur maskiert, und funktioniert auch für Firmen ohne veröffentlichte
Meldung. Konto, Zuordnung und Login laufen durch ContactAccessService —
neu dort: Herkunft (profile_claim auf dem Token) und Ziel-Firma, auf deren
Kundenseite der Login landet. 410-entfernte bleiben Tombstone, aufgegangene
Slugs führen zum Überlebenden (MergedNewsroomLookup, geteilt mit der
Newsroom-Seite). 3 Links je Newsroom und Tag, 15 je IP und Stunde.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 10:42:20 +00:00
0be476b821 dev: Block 1a in der Roadmap als erledigt markiert, 1a.4-Entscheidung festgehalten
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 10:32:33 +00:00
8baca9dd78 api: neue Firmen dem bestehenden Newsroom zuordnen (Roadmap 1a.4)
connektar legt je Meldung eine Firma an. ApiNewsroomAssignment prüft vor
dem Anlegen, ob unter derselben Firmen-E-Mail schon dieselbe Firma steht
(Namensregel der Klassifizierung), und gibt dann deren ID zurück — der
Alt-Vertrag (201 + Location) bleibt. Nie-veröffentlicht-Adressen werden nie
zugeordnet, abweichende Namen bekommen wie bisher eine neue Firma. Liegt
die Firma auf dem anderen Portal, wird sie both; bei fremd belegtem Slug
bleibt es beim alten Verhalten.

LegacyPressReleaseController: both ist kein Ziel-Portal für eine Meldung —
dann entscheidet das Portal des API-Nutzers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 10:26:13 +00:00
d60f31d78e dev: kompletten Live-Lauf der Zusammenführung protokolliert
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 10:15:07 +00:00
cd16fd14f3 dev: Live-Läufe der Zusammenführung protokolliert
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 09:51:58 +00:00
202cf340f9 companies: Zusammenführungen zum Nachprüfen auflisten
companies:merges --batch= zeigt je Vorgang Überlebenden, Newsroom-URLs auf
beiden Portalen, aufgegangene Firmen und deren alte URLs (301). Der scharfe
Lauf gibt die ersten 30 Vorgänge jetzt ebenfalls als Tabelle aus.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 09:10:54 +00:00
1368e2cf07 dev: Live-Trockenlauf der Zusammenführung protokolliert
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 08:58:30 +00:00
8b86b88614 companies: Zusammenführung im Bestand, rückabwickelbar (Roadmap 1a.3)
companies:merge führt Firmen mit derselben Firmen-E-Mail zu einem Newsroom
zusammen — nur über Adressen, die 1a.2 als Dublette oder Namensvariante
klassifiziert hat. Es bleibt die Firma mit den meisten jemals
veröffentlichten Meldungen; Meldungen, Kontakte, Nutzer- und
Zahlungsverknüpfungen wandern per company_id, jede Bewegung steht in
company_merge_moves. companies:unmerge spielt sie rückwärts ab.

77 % der Gruppen sind dieselbe Firma auf beiden Portalen (Altsystem: eine
Firma je Portal). Der Überlebende wird dann `both`; die Meldungen behalten
ihr Portal. Der alte Newsroom-Slug führt per 301 zum Überlebenden.
Slug-Kollisionen auf dem anderen Portal werden übersprungen, nicht
umbenannt. Company::slugExists() kennt jetzt `both`.

Dev-DB: 14 142 Adressen, 16 194 Firmen, 13 297 Meldungen in 2 min;
Rückabwicklung in 1:15 min exakt auf den Stand davor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 08:43:46 +00:00
00af8b1d8a companies: Aufrufe als Veröffentlichungsnachweis im Altbestand werten
Das Altsystem hat 2009–2018 rund 126 000 Meldungen auf `rejected` gesetzt,
83 000 davon mit Aufrufen — die waren online und sind pauschal offline
genommen worden. Wer nur `status = published` zählt, stellt pr4you, Blödorn
und Crossover Point mit Hunderten Meldungen neben yahoo@yahoo.com.

Eine Firma gilt jetzt als veröffentlicht, wenn sie eine Meldung mit
`published` ODER `hits > 0` hat. Nie veröffentlicht: 23 → 7 Adressen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 10:20:57 +00:00
e2f7cf1624 dev: scharfer Klassifizierungslauf auf Live protokolliert
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 10:02:26 +00:00
5e5414ad20 dev: Live-Trockenlauf der Adress-Klassifizierung protokolliert
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 09:50:17 +00:00
5e46ff9f68 companies: Adress-Klassifizierung persistieren (Roadmap 1a.2, E1 = 3)
Die Klassifizierung aus dem Trockenlauf wird jetzt nach company_email_groups
geschrieben — eine Zeile je Adresse, denn ob zusammengeführt werden darf, ist
eine Eigenschaft der Adresse und nicht der Firma. Die Zusammenführung (1a.3)
und die laufende Zuordnung neuer API-Meldungen (1a.4) lesen dort, statt den
Trockenlauf jedes Mal neu zu rechnen.

Die Logik liegt jetzt in CompanyAddressClassifier und wird von Bericht und
Persistierung gleichermaßen benutzt. Beide müssen dieselbe Antwort geben —
sonst zeigt der Bericht etwas anderes, als die Zusammenführung später tut.

manual_classification übersteuert die berechnete Klasse und überlebt jede
Neuberechnung. Nötig für Grenzfälle, die nur ein Mensch entscheiden kann:
presse@pr4you.de ist eine echte Agentur, sieht in den Daten aber aus wie eine
Wegwerfadresse. effectiveClassification() ist die einzige Stelle, die
Abnehmer fragen dürfen.

Eigene Tabelle statt Spalten auf companies: Die Tabelle trägt einen
FULLTEXT-Index, jedes ADD COLUMN dort ist ein Rebuild. Die Zuordnung Firma →
Gruppe ist ohnehin nur die normalisierte Adresse, und die eine Stelle, die
festlegt, was "dieselbe Adresse" heißt, ist jetzt Company::emailKey().

E1 ist entschieden: Schwellwert 3, nach drei Live-Läufen. Bei 2 wird jede
zweite Schreibweise zur "Agentur", bei 4 wandern echte Mehrkunden-Adressen in
die Handarbeit. Konfiguriert in config/newsroom.php; Bericht und
Klassifizierung lesen den Wert von dort.

Die Ähnlichkeitsprüfung ist quadratisch und jetzt bei 25 Repräsentanten
gedeckelt — ab da steht die Klasse fest, und yahoo@yahoo.com mit über 4000
Namen hätte sonst neun Millionen Levenshtein-Aufrufe gekostet. Laufzeit über
den ganzen Bestand: 25 s → 3 s, Ergebnis unverändert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 09:11:09 +00:00
2691a4d988 conversion: Konversionsleiter mit Herkunft auf dem Magic-Link-Token (Block 0)
Die zu messende Strecke führt von einer Mail über presseecho.de per Magic Link
auf pressekonto.com und endet in einer Buchung — drei Wechsel, zwei Domains. Ein
cookiefreies Analytics-Tool verliert die Sitzung genau dort, wo die interessante
Konversion passiert. Claim, Registrierung und Buchung sind außerdem keine
Seitenaufrufe, sondern Zustandsänderungen in dieser Datenbank, bei denen die
Identität ohnehin bekannt ist. Deshalb eigene Ereignisdaten statt Umami; Umami
behält Seitenaufrufe und Suchbegriffe.

`conversion_events` hält Ereignistyp, Herkunft sowie Firmen- und Meldungsbezug
— keine Personen, keine Klartextadressen, keine IP. Der rote Faden durch den
Trichter ist die Firma; für die Frage "an welcher Stufe bricht es ab" reicht das
vollständig.

Der Kern ist die Herkunft auf dem Token (neue Spalte magic_links.source). Ohne
sie weiß die Auswertung am Ende, dass jemand gebucht hat, aber nicht, ob er aus
einer Ablehnungsmail kam. Eine Session überlebt den Domainwechsel nicht, das
Token schon. Die Mail-Links tragen zusätzlich `?src=` und `&pr=`; die
Meldungs-ID ist fälschbar, schaltet aber nichts frei und verfälscht im
schlimmsten Fall nur unsere eigene Statistik. Mit dem Magic-Link-Zugang aus 1b.2
entfällt sie.

Fünf Stufen statt gleichrangiger Punkte, jede mit ihrer Diagnose am Enum: Vier
beliebige Konversionspunkte sagen nicht, WO es hakt. Geschrieben wird an Stufe 1
(globale Middleware — die Mail-Links zeigen auf Hub, Spokes und öffentliche
Seiten, eine vergessene Route wäre eine unsichtbare Lücke), Stufe 2
(Magic-Link-Einlösung), Stufe 4 (Registrierung) und Stufe 5 (beide
Zahlungspfade, je Firma dedupliziert — eine zweite Buchung ist keine erste).
Stufe 3 ist definiert und wird ausgewertet; ihre Schreibstelle kommt mit der
Profilpflege aus 1b.3.

Für alle Schreibstellen gilt: Fehler werden geschluckt. Die Messung begleitet
einen Vorgang, sie ist nicht der Vorgang — eine kaputte Statistik darf keine
Anmeldung und erst recht keine Buchung scheitern lassen.

`conversion:funnel` zählt je Stufe die eindeutigen Firmen, nicht die Ereignisse:
Ein doppelter Klick ist kein doppelter Interessent, und nur mit derselben
Einheit auf allen Stufen ist ein Übergang überhaupt eine Quote. Ereignisse ohne
Firmenbezug stehen daneben statt im Nenner.

Aufbewahrung 395 Tage, zusammen mit der Tabelle festgelegt statt nachgetragen,
täglich im Scheduler bereinigt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 13:12:12 +00:00
2391c853e3 reports: Aufrufe nach Alter aufschlüsseln und Basiswerte festhalten
Der Gesamtmedian über ein Zeitfenster taugt nicht als Basiswert. `hits` ist ein
mitlaufender Gesamtzähler, und die Aufrufe wachsen nach der Veröffentlichung
über Wochen weiter — auf dem Bestand liegt der Schnitt in der ersten Woche bei
rund 75 und nach drei Wochen bei rund 230, ohne dass ein Plateau erreicht wäre.
Gemessen wird damit nicht, wie viele Aufrufe eine Meldung bekommt, sondern wie
viel eine zufällig alte Meldung bisher gesammelt hat. Dieser Wert verschiebt
sich mit der Altersmischung des Fensters; zwei Messungen zu verschiedenen
Zeitpunkten wären nicht vergleichbar, und genau das braucht ein Basiswert.

Der Report weist die Aufrufe deshalb zusätzlich nach Alterskohorten aus. Erst
die einzelne Kohorte ("Median der Meldungen im Alter von 28 bis 55 Tagen") ist
in einem halben Jahr identisch erhebbar. Der Altbestand bleibt außen vor — seine
Zähler stammen aus dem Altsystem und haben keine hier nachvollziehbare
Vorgeschichte.

Dazu die am 01.09. auf Live erhobenen Basiswerte und die daraus folgende
Empfehlung zu E2: Rohzahlen. Es gibt keinen Bodensatz, den eine Einordnung
kaschieren müsste (unter 25 Aufrufen liegen 2 % beziehungsweise 0 % der
Meldungen), die Verteilungen sind nahezu symmetrisch, und eine Einordnung gegen
den Portaldurchschnitt wäre wegen der Wachstumskurve sogar falsch — sie
verglich eine zwei Tage alte Meldung gegen einen Schnitt aus drei Monaten.

Die Roadmap führte openPR als zweite Zulaufquelle. Das ist korrigiert: openPR
ist der Presse-API-Verteiler von connektar, also derselbe Zulauf. In den Daten
hat genau ein API-Token je einen Zugriff gemacht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 12:40:51 +00:00
aab22401d0 reports: Basiswerte für die Konversionsmessung (Roadmap 0.1 und 0.2)
Beide Werte müssen gemessen sein, bevor die erste Maßnahme aus Block 1 live
geht — ohne Vergleichspunkt wäre jede spätere Bewertung eine Vermutung.

press-releases:view-distribution gibt die Verteilung der Aufrufzahlen je Portal
aus: Median, Quartile, P10/P90 und eine Häufigkeitsverteilung. Ein Mittelwert
allein trägt hier nicht — eine Handvoll Ausreißer zieht ihn nach oben, während
der typische Absender darunter liegt. Genau davor soll E2 schützen, deshalb
wird die Schiefe (Mittelwert gegen Median) ausdrücklich mit ausgewiesen.

Altbestand und seit dem Relaunch eingegangene Meldungen stehen getrennt: Ihre
Zähler haben unterschiedliche Vorgeschichten, und eine Einordnung, die beide
vermischt, vergleicht Äpfel mit Birnen. Mit 410 entfernte Meldungen zählen
nicht mit, eine Meldung auf beiden Portalen bei beiden.

press-releases:rejection-rate gibt die Ablehnungsquote je Einreichungsquelle
aus. Gezählt wird je EINREICHUNG, nicht je Meldung: Ein Übergang nach `review`
eröffnet eine Einreichung, die nächste Entscheidung schließt sie. Eine
überarbeitete und erneut eingereichte Meldung ist damit ein zweiter Vorgang mit
eigenem Ausgang — und genau das ist die Größe, die die Infoseite (1d) später
verändern soll. Gruppiert wird nach dem Einreichenden, nicht nach dem
Entscheider; letzterer ist immer die KI-Prüfung und als Gruppierung wertlos.
Jeder API-Client bekommt eine eigene Zeile, Einreichungen aus dem Kundenbereich
fallen zu "direkt" zusammen.

Noch offene Prüfungen zählen nicht in den Nenner, sonst sänke die Quote allein
dadurch, dass der Zeitraum bis heute reicht. Entscheidungen zu Einreichungen
von vor dem Zeitraum werden gemeldet statt verschluckt — ein zu kurz gewählter
Zeitraum sähe sonst wie eine niedrige Quote aus.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 11:57:25 +00:00
81d9c43ab7 dev: Bestandsaufnahme und Umsetzungsprotokoll zum Code-Durchgang 01.09.
Vollständige Prüfung von app/, den Migrationen und dem gesamten bis dahin nicht
committeten Arbeitsstand: gefundene Fehler nach Schwere, die Ursachenanalyse
zur unbrauchbar gewordenen statischen Analyse, und die Voraussetzungen, die die
Roadmap braucht und die noch fehlten.

Das Umsetzungsprotokoll hält je Befund fest, was geändert wurde, welche
Vorschläge abgewandelt oder verworfen wurden und warum — darunter zwei
Falschbefunde meiner eigenen Bestandsaufnahme, die beim Beheben aufgefallen
sind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 11:08:07 +00:00
99f10e6bf0 docs: R6.5 in der Roadmap abgehakt (Merge nach main + Tag v1.0.0)
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 07:58:20 +00:00
8aba835e9d docs: verschobene Doku-Pfade in Code-Kommentaren nachgezogen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 07:53:08 +00:00
665c94f162 docs: dev-Doku für den Live-Betrieb neu sortiert — Archiv + Betrieb + Roadmap
Das System ist seit 20.07.2026 live; die dev-Doku spiegelt jetzt den
Betriebszustand statt der Release-Vorbereitung:

- dev/betrieb/: Deploy-Runbook (neu: §10 Betrieb nach Go-Live mit
  SFTP-Deploy-Checkliste inkl. queue:restart-Pflicht und den
  Betriebs-Erkenntnissen der Go-Live-Woche), Legacy-API-Parität, Umami.
- dev/roadmap.md: offene Nachlauf-Punkte (Go-Live-Mails, Merge nach main,
  Stripe-Test-Artefakte, Key-Rotation, Alt-System-Abschaltung) und
  Ideen-Backlog.
- dev/archive/: abgeschlossene Phasen — Migration-2026-Planung,
  Cutover-Runbook (mit Erledigt-Vermerk), Release-Plan/QA-Checkliste,
  Optimierungsphasen, Frontend-Konzepte + Responsive-Audits vom 16.07.,
  Altsystem-Reste.
- dev/README.md als Wegweiser; gitignore-Pfad der SQL-Dumps mitgezogen
  (dev/archive/migration-2026/sql/ bleibt untracked, ebenso dev/migration/
  und dev/api/).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 07:52:38 +00:00
c8a292eb8c docs: Cutover-Delta nutzt --since, Medien-Delta-Hinweis
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 13:03:35 +00:00
263f030a6e docs: Runbooks auf den echten Migrationsstand nachgezogen (16.07.)
Cutover-Runbook: Legacy-DBs liegen bei KAS und sind als lokale Snapshot-
Kopien eingespielt (Delta-Läufe brauchen vorher frische Dumps); privatize-
media läuft mit --prune-missing; retire-agency-type als Satellit ergänzt;
verlorene Presseecho-PM-Bilder (~22k) als bewusste Entscheidung dokumentiert.
Deploy-Runbook: Seeder-Liste um LegalDocumentSeeder/RejectionCategorySeeder
ergänzt, billing:sync-stripe-plans nach frischem Seeding, Tinker-Befehl für
die Admin-Rolle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:50:59 +00:00
6253b36891 docs: Cutover-Runbook für den Go-Live (Legacy-Import & Umschaltung)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 16:07:13 +00:00
6f6cb13f2e docs: Audit-Protokoll, KI-Kennzeichnung & Release-Notizen
- dev/optimize/optimize-14-07-2026.md: Security-/Code-Audit inkl.
  Fix-Protokoll (Befunde gegen den Code verifiziert).
- docs/weiteres/KI-Kennzeichnung.md, Pm Tracken.md, Decision-Updates und
  Release-/Entwicklungsplan aktualisiert.

Hinweis: Der Audit-Bericht enthält keine echten Geheimnisse (nur die
öffentlichen Key-Präfixe), aber eine vollständige Befundübersicht.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 13:50:41 +00:00
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
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
d92e0f65c8 Doku: Boost-Platzierung entschieden + umgesetzt (Slot-Grenzen, Kategorie-Boost)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 12:28:03 +00:00
b1be14e1b3 Doku: Boost-Platzierung offene Punkte (Hero-Slider, Boost-Begrenzung) fuer Montag
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 17:26:49 +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
fef45733df API-Legacy-Paritaet: Arbeitsdokument mit Alt-Vertrag, Log-Auswertung, Schrittplan
- Alt-Vertrag komplett aus dem Symfony-Code extrahiert (Routen, Params
  verschachtelt, Response-Shapes, Sofort-Publish-Logik, Fehlerformat)
- Prod-Logs (dev/api, User) ausgewertet: ein Client (connektar),
  5 Endpoints; Company-/Kontakt-Create heute 410 -> muessen nachgebaut
  werden; Sofort-Publish beim POST belegt
- Entscheidungen 10.07.: Sofort-Publish + Admin-Schalter KI-Pruefung,
  getrennte Legacy-/v1-API, Ablehnungs-Rueckkanal spaeter
- dev/api/ enttrackt (.gitignore, Produktivdaten analog SQL-Dumps)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 13:40:24 +00:00