backupchecks/containers/backupchecks/src/backend/app/models.py

696 lines
28 KiB
Python

from datetime import datetime
from flask_login import UserMixin
from flask import session, has_request_context
from werkzeug.security import generate_password_hash, check_password_hash
from .database import db
class User(db.Model, UserMixin):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
# username is the primary login identifier
username = db.Column(db.String(255), unique=True, nullable=False)
# email is kept for future use and may be NULL
email = db.Column(db.String(255), nullable=True)
password_hash = db.Column(db.String(255), nullable=False)
role = db.Column(db.String(50), nullable=False, default="viewer")
# UI theme preference: 'auto' (follow OS), 'light', 'dark'
theme_preference = db.Column(db.String(16), nullable=False, default="auto")
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
def set_password(self, password: str) -> None:
self.password_hash = generate_password_hash(password)
def check_password(self, password: str) -> bool:
return check_password_hash(self.password_hash, password)
@property
def roles(self) -> list[str]:
"""Return all assigned roles.
The database stores roles as a comma-separated string for backwards
compatibility with older schemas.
"""
raw = (self.role or "").strip()
if not raw:
return ["viewer"]
parts = [p.strip() for p in raw.split(",")]
roles = [p for p in parts if p]
return roles or ["viewer"]
@property
def active_role(self) -> str:
"""Return the currently active role for this user.
When a request context exists, the active role is stored in the session.
If the stored role is not assigned to the user, it falls back to the
first assigned role.
"""
default_role = self.roles[0]
if not has_request_context():
return default_role
selected = (session.get("active_role") or "").strip()
if selected and selected in self.roles:
return selected
session["active_role"] = default_role
return default_role
def set_active_role(self, role: str) -> None:
"""Set the active role in the current session (if possible)."""
if not has_request_context():
return
role = (role or "").strip()
if role and role in self.roles:
session["active_role"] = role
else:
session["active_role"] = self.roles[0]
@property
def is_admin(self) -> bool:
return self.active_role == "admin"
class SystemSettings(db.Model):
__tablename__ = "system_settings"
id = db.Column(db.Integer, primary_key=True)
# Graph / mail settings
graph_tenant_id = db.Column(db.String(255), nullable=True)
graph_client_id = db.Column(db.String(255), nullable=True)
graph_client_secret = db.Column(db.String(255), nullable=True)
graph_mailbox = db.Column(db.String(255), nullable=True)
incoming_folder = db.Column(db.String(255), nullable=True)
processed_folder = db.Column(db.String(255), nullable=True)
# Import configuration
auto_import_enabled = db.Column(db.Boolean, nullable=False, default=False)
auto_import_interval_minutes = db.Column(db.Integer, nullable=False, default=15)
auto_import_max_items = db.Column(db.Integer, nullable=False, default=50)
manual_import_batch_size = db.Column(db.Integer, nullable=False, default=50)
auto_import_cutoff_date = db.Column(db.Date, nullable=True)
# Debug storage: store raw EML in database for a limited retention window.
# 0 = disabled, 7/14 = retention days.
ingest_eml_retention_days = db.Column(db.Integer, nullable=False, default=7)
# Daily Jobs: from which date 'Missed' status should start to be applied.
daily_jobs_start_date = db.Column(db.Date, nullable=True)
# UI display timezone (IANA name). Used for rendering times in the web interface.
ui_timezone = db.Column(db.String(64), nullable=False, default="Europe/Amsterdam")
# Autotask integration settings
autotask_enabled = db.Column(db.Boolean, nullable=False, default=False)
autotask_environment = db.Column(db.String(32), nullable=True) # sandbox | production
autotask_api_username = db.Column(db.String(255), nullable=True)
autotask_api_password = db.Column(db.String(255), nullable=True)
autotask_tracking_identifier = db.Column(db.String(255), nullable=True)
autotask_base_url = db.Column(db.String(512), nullable=True) # Backupchecks base URL for deep links
# Autotask defaults (IDs are leading)
autotask_default_queue_id = db.Column(db.Integer, nullable=True)
autotask_default_ticket_source_id = db.Column(db.Integer, nullable=True)
autotask_default_ticket_status = db.Column(db.Integer, nullable=True)
autotask_priority_warning = db.Column(db.Integer, nullable=True)
autotask_priority_error = db.Column(db.Integer, nullable=True)
# Cached reference data (for dropdowns)
autotask_cached_queues_json = db.Column(db.Text, nullable=True)
autotask_cached_ticket_sources_json = db.Column(db.Text, nullable=True)
autotask_cached_priorities_json = db.Column(db.Text, nullable=True)
autotask_cached_ticket_statuses_json = db.Column(db.Text, nullable=True)
autotask_reference_last_sync_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
class AdminLog(db.Model):
__tablename__ = "admin_logs"
id = db.Column(db.Integer, primary_key=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
user = db.Column(db.String(255), nullable=True)
event_type = db.Column(db.String(64), nullable=False)
message = db.Column(db.Text, nullable=False)
details = db.Column(db.Text, nullable=True)
class Customer(db.Model):
__tablename__ = "customers"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), unique=True, nullable=False)
active = db.Column(db.Boolean, nullable=False, default=True)
# Autotask company mapping (Phase 3)
# Company ID is leading; name is cached for UI display.
autotask_company_id = db.Column(db.Integer, nullable=True)
autotask_company_name = db.Column(db.String(255), nullable=True)
# Mapping status: ok | renamed | missing | invalid
autotask_mapping_status = db.Column(db.String(20), nullable=True)
autotask_last_sync_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
class Override(db.Model):
__tablename__ = "overrides"
id = db.Column(db.Integer, primary_key=True)
# Level of the override: global or object (job-level is no longer used)
level = db.Column(db.String(20), nullable=False)
# Scope for global overrides (optional wildcard fields)
backup_software = db.Column(db.String(255), nullable=True)
backup_type = db.Column(db.String(255), nullable=True)
# Scope for object overrides
job_id = db.Column(db.Integer, db.ForeignKey("jobs.id"), nullable=True)
object_name = db.Column(db.String(255), nullable=True)
# Matching criteria on object status / error message
match_status = db.Column(db.String(32), nullable=True)
match_error_contains = db.Column(db.String(255), nullable=True)
# Matching mode for error text: contains (default), exact, starts_with, ends_with
match_error_mode = db.Column(db.String(20), nullable=True)
# Behaviour flags
treat_as_success = db.Column(db.Boolean, nullable=False, default=True)
active = db.Column(db.Boolean, nullable=False, default=True)
# Validity window
start_at = db.Column(db.DateTime, nullable=False)
end_at = db.Column(db.DateTime, nullable=True)
# Management metadata
comment = db.Column(db.Text, nullable=True)
created_by = db.Column(db.String(255), nullable=True)
updated_by = db.Column(db.String(255), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
class Job(db.Model):
__tablename__ = "jobs"
id = db.Column(db.Integer, primary_key=True)
customer_id = db.Column(db.Integer, db.ForeignKey("customers.id"), nullable=True)
backup_software = db.Column(db.String(128), nullable=True)
backup_type = db.Column(db.String(128), nullable=True)
job_name = db.Column(db.String(512), nullable=True)
from_address = db.Column(db.String(512), nullable=True)
schedule_type = db.Column(db.String(32), nullable=True) # daily, weekly, monthly, yearly
schedule_days_of_week = db.Column(db.String(64), nullable=True) # e.g. "Mon,Tue,Wed"
schedule_day_of_month = db.Column(db.Integer, nullable=True) # 1-31
schedule_times = db.Column(db.String(255), nullable=True) # e.g. "01:00,13:15"
auto_approve = db.Column(db.Boolean, nullable=False, default=True)
active = db.Column(db.Boolean, nullable=False, default=True)
# Archived jobs are excluded from Daily Jobs and Run Checks.
# JobRuns remain in the database and are still included in reporting.
archived = db.Column(db.Boolean, nullable=False, default=False)
archived_at = db.Column(db.DateTime, nullable=True)
archived_by_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
customer = db.relationship(
"Customer",
backref=db.backref("jobs", lazy="dynamic"),
lazy="joined",
)
class JobRun(db.Model):
__tablename__ = "job_runs"
id = db.Column(db.Integer, primary_key=True)
job_id = db.Column(db.Integer, db.ForeignKey("jobs.id"), nullable=False)
mail_message_id = db.Column(db.Integer, db.ForeignKey("mail_messages.id"), nullable=True)
run_at = db.Column(db.DateTime, nullable=True)
status = db.Column(db.String(64), nullable=True)
remark = db.Column(db.Text, nullable=True)
missed = db.Column(db.Boolean, nullable=False, default=False)
override_applied = db.Column(db.Boolean, nullable=False, default=False)
# Override metadata for reporting/auditing.
# These are populated when override flags are recomputed.
override_applied_override_id = db.Column(db.Integer, nullable=True)
override_applied_level = db.Column(db.String(16), nullable=True)
override_applied_reason = db.Column(db.Text, nullable=True)
# Optional storage metrics (e.g. for repository capacity monitoring)
storage_used_bytes = db.Column(db.BigInteger, nullable=True)
storage_capacity_bytes = db.Column(db.BigInteger, nullable=True)
storage_free_bytes = db.Column(db.BigInteger, nullable=True)
storage_free_percent = db.Column(db.Float, nullable=True)
# Run review (Run Checks)
reviewed_at = db.Column(db.DateTime, nullable=True)
reviewed_by_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
# Autotask integration (Phase 4: ticket creation from Run Checks)
autotask_ticket_id = db.Column(db.Integer, nullable=True)
autotask_ticket_number = db.Column(db.String(64), nullable=True)
autotask_ticket_created_at = db.Column(db.DateTime, nullable=True)
autotask_ticket_created_by_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
job = db.relationship(
"Job",
backref=db.backref("runs", lazy="dynamic", cascade="all, delete-orphan"),
)
reviewed_by = db.relationship("User", foreign_keys=[reviewed_by_user_id])
autotask_ticket_created_by = db.relationship("User", foreign_keys=[autotask_ticket_created_by_user_id])
class JobRunReviewEvent(db.Model):
__tablename__ = "job_run_review_events"
id = db.Column(db.Integer, primary_key=True)
run_id = db.Column(db.Integer, db.ForeignKey("job_runs.id"), nullable=False)
action = db.Column(db.String(32), nullable=False) # REVIEWED | UNREVIEWED
actor_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
note = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
run = db.relationship(
"JobRun",
backref=db.backref("review_events", lazy="dynamic", cascade="all, delete-orphan"),
)
actor = db.relationship("User", foreign_keys=[actor_user_id])
class JobObject(db.Model):
__tablename__ = "job_objects"
id = db.Column(db.Integer, primary_key=True)
job_run_id = db.Column(db.Integer, db.ForeignKey("job_runs.id"), nullable=False)
object_name = db.Column(db.String(512), nullable=False)
object_type = db.Column(db.String(128), nullable=True)
status = db.Column(db.String(64), nullable=True)
error_message = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
job_run = db.relationship(
"JobRun",
backref=db.backref("objects", lazy="dynamic", cascade="all, delete-orphan"),
)
class MailMessage(db.Model):
__tablename__ = "mail_messages"
id = db.Column(db.Integer, primary_key=True)
# Basic mail metadata
message_id = db.Column(db.String(512), unique=True, nullable=True)
from_address = db.Column(db.String(512), nullable=True)
subject = db.Column(db.String(1024), nullable=True)
received_at = db.Column(db.DateTime, nullable=True)
# Parsed backup metadata
backup_software = db.Column(db.String(128), nullable=True)
backup_type = db.Column(db.String(128), nullable=True)
job_name = db.Column(db.String(512), nullable=True)
from_address = db.Column(db.String(512), nullable=True)
overall_status = db.Column(db.String(32), nullable=True)
overall_message = db.Column(db.Text, nullable=True)
parse_result = db.Column(db.String(32), nullable=True)
parse_error = db.Column(db.String(512), nullable=True)
parsed_at = db.Column(db.DateTime, nullable=True)
# Optional storage metrics (e.g. repository capacity monitoring)
storage_used_bytes = db.Column(db.BigInteger, nullable=True)
storage_capacity_bytes = db.Column(db.BigInteger, nullable=True)
storage_free_bytes = db.Column(db.BigInteger, nullable=True)
storage_free_percent = db.Column(db.Float, nullable=True)
# Link back to Job and location (inbox/history)
job_id = db.Column(db.Integer, db.ForeignKey("jobs.id"), nullable=True)
location = db.Column(db.String(32), nullable=False, default="inbox")
# Raw / rendered content storage (for inline popup)
html_body = db.Column(db.Text, nullable=True)
text_body = db.Column(db.Text, nullable=True)
# Optional raw RFC822 message storage (debug) - controlled by SystemSettings.ingest_eml_retention_days
eml_blob = db.Column(db.LargeBinary, nullable=True)
eml_stored_at = db.Column(db.DateTime, nullable=True)
# Approval metadata
approved = db.Column(db.Boolean, nullable=False, default=False)
approved_at = db.Column(db.DateTime, nullable=True)
approved_by_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
# Soft-delete metadata (Inbox delete -> Admin restore)
deleted_at = db.Column(db.DateTime, nullable=True)
deleted_by_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
deleted_by_user = db.relationship("User", foreign_keys=[deleted_by_user_id])
class MailObject(db.Model):
__tablename__ = "mail_objects"
id = db.Column(db.Integer, primary_key=True)
mail_message_id = db.Column(db.Integer, db.ForeignKey("mail_messages.id"), nullable=False)
object_name = db.Column(db.String(512), nullable=False)
object_type = db.Column(db.String(128), nullable=True)
status = db.Column(db.String(64), nullable=True)
error_message = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
class Ticket(db.Model):
__tablename__ = "tickets"
id = db.Column(db.Integer, primary_key=True)
ticket_code = db.Column(db.String(32), unique=True, nullable=False)
title = db.Column(db.String(255))
description = db.Column(db.Text)
# Date (Europe/Amsterdam) from which this ticket should be considered active
# for the scoped job(s) in Daily Jobs / Job Details views.
active_from_date = db.Column(db.Date, nullable=False)
# Audit timestamp: when the ticket was created (UTC, naive)
start_date = db.Column(db.DateTime, nullable=False)
resolved_at = db.Column(db.DateTime)
# Resolution origin for audit/UI: psa | backupchecks
resolved_origin = db.Column(db.String(32))
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class TicketScope(db.Model):
__tablename__ = "ticket_scopes"
id = db.Column(db.Integer, primary_key=True)
ticket_id = db.Column(db.Integer, db.ForeignKey("tickets.id"), nullable=False)
scope_type = db.Column(db.String(32), nullable=False)
customer_id = db.Column(db.Integer, db.ForeignKey("customers.id"))
backup_software = db.Column(db.String(128))
backup_type = db.Column(db.String(128))
job_id = db.Column(db.Integer, db.ForeignKey("jobs.id"))
job_name_match = db.Column(db.String(255))
job_name_match_mode = db.Column(db.String(32))
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
resolved_at = db.Column(db.DateTime)
class TicketJobRun(db.Model):
__tablename__ = "ticket_job_runs"
id = db.Column(db.Integer, primary_key=True)
ticket_id = db.Column(db.Integer, db.ForeignKey("tickets.id"), nullable=False)
job_run_id = db.Column(db.Integer, db.ForeignKey("job_runs.id"), nullable=False)
linked_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
link_source = db.Column(db.String(64), nullable=False)
__table_args__ = (db.UniqueConstraint("ticket_id", "job_run_id", name="uq_ticket_job_run"),)
class Remark(db.Model):
__tablename__ = "remarks"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(255))
body = db.Column(db.Text, nullable=False)
# Date (Europe/Amsterdam) from which this remark should be considered active
# for the scoped job(s) in Daily Jobs / Job Details views.
active_from_date = db.Column(db.Date)
start_date = db.Column(db.DateTime)
resolved_at = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class RemarkScope(db.Model):
__tablename__ = "remark_scopes"
id = db.Column(db.Integer, primary_key=True)
remark_id = db.Column(db.Integer, db.ForeignKey("remarks.id"), nullable=False)
scope_type = db.Column(db.String(32), nullable=False)
customer_id = db.Column(db.Integer, db.ForeignKey("customers.id"))
backup_software = db.Column(db.String(128))
backup_type = db.Column(db.String(128))
job_id = db.Column(db.Integer, db.ForeignKey("jobs.id"))
job_name_match = db.Column(db.String(255))
job_name_match_mode = db.Column(db.String(32))
job_run_id = db.Column(db.Integer, db.ForeignKey("job_runs.id"))
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
class RemarkJobRun(db.Model):
__tablename__ = "remark_job_runs"
id = db.Column(db.Integer, primary_key=True)
remark_id = db.Column(db.Integer, db.ForeignKey("remarks.id"), nullable=False)
job_run_id = db.Column(db.Integer, db.ForeignKey("job_runs.id"), nullable=False)
linked_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
link_source = db.Column(db.String(64), nullable=False)
__table_args__ = (db.UniqueConstraint("remark_id", "job_run_id", name="uq_remark_job_run"),)
class FeedbackItem(db.Model):
__tablename__ = "feedback_items"
id = db.Column(db.Integer, primary_key=True)
# bug | feature
item_type = db.Column(db.String(16), nullable=False)
title = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text, nullable=False)
component = db.Column(db.String(255), nullable=True)
# open | resolved
status = db.Column(db.String(16), nullable=False, default="open")
created_by_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
resolved_by_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
resolved_at = db.Column(db.DateTime, nullable=True)
deleted_by_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
deleted_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
class FeedbackVote(db.Model):
__tablename__ = "feedback_votes"
id = db.Column(db.Integer, primary_key=True)
feedback_item_id = db.Column(
db.Integer, db.ForeignKey("feedback_items.id"), nullable=False
)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
__table_args__ = (
db.UniqueConstraint(
"feedback_item_id", "user_id", name="uq_feedback_vote_item_user"
),
)
class FeedbackReply(db.Model):
__tablename__ = "feedback_replies"
id = db.Column(db.Integer, primary_key=True)
feedback_item_id = db.Column(
db.Integer, db.ForeignKey("feedback_items.id", ondelete="CASCADE"), nullable=False
)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
message = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
class NewsItem(db.Model):
__tablename__ = "news_items"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(255), nullable=False)
body = db.Column(db.Text, nullable=False)
link_url = db.Column(db.String(2048), nullable=True)
severity = db.Column(db.String(32), nullable=False, default="info") # info, warning
pinned = db.Column(db.Boolean, nullable=False, default=False)
active = db.Column(db.Boolean, nullable=False, default=True)
publish_from = db.Column(db.DateTime, nullable=True)
publish_until = db.Column(db.DateTime, nullable=True)
created_by_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
class NewsRead(db.Model):
__tablename__ = "news_reads"
id = db.Column(db.Integer, primary_key=True)
news_item_id = db.Column(db.Integer, db.ForeignKey("news_items.id"), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
read_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
# --- Reporting (phase 1: raw data foundation) ---------------------------------
class ReportDefinition(db.Model):
__tablename__ = "report_definitions"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text, nullable=True)
# one-time | scheduled
report_type = db.Column(db.String(32), nullable=False, default="one-time")
# csv | html | pdf
output_format = db.Column(db.String(16), nullable=False, default="csv")
# customer scope for report generation
# all | single | multiple
customer_scope = db.Column(db.String(16), nullable=False, default="all")
# JSON encoded list of customer ids. NULL/empty when scope=all.
customer_ids = db.Column(db.Text, nullable=True)
period_start = db.Column(db.DateTime, nullable=False)
period_end = db.Column(db.DateTime, nullable=False)
# For scheduled reports in later phases (cron / RRULE style string)
schedule = db.Column(db.String(255), nullable=True)
# JSON report definition for UI (columns, charts, filters, templates)
# Stored as TEXT to remain flexible and allow future PDF rendering.
report_config = db.Column(db.Text, nullable=True)
created_by_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(
db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
)
created_by = db.relationship("User", foreign_keys=[created_by_user_id])
class ReportObjectSnapshot(db.Model):
__tablename__ = "report_object_snapshots"
id = db.Column(db.Integer, primary_key=True)
report_id = db.Column(db.Integer, db.ForeignKey("report_definitions.id"), nullable=False)
# Object identity (from customer_objects.object_name)
object_name = db.Column(db.Text, nullable=False)
# Job identity
job_id = db.Column(db.Integer, nullable=True)
job_name = db.Column(db.Text, nullable=True)
customer_id = db.Column(db.Integer, nullable=True)
customer_name = db.Column(db.Text, nullable=True)
backup_software = db.Column(db.Text, nullable=True)
backup_type = db.Column(db.Text, nullable=True)
# Run identity
run_id = db.Column(db.Integer, nullable=True)
run_at = db.Column(db.DateTime, nullable=True)
status = db.Column(db.Text, nullable=True)
missed = db.Column(db.Boolean, nullable=False, default=False)
override_applied = db.Column(db.Boolean, nullable=False, default=False)
reviewed_at = db.Column(db.DateTime, nullable=True)
ticket_number = db.Column(db.Text, nullable=True)
remark = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
report = db.relationship(
"ReportDefinition",
backref=db.backref("object_snapshots", lazy="dynamic", cascade="all, delete-orphan"),
)
class ReportObjectSummary(db.Model):
__tablename__ = "report_object_summaries"
id = db.Column(db.Integer, primary_key=True)
report_id = db.Column(db.Integer, db.ForeignKey("report_definitions.id"), nullable=False)
object_name = db.Column(db.Text, nullable=False)
customer_id = db.Column(db.Integer, nullable=True)
customer_name = db.Column(db.Text, nullable=True)
total_runs = db.Column(db.Integer, nullable=False, default=0)
success_count = db.Column(db.Integer, nullable=False, default=0)
success_override_count = db.Column(db.Integer, nullable=False, default=0)
warning_count = db.Column(db.Integer, nullable=False, default=0)
failed_count = db.Column(db.Integer, nullable=False, default=0)
missed_count = db.Column(db.Integer, nullable=False, default=0)
success_rate = db.Column(db.Float, nullable=False, default=0.0)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
report = db.relationship(
"ReportDefinition",
backref=db.backref("object_summaries", lazy="dynamic", cascade="all, delete-orphan"),
)