Commit graph

406 commits

Author SHA1 Message Date
9a88e82baa billing/dev: Vertragsende in der MAN-Abrechnung, Rechnungslauf ohne Tageskappe, Qualitätsgates (Audit Block 5+6, Deploy E)
Block 5 — MAN-Abrechnung

F06 (E6): Ein explizites Legacy-Vertragsende (grandfathered_until) deckelt
jetzt Auswahl und Sperr-Nachprüfung: Beginnt die nächste Periode nach dem
Vertragsende, gibt es keine Rechnung mehr. Neuer täglicher Befehl
billing:expire-grandfathering (04:15, vor dem Rechnungslauf) setzt
abgelaufene Posten auf gekündigt und schickt je Kunde eine Mail
(GrandfatheringExpired). Auf der Dev-Kopie trägt kein Posten ein
Vertragsende; der Fehler war latent.

F16: Nicht abrechenbare Vereinbarungen bekommen einen Sperrvermerk
(billing_hold_reason/-at), sichtbar auf der Admin-Zahlungsseite, und
stehen im Rechnungslauf hinten an. Der Lauf arbeitet den fälligen Bestand
in Seiten ab; --limit deckelt die erstellten Rechnungen (500), nicht die
betrachteten Fälle. Vorher blockierten 50 unvollständige Fälle die 51.
dauerhaft, und der Durchsatz war auf 50 am Tag begrenzt.

Block 6 — Entwicklung

F24: Lint-Workflow prüft mit pint --test statt zu formatieren; keine
Schreibrechte mehr.
F27: composer run dev startet beide Vite-Server (npm run dev:all).
F28: docs/README.md als historischer Stand markiert, toter Link auf das
Hub-Flux-Archiv korrigiert, aktueller Einstieg dev/README.md.
F26: Login-Modal übernimmt den Fokus, hält Tab im Dialog und gibt den
Fokus beim Schließen an den Auslöser zurück – ohne neue Abhängigkeit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 10:41:04 +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
301bca3a90 auth: automatische Firmenrechte mit Herkunft und Entzug, Nachweis vor Sicherheitsaktionen (Audit Block 1, Deploy C)
F10: company_user.granted_via unterscheidet automatisch vergebene
Rechte (E-Mail-Sync) von manuellen und aus dem Altbestand. Beim Login
senkt der Sync automatische responsible-Rechte ohne aktuellen E-Mail-
Treffer auf member (E2) — Mitarbeiterwechsel nehmen den Schreibzugriff
mit, Lesezugriff bleibt. Das Admin-Formular markiert nur neue oder
geänderte Zeilen als manual, unveränderte behalten ihre Herkunft.

Bestandsprüfung: php artisan company-memberships:audit listet
responsible-Zuordnungen ohne E-Mail-Grundlage (Tabelle ab Go-Live, CSV
vollständig) und markiert mit --classify die erklärbaren als auto. Auf
Live nach der Migration einmal mit --classify laufen lassen; nicht
erklärbare Zeilen bleiben manual und damit geschützt.

F03 (E3): Zwei-Faktor deaktivieren, Wiederherstellungs-Codes erneuern
und die E-Mail-Änderung verlangen einen aktuellen App-Code oder einen
Wiederherstellungs-Code (der dabei verbraucht wird); ohne 2FA das
Passwort. Reine Magic-Link-Konten ohne Passwort (der Altbestand) können
die Adresse weiter ändern — die neue Adresse muss ihren Bestätigungslink
ohnehin einlösen. Fünf Fehlversuche pro Minute.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 10:19:28 +00:00
eeb1cde48d newsletter: jeden Empfänger genau einmal, atomarer Start, Wiederanlauf (Audit Block 2, Deploy B)
F05: chunkById hinter der created_at-Sortierung der Vorschau las ab der
zweiten Seite Empfänger doppelt und ließ die übrigen aus. Auf der Dev-
Kopie erreichte eine Kampagne an 59.705 Nutzer 500 Adressen, davon 480
doppelt. Die Sortierung wird vor dem ID-Chunking verworfen (E4: die
Versandreihenfolge hat keine fachliche Bedeutung), ein Unique-Index auf
(Kampagne, User) fängt jede weitere Doppelung ab; die Migration räumt
vorhandene Doppelungen vorher weg (auf der Dev-Kopie 470 Zeilen).

F14: Der Start übernimmt den Entwurf per bedingtem UPDATE draft→sending;
nur der Gewinner friert Empfänger ein (insertOrIgnore) und plant Jobs
aus dem Protokoll ein. Abbruch beim Einfrieren gibt den Entwurf frei.

F15: Der Versandjob beansprucht die Zeile pending→sending, schreibt
Empfängerstatus, Zähler und Kampagnenabschluss in einer Transaktion und
gibt den Claim bei Versandfehler zurück. Tries und Backoff am Job.
Zusatzbefund: Adressen auf der Sperrliste galten als „gesendet“, weil der
Rückgabewert von Mail::send nicht ausgewertet wurde — jetzt Status
„suppressed“ ohne Zähler.

Neu: php artisan newsletter:resume {kampagne} [--failed]
[--stuck-minutes=30] plant ausstehende Empfänger einer laufenden
Kampagne neu ein; der Circuit-Breaker-Hinweis nennt ihn statt
queue:retry, das Newsletter-Empfänger nicht nachholt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 10:19:28 +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
3c6fb69032 admin: URL Inspection immer auf en-US abfragen
coverageState ist ein übersetzter Anzeigetext; mit de-DE kam „Seite mit
Weiterleitung" zurück und die Befund-Zuordnung griff ins Leere. Die
Enum-Felder sind sprachunabhängig, der Text nicht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 14:55:11 +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
6b8728242f admin: Indexierungsstatus aus der Search Console, Kundenansicht vorbereitet und zu (Anhang 1: 1c.4, E8)
press-releases:check-index-status fragt täglich je Portal ein Kontingent
veröffentlichter Meldungen über die URL Inspection API ab, nach Alter
gestaffelt, nie geprüfte zuerst. Jede Antwort wird als Momentaufnahme in
press_release_index_checks gespeichert; der reduzierte Befund (IndexState)
trennt „noch nicht gecrawlt" von „gecrawlt, nicht indexiert".

Client authentifiziert sich als Service-Konto über ein selbst signiertes
JWT — kein Google-SDK. Admin ▸ Reports ▸ Indexierung zeigt die Verteilung
je Portal getrennt nach Direkteinreichung und Syndikation (Status-Log
source=api; Altbestand ohne Log zählt als direkt).

Das Kundenpanel heißt jetzt „Auslieferung", zeigt Sitemap-Datum und die
Abrufe durch Such- und KI-Dienste mit dem Satz „Ein Abruf ist kein Zitat";
der Google-Index-Block hängt an GSC_CUSTOMER_VISIBLE (false).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 14:21:51 +00:00
951b6c47fa web: Crawler-Abruf-Protokoll und Panel „Auslieferung" ohne Google-Link (Anhang 1: 1c.0, 1c.1, 1c.6)
Abrufe öffentlicher Meldungen durch bekannte Such- und KI-Dienste werden
je Meldung mit Dienst, gekürztem User-Agent und Zeitpunkt protokolliert —
keine IP. Dienste und Muster stehen in config/crawlers.php. Aufzeichnung
wie der Aufruf-Zähler per defer() und fehlertolerant; Bereinigung hängt
an press-releases:purge-events.

VisibilityStats liefert die Abrufe je Dienst (letzter Abruf, Anzahl) und
verliert den site:-Suchlink (Stufe 1 verspricht nichts, was von Dritten
abhängt).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 14:21:35 +00:00
134af424ea web: Unter-Navigation Preise/Transparenz auf allen vier Seiten mit aktivem Punkt
"Tarife & Preise" als erster Punkt; x-web.static-page bekommt einen Slot
`aboveTitle`, die drei Transparenzseiten tragen dieselbe Leiste mit
markierter aktueller Seite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 12:23:42 +00:00
c96c8a6e45 web: Transparenz-Navigation ueber der Headline der Preisseite (Roadmap 2.4)
x-web.transparency-nav (Label + drei Chips) statt der kleinen Zeile unter
dem USt-Hinweis; Komponente kennt `current` fuer spaetere Wiederverwendung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 12:12:22 +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
5f7fbd0c65 web: Hilfeseite "Was eine gute Pressemitteilung ausmacht", Hinweis in der Ablehnungsmail (Roadmap 1d.1, 1d.2, 1d.6)
Ohne kommerziellen Anteil (E6): Anlass, Aufbau, Sprache, Pruefablauf mit
konfigurierbarer Versuchszahl, die sieben Qualitaetsdimensionen offen,
Gewichtung nicht (Offenlegungsgrenze), Pressebilder, Partner als BALD.
DE und EN, Sitemap, Hilfe-Center. Die Ablehnungsmail verlinkt die Seite
auf dem Portal der Meldung mit Herkunft mail_reject.

Config: yellow_to_manual_queue Vorgabe false (Entscheidung 16.09.2026),
max_automatic_attempts = 3 (E3).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 13:59:23 +00:00
7fb5a2f9e9 billing: tote Null-Pruefungen auf NOT-NULL-Spalten entfernt (phpstan)
current_period_start/-end und invoice_date sind NOT NULL; die nullsafe-
Zugriffe und der Fallback 'n/a' konnten nie greifen. Suite wieder auf
"No errors".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 12:34:56 +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
a69b02193c web: Melden- und Verwaltungsblock auf der Meldungsseite hervorheben
Die beiden Hinweise unter der Mitteilung („Inhalt melden“, „mit der
hinterlegten E-Mail anmelden“) waren Textzeilen in 11,5 px und wurden
überlesen. Jetzt zwei klar getrennte Felder mit Buttons; der
Verwaltungsblock nennt die maskierte Empfängeradresse und führt auf die
Claim-Seite des Newsrooms. Derselbe Hinweis noch einmal in der
Seitenleiste unter „Weiterführend“, wie in der Veröffentlichungsmail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 12:58:39 +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
a3e0042055 newsroom: Seite verschwindet mit der letzten veröffentlichten Meldung (Roadmap 1b.1/1b.5)
Die Sichtbarkeitsregel bestand bereits (Detailseite 404, Verzeichnis und
Sitemap nur mit veröffentlichter Meldung). Neu: archive(),
changeStatusFromAdmin() weg von published und deleteFromAdmin() einer
veröffentlichten Meldung leeren den Seiten-Cache — vorher blieb die
Newsroom-Seite bis zu eine Stunde stehen, während die Depublikation längst
sofort griff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 10:34:14 +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
66d8398e97 api: Token-Gate erkennt aktive Stripe-Abos
Neukunden, die per Stripe buchen (STR-Kreis), konnten keine API-Tokens
erstellen: ApiAccessEligibilityService prüfte nur user_payment_options
(MAN-Kreis) und die letzte Legacy-Rechnung, nicht das Cashier-Abo.
Jetzt zählt subscribed() wie in User::hasActiveBooking() ebenfalls.

Auslöser: Ticket press@intratec.us (Starter, STR-00008 bezahlt,
„Create tokens" trotzdem gesperrt).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 10:34:38 +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