presseportale/dev/archive/optimize/system-optimierungen-analyse-claude.md
Kevin Adametz 6a4c4d8837 docs: dev-Doku für den Live-Betrieb neu sortiert — Archiv + Betrieb + Roadmap
Das System ist seit 20.07.2026 live; die dev-Doku spiegelt jetzt den
Betriebszustand statt der Release-Vorbereitung:

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 07:52:38 +00:00

27 KiB
Raw Permalink Blame History

System-Optimierungs-Analyse (Pressekonto)

Erstellt von Claude Code am 2026-06-18. Read-only-Analyse — es wurden keine Dateien geändert. Fokus: Code-Doppelungen, geteilter/wiederverwendbarer Code, Effizienz- und Best-Practice-Potenzial über Controller, Services, Models, Middleware, Livewire/Volt-Komponenten und Blade-Views.

Inhaltsverzeichnis

  1. Management Summary
  2. Top-Prioritäten (höchster Hebel)
  3. Backend: Controller, Services, Models, Middleware
  4. Livewire/Volt: Press-Release-Editoren
  5. Livewire/Volt: Admin-CRUD, Customer, Settings, Auth
  6. Web-Frontend (gehört dem User, separat committen)
  7. Vorgeschlagene neue Bausteine (Traits/Components/Services)

1. Management Summary

Das System ist architektonisch bereits sauber aufgesetzt: eine umfangreiche Service-Schicht (app/Services/**), Domain-/Theme-System, Concerns-Verzeichnis, geteilte Portal-Komponenten (x-portal.stat-card, components.portal.pagination) und ein PressReleaseService existieren. Die Probleme liegen nicht in der Struktur, sondern in Duplikation an den Rändern:

  • Die größten Brocken sind die vier Press-Release-Editoren (admin + customer, create + edit) mit zusammen ~5.500 Zeilen Blade/PHP, die zu ~80–95 % identische Validierung, Scheduling-Logik, Persistierung und Formular-Markup enthalten.
  • Querschnittslogik ist mehrfach implementiert: Firmenzugriff/Ownership (3–4×), Zahlungsoption-Eligibility (4×), VAT-Normalisierung (3×), Volltextsuche-Fallback (6×), Tabellen-Sortierung/Filter (3×).
  • Diverging-Risk (Korrektheit!): Mehrere dieser Duplikate sind bereits leicht auseinandergedriftet (z. B. Legacy-Invoice-Eligibility liefert bei null mal deny, mal needs_review). Das ist nicht nur Wartungslast, sondern eine latente Fehlerquelle.
  • Domain/Theme/Portal-Auflösung ist über 4 Stellen (Provider + 3 Middleware) verteilt und teils mit unterschiedlichen Theme-Vokabularen.
  • Wenige echte Performance-/Security-Punkte: synchrones API-Logging pro Request, Query-Sturm im Status-Widget, timing-unsicherer Basic-Auth-Vergleich, Race-Conditions bei Wallet-/Rechnungsnummern-Erstellung.

Mehrheit der Findings ist risikoarm (reine Extraktion ohne Verhaltensänderung) bei hohem Zeilen-Einsparpotenzial.


2. Top-Prioritäten (höchster Hebel)

# Maßnahme Bereich Severity Aufwand
1 InteractsWithPressReleaseScheduling-Trait + Admin-Editoren nutzen bestehenden Tags-Trait PR-Editoren Hoch Niedrig
2 Persistierung + formRules() der PR-Editoren in PressReleaseService / geteilten Trait verlagern PR-Editoren Hoch Mittel
3 Company::scopeAccessibleBy() / CompanyPolicy — Firmenzugriff zentralisieren Backend Hoch Mittel
4 Eine User::hasActivePaymentOption() + latestLegacyInvoice() (Eligibility-Divergenz beheben) Backend Hoch (Korrektheit) Mittel
5 Domain/Theme/Portal-Auflösung in einen DomainContext-Service konsolidieren Middleware Hoch Mittel
6 Concerns: HasSortableTable, HasFullTextSearch, HasResettableFilters Admin-CRUD Hoch Niedrig
7 <x-portal.alert> + <x-session-status> + <x-password-input> / <x-form-field> Admin/Auth Hoch Niedrig
8 Portal::badgeColor() + <x-portal.portal-badge> / <x-portal.status-badge> Querschnitt Hoch Niedrig
9 Synchrones LogApiUsage → terminable/queued; Status-Widget-Queries memoisieren Performance Hoch Mittel
10 BasicAuthMiddleware → hash_equals(); BASIC_AUTH_ENABLED aus env() in Config Security Hoch Niedrig

3. Backend: Controller, Services, Models, Middleware

3.1 N+1 / Query-Effizienz

  • Status-Widget-Query-Sturm (Hoch) — app/Services/Customer/CustomerStatusSummary.php:35-66 feuert pro Request viele Einzelqueries (currentPlan, subscription, exists(), hasActiveBooking() mit 3 Sub-Queries, offene Single-Purchases). Da das Widget always-on im Header sitzt, summiert sich das. → Sub-Ergebnisse memoisieren/eager-loaden und durchreichen.
  • Offene Single-Purchase 3× gezählt (Mittel) — gleiche COUNT-Query in CustomerStatusSummary.php:61, User.php:215, User.php:229. → Einmal berechnen (wie currentPlan() memoisiert in User.php:128-162).
  • LegacySubscriptionService::cancelBundleFor (Mittel) — app/Services/Billing/LegacySubscriptionService.php:45-54 macht ein $post->update() pro Iteration. → Bulk-update() oder DB::transaction.
  • selectedCompany() doppelte Query (Mittel) — app/Services/Customer/CustomerCompanyContext.php:49 + 153-158: exists() und findFor() führen dieselbe Query aus. → Einmal first().
  • LogApiUsage synchroner INSERT pro Request (Hoch) — app/Http/Middleware/LogApiUsage.php:35-46 schreibt bei jedem API-Response. → Terminable Middleware oder Queued Job.

3.2 Duplizierte Querschnittslogik

  • Firmen-Ownership-Query 3–4× (Hoch) — identische owner_user_id == me OR whereHas('users', me)-Closure in Api/V1/PressReleaseController.php:182-191, CompanyController.php:18-21 & :39-42, PressReleaseImageController.php:87; überlappt zusätzlich User::accessibleCompanyIds() (User.php:406-413) und CustomerCompanyContext::accessibleCompanyQuery() (:143-151). → Ein Company::scopeAccessibleBy() oder CompanyPolicy (die in ContactAccessService.php:229 schon referenziert wird).
  • Zahlungsoption-Eligibility 4× (Hoch) — gleicher Status-Set, leicht abweichende Periodenlogik in Api/ApiAccessEligibilityService.php:28-46, CustomerStatusSummary.php:47-52, User::hasUnlimitedPressReleaseQuota() (User.php:191-196), User::hasActiveBooking() (User.php:256-261). Variable $hasGrandfathered in CustomerStatusSummary.php:47 matcht faktisch Active oder Grandfathered (Mislabel). → UserPaymentOption-Scope + eine User::hasActivePaymentOption().
  • Legacy-Invoice-Eligibility divergiert (Hoch — Korrektheit) — ApiAccessEligibilityService::hasPaidLegacyInvoice() (:48-56) behandelt null als deny, LegacyApiCustomerReporter::classification() (:131-142) als needs_review. Beide re-implementieren „latest by invoice_date then id". → Ein User::latestLegacyInvoice() + eine Eligibility-Methode.
  • VAT-ID-Normalisierung + EL/GR-Mapping 3× (Mittel) — VatResolver.php:30,56,63, VatIdValidationService.php:37,44,69, StripeCheckoutService.php:48,54,56. → VatResolver::normalize() / isEuCountry() zentral; StripeCheckoutService konsumiert statt selbst herzuleiten.
  • Case-insensitive Email-Lookup via LOWER() (Mittel) — whereRaw('LOWER(email) = ?') in ContactAccessService.php:50,130-148,176, SocialAuthService.php:32; defeated den Email-Index. → Beim Schreiben normalisieren + User::findByEmail()-Scope.
  • Lazy-Account-Erstellung doppelt (Mittel) — ContactAccessService::createAccount() (:188-208) ≈ SocialAuthService::createUser() (:65-82). → gemeinsame Provisioning-Methode.
  • Currency-Literal inkonsistent (Mittel) — 'EUR' in CheckoutController.php:66, CreditTopupService.php:59, ManualInvoiceService.php:125 vs. 'eur' in ExtraPmPurchaseService.php:50, ProofPdfService.php:69. → config('billing.default_currency').

3.3 Fat Controller / Validierung / Model

  • Api/V1/PressReleaseController (Mittel) — :201-220 re-implementiert Unique-Slug, obwohl PressRelease bereits HasUniqueSlug mit slugScopeAttributes() nutzt (PressRelease.php:27,32). Create/Update/Reclassify-Orchestrierung (:44-122) gehört in PressReleaseService.
  • LegalRequestController (Mittel) — Inline-Validation :42-57 → StoreLegalRequestRequest (Projekt nutzt app/Http/Requests/Api/V1/* bereits). Portal-aus-Host :103-122 dupliziert Portal-Detection; „max 1 open per PM+type"-Regel :71-91 → LegalRequestService.
  • CheckoutController::singlePm (Niedrig) — baut SinglePurchase inline :61-67 (hardcoded 'EUR') → Service-Methode analog CreditTopupService::startTopup.
  • Fat User Model (Mittel) — app/Models/User.php mischt Auth, Relations, Stripe-Adresse, Plan/Tier, Quota-Mathe, Booking-Gates. → Trait HasBillingProfile/InteractsWithQuota (:86-262, 352-355).

3.4 Authorization → Policies/Gates

  • Firmenzugriff umgeht bestehende CompanyPolicy (Hoch) — CustomerCompanyContext.php:143-151,244-249 + 3 API-Controller (s. 3.2). → über Gate/Policy.
  • Domain-Auth-Middleware → Gates (Mittel) — EnsureUserIsAdmin.php:28, EnsureUserIsCustomer.php:13, EnsureApiUserIsActive.php:18 rollen $user?->method()+abort von Hand; API-Active-Check dupliziert is_active aus canAccessAdmin/Customer (User.php:379,392). → Gate::define('access-admin'…) + can:-Middleware.
  • Impersonation-Logik im Admin-Gate (Mittel) — EnsureUserIsAdmin.php:20-26 mischt Impersonation-Redirect in den Access-Check. → eigene Middleware. Zusätzlich totes loadMissing('roles') :16-18 (Niedrig).

3.5 Middleware-Overlap (Domain/Theme/Portal/Edition)

  • Vierfach duplizierte Host→Config-Auflösung (Hoch) — ThemeServiceProvider::boot (:29-88), ThemeMiddleware::handle (:16-56), SetDomainUrl::handle (:22-58), SetCurrentPortal::resolvePortal (:36-55) lesen alle Host/app.theme und schreiben Config; SetDomainUrl ruft dieselben URL::forceRootUrl/forceScheme/useAssetOrigin erneut. → ein DomainContext-Service. (Hinweis: SetDomainUrl scheint nicht in bootstrap/app.php registriert — vor Refactor prüfen, ob toter Code.)
  • Disjunkte Theme-Vokabulare (Hoch — Korrektheits-Smell) — ThemeMiddleware.php:22-53 kennt b2in/b2a/stileigentum/style2own, SetCurrentPortal.php:50-54 nur presseecho/businessportal24 (Rest → null-Portal). → Theme→Portal-Map an einer Stelle (Portal-Enum oder config/domains.php).
  • ThemeMiddleware mutiert REQUEST_URI (Mittel) — :43-52 schreibt URIs um (fragil), :33-38 re-implementiert ?theme=-Override mit anderer Whitelist als der Provider. → Route-Prefixes (wie SetEdition) + ein Override-Pfad.
  • CurrentPortalContext mutable static (Mittel) — app/Services/CurrentPortalContext.php:15 hält private static ?Portal pro Request (Octane-/Test-Risiko). → request-scoped Container-Binding.

3.6 Weitere Korrektheit/Security

  • BasicAuthMiddleware (Hoch — Security) — :45 nutzt != (timing-unsicher) → hash_equals(). bootstrap/app.php:58 liest env('BASIC_AUTH_ENABLED') zur Laufzeit → unter config:cache null ⇒ Basic-Auth still deaktiviert. → Config-Key.
  • CreditWalletService::post Race (Mittel) — :88-89 firstOrCreate() (unlocked) dann separates lockForUpdate()->find(). → Unique-Index auf credit_wallets.user_id + locked Upsert.
  • InvoiceNumberGenerator Erstzeilen-Kollision (Niedrig/Mittel) — :41-63 ->first() (kein Lock) dann insert(); zwei parallele Erstaufrufe ⇒ doppelte MAN-00001. → Unique-Index auf circle + Retry.
  • LegacyInvoicePdfController Laufzeit-Schema::hasColumn (Niedrig) — :26 Schema-Introspektion pro Download. → nach Migration entfernen.
  • ExtraPmPurchaseService magisches * 100 (Niedrig) — :50 Credit→Cents außerhalb CreditPricingService. → in Pricing-Service.

4. Livewire/Volt: Press-Release-Editoren

Vier Dateien, ~5.500 Zeilen, ~80–95 % Überschneidung. Größter einzelner Optimierungs-Hebel im Projekt. admin/press-releases/{create,edit,index,show}.blade.php, customer/press-releases/{create,edit,index,show}.blade.php.

Hoch

  • H1 — Scheduling-Maschinerie 4× wortgleich. updatedPublishMode/ScheduledDate/ScheduledTime(), syncScheduledAt(), resolveScheduledAt(), validateScheduledAtWhenReady(), updated(), notifyValidationError() und der scheduled*-Block in formRules() sind identisch (admin/create:62-89,169-255; admin/edit:121-143,215-339; customer/create:123-145,216-302; customer/edit:163-185,253-339). → InteractsWithPressReleaseScheduling-Trait (spiegelt bestehendes InteractsWithPressReleaseTags). ~150 Zeilen × 3.
  • H2 — formRules() ~95 % identisch (admin/create:152-194; admin/edit:198-240; customer/create:197-241; customer/edit:237-278). Einziger Unterschied: admin portal+companyId exists, customer companyId integer. → baseRules() im Trait, Komponenten überschreiben nur portal/companyId.
  • H3 — presubmitChecks 4× dupliziert, mit Drift. 6-Punkt-Checkliste (admin/create:380-425; admin/edit:574-619; customer/create:583-632; customer/edit:561-608). customer/create nutzt trans_choice(...) + selectedCompany()?->name, die anderen drei __(':n Zeichen') + statischen Text — versehentliche Copy-Paste-Drift. → eine kanonische Version im Trait.
  • H4 — Kontakt-Helfer dupliziert, divergente Query-Styles. defaultContactIdFor(), companyContacts(), companyContact() in allen vier; admin nutzt Contact::withoutGlobalScopes(), customer Contact::forCompany(). tagSuggestionsFor() identisch. → tagSuggestionsFor() in Tags-Trait; Kontakt-Helfer in Trait/Scope.
  • H5 — Admin-Editoren nutzen InteractsWithPressReleaseTags NICHT. Customer (use ... in create:28, edit:29), Admin inlined addTag/removeTag/tagsArray + tags-Computed (admin/create:113-144,374-378,430-442; admin/edit:162-193,...). → use InteractsWithPressReleaseTags; + ~35 Zeilen je Datei löschen.

Mittel

  • M1 — Persistierung dupliziert; Customer zeigt das bessere Muster. Customer zentralisiert in persistDraft()/persistEdit() + pressReleaseAttributes() (customer/create:521-543); Admin inlined volle PressRelease::create([...])/update([...]) (admin/create:310-328; admin/edit:355-370). Slug + HtmlSanitizer::clean() + contacts()->sync() + contentChanged→reclassify/rescore (admin/edit:383-388; customer/edit:435-440,497-502) überall wiederholt. → PressReleaseService::createDraft() / updateContent().
  • M2 — Firmen-Combobox-Suche dupliziert, zwei Backends. Admin baut Query inline in with() (admin/create:344-360, admin/edit:526-542), customer delegiert an CustomerCompanyContext::searchCompaniesFor(). supportsFullTextSearch() kopiert (admin/create:493-496, admin/edit:687-690, admin/index:384-388). → Company-Scope searchByTerm().
  • M3 — Formular-Markup ~85 % identisch (Firma-Selektor, Titel mit Char-Meter @php, Untertitel, Fließtext-Meter, Boilerplate, Tag-Input-Chips, Veröffentlichungs-Panel, Pressekontakt, Pre-Submit-Check). → Blade-Components: <x-portal.pr-char-meter>, <x-portal.pr-tag-input>, <x-portal.pr-presubmit-check>, <x-portal.pr-publish-schedule>, <x-portal.pr-contact-select>, <x-portal.pr-boilerplate-panel>. Größte Roh-Zeilenersparnis.
  • M4 — Listenzeilen admin vs. customer dupliziert (admin/index:836-968 ≈ customer/index:421-531). Filter-Chips mit inline-<svg> ~9× in admin/index:662-801, 3× customer/index:320-376. → <x-portal.pr-table-row> + <x-portal.filter-chip>.
  • M5 — Show-Seiten duplizieren Status-Timeline + Rejection-Banner (admin/show:223-251,431-472 ≈ customer/show:345-370,738-771). statusColor/$statusClass-match an 4+ Stellen. → Mappings auf PressReleaseStatus-Enum (badgeClass()/color()) + <x-portal.pr-status-timeline>, <x-portal.pr-rejection-banner>.
  • M6 — Status-Aktionen dupliziert (publish/reject/archive in admin/index:163-215, admin/edit:446-486, admin/show:25-68). withoutGlobalScopes()->findOrFail($id) ~10× in Admin-Code. → HandlesAdminPressReleaseActions-Trait + findAdminPR().

Niedrig

  • L1 — categoryOptions() admin gecacht (AdminPerformanceCache), customer uncached pro Render (customer/create:557-561, customer/edit:531-535). → gemeinsamer Helfer + Cache.
  • L2 — Kategorie-Anzeigename ->translations->firstWhere('locale','de')?->name ?? ... ~10× → Category::localizedName().
  • L3 — Kontakt-Name trim(first.' '.last) ?: ... mehrfach → Contact::displayName().
  • L4 — statusColor aus with() (admin/edit:558-564, admin/show:111-117) scheinbar ungenutzt (Blade nutzt lokales $statusClass). → prüfen/entfernen.
  • L5 — Image-/Attachment-Manager teilen getPressRelease()/canChange*() (images-manager:257-269, attachments-manager:214-239). Attachments aktuell deaktiviert → bei Reaktivierung ManagesPressReleaseMedia-Trait; canChange* gehört in PressReleasePolicy.

5. Livewire/Volt: Admin-CRUD, Customer, Settings, Auth

Tabellen-Plumbing → Concerns (Hoch)

  • sort($column) wortgleich in admin/users:39-48, companies/index:45-54, contacts/index:51-60. → HasSortableTable ($sortBy/$sortDir/sort() + Whitelist-Helfer).
  • supportsFullTextSearch() 6×+ byte-gleich (users/edit:627-631, users/create:216-220, companies/index:169-173, contacts/index:262-266, contacts/edit:143-147, companies/show:178-182) + „whereFullText else multi-LIKE"-Block mehrfach. → HasFullTextSearch + applyFuzzySearch($query,$columns,$term).
  • updatedXxxFilter() nur resetPage() (users:285-313 = 6 Methoden, companies/index:175-227, contacts/index:62-90). → HasResettableFilters + filterProperties()-Array.
  • stats() mit SUM(CASE WHEN is_active...) strukturidentisch (users:187-203, companies/index:122-141). → AdminPerformanceCache::activeInactiveStats().
  • Count-Hydration (users:205-218 hydrateCompanyCounts, companies/index:143-167 hydrateCounts). → hydrateRelationCounts($paginator, $map).

Create vs. Edit Duplikation (Hoch)

  • User create/edit teilen Rule-Sets + Billing/Company-Logik (create:110-130 ⊂ edit:377-421; billingIsComplete() create:222-229 = edit:708-715; availableRoles() create:209-214 = edit:620-625). save() trägt ~175 Zeilen Business-Logik (edit:375-550). → UserFormRules-Provider + UserUpsertService/SaveUserAction.
  • Billing-/Company-/Role-Markup dupliziert (Rechnungsadresse create:386-436 ≈ edit:1177-1228; Company-Combobox create:322-376 ≈ edit:1003-1069; Rollen create:261-313 ≈ edit:842-995). → <x-admin.billing-fields>, <x-admin.company-link-picker>, <x-admin.role-checkboxes>.
  • Kontakt-Validierung/Markup (users/edit:406-412, contacts/edit, customer/profile:147-168, press-kits/show:169-175). → ValidatesContactData-Concern + <x-contact-fields> (admin+customer geteilt).

Formatierungs-Helfer → Components/Enum (Hoch/Mittel)

  • portalBadgeColor(?Portal) 3–4× (companies/index:229-237, companies/show:184-192, contacts/index:383-391, contacts/edit:170-178). → Portal::badgeColor() + <x-portal.portal-badge>.
  • Status-Badge (ok/err-dot) (companies/index:519-524, users:558-564, customer press-kits). → <x-portal.status-badge :active>.
  • pressReleaseStatusColor() (users:161-170) → auf PressReleaseStatus-Enum.
  • formatEuro() (customer/bookings:36-39) → <x-portal.money> / Helfer.
  • Datum ?->format('d.m.Y H:i') ?? '–' überall → <x-portal.datetime :value>.

Seiten-Struktur → Components (Mittel)

  • Page-Header-Block (users:318-337, users/create:234-253, users/edit:720-746, companies/index:301-320, contacts/index:395-421, payments/index:158-180, companies/show:215-268, companies/edit:199-220). → <x-portal.page-header :eyebrow :title :subtitle> mit Action-Slot.
  • KPI-Grid-Wrapper (users:340-353, companies/index:323-335). → <x-portal.stat-cards :stats>.
  • Filter-Panel + flux:table + Pagination-Footer (users:599-601, companies/index:588-590, payments 4×). → <x-portal.table-panel> / <x-portal.table-pagination :paginator>.
  • Empty-State-Zelle (users:582-594, companies/index:569-584). → <x-portal.empty-state>.
  • Combobox-Pattern (flux:select variant=combobox + Clear-Button: users/create:322-352, users/edit:1003-1043,1074-1121, companies/index:369-445, companies/show:399-432, contacts/edit:211-243). → <x-portal.searchable-select>.

Flash/Notification (Hoch)

  • Auto-Dismiss-Toast (x-data="{show:true}" x-init=setTimeout) in users/edit:748-754, contacts/index:423-437, payments/index:182-196; Session-Varianten companies/show:196-213.
  • @if(session('status')) in auth/login:128-132, register:57-61, forgot-password:30-34, verify-email:33-37, magic-link:87-91; settings nutzen <x-action-message>. → ein <x-portal.alert :type :dismissible> + <x-session-status> app-weit.

Auth & Settings (Hoch/Mittel)

  • Password show/hide-Feld (auth/login:134-180, register:63-143, confirm-password:40-72, reset-password:73-135) → <x-password-input> (Hoch).
  • Email-Input + @error-Markup in allen Auth-Forms → <x-form-field>/<x-email-input>.
  • Rate-Limiting inline & divergent (auth/login:100-124, magic-link:54-83) → ensureNotRateLimited()/throttleKey()-Trait.
  • Heavy Auth-Logik in Volt (2FA/Activation/Redirect: auth/login:28-95, settings/profile:19-73) → Fortify-Actions/Response-Contracts (architektonisch).
  • Divider + Secondary-Action (auth/login:196-218 ≈ register:174-192) → <x-auth.divider-actions>.

Customer-Bereich (Mittel/Niedrig)

  • Portal-Pill/Status-Badge wiederholt (press-kits/index:669-685,778-786, dashboard:352-358, press-kits/show:298-307) → dieselben <x-portal.portal-badge>/<x-portal.status-badge>.
  • Company-Logo-Logik (logoVariant/logoInitials/logoUrl, press-kits/index:221-259,645-650,760-766) → <x-portal.company-logo :company>.
  • Inline-Edit-Form-Toggles (showCompanyForm/showContactForm) in customer/profile, press-kits/show → HasInlineFormModals-Concern.
  • Onboarding-Step-Grid (dashboard:382-398, press-kits/index:611-630) → <x-portal.step-guide :steps>.

Bereits gute Wiederverwendung: <x-portal.stat-card> und components.portal.pagination werden konsistent genutzt — als Muster für die neuen Components übernehmen.


6. Web-Frontend (gehört dem User, separat committen)

Hinweis: resources/views/web/** und resources/views/components/web/** gehören laut Projekt-Memory dem User und werden separat committet. Findings sind beratend; nicht in Feature-Commits bündeln. (Vor jedem Löschen vom User bestätigen lassen — „unused"-Zählungen stammen aus grep über x-web.<name> und prüfen keine dynamische Komponenten-Auflösung.)

Hoch

  • Pricing-Cards 3× handcodiert in web/pressekonto.blade.php:401-466,469-535,538-~610 — fast identische ~67-Zeilen-<article>s, derselbe Check/Dash/Cross-SVG ~15×. Es existiert bereits ein datengetriebenes <x-web.pricing-teaser> (components/web/pricing-teaser.blade.php, iteriert Plan::active()), das diese Seite ignoriert. → pricing-teaser einsetzen; ~200 Zeilen → eine Schleife.
  • press-release-card vs. presseecho-release-card ~95 % identisch (je ~160 Zeilen; nur Default-$contentTypeClasses, Gradient-Klassen, Badge unterscheiden sich). presseecho-release-card hat 0 Verwendungen. → vereinheitlichen auf eine Komponente mit variant-Prop oder löschen.

Mittel

  • Industrie-Icon-Map 3× dupliziert (press-release-card:15-31, presseecho-release-card:15-31, highlight-card:5-22). → in CategoryService (zentralisiert bereits getIconPath()) oder Config.
  • Release-Normalisierung 3× (feed-item:8-50, feed-top-item:6-36, focus-side-item:8-33): gleiche Kategorie-/Firma-/Datum-/Bild-@php-Blöcke; „verified"-SVG verbatim. → View-Model ReleaseCardData::from($release) + <x-web.icons.verified-badge>.
  • Footer standard vs. hub (site-footer:1-82 ≈ hub/site-footer:1-82) → ein <x-web.site-footer> mit columns+variant.
  • Top-Utility-Bar inline in site-header:47-93 vs. komponentisiert in hub/top-utility-bar:1-55 → gemeinsame <x-web.top-utility-bar>.
  • Tote, array-getriebene Legacy-Cards (0 Produktiv-Verwendung, nur web/examples/): presseecho-release-card, press-release-list-item, dossier-card, article-card, highlight-card+highlights-slider, main-navigation. → nach Bestätigung mitsamt web/examples/ entfernen.
  • Inline-SVGs (CTA-Pfeil M2 6h8… in 5 Dateien, Building-Icon, verified-Check) → kleines components/web/icons/-Set.
  • Inline-Array-@foreach-Sektionen in veroeffentlichen.blade.php:196-211,227-249,265-280,293-310,383-405 → <x-web.criteria-item> + statische Arrays in View-Data.

Niedrig

  • Layout-Shell max-w-layout mx-auto px-8 ~20× in pressekonto.blade.php → <x-web.container> oder .layout-container-Utility (wo .card/.badge schon liegen).
  • Such-Form-Block doppelt in site-header (Desktop :110-130, Mobile :196-226) → <x-web.search-form :variant>.
  • Sidebar-Familie (sidebar-widget + -categories/-companies/-newsletter/-rss) ist gut faktorisiert, aktuell aber 0 Produktiv-Verwendung — vor Behalten prüfen.

7. Vorgeschlagene neue Bausteine

Traits/Concerns (app/Livewire/Concerns/, app/Models/Concerns/)

  • InteractsWithPressReleaseScheduling, HandlesAdminPressReleaseActions, ManagesPressReleaseMedia
  • HasSortableTable, HasFullTextSearch, HasResettableFilters, ValidatesContactData, HasInlineFormModals
  • User: HasBillingProfile / InteractsWithQuota

Services (app/Services/**)

  • PressReleaseService::createDraft() / updateContent() (Persistierung + reclassify/rescore)
  • UserUpsertService, LegalRequestService
  • DomainContext (konsolidiert Theme/Portal/Domain-Auflösung)
  • Zentralisierung in bestehenden Services: VatResolver::normalize/isEuCountry, User::hasActivePaymentOption/latestLegacyInvoice/findByEmail

Query-Scopes / Enum-Methoden

  • Company::scopeAccessibleBy(), Company::scopeSearchByTerm(), Contact::scopeForCompany() (vereinheitlicht), UserPaymentOption-Active-Scope
  • Portal::badgeColor(), PressReleaseStatus::badgeClass()/color(), Category::localizedName(), Contact::displayName()

Policies/Gates

  • CompanyPolicy durchgängig nutzen; Gate::define('access-admin'|'access-customer') + can:-Middleware

Blade-Components

  • Portal: pr-char-meter, pr-tag-input, pr-presubmit-check, pr-publish-schedule, pr-contact-select, pr-boilerplate-panel, pr-table-row, pr-status-timeline, pr-rejection-banner, filter-chip, alert, page-header, stat-cards, table-panel, table-pagination, searchable-select, empty-state, portal-badge, status-badge, money, datetime, company-logo, step-guide
  • Admin: billing-fields, company-link-picker, role-checkboxes
  • Auth/Shared: password-input, form-field, session-status, contact-fields, auth.divider-actions

Empfohlenes Vorgehen

  1. Risikoarme Extraktionen zuerst (Traits H1/H5, Concerns 6, Components 7/8 oben) — reine Deduplikation ohne Verhaltensänderung, jeweils mit bestehenden Pest-Tests absichern.
  2. Korrektheits-Divergenzen (Eligibility 3.2, Theme-Vokabular 3.5, Security 3.6) — vor Refactor jeweils einen Test schreiben, der das aktuelle Soll-Verhalten festschreibt.
  3. Persistierung in Services (M1, UserUpsertService) — mittlerer Aufwand, größter architektonischer Gewinn.
  4. Web-Frontend separat mit dem User abstimmen.