696 lines
28 KiB
Python
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"),
|
|
)
|