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

95 lines
4.5 KiB
Markdown

# Data Model: Operations Tenantless Canonical Migration
**Feature**: 078-operations-tenantless-canonical
**Date**: 2026-02-06
---
## Overview
This feature does **not** introduce new models or migrations. It reorganizes how existing models are rendered and routed.
## Entities (Existing — No Changes)
### OperationRun
| Field | Type | Notes |
|-------|------|-------|
| `id` | `bigint` (PK) | Auto-increment |
| `workspace_id` | `bigint` (FK) | Required. Authorization boundary. |
| `tenant_id` | `bigint` (FK, nullable) | Null for workspace-level runs (e.g., onboarding). |
| `type` | `string` | Operation type slug (e.g., `policy.sync`, `restore.execute`). |
| `status` | `enum` | `OperationRunStatus`: Queued, Running, Completed, Cancelled. |
| `outcome` | `enum` (nullable) | `OperationRunOutcome`: Succeeded, PartiallySucceeded, Failed. |
| `context` | `jsonb` | Contains `target_scope`, `verification_report`, etc. |
| `summary_counts` | `jsonb` | `{total, processed, succeeded, failed, skipped}` |
| `failure_summary` | `jsonb` (nullable) | Sanitized failure details. |
| `initiated_by` | `bigint` (FK, nullable) | User who started the run. |
| `started_at` | `timestamp` (nullable) | |
| `completed_at` | `timestamp` (nullable) | |
| `created_at` | `timestamp` | |
| `updated_at` | `timestamp` | |
**Relationships**: `belongsTo Workspace`, `belongsTo Tenant` (nullable), `belongsTo User` (initiated_by)
### WorkspaceMembership (Authorization Boundary)
The `OperationRunPolicy::view()` checks:
1. User must be a member of `$run->workspace_id`
2. Returns `Response::denyAsNotFound()` if not a member
No changes to this model or policy logic.
## Routing Changes (No Model Impact)
### Routes Removed
| Route Name | Pattern | Handler |
|------------|---------|---------|
| `filament.admin.resources.operations.index` | `GET /admin/t/{tenant}/operations` | `ListOperationRuns` (resource-generated) |
| `filament.admin.resources.operations.view` | `GET /admin/t/{tenant}/operations/r/{record}` | `ViewOperationRun` |
### Route Added
| Route Name | Pattern | Handler |
|------------|---------|---------|
| `admin.operations.legacy-index` | `GET /admin/t/{tenant}/operations` | Redirect 302 to `/admin/operations` |
### Routes Retained (Unchanged)
| Route Name | Pattern | Handler |
|------------|---------|---------|
| `admin.operations.index` | `GET /admin/operations` | `Operations.php` |
| `admin.operations.view` | `GET /admin/operations/{run}` | `TenantlessOperationRunViewer` |
### Files Deleted
| File | Reason |
|------|--------|
| `app/Filament/Resources/OperationRunResource/Pages/ViewOperationRun.php` | Replaced by TenantlessOperationRunViewer |
| `app/Filament/Resources/OperationRunResource/Pages/ListOperationRuns.php` | Replaced by Operations.php |
| `app/Livewire/Monitoring/OperationsDetail.php` | Dead code |
| `resources/views/livewire/monitoring/operations-detail.blade.php` | Dead code (blade for OperationsDetail) |
### Files Modified
| File | Change |
|------|--------|
| `app/Filament/Resources/OperationRunResource.php` | `getPages()` returns `[]` |
| `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` | Reuse infolist via schema, add related links |
| `resources/views/filament/pages/operations/tenantless-operation-run-viewer.blade.php` | Replace hand-coded HTML with `{{ $this->infolist }}` |
| `app/Filament/Widgets/Operations/OperationsKpiHeader.php` | Hide stats when no tenant context |
### Test Files Requiring Updates
| File | Change Required |
|------|----------------|
| `tests/Feature/Verification/VerificationAuthorizationTest.php` | Replace `OperationRunResource::getUrl('view')` with `route('admin.operations.view')` |
| `tests/Feature/OpsUx/FailureSanitizationTest.php` | Replace `OperationRunResource::getUrl('view')` with canonical route; replace `ViewOperationRun` mount |
| `tests/Feature/OpsUx/CanonicalViewRunLinksTest.php` | Update guard regex to account for headless resource |
| `tests/Feature/Verification/VerificationReportViewerDbOnlyTest.php` | Replace `ViewOperationRun` mount with `TenantlessOperationRunViewer` |
| `tests/Feature/Verification/VerificationReportRedactionTest.php` | Replace `ViewOperationRun` mount with `TenantlessOperationRunViewer` |
| `tests/Feature/Verification/VerificationReportMissingOrMalformedTest.php` | Replace `ViewOperationRun` mount with `TenantlessOperationRunViewer` |
| `tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php` | Remove `ListOperationRuns` test; add route-not-registered assertion |
| `tests/Feature/Monitoring/OperationsTenantScopeTest.php` | Remove `ListOperationRuns` test |