From c3e855bd9bcd11bce7f7f6806799911c3b6f631d Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 24 May 2026 22:50:59 -0400 Subject: [PATCH] =?UTF-8?q?fc3h:=20BackupRun=20model=20=E2=80=94=20artifac?= =?UTF-8?q?t=20record=20for=20backup/restore=20runs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/app/models/__init__.py | 2 + backend/app/models/backup_run.py | 63 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 backend/app/models/backup_run.py diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index f7a3c94..0cefb2d 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -2,6 +2,7 @@ from .app_setting import AppSetting from .artist import Artist +from .backup_run import BackupRun from .base import Base from .credential import Credential from .download_event import DownloadEvent @@ -27,6 +28,7 @@ __all__ = [ "Base", "AppSetting", "Artist", + "BackupRun", "Source", "Credential", "Post", diff --git a/backend/app/models/backup_run.py b/backend/app/models/backup_run.py new file mode 100644 index 0000000..773a628 --- /dev/null +++ b/backend/app/models/backup_run.py @@ -0,0 +1,63 @@ +"""FC-3h: backup_run — operator-facing artifact record for a backup run. + +One row per backup attempt (kind='db' or 'images'). Lifecycle +tracking (started_at/finished_at/duration_ms/exception text) lives +in task_run from FC-3i — this row records artifact metadata: file +paths, sizes, tag (retention protection), and restore lineage via +restored_from_id. + +Status values (String, not Postgres ENUM — per +feedback_check_existing_enums): + pending — created but task hasn't started yet (rare; usually + status starts as 'running' from the task body). + running — backup task is in flight. + ok — artifact successfully written. + error — task raised; error column populated. + restoring — this row represents a restore attempt (kind = restored + kind); linked to source via restored_from_id. + restored — restore completed successfully. +""" + +from datetime import datetime + +from sqlalchemy import ( + JSON, + BigInteger, + DateTime, + ForeignKey, + Integer, + String, + Text, +) +from sqlalchemy.orm import Mapped, mapped_column + +from .base import Base + + +class BackupRun(Base): + __tablename__ = "backup_run" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + kind: Mapped[str] = mapped_column(String(16), nullable=False, index=True) + status: Mapped[str] = mapped_column( + String(16), nullable=False, default="pending", index=True, + ) + tag: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True) + triggered_by: Mapped[str] = mapped_column(String(32), nullable=False) + started_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, index=True, + ) + finished_at: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True, index=True, + ) + sql_path: Mapped[str | None] = mapped_column(Text, nullable=True) + tar_path: Mapped[str | None] = mapped_column(Text, nullable=True) + size_bytes: Mapped[int | None] = mapped_column(BigInteger, nullable=True) + error: Mapped[str | None] = mapped_column(Text, nullable=True) + manifest: Mapped[dict] = mapped_column( + JSON, nullable=False, default=dict, server_default="{}", + ) + restored_from_id: Mapped[int | None] = mapped_column( + ForeignKey("backup_run.id", ondelete="SET NULL"), + nullable=True, + )