diff --git a/containers/backupchecks/src/backend/app/__init__.py b/containers/backupchecks/src/backend/app/__init__.py index 2b54a76..43857f0 100644 --- a/containers/backupchecks/src/backend/app/__init__.py +++ b/containers/backupchecks/src/backend/app/__init__.py @@ -69,6 +69,9 @@ def create_app(): This ensures that when a user opens the site for the first time each day, they land on the dashboard regardless of the bookmarked/deeplinked URL. + + This behavior is controlled by the system setting `require_daily_dashboard_visit`. + When disabled (the default), users can navigate directly to any page. """ # Only for normal page loads. @@ -99,6 +102,18 @@ def create_app(): session["daily_dashboard_seen"] = _get_today_ui_date() return None + # Check if the feature is enabled in system settings. + try: + from .models import SystemSettings + + settings = SystemSettings.query.first() + if not settings or not getattr(settings, "require_daily_dashboard_visit", False): + # Feature is disabled; skip redirect. + return None + except Exception: + # On any error (e.g. column doesn't exist yet), skip redirect. + return None + today = _get_today_ui_date() seen = (session.get("daily_dashboard_seen") or "").strip() if seen != today: diff --git a/containers/backupchecks/src/backend/app/main/routes_settings.py b/containers/backupchecks/src/backend/app/main/routes_settings.py index 7018135..905f61e 100644 --- a/containers/backupchecks/src/backend/app/main/routes_settings.py +++ b/containers/backupchecks/src/backend/app/main/routes_settings.py @@ -430,6 +430,10 @@ def settings(): if "ui_timezone" in request.form: settings.ui_timezone = (request.form.get("ui_timezone") or "").strip() or "Europe/Amsterdam" + # Navigation setting is in the same form (General tab), so process it here. + # Checkbox: present in form = checked, absent = unchecked. + settings.require_daily_dashboard_visit = bool(request.form.get("require_daily_dashboard_visit")) + # Daily Jobs if "daily_jobs_start_date" in request.form: daily_jobs_start_date_str = (request.form.get("daily_jobs_start_date") or "").strip() diff --git a/containers/backupchecks/src/backend/app/migrations.py b/containers/backupchecks/src/backend/app/migrations.py index 334be39..a8e9976 100644 --- a/containers/backupchecks/src/backend/app/migrations.py +++ b/containers/backupchecks/src/backend/app/migrations.py @@ -801,6 +801,8 @@ def run_migrations() -> None: migrate_news_tables() migrate_reporting_tables() migrate_reporting_report_config() + migrate_performance_indexes() + migrate_system_settings_require_daily_dashboard_visit() print("[migrations] All migrations completed.") @@ -844,6 +846,78 @@ def migrate_jobs_archiving() -> None: print("[migrations] migrate_jobs_archiving completed.") +def migrate_system_settings_require_daily_dashboard_visit() -> None: + """Add require_daily_dashboard_visit column to system_settings if missing. + + When enabled, authenticated users are redirected to the dashboard on + their first page view each day. + """ + table = "system_settings" + column = "require_daily_dashboard_visit" + + try: + engine = db.get_engine() + except Exception as exc: + print(f"[migrations] Could not get engine for system_settings require_daily_dashboard_visit migration: {exc}") + return + + try: + if _column_exists(table, column): + print("[migrations] system_settings.require_daily_dashboard_visit already exists.") + return + + with engine.begin() as conn: + conn.execute( + text( + f'ALTER TABLE "{table}" ADD COLUMN {column} BOOLEAN NOT NULL DEFAULT FALSE' + ) + ) + + print("[migrations] migrate_system_settings_require_daily_dashboard_visit completed.") + except Exception as exc: + print(f"[migrations] Failed to migrate system_settings.require_daily_dashboard_visit: {exc}") + + +def migrate_performance_indexes() -> None: + """Add performance indexes for frequently queried foreign key columns. + + These indexes significantly improve query performance on slow storage, + especially for Daily Jobs and Run Checks pages. + + This migration is idempotent. + """ + try: + engine = db.get_engine() + except Exception as exc: + print(f"[migrations] Could not get engine for performance indexes migration: {exc}") + return + + with engine.begin() as conn: + # JobRun indexes + conn.execute(text('CREATE INDEX IF NOT EXISTS idx_job_run_job_id ON job_runs (job_id)')) + conn.execute(text('CREATE INDEX IF NOT EXISTS idx_job_run_job_id_run_at ON job_runs (job_id, run_at)')) + conn.execute(text('CREATE INDEX IF NOT EXISTS idx_job_run_job_id_reviewed_at ON job_runs (job_id, reviewed_at)')) + conn.execute(text('CREATE INDEX IF NOT EXISTS idx_job_run_mail_message_id ON job_runs (mail_message_id)')) + + # MailMessage indexes + conn.execute(text('CREATE INDEX IF NOT EXISTS idx_mail_message_job_id ON mail_messages (job_id)')) + conn.execute(text('CREATE INDEX IF NOT EXISTS idx_mail_message_location ON mail_messages (location)')) + conn.execute(text('CREATE INDEX IF NOT EXISTS idx_mail_message_job_id_location ON mail_messages (job_id, location)')) + + # MailObject indexes + conn.execute(text('CREATE INDEX IF NOT EXISTS idx_mail_object_mail_message_id ON mail_objects (mail_message_id)')) + + # TicketScope indexes + conn.execute(text('CREATE INDEX IF NOT EXISTS idx_ticket_scope_ticket_id ON ticket_scopes (ticket_id)')) + conn.execute(text('CREATE INDEX IF NOT EXISTS idx_ticket_scope_job_id ON ticket_scopes (job_id)')) + + # RemarkScope indexes + conn.execute(text('CREATE INDEX IF NOT EXISTS idx_remark_scope_remark_id ON remark_scopes (remark_id)')) + conn.execute(text('CREATE INDEX IF NOT EXISTS idx_remark_scope_job_id ON remark_scopes (job_id)')) + + print("[migrations] migrate_performance_indexes completed.") + + def migrate_reporting_report_config() -> None: """Add report_definitions.report_config column if missing. diff --git a/containers/backupchecks/src/backend/app/models.py b/containers/backupchecks/src/backend/app/models.py index 3d23da6..fb9329f 100644 --- a/containers/backupchecks/src/backend/app/models.py +++ b/containers/backupchecks/src/backend/app/models.py @@ -107,6 +107,11 @@ class SystemSettings(db.Model): # 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") + # Navigation behavior: require visiting dashboard first each day. + # When enabled, authenticated users are redirected to the dashboard on + # their first page view each day before they can navigate elsewhere. + require_daily_dashboard_visit = db.Column(db.Boolean, nullable=False, default=False) + 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 diff --git a/containers/backupchecks/src/templates/main/settings.html b/containers/backupchecks/src/templates/main/settings.html index bdc5fbe..7bb609c 100644 --- a/containers/backupchecks/src/templates/main/settings.html +++ b/containers/backupchecks/src/templates/main/settings.html @@ -136,6 +136,17 @@ +