# 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

<!--
  ACTION REQUIRED: Replace the content in this section with the technical details
  for the project. The structure here is presented in advisory capacity to guide
  the iteration process.
-->

**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)
<!--
  ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
  for this feature. Delete unused options and expand the chosen structure with
  real paths (e.g., apps/admin, packages/something). The delivered plan must
  not include Option labels.
-->

```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
