Konsolidierter Entwicklungsplan aus den zwei Analyse-Dokumenten plus
Umsetzung der Phase-0-Massnahmen (Korrektheit/Security, je mit Tests):
0.1 BasicAuth: timing-sicherer Vergleich (hash_equals, beide Felder ohne
Short-Circuit); Enabled-Check zur Laufzeit via config('auth.basic.*')
statt env() im Bootstrap-Closure (greift jetzt unter config:cache);
Config-Block + .env.example. Fund: auth.basic.* war nie als Config
definiert, Gate war faktisch unkonfigurierbar.
0.2 Legacy-Invoice-"latest"-Abfrage zentralisiert (LegacyInvoice::
scopeLatestFirst + User::latestLegacyInvoice); beide Consumer N+1-frei
umgestellt; unterschiedliches null-Verhalten bewusst erhalten.
0.3 Payment-Option-Eligibility vereinheitlicht (period-aware, strikt):
UserPaymentOption::scopeActiveNow/grandfatheredNow + User::
hasActivePaymentOption; hasActiveBooking/hasUnlimitedPressReleaseQuota
und ApiAccessEligibilityService darauf umgestellt; $hasGrandfathered-
Mislabel behoben.
0.4 Theme->Portal-Vokabular: totes, projektfremdes ThemeMiddleware
geloescht; Mapping in Portal::fromTheme() zentralisiert; fremden
b2in-Default im ThemeServiceProvider auf Portal-Default korrigiert.
0.5 InvoiceNumberGenerator: Erst-Insert race-sicher via insertOrIgnore
(Unique-Index auf circle existiert bereits). CreditWalletService war
bereits abgesichert (Unique-Index + createOrFirst).
Tests: 4 neue Dateien, 5 bestehende auf gueltiges grandfathered_until
angepasst. Volle Suite gruen (793 passed, 1 skipped).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
Entwicklungsplan: System-Optimierung Pressekonto
Konsolidiert aus
system-optimierung-analyse-codex.mdundsystem-optimierungen-analyse-claude.md(Stand 18.06.2026). Reihenfolge nach Risiko/Nutzen: erst Korrektheit & Security, dann risikoarme Deduplikation, dann größere Extraktionen.
Arbeitsregeln (für jeden Schritt)
- Test-First bei Korrektheits-/Verhaltensänderungen (Phase 0): erst einen Pest-Test schreiben, der das Soll-Verhalten festschreibt, dann ändern.
- Test-After bei reiner Extraktion (Phasen 1–5): bestehende Feature-Tests vor und nach dem Schritt laufen lassen — sie müssen grün bleiben.
- Tests gezielt laufen:
vendor/bin/sail artisan test --compact --filter=<Name>. - Nach PHP-Änderungen:
vendor/bin/sail bin pint --dirty --format agent. - Web-Frontend (
resources/views/web/**,components/web/**) gehört dem User → Phase 6, separat committen, vor jedem Löschen bestätigen lassen. - Jeder Schritt = ein eigener, fokussierter Commit.
Phase 0 — Korrektheit & Security (höchste Dringlichkeit, Test-First)
Latente Fehlerquellen und Sicherheitslücken. Klein, aber wichtig.
- 0.1 BasicAuth absichern —
!=→hash_equals()(timing-sicher, beide Felder ohne Short-Circuit). Enabled-Check vonenv()(brach unterconfig:cache/im Bootstrap) in die Middleware zur Laufzeit viaconfig('auth.basic.*')verlagert; Config-Block inconfig/auth.php+.env.exampleergänzt. Fund:auth.basic.user/passwordwaren nie als Config-Key definiert → Gate war faktisch nicht konfigurierbar; jetzt nur erzwungen, wenn aktiviert UND Zugangsdaten gesetzt. Test:tests/Feature/BasicAuthMiddlewareTest.php. - 0.2 Legacy-Invoice-„latest"-Abfrage zentralisiert —
LegacyInvoice::scopeLatestFirst()als Single Source of Truth +User::latestLegacyInvoice();ApiAccessEligibilityServiceundLegacyApiCustomerReporter(N+1-frei via Eager-Load-Scope) nutzen sie. Bewusst erhalten: unterschiedlichesnull-Verhalten (Gate=deny vs. Report=needs_review) ist kontextuell korrekt, keine Vereinheitlichung. Test:tests/Feature/ApiAccessEligibilityServiceTest.php. - 0.3 Zahlungsoption-Eligibility vereinheitlicht (period-aware, strikt) — Entscheidung des Users: überall period-aware. Neue Single Source of Truth:
UserPaymentOption::scopeActiveNow()(Active in laufender Periode ODER laufender Bestandsschutz) +scopeGrandfatheredNow();User::hasActivePaymentOption(). Umgestellt:User::hasUnlimitedPressReleaseQuota,User::hasActiveBooking,ApiAccessEligibilityService. Mislabel behoben:CustomerStatusSummary::$hasGrandfatherednutzt jetztgrandfatheredNow()(spezifisch Bestandsschutz statt „Active ODER Grandfathered"). Test:tests/Feature/ApiAccessEligibilityServiceTest.php; 5 bestehende Tests auf gültigesgrandfathered_untilangepasst (waren via Factory-optional()nicht-deterministisch). Folge der strikten Wahl (geklärt 19.06.): Ein Grandfathered-User ohnegrandfathered_untilzählt nicht mehr als aktiv — User-Entscheidung:NULL = abgelaufen/ungültig, Verhalten beibehalten (konsistent mit altem API-Gate), kein weiterer Code-Change. - 0.4 Theme→Portal-Vokabular vereinheitlicht — Fund:
ThemeMiddlewarewar nirgends registriert und referenzierte projektfremde Themes (b2in/b2a/stileigentum/style2ownaus einem anderen Projekt) → ersatzlos gelöscht. Theme→Portal-Mapping zentralisiert inPortal::fromTheme()(genutzt vonSetCurrentPortal). Fremden$defaults-Fallback imThemeServiceProvider(b2in/build/b2in) auf echten Portal-Default (portal/build/portal) korrigiert. Test:tests/Unit/PortalFromThemeTest.php. - 0.5 Race-Conditions abgesichert — Fund: Die empfohlenen Unique-Indizes existieren bereits (
credit_wallets.user_id,invoice_number_sequences.circle) → keine Migration nötig.CreditWalletService::postist damit bereits race-sicher (Unique-Index +firstOrCreate/createOrFirstfängt Unique-Violation; Saldo-Update unterlockForUpdatein Transaktion) → kein Change.InvoiceNumberGenerator::next()gehärtet: Erst-Insert-Pfad (kein Seeder → produktiv getroffen) crashte bei parallelem Erstaufruf an der Unique-Violation; jetztinsertOrIgnore(race-sicher, null-Branch eliminiert). Test ergänzt:tests/Feature/Billing/InvoiceNumberGeneratorTest.php.
Phase 1 — Aufräumen / Toter Code (risikoarm, Paket A)
Keine Fachlogik-Änderung. Reduziert erreichbare Demo-Komponenten und Altlasten.
- 1.1 Prototyp
admin/users/tableentfernen — Route inroutes/admin.php+resources/views/livewire/admin/users/table.blade.php. Filtert auf nicht existentesusers.status, liest$user->role(statt Spatie), englische Demo-Menüs → kann SQL-Fehler/falsche UI erzeugen. Test für entfernte Route anpassen/ergänzen. - 1.2 Tote Admin-Wrapper-Views prüfen & entfernen —
resources/views/admin/*werden außeradmin.dashboard(viaDashboardController) nicht produktiv gerendert. Wrapper mit alten Parametern (request()->route('pressRelease')vs. aktuelle{id}-Route) entfernen oder klar als Legacy markieren.admin.dashboardbehalten. - 1.3 Tote Helfer entfernen —
EnsureUserIsAdmin:16-18totesloadMissing('roles');statusColorauswith()(admin/edit:558-564, admin/show:111-117) prüfen/entfernen;LegacyInvoicePdfController:26Laufzeit-Schema::hasColumnnach Migration entfernen.
Phase 2 — Press-Release-Editoren entdoppeln (größter Einzel-Hebel, Paket B)
4 Dateien, ~5.500 Zeilen, ~80–95 % Überschneidung. In kleinen, risikoarmen Schritten.
- 2.1 Admin-Editoren nutzen
InteractsWithPressReleaseTags(H5) — Customer nutzt den Trait bereits, Admin inlinedaddTag/removeTag/tagsArray+tags-Computed. →use-Statement + ~35 Zeilen je Datei löschen. - 2.2
InteractsWithPressReleaseScheduling-Trait (H1) —updatedPublishMode/ScheduledDate/ScheduledTime,syncScheduledAt,resolveScheduledAt,validateScheduledAtWhenReady,notifyValidationError+scheduled*-Block sind 4× wortgleich. Spiegelt bestehenden Tags-Trait. - 2.3
formRules()zentralisieren (H2) — ~95 % identisch; einziger Unterschied adminportal+companyId existsvs. customercompanyId integer. →baseRules()im Trait, Komponenten überschreiben nur abweichende Keys. - 2.4
presubmitCheckskanonisieren (H3) — 4× dupliziert mit Copy-Paste-Drift (trans_choicevs.__). → eine Version im Trait. - 2.5 Kontakt-Helfer +
tagSuggestionsFor()zentralisieren (H4) —defaultContactIdFor/companyContacts/companyContactin allen vier, divergente Query-Styles. → Trait/Scope. - 2.6 Persistierung in
PressReleaseService(M1) — Customer hat bereitspersistDraft/persistEdit/pressReleaseAttributes, Admin inlined vollecreate/update. Slug +HtmlSanitizer::clean()+contacts()->sync()+contentChanged→reclassify/rescore überall wiederholt. →PressReleaseService::createDraft()/updateContent(). - 2.7 Firmen-Combobox-Suche (M2) — Admin baut Query inline, Customer delegiert an
CustomerCompanyContext::searchCompaniesFor().supportsFullTextSearch()3× kopiert. →Company::scopeSearchByTerm()(siehe auch 3.x). - 2.8 Formular-Markup als Blade-Components (M3, größte Roh-Zeilenersparnis) —
pr-char-meter,pr-tag-input,pr-presubmit-check,pr-publish-schedule,pr-contact-select,pr-boilerplate-panel. - 2.9 Listen-/Show-Components + Admin-Aktionen (M4–M6) —
pr-table-row,filter-chip,pr-status-timeline,pr-rejection-banner;HandlesAdminPressReleaseActions-Trait (findAdminPR()ersetzt ~10×withoutGlobalScopes()->findOrFail($id)); Status-matchaufPressReleaseStatus::badgeClass()/color().
Phase 3 — Backend-Querschnittslogik zentralisieren (Paket D + Domain)
- 3.1
Company::scopeAccessibleBy()/CompanyPolicy— identischeowner_user_id == me OR whereHas('users', me)-Closure in 4 Controllern + Überlappung mitUser::accessibleCompanyIds()undCustomerCompanyContext::accessibleCompanyQuery(). Policy wird inContactAccessService:229bereits referenziert. → konsequent über Gate/Policy. - 3.2 API-PressRelease-Zugriff + Eager-Loads zentralisieren —
findOwnedPressReleasedoppelt (PressRelease/Image-Controller); Relation-Liste (company:…,category.translations,images) mehrfach. → expliziter Scope (ownedByApiUser) +withApiResourceRelations(). Unique-Slug-Reimplementierung (PressReleaseController:201-220) durch bestehendesHasUniqueSlugersetzen. - 3.3
DomainContext-Service — Host→Config-Auflösung 4× (ThemeServiceProvider::boot,ThemeMiddleware,SetDomainUrl,SetCurrentPortal). Vorher prüfen, obSetDomainUrltoter Code ist (scheint nicht inbootstrap/app.phpregistriert).CurrentPortalContextmutable static → request-scoped Binding. - 3.4 Performance —
LogApiUsagesynchroner INSERT/Request → terminable/queued.CustomerStatusSummary:35-66Query-Sturm (always-on im Header) memoisieren; offene Single-Purchase 3× → einmal berechnen.CustomerCompanyContext::selectedCompany()doppelte Query → einmalfirst().LegacySubscriptionService::cancelBundleForUpdate-pro-Iteration → Bulk/Transaction. - 3.5 Kleinere Zentralisierungen —
VatResolver::normalize/isEuCountry(3×),User::findByEmail()stattLOWER()-Queries (defeated Index, 4×),config('billing.default_currency')statt'EUR'/'eur'-Literale, gemeinsame Account-Provisioning-Methode (ContactAccessService≈SocialAuthService). - 3.6 Auth-Gates —
EnsureUserIsAdmin/Customer,EnsureApiUserIsActivevon Hand-Rolling →Gate::define('access-admin'|'access-customer')+can:-Middleware. Impersonation-Redirect aus dem Admin-Gate in eigene Middleware.
Phase 4 — Web-Routen entflechten (Paket C)
routes/web.php > 1100 Zeilen mit kompletter Datenaufbereitung. Schrittweise, gut absicherbar.
- 4.1 Portal-/Domain-Kontext extrahieren (baut auf 3.3
DomainContextauf). - 4.2 Published-Release-Query zentralisieren — gleiche Bedingung (Portal
both,status=published, Sprache,published_at <= now()) mehrfach, teils mit/ohnewithoutGlobalScope(PortalScope). → ScopesscopePublished,scopeForPortalValues,scopeForLanguage,scopeVisibleInArchive. - 4.3 Page-Data in Services —
HomePageData/CategoryPageData/NewsroomPageDatafür Home/Kategorie/Newsroom/Detail/Suche/Preise. - 4.4 Routen auf schlanke (Invokable) Controller umstellen — schrittweise je Seite.
Phase 5 — Admin-CRUD & Auth: Concerns + Blade-Components (risikoarm)
- 5.1 Tabellen-Concerns —
HasSortableTable(sort()3× wortgleich),HasFullTextSearch(supportsFullTextSearch()6×+ +applyFuzzySearch),HasResettableFilters(updatedXxxFilter()→resetPage).stats()mitSUM(CASE WHEN is_active)→AdminPerformanceCache::activeInactiveStats(); Count-Hydration →hydrateRelationCounts(). - 5.2 User create/edit entdoppeln —
UserFormRules-Provider +UserUpsertService/SaveUserAction(save()trägt ~175 Zeilen). Markup →<x-admin.billing-fields>,<x-admin.company-link-picker>,<x-admin.role-checkboxes>. Kontakt →ValidatesContactData-Concern +<x-contact-fields>. - 5.3 Formatierungs-Components/Enum —
Portal::badgeColor()+<x-portal.portal-badge>;<x-portal.status-badge>;PressReleaseStatus-Enum-Methoden;<x-portal.money>,<x-portal.datetime>,Category::localizedName(),Contact::displayName(). - 5.4 Seiten-Struktur-Components —
<x-portal.page-header>,<x-portal.stat-cards>,<x-portal.table-panel>,<x-portal.table-pagination>,<x-portal.empty-state>,<x-portal.searchable-select>. (Muster: bestehende<x-portal.stat-card>/components.portal.pagination.) - 5.5 Flash/Auth-Components —
<x-portal.alert :dismissible>+<x-session-status>app-weit;<x-password-input>,<x-form-field>/<x-email-input>;ensureNotRateLimited()/throttleKey()-Trait (login/magic-link divergieren);<x-auth.divider-actions>.
Phase 6 — Web-Frontend (separat, mit User abstimmen)
resources/views/web/**+components/web/**gehören dem User. Beratend; eigener Commit. „unused"-Zählungen stammen ausgrep— dynamische Auflösung vor Löschen prüfen.
- 6.1 Pricing-Cards —
web/pressekonto.blade.phpcodet 3× ~67-Zeilen-<article>s, obwohl datengetriebenes<x-web.pricing-teaser>(iteriertPlan::active()) existiert. → einsetzen (~200 Zeilen → eine Schleife). - 6.2 Release-Cards vereinheitlichen —
press-release-card≈presseecho-release-card(~95 %, letztere 0 Verwendungen). → eine Komponente mitvariant-Prop oder löschen. Industrie-Icon-Map (3×) →CategoryService/Config; Release-Normalisierung (3×) →ReleaseCardData::from(). - 6.3 Footer/Header/Toter Code —
site-footer≈hub/site-footer, Top-Utility-Bar; tote Legacy-Cards inweb/examples/nach Bestätigung entfernen; Inline-SVGs →components/web/icons/-Set;<x-web.container>fürmax-w-layout-Shell.
Reihenfolge-Empfehlung (TL;DR)
- Phase 0 zuerst — Korrektheit/Security, Test-First.
- Phase 1 — schnelles Aufräumen, kein Risiko.
- Phase 2 — größter Dedup-Gewinn (PR-Editoren), inkrementell.
- Phase 3 + 4 — Backend/Web-Architektur (mittlerer Aufwand, hoher struktureller Gewinn).
- Phase 5 — Admin/Auth-Components.
- Phase 6 — Web-Frontend separat mit dem User.
Beibehalten (gute Muster): PressRelease::accessibleBy, PortalScope, HasUniqueSlug, Form Requests + Eloquent Resources, AdminPerformanceCache, InteractsWithPressReleaseTags, <x-portal.stat-card>/components.portal.pagination.