8.5 KiB
Implementation Plan: Tenant Review Pack Export v1 (CSV + ZIP)
Branch: 109-review-pack-export | Date: 2026-02-23 | Spec: spec.md
Input: Feature specification from /specs/109-review-pack-export/spec.md
Summary
Implement an exportable audit artifact (ZIP with CSV + JSON) for MSP/Enterprise tenant reviews. The system collects pre-cached stored reports, findings, tenant hardening status, and recent operations from the database (no Graph API calls), assembles them into a deterministic ZIP archive, and stores it on a private local disk. Downloads use signed temporary URLs. Features include fingerprint-based content deduplication, RBAC-gated access (REVIEW_PACK_VIEW / REVIEW_PACK_MANAGE), retention pruning with configurable expiry, and OperationRun-based observability. The Filament UI provides a ReviewPackResource (list + view), a Tenant Dashboard card, and a modal-based generation trigger.
Technical Context
Language/Version: PHP 8.4 (Laravel 12)
Primary Dependencies: Filament v5, Livewire v4, Laravel Framework v12
Storage: PostgreSQL (jsonb columns for summary/options), local filesystem (exports disk) for ZIP artifacts
Testing: Pest v4 (Feature tests with Livewire component testing)
Target Platform: Linux server (Sail/Docker local, Dokploy VPS staging/production)
Project Type: web (Laravel monolith with Filament admin panel)
Performance Goals: Pack generation < 60s for 1,000 findings + 10 stored reports; download streaming with no memory loading of full file
Constraints: DB-only generation (no Graph API calls); chunked iteration for large finding sets; temp file for ZIP assembly (no full in-memory ZIP)
Scale/Scope: Multi-workspace, multi-tenant; packs are tenant-scoped; typical pack size < 10 MB
Constitution Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
| Rule | Status | Notes |
|---|---|---|
| Inventory-first | PASS | ReviewPack reads from existing stored_reports + findings (pre-cached inventory data). No new inventory sources introduced. |
| Read/write separation | PASS | Generation is a queued job (async write). User action is enqueue-only. Destructive actions (expire/delete) require ->requiresConfirmation(). All writes audit-logged via OperationRun. |
| Graph contract path | PASS | No Graph API calls in this feature. All data sourced from DB. FR-006 explicitly forbids Graph calls during generation. |
| Deterministic capabilities | PASS | REVIEW_PACK_VIEW and REVIEW_PACK_MANAGE added to canonical Capabilities.php registry. Discoverable via Capabilities::all() reflection. No raw strings. |
| RBAC-UX: two planes | PASS | All routes under /admin plane. Tenant-context routes scoped via /admin/t/{tenant}/.... No /system plane involvement. |
| RBAC-UX: non-member = 404 | PASS | Non-member workspace/tenant access returns 404 (deny-as-not-found). Policy enforces workspace_id + tenant_id before any data returned. Download endpoint returns 404 for non-existent/inaccessible packs. |
| RBAC-UX: member missing cap = 403 | PASS | Members without REVIEW_PACK_VIEW get 403 on list/download. Members without REVIEW_PACK_MANAGE get 403 on generate/expire. |
| RBAC-UX: destructive confirmation | PASS | Expire and Delete actions use ->requiresConfirmation() with clear warning text. Regenerate requires confirmation if a ready pack exists. |
| RBAC-UX: global search | N/A | ReviewPackResource does not participate in global search (no $recordTitleAttribute). Intentionally excluded. |
| Workspace isolation | PASS | DerivesWorkspaceIdFromTenant auto-sets workspace_id. All queries scope by workspace_id + tenant_id. Download controller validates pack ownership before streaming. |
| Tenant isolation | PASS | Partial unique index scoped to (workspace_id, tenant_id, fingerprint). All scopes include tenant_id. Dashboard widget reads only current tenant packs. |
| Run observability | PASS | OperationRun of type tenant.review_pack.generate tracks lifecycle. Active-run dedupe via partial unique index. Failure reason stored in context['reason_code']. |
| Automation | PASS | Prune command uses withoutOverlapping(). GenerateReviewPackJob unique via OperationRun active dedupe (existing pattern). |
| Data minimization | PASS | FR-008: no webhooks, tokens, secrets, or raw Graph dumps exported. FR-009: PII redaction via include_pii toggle. |
| BADGE-001 | PASS | ReviewPackStatus added to BadgeDomain enum with ReviewPackStatusBadge mapper. All 5 statuses mapped. |
| Filament Action Surface Contract | PASS | UI Action Matrix in spec. List: header action (Generate), row actions (Download + Expire), empty state CTA. View: header actions (Download + Regenerate). Card: inline actions. Destructive actions confirmed. |
| UX-001 | PASS | View page uses Infolist. Empty state has specific title + explanation + 1 CTA. Status badges use BADGE-001. Generate modal fields in Sections. Table has search/sort/filters. |
Post-design re-evaluation: All checks PASS. No violations found.
Project Structure
Documentation (this feature)
specs/109-review-pack-export/
├── plan.md # This file
├── spec.md # Feature specification
├── research.md # Phase 0: resolved unknowns
├── data-model.md # Phase 1: entity design
├── quickstart.md # Phase 1: implementation guide
├── contracts/ # Phase 1: API + action contracts
│ └── api-contracts.md
├── checklists/
│ └── requirements.md
└── tasks.md # Phase 2 output (created by /speckit.tasks)
Source Code (repository root)
app/
├── Console/Commands/
│ └── PruneReviewPacksCommand.php
├── Filament/
│ ├── Resources/
│ │ └── ReviewPackResource.php
│ │ └── Pages/
│ │ ├── ListReviewPacks.php
│ │ └── ViewReviewPack.php
│ └── Widgets/
│ └── TenantReviewPackCard.php
├── Http/Controllers/
│ └── ReviewPackDownloadController.php
├── Jobs/
│ └── GenerateReviewPackJob.php
├── Models/
│ └── ReviewPack.php
├── Notifications/
│ └── ReviewPackStatusNotification.php
├── Services/
│ └── ReviewPackService.php
└── Support/
├── ReviewPackStatus.php
└── Badges/Mappers/
└── ReviewPackStatusBadge.php
config/
├── filesystems.php # Modified: add exports disk
└── tenantpilot.php # Modified: add review_pack section
database/
├── factories/
│ └── ReviewPackFactory.php
└── migrations/
└── XXXX_create_review_packs_table.php
routes/
├── web.php # Modified: add download route
└── console.php # Modified: add prune schedule entry
tests/Feature/ReviewPack/
├── ReviewPackGenerationTest.php
├── ReviewPackDownloadTest.php
├── ReviewPackRbacTest.php
├── ReviewPackPruneTest.php
├── ReviewPackResourceTest.php
└── ReviewPackWidgetTest.php
Structure Decision: Standard Laravel monolith structure. All new files follow existing directory conventions. No new base folders introduced.
Complexity Tracking
No Constitution violations detected. No complexity justifications needed.
Filament v5 Agent Output Contract
- Livewire v4.0+ compliance: Yes. Filament v5 requires Livewire v4 — all components are Livewire v4 compatible.
- Provider registration: Panel provider already registered in
bootstrap/providers.php. No new panel required. - Global search: ReviewPackResource does NOT participate in global search. No
$recordTitleAttributeset. Intentional. - Destructive actions: Expire action uses
->action(...)+->requiresConfirmation()+->color('danger'). Regenerate uses->requiresConfirmation()when a ready pack exists. Hard-delete in prune command requires explicit--hard-deleteflag. - Asset strategy: No custom frontend assets. Standard Filament components only.
php artisan filament:assetsalready in deploy pipeline. - Testing plan: Pest Feature tests covering: generation job (happy + failure paths), download controller (signed URL, expired, RBAC), RBAC enforcement (404/403 matrix), prune command (expire + hard-delete), Filament Resource (Livewire component tests for list/view pages + actions), Dashboard widget (Livewire component test).