# Implementation Plan: Alert Targets — Test Actions + Last Test Status **Branch**: `feat/100-alert-target-test-actions` | **Date**: 2026-02-18 | **Spec**: `specs/100-alert-target-test-actions/spec.md` **Input**: Feature specification from `specs/100-alert-target-test-actions/spec.md` **Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts. ## Summary Add a derived “Last test status” indicator to Alert Targets (Filament `AlertDestinationResource` view and edit pages), plus a capability-gated “Send test message” action that enqueues a test delivery (no outbound calls in the request thread). The status is derived solely from the latest `alert_deliveries` row with `event_type = 'alerts.test'` for the destination. ## Technical Context **Language/Version**: PHP 8.4.15 (Laravel 12) **Primary Dependencies**: Filament v5, Livewire v4, Laravel Queue, Laravel Notifications **Storage**: PostgreSQL (Sail locally) **Testing**: Pest v4 (via `vendor/bin/sail artisan test`) **Target Platform**: Laravel web app (Sail-first locally; Dokploy containers in staging/prod) **Project Type**: Web application (monolith) **Performance Goals**: DB-only page rendering; avoid N+1; derived last-test status computed with a single indexed query **Constraints**: No outbound network calls in request/response; no “last test” DB field; deterministic mapping (Never/Sent/Failed/Pending) **Scale/Scope**: Workspace-scoped admin UX; minimal UI surfaces (view + edit pages) ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* - Inventory-first: N/A (no inventory changes) - Read/write separation: test request is a small DB-only write; includes confirmation + audit + tests - Graph contract path: no Graph calls - Deterministic capabilities: uses existing `Capabilities::ALERTS_VIEW` / `Capabilities::ALERTS_MANAGE` registry - RBAC-UX: existing workspace policy semantics (non-member 404 via policy) + member-without-capability 403 on action execution - Workspace isolation: enforced via `WorkspaceContext` + `AlertDestinationPolicy` - Destructive confirmations: “Send test message” is not destructive but still requires confirmation per spec - Tenant isolation: deliveries viewer remains tenant-safe; tenantless test deliveries are treated as workspace-owned and are safe to reveal - Run observability: external sends happen via existing `alerts.deliver` operation run created by `DeliverAlertsJob` - Data minimization: test payload + errors are sanitized (no webhook URL / recipients) - Badge semantics: new badge domains/mappers added; no ad-hoc mappings - Filament action surface: edits are confined to edit header actions + read-only status section; action is capability-gated and audited ## Project Structure ### Documentation (this feature) ```text specs/100-alert-target-test-actions/ ├── plan.md ├── spec.md ├── research.md ├── data-model.md ├── quickstart.md ├── contracts/ └── checklists/ ``` ### Source Code (repository root) ```text app/ ├── Filament/ │ └── Resources/ │ ├── AlertDestinationResource.php │ ├── AlertDeliveryResource.php │ └── AlertDestinationResource/Pages/EditAlertDestination.php ├── Jobs/Alerts/ │ └── DeliverAlertsJob.php ├── Models/ │ └── AlertDelivery.php ├── Policies/ │ ├── AlertDestinationPolicy.php │ └── AlertDeliveryPolicy.php ├── Services/Alerts/ │ ├── AlertSender.php │ └── AlertDispatchService.php └── Support/ ├── Audit/AuditActionId.php └── Badges/ database/migrations/ tests/ ├── Feature/ └── Unit/ ``` **Structure Decision**: Laravel monolith; Filament resources under `app/Filament`, domain services/jobs under `app/Services` and `app/Jobs`, tests in `tests/`. ## Complexity Tracking > **Fill ONLY if Constitution Check has violations that must be justified** | Violation | Why Needed | Simpler Alternative Rejected Because | |-----------|------------|-------------------------------------| | [e.g., 4th project] | [current need] | [why 3 projects insufficient] | | [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] | ## Phase 0 — Outline & Research (completed) Artifacts: - `specs/100-alert-target-test-actions/research.md` Key outcomes: - Deep link uses Filament v5 `filters[...]` query string. - Timestamp mapping uses existing columns (`sent_at`, `updated_at`, `send_after`). - Test deliveries must be tenantless to avoid tenant-entitlement divergence. ## Phase 1 — Design & Contracts ### Data model changes The current `alert_deliveries` table enforces tenant and rule FKs as required. For test sends: - Make `tenant_id` nullable (test deliveries have no tenant context) - Make `alert_rule_id` nullable (test deliveries are not tied to a routing rule) Also add an index to support the derived “last test” lookup: - `(workspace_id, alert_destination_id, event_type, created_at)` ### Backend design 1. Add a small resolver to compute `LastTestStatus` from `alert_deliveries`: - Input: `workspace_id`, `alert_destination_id` - Query: latest delivery with `event_type = 'alerts.test'` - Output: `{status, timestamp, delivery_id?}` 2. Add a service for creating a test delivery: - Authorize using existing `AlertDestinationPolicy::update` (manage capability) - Enforce rate limit (60s) by checking latest test delivery `created_at` - Create delivery with: - `workspace_id` = current workspace - `tenant_id` = null - `alert_rule_id` = null - `alert_destination_id` = destination id - `event_type` = `alerts.test` - `status` = `queued` - `fingerprint_hash` = deterministic stable string (no secrets) (e.g. `test:{destination_id}`) - `payload` = minimal safe text 3. Queue execution: - Dispatch `DeliverAlertsJob($workspaceId)` so the test is processed without waiting for a scheduler. ### Filament UI design 1. Alert Targets (View page) - Provide a read-only view for view-only users. - Render the derived “Last test” status (badge + timestamp). - Add header actions: - **Send test message**: visible but disabled when user lacks `ALERTS_MANAGE`. - **View last delivery**: visible only if at least one test delivery exists. 2. Alert Targets (Edit page) - Add a read-only “Last test” status at the top of the edit form using a form component that can render as a badge. - Add header actions: - **Send test message** (mutating): `Action::make(...)->requiresConfirmation()->action(...)` - Visible for workspace members with `ALERTS_VIEW`. - Disabled via UI enforcement when user lacks `ALERTS_MANAGE`. - **View last delivery** (navigation): `Action::make(...)->url(...)` - Visible only if at least one test delivery exists. 3. Deliveries viewer (List page) - Add table filters: - `event_type` (SelectFilter) - `alert_destination_id` (SelectFilter) - Adjust tenant entitlement filter to include tenantless deliveries (`tenant_id IS NULL`). ### Authorization & semantics - Non-member: existing policies return `denyAsNotFound()`. - Member without `ALERTS_MANAGE`: action execution must result in 403; UI remains visible but disabled. ### Audit logging - Add a new `AuditActionId` value: `alert_destination.test_requested`. - Log when a test is requested with redacted metadata (no webhook URL / recipients). ### Badge semantics - Add badge domain(s) and tests: - Alert delivery status badge mapping (to replace the ad-hoc mapping in `AlertDeliveryResource`). - Alert destination last-test status badge mapping. ### Contracts - Deep link contract + event type contract live in `specs/100-alert-target-test-actions/contracts/`. ## Post-Design Constitution Re-check - RBAC-UX: enforced via policies + capability registry; test action server-authorized. - BADGE-001: new badge domains + tests planned; no ad-hoc mappings. - OPS/run observability: outbound delivery occurs only in queued job; `alerts.deliver` operation run remains the monitoring source. - DB-only rendering: derived status and links use indexed DB queries; no external calls. ## Phase 2 — Implementation planning (for tasks.md) Next steps (to be expanded into `tasks.md`): 1. DB migration: make `alert_deliveries.tenant_id` and `alert_rule_id` nullable + add supporting index. 2. Update `AlertDelivery` model/relationships/casts if needed for tenantless + ruleless deliveries. 3. Update `AlertDeliveryPolicy` + `AlertDeliveryResource` query to allow tenantless deliveries. 4. Add badge domains + mapping tests. 5. Add “Send test message” header action + “Last test” badge section to `EditAlertDestination`. 6. Add feature tests (Pest) for: - derived status mapping (Never/Sent/Failed/Pending) - rate limiting - RBAC (manage vs view) - deep link visibility