TenantAtlas/specs/078-operations-tenantless-canonical/plan.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

14 KiB

Implementation Plan: Operations Tenantless Canonical Migration

Branch: 078-operations-tenantless-canonical | Date: 2025-07-13 | Spec: spec.md Input: Feature specification from /specs/078-operations-tenantless-canonical/spec.md

Summary

Make Operations detail fully canonical at /admin/operations/{run} by converting TenantlessOperationRunViewer to reuse OperationRunResource::infolist() via Filament v5's unified schema system, decommissioning auto-generated tenant-scoped resource pages (they naturally 404 after route removal), cleaning up dead code, and updating all affected tests.

Key approach: Filament v5 deprecated InteractsWithInfolists — every Page already has InteractsWithSchemas. The existing OperationRunResource::infolist() is public static with no $this references and already handles Filament::getTenant() returning null. This means TenantlessOperationRunViewer can define infolist(Schema $schema) to delegate directly, achieving full visual parity with zero code duplication.

Technical Context

Language/Version: PHP 8.4 (Laravel 12) Primary Dependencies: Filament v5, Livewire v4, Filament Infolists (schema-based) Storage: PostgreSQL (no new migrations — read-only model changes) Testing: Pest v4 (Feature tests) Target Platform: Web (Laravel Sail / Docker) Project Type: Web application (monolith) Performance Goals: DB-only rendering (no external calls on page load) Constraints: Tenantless pages must render without Filament::getTenant(); no new dependencies Scale/Scope: ~15 files modified/deleted, ~8 test files updated, 0 new migrations

Constitution Check

GATE: All pass. No violations.

Principle Status Notes
Inventory-first N/A No inventory changes
Read/write separation Pass Feature is read-only (rendering changes only)
Graph contract path N/A No Graph calls
Deterministic capabilities N/A No capability changes
RBAC-UX planes Pass Workspace-level auth only; non-member = 404 (RBAC-UX-002)
RBAC-UX destructive N/A No destructive actions
RBAC-UX global search Pass Resource has $shouldRegisterNavigation = false, no $recordTitleAttribute
Tenant isolation Pass Reads workspace-scoped; no cross-tenant access
Run observability N/A No new operations; monitoring pages remain DB-only
Automation N/A No queued/scheduled work
Data minimization Pass No new data stored
Badge semantics Pass Existing BadgeRenderer reused via infolist — no new badge mappings

Post-design re-check: Same results — no constitution violations.

Project Structure

Documentation (this feature)

specs/078-operations-tenantless-canonical/
+-- spec.md              # Feature specification
+-- plan.md              # This file
+-- research.md          # Phase 0: Filament v5 schema research
+-- data-model.md        # Phase 1: Entity & routing changes
+-- quickstart.md        # Phase 1: Verification steps
+-- contracts/
|   +-- routes.md        # Route contract (before/after)
+-- checklists/
|   +-- requirements.md  # Spec quality checklist
+-- tasks.md             # Phase 2 output (created by /speckit.tasks)

Source Code (files touched)

app/
+-- Filament/
|   +-- Resources/
|   |   +-- OperationRunResource.php               # getPages() returns []
|   |   +-- OperationRunResource/Pages/
|   |       +-- ViewOperationRun.php                # DELETE
|   |       +-- ListOperationRuns.php               # DELETE
|   +-- Pages/
|   |   +-- Operations/
|   |       +-- TenantlessOperationRunViewer.php    # Infolist reuse + related links
|   +-- Widgets/
|       +-- Operations/
|           +-- OperationsKpiHeader.php             # Hide stats when no tenant
+-- Livewire/
    +-- Monitoring/
        +-- OperationsDetail.php                    # DELETE (dead code)

resources/views/
+-- filament/pages/operations/
|   +-- tenantless-operation-run-viewer.blade.php   # Replace HTML with infolist render
+-- livewire/monitoring/
    +-- operations-detail.blade.php                 # DELETE (dead code)

tests/Feature/
+-- Operations/
|   +-- TenantlessOperationRunViewerTest.php        # Update infolist assertions
+-- Monitoring/
|   +-- OperationsCanonicalUrlsTest.php             # Remove ListOperationRuns, add route-gone
|   +-- OperationsTenantScopeTest.php               # Remove ListOperationRuns reference
+-- Verification/
|   +-- VerificationAuthorizationTest.php           # Canonical route instead of getUrl
|   +-- VerificationReportViewerDbOnlyTest.php      # TenantlessViewer replaces ViewOperationRun
|   +-- VerificationReportRedactionTest.php         # TenantlessViewer replaces ViewOperationRun
|   +-- VerificationReportMissingOrMalformedTest.php # TenantlessViewer replaces ViewOperationRun
+-- OpsUx/
|   +-- FailureSanitizationTest.php                 # Canonical route + TenantlessViewer
|   +-- CanonicalViewRunLinksTest.php               # Update guard regex
+-- 078/                                            # NEW spec-specific tests
    +-- CanonicalDetailRenderTest.php
    +-- LegacyRoutesReturnNotFoundTest.php
    +-- KpiHeaderTenantlessTest.php
    +-- VerificationReportTenantlessTest.php
    +-- TenantListRedirectTest.php
    +-- RelatedLinksOnDetailTest.php

Structure Decision: Standard Laravel monolith. No new directories except tests/Feature/078/ for spec tests.

Complexity Tracking

No constitution violations to justify.

Violation Why Needed Simpler Alternative Rejected Because
None N/A N/A

Implementation Phases

Phase A — Headless Resource + Dead Code Cleanup

Goal: Remove auto-generated routes; delete dead code. Risk: Low — removing unused pages and dead code. Tests first: T-078-004 (routes not registered), T-078-002 (legacy URLs 404).

Step File Change
A.1 OperationRunResource.php getPages() returns []
A.2 ViewOperationRun.php Delete file
A.3 ListOperationRuns.php Delete file
A.4 OperationsDetail.php Delete file (dead code)
A.5 operations-detail.blade.php Delete file (dead code)
A.6 tests/Feature/078/LegacyRoutesReturnNotFoundTest.php New: T-078-002 + T-078-004
A.7 7 existing test files Replace ViewOperationRun / ListOperationRuns / getUrl('view') references

Exit criteria: All existing tests pass; legacy URLs return 404; no routes registered for resource.

Phase B — Infolist Reuse on TenantlessOperationRunViewer

Goal: Replace hand-coded Blade with Filament schema-based infolist for full visual parity. Risk: Medium — schema auto-discovery on standalone Page needs verification. Tests first: T-078-001 (canonical detail renders), T-078-008 (verification report tenantless).

Step File Change
B.1 TenantlessOperationRunViewer.php Add infolist(Schema $schema) — delegates to OperationRunResource::infolist($schema)
B.2 TenantlessOperationRunViewer.php Add defaultInfolist(Schema $schema)->record($this->run)->columns(2)
B.3 TenantlessOperationRunViewer.php Add content(Schema $schema) — returns EmbeddedSchema::make('infolist')
B.4 TenantlessOperationRunViewer.php Add public bool $opsUxIsTabHidden = false property (polling callback)
B.5 tenantless-operation-run-viewer.blade.php Replace hand-coded HTML with infolist render tag
B.6 tests/Feature/078/CanonicalDetailRenderTest.php New: T-078-001 (+ T-078-007 guard)
B.7 tests/Feature/078/VerificationReportTenantlessTest.php New: T-078-008

Exit criteria: Canonical detail shows identical layout to old tenant-scoped view; verification report section renders.

Goal: Replace "Admin details" button with OperationRunLinks::related() action group. Risk: Low — mechanism already exists, just wiring. Tests first: T-078-010 (related links appear), T-078-005 (no "Admin details" link).

Step File Change
C.1 TenantlessOperationRunViewer.php Add getHeaderActions() using OperationRunLinks::related()
C.2 TenantlessOperationRunViewer.php Remove "Admin details" button code (~line 61)
C.3 tests/Feature/078/RelatedLinksOnDetailTest.php New: T-078-010 + T-078-005 + T-078-012

Exit criteria: Related links render for different run types; "Admin details" link absent.

Phase D — KPI Header Tenantless Handling

Goal: Hide KPI stats when no tenant context (not workspace-scoped — deferred). Risk: Low — conditional early return. Tests first: T-078-006 (KPI hidden in tenantless mode), T-078-011 (tenantless list query safety).

Step File Change
D.1 OperationsKpiHeader.php getStats(): if Filament::getTenant() is null then return []
D.2 tests/Feature/078/KpiHeaderTenantlessTest.php New: T-078-006
D.3 tests/Feature/078/OperationsListTenantlessSafetyTest.php New: T-078-011

Exit criteria: Operations page without tenant context renders without errors; KPI section hidden.

Phase E — List Redirect (FR-078-012)

Goal: Convenience redirect for decommissioned list URL. Risk: Low — single route addition. Tests first: T-078-009 (302 redirect).

Step File Change
E.1 routes/web.php Add redirect: /admin/t/{tenant}/operations 302 to /admin/operations
E.2 tests/Feature/078/TenantListRedirectTest.php New: T-078-009

Exit criteria: Tenant-scoped list URL redirects; no other URLs affected.


Key Design Decisions

D-001 — Schema-based infolist (not InteractsWithInfolists)

Filament v5 unified forms/infolists/tables into the schema system. InteractsWithInfolists is deprecated. Every Page already has InteractsWithSchemas via BasePage. Define infolist(Schema $schema) on the page and it auto-discovers via reflection.

See: research.md R-001

D-002 — Empty getPages() (not resource exclusion)

Returning [] from getPages() cleanly prevents all route registration while keeping the class available for ::table() and ::infolist() reuse. Simpler than excluding from panel discovery.

See: research.md R-003

D-003 — Natural 404 (not redirect handlers)

After route decommission, legacy detail URLs naturally 404. No redirect handlers needed — simplest approach, zero information leakage, zero maintenance.

See: spec.md clarifications (FR-078-005/006 removed)

D-004 — KPI hidden (not workspace-scoped queries)

Phase 1 hides KPI when no tenant. Full workspace-scoped KPI requires refactoring 6 queries + ActiveRuns::existForWorkspace() — deferred to separate spec.

See: research.md R-004, R-005


Risk Assessment

Risk Impact Likelihood Mitigation
Schema auto-discovery fails on standalone Page Medium Low Research confirms it works (R-001); fallback: static builder extraction
Test updates miss a reference to deleted pages Low Medium grep -r sweep for ViewOperationRun and ListOperationRuns before PR
Infolist polling breaks without opsUxIsTabHidden Low Medium Add property explicitly; existing poll logic has null-safe fallback
KPI widget error when tenant is null Low Low Already returns [] on null; this change makes it explicit

Test Strategy

New Tests (spec-specific in tests/Feature/078/)

Test ID File Coverage
T-078-001 CanonicalDetailRenderTest.php Detail renders with/without tenant_id
T-078-002 LegacyRoutesReturnNotFoundTest.php Legacy detail URLs return 404
T-078-004 LegacyRoutesReturnNotFoundTest.php Route names not registered
T-078-005 RelatedLinksOnDetailTest.php No "Admin details" link
T-078-006 KpiHeaderTenantlessTest.php KPI hidden without tenant
T-078-008 VerificationReportTenantlessTest.php Verification report renders tenantless
T-078-009 TenantListRedirectTest.php List redirect 302
T-078-010 RelatedLinksOnDetailTest.php Related links in header actions
T-078-011 OperationsListTenantlessSafetyTest.php List renders safely with tenant and tenantless context
T-078-012 RelatedLinksOnDetailTest.php Canonical CTA label is "View run" and legacy CTA removed

Updated Tests (existing)

File Change
VerificationAuthorizationTest.php getUrl('view') replaced with route('admin.operations.view')
FailureSanitizationTest.php getUrl('view') replaced with canonical route; ViewOperationRun replaced with TenantlessOperationRunViewer
CanonicalViewRunLinksTest.php Update guard regex for headless resource
VerificationReportViewerDbOnlyTest.php ViewOperationRun replaced with TenantlessOperationRunViewer
VerificationReportRedactionTest.php ViewOperationRun replaced with TenantlessOperationRunViewer
VerificationReportMissingOrMalformedTest.php ViewOperationRun replaced with TenantlessOperationRunViewer
OperationsCanonicalUrlsTest.php Remove ListOperationRuns test, add route-gone assertion
OperationsTenantScopeTest.php Remove ListOperationRuns reference

Focused Test Command

vendor/bin/sail artisan test --compact \
  tests/Feature/078/ \
  tests/Feature/Operations/TenantlessOperationRunViewerTest.php \
  tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php \
  tests/Feature/Monitoring/OperationsTenantScopeTest.php \
  tests/Feature/Verification/VerificationAuthorizationTest.php \
  tests/Feature/Verification/VerificationReportViewerDbOnlyTest.php \
  tests/Feature/Verification/VerificationReportRedactionTest.php \
  tests/Feature/Verification/VerificationReportMissingOrMalformedTest.php \
  tests/Feature/OpsUx/FailureSanitizationTest.php \
  tests/Feature/OpsUx/CanonicalViewRunLinksTest.php