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
55 lines
3.5 KiB
Markdown
55 lines
3.5 KiB
Markdown
# 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 Filament’s `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 constitution’s 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`.
|
||
|