# 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**: Add a 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.
