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

3.5 KiB
Raw Permalink Blame History

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.