Commit graph

312 commits

Author SHA1 Message Date
239ec571bc Plan: Rechtstexte-Architektur referenziert + Hub-Geruest-Vorarbeit notiert
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:48:07 +00:00
258502bb85 Hub (Web): statische Seiten (Rechtstexte, API, FAQ ...) rendern das Hub-Geruest
x-web.static-page ist jetzt host-aware: Auf der pressekonto-Domain laden
Utility-Bar + Hub-Header/-Footer (inkl. Kontakt- und Widerrufs-Modal,
damit die Footer-Buttons ueberall funktionieren) statt des
Spoke-Geruests; Breadcrumb fuehrt zur Hub-Landing. Hub-Anker-Navigation
(#tarife/#funktion/#familie/#faq) absolut auf die Landing, damit sie
auch von Unterseiten aus funktioniert. Spokes unveraendert (Test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:47:47 +00:00
d6461f5c5e Admin: Widerrufe laufen in der Recht-&-Compliance-Queue auf
Eigene Widerrufe-Sektion unter der Anfragen-Tabelle in
/admin/legal-requests: Vorgangsnummer, Zuordnungsdaten, Statusfilter
(offen/alle/erledigt, eigener Paginator), Erledigt/Wieder-oeffnen-Aktion
mit resolved_at; KPI-Karte "Offene Widerrufe". Dazu
WithdrawalRequest-Factory (+resolved-State) fuer die Tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:29:32 +00:00
8001280185 Plan: Online-Widerruf (R3.0) erledigt + Follow-ups notiert
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:04:05 +00:00
515e249132 Recht (Web): Vertrag-widerrufen-Button + zweistufiges Widerrufs-Modal im Hub
Gut sichtbare Schaltflaeche "Vertrag widerrufen" im Footer; das Modal
fuehrt zweistufig durch den Widerruf: Schritt 1 nur Zuordnungsdaten,
Schritt 2 Kontrollansicht mit Bestaetigungsbutton "Jetzt widerrufen".
Erfolgs-/Fehlerzustand oeffnet automatisch (getrennter Error-Bag,
kollidiert nicht mit dem Kontakt-Modal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:03:37 +00:00
8c0bf00604 Recht: Online-Widerruf ohne Login (gesetzlicher Widerrufsbutton) - Backend
POST /widerruf (host-agnostisch, throttled, Honeypot, eigener
withdrawal-Error-Bag): persistiert nur Zuordnungsdaten (Name, E-Mail,
optionale Bestellnummer - KEIN Grund, darf nicht verlangt werden) und
versendet sofort die gesetzlich vorgeschriebene automatische
Eingangsbestaetigung mit Zeitstempel an den Verbraucher plus eine
interne Benachrichtigung an die Brand-Kontaktadresse des Hosts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:03:37 +00:00
47d03ec777 Hub (Web): Auth-aware CTAs, Kontakt-Modal statt Mailto, Pruefung-Wording
- Angemeldete Nutzer sehen ueberall (Header, Hero, Kuendbar-Box, CTA,
  Footer) "Zum pressekonto" (rollengerecht via LoginRedirect::homeFor)
  statt Anmelden/Konto erstellen
- Keine E-Mail-Adresse mehr im Quelltext (Spam-Schutz): neues
  <x-web.hub.contact-modal> (Alpine, Honeypots, Erfolgs-/Fehlerzustand,
  auto-open nach Submit) am host-agnostischen kontakt.store-Endpunkt;
  Trigger in Enterprise-Streifen, FAQ-Support, DSGVO-Antwort, Footer
  und Utility-Bar
- "Redaktionelle Pruefung" -> "Pruefung" in allen Hub-Inhalten;
  "verifizierter Newsroom" -> "Ihr eigener Newsroom"

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:52:29 +00:00
88d6935014 Kontakt: Hub-Domain-Anfragen gehen an die pressekonto-Brand-Adresse
Der WebPortalContext kennt nur die beiden Spokes - Anfragen von
pressekonto.test waeren sonst bei businessportal24 gelandet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:52:29 +00:00
38d7cca086 Plan: R2.2/R2.3 erledigt (Hub-Ueberarbeitung + Plan-Katalog-Pipeline verifiziert)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:24:39 +00:00
8d4b3b5ca3 R2.2/R2.3 (Web): Hub-Landing live-faehig - datengetriebene Tarife, ehrliche Inhalte
- Tarif-Grid aus Plan::active() (Preise/Kontingente/Limits/Vorteile wie
  Buchungsseite + admin/payments/plans), Alpine-Toggle monatlich/jaehrlich
- Fake-Katalog (0/49/149 Euro, Standard/Enterprise-Featureliste mit
  SSO/SLA/ISO-Claims) ersetzt; Enterprise = ehrliche Anfrage-Zeile
- Fake-Kundenliste (Siemens/BASF/...) und erfundene Kennzahlen raus;
  Plattform-Zahlen jetzt echt aus HubPageData
- Alle href="#" beseitigt: Footer-Rechtslinks auf /de/*-Seiten,
  Kontakt als mailto, Status/Doku/Changelog-Eintraege entfernt
- Unhaltbare Claims entschaerft (14-Tage-Test, 4h-Pruefzeit, sekundengenau,
  anteilige Erstattung, ISO 27001, RSS/Badge/CSV-Features)
- FAQ datengetrieben (Einzel-PM-Preis, ab-Preis aus Katalog, API-Doku-Link)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:24:04 +00:00
f6edb4aec5 R2.2: HubPageData - Plan-Katalog + echte Plattform-Kennzahlen fuer die Hub-Landing
Liefert Plan::active(), Einzel-PM-Preis und stuendlich gecachte echte
Zahlen (Archiv-Mitteilungen gesamt/je Portal, aktive Newsrooms,
Branchen) an web.pressekonto. Tests: Katalog-Preise statt Fake-Liste,
keine toten Links/erfundenen Referenzen, Rechtsseiten auf der
Hub-Domain erreichbar. (Gehoert zum Web-Commit der Hub-Ueberarbeitung.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:24:04 +00:00
5226f5de2b Plan: R2.1 erledigt + naechste Schritte notiert
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:15:25 +00:00
4e4aff115d Fix: legal-request-Honeypot nickt Bots wieder still ab
Die size:0-Regeln liefen vor dem Honeypot-Zweig: ein gefuelltes Feld
produzierte einen Validierungsfehler (verraet die Falle), der stille
done()-Pfad war tot. Honeypot-Check jetzt vor der Validierung; Test
schaerft das Soll (kein Fehler, normaler Erfolgs-Redirect).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:14:52 +00:00
9875e7b6e1 R2.1 (Web): Kontaktformular auf der Kontakt-Seite aktiviert
Ersetzt den TODO-Platzhalter: Name/E-Mail/Nachricht, Honeypots,
Erfolgs-Flash statt Formular nach Versand, Datenschutz-Hinweis.
Styling analog legal-request. Telefon/Adresse sind weiterhin
Platzhalterdaten (R3.2, [EXTERN]).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:14:52 +00:00
a4d054b3ab R2.1: Kontaktformular-Backend (Controller + Mailable + Brand-Empfaenger)
POST kontakt mit Validierung, Honeypot (vor der Validierung, stilles
Abnicken), IP-Rate-Limit (5/h) + throttle:10,1. Versand an
brand.contact_email (neue CONTACT_EMAIL_*-Keys, [EXTERN: echte
Adressen]), Fallback MAIL_FROM_ADDRESS; Reply-To = Absender.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:14:52 +00:00
1e9c2f96ed Plan: R0 komplett + R1.1/R1.3 erledigt
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:08:45 +00:00
54a2e64698 R1.3: Quota-Reset als fester Bestandteil des Tarif-Modells festgeschrieben
Der "Stub bis zum Tarif-Modul"-Kommentar war ueberholt: Das Tarif-Modul
existiert und rechnet in pressReleaseQuotaRemaining() gegen genau diesen
Zaehler. Kalendermonats-Reset dokumentiert als bewusste Vereinfachung
(nicht an der individuellen Abo-Periode ausgerichtet). Keine Logik-
Aenderung; Verhalten via PressReleaseQuotaTest abgedeckt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:08:23 +00:00
ad2879daa4 R1.1: Buchungs-Gate geprueft + safe-by-default; Legacy-Buchungspfade festgeschrieben
Default von billing.enforce_booking auf true gedreht: ein in Produktion
vergessener Env-Key haette das Gate sonst still geoeffnet (jeder haette
ohne Zahlung veroeffentlichen koennen). Tests ergaenzt: Grandfathered
gueltig/abgelaufen/NULL (= abgelaufen, Entscheidung 19.06.), Active
ausserhalb der Periode, Service-Level-Submit mit Legacy-Buchung und
BookingRequiredException ohne Buchung. Scope-Logik selbst war korrekt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:07:01 +00:00
c3db80c651 R0.5: eigene Fehlerseiten 403/404/419/500/503 (self-contained, domain-aware)
Bisher griffen die Laravel-Default-Seiten. Gemeinsames Inline-CSS-Layout
ohne Vite-Abhaengigkeit (rendert auch bei kaputtem Asset-Build), nutzt
die geteilte domainName-Variable mit Fallback, noindex, Dark-Mode via
prefers-color-scheme.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:01:29 +00:00
17eaa80135 R0.4: Seeder produktionssicher (PlanSeeder, keine Fake-Stripe-IDs, Dev-User-Guard)
- PlanSeeder (Starter/Business/Pro/Agency) haengt jetzt im DatabaseSeeder
- PaymentOptionSeeder erfindet keine prod_/price_placeholder-IDs mehr
  (Spalten nullable; echte Verknuepfung via Stripe-Sync/Pflege)
- Demo-Zugaenge admin@pressekonto.test/test@example.com (Passwort
  "password") werden nur noch in local/testing angelegt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:58:30 +00:00
f9e3c64238 R0.3: Newsletter-Sync-Binding config-aware + ehrliche Test-Sync-Meldung
Bisher wurde der NullNewsletterSyncClient unbedingt gebunden -
NEWSLETTER_SYNC_ENABLED=true mit echtem Provider haette stumm nichts
getan. Jetzt: Platzhalter-Provider -> No-Op-Client, aktivierter aber
nicht implementierter Provider -> RuntimeException (fail fast). Das
Admin-UI weist beim Test-Sync auf den Platzhalter hin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:55:35 +00:00
a4184af4fb R0.2: gdpr_consent_at bei allen Registrierungspfaden befuellen (DSGVO)
Feld existierte seit der Migration-2026-Erweiterung, wurde aber nirgends
geschrieben. Jetzt gesetzt bei: Volt-Registrierung (bestaetigte AGB-/
Datenschutz-Checkbox), Magic-Link-Provisioning und Google-OAuth (beide
via CustomerAccountProvisioner, Konto-Anlage nutzerinitiiert).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:53:41 +00:00
1a612e04b2 R0.1: tote Dev-Varianten-Routen entfernt (fehlende Views -> 500 auf BP24)
/variant-1 + /variant-2 referenzierten nicht existierende Views
web.businessportal24-variant-float-glow/-glass-gradient; Fragmente der
alten Frontend-Version, nirgends verlinkt. Regressionstest ersetzt den
Stub in DevExampleRoutesTest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:50:37 +00:00
cb2fe97609 Plan: Release-Entwicklungsplan (R0-R6) aus Vier-Achsen-Analyse konsolidiert
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:50:37 +00:00
db7c1dfbf1 Perf: customer press-releases-Liste ueber UNION statt OR-Paginate
Die Uebersicht /admin/me/press-releases war auch fuer leere Konten ~2s: der OR-Filter in scopeAccessibleBy zwang die Paginator-count(*) zum Full-Scan (EXPLAIN type=ALL, ~174k Zeilen) und den Zeilen-Fetch bei 0 Treffern zum vollen created_at-Index-Scan (~1s). Neue PressRelease::accessiblePageFor() nutzt dieselbe index-freundliche UNION wie accessibleStatusCountsFor(): schmale UNION liefert die Seiten-IDs ueber user_id-/company_id-Index, danach nur 25 Modelle hydratisiert; Total via schnellem UNION-count. Messung: leer ~2000ms -> 167ms, groesstes Konto (20.830 PMs) ~740ms -> 468ms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 10:55:45 +00:00
90f3245e41 Plan: Phase 5 vollstaendig abgeschlossen (5.2/5.3/5.5 auf erledigt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 10:44:15 +00:00
349991a4e8 5.2: User-Formular-Feldraster in Blade-Components (role/billing) extrahiert
role- und billing-Feldgrids aus create+edit in <x-portal.user-role-fields> und <x-portal.user-billing-fields> gezogen (nur das innere Grid; Panel-/Card-Wrapper bleiben pro Seite). Nebenbei-Bugfix: create zeigt jetzt den billing.name-Validierungsfehler (Feld wurde via userBillingRules() schon validiert, Fehler aber nie gerendert). company-link-picker + contact-fields bleiben bewusst inline (divergiert bzw. nur in edit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 10:43:05 +00:00
55d0804917 5.3: PressReleaseStatus::badgeClass() zentralisiert + toten statusColor-Code entfernt
Status-Badge-Farblogik (ok/warn/err/muted) aus 7 divergenten match-Bloecken in eine Enum-Methode gezogen. Archiviert/Entwurf jetzt konsistent 'muted' (grau) auch auf Detail-/Edit-/Log-Seiten (vorher dort 'hub'-Akzent). Toter statusColor-Block (Flux-Farben, nie referenziert) in customer/show entfernt. flux:badge :color in admin/users bleibt bewusst separat (eigenes Farbvokabular, Einzel-Call-Site).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 10:36:15 +00:00
0e08d8acc4 5.5: Auth-Components abgeschlossen (funktional gegengetestet) + Plan-Stand
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 10:11:23 +00:00
1503d3a7ed Fix: rollenlose aktive Nutzer -> Backfill-Command + Import-Nachlauf
Problem: Ein eingeloggter Nutzer ohne admin/editor/customer-Rolle wird per
redirectUsersTo auf die Startseite geleitet und haengt dort fest. Ursache
sind Alt-Konten, deren Rollen-Zuweisung nie lief (frueher Import bzw. ein
nach dem User-Insert fehlgeschlagener Rollen-Sync).

- Neues Command users:backfill-portal-role (--role, --dry-run): weist aktiven,
  komplett rollenlosen Nutzern die customer-Rolle zu. api-only-Konten haben
  bereits eine Rolle und bleiben unangetastet; inaktive werden uebersprungen.
- legacy:import ruft den Backfill nach dem User-Schritt automatisch auf
  (self-heal, da updateOrCreate + Rollen-Sync nicht atomar sind).

Analyse-Ergebnis nebenbei: In der aktuellen DB war nur genau 1 echter Fall
(bereits gefixt); die zuvor genannten ~167 waren fast alle api-only (gewollt
ohne Portal-Zugriff). Command laeuft daher jetzt sauber auf 0.

Test: BackfillPortalRoleTest (customer-Zuweisung, Rollen-Erhalt bei
customer/api-only, Ueberspringen Inaktiver, dry-run).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 09:23:56 +00:00
9fadfe6458 5.5: <x-password-input> + 6 Auth-Passwortfelder umgestellt
Self-contained Alpine-Toggle (Anzeigen/Verbergen) pro Feld; Props name/label/
autocomplete/placeholder/required/autofocus/show-error, id default auth-<name>,
Slot :labelAction fuer den 'Passwort vergessen?'-Link (login). Ersetzt die
field-pw-wrap-Bloecke in login, register (2), reset-password (2), confirm-
password (1). Die nun ungenutzten form-level x-data={showPassword...} entfernt.

Nebeneffekt/Bugfix: register+reset nutzten :placeholder="__(...)" auf rohem
<input> -> wurde als Alpine-Bind woertlich ausgegeben und schlug fehl (leerer
Platzhalter). Als Component-Prop wird __() korrekt in PHP aufgeloest, der
Hinweistext (z. B. 'Mindestens 8 Zeichen') erscheint jetzt.

Bestaetigungsfelder ohne Fehleranzeige (show-error=false, wie zuvor).
116 Auth-Tests gruen, alle Views kompilieren.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 09:04:26 +00:00
1729e35943 5.2: gemeinsamen Firmen-Sync-Kern als User::syncCompaniesWithRoles() gezogen
Analyse der beiden ~40/160-Zeilen-save()-Methoden (users/create vs edit): sie
teilen nur den User-Attribut-Payload und den byte-identischen Firmen-Sync-Loop
(Bearbeiten hat zusaetzlich Profil/Kontakte/volle Billing-CRUD). Ein voller
UserUpsertService waere Over-Engineering (ueberwiegend Branches) — daher nur
der wirklich gemeinsame Persistenz-Kern:

User::syncCompaniesWithRoles(array $companyIds, array $roles) baut das
Pivot-Payload (fehlende Rolle => 'member') und synct. Beide Formulare nutzen
es; im Bearbeiten kommen die linkedCompanyIds nun direkt aus $validated.

Test: UserSyncCompaniesTest (Rollen-Zuordnung, member-Default, Detach).
29 User-Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:47:23 +00:00
6b1d84fc01 Doku: 5.4 abgeschlossen + 5.5 Teil-Stand im Entwicklungsplan
5.4 als erledigt markiert (page-header/empty-state/table-pagination; table-
panel + searchable-select bewusst nicht gebaut). 5.5 Teil-erledigt (alert +
session-status); password-input + rate-limit-trait mit Begruendung zurueck-
gestellt. Wiedereinstieg-Abschnitt + Datum aktualisiert, Perf-Fix vermerkt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:44:44 +00:00
b1b832df4c 5.5: <x-session-status> + 4 Auth-Seiten umgestellt
Self-conditional Component fuer den session('status')-Block (field-status +
role=status); Margin per class. Ersetzt den identischen 5-Zeilen-Block in
login, forgot-password, register, magic-link.

Bewusst NICHT umgestellt: verify-email (fixe Meldung statt session('status'),
Bedingung ===verification-link-sent, mb-6) und settings/profile (eigener
Kontext) — bleiben unveraendert. Alle Views kompilieren, 95 Auth-Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:41:46 +00:00
587d1fd312 5.5: <x-portal.alert> + 6 Flash-Bloecke umgestellt
Erste 5.5-Component: buendelt den kopierten session()-Flash-Alert (Icon +
Farbschema) mit variant=success|error; Nachricht als Slot. Erzeugt
identisches Markup.

Umgestellt: categories/create (success), categories/edit (success+error),
footer-codes/index (success+error), presets/index (success).

Bewusst NICHT umgestellt: companies/show (abweichendes, icon-loses Design
mit zusaetzlicher info-Variante und anderen Farbtokens) — bleibt bespoke.

Render-Smoke geprueft, alle Views kompilieren, 23 Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:39:44 +00:00
505852f29e 5.4: <x-portal.table-pagination> + 13 Wrapper ersetzt
Buendelt den 13-fach byte-identischen Paginierungs-Fuss (border-t + p-4 um
->links('components.portal.pagination')) in eine Component mit :paginator-
Prop. Erzeugt identisches HTML -> kein visuelles Risiko.

Ersetzt in: users, invoices/index, payments/index (4x: Bestandsschutz,
Subscriptions, Purchases, Invoices), contacts/index, legal-requests/index,
presets/index, companies/index, footer-codes/index, customer/invoices,
customer/press-releases/index.

Bewusst BARE gelassen (kein Wrapper im Original): press-releases/index,
categories/index, press-kits/index. Alle Views kompilieren, 251 Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:35:37 +00:00
1c75e61179 5.4: empty-state auf customer/press-releases + customer/press-kits ausgerollt
Component um optionalen Default-Slot erweitert (rendert unter den Aktionen)
fuer Zusatzinhalt wie die Onboarding-Schritte der Leerzustaende.

Umgestellt:
- customer/press-releases/index: 3 States (Suche ohne Treffer, Filter ohne
  Treffer=warm, gar keine PMs inkl. Onboarding-Grid im Default-Slot)
- customer/press-kits/index: 2 States (Filter=warm, keine Firma inkl.
  Onboarding-Grid), Panel-Wrapper bleibt erhalten

Der Such-Titel behaelt das originale ASCII-Anfuehrungszeichen (via einfach-
gequotetem Attribut mit escaptem PHP-String). Alle Views kompilieren,
162 Customer-/PM-Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:06:21 +00:00
077d313baa 5.4: <x-portal.empty-state> + admin/press-releases/index umgestellt
Zweite Struktur-Component: buendelt den empty-stage-Block (runder Icon-Rahmen
+ Titel + Subtext + optionale Aktionen). Props: title, subtitle, variant
(default|warm|err); Slots :icon und :actions.

Erst-Umstellung: die zwei Leerzustaende der Admin-PM-Liste (gefiltert = warm
mit 'Filter zuruecksetzen', leer = Standard mit 'Neue PM anlegen'). Weitere
Vorkommen (customer/press-releases, customer/press-kits) folgen nach Sicht-
pruefung. 36 Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:05:19 +00:00
feda8be4d3 5.4 (Teil 6): page-header in payments/invoices/legal/coupons/reports/newsletter
Component minimal-additiv erweitert (Defaults unveraendert):
- size=sm (28px) fuer legal-requests/show
- description-width=720px fuer breite Fliesstexte

Umgestellt (letzter grosser Batch):
- payments/index, payments/plans (720px, Aktionen)
- invoices/index, coupons/index (720px, Badge via :meta, keine Aktionen)
- legal-requests/index (Standard), legal-requests/show (sm, Status-Badge)
- reports/slow-requests (Logdatei-Zaehler-Badge als Aktion)
- newsletter/sync (Sync-Status via :meta, Dry-Run/Test-Sync-Buttons)

CSS-Hash unveraendert -> keine neuen Klassen. 74 Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:36:33 +00:00
27a77bfc50 5.4 (Teil 5): page-header in roles/* + footer-codes/* + presets/*
Neun weitere Standard-Header (md) auf <x-portal.page-header>:
- roles/index (bedingter Anlegen-Button), roles/create (Guard-Badge via
  :meta), roles/edit (ID + Guard + bedingte Systemrolle via :meta)
- footer-codes/index, footer-codes/create, footer-codes/edit (ID + Global +
  Aktiv/Inaktiv via :meta, Titel als Beschreibung)
- presets/index, presets/create, presets/edit (ID via :meta)

Roles-/FooterCode-/Preset-Tests (23) gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:01:47 +00:00
1eb2243959 5.4 (Teil 4): page-header size=lg-Variante + press-releases/* + companies/index
Component um size-Prop erweitert: md (30px, Standard) | lg (34px, Landing-
Header). lg vergroessert zusaetzlich Beschreibung (13.5px) und Button-Abstand
(gap-3) — exakt die bisherige Sonderoptik der Listen-Landing-Header.

Umgestellt:
- press-releases/create (md), press-releases/edit (md, Status-Badge + ID via
  :meta, 3 Aktionen inkl. Modal-Trigger)
- press-releases/index (lg) und companies/index (lg) — Letzterer war zuvor
  bewusst ausgelassen, ist mit der lg-Variante jetzt originalgetreu abbildbar.

Bewusst NICHT umgestellt: press-releases/show (zusaetzlicher Untertitel +
mehrteilige Firma/Kategorie/Autor-Beschreibung, bleibt bespoke).

md-Ausgabe unveraendert (nur Klassenreihenfolge). 321 Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:16:40 +00:00
2a3b5612cb 5.4 (Teil 3): page-header in users/* + contacts/* umgestellt
Sieben weitere Header auf <x-portal.page-header>:
- users (index), users/create, contacts/index, contacts/create (Standard;
  contacts/create mit bedingtem 'Firma vorausgewaehlt'-Badge via :meta)
- users/show, users/edit (ID + Portal + Aktiv/Inaktiv-Badges via :meta,
  dynamischer Titel/Beschreibung aus User-Daten)
- contacts/edit (ID + Portal-Label via :meta, ohne Beschreibung)

Component: break-words am H1 ergaenzt (fuer lange Namen/E-Mails in show/edit;
bei kurzen Titeln ein No-op). User-/Contact-Tests (129) gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:08:06 +00:00
f3a9c423dc Fix: fehlendes .search-wrap-Styling (Suchfeld in /admin/me/firmen unsichtbar)
Die Firmen-Toolbar nutzt .search-wrap fuer das Suchfeld, aber die Klasse war
in keinem CSS definiert -> das Feld hatte weder Hintergrund noch Border und
war kaum erkennbar. Styling ergaenzt (weisser Card-Hintergrund #ffffff,
gleiche Border/Radius wie .filter-chip, focus-within-Highlight), damit es
sich konsistent in die Filterleiste einfuegt. Dark-Mode ueber die Tokens.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 08:49:19 +00:00
af15e84984 5.4 (Teil 2): page-header in categories/* + companies/create,edit umgestellt
Vier weitere Standard-Header auf <x-portal.page-header> umgestellt:
categories/index, categories/edit (ID/PM/Unterkategorien-Badges via :meta),
companies/create, companies/edit (ID-Badge via :meta).

Bewusst NICHT umgestellt (Sonderfaelle, bleiben unveraendert):
- companies/index: abweichende Titelgroesse (text-[34px] statt 30px)
- companies/show: Sonder-Header mit Logo-Bild + Website-Link

Categories-/Companies-Tests (82) gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 08:31:45 +00:00
83bb2a7caa 5.4 (Teil 1): <x-portal.page-header> + categories/create umgestellt
Erste Seiten-Struktur-Component fuer das Admin-Backend: buendelt den 35-fach
kopierten page-header-Block (Hub-Badge + Eyebrow + H1 + Beschreibung, dazu
Slots :meta und :actions fuer die variablen Teile). Muster wie <x-portal.
stat-card>.

Bewusst nur EINE Seite (categories/create) umgestellt, damit die Optik
einzeln im Browser gegengeprueft werden kann, bevor die restlichen 34 Header
folgen. AdminCategoryManagement-Tests gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:00:09 +00:00
580c918ad3 Perf: Status-Counts der PM-Uebersicht via UNION statt OR-GROUP-BY
Die nach Status gruppierte Zaehlung lief ueber accessibleBy() (user_id OR
company_id IN sub). Der OR-Ausdruck zwingt MySQL zu Full-Scans (EXPLAIN:
companies type=ALL, press_releases Full-Index-Scan ueber 124k Zeilen) und
war mit ~500-950ms der groesste Einzelposten der Seite.

Neue PressRelease::accessibleStatusCountsFor(User, ?Closure): kombiniert
Autor- und Firmen-Zweig als indexfreundliche UNION-Subquery (per PK
dedupliziert) und gruppiert darueber. Im Benchmark ~2,5x schneller bei
identischer Verteilung. Der Firmen-Filter der Uebersicht wird als geteilte
Closure auf Counts UND Liste angewandt (garantiert deckungsgleiche Filterung).

Gesamtmessung /admin/me/press-releases (User 14016, 26.772 Firmen / 20.830
PMs): Query-Pfad ~2850ms -> ~740ms warm; hydratisierte Company-Models 24.

Tests: accessibleStatusCountsFor zaehlt Autor+Firma je einmal (UNION-Dedup)
und wendet Zusatz-Constraints auf beide Zweige an.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 13:55:13 +00:00
07d98ed79e Perf: accessibleBy() nutzt company_id-Subquery statt materialisiertem whereIn
scopeAccessibleBy() haengte den Firmenzugriff bisher als whereIn() mit allen
via accessibleCompanyIds() materialisierten Firmen-IDs an. Bei Power-Usern
(26k+ Firmen) ist das ein riesiger IN-Block im SQL.

Jetzt: orWhereIn('company_id', Company::accessibleBy($user)->select('id')) —
MySQL loest die Subquery als Semi-Join ueber den company_id-Index. Gleiche
Semantik (Company-Global-Scopes werden mitkompiliert), aber beschraenkte
SQL-Groesse und keine PHP-seitige ID-Liste mehr.

Benchmark (User 14016): ein korreliertes orWhereHas-EXISTS war ~3x langsamer
(1070ms vs 335ms COUNT) — die Subquery-whereIn-Form ist die schnellste.
accessibleCompanyIds() bleibt fuer canAccessCompany() erhalten.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 11:30:49 +00:00
ed1aa07ae6 Perf-Fix: User::accessibleCompanyIds() hydratisiert keine Company-Models mehr
Auf /admin/me/press-releases wurden bei einem User mit sehr vielen Firmen
~107.000 Company-Models geladen. Ursache: accessibleCompanyIds() nutzte
das Eloquent-Relation-pluck() (ownedCompanies()->pluck() / companies()->
pluck()), das pro ID ein vollstaendiges Model hydratisiert. Die Methode
wird ueber PressRelease::scopeAccessibleBy() pro Seitenaufruf ~2x
ausgewertet (Status-Count- + Listen-Query) -> ~2x 53k Models.

Fix:
- toBase()->pluck() statt Relation-pluck(): liefert nur die ID-Werte ohne
  Hydration, wendet die Global Scopes (PortalScope) weiterhin an.
- Ergebnis pro Request memoisiert (cachedAccessibleCompanyIds), da der Scope
  die Methode mehrfach aufruft.

Messung (User 14016, 26.772 Firmen): 107.238 -> 24 hydratisierte
Company-Models auf der PM-Liste. Ergebnis-IDs unveraendert.

Regressionstests: accessibleCompanyIds liefert owned+member ohne Dubletten
und hydratisiert 0 Company-Models.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 11:18:51 +00:00
c16b0b77e2 Doku: portalBadgeColor()-Klaerung im Plan vermerkt (toter Code entfernt)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 10:57:19 +00:00
055fd5c47f Optimierung 5.3: toten portalBadgeColor()-Helfer entfernt
In companies/index, companies/show und contacts/index war die private
Methode portalBadgeColor() definiert, aber nirgends aufgerufen (kein
Aufruf im Blade-Markup oder PHP). Entfernt; in companies/show wird damit
auch der Portal-Import ungenutzt und faellt weg.

Klaert den im Wiedereinstieg-Plan offenen Punkt: keine Portal::badgeColor()-
Zentralisierung noetig, da es keine echten Call-Sites gab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 10:56:50 +00:00