From 270181d509453f2e5a1c78121195ea14df34c7d7 Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Thu, 19 Feb 2026 00:11:21 +0100 Subject: [PATCH] spec: add feature 100 alert target test actions --- .github/agents/copilot-instructions.md | 4 +- .specify/memory/constitution.md | 1 + .../checklists/requirements.md | 34 +++ .../contracts/event-types.md | 17 ++ .../contracts/filament-deep-link-filters.md | 32 +++ .../data-model.md | 79 +++++++ specs/100-alert-target-test-actions/plan.md | 221 ++++++++++++++++++ .../quickstart.md | 20 ++ .../100-alert-target-test-actions/research.md | 68 ++++++ specs/100-alert-target-test-actions/spec.md | 156 +++++++++++++ 10 files changed, 630 insertions(+), 2 deletions(-) create mode 100644 specs/100-alert-target-test-actions/checklists/requirements.md create mode 100644 specs/100-alert-target-test-actions/contracts/event-types.md create mode 100644 specs/100-alert-target-test-actions/contracts/filament-deep-link-filters.md create mode 100644 specs/100-alert-target-test-actions/data-model.md create mode 100644 specs/100-alert-target-test-actions/plan.md create mode 100644 specs/100-alert-target-test-actions/quickstart.md create mode 100644 specs/100-alert-target-test-actions/research.md create mode 100644 specs/100-alert-target-test-actions/spec.md diff --git a/.github/agents/copilot-instructions.md b/.github/agents/copilot-instructions.md index bcf1b99..c83cbd3 100644 --- a/.github/agents/copilot-instructions.md +++ b/.github/agents/copilot-instructions.md @@ -29,6 +29,7 @@ ## Active Technologies - PHP 8.4 (Laravel 12) + Filament v5, Livewire v4, Laravel Sail, Tailwind CSS v4 (085-tenant-operate-hub) - PostgreSQL (Sail), SQLite in tests (087-legacy-runs-removal) - PHP 8.4.x + Laravel 12, Filament v5, Livewire v4, Microsoft Graph integration via `GraphClientInterface` (095-graph-contracts-registry-completeness) +- PHP 8.4.15 (Laravel 12) + Filament v5, Livewire v4, Laravel Queue, Laravel Notifications (100-alert-target-test-actions) - PHP 8.4.15 (feat/005-bulk-operations) @@ -48,8 +49,7 @@ ## Code Style PHP 8.4.15: Follow standard conventions ## Recent Changes +- 100-alert-target-test-actions: Added PHP 8.4.15 (Laravel 12) + Filament v5, Livewire v4, Laravel Queue, Laravel Notifications - 095-graph-contracts-registry-completeness: Added PHP 8.4.x + Laravel 12, Filament v5, Livewire v4, Microsoft Graph integration via `GraphClientInterface` -- 090-action-surface-contract-compliance: Added PHP 8.4.15 -- 087-legacy-runs-removal: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4 diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index abd426b..e17ec6f 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -70,6 +70,7 @@ ### Tenant Isolation is Non-negotiable - Tenant-owned tables MUST include workspace_id and tenant_id as NOT NULL. - Workspace-owned tables MUST include workspace_id and MUST NOT include tenant_id. - Exception: OperationRun MAY have tenant_id nullable to support canonical workspace-context monitoring views; however, revealing any tenant-bound runs still MUST enforce entitlement checks to the referenced tenant scope. +- Exception: AlertDelivery MAY have tenant_id nullable for workspace-scoped, non-tenant-operational artifacts (e.g., `event_type=alerts.test`). Tenant-bound delivery records still MUST enforce tenant entitlement checks, and tenantless delivery rows MUST NOT contain tenant-specific data. ### RBAC & UI Enforcement Standards (RBAC-UX) diff --git a/specs/100-alert-target-test-actions/checklists/requirements.md b/specs/100-alert-target-test-actions/checklists/requirements.md new file mode 100644 index 0000000..8925dee --- /dev/null +++ b/specs/100-alert-target-test-actions/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: Alert Targets Test Actions + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-02-18 +**Feature**: [specs/100-alert-target-test-actions/spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Spec is ready for `/speckit.plan`. diff --git a/specs/100-alert-target-test-actions/contracts/event-types.md b/specs/100-alert-target-test-actions/contracts/event-types.md new file mode 100644 index 0000000..cac15c5 --- /dev/null +++ b/specs/100-alert-target-test-actions/contracts/event-types.md @@ -0,0 +1,17 @@ +# Contract — Alert Event Types (workspace alerts) + +## Overview + +This add-on introduces a single new event type used for *test deliveries*. + +## Event Type + +- `alerts.test` + - Purpose: user-triggered “Send test message” action on an alert target. + - Storage: `alert_deliveries.event_type` + - Delivery: sent via the existing alert delivery pipeline (`DeliverAlertsJob` + `AlertSender`). + +## Notes + +- This is an internal event type string; it is not a Microsoft Graph contract. +- No secrets are stored in payload/error text. diff --git a/specs/100-alert-target-test-actions/contracts/filament-deep-link-filters.md b/specs/100-alert-target-test-actions/contracts/filament-deep-link-filters.md new file mode 100644 index 0000000..fe00316 --- /dev/null +++ b/specs/100-alert-target-test-actions/contracts/filament-deep-link-filters.md @@ -0,0 +1,32 @@ +# Contract — Filament deep link into Alert Deliveries + +## Goal + +Allow a deterministic deep link from an Alert Target (destination) to the Deliveries viewer filtered to the most relevant rows. + +## Target + +- Resource: `AlertDeliveryResource` +- Page: list/index + +## Query-string filter contract (Filament v5) + +Use Filament’s `filters[...]` query-string state to pre-apply table filters: + +- Filter by event type: + - `filters[event_type][value]=alerts.test` +- Filter by destination: + - `filters[alert_destination_id][value]={DESTINATION_ID}` + +Combined example: + +`/admin/alert-deliveries?filters[event_type][value]=alerts.test&filters[alert_destination_id][value]=123` + +## Required table filters + +The Deliveries list must define these filters with matching keys: + +- `SelectFilter::make('event_type')` +- `SelectFilter::make('alert_destination_id')` + +(These are additive to the existing `status` filter.) diff --git a/specs/100-alert-target-test-actions/data-model.md b/specs/100-alert-target-test-actions/data-model.md new file mode 100644 index 0000000..f6f537c --- /dev/null +++ b/specs/100-alert-target-test-actions/data-model.md @@ -0,0 +1,79 @@ +# Phase 1 — Data Model (099.1 Add-on) + +## Entities + +### AlertDestination (existing) + +- **Table**: `alert_destinations` +- **Ownership**: workspace-owned +- **Key fields (relevant here)**: + - `id` + - `workspace_id` + - `type` (e.g. Teams webhook, Email) + - `config` (Teams webhook URL or list of recipients) + - `is_enabled` + +### AlertDelivery (existing, extended for test sends) + +- **Table**: `alert_deliveries` +- **Ownership**: + - v1 deliveries for real alerts remain tenant-associated + - **test deliveries for this add-on are tenantless** (workspace-only) + +- **Key fields (relevant here)**: + - `id` + - `workspace_id` (required) + - `tenant_id` (**nullable for test deliveries**) + - `alert_rule_id` (**nullable for test deliveries**) + - `alert_destination_id` (required) + - `event_type` (string, includes `alerts.test`) + - `status` (`queued|deferred|sent|failed|suppressed|canceled`) + - `send_after` (nullable; used for deferral/backoff) + - `sent_at` (nullable) + - `attempt_count` + - `last_error_code`, `last_error_message` (sanitized) + - `payload` (array/json) + - timestamps: `created_at`, `updated_at` + +## Relationships + +- `AlertDelivery` → `AlertDestination` (belongsTo via `alert_destination_id`) +- `AlertDelivery` → `AlertRule` (belongsTo via `alert_rule_id`, nullable) +- `AlertDelivery` → `Tenant` (belongsTo via `tenant_id`, nullable) + +## New derived concepts (no storage) + +### LastTestStatus (derived) + +Derived from the most recent `alert_deliveries` record where: +- `alert_destination_id = {destination}` +- `event_type = 'alerts.test'` + +Mapping: +- no record → `Never` +- `status in (queued, deferred)` → `Pending` +- `status = sent` → `Sent` +- `status = failed` → `Failed` + +Associated timestamp (derived): +- Sent → `sent_at` +- Failed → `updated_at` +- Pending → `send_after` (fallback `created_at`) + +## Validation / invariants + +- Creating a test delivery requires: + - `alert_destination_id` exists and belongs to current workspace + - destination is enabled (if disabled, refuse test request) + - rate limit: no prior test delivery for this destination in last 60 seconds +- Test delivery record must not persist secrets in payload or error message. + +## Migration notes + +To support tenantless test deliveries: +- Make `alert_deliveries.tenant_id` nullable and adjust the FK behavior. +- Make `alert_deliveries.alert_rule_id` nullable and adjust the FK behavior. +- Add or adjust indexes for efficient status lookup per destination + event type: + - `(workspace_id, alert_destination_id, event_type, created_at)` + +(Exact migration steps and DB constraint changes are specified in the implementation plan.) diff --git a/specs/100-alert-target-test-actions/plan.md b/specs/100-alert-target-test-actions/plan.md new file mode 100644 index 0000000..3ef7b63 --- /dev/null +++ b/specs/100-alert-target-test-actions/plan.md @@ -0,0 +1,221 @@ +# 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 diff --git a/specs/100-alert-target-test-actions/quickstart.md b/specs/100-alert-target-test-actions/quickstart.md new file mode 100644 index 0000000..6b8b18d --- /dev/null +++ b/specs/100-alert-target-test-actions/quickstart.md @@ -0,0 +1,20 @@ +# Quickstart (099.1 Add-on) + +## Prereqs + +- Laravel Sail is used for local dev. + +## Run locally + +- Start containers: `vendor/bin/sail up -d` +- Run migrations: `vendor/bin/sail artisan migrate` + +## Run tests (focused) + +- `vendor/bin/sail artisan test --compact --filter=AlertTargetTest` + +(Replace filter/name with the final Pest test names once implemented.) + +## Format + +- `vendor/bin/sail bin pint --dirty` diff --git a/specs/100-alert-target-test-actions/research.md b/specs/100-alert-target-test-actions/research.md new file mode 100644 index 0000000..5890607 --- /dev/null +++ b/specs/100-alert-target-test-actions/research.md @@ -0,0 +1,68 @@ +# Phase 0 — Research (099.1 Add-on) + +This research resolves the open technical questions from the feature spec and anchors decisions to existing code in this repository. + +## Decision 1 — Where “Alert Target View/Edit” lives in code + +- **Decision**: Implement “Last test status” + “Send test message” on the Alert Target **Edit** page only. +- **Rationale**: The current Filament resource for alert targets is `AlertDestinationResource` and only defines `index/create/edit` pages (no View page). +- **Alternatives considered**: + - Add a new View page for `AlertDestinationResource` (rejected: expands UX surface and is not required for this add-on). + +## Decision 2 — Canonical event type string + +- **Decision**: Use a single canonical event type string for test sends: `alerts.test`. +- **Rationale**: The add-on spec requires deriving status from `alert_deliveries.event_type`. Keeping this string stable allows deterministic filtering and deep links. +- **Alternatives considered**: + - Use `test` or `destination_test` (rejected: deviates from spec and makes intent less explicit). + +## Decision 3 — Timestamp mapping vs actual schema + +- **Decision**: Map timestamps using existing columns: + - **Sent** → `sent_at` + - **Failed** → `updated_at` (no `failed_at` column exists) + - **Pending** → `send_after` if set, else `created_at` +- **Rationale**: The `alert_deliveries` schema only has `send_after`, `sent_at`, and `updated_at`. The spec’s `deliver_at` / `failed_at` naming is treated as conceptual. +- **Alternatives considered**: + - Add `failed_at` / `deliver_at` columns (rejected: violates “no new DB fields”). + +## Decision 4 — How to represent test deliveries given current tenant enforcement + +- **Decision**: Store test deliveries in `alert_deliveries` as **workspace-scoped, tenantless** records: + - `workspace_id` set + - `tenant_id` nullable + - `alert_rule_id` nullable + - `alert_destination_id` set +- **Rationale**: + - Alert targets (`AlertDestination`) are workspace-owned and accessible without a selected tenant. + - Current enforcement (`DerivesWorkspaceIdFromTenant`) requires a tenant and would make test deliveries invisible/uneven across users due to tenant entitlements. + - Deliver job execution (`DeliverAlertsJob`) does not require a tenant; it only needs the destination + payload. +- **Alternatives considered**: + - Pick an arbitrary tenant ID for test deliveries (rejected: would be invisible to some operators and breaks “single truth” per target). + - Create a synthetic “workspace tenant” record (rejected: adds data-model complexity). + +## Decision 5 — Deep link mechanics for the Deliveries viewer + +- **Decision**: Deep link into `AlertDeliveryResource` list view using Filament v5 filter query-string binding: + - `?filters[event_type][value]=alerts.test&filters[alert_destination_id][value]={DESTINATION_ID}` +- **Rationale**: Filament v5 `ListRecords` binds `tableFilters` to the URL under the `filters` key; `SelectFilter` uses `value`. +- **Alternatives considered**: + - Custom dedicated “Test deliveries” page (rejected: new surface beyond spec). + +## Decision 6 — Badge semantics centralization (BADGE-001) + +- **Decision**: Introduce centralized badge domains/mappers for: + - `AlertDeliveryStatus` (queued/deferred/sent/failed/suppressed/canceled) + - `AlertDestinationLastTestStatus` (never/pending/sent/failed) +- **Rationale**: Current `AlertDeliveryResource` implements ad-hoc status label/color helpers. This add-on must comply with BADGE-001 and should not add more local mappings. +- **Alternatives considered**: + - Keep ad-hoc mappings in the new code (rejected: violates BADGE-001). + +## Decision 7 — OperationRun usage + +- **Decision**: Do **not** create a new per-test `OperationRun`. +- **Rationale**: + - The user-triggered action is DB-only (authorization + rate limit + create delivery + audit), typically <2s. + - The external work runs via the existing `alerts.deliver` operation run (created by `DeliverAlertsJob`). +- **Alternatives considered**: + - Create a dedicated operation run type (rejected: higher complexity for small UX add-on). diff --git a/specs/100-alert-target-test-actions/spec.md b/specs/100-alert-target-test-actions/spec.md new file mode 100644 index 0000000..f568257 --- /dev/null +++ b/specs/100-alert-target-test-actions/spec.md @@ -0,0 +1,156 @@ +# Feature Specification: Alert Targets Test Actions + +**Feature Branch**: `feat/100-alert-target-test-actions` +**Created**: 2026-02-18 +**Status**: Draft +**Input**: User description: "099.1 — Alert Targets: Send Test Message + Last Test Status (Teams + Email)" + +## Spec Scope Fields *(mandatory)* + +- **Scope**: workspace +- **Primary Routes**: Alert Target View, Alert Target Edit, Deliveries viewer (filtered deep links) +- **Data Ownership**: + - Workspace-owned: alert targets + - Tenant-owned: alert delivery history (non-test) + - Workspace-scoped: test deliveries (`event_type=alerts.test`) may be tenantless (`tenant_id` nullable) +- **RBAC**: + - Workspace membership is required to access Alert Targets and Deliveries. + - Users with manage capability can request a test send. + - Users with view-only capability can see test status but cannot request a test send. + +For canonical-view specs, the spec MUST define: + +- **Default filter behavior when tenant-context is active**: Not applicable (workspace scope). +- **Explicit entitlement checks preventing cross-tenant leakage**: Targets and deliveries are only visible within the current workspace; non-members must not receive any existence hints. + +## Clarifications + +### Session 2026-02-18 + +- Q: Which timestamps should the “Last test … at ” subtext use? → A: Sent → `sent_at`; Failed → `updated_at`; Pending → `send_after`. +- Q: What should the test-send rate limit be per target? → A: 60 seconds per target. +- Q: What should “View last delivery” open? → B: Deliveries list viewer filtered to `alert_destination_id` + `event_type=alerts.test`. +- Q: When should “View last delivery” be shown on the Alert Target pages? → B: Show only if at least one test delivery exists. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Send a test message for a target (Priority: P1) + +As an admin, I want to send a test alert to a configured Alert Target so I can verify the integration works before relying on it in production. + +**Why this priority**: This is the fastest way to detect misconfiguration (wrong destination, blocked network path, invalid credentials) and reduce support/incident time. + +**Independent Test**: Can be fully tested by requesting a test send and confirming a new test delivery record exists and can be inspected. + +**Acceptance Scenarios**: + +1. **Given** I am a workspace member with manage permission, **When** I confirm “Send test message” on an Alert Target, **Then** the system creates exactly one new test delivery record for that target and indicates the request was queued. +2. **Given** I am a workspace member without manage permission, **When** I attempt to execute “Send test message”, **Then** the action is blocked and no delivery record is created. +3. **Given** I have requested a test very recently for the same target, **When** I attempt another test immediately, **Then** the system refuses the request and does not create a new delivery record. + +--- + +### User Story 2 - See the last test status at a glance (Priority: P2) + +As an admin, I want to see whether the last test send for an Alert Target succeeded, failed, was never executed, or is still pending so I can assess health without digging through deliveries. + +**Why this priority**: Health visibility reduces troubleshooting time and prevents silent failures. + +**Independent Test**: Can be fully tested by viewing a target with (a) no test deliveries, (b) a successful test delivery, (c) a failed test delivery and verifying the badge and timestamp. + +**Acceptance Scenarios**: + +1. **Given** a target has no test delivery records, **When** I open the target View or Edit page, **Then** I see a badge “Last test: Never”. +2. **Given** the most recent test delivery record is successful, **When** I open the target View or Edit page, **Then** I see a badge “Last test: Sent” and an associated timestamp. +3. **Given** the most recent test delivery record is failed, **When** I open the target View or Edit page, **Then** I see a badge “Last test: Failed” and an associated timestamp. +4. **Given** the most recent test delivery record is queued or deferred, **When** I open the target View or Edit page, **Then** I see a badge “Last test: Pending”. + +--- + +### User Story 3 - Jump from target to the relevant delivery details (Priority: P3) + +As an admin, I want a quick link from the Alert Target to the most recent test delivery details so I can troubleshoot outcomes efficiently. + +**Why this priority**: It reduces clicks and prevents mistakes when searching through delivery history. + +**Independent Test**: Can be tested by clicking a “View last delivery” link and verifying the deliveries view is pre-filtered for this target and the test event type. + +**Acceptance Scenarios**: + +1. **Given** a target has at least one test delivery record, **When** I click “View last delivery” from the target page, **Then** I am taken to the deliveries list viewer scoped to the same workspace and filtered to that target and the test event type. + +### Edge Cases + +- Target exists but has never been tested. +- Target has multiple test deliveries; only the most recent one is used for status. +- The most recent test delivery is queued/deferred; status must show pending without implying success. +- Users without workspace membership attempt to access targets or deliveries (must be deny-as-not-found). +- Failure details must not expose secrets (destination URLs, recipients). +- Rapid repeated test requests (anti-spam / rate limiting) must not create additional delivery records. + +## Requirements *(mandatory)* + +**Constitution alignment (required):** This feature introduces a user-triggered action that creates a delivery record and schedules work to be executed asynchronously. +The spec requires: confirmation, RBAC enforcement, anti-spam rate limiting, audit logging, tenant/workspace isolation, and tests. + +**Constitution alignment (RBAC-UX):** + +- Authorization planes involved: Admin UI (workspace-scoped). +- 404 vs 403 semantics: + - Non-member / not entitled to workspace scope → 404 (deny-as-not-found) + - Workspace member but missing manage capability → 403 for executing the test action +- All mutations (test request) require server-side authorization. + +**Constitution alignment (BADGE-001):** “Last test: Sent/Failed/Pending/Never” MUST use centralized badge semantics (no ad-hoc mappings). + +**Constitution alignment (Filament Action Surfaces):** This feature modifies Filament pages (Alert Target view/edit) and therefore includes a UI Action Matrix. + +### Functional Requirements + +- **FR-001 (Derived status, no new fields)**: The system MUST display a “Last test status” indicator on Alert Target View and Edit pages derived solely from existing alert delivery records. +- **FR-002 (Deterministic selection)**: The “Last test status” MUST be derived from the single most recent delivery record for the given target where the delivery event type is “alerts.test”, ordered by `created_at` (desc) then `id` (desc). +- **FR-003 (Status mapping)**: The system MUST map the most recent test delivery record to one of: Never (no record), Sent, Failed, or Pending. +- **FR-004 (Timestamp semantics)**: The UI MUST display a timestamp that reflects when the outcome occurred: Sent → `sent_at`; Failed → `updated_at`; Pending → `send_after`. +- **FR-005 (DB-only UI)**: Requesting a test send MUST not perform synchronous external delivery attempts in the user’s request/response flow. +- **FR-006 (Confirmation)**: The “Send test message” action MUST require explicit confirmation, explaining that it will contact the configured destination. +- **FR-007 (Anti-spam rate limit)**: The system MUST prevent repeated test requests for the same target within 60 seconds. +- **FR-008 (RBAC)**: Only workspace members with manage permission can request a test send; view-only users can see the action but cannot execute it. +- **FR-009 (Deny-as-not-found)**: Users without workspace membership MUST receive deny-as-not-found behavior for targets and deliveries. +- **FR-010 (Auditability)**: The system MUST record an audit event when a test is requested, without including destination secrets. +- **FR-011 (Deep link)**: The system SHOULD provide a “View last delivery” link from the target to the Deliveries list viewer filtered to that target and `event_type=alerts.test`. +- **FR-012 (Deep link visibility)**: The system SHOULD show “View last delivery” only when at least one test delivery exists for the target. + +### Assumptions + +- Alerts v1 already provides Alert Targets, Alert Deliveries, and a Deliveries viewer. +- A test send is represented as a delivery record with event type “alerts.test”. + +### Non-Goals + +- No new database fields for storing “last test status”. +- No bulk “test all targets” feature. +- No destination setup wizard. +- No per-row list view badge (avoids performance/N+1 concerns in v1). + +## UI Action Matrix *(mandatory when Filament is changed)* + +| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions | +|---|---|---|---|---|---|---|---|---|---|---| +| Alert Target (View) | Alert Target view page | Send test message (confirm, manage-only), View last delivery (navigate) | Not changed in this feature | Not changed in this feature | None | None | Same as Header Actions | Not applicable | Yes | View-only users see disabled “Send test message”. | +| Alert Target (Edit) | Alert Target edit page | Send test message (confirm, manage-only), View last delivery (navigate) | Not changed in this feature | Not changed in this feature | None | None | Same as Header Actions | Existing save/cancel | Yes | “Last test status” appears above the form. | +| Deliveries viewer | Deliveries list/details | None (existing) | Filtered via deep link | Existing row actions | Existing | Existing | Existing | Not applicable | Existing | Must remain workspace-scoped. | + +### Key Entities *(include if feature involves data)* + +- **Alert Target**: A destination configuration for alerts (e.g., Teams webhook or email destination), scoped to a workspace. +- **Alert Delivery**: A delivery attempt record that captures event type (including “alerts.test”), status, timestamps, and safe diagnostic details. +- **Audit Event**: A workspace-scoped audit entry representing a user-triggered test request. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: An admin can determine “last test status” for a target within 5 seconds from the target page. +- **SC-002**: An admin can request a test send in under 30 seconds (including confirmation) without leaving the target page. +- **SC-003**: A test request always results in either (a) a new test delivery record being created, or (b) a clear refusal due to rate limiting or missing permissions. +- **SC-004**: Troubleshooting time for “alerts not delivered” issues is reduced because the last test outcome and a direct link to details are immediately available.