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
289 lines
14 KiB
Markdown
289 lines
14 KiB
Markdown
# Implementation Plan: Operations Tenantless Canonical Migration
|
|
|
|
**Branch**: `078-operations-tenantless-canonical` | **Date**: 2025-07-13 | **Spec**: [spec.md](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)
|
|
|
|
```text
|
|
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)
|
|
|
|
```text
|
|
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.
|
|
|
|
### Phase C — Contextual Navigation (Related Links)
|
|
|
|
**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](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](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](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
|
|
|
|
```bash
|
|
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
|
|
```
|