TenantAtlas/specs/100-alert-target-test-actions/plan.md
ahmido d49d33ac27 feat(alerts): test message + last test status + deep links (#122)
Implements feature 100 (Alert Targets):

- US1: “Send test message” action (RBAC + confirmation + rate limit + audit + async job)
- US2: Derived “Last test” status badge (Never/Sent/Failed/Pending) on view + edit surfaces
- US3: “View last delivery” deep link + deliveries viewer filters (event_type, destination) incl. tenantless test deliveries

Tests:
- Full suite green (1348 passed, 7 skipped)
- Added focused feature tests for send test, last test resolver/badges, and deep-link filters

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #122
2026-02-18 23:12:38 +00:00

222 lines
9.3 KiB
Markdown

# 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