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
20 KiB
Feature Specification: Operations Tenantless Canonical Migration
Feature Branch: 078-operations-tenantless-canonical
Created: 2026-02-06
Status: Draft
Stack: Filament v5 + Livewire v4 (native only)
Input: Eliminate dual "run detail" surfaces (tenant-scoped Filament view vs tenantless canonical viewer) and make Operations truly canonical, supportable, and secure.
0. Executive Summary
Operations is already canonical at the index and tenantless detail (/admin/operations, /admin/operations/{run}), but auto-generated tenant-scoped Filament resource routes still exist as side-effects of panel discovery:
- Tenant-scoped detail:
/admin/t/{tenant}/operations/r/{record} - Tenant-scoped list:
/admin/t/{tenant}/operations
This spec makes the tenantless detail page the only run detail view, removes the auto-generated tenant-scoped pages (legacy detail URLs naturally 404), and ensures deny-as-not-found security—without introducing non-native UI frameworks.
Clarifications
Session 2026-02-06
- Q: Should a tenant-scoped legacy URL (
/admin/t/{tenant}/operations/r/{record}) for a run withtenant_id = nullbe allowed (redirect) or blocked (404)? → A:Allow redirectSuperseded — FR-078-005/006 removed; legacy detail URLs naturally 404 after route decommission.
1. Goals
- Single canonical run detail view —
/admin/operations/{run} - Remove auto-generated tenant-scoped pages — decommission both
/admin/t/{tenant}/operations/r/{record}and/admin/t/{tenant}/operations - Clean decommission — legacy tenant-scoped detail URLs naturally 404 after route removal (no redirect handlers needed)
- No duplication of UI logic — reuse
OperationRunResource::infolist()in the tenantless viewer - Enterprise security semantics — non-member → 404, no existence leakage
2. Non-Goals
- Building a new operations dashboard
- Introducing an alerts engine
- Changing operation execution semantics
- Implementing fine-grained
operations.viewcapabilities (access remains workspace-membership) - Workspace-scoped KPI header (tracked separately; header hidden in tenantless mode for Phase 1)
3. Current State (Baseline)
Registered routes (today):
| Route | Pattern | Name | Handler | Scope |
|---|---|---|---|---|
| Canonical list | GET /admin/operations |
admin.operations.index |
Operations.php (custom page) |
Workspace (session) |
| Canonical detail | GET /admin/operations/{run} |
admin.operations.view |
TenantlessOperationRunViewer.php |
Workspace membership |
| Auto-generated list | GET /admin/t/{tenant}/operations |
filament.admin.resources.operations.index |
ListOperationRuns.php |
Tenant-scoped |
| Auto-generated view | GET /admin/t/{tenant}/operations/r/{record} |
filament.admin.resources.operations.view |
ViewOperationRun.php |
Tenant-scoped |
Key constraints:
- Tenant-scoped routes exist because
OperationRunResourceis auto-discovered in a tenant panel (AdminPanelProvider::discoverResources). - All production "View run" links already point to canonical tenantless detail via
OperationRunLinks::view()→OperationRunLinks::tenantlessView(). - The resource already declares
$isScopedToTenant = falseand$shouldRegisterNavigation = false.
Link helper state:
OperationRunLinks::view($run, $tenant)delegates totenantlessView($run)— the$tenantparameter is a no-op.OperationRunLinks::related($run, $tenant)returns up to 11 contextual links (Policies, Inventory, Drift, Backup Sets, Provider Connections, etc.).OperationRunUrl::view()andOperationRunUrl::index()are thin wrappers aroundOperationRunLinks.
4. Enterprise Principles (Normative)
P-078-001 — Canonical deep links
There MUST be exactly one canonical URL to open a run: /admin/operations/{run}.
P-078-002 — No tenant context dependency
Canonical run detail MUST render without tenant context and MUST NOT require /admin/t/{tenant}.
P-078-003 — Deny-as-not-found
Any user not entitled to view a run MUST receive 404, not 403. (Constitution: RBAC-UX-002)
P-078-004 — No leakage via legacy URLs
Decommissioned tenant-scoped routes MUST NOT exist after migration. Removed routes naturally return 404 — no redirect handlers, no existence leakage.
P-078-005 — Filament-native
Use Filament pages/resources/infolists/actions and Livewire v4 only. No custom SPA routing.
User Scenarios & Testing
User Story 1 — View operation run via canonical URL (Priority: P1)
A workspace member clicks a "View run" link (from notification, widget, or operations list) and sees the full run detail at /admin/operations/{run} — regardless of whether the run has a tenant or not.
Why this priority: This is the core feature — the single canonical detail surface that replaces the dual-surface.
Independent Test: Create runs with and without tenant_id, navigate to /admin/operations/{run}, assert all sections render (summary, target scope, verification report, context JSON).
Acceptance Scenarios:
- Given a run with
tenant_idset and user is workspace member, When user visits/admin/operations/{run}, Then full detail renders including target scope, verification report (if present), summary counts, and context JSON. - Given a run with
tenant_id = null(e.g., onboarding run), When user visits/admin/operations/{run}, Then detail renders without crash; target scope shows "No target scope details recorded." - Given a run with a verification report in context, When user visits canonical detail, Then verification report section renders correctly with badge rendering and acknowledgements — even though
Filament::getTenant()returns null. - Given user is NOT a workspace member, When user visits
/admin/operations/{run}, Then 404 (deny-as-not-found).
User Story 2 — Legacy tenant-scoped detail URLs return 404 (Priority: P2)
A user has a bookmarked or old notification link pointing to /admin/t/{tenant}/operations/r/{record}. After route decommission, the system returns 404 — no redirect, no existence leakage.
Why this priority: Ensures decommissioned routes don't silently serve stale pages or leak information.
Independent Test: Hit legacy detail URLs; assert 404 for all users regardless of membership.
Acceptance Scenarios:
- Given any user (member or non-member), When user visits
/admin/t/{tenant}/operations/r/{record}, Then 404. - Given any user, When user visits
/admin/operations/r/{record}, Then 404 (the/r/slug variant also does not exist).
User Story 3 — Contextual navigation from run detail (Priority: P2)
On the canonical detail page, a workspace member sees contextual "Open" actions (e.g., "Open tenant", "Policies", "Backup Set", "Provider Connection") based on the run's context — using the existing OperationRunLinks::related() mechanism.
Why this priority: Replaces the "Admin details" back-link with richer, already-implemented navigation.
Independent Test: Create runs of different types, verify that the related links appear in the header actions group.
Acceptance Scenarios:
- Given a run of type
policy.syncwithpolicy_idin context, When viewing canonical detail, Then header shows "Open" group with links to Operations index, Policy, and Policies list. - Given a run with
tenant_idand user is tenant member, When viewing canonical detail, Then related links include tenant-scoped resources (viaOperationRunLinks::related()). - Given a run with
tenant_id = null, When viewing canonical detail, Then no tenant-specific related links appear; only generic links (Operations index).
User Story 4 — Operations list remains workspace-scoped (Priority: P3)
The /admin/operations page continues to show all runs for the current workspace, with an optional tenant default filter when tenant context is active.
Why this priority: Existing behavior; this story ensures no regression during migration.
Independent Test: Visit /admin/operations with and without tenant context; verify workspace scoping and default filter behavior.
Acceptance Scenarios:
- Given workspace context set but no tenant context, When visiting
/admin/operations, Then all workspace runs shown. - Given tenant context active, When visiting
/admin/operations, Then tenant filter defaults to active tenant but can be cleared to show all.
Edge Cases
- Run with
workspace_id = 0or null → 404 (existing behavior inOperationRunPolicy::view) - Run exists but requesting user has no workspace membership → 404 (deny-as-not-found)
- Verification report section with
Filament::getTenant()returning null → falls back toOperationRunLinks::tenantlessView()for previous-run URLs (already handled in infolist) - Legacy tenant-scoped URLs (
/admin/t/{tenant}/operations/r/{record}) → 404 (routes no longer registered after decommission)
Requirements
Constitution alignment (RBAC-UX):
- Authorization plane: Workspace-level (not tenant-level). Operations detail requires workspace membership only.
- 404 vs 403: Non-member → 404 (RBAC-UX-002). No capability-gating for view-only operations access in this spec.
- Server-side enforcement:
OperationRunPolicy::view()+WorkspaceMembershipcheck inTenantlessOperationRunViewer::mount(). - Legacy tenant-scoped routes are decommissioned (naturally 404); no redirect handlers needed (P-078-004).
- Global search:
OperationRunResourcehas$shouldRegisterNavigation = falseand no$recordTitleAttribute— not globally searchable.
Constitution alignment (OPS-EX-AUTH-001): Not applicable — no auth handshakes involved.
Constitution alignment (BADGE-001): No badge changes. Existing badge mappings (OperationRunStatus, OperationRunOutcome) are reused via shared BadgeRenderer in the reused infolist.
Functional Requirements
5.1 Canonical Run Detail — Feature-Complete Tenantless Page
- FR-078-001:
GET /admin/operations/{run}MUST display: run summary (status/outcome/timestamps/initiator), target scope, verification report (DB-only), summary counts, failure summary, and context JSON (redacted). - FR-078-002: Tenantless detail page MUST reuse
OperationRunResource::infolist()via Filament v5's native schema system.TenantlessOperationRunViewerMUST definepublic function infolist(Schema $schema): Schemadelegating toOperationRunResource::infolist($schema), providedefaultInfolist(Schema $schema)to bind->record($this->run)->columns(2), and render the schema via{{ $this->infolist }}(orEmbeddedSchema::make('infolist')incontent()). - FR-078-003: Tenantless detail MUST reuse
OperationRunLinks::related($run, $tenant)for contextual header actions (replacing the removed "Admin details" button). If$run->tenantis null, only generic links (Operations index) appear.
5.2 Decommission Auto-Generated Tenant-Scoped Pages
- FR-078-004: Remove the
'view'page entry fromOperationRunResource::getPages()and deleteViewOperationRun.php. This eliminates the auto-generated/admin/t/{tenant}/operations/r/{record}route. - FR-078-011: Remove the
'index'page entry fromOperationRunResource::getPages()and deleteListOperationRuns.php. This eliminates the auto-generated/admin/t/{tenant}/operationsroute. The resource retains only itstable()andinfolist()schema builders for reuse by the custom pages.
5.3 Legacy URL Handling
Legacy detail URLs (/admin/t/{tenant}/operations/r/{record} and /admin/operations/r/{record}) naturally return 404 after route decommission — no redirect handlers are needed. FR-078-005 and FR-078-006 removed.
-
FR-078-012: Add a convenience redirect for
GET /admin/t/{tenant}/operations:- Redirect 302 to
/admin/operations.
Note: No auth check needed since
/admin/operationsalready enforces workspace membership. - Redirect 302 to
5.4 KPI Header Handling (Phase 1 Simplification)
-
FR-078-008:
OperationsKpiHeaderwidget MUST continue to render when tenant context is available. When no tenant context exists (tenantless pages), the KPI header SHOULD be hidden (not rendered) rather than showing zeros. Full workspace-scoped KPI support is deferred to a follow-up spec. -
FR-078-009: Polling/active-run queries on the canonical list page (
Operations.php) MUST handletenant_id = nullruns and tenantless rendering safely. This MUST be verified by an explicit regression test covering list rendering with and without tenant context.
5.5 Dead Code Cleanup
- FR-078-010: Delete
app/Livewire/Monitoring/OperationsDetail.phpand its Blade viewresources/views/livewire/monitoring/operations-detail.blade.php. This component is unreferenced dead code that enforces an obsolete tenant-only abort check.
Key Entities
- OperationRun: The central model. Has
workspace_id(required for authorization),tenant_id(nullable — onboarding/provider-check runs may lack it),context(JSONB with target_scope, verification_report, etc.),summary_counts,failure_summary. - WorkspaceMembership: The authorization boundary. View access requires membership in the run's workspace.
- OperationRunLinks: Centralized link helper.
view()→tenantlessView()(already canonical).related()provides contextual navigation links.
Success Criteria
Measurable Outcomes
- SC-001: Exactly one canonical run detail URL exists:
/admin/operations/{run}. The routefilament.admin.resources.operations.viewis no longer registered. - SC-002: All "View run" links across notifications, widgets, tables, and jobs resolve to
/admin/operations/{run}(no tenant-scoped detail links remain). - SC-003: Legacy tenant-scoped detail URLs return 404 after route decommission (no redirect handlers, no information leakage).
- SC-004: Runs with
tenant_id = nullrender on canonical detail without errors. - SC-005: Verification report section renders correctly on canonical detail when
Filament::getTenant()returns null. - SC-006: All existing Pest tests pass after migration (no regressions).
Security & RBAC
SR-078-001 — View permissions
- Viewing operations list and run detail requires workspace membership (baseline).
- Non-member → 404 (deny-as-not-found, per RBAC-UX-002).
- No fine-grained capabilities for operations view in this spec.
SR-078-002 — No sensitive leakage in context JSON
- Context JSON shown in UI uses allowlist-based redaction (existing behavior).
- The verification report section already sanitizes sensitive data.
UX Requirements
UX-078-001 — Consistent CTA label
All run CTAs MUST use the canonical label per ux-contracts.md:
- "View run" (exact casing)
UX-078-002 — Context navigation via related links
Canonical detail header MUST show an "Open" action group populated by OperationRunLinks::related(). This provides all relevant contextual links (Operations index, Provider Connection, Policies, Backup Sets, Restore Runs, Drift, Inventory, etc.) based on run type and context.
UX-078-003 — Empty/missing target scope
If target scope is missing: show the existing non-blocking text "No target scope details were recorded for this run." Do not crash; degrade gracefully.
Tests (Mandatory)
T-078-001 — Canonical run detail renders without tenant context
- Create run with
tenant_id = nulland withtenant_idset - Assert
/admin/operations/{run}renders summary sections without crash
T-078-002 — Legacy tenant-scoped detail URLs return 404
- Any user visiting
/admin/t/{tenant}/operations/r/{record}→ 404 (route not registered) - Any user visiting
/admin/operations/r/{record}→ 404 (route not registered)
T-078-004 — No auto-generated tenant-scoped routes exist
- Assert route names
filament.admin.resources.operations.viewandfilament.admin.resources.operations.indexare not registered (or return 404)
T-078-005 — No "Admin details" link on canonical detail
- Assert canonical detail page does not render
/admin/t/.../operations/r/...links
T-078-006 — KPI header hidden in tenantless mode
- On
/admin/operationswithout tenant context, KPI header does not render (or renders gracefully)
T-078-007 — DB-only rendering
- Rendering canonical detail does not dispatch jobs or perform HTTP calls (existing guard tests remain valid)
T-078-008 — Verification report renders on tenantless detail
- Create run with verification report in context and
tenant_idset - Visit
/admin/operations/{run}withoutFilament::getTenant()set - Assert verification report section renders (badge, acknowledgements, change indicator with tenantless previous-run URL)
T-078-009 — Tenant-scoped list redirect
/admin/t/{tenant}/operationsredirects (302) to/admin/operations
T-078-010 — Related links appear on canonical detail
- Create run of type
restore.executewithrestore_run_idin context - Assert header actions include "Restore Run" link
T-078-011 — Operations list tenantless safety
- Visit
/admin/operationswith and without tenant context - Assert list renders successfully and includes
tenant_id = nullruns without polling/query errors
T-078-012 — Canonical CTA label consistency
- Assert run entry points use the exact label "View run"
- Assert canonical detail no longer shows the legacy "Admin details" CTA
Acceptance Criteria
- ✅ Exactly one canonical run detail URL:
/admin/operations/{run} - ✅ Auto-generated tenant-scoped routes (list + view) are removed; legacy detail URLs return 404
- ✅ No "Admin details" / tenant-scoped back-links from canonical pages
- ✅ Operations list works in workspace mode; tenant context only adds default filter
- ✅ Runs with
tenant_id = nulldisplay safely - ✅ Verification report renders correctly without tenant context
- ✅ Related contextual links (from
OperationRunLinks::related()) replace the removed "Admin details" button - ✅ Dead code (
OperationsDetail.php+ Blade view) removed - ✅ Tests cover 404 semantics, infolist rendering, and canonical link rules
- ✅ Implementation is Filament v5 + Livewire v4 native only
Implementation Notes (Non-Normative)
Infolist Reuse Strategy
Use Filament v5's native schema flow on the standalone page:
- Define
infolist(Schema $schema)and delegate toOperationRunResource::infolist($schema) - Define
defaultInfolist(Schema $schema)with->record($this->run)->columns(2) - Render with
{{ $this->infolist }}(and useEmbeddedSchema::make('infolist')fromcontent()when needed)
Resource After Decommission
After removing both page entries from getPages(), OperationRunResource becomes a "headless" resource — it provides table() and infolist() schema builders reused by:
Operations.php(custom list page, viaOperationRunResource::table($table))TenantlessOperationRunViewer(canonical detail, via infolist delegation)
The resource class itself is retained; only its auto-generated routes are eliminated.
KPI Header Deferral
OperationsKpiHeader currently queries 6 times by tenant_id and uses ActiveRuns::existForTenant(). Full workspace-scoping requires adding existForWorkspace() to ActiveRuns and refactoring all 6 queries. This is deferred to a separate spec to keep this migration focused. Phase 1 simply hides the KPI header when tenant context is absent.
Open Decisions
- "Copy JSON" capability-gating: Recommended for enterprise environments but not in scope for this spec.