TenantAtlas/specs/091-backupschedule-retention-lifecycle/research.md
ahmido 1c098441aa feat(spec-091): BackupSchedule lifecycle + create-CTA placement rule (#109)
Implements Spec 091 “BackupSchedule Retention & Lifecycle (Archive/Restore/Force Delete)”.

- BackupSchedule lifecycle:
  - Archive (soft delete) with confirmation; restores via Restore action; Force delete with confirmation and strict gating.
  - Force delete blocked when historical runs exist.
  - Archived schedules never dispatch/execute (dispatcher + job guard).
  - Audit events emitted for archive/restore/force delete.
  - RBAC UX semantics preserved (non-member hidden/404; member w/o capability disabled + server-side 403).

- Filament UX contract update:
  - Create CTA placement rule across create-enabled list pages:
    - Empty list: only large centered empty-state Create CTA.
    - Non-empty list: only header Create action.
  - Tests added/updated to enforce the rule.

Verification:
- `vendor/bin/sail bin pint --dirty`
- Focused tests: BackupScheduling + RBAC enforcement + EmptyState CTAs + Create CTA placement

Notes:
- Filament v5 / Livewire v4 compliant.
- Manual quickstart verification in `specs/091-backupschedule-retention-lifecycle/quickstart.md` remains to be checked (T031).

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #109
2026-02-14 13:46:06 +00:00

55 lines
3.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Research — Spec 091 (BackupSchedule Retention & Lifecycle)
## Decisions
### Decision 1 — Use Eloquent SoftDeletes for “Archived” state
- **Chosen**: Add `SoftDeletes` to `App\Models\BackupSchedule` and add `deleted_at` to `backup_schedules`.
- **Rationale**: The repo already models reversible “archive” lifecycles using SoftDeletes (e.g., `BackupSet`) and Filaments `TrashedFilter`. SoftDeletes also makes “archived schedules never execute” the default behavior in scheduler queries.
- **Alternatives considered**:
- `archived_at` boolean/timestamp field → rejected (would require manual scoping in every query and more room for mistakes).
### Decision 2 — Archived schedules MUST never be dispatched nor executed
- **Chosen**:
- Scheduler dispatch path (`BackupScheduleDispatcher::dispatchDue`) relies on default SoftDeletes query scoping (archived schedules excluded).
- Job execution path (`RunBackupScheduleJob`) will add an explicit guard to skip if the schedule is archived at execution time.
- **Rationale**: Prevents race conditions where a schedule is archived after dispatch but before job execution.
- **Alternatives considered**:
- Only guard in dispatcher → rejected (not sufficient for race conditions).
### Decision 3 — Reuse central RBAC enforcement helpers for lifecycle actions
- **Chosen**: Wrap Filament actions using `App\Support\Rbac\UiEnforcement`.
- **Rationale**: `UiEnforcement` implements the constitutions RBAC-UX semantics (non-member hidden + server guard; member missing capability disabled + server guard) and validates capabilities against the canonical registry.
- **Alternatives considered**:
- Inline `->visible()`, `->disabled()`, `->authorize()` scattered in the resource → rejected (drift + violates “central helper” guidance).
### Decision 4 — Force delete capability and constraints
- **Chosen**:
- Force delete is available only when the schedule is archived.
- Force delete is gated by `Capabilities::TENANT_DELETE` (stricter than manage).
- Force delete is blocked if historical runs exist.
- **Rationale**: Matches the spec clarification: permanent deletion must be highly privileged and must not break historical traceability.
- **Alternatives considered**:
- Allow force delete with cascading deletion of historical runs → rejected (breaks auditability).
### Decision 5 — Define “historical runs exist” using existing operational records
- **Chosen**: Treat “historical runs exist” as: `BackupSchedule::operationRuns()->exists()`.
- **Rationale**: In this repo, the scheduler and run lifecycle already track schedule activity via `OperationRun` with `context->backup_schedule_id`.
- **Alternatives considered**:
- Use `backup_schedule_runs` table → not currently used by the scheduling runtime (it exists in migrations but the runtime path is `OperationRun`).
### Decision 6 — Audit logging via existing tenant audit logger
- **Chosen**: Use `App\Services\Intune\AuditLogger` with stable action identifiers:
- `backup_schedule.archived`
- `backup_schedule.restored`
- `backup_schedule.force_deleted`
- **Rationale**: The repo already centralizes audit logging and sanitizes metadata.
## Repo Evidence (Key Findings)
- Scheduling dispatch is centralized in `App\Services\BackupScheduling\BackupScheduleDispatcher` and streams schedules via `cursor()`.
- Existing tenant capabilities include:
- `Capabilities::TENANT_BACKUP_SCHEDULES_MANAGE`
- `Capabilities::TENANT_DELETE`
- Filament already uses SoftDeletes patterns with `TrashedFilter` and lifecycle actions in `BackupSetResource`.