presseportale/dev/optimize/entwicklungsplan.md
Kevin Adametz 554a731a24 Optimierung Phase 1: Aufraeumen / toter Code
1.1 Prototyp admin/users/table entfernt: Route (war durch admin/users/{id}
    ohnehin unerreichbar) + Volt-Komponente table.blade.php geloescht,
    ADMIN_ROUTES.md aktualisiert; Regressions-Test in PanelConsolidationTest.
1.2 16 tote Legacy-Wrapper-Views unter resources/views/admin/* geloescht
    (nutzten alte Route-Parameter, nirgends via view/@include/@livewire
    gerendert); admin.dashboard + .md-Doku behalten.
1.3 Tote Helfer entfernt: EnsureUserIsAdmin redundantes loadMissing('roles'),
    ungenutzter statusColor-Key aus with() in admin PR edit/show,
    LegacyInvoicePdfController Laufzeit-Schema::hasColumn (Spalte existiert
    seit Migration) inkl. Schema-Import.

Volle Suite gruen (794 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 08:27:12 +00:00

14 KiB
Raw Blame History

Entwicklungsplan: System-Optimierung Pressekonto

Konsolidiert aus system-optimierung-analyse-codex.md und system-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 von env() (brach unter config:cache/im Bootstrap) in die Middleware zur Laufzeit via config('auth.basic.*') verlagert; Config-Block in config/auth.php + .env.example ergänzt. Fund: auth.basic.user/password waren 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(); ApiAccessEligibilityService und LegacyApiCustomerReporter (N+1-frei via Eager-Load-Scope) nutzen sie. Bewusst erhalten: unterschiedliches null-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::$hasGrandfathered nutzt jetzt grandfatheredNow() (spezifisch Bestandsschutz statt „Active ODER Grandfathered"). Test: tests/Feature/ApiAccessEligibilityServiceTest.php; 5 bestehende Tests auf gültiges grandfathered_until angepasst (waren via Factory-optional() nicht-deterministisch). Folge der strikten Wahl (geklärt 19.06.): Ein Grandfathered-User ohne grandfathered_until zä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: ThemeMiddleware war nirgends registriert und referenzierte projektfremde Themes (b2in/b2a/stileigentum/style2own aus einem anderen Projekt) → ersatzlos gelöscht. Theme→Portal-Mapping zentralisiert in Portal::fromTheme() (genutzt von SetCurrentPortal). Fremden $defaults-Fallback im ThemeServiceProvider (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::post ist damit bereits race-sicher (Unique-Index + firstOrCreate/createOrFirst fängt Unique-Violation; Saldo-Update unter lockForUpdate in Transaktion) → kein Change. InvoiceNumberGenerator::next() gehärtet: Erst-Insert-Pfad (kein Seeder → produktiv getroffen) crashte bei parallelem Erstaufruf an der Unique-Violation; jetzt insertOrIgnore (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/table entfernt — Route aus routes/admin.php (war ohnehin durch admin/users/{id} unerreichbar) + resources/views/livewire/admin/users/table.blade.php gelöscht; Eintrag aus routes/ADMIN_ROUTES.md entfernt. Regressions-Test in PanelConsolidationTest (Route::has('admin.users.table') ist false).
  • 1.2 Tote Admin-Wrapper-Views entfernt — verifiziert: kein view()/@include/@extends/@livewire rendert resources/views/admin/* außer admin.dashboard (via DashboardController); Wrapper nutzten alte Route-Parameter (request()->route('company')). 16 tote Wrapper-Blades gelöscht; dashboard.blade.php + die .md-Doku behalten.
  • 1.3 Tote Helfer entfernt — EnsureUserIsAdmin redundantes loadMissing('roles') (Spatie hasAnyRole() lädt ohnehin); statusColor-Key aus with() in admin edit/show (Blade nutzt nur lokales $statusClass); LegacyInvoicePdfController Laufzeit-Schema::hasColumn (Spalte existiert seit Migration 2026_05_04) + unbenutzter Schema-Import.

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 inlined addTag/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 admin portal+companyId exists vs. customer companyId integer. → baseRules() im Trait, Komponenten überschreiben nur abweichende Keys.
  • 2.4 presubmitChecks kanonisieren (H3) — 4× dupliziert mit Copy-Paste-Drift (trans_choice vs. __). → eine Version im Trait.
  • 2.5 Kontakt-Helfer + tagSuggestionsFor() zentralisieren (H4) — defaultContactIdFor/companyContacts/companyContact in allen vier, divergente Query-Styles. → Trait/Scope.
  • 2.6 Persistierung in PressReleaseService (M1) — Customer hat bereits persistDraft/persistEdit/pressReleaseAttributes, Admin inlined volle create/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-match auf PressReleaseStatus::badgeClass()/color().

Phase 3 — Backend-Querschnittslogik zentralisieren (Paket D + Domain)

  • 3.1 Company::scopeAccessibleBy() / CompanyPolicy — identische owner_user_id == me OR whereHas('users', me)-Closure in 4 Controllern + Überlappung mit User::accessibleCompanyIds() und CustomerCompanyContext::accessibleCompanyQuery(). Policy wird in ContactAccessService:229 bereits referenziert. → konsequent über Gate/Policy.
  • 3.2 API-PressRelease-Zugriff + Eager-Loads zentralisieren — findOwnedPressRelease doppelt (PressRelease/Image-Controller); Relation-Liste (company:…, category.translations, images) mehrfach. → expliziter Scope (ownedByApiUser) + withApiResourceRelations(). Unique-Slug-Reimplementierung (PressReleaseController:201-220) durch bestehendes HasUniqueSlug ersetzen.
  • 3.3 DomainContext-Service — Host→Config-Auflösung 4× (ThemeServiceProvider::boot, ThemeMiddleware, SetDomainUrl, SetCurrentPortal). Vorher prüfen, ob SetDomainUrl toter Code ist (scheint nicht in bootstrap/app.php registriert). CurrentPortalContext mutable static → request-scoped Binding.
  • 3.4 Performance — LogApiUsage synchroner INSERT/Request → terminable/queued. CustomerStatusSummary:35-66 Query-Sturm (always-on im Header) memoisieren; offene Single-Purchase 3× → einmal berechnen. CustomerCompanyContext::selectedCompany() doppelte Query → einmal first(). LegacySubscriptionService::cancelBundleFor Update-pro-Iteration → Bulk/Transaction.
  • 3.5 Kleinere Zentralisierungen — VatResolver::normalize/isEuCountry (3×), User::findByEmail() statt LOWER()-Queries (defeated Index, 4×), config('billing.default_currency') statt 'EUR'/'eur'-Literale, gemeinsame Account-Provisioning-Methode (ContactAccessService ≈ SocialAuthService).
  • 3.6 Auth-Gates — EnsureUserIsAdmin/Customer, EnsureApiUserIsActive von 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 DomainContext auf).
  • 4.2 Published-Release-Query zentralisieren — gleiche Bedingung (Portal both, status=published, Sprache, published_at <= now()) mehrfach, teils mit/ohne withoutGlobalScope(PortalScope). → Scopes scopePublished, scopeForPortalValues, scopeForLanguage, scopeVisibleInArchive.
  • 4.3 Page-Data in Services — HomePageData/CategoryPageData/NewsroomPageData fü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() mit SUM(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 aus grep — dynamische Auflösung vor Löschen prüfen.

  • 6.1 Pricing-Cards — web/pressekonto.blade.php codet 3× ~67-Zeilen-<article>s, obwohl datengetriebenes <x-web.pricing-teaser> (iteriert Plan::active()) existiert. → einsetzen (~200 Zeilen → eine Schleife).
  • 6.2 Release-Cards vereinheitlichen — press-release-card ≈ presseecho-release-card (~95 %, letztere 0 Verwendungen). → eine Komponente mit variant-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 in web/examples/ nach Bestätigung entfernen; Inline-SVGs → components/web/icons/-Set; <x-web.container> für max-w-layout-Shell.

Reihenfolge-Empfehlung (TL;DR)

  1. Phase 0 zuerst — Korrektheit/Security, Test-First.
  2. Phase 1 — schnelles Aufräumen, kein Risiko.
  3. Phase 2 — größter Dedup-Gewinn (PR-Editoren), inkrementell.
  4. Phase 3 + 4 — Backend/Web-Architektur (mittlerer Aufwand, hoher struktureller Gewinn).
  5. Phase 5 — Admin/Auth-Components.
  6. 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.