Commit graph

147 commits

Author SHA1 Message Date
ff75159b8f web: RSS-Feed je Newsroom, RSS-Übersichtsseite, „RSS" in der Topleiste (Roadmap 3.1, Nachtrag)
- /{edition}/newsroom/{slug}/feed: Sichtbarkeit wie die Newsroom-Seite
  (aktiv, nicht entfernt, mindestens eine Meldung), Prüfung vor dem Cache,
  301 bei zusammengeführter Firma; Autodiscovery und Link „RSS-Feed" auf
  der Newsroom-Seite; Hub leitet auf das Portal der Firma.
- /{edition}/rss (RssPageController, static.rss DE/EN): Was RSS ist, wie
  ein Reader funktioniert, Hauptfeed, alle Branchen-Feeds mit Adresse,
  Muster des Newsroom-Feeds, Hinweise. Portalseite mit Portal-Kontext,
  Hub → presseecho, in Sitemap und llms.txt.
- Topleiste, Burger-Menü und Footer verlinken RSS neben FAQ/Hilfe/API.
- FeedController in portal/category/newsroom aufgeteilt; Panel
  „Auslieferung" nennt zusätzlich den Newsroom-Feed.
- Tests: FeedTest (+3), RssPageTest (4), StaticPagesTest, VisibilityStatsTest.
- dev/2026-09-01: Nachtrag in Protokoll Teil 11 und Roadmap 3.1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 08:27:14 +00:00
9d5e5a0b54 web: RSS-Feeds je Branche und je Portal (Roadmap 3.1); dev: Sichtung des Ordners, Protokoll-Teile umnummeriert
- /{edition}/feed und /{edition}/category/{slug}/feed (FeedController,
  RSS 2.0 mit atom:link self, dc:creator, category; jüngste 50 der Ausgabe,
  Branche inkl. aktiver Unterkategorien, nur visibleInArchive).
- Cache über WebPageCache (Key-Gruppe feed), Hub leitet per 301 aufs Portal
  (canonical.portal_only_content_routes).
- Autodiscovery über @yield('feed_links') auf Start-, Branchen- und
  Archivseiten, sichtbarer Link in der Archiv-Leiste, llms.txt nennt die
  Feed-Muster, Block „RSS-Feeds" im Panel „Auslieferung"
  (VisibilityStats::feedLinks).
- Übersetzungen unter web.rss (web.feed ist die Startseiten-Liste).
- Tests: FeedTest (7), VisibilityStatsTest erweitert.
- dev/2026-09-01: Sichtung 18.09. (Teil 11), zweite Teil-Serie des
  Protokolls jetzt 7–10 mit nachgezogenen Verweisen, Anhang 1 abgeglichen,
  E6 abgehakt, 2.6 extern, 1c.5/E8 wartet auf Daten, 3.1 abgehakt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 13:57:09 +00:00
5122624687 auth: automatische Firmenzuordnung beim Wegfall der E-Mail-Grundlage entfernen statt senken (Block 7.13, U01, E7)
- Login-Sync löscht auto-Pivots ohne aktuellen Treffer; member wäre die
  Teamrolle mit Schreibrecht auf alle Firmen-PMs, keine Leserolle
- hochgestufte manuelle Mitgliedschaften tragen granted_via=auto-upgrade
  und fallen auf member zurück; manual und owner bleiben unberührt
- company-memberships:audit --classify stuft nur manual-Zeilen um
- Plan: E2 revidiert, E7 entschieden, 1.5 und 7.13 nachgezogen

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 13:00:48 +00:00
c26bdff681 admin: Bericht „Indexierung & Abrufe" — Rolle Erstquelle/Kopie, Quellenbefund, Crawler-Abrufe; Schlüsselpfad ab Projektwurzel
Der Bericht meldete auf Live „nicht konfiguriert", während der Lauf
längst Daten holte: der relative Pfad der Schlüsseldatei war im Cron
(Projektwurzel) lesbar, im Web-Request (public/) nicht. Relative Pfade
werden jetzt ab base_path() aufgelöst.

Die Verteilung trennt zusätzlich nach Rolle in der kanonischen Zuordnung
(1c.8): bei einer Kopie mit Verweis ist „ausgeschlossen" der erwartete
Befund. Neu: Quellenbefund über Erstquellen (welche Adresse Google führt,
fremde Hosts), Abrufe durch Such- und KI-Dienste je Dienst, Tag und
Meldung mit CSV. Herkunftsmarker als Join statt korreliertem EXISTS,
Aggregate 15 Minuten gecacht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 12:53:22 +00:00
1c421c0377 dev: Umsetzungsaudit vom 18.09. abgelegt, Block 7 mit Abgleich, Entscheidungen E7–E9 und Protokoll im Entwicklungsplan
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 12:22:43 +00:00
c46dfd3b83 dev: Protokoll — Gesamtsuite grün, zwei zufallsabhängige Tests festgezurrt
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 10:56:21 +00:00
226fb8131a dev: Umsetzungsprotokoll und Live-Schritte zum Audit-Entwicklungsplan
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 10:41:58 +00:00
72432fdb82 web/api/ki: Soft Deletes wieder wirksam, Caches bei Firmenwechsel, KI-Ergebnis an Inhalt gebunden (Audit Block 3+4, Deploy D)
Block 3 — Sichtbarkeit, Löschung, Cache, Eingaben

F04: withoutGlobalScopes() ohne Argument entfernte neben dem PortalScope
auch den Soft-Delete-Filter. Öffentliche, Kunden-, API-, Vorschau- und
Medienpfade sowie die PressRelease-Helfer schreiben jetzt
withoutGlobalScope(PortalScope::class). Zwei Stellen, die der Audit nicht
kannte: die öffentliche Meldungsseite zeigte gelöschte Firmenkontakte mit
Name, Mail und Telefon; das Pressefach listete gelöschte Kontakte weiter
und ließ sie bearbeiten. Ein Architekturtest friert die 39 bewusst
verbliebenen Stellen (Admin, Importer, Befehle, Slug-Eindeutigkeit) ein.
Der Vorschau-Test prüfte bisher forceDelete statt delete.

F22: LOWER(email) machte auf companies (110k Zeilen) bei jedem Login einen
Vollscan; die Kollation ist ohnehin case-insensitiv. EmailQuery vergleicht
auf MySQL direkt über den Index, auf SQLite (Tests) weiter per LOWER.
Neuer Index contacts_email_idx.

F13: Firmen-Observer verwirft die öffentlichen Seiten-Caches bei
Löschung, Wiederherstellung und is_active-Wechsel auf jedem Weg; die
Newsroom-Seite prüft Sichtbarkeit vor dem Cache-Treffer; Kontakt-
Änderungen im Pressefach flushen. Der Tombstone-Test wärmte den Cache
vorher nicht und traf den Fehler nie.

F12: Gemeldete URLs werden über Host, Ausgabe und Slug aufgelöst
(ReportedUrlResolver); fremde Hosts zählen nicht, bei Doppelveröffentlichung
gewinnt die Erstquelle.

F11: per_page 1–100, status als Enum, language nur de/en, Kategorie muss
aktiv sein und zum Ziel-Portal passen. Legacy-Aliasse behalten ihren
Vertrag.

F23: Rückgabewert von put() wird geprüft; Logo erst speichern, dann
Datenbank, dann altes Logo löschen. Fehler enden als Feldfehler, nicht
als verlorenes Logo.

Block 4 — KI-Prüfung und Scheduler

F07: Klassifikation und Score tragen die Prüfsumme des Inhalts bei
Einplanung. Weicht sie beim Speichern ab, wird das Ergebnis verworfen
(ki_audits.discarded_at) und ein neuer Lauf eingeplant. Der Scheduler
veröffentlicht keine Meldung, deren jüngste Klassifikation nicht zum
aktuellen Text passt, sondern plant eine Prüfung mit Routing ein.

F18: Kontingent an den Kalendermonat gebunden (users.press_release_quota_period).
Verbrauch aus einem früheren Monat zählt null, auch ohne Reset-Cron; der
Reset ist idempotent und lässt den laufenden Monat in Ruhe.

F20: Der Publikations-Scheduler liefert bei technischen Fehlern einen
Fehler-Exit-Code, damit der Failure-Hook greift.

F17: KI-Jobs mit $timeout = 120, retry_after 150; Runbook ergänzt.

F19: Aggregation der Aufrufe unter einem Cache-Lock für alle Einstiege.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 10:40:48 +00:00
1d76f5a733 auth: Firmenrechte nur aus nachgewiesener Adresse, 2FA auf allen Login-Wegen (Audit Block 1, Deploy A)
Audit vom 11.09. gegen HEAD geprüft, Entwicklungsplan dazu unter
dev/2026-09-11-audit/ENTWICKLUNGSPLAN.md. Dieser Commit ist Deploy A:
die sechs kleinen Punkte aus Block 1 ohne Schemaänderung.

F01: Der Fortify-Endpunkt PUT /user/profile-information tauschte die
E-Mail sofort und setzte die Verifizierung zurück; der Login-Sync vergab
danach Firmenrechte allein nach E-Mail-Treffer. Feature abgeschaltet
(die Kunden-UI hat den pending_email-Prozess), und der Sync verlangt
jetzt ein verifiziertes, aktives Konto – für alle Aufrufer.

F02: Google-Login übergibt Konten mit bestätigtem TOTP in die Challenge,
wie Passwort- und Magic-Link-Login.

F03: showRecoveryCodes ist gesperrt; ein Client-Update konnte die
Recovery-Codes entschlüsseln lassen.

F08: Nutzer-Detailroute unter can:users:manage (persistente Livewire-
Middleware), Gutschriften autorisieren zusätzlich in der Action.

F09: Gate::before greift nur für aktive Super-Admins; deaktivierte
fallen auf die Gates mit is_active-Prüfung zurück.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 10:08:55 +00:00
edb2d49199 web: Portal-Inhalte sind auf dem Hub nicht mehr erreichbar (Anhang 2, E10)
Kategorien, Jahresarchive, Meldungen und Newsrooms liefen auch unter
pressekonto.com — die Ausgabe-Routen gelten für alle Hosts, und der
Portal-Kontext fällt dort auf businessportal24 zurück. Jetzt 301 auf das
Portal.

Meldung und Newsroom folgen ihrem eigenen Portal, nicht einer festen
Vorgabe; bei Doppelveröffentlichung (gleicher Slug auf beiden Portalen)
gewinnt die Erstquelle, sonst endete die Weiterleitung auf der Kopie.
Kategorie, Archiv und Suche gibt es auf beiden Portalen — dort entscheidet
canonical.portal_only_host.

Bewusst nicht weitergeleitet wird ein unbekannter Slug (sonst 301 in ein
404) und alles, was kein GET ist (ein 301 auf POST verliert die Anfrage —
Newsroom beanspruchen und Share-Zählung laufen weiter). Query-Parameter
bleiben erhalten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 09:15:04 +00:00
5dcaf4e7b0 web: E9 und E10 entschieden — Zuordnung aktiv, eine Adresse je statischer Seite, Hub-Portalseiten leiten weiter
E9: Kategorie-Zuordnung wie vorgeschlagen übernommen, Zuordnung ist
standardmäßig an (CANONICAL_ASSIGNMENT_ENABLED). Auf der Dev-Kopie
verifiziert: Kopie trägt rel=canonical auf die Erstquelle und fällt aus der
Sitemap ihres Portals.

E10: faq, hilfe, gute-pressemitteilung, die vier Transparenzseiten und die
API-Doku lagen auf allen drei Hosts mit identischem Text und je eigenem
Self-Canonical. Jetzt führt presseecho die Informationsseiten, der Hub die
API-Doku; Sitemap und llms.txt nennen nur die führende Adresse.
Rechtstexte (Impressum, Datenschutz, AGB, Richtlinien, Meldeweg,
Kündigung) bleiben bewusst je Host — jede Domain ist ein eigenes Angebot
mit eigener Anbieterkennzeichnung.

Bei der Erreichbarkeitsprüfung des Hubs gefunden und behoben:
/de/preise, /de/veroeffentlichen, /de/newsrooms und /de/suche wurden unter
pressekonto.com mit dem Branding von businessportal24 ausgeliefert (der
Portal-Kontext fällt dort darauf zurück) — jetzt 301 auf presseecho, die
Transparenz-Navigation verlinkt direkt dorthin. /de/kontakt und
/de/ueber-uns sprangen über /de auf die Startseite, jetzt in einem Schritt.

Der Self-Canonical kommt jetzt aus dem aufgerufenen Host; Portal-Inhalte,
die über den Hub laufen, behalten ihr Portal (webPortalContextApplied).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 07:52:09 +00:00
4edb915a9a web: Doppelveröffentlichung — Prüfsumme, kanonische Zuordnung (aus bis E9), Quellenauskunft, noindex im Portal (Anhang 2)
Über die API kommt praktisch jede Meldung zweimal, je Portal eine Zeile
mit gleichem Text: 9.092 Paare, 53 % des Archivs, bisher jede Fassung
self-canonical und in beiden Sitemaps — Google wählte.

1c.7: press_releases.content_hash (SHA-256 über Titel + Text ohne Markup),
gesetzt beim Speichern, Bestand per press-releases:hash-content
(Query-Builder, Blöcke). Migration in zwei Statements — ADD COLUMN INSTANT,
ADD INDEX INPLACE, kein Rebuild. press-releases:duplicates-report zählt
Paare je Hauptkategorie.

1c.8: canonical_press_release_id + CanonicalAssigner. Erstquelle folgt der
Kategorie (config/canonical.php, Vorschlag zu E9), sonst die zuerst
veröffentlichte Fassung. Zuordnung bei jeder Veröffentlichung und über den
Bestand (press-releases:assign-canonicals). Die Kopie trägt rel=canonical
und mainEntityOfPage auf die Erstquelle; Sitemap und News-Sitemap nennen
nur sie; Leser sehen weiterhin beide. Alles hinter
CANONICAL_ASSIGNMENT_ENABLED (false), bis E9 entschieden ist.

1c.9: user_canonical in den Index-Prüfungen, sourceVerdict() unterscheidet
eigene Adresse / andere eigene Fassung / fremdes Portal; Anzeige im Block
„Google-Index" hinter GSC_CUSTOMER_VISIBLE, ohne Aussage über das Warum.

Dazu: Abschnitt zur einen Quelle auf /gute-pressemitteilung (Verteiler →
viele Kopien, eigene Website zuerst, keine Garantie), noindex in Admin-,
Kunden- und Auth-Layouts, UmamiSnippetTest von der lokalen .env gelöst.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 16:09:16 +00:00
87558aba4f web: Hilfe in der Topleiste neben FAQ; 2.5/E5 geschlossen
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:40:41 +00:00
d9e181a03e docs: Search Console live, Schlüsseldateien ausgeschlossen, Kontingent erklärt
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:29:19 +00:00
375138679d 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
106edb1384 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
4616bb7aad 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
b60a28dc9c 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
8cd76df0bb 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
8ea5e52c84 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
5d8e379e85 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
4dffb1368f 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
9380937562 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
13716b9c06 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
3ef1b84600 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
a00f9dc79a 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
f581e1eb3a 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
4cefdf7f5c 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
6e159ecf06 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
e7e12b9436 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
f9c97ccf2f 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
8bf40ee37e 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
85b039e739 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
7f714fb664 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
0c1c072487 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
0f52e9fddc 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
8dad652d67 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
4303266841 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
b91f41d408 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
235c20ce1d 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
f3e7aca0f2 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
2aebb9d27e 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
77318dcb79 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
8a1e79aebb 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
c17e4b9346 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
a0ac32cf95 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
45f2d90e4e 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
748a0d4b7e 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
c497628af3 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
ac4b9596da 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