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