- plan.md: 5 implementation phases (A-E), constitution check, risk assessment, test strategy - research.md: 5 findings (R-001 through R-005) on Filament v5 schema reuse - data-model.md: entity documentation, routing changes, file deletion/modification map - contracts/routes.md: canonical vs decommissioned route contracts - quickstart.md: verification steps for implementors - Updated copilot agent context with plan technologies
5.3 KiB
Research: Operations Tenantless Canonical Migration
Feature: 078-operations-tenantless-canonical
Date: 2026-02-06
R-001 — Filament v5 Infolist Reuse on Standalone Pages
Decision: Use the native InteractsWithSchemas mechanism (already on every Page). No need for InteractsWithInfolists or HasInfolists.
Rationale: In Filament v5, the schema system is unified — forms, infolists, and tables all go through InteractsWithSchemas. The InteractsWithInfolists trait is fully deprecated (every method proxies to schema equivalents). HasInfolists is an empty marker interface. Every Filament\Pages\Page already extends BasePage which uses InteractsWithSchemas and implements HasSchemas.
How it works:
- Define
public function infolist(Schema $schema): Schemaon the Page InteractsWithSchemasauto-discovers it via reflection- Render via
{{ $this->infolist }}(magic property) orEmbeddedSchema::make('infolist')
Alternatives considered:
- Adding
InteractsWithInfoliststrait → Rejected: deprecated shim, adds no functionality - Extracting a static builder from
OperationRunResource→ Not needed:infolist()is alreadystatic
Source files verified:
vendor/filament/infolists/src/Concerns/InteractsWithInfolists.php— all methods@deprecatedvendor/filament/infolists/src/Contracts/HasInfolists.php— empty interfacevendor/filament/support/src/Pages/BasePage.php—implements HasSchemas, usesInteractsWithSchemasvendor/filament/support/src/Concerns/InteractsWithSchemas.php— auto-discovers methods by name
R-002 — OperationRunResource::infolist() Compatibility
Decision: Call OperationRunResource::infolist($schema) directly from the standalone Page.
Rationale: The method is public static, uses no $this references, and already handles tenantless context gracefully (Filament::getTenant() returns null → falls back to OperationRunLinks::tenantlessView()).
Requirements for the standalone Page:
public function infolist(Schema $schema): Schema→ delegates toOperationRunResource::infolist($schema)public function defaultInfolist(Schema $schema): Schema→ sets->record($this->run)->columns(2)public bool $opsUxIsTabHidden = falseproperty → needed for polling callback in infolist- Override
content()or render via Blade{{ $this->infolist }}
Alternatives considered:
- Extracting infolist into a shared trait → Overengineered for one consumer
- Keeping hand-coded Blade → Defeats the single-source goal
R-003 — Headless Resource Pattern (Empty getPages)
Decision: Return [] from OperationRunResource::getPages() to eliminate all auto-generated routes.
Rationale: Resource::routes() iterates getPages() in a foreach — empty array means zero route registrations. The resource class is retained as a static utility providing ::table() and ::infolist() schema builders.
Impact: The following route names will no longer exist:
filament.admin.resources.operations.indexfilament.admin.resources.operations.view
Files that reference these routes (need updating):
tests/Feature/Verification/VerificationAuthorizationTest.php(L38, L74) —OperationRunResource::getUrl('view', ...)tests/Feature/OpsUx/FailureSanitizationTest.php(L58) —OperationRunResource::getUrl('view', ...)tests/Feature/OpsUx/CanonicalViewRunLinksTest.php(L25) — guard test scanning for stale references (update regex)tests/Feature/Verification/VerificationDbOnlyTest.php—ViewOperationRunLivewire testtests/Feature/OpsUx/FailureSanitizationTest.php—ViewOperationRunLivewire testtests/Feature/Verification/VerificationReportRenderingTest.php—ViewOperationRunLivewire testtests/Feature/Monitoring/OperationsCanonicalUrlsTest.php(L121) —ListOperationRunsLivewire testtests/Feature/Monitoring/OperationsTenantScopeTest.php(L119) —ListOperationRunsLivewire test
Alternatives considered:
- Keeping a dummy page that redirects → Adds complexity, spec says natural 404
- Excluding resource from discovery → More fragile than empty
getPages()
R-004 — KPI Header Tenantless Behavior
Decision: Hide OperationsKpiHeader when no tenant context is available (Phase 1).
Rationale: The widget runs 6 queries all scoped by tenant_id. Without tenant context, all queries return 0. Showing zeros is misleading. Workspace-scoped queries are a larger refactor deferred to a separate spec.
Implementation: Check Filament::getTenant() in the widget's getStats() — return empty array if null. Or conditionally register the widget on the page based on tenant presence.
Source: app/Filament/Widgets/Operations/OperationsKpiHeader.php — 131 lines, 4 stat cards, $isLazy = false
R-005 — Legacy List Redirect (FR-078-012)
Decision: Optional 302 redirect from /admin/t/{tenant}/operations to /admin/operations.
Rationale: Unlike detail URLs which naturally 404 after page decommission, the list URL pattern may still be reached through Filament's navigation system during the transition period. A simple redirect avoids confusion.
Note: This is the only redirect in the spec. All detail-level legacy URLs naturally 404.