# 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**: 1. Define `public function infolist(Schema $schema): Schema` on the Page 2. `InteractsWithSchemas` auto-discovers it via reflection 3. Render via `{{ $this->infolist }}` (magic property) or `EmbeddedSchema::make('infolist')` **Alternatives considered**: - Adding `InteractsWithInfolists` trait → Rejected: deprecated shim, adds no functionality - Extracting a static builder from `OperationRunResource` → Not needed: `infolist()` is already `static` **Source files verified**: - `vendor/filament/infolists/src/Concerns/InteractsWithInfolists.php` — all methods `@deprecated` - `vendor/filament/infolists/src/Contracts/HasInfolists.php` — empty interface - `vendor/filament/support/src/Pages/BasePage.php` — `implements HasSchemas`, uses `InteractsWithSchemas` - `vendor/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**: 1. `public function infolist(Schema $schema): Schema` → delegates to `OperationRunResource::infolist($schema)` 2. `public function defaultInfolist(Schema $schema): Schema` → sets `->record($this->run)->columns(2)` 3. `public bool $opsUxIsTabHidden = false` property → needed for polling callback in infolist 4. 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.index` - `filament.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` — `ViewOperationRun` Livewire test - `tests/Feature/OpsUx/FailureSanitizationTest.php` — `ViewOperationRun` Livewire test - `tests/Feature/Verification/VerificationReportRenderingTest.php` — `ViewOperationRun` Livewire test - `tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php` (L121) — `ListOperationRuns` Livewire test - `tests/Feature/Monitoring/OperationsTenantScopeTest.php` (L119) — `ListOperationRuns` Livewire 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.