TenantAtlas/specs/078-operations-tenantless-canonical/plan.md
Ahmed Darrazi e57157016c plan: add implementation plan and design artifacts for 078
- 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
2026-02-07 00:11:20 +01:00

13 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
|   +-- VerificationDbOnlyTest.php                  # TenantlessViewer replaces ViewOperationRun
|   +-- VerificationReportRenderingTest.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-005
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

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).

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

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

Phase E — Optional 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 CanonicalDetailRenderTest.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

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
VerificationDbOnlyTest.php ViewOperationRun replaced with TenantlessOperationRunViewer
VerificationReportRenderingTest.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/VerificationDbOnlyTest.php \
  tests/Feature/Verification/VerificationReportRenderingTest.php \
  tests/Feature/OpsUx/FailureSanitizationTest.php \
  tests/Feature/OpsUx/CanonicalViewRunLinksTest.php