TenantAtlas/specs/032-backup-scheduling-mvp/spec.md
ahmido 4d3fcd28a9 feat/032-backup-scheduling-mvp (#34)
What
Implements tenant-scoped backup scheduling end-to-end: schedules CRUD, minute-based dispatch, queued execution, run history, manual “Run now/Retry”, retention (keep last N), and auditability.

Key changes

Filament UI: Backup Schedules resource with tenant scoping + SEC-002 role gating.
Scheduler + queue: tenantpilot:schedules:dispatch command wired in scheduler (runs every minute), creates idempotent BackupScheduleRun records and dispatches jobs.
Execution: RunBackupScheduleJob syncs policies, creates immutable backup sets, updates run status, writes audit logs, applies retry/backoff mapping, and triggers retention.
Run history: Relation manager + “View” modal rendering run details.
UX polish: row actions grouped; bulk actions grouped (run now / retry / delete). Bulk dispatch writes DB notifications (shows in notifications panel).
Validation: policy type hard-validation on save; unknown policy types handled safely at runtime (skipped/partial).
Tests: comprehensive Pest coverage for CRUD/scoping/validation, idempotency, job outcomes, error mapping, retention, view modal, run-now/retry notifications, bulk delete (incl. operator forbidden).
Files / Areas

Filament: BackupScheduleResource.php and app/Filament/Resources/BackupScheduleResource/*
Scheduling/Jobs: app/Console/Commands/TenantpilotDispatchBackupSchedules.php, app/Jobs/RunBackupScheduleJob.php, app/Jobs/ApplyBackupScheduleRetentionJob.php, console.php
Models/Migrations: app/Models/BackupSchedule.php, app/Models/BackupScheduleRun.php, database/migrations/backup_schedules, backup_schedule_runs
Notifications: BackupScheduleRunDispatchedNotification.php
Specs: specs/032-backup-scheduling-mvp/* (tasks/checklist/quickstart updates)
How to test (Sail)

Run tests: ./vendor/bin/sail artisan test tests/Feature/BackupScheduling
Run formatter: ./vendor/bin/sail php ./vendor/bin/pint --dirty
Apply migrations: ./vendor/bin/sail artisan migrate
Manual dispatch: ./vendor/bin/sail artisan tenantpilot:schedules:dispatch
Notes

Uses DB notifications for queued UI actions to ensure they appear in the notifications panel even under queue fakes in tests.
Checklist gate for 032 is PASS; tasks updated accordingly.

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #34
2026-01-05 04:22:13 +00:00

131 lines
7.0 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.

# Feature Specification: Backup Scheduling MVP (032)
**Feature**: Automatisierte Backups per Zeitplan (pro Tenant)
**Created**: 2026-01-05
**Status**: Ready for implementation (MVP)
**Risk**: Medium (Backup-only, no restore scheduling)
**Dependencies**: Tenant Portfolio + Tenant Context Switch ✅
## Context
TenantPilot unterstützt manuelle Backups. Kunden/MSPs benötigen regelmäßige, zuverlässige Backups pro Tenant (z. B. nightly), inkl. nachvollziehbarer Runs, Fehlercodes und Retention.
## Goals
- Pro Tenant können 1..n Backup Schedules angelegt werden.
- Schedules laufen automatisch via Queue/Worker.
- Jeder Lauf wird als Run auditierbar gespeichert (Status, Counts, Fehler).
- Retention löscht alte Backups nach Policy.
- Filament UI: Schedules verwalten, Run-History ansehen, “Run now”, “Retry”.
## Clarifications
### Session 2026-01-05
- Q: Wie sollen wir mit `policy_types` umgehen, die nicht in `config('tenantpilot.supported_policy_types')` enthalten sind?
→ A: Beim Speichern hart validieren und ablehnen; zur Laufzeit defensiv re-checken (Legacy/DB), unknown types skippen und Run als `partial` markieren mit `error_code=UNKNOWN_POLICY_TYPE` und Liste betroffener Types.
- Q: Wenn zur Laufzeit alle `policy_types` unbekannt sind (0 valid types nach Skip) welcher Status?
→ A: `skipped` (fail-safe).
## Non-Goals (MVP)
- Kein Kalender-UI als Pflicht (kann später ergänzt werden).
- Kein Cross-Tenant Bulk Scheduling (MSP-Templates später).
- Kein “drift-triggered scheduling” (kommt nach Drift-MVP).
- Kein Restore via Scheduling (nur Backup).
## Definitions
- **Schedule**: Wiederkehrender Plan (daily/weekly, timezone).
- **Run**: Konkrete Ausführung eines Schedules (scheduled_for + status).
- **BackupSet**: Ergebniscontainer eines Runs.
**MVP Semantik**: **1 Run = 1 neues BackupSet** (kein Rolling-Reuse im MVP).
## Requirements
### Functional Requirements
- **FR-001**: Schedules sind tenant-scoped via `tenant_id` (FK auf `tenants.id`).
- **FR-002**: Dispatcher erkennt “due” schedules und erstellt genau einen Run pro Zeit-Slot (idempotent).
- **FR-003**: Run nutzt bestehende Services:
- Sync Policies (nur selektierte policy types)
- Create BackupSet aus lokalen Policy-IDs (inkl. Foundations optional)
- **FR-003a**: `policy_types` sind ausschließlich Keys aus `config('tenantpilot.supported_policy_types')`.
- **FR-003b**: UI/Server-side Validation verhindert das Speichern unbekannter `policy_types`.
- **FR-003c**: Laufzeit-Validierung (defensiv): Unbekannte `policy_types` werden geskippt; wenn mindestens ein gültiger Type verarbeitet wurde, wird der Run als `partial` markiert und `error_code=UNKNOWN_POLICY_TYPE` gesetzt (inkl. Liste der betroffenen Types in `summary`).
- **FR-003d**: Wenn zur Laufzeit nach dem Skip **0 gültige Types** verbleiben, wird **kein BackupSet** erzeugt und der Run als `skipped` markiert (mit `error_code=UNKNOWN_POLICY_TYPE` und Liste der betroffenen Types in `summary`).
- **FR-004**: Run schreibt `backup_schedule_runs` mit Status + Summary + Error-Codes.
- **FR-005**: “Run now” erzeugt sofort einen Run (scheduled_for=now) und dispatcht Job.
- **FR-006**: “Retry” erzeugt einen neuen Run für denselben Schedule.
- **FR-007**: Retention hält nur die letzten N BackupSets pro Schedule (soft delete BackupSets).
- **FR-008**: Concurrency: Pro Schedule darf nur ein Run gleichzeitig laufen. Wenn bereits ein Run läuft, wird ein neuer Run nicht parallel gestartet und stattdessen als `skipped` markiert (mit Fehlercode).
### UX Requirements (Filament)
- **UX-001**: Schedule-Liste zeigt Enabled, Frequency, Time+Timezone, Policy Types Summary, Retention, Last Run, Next Run.
- **UX-002**: Run-History pro Schedule zeigt scheduled_for, status, duration, counts, error_code/message, Link zum BackupSet.
- **UX-003**: “Run now” und “Retry” sind nur mit passenden Rechten verfügbar.
### Security / Authorization
- **SEC-001**: Tenant Isolation: User sieht/managt nur Schedules des aktuellen Tenants.
- **SEC-002 (MVP)**: Authorization erfolgt über TenantRole (wie Tenant Portfolio):
- `readonly`: Schedules ansehen + Runs ansehen
- `operator`: zusätzlich “Run now” / “Retry”
- `manager` / `owner`: zusätzlich Schedules verwalten (CRUD)
- **SEC-003**: Dispatcher, Run-Execution und Retention schreiben tenant-scoped Audit Logs (keine Secrets/Tokens), inkl. Run-Start/Run-Ende und Retention-Ergebnis (z. B. Anzahl gelöschter BackupSets).
### Reliability / Non-Functional Requirements
- **NFR-001**: Idempotency durch Unique Slot-Constraint (`backup_schedule_id` + `scheduled_for`).
- **NFR-002**: Klare Fehlercodes (z. B. TOKEN_EXPIRED, PERMISSION_MISSING, GRAPH_THROTTLE, UNKNOWN).
- **NFR-003**: Retries: Throttling (z. B. 429/503) → Backoff; 401/403 → kein Retry; Unknown → begrenzte Retries und danach failed.
- **NFR-004**: Missed runs policy (MVP): **No catch-up** — wenn offline, wird nicht nachgeholt, nur nächster Slot.
### Scheduling Semantics
- `scheduled_for` ist **minute-basiert** (Slot), in UTC gespeichert. Due-Berechnung erfolgt in der Schedule-Timezone.
- DST (MVP): Bei ungültiger lokaler Zeit wird der Slot übersprungen (Run `skipped`). Bei ambiger lokaler Zeit wird die erste Occurrence verwendet.
## Data Model
### backup_schedules
- `id` bigint
- `tenant_id` FK tenants.id
- `name` string
- `is_enabled` bool default true
- `timezone` string default 'UTC'
- `frequency` string enum: daily|weekly
- `time_of_day` time
- `days_of_week` json nullable (array<int>, weekly only; 1=Mon..7=Sun)
- `policy_types` jsonb (array<string>)
- `include_foundations` bool default true
- `retention_keep_last` int default 30
- `last_run_at` datetime nullable
- `last_run_status` string nullable
- `next_run_at` datetime nullable
- timestamps
Indexes:
- (tenant_id, is_enabled)
- (next_run_at) optional
### backup_schedule_runs
- `id` bigint
- `backup_schedule_id` FK
- `tenant_id` FK (denormalisiert)
- `scheduled_for` datetime
- `started_at` datetime nullable
- `finished_at` datetime nullable
- `status` string enum: running|success|partial|failed|canceled|skipped
- `summary` jsonb (policies_total, policies_backed_up, errors_count, type_breakdown, warnings)
- `error_code` string nullable
- `error_message` text nullable
- `backup_set_id` FK nullable
- timestamps
Indexes:
- (backup_schedule_id, scheduled_for)
- (tenant_id, created_at)
- **Unique**: (backup_schedule_id, scheduled_for)
## Acceptance Criteria
- User kann pro Tenant einen Schedule anlegen (daily/weekly, time, timezone, policy types, retention).
- Dispatcher erstellt Runs zur geplanten Zeit (Queue Worker vorausgesetzt).
- UI zeigt Last Run + Next Run + Run-History.
- Run now startet sofort.
- Fehlerfälle (Token/Permission/Throttle) werden als failed/partial markiert mit error_code.
- Unbekannte `policy_types` können nicht gespeichert werden; falls Legacy-Daten vorkommen, werden sie zur Laufzeit geskippt: mit valid types → `partial`, ohne valid types → `skipped` (jeweils `error_code=UNKNOWN_POLICY_TYPE`).
- Retention hält nur die letzten N BackupSets pro Schedule.