Phase 7: scheduled (recurring) backups (0.7.0)

- BackupSchedule model + schedule_service: next-run calc (hourly/daily/weekly,
  UTC), background scheduler loop (lifespan), run-one with retention pruning
  (keep newest N per stack on the destination), backup_failed notify event.
- routers/schedules.py: schedules CRUD + run-now; registered in main.py.
- Frontend: api/schedules.ts + Settings → Scheduled backups (list with next/last
  run + status, enable/disable, run-now, delete; add form with stack/destination/
  frequency/time/weekday/retention/volumes).

Rough-verified only (per request): py_compile, frontend tsc build, app import
(95 routes), next-run math sanity. Full live run to be tested after deploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
menzelj
2026-06-07 22:02:52 +00:00
co-authored by Claude Opus 4.8
parent 7bd449101d
commit 84ef3df59e
10 changed files with 697 additions and 5 deletions
+2 -1
View File
@@ -2,6 +2,7 @@
from models.agent import Agent
from models.audit import AuditLog
from models.backup_destination import BackupDestination
from models.backup_schedule import BackupSchedule
from models.setting import Setting, Webhook
from models.stack import Stack
from models.template import Template
@@ -9,5 +10,5 @@ from models.user import User
__all__ = [
"User", "Stack", "AuditLog", "Template", "Setting", "Webhook", "Agent",
"BackupDestination",
"BackupDestination", "BackupSchedule",
]
+80
View File
@@ -0,0 +1,80 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import Optional
from sqlmodel import Field, SQLModel
def _now() -> datetime:
return datetime.now(timezone.utc)
FREQUENCIES = ["hourly", "daily", "weekly"]
class BackupSchedule(SQLModel, table=True):
"""An automatic, recurring backup of a local stack to a destination."""
id: Optional[int] = Field(default=None, primary_key=True)
stack_id: str
destination_id: int
frequency: str = "daily" # one of FREQUENCIES
hour: int = 3 # UTC, used for daily/weekly
minute: int = 0
weekday: int = 0 # 0=Mon .. 6=Sun, used for weekly
include_volumes: bool = True
stop_first: bool = True
keep: int = 7 # retention: keep newest N for this stack on the dest (0 = all)
enabled: bool = True
last_run: Optional[datetime] = None
last_status: Optional[str] = None # "ok" | "error: ..."
next_run: Optional[datetime] = None
created_at: datetime = Field(default_factory=_now)
# --- API schemas ---
class ScheduleCreate(SQLModel):
stack_id: str
destination_id: int
frequency: str = "daily"
hour: int = 3
minute: int = 0
weekday: int = 0
include_volumes: bool = True
stop_first: bool = True
keep: int = 7
enabled: bool = True
class ScheduleUpdate(SQLModel):
destination_id: Optional[int] = None
frequency: Optional[str] = None
hour: Optional[int] = None
minute: Optional[int] = None
weekday: Optional[int] = None
include_volumes: Optional[bool] = None
stop_first: Optional[bool] = None
keep: Optional[int] = None
enabled: Optional[bool] = None
class ScheduleRead(SQLModel):
id: int
stack_id: str
destination_id: int
destination_name: Optional[str]
frequency: str
hour: int
minute: int
weekday: int
include_volumes: bool
stop_first: bool
keep: int
enabled: bool
last_run: Optional[datetime]
last_status: Optional[str]
next_run: Optional[datetime]
created_at: datetime
+2
View File
@@ -16,6 +16,7 @@ EVENT_STACK_START = "stack_start"
EVENT_STACK_STOP = "stack_stop"
EVENT_STACK_ERROR = "stack_error"
EVENT_PULL_FAILED = "pull_failed"
EVENT_BACKUP_FAILED = "backup_failed"
ALL_EVENTS = [
EVENT_UPDATE_AVAILABLE,
@@ -23,6 +24,7 @@ ALL_EVENTS = [
EVENT_STACK_STOP,
EVENT_STACK_ERROR,
EVENT_PULL_FAILED,
EVENT_BACKUP_FAILED,
]
WEBHOOK_TYPES = ["ntfy", "discord", "slack", "gotify", "generic"]