TenantAtlas/specs/078-operations-tenantless-canonical/research.md
ahmido d56ba85755 Spec 078: Operations tenantless canonical detail (#95)
Implements Spec 078 operations tenantless canonical migration.

Highlights:
- Canonical run detail at `/admin/operations/{run}` renders with standard Filament chrome + sidebar and reuses `OperationRunResource::infolist()` (schema-based, Filament v5).
- Legacy tenant-scoped resource pages removed; legacy URLs return 404 as required.
- Added full spec test pack under `tests/Feature/078/` and updated existing tests.
- Added safe refresh/header actions wiring and KPI header guard when tenant context is null.

Validation:
- `vendor/bin/sail artisan test --compact tests/Feature/078/` (pass)
- `vendor/bin/sail bin pint --dirty` (pass)

Notes:
- Livewire v4+ compliant (Filament v5).
- Panel providers remain registered in `bootstrap/providers.php` (Laravel 11+ standard).

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box>
Reviewed-on: #95
2026-02-07 09:07:26 +00:00

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:

  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.phpimplements 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.phpViewOperationRun Livewire test
  • tests/Feature/OpsUx/FailureSanitizationTest.phpViewOperationRun Livewire test
  • tests/Feature/Verification/VerificationReportRenderingTest.phpViewOperationRun 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.