# Implementation Plan: Inventory Sync Button (046) **Branch**: `046-inventory-sync-button` | **Date**: 2026-01-08 | **Spec**: `specs/046-inventory-sync-button/spec.md` **Input**: Feature specification from `specs/046-inventory-sync-button/spec.md` ## Summary Add a “Run Inventory Sync” action in the Inventory area that dispatches a queued job, records a new `InventorySyncRun` attributed to the initiating user, and provides visibility via: - Filament database notifications - Existing bottom-right progress widget (powered by `BulkOperationRun` + `BulkOperationProgress`) ## Technical Context **Language/Version**: PHP 8.4.15 **Primary Dependencies**: Laravel 12, Filament v4, Livewire v3 **Storage**: PostgreSQL (JSONB used for run payload + error context) **Testing**: Pest v4 (Laravel test runner) **Target Platform**: Containerized (Laravel Sail locally), deployed via Dokploy **Project Type**: Web application (Laravel + Filament admin panel) **Performance Goals**: UI action should enqueue quickly (<2s perceived) and never block on Graph calls **Constraints**: Tenant-isolated, idempotent start semantics, observable background execution, queue workers may be down **Scale/Scope**: Single-tenant interactive action; one run per tenant/selection at a time (locks/concurrency) ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* - Inventory-first: This feature refreshes Inventory (“last observed”) and does not touch snapshots/backups. - Read/write separation: No Graph writes. Only reads + local DB writes (run records + inventory items). - Graph contract path: Inventory sync already uses `GraphClientInterface`; this feature does not add new Graph endpoints. - Deterministic capabilities: Uses `PolicyTypeResolver` + `config('tenantpilot.supported_policy_types')` to derive default selection. - Tenant isolation: Uses `Tenant::current()` for all run creation and progress tracking. - Automation: Uses existing inventory locks/concurrency; runs are observable via `InventorySyncRun` and `BulkOperationRun`. - Data minimization: Inventory sync continues to store metadata + sanitized meta JSON; no secrets in notifications/logs. ## Project Structure ### Documentation (this feature) ```text specs/046-inventory-sync-button/ ├── plan.md ├── research.md ├── data-model.md ├── quickstart.md ├── contracts/ │ └── internal-actions.md └── tasks.md # Phase 2 output (/speckit.tasks) - not created here ``` ### Source Code (repository root) ```text app/ ├── Filament/ │ └── Pages/ │ └── InventoryLanding.php # add header action “Run Inventory Sync” ├── Jobs/ │ └── RunInventorySyncJob.php # new queued job ├── Models/ │ ├── InventorySyncRun.php # add user relationship (nullable) │ └── BulkOperationRun.php # reused by progress widget ├── Services/ │ ├── BulkOperationService.php # reused for progress + audit │ ├── Intune/ │ │ └── AuditLogger.php # reused for inventory audit trail │ └── Inventory/ │ └── InventorySyncService.php # add entrypoint to run sync attributed to user database/migrations/ └── xxxx_xx_xx_xxxxxx_add_user_id_to_inventory_sync_runs_table.php resources/views/ └── filament/pages/inventory-landing.blade.php # may need to show action if not already in header tests/Feature/ └── Inventory/InventorySyncButtonTest.php # new Pest feature test ``` **Structure Decision**: Web application, implemented in Filament page actions + Laravel queued job. ## Phase 0 Output (Research) Completed in `specs/046-inventory-sync-button/research.md`. Key decisions used in this plan: - Reuse existing bottom-right progress widget by creating a `BulkOperationRun` (`resource=inventory`, `action=sync`). - Authorize using existing tenant role capability: `User::canSyncTenant(Tenant::current())`. - Default selection = “full inventory” (all supported policy types, foundations + dependencies enabled). ## Phase 1 Output (Design) Completed in: - `specs/046-inventory-sync-button/data-model.md` - `specs/046-inventory-sync-button/contracts/internal-actions.md` ## Phase 2 Plan (Implementation Outline) ### 1) Data model: store initiator on run record - Add nullable `user_id` foreign key to `inventory_sync_runs`. - Add `InventorySyncRun::user()` relationship. - Backfill is not required (existing rows can remain `null`). ### 2) Job orchestration: queued execution + progress tracking - Add `RunInventorySyncJob` that: - Loads the tenant + user + `BulkOperationRun`. - Marks the `BulkOperationRun` as running (`BulkOperationService::start`). - Runs inventory sync via `InventorySyncService` in a way that sets `InventorySyncRun.user_id`. - Records bulk progress as a single-item operation: - success: `recordSuccess` + `complete` - failure/skip: `recordFailure`/`recordSkippedWithReason` + `complete` (or `fail` for hard failures) - Sends Filament database notifications to the initiating user on completion/failure. ### 3) UI: Inventory “Run Inventory Sync” action - Add a header action on `InventoryLanding`: - Visible/authorized only if `auth()->user()` is a `User` and `canSyncTenant(Tenant::current())`. - On click: - Compute default selection payload. - Pre-flight check: if a matching `InventorySyncRun` is currently `running` (same tenant + selection hash), show an informational notification and do not dispatch. - Create a `BulkOperationRun` via `BulkOperationService::createRun(...)` with `total_items=1`. - Send a “started” Filament DB notification with the “check bottom right progress bar” copy. - Dispatch `RunInventorySyncJob` with identifiers. ### 4) Audit trail - Use `AuditLogger` to emit at minimum: - `inventory.sync.dispatched` - terminal event keyed by outcome: `inventory.sync.completed|inventory.sync.failed|inventory.sync.skipped` ### 5) Tests (Pest) - New feature test covering: - authorized user can dispatch a sync from the Inventory page and it creates a `BulkOperationRun`. - `RunInventorySyncJob` sets `InventorySyncRun.user_id`. - unauthorized user cannot see/execute the action. - concurrency case: when a run is already `running`, a second dispatch is prevented.