From b0efa7f21d4089b91972fabe84e5ee347c6d9919 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 13:34:21 +0100 Subject: [PATCH 01/18] Remove customer name from Autotask ticket titles Changed ticket title format from: [Backupchecks] Customer Name - Job Name - Status To: [Backupchecks] Job Name - Status Customer information is already available in the ticket's company field, making it redundant in the title and causing unnecessarily long ticket titles. Co-Authored-By: Claude Sonnet 4.5 --- .../backupchecks/src/backend/app/main/routes_run_checks.py | 2 +- docs/changelog-claude.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/containers/backupchecks/src/backend/app/main/routes_run_checks.py b/containers/backupchecks/src/backend/app/main/routes_run_checks.py index 72ce89a..12ee4b0 100644 --- a/containers/backupchecks/src/backend/app/main/routes_run_checks.py +++ b/containers/backupchecks/src/backend/app/main/routes_run_checks.py @@ -1464,7 +1464,7 @@ def api_run_checks_create_autotask_ticket(): } ) - subject = f"[Backupchecks] {customer.name} - {job.job_name or ''} - {status_display}" + subject = f"[Backupchecks] {job.job_name or ''} - {status_display}" description = _compose_autotask_ticket_description( settings=settings, job=job, diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index 1a386e3..cd5e1f3 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -4,6 +4,9 @@ This file documents all changes made to this project via Claude Code. ## [2026-02-09] +### Changed +- Removed customer name from Autotask ticket title to keep titles concise (format changed from "[Backupchecks] Customer - Job Name - Status" to "[Backupchecks] Job Name - Status") + ### Fixed - Fixed Autotask ticket description being set to NULL when resolving tickets via `update_ticket_resolution_safe` by adding "description" to the optional_fields list, ensuring the original description is preserved during PUT operations From 60c7e89dc250bc62ac29c506e4ad3180ad961b24 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 13:45:05 +0100 Subject: [PATCH 02/18] Add cleanup orphaned jobs maintenance option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added new maintenance option in Settings → Maintenance to delete jobs that are no longer linked to an existing customer (customer_id is NULL or customer doesn't exist). Features: - Finds all jobs without valid customer link - Deletes jobs, runs, and related emails permanently - Cleans up auxiliary tables (ticket_job_runs, remark_job_runs, scopes, overrides) - Provides feedback on deleted items count - Logs action to audit log Use case: When customers are removed, their jobs and emails should be completely removed from the database. Co-Authored-By: Claude Sonnet 4.5 --- .../src/backend/app/main/routes_settings.py | 117 ++++++++++++++++++ .../src/templates/main/settings.html | 12 ++ docs/changelog-claude.md | 3 + 3 files changed, 132 insertions(+) diff --git a/containers/backupchecks/src/backend/app/main/routes_settings.py b/containers/backupchecks/src/backend/app/main/routes_settings.py index 2a16256..bb19ea7 100644 --- a/containers/backupchecks/src/backend/app/main/routes_settings.py +++ b/containers/backupchecks/src/backend/app/main/routes_settings.py @@ -124,6 +124,123 @@ def settings_jobs_delete_all(): return redirect(url_for("main.settings")) +@main_bp.route("/settings/jobs/delete-orphaned", methods=["POST"]) +@login_required +@roles_required("admin") +def settings_jobs_delete_orphaned(): + """Delete jobs that have no customer (customer_id is NULL or customer does not exist). + + Also deletes all related emails from the database since the customer is gone. + """ + try: + # Find jobs without valid customer + orphaned_jobs = Job.query.outerjoin(Customer, Job.customer_id == Customer.id).filter( + db.or_( + Job.customer_id.is_(None), + Customer.id.is_(None) + ) + ).all() + + if not orphaned_jobs: + flash("No orphaned jobs found.", "info") + return redirect(url_for("main.settings", section="maintenance")) + + job_count = len(orphaned_jobs) + mail_count = 0 + run_count = 0 + + # Collect mail message ids and run ids for cleanup + mail_message_ids = [] + run_ids = [] + job_ids = [job.id for job in orphaned_jobs] + + for job in orphaned_jobs: + for run in job.runs: + if run.id is not None: + run_ids.append(run.id) + run_count += 1 + if run.mail_message_id: + mail_message_ids.append(run.mail_message_id) + + # Delete related mails permanently (customer is gone) + if mail_message_ids: + mail_count = len(mail_message_ids) + MailMessage.query.filter(MailMessage.id.in_(mail_message_ids)).delete(synchronize_session=False) + + # Helper function for safe SQL execution + def _safe_execute(stmt, params): + try: + db.session.execute(stmt, params) + except Exception: + pass + + # Clean up auxiliary tables that may not have ON DELETE CASCADE + if run_ids: + from sqlalchemy import text, bindparam + _safe_execute( + text("DELETE FROM ticket_job_runs WHERE job_run_id IN :run_ids").bindparams( + bindparam("run_ids", expanding=True) + ), + {"run_ids": run_ids}, + ) + _safe_execute( + text("DELETE FROM remark_job_runs WHERE job_run_id IN :run_ids").bindparams( + bindparam("run_ids", expanding=True) + ), + {"run_ids": run_ids}, + ) + + if job_ids: + from sqlalchemy import text, bindparam + # Clean up scopes + _safe_execute( + text("DELETE FROM ticket_scopes WHERE job_id IN :job_ids").bindparams( + bindparam("job_ids", expanding=True) + ), + {"job_ids": job_ids}, + ) + _safe_execute( + text("DELETE FROM remark_scopes WHERE job_id IN :job_ids").bindparams( + bindparam("job_ids", expanding=True) + ), + {"job_ids": job_ids}, + ) + # Clean up overrides + _safe_execute( + text("DELETE FROM overrides WHERE job_id IN :job_ids").bindparams( + bindparam("job_ids", expanding=True) + ), + {"job_ids": job_ids}, + ) + + # Delete all orphaned jobs (runs/objects are cascaded via ORM relationships) + for job in orphaned_jobs: + db.session.delete(job) + + db.session.commit() + + flash( + f"Deleted {job_count} orphaned job(s), {run_count} run(s), and {mail_count} email(s).", + "success" + ) + + _log_admin_event( + event_type="maintenance_delete_orphaned_jobs", + details={ + "jobs_deleted": job_count, + "runs_deleted": run_count, + "mails_deleted": mail_count, + }, + ) + + except Exception as exc: + db.session.rollback() + print(f"[settings-jobs] Failed to delete orphaned jobs: {exc}") + flash("Failed to delete orphaned jobs.", "danger") + + return redirect(url_for("main.settings", section="maintenance")) + + @main_bp.route("/settings/objects/backfill", methods=["POST"]) @login_required @roles_required("admin") diff --git a/containers/backupchecks/src/templates/main/settings.html b/containers/backupchecks/src/templates/main/settings.html index 301d790..d6bc75a 100644 --- a/containers/backupchecks/src/templates/main/settings.html +++ b/containers/backupchecks/src/templates/main/settings.html @@ -549,6 +549,18 @@ +
+
+
Cleanup orphaned jobs
+
+

Delete jobs that are no longer linked to an existing customer. Related emails and runs will be permanently deleted from the database.

+
+ +
+
+
+
+
Jobs maintenance
diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index cd5e1f3..0798f7c 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -4,6 +4,9 @@ This file documents all changes made to this project via Claude Code. ## [2026-02-09] +### Added +- Added "Cleanup orphaned jobs" maintenance option in Settings → Maintenance to delete jobs without valid customer links and their associated emails/runs permanently from database (useful when customers are removed) + ### Changed - Removed customer name from Autotask ticket title to keep titles concise (format changed from "[Backupchecks] Customer - Job Name - Status" to "[Backupchecks] Job Name - Status") From ff316d653a9f1187931a66e9299987bbd800a263 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 13:46:55 +0100 Subject: [PATCH 03/18] Auto-commit local changes before build (2026-02-09 13:46:55) --- .last-branch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.last-branch b/.last-branch index b7e5a1e..d27683e 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260209-01-fix-ticket-description +v20260209-03-cleanup-orphaned-jobs From 91062bdb0decf575d4aa89eb965c99f050150fec Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 13:49:02 +0100 Subject: [PATCH 04/18] Update .last-branch --- .last-branch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.last-branch b/.last-branch index d27683e..0cbcebd 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260209-03-cleanup-orphaned-jobs +v20260209-02-remove-customer-from-ticket-title From fec28b2bfa14e46536ecc394b22c9d5391d12bab Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 13:49:21 +0100 Subject: [PATCH 05/18] Auto-commit local changes before build (2026-02-09 13:49:21) --- .last-branch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.last-branch b/.last-branch index 0cbcebd..d27683e 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260209-02-remove-customer-from-ticket-title +v20260209-03-cleanup-orphaned-jobs From 7f8dffa3ae522c50ef8c7126c0c332ca077fb664 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 14:07:12 +0100 Subject: [PATCH 06/18] Add preview page for orphaned jobs before deletion Added verification step before deleting orphaned jobs: - New GET endpoint /settings/jobs/orphaned to preview the list - Shows detailed table with job name, backup software/type, customer ID, run count, and email count - "Preview orphaned jobs" button on maintenance page - Delete button on preview page shows exact count - Summary shows total jobs, runs, and emails to be deleted This allows admins to verify which jobs will be deleted before taking the destructive action. Co-Authored-By: Claude Sonnet 4.5 --- .../src/backend/app/main/routes_settings.py | 35 ++++++++ .../src/templates/main/settings.html | 9 +- .../main/settings_orphaned_jobs.html | 87 +++++++++++++++++++ docs/changelog-claude.md | 1 + 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 containers/backupchecks/src/templates/main/settings_orphaned_jobs.html diff --git a/containers/backupchecks/src/backend/app/main/routes_settings.py b/containers/backupchecks/src/backend/app/main/routes_settings.py index bb19ea7..e2a4038 100644 --- a/containers/backupchecks/src/backend/app/main/routes_settings.py +++ b/containers/backupchecks/src/backend/app/main/routes_settings.py @@ -124,6 +124,41 @@ def settings_jobs_delete_all(): return redirect(url_for("main.settings")) +@main_bp.route("/settings/jobs/orphaned", methods=["GET"]) +@login_required +@roles_required("admin") +def settings_jobs_orphaned(): + """Show list of orphaned jobs for verification before deletion.""" + # Find jobs without valid customer + orphaned_jobs = Job.query.outerjoin(Customer, Job.customer_id == Customer.id).filter( + db.or_( + Job.customer_id.is_(None), + Customer.id.is_(None) + ) + ).order_by(Job.job_name.asc()).all() + + # Build list with details + jobs_list = [] + for job in orphaned_jobs: + run_count = JobRun.query.filter_by(job_id=job.id).count() + mail_count = JobRun.query.filter_by(job_id=job.id).filter(JobRun.mail_message_id.isnot(None)).count() + + jobs_list.append({ + "id": job.id, + "job_name": job.job_name or "Unnamed", + "backup_software": job.backup_software or "-", + "backup_type": job.backup_type or "-", + "customer_id": job.customer_id, + "run_count": run_count, + "mail_count": mail_count, + }) + + return render_template( + "main/settings_orphaned_jobs.html", + orphaned_jobs=jobs_list, + ) + + @main_bp.route("/settings/jobs/delete-orphaned", methods=["POST"]) @login_required @roles_required("admin") diff --git a/containers/backupchecks/src/templates/main/settings.html b/containers/backupchecks/src/templates/main/settings.html index d6bc75a..ab581fa 100644 --- a/containers/backupchecks/src/templates/main/settings.html +++ b/containers/backupchecks/src/templates/main/settings.html @@ -554,9 +554,12 @@
Cleanup orphaned jobs

Delete jobs that are no longer linked to an existing customer. Related emails and runs will be permanently deleted from the database.

-
- -
+
+ Preview orphaned jobs +
+ +
+
diff --git a/containers/backupchecks/src/templates/main/settings_orphaned_jobs.html b/containers/backupchecks/src/templates/main/settings_orphaned_jobs.html new file mode 100644 index 0000000..1933db5 --- /dev/null +++ b/containers/backupchecks/src/templates/main/settings_orphaned_jobs.html @@ -0,0 +1,87 @@ +{% extends "layout/base.html" %} + +{% block title %}Orphaned Jobs Preview{% endblock %} + +{% block content %} +
+
+
+

Orphaned Jobs Preview

+

Jobs without a valid customer link

+
+ Back to Settings +
+ + {% if orphaned_jobs %} +
+ ⚠️ Warning: Found {{ orphaned_jobs|length }} orphaned job(s). Review the list below before deleting. +
+ +
+
+ Orphaned Jobs List +
+ +
+
+
+
+ + + + + + + + + + + + + {% for job in orphaned_jobs %} + + + + + + + + + {% endfor %} + + + + + + + + +
Job NameBackup SoftwareBackup TypeCustomer IDRunsEmails
{{ job.job_name }}{{ job.backup_software }}{{ job.backup_type }} + {% if job.customer_id %} + {{ job.customer_id }} (deleted) + {% else %} + NULL + {% endif %} + {{ job.run_count }}{{ job.mail_count }}
Total{{ orphaned_jobs|sum(attribute='run_count') }}{{ orphaned_jobs|sum(attribute='mail_count') }}
+
+
+
+ +
+ ℹ️ What will be deleted: +
    +
  • {{ orphaned_jobs|length }} job(s)
  • +
  • {{ orphaned_jobs|sum(attribute='run_count') }} job run(s)
  • +
  • {{ orphaned_jobs|sum(attribute='mail_count') }} email(s)
  • +
  • All related data (backup objects, ticket/remark links, scopes, overrides)
  • +
+
+ + {% else %} +
+ ✅ No orphaned jobs found. +

All jobs are properly linked to existing customers.

+
+ {% endif %} +
+{% endblock %} diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index 0798f7c..f46ffe1 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -6,6 +6,7 @@ This file documents all changes made to this project via Claude Code. ### Added - Added "Cleanup orphaned jobs" maintenance option in Settings → Maintenance to delete jobs without valid customer links and their associated emails/runs permanently from database (useful when customers are removed) +- Added "Preview orphaned jobs" button to show detailed list of jobs to be deleted with run/email counts before confirming deletion (verification step for safety) ### Changed - Removed customer name from Autotask ticket title to keep titles concise (format changed from "[Backupchecks] Customer - Job Name - Status" to "[Backupchecks] Job Name - Status") From e932fdf30a306feb5b9452be4473e3eb726517cd Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 14:10:33 +0100 Subject: [PATCH 07/18] Remove direct delete button, enforce preview step Removed 'Delete orphaned jobs' button from maintenance page to enforce verification workflow. Users must now: 1. Click 'Preview orphaned jobs' to see the list 2. Verify which jobs will be deleted 3. Click 'Delete All' on the preview page This prevents accidental deletion without verification. Co-Authored-By: Claude Sonnet 4.5 --- containers/backupchecks/src/templates/main/settings.html | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/containers/backupchecks/src/templates/main/settings.html b/containers/backupchecks/src/templates/main/settings.html index ab581fa..f53fe36 100644 --- a/containers/backupchecks/src/templates/main/settings.html +++ b/containers/backupchecks/src/templates/main/settings.html @@ -554,12 +554,8 @@
Cleanup orphaned jobs

Delete jobs that are no longer linked to an existing customer. Related emails and runs will be permanently deleted from the database.

-
- Preview orphaned jobs -
- -
-
+

Step 1: Preview the list to verify which jobs will be deleted.

+ Preview orphaned jobs
From 82fff08ebbc304f7276852fa354eaaaf04589504 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 14:11:52 +0100 Subject: [PATCH 08/18] Remove redundant Step 1 text from maintenance card Co-Authored-By: Claude Sonnet 4.5 --- containers/backupchecks/src/templates/main/settings.html | 1 - 1 file changed, 1 deletion(-) diff --git a/containers/backupchecks/src/templates/main/settings.html b/containers/backupchecks/src/templates/main/settings.html index f53fe36..7d9df5b 100644 --- a/containers/backupchecks/src/templates/main/settings.html +++ b/containers/backupchecks/src/templates/main/settings.html @@ -554,7 +554,6 @@
Cleanup orphaned jobs

Delete jobs that are no longer linked to an existing customer. Related emails and runs will be permanently deleted from the database.

-

Step 1: Preview the list to verify which jobs will be deleted.

Preview orphaned jobs
From f332e612883662ceca47137d83950a083d8828d3 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 14:14:17 +0100 Subject: [PATCH 09/18] Fix foreign key constraint error when deleting orphaned jobs Moved mail deletion to after job deletion to avoid foreign key constraint violations. The job_runs have a foreign key to mail_messages, so jobs (and their cascaded runs) must be deleted first before the mails can be deleted. Correct order: 1. Clean up auxiliary tables (ticket_job_runs, remark_job_runs, etc) 2. Delete jobs (cascades to runs via ORM) 3. Delete mails (no more foreign key references) Co-Authored-By: Claude Sonnet 4.5 --- .../src/backend/app/main/routes_settings.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/containers/backupchecks/src/backend/app/main/routes_settings.py b/containers/backupchecks/src/backend/app/main/routes_settings.py index e2a4038..29e4cb9 100644 --- a/containers/backupchecks/src/backend/app/main/routes_settings.py +++ b/containers/backupchecks/src/backend/app/main/routes_settings.py @@ -197,11 +197,6 @@ def settings_jobs_delete_orphaned(): if run.mail_message_id: mail_message_ids.append(run.mail_message_id) - # Delete related mails permanently (customer is gone) - if mail_message_ids: - mail_count = len(mail_message_ids) - MailMessage.query.filter(MailMessage.id.in_(mail_message_ids)).delete(synchronize_session=False) - # Helper function for safe SQL execution def _safe_execute(stmt, params): try: @@ -252,6 +247,12 @@ def settings_jobs_delete_orphaned(): for job in orphaned_jobs: db.session.delete(job) + # Now delete related mails permanently (customer is gone) + # This must happen AFTER deleting jobs/runs to avoid foreign key constraint violations + if mail_message_ids: + mail_count = len(mail_message_ids) + MailMessage.query.filter(MailMessage.id.in_(mail_message_ids)).delete(synchronize_session=False) + db.session.commit() flash( From ff4942272ffda280140941f10303f74a591979c5 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 14:25:13 +0100 Subject: [PATCH 10/18] Fix foreign key constraint: unlink mails from jobs before deletion Added UPDATE to set mail_messages.job_id = NULL before deleting jobs to avoid foreign key constraint violation. The mail_messages table has a foreign key to jobs, so we must unlink them first. Complete correct order: 1. Clean up auxiliary tables 2. Unlink mails from jobs (SET job_id = NULL) 3. Delete jobs (cascades to runs) 4. Delete mails Co-Authored-By: Claude Sonnet 4.5 --- .../backupchecks/src/backend/app/main/routes_settings.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/containers/backupchecks/src/backend/app/main/routes_settings.py b/containers/backupchecks/src/backend/app/main/routes_settings.py index 29e4cb9..bbcd119 100644 --- a/containers/backupchecks/src/backend/app/main/routes_settings.py +++ b/containers/backupchecks/src/backend/app/main/routes_settings.py @@ -243,6 +243,15 @@ def settings_jobs_delete_orphaned(): {"job_ids": job_ids}, ) + # Unlink mails from jobs before deleting jobs + # mail_messages.job_id references jobs.id + _safe_execute( + text("UPDATE mail_messages SET job_id = NULL WHERE job_id IN :job_ids").bindparams( + bindparam("job_ids", expanding=True) + ), + {"job_ids": job_ids}, + ) + # Delete all orphaned jobs (runs/objects are cascaded via ORM relationships) for job in orphaned_jobs: db.session.delete(job) From 710aba97e461bef2aaf2778cf7d77787c42cc205 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 14:26:53 +0100 Subject: [PATCH 11/18] Fix foreign key constraint: delete mail_objects before mails Added deletion of mail_objects before deleting mail_messages to avoid foreign key constraint violation. The mail_objects table has a foreign key to mail_messages. Complete deletion order: 1. Clean up auxiliary tables 2. Unlink mails from jobs 3. Delete mail_objects 4. Delete jobs (cascades to runs) 5. Delete mails Co-Authored-By: Claude Sonnet 4.5 --- .../src/backend/app/main/routes_settings.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/containers/backupchecks/src/backend/app/main/routes_settings.py b/containers/backupchecks/src/backend/app/main/routes_settings.py index bbcd119..9b5fd96 100644 --- a/containers/backupchecks/src/backend/app/main/routes_settings.py +++ b/containers/backupchecks/src/backend/app/main/routes_settings.py @@ -252,6 +252,17 @@ def settings_jobs_delete_orphaned(): {"job_ids": job_ids}, ) + # Delete mail_objects before deleting mails + # mail_objects.mail_message_id references mail_messages.id + if mail_message_ids: + from sqlalchemy import text, bindparam + _safe_execute( + text("DELETE FROM mail_objects WHERE mail_message_id IN :mail_ids").bindparams( + bindparam("mail_ids", expanding=True) + ), + {"mail_ids": mail_message_ids}, + ) + # Delete all orphaned jobs (runs/objects are cascaded via ORM relationships) for job in orphaned_jobs: db.session.delete(job) From 08437aff7ff47159fa9d58772ceb5a8ccaa5a1b1 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 14:31:34 +0100 Subject: [PATCH 12/18] Fix audit logging call for orphaned jobs deletion Added missing 'message' parameter to _log_admin_event call and converted details dict to JSON string to match the function signature. Co-Authored-By: Claude Sonnet 4.5 --- .../backupchecks/src/backend/app/main/routes_settings.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/containers/backupchecks/src/backend/app/main/routes_settings.py b/containers/backupchecks/src/backend/app/main/routes_settings.py index 9b5fd96..3de4394 100644 --- a/containers/backupchecks/src/backend/app/main/routes_settings.py +++ b/containers/backupchecks/src/backend/app/main/routes_settings.py @@ -282,11 +282,12 @@ def settings_jobs_delete_orphaned(): _log_admin_event( event_type="maintenance_delete_orphaned_jobs", - details={ + message=f"Deleted {job_count} orphaned jobs, {run_count} runs, and {mail_count} emails", + details=json.dumps({ "jobs_deleted": job_count, "runs_deleted": run_count, "mails_deleted": mail_count, - }, + }), ) except Exception as exc: From 96092517b4e39a674183adf49320f7f87da29630 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 14:37:02 +0100 Subject: [PATCH 13/18] Add test email generator for testing and development MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added feature to generate test emails in inbox for testing purposes: - Simulates backup notifications from Veeam, Synology, and NAKIVO - Configurable count (1-50 emails) - Random job names, statuses, and timestamps - Emails are parser-compatible for testing inbox approval workflow - Useful for testing orphaned jobs cleanup and other maintenance ops - Admin-only feature in Settings → Maintenance Templates include: - Veeam: Various job statuses with detailed backup info - Synology: Backup task notifications - NAKIVO: Job completion reports Co-Authored-By: Claude Sonnet 4.5 --- .../src/backend/app/main/routes_settings.py | 154 ++++++++++++++++++ .../src/templates/main/settings.html | 18 ++ docs/changelog-claude.md | 1 + 3 files changed, 173 insertions(+) diff --git a/containers/backupchecks/src/backend/app/main/routes_settings.py b/containers/backupchecks/src/backend/app/main/routes_settings.py index 3de4394..201710c 100644 --- a/containers/backupchecks/src/backend/app/main/routes_settings.py +++ b/containers/backupchecks/src/backend/app/main/routes_settings.py @@ -298,6 +298,160 @@ def settings_jobs_delete_orphaned(): return redirect(url_for("main.settings", section="maintenance")) +@main_bp.route("/settings/test-emails/generate", methods=["POST"]) +@login_required +@roles_required("admin") +def settings_generate_test_emails(): + """Generate test emails in inbox for testing parsers and orphaned jobs cleanup.""" + try: + count_str = request.form.get("count", "5") + try: + count = int(count_str) + count = max(1, min(count, 50)) # Limit between 1 and 50 + except Exception: + count = 5 + + from datetime import datetime, timedelta + import random + + # Template configurations for different backup software + templates = [ + { + "software": "Veeam", + "sender": "veeam@test.local", + "subject_template": "Backup job '{job}' completed {status}", + "body_template": """Job name: {job} +Status: {status} +Start time: {start_time} +End time: {end_time} +Total size: {size} GB +Objects processed: {objects} + +{message} +""", + "statuses": ["successfully", "with warnings", "failed"], + }, + { + "software": "Synology", + "sender": "synology@test.local", + "subject_template": "[Synology] Backup Task {job} {status}", + "body_template": """Dear Administrator, + +Backup task '{job}' has {status}. + +Task: {job} +Status: {status} +Start: {start_time} +Finish: {end_time} +Data transferred: {size} GB + +{message} +""", + "statuses": ["completed successfully", "completed with warnings", "failed"], + }, + { + "software": "NAKIVO", + "sender": "nakivo@test.local", + "subject_template": "Job '{job}' finished {status}", + "body_template": """NAKIVO Backup & Replication + +Job: {job} +Status: {status} +Started: {start_time} +Completed: {end_time} +Size: {size} GB + +{message} +""", + "statuses": ["successfully", "with warnings", "with errors"], + }, + ] + + job_names = [ + "Daily VM Backup", + "Weekly File Server", + "SQL Database Backup", + "Exchange Mailbox", + "Critical Servers", + "Development Environment", + "Production Backup", + "Archive Job", + ] + + messages = { + "successfully": "All backup operations completed without issues.", + "with warnings": "Backup completed but some files were skipped.", + "failed": "Backup failed. Please check the logs for details.", + "with errors": "Some backup objects failed to process.", + } + + created_count = 0 + now = datetime.utcnow() + + for i in range(count): + template = random.choice(templates) + job_name = random.choice(job_names) + status = random.choice(template["statuses"]) + + start_time = now - timedelta(hours=random.randint(1, 24), minutes=random.randint(0, 59)) + end_time = start_time + timedelta(minutes=random.randint(5, 120)) + size = random.randint(10, 500) + objects_count = random.randint(5, 50) + + # Find message for status + message = messages.get(status, messages.get("successfully")) + for key in messages: + if key in status: + message = messages[key] + break + + subject = template["subject_template"].format( + job=job_name, + status=status, + ) + + body = template["body_template"].format( + job=job_name, + status=status, + start_time=start_time.strftime("%Y-%m-%d %H:%M:%S"), + end_time=end_time.strftime("%Y-%m-%d %H:%M:%S"), + size=size, + objects=objects_count, + message=message, + ) + + # Create mail message in inbox + mail = MailMessage( + sender=template["sender"], + subject=subject, + body=body, + text_body=body, + html_body=f"
{body}
", + received_at=start_time, + location="inbox", + job_id=None, + ) + db.session.add(mail) + created_count += 1 + + db.session.commit() + + flash(f"Generated {created_count} test email(s) in inbox.", "success") + + _log_admin_event( + event_type="maintenance_generate_test_emails", + message=f"Generated {created_count} test emails", + details=json.dumps({"count": created_count}), + ) + + except Exception as exc: + db.session.rollback() + print(f"[settings-test] Failed to generate test emails: {exc}") + flash("Failed to generate test emails.", "danger") + + return redirect(url_for("main.settings", section="maintenance")) + + @main_bp.route("/settings/objects/backfill", methods=["POST"]) @login_required @roles_required("admin") diff --git a/containers/backupchecks/src/templates/main/settings.html b/containers/backupchecks/src/templates/main/settings.html index 7d9df5b..3f768b9 100644 --- a/containers/backupchecks/src/templates/main/settings.html +++ b/containers/backupchecks/src/templates/main/settings.html @@ -559,6 +559,24 @@ +
+
+
Generate test emails
+
+

Generate test emails in the inbox for testing parsers and maintenance operations. Emails simulate Veeam, Synology, and NAKIVO backups.

+
+
+ + +
+
+ +
+
+
+
+
+
Jobs maintenance
diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index f46ffe1..03255e3 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -7,6 +7,7 @@ This file documents all changes made to this project via Claude Code. ### Added - Added "Cleanup orphaned jobs" maintenance option in Settings → Maintenance to delete jobs without valid customer links and their associated emails/runs permanently from database (useful when customers are removed) - Added "Preview orphaned jobs" button to show detailed list of jobs to be deleted with run/email counts before confirming deletion (verification step for safety) +- Added "Generate test emails" feature in Settings → Maintenance to create test emails in inbox that simulate Veeam, Synology, and NAKIVO backup notifications (useful for testing parsers, orphaned jobs cleanup, and other maintenance operations) ### Changed - Removed customer name from Autotask ticket title to keep titles concise (format changed from "[Backupchecks] Customer - Job Name - Status" to "[Backupchecks] Job Name - Status") From e187bc3fa50fa8dee2a32abdfdfdaad0bb089763 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 14:37:36 +0100 Subject: [PATCH 14/18] Auto-commit local changes before build (2026-02-09 14:37:36) --- .last-branch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.last-branch b/.last-branch index d27683e..d0a509c 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260209-03-cleanup-orphaned-jobs +v20260209-04-test-email-generator From 19ef9dc32adef3014f0710fd14ca97962ed15bba Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 14:44:19 +0100 Subject: [PATCH 15/18] Update test email generator with fixed sets and separate buttons Changed from configurable count input to three separate buttons for success, warning, and error test emails. Each button generates exactly 3 emails with consistent data for reproducible testing. Changes: - Updated routes_settings.py to use fixed email sets instead of random data - Changed route from /settings/test-emails/generate to /settings/test-emails/generate/ - Created three predefined email sets (success, warning, error) with fixed content - Updated settings.html UI to show three separate buttons instead of count input - Each set contains 3 emails simulating Veeam, Synology, and NAKIVO backups - Updated changelog with detailed description Co-Authored-By: Claude Sonnet 4.5 --- .../src/backend/app/main/routes_settings.py | 254 ++++++++++-------- .../src/templates/main/settings.html | 22 +- docs/changelog-claude.md | 2 +- 3 files changed, 153 insertions(+), 125 deletions(-) diff --git a/containers/backupchecks/src/backend/app/main/routes_settings.py b/containers/backupchecks/src/backend/app/main/routes_settings.py index 201710c..df989d7 100644 --- a/containers/backupchecks/src/backend/app/main/routes_settings.py +++ b/containers/backupchecks/src/backend/app/main/routes_settings.py @@ -298,136 +298,162 @@ def settings_jobs_delete_orphaned(): return redirect(url_for("main.settings", section="maintenance")) -@main_bp.route("/settings/test-emails/generate", methods=["POST"]) +@main_bp.route("/settings/test-emails/generate/", methods=["POST"]) @login_required @roles_required("admin") -def settings_generate_test_emails(): - """Generate test emails in inbox for testing parsers and orphaned jobs cleanup.""" +def settings_generate_test_emails(status_type): + """Generate test emails in inbox for testing parsers and orphaned jobs cleanup. + + Fixed sets for consistent testing and reproducibility. + """ try: - count_str = request.form.get("count", "5") - try: - count = int(count_str) - count = max(1, min(count, 50)) # Limit between 1 and 50 - except Exception: - count = 5 - from datetime import datetime, timedelta - import random - # Template configurations for different backup software - templates = [ - { - "software": "Veeam", - "sender": "veeam@test.local", - "subject_template": "Backup job '{job}' completed {status}", - "body_template": """Job name: {job} -Status: {status} -Start time: {start_time} -End time: {end_time} -Total size: {size} GB -Objects processed: {objects} + # Fixed test email sets per status type + email_sets = { + "success": [ + { + "from_address": "veeam@test.local", + "subject": "Backup job 'Daily VM Backup' completed successfully", + "body": """Job name: Daily VM Backup +Status: Success +Start time: 2026-02-09 01:00:00 +End time: 2026-02-09 02:15:00 +Total size: 150 GB +Objects processed: 25 -{message} -""", - "statuses": ["successfully", "with warnings", "failed"], - }, - { - "software": "Synology", - "sender": "synology@test.local", - "subject_template": "[Synology] Backup Task {job} {status}", - "body_template": """Dear Administrator, +All backup operations completed without issues.""", + }, + { + "from_address": "synology@test.local", + "subject": "[Synology] Backup Task SQL Database Backup completed successfully", + "body": """Dear Administrator, -Backup task '{job}' has {status}. +Backup task 'SQL Database Backup' has completed successfully. -Task: {job} -Status: {status} -Start: {start_time} -Finish: {end_time} -Data transferred: {size} GB +Task: SQL Database Backup +Status: Success +Start: 2026-02-09 02:00:00 +Finish: 2026-02-09 02:45:00 +Data transferred: 75 GB -{message} -""", - "statuses": ["completed successfully", "completed with warnings", "failed"], - }, - { - "software": "NAKIVO", - "sender": "nakivo@test.local", - "subject_template": "Job '{job}' finished {status}", - "body_template": """NAKIVO Backup & Replication +All backup operations completed without issues.""", + }, + { + "from_address": "nakivo@test.local", + "subject": "Job 'Exchange Mailbox' finished successfully", + "body": """NAKIVO Backup & Replication -Job: {job} -Status: {status} -Started: {start_time} -Completed: {end_time} -Size: {size} GB +Job: Exchange Mailbox +Status: Success +Started: 2026-02-09 03:00:00 +Completed: 2026-02-09 03:30:00 +Size: 50 GB -{message} -""", - "statuses": ["successfully", "with warnings", "with errors"], - }, - ] +All backup operations completed without issues.""", + }, + ], + "warning": [ + { + "from_address": "veeam@test.local", + "subject": "Backup job 'Weekly File Server' completed with warnings", + "body": """Job name: Weekly File Server +Status: Warning +Start time: 2026-02-09 01:00:00 +End time: 2026-02-09 02:30:00 +Total size: 200 GB +Objects processed: 35 - job_names = [ - "Daily VM Backup", - "Weekly File Server", - "SQL Database Backup", - "Exchange Mailbox", - "Critical Servers", - "Development Environment", - "Production Backup", - "Archive Job", - ] +Backup completed but some files were skipped.""", + }, + { + "from_address": "synology@test.local", + "subject": "[Synology] Backup Task Critical Servers completed with warnings", + "body": """Dear Administrator, - messages = { - "successfully": "All backup operations completed without issues.", - "with warnings": "Backup completed but some files were skipped.", - "failed": "Backup failed. Please check the logs for details.", - "with errors": "Some backup objects failed to process.", +Backup task 'Critical Servers' has completed with warnings. + +Task: Critical Servers +Status: Warning +Start: 2026-02-09 02:00:00 +Finish: 2026-02-09 03:00:00 +Data transferred: 300 GB + +Backup completed but some files were skipped.""", + }, + { + "from_address": "nakivo@test.local", + "subject": "Job 'Production Backup' finished with warnings", + "body": """NAKIVO Backup & Replication + +Job: Production Backup +Status: Warning +Started: 2026-02-09 03:00:00 +Completed: 2026-02-09 04:00:00 +Size: 250 GB + +Some backup objects were skipped.""", + }, + ], + "error": [ + { + "from_address": "veeam@test.local", + "subject": "Backup job 'Development Environment' failed", + "body": """Job name: Development Environment +Status: Failed +Start time: 2026-02-09 01:00:00 +End time: 2026-02-09 01:15:00 +Total size: 0 GB +Objects processed: 0 + +Backup failed. Please check the logs for details.""", + }, + { + "from_address": "synology@test.local", + "subject": "[Synology] Backup Task Archive Job failed", + "body": """Dear Administrator, + +Backup task 'Archive Job' has failed. + +Task: Archive Job +Status: Failed +Start: 2026-02-09 02:00:00 +Finish: 2026-02-09 02:05:00 +Data transferred: 0 GB + +Backup failed. Please check the logs for details.""", + }, + { + "from_address": "nakivo@test.local", + "subject": "Job 'Critical Servers' finished with errors", + "body": """NAKIVO Backup & Replication + +Job: Critical Servers +Status: Failed +Started: 2026-02-09 03:00:00 +Completed: 2026-02-09 03:10:00 +Size: 0 GB + +Some backup objects failed to process.""", + }, + ], } + if status_type not in email_sets: + flash("Invalid status type.", "danger") + return redirect(url_for("main.settings", section="maintenance")) + + emails = email_sets[status_type] created_count = 0 now = datetime.utcnow() - for i in range(count): - template = random.choice(templates) - job_name = random.choice(job_names) - status = random.choice(template["statuses"]) - - start_time = now - timedelta(hours=random.randint(1, 24), minutes=random.randint(0, 59)) - end_time = start_time + timedelta(minutes=random.randint(5, 120)) - size = random.randint(10, 500) - objects_count = random.randint(5, 50) - - # Find message for status - message = messages.get(status, messages.get("successfully")) - for key in messages: - if key in status: - message = messages[key] - break - - subject = template["subject_template"].format( - job=job_name, - status=status, - ) - - body = template["body_template"].format( - job=job_name, - status=status, - start_time=start_time.strftime("%Y-%m-%d %H:%M:%S"), - end_time=end_time.strftime("%Y-%m-%d %H:%M:%S"), - size=size, - objects=objects_count, - message=message, - ) - - # Create mail message in inbox + for email_data in emails: mail = MailMessage( - sender=template["sender"], - subject=subject, - body=body, - text_body=body, - html_body=f"
{body}
", - received_at=start_time, + from_address=email_data["from_address"], + subject=email_data["subject"], + text_body=email_data["body"], + html_body=f"
{email_data['body']}
", + received_at=now - timedelta(hours=created_count), location="inbox", job_id=None, ) @@ -436,12 +462,12 @@ Size: {size} GB db.session.commit() - flash(f"Generated {created_count} test email(s) in inbox.", "success") + flash(f"Generated {created_count} {status_type} test email(s) in inbox.", "success") _log_admin_event( event_type="maintenance_generate_test_emails", - message=f"Generated {created_count} test emails", - details=json.dumps({"count": created_count}), + message=f"Generated {created_count} {status_type} test emails", + details=json.dumps({"status_type": status_type, "count": created_count}), ) except Exception as exc: diff --git a/containers/backupchecks/src/templates/main/settings.html b/containers/backupchecks/src/templates/main/settings.html index 3f768b9..2254fc6 100644 --- a/containers/backupchecks/src/templates/main/settings.html +++ b/containers/backupchecks/src/templates/main/settings.html @@ -563,16 +563,18 @@
Generate test emails
-

Generate test emails in the inbox for testing parsers and maintenance operations. Emails simulate Veeam, Synology, and NAKIVO backups.

-
-
- - -
-
- -
-
+

Generate fixed test email sets in the inbox for testing parsers and maintenance operations. Each set contains 3 emails simulating Veeam, Synology, and NAKIVO backups.

+
+
+ +
+
+ +
+
+ +
+
diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index 03255e3..2334075 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -7,7 +7,7 @@ This file documents all changes made to this project via Claude Code. ### Added - Added "Cleanup orphaned jobs" maintenance option in Settings → Maintenance to delete jobs without valid customer links and their associated emails/runs permanently from database (useful when customers are removed) - Added "Preview orphaned jobs" button to show detailed list of jobs to be deleted with run/email counts before confirming deletion (verification step for safety) -- Added "Generate test emails" feature in Settings → Maintenance to create test emails in inbox that simulate Veeam, Synology, and NAKIVO backup notifications (useful for testing parsers, orphaned jobs cleanup, and other maintenance operations) +- Added "Generate test emails" feature in Settings → Maintenance with three separate buttons to create fixed test email sets (success/warning/error) in inbox for testing parsers and maintenance operations (each set contains exactly 3 emails simulating Veeam, Synology, and NAKIVO backup notifications with consistent data for reproducible testing) ### Changed - Removed customer name from Autotask ticket title to keep titles concise (format changed from "[Backupchecks] Customer - Job Name - Status" to "[Backupchecks] Job Name - Status") From 26848998e14770668d21180bdbde56b85ce29260 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 14:54:42 +0100 Subject: [PATCH 16/18] Fix test email generator to use correct Veeam format and consistent job name Changed test emails to use proper Veeam Backup Job format that matches parser expectations. All test emails now use the same job name "Test-Backup-Job" so they appear as different runs of the same job, enabling proper status testing. Changes: - Switched from multiple backup software to Veeam only for simplicity - Fixed subject format to: Veeam Backup Job "Test-Backup-Job" finished with Success/WARNING/Failed - Fixed body format to include: Backup job: Test-Backup-Job - All 3 emails per set use same job name but different dates - Added realistic VM objects (VM-APP01, VM-DB01, VM-WEB01) with status details - Each set shows different failure scenarios for testing - Updated changelog description Co-Authored-By: Claude Sonnet 4.5 --- .../src/backend/app/main/routes_settings.py | 224 ++++++++++++------ docs/changelog-claude.md | 2 +- 2 files changed, 156 insertions(+), 70 deletions(-) diff --git a/containers/backupchecks/src/backend/app/main/routes_settings.py b/containers/backupchecks/src/backend/app/main/routes_settings.py index df989d7..5fde857 100644 --- a/containers/backupchecks/src/backend/app/main/routes_settings.py +++ b/containers/backupchecks/src/backend/app/main/routes_settings.py @@ -309,46 +309,74 @@ def settings_generate_test_emails(status_type): try: from datetime import datetime, timedelta - # Fixed test email sets per status type + # Fixed test email sets per status type (Veeam only for consistent testing) + # All emails use the same job name "Test-Backup-Job" so they appear as different + # runs of the same job for proper status testing email_sets = { "success": [ { "from_address": "veeam@test.local", - "subject": "Backup job 'Daily VM Backup' completed successfully", - "body": """Job name: Daily VM Backup -Status: Success + "subject": 'Veeam Backup Job "Test-Backup-Job" finished with Success', + "body": """Backup job: Test-Backup-Job + +Session details: Start time: 2026-02-09 01:00:00 End time: 2026-02-09 02:15:00 Total size: 150 GB -Objects processed: 25 +Duration: 01:15:00 + +Processing VM-APP01 +Success + +Processing VM-DB01 +Success + +Processing VM-WEB01 +Success All backup operations completed without issues.""", }, { - "from_address": "synology@test.local", - "subject": "[Synology] Backup Task SQL Database Backup completed successfully", - "body": """Dear Administrator, + "from_address": "veeam@test.local", + "subject": 'Veeam Backup Job "Test-Backup-Job" finished with Success', + "body": """Backup job: Test-Backup-Job -Backup task 'SQL Database Backup' has completed successfully. +Session details: +Start time: 2026-02-08 01:00:00 +End time: 2026-02-08 02:10:00 +Total size: 145 GB +Duration: 01:10:00 -Task: SQL Database Backup -Status: Success -Start: 2026-02-09 02:00:00 -Finish: 2026-02-09 02:45:00 -Data transferred: 75 GB +Processing VM-APP01 +Success + +Processing VM-DB01 +Success + +Processing VM-WEB01 +Success All backup operations completed without issues.""", }, { - "from_address": "nakivo@test.local", - "subject": "Job 'Exchange Mailbox' finished successfully", - "body": """NAKIVO Backup & Replication + "from_address": "veeam@test.local", + "subject": 'Veeam Backup Job "Test-Backup-Job" finished with Success', + "body": """Backup job: Test-Backup-Job -Job: Exchange Mailbox -Status: Success -Started: 2026-02-09 03:00:00 -Completed: 2026-02-09 03:30:00 -Size: 50 GB +Session details: +Start time: 2026-02-07 01:00:00 +End time: 2026-02-07 02:20:00 +Total size: 152 GB +Duration: 01:20:00 + +Processing VM-APP01 +Success + +Processing VM-DB01 +Success + +Processing VM-WEB01 +Success All backup operations completed without issues.""", }, @@ -356,85 +384,143 @@ All backup operations completed without issues.""", "warning": [ { "from_address": "veeam@test.local", - "subject": "Backup job 'Weekly File Server' completed with warnings", - "body": """Job name: Weekly File Server -Status: Warning + "subject": 'Veeam Backup Job "Test-Backup-Job" finished with WARNING', + "body": """Backup job: Test-Backup-Job + +Session details: Start time: 2026-02-09 01:00:00 End time: 2026-02-09 02:30:00 -Total size: 200 GB -Objects processed: 35 +Total size: 148 GB +Duration: 01:30:00 + +Processing VM-APP01 +Success + +Processing VM-DB01 +Warning +Warning: Low free space on target datastore + +Processing VM-WEB01 +Success Backup completed but some files were skipped.""", }, { - "from_address": "synology@test.local", - "subject": "[Synology] Backup Task Critical Servers completed with warnings", - "body": """Dear Administrator, + "from_address": "veeam@test.local", + "subject": 'Veeam Backup Job "Test-Backup-Job" finished with WARNING', + "body": """Backup job: Test-Backup-Job -Backup task 'Critical Servers' has completed with warnings. +Session details: +Start time: 2026-02-08 01:00:00 +End time: 2026-02-08 02:25:00 +Total size: 142 GB +Duration: 01:25:00 -Task: Critical Servers -Status: Warning -Start: 2026-02-09 02:00:00 -Finish: 2026-02-09 03:00:00 -Data transferred: 300 GB +Processing VM-APP01 +Warning +Warning: Retry was successful after initial failure -Backup completed but some files were skipped.""", +Processing VM-DB01 +Success + +Processing VM-WEB01 +Success + +Backup completed with warnings.""", }, { - "from_address": "nakivo@test.local", - "subject": "Job 'Production Backup' finished with warnings", - "body": """NAKIVO Backup & Replication + "from_address": "veeam@test.local", + "subject": 'Veeam Backup Job "Test-Backup-Job" finished with WARNING', + "body": """Backup job: Test-Backup-Job -Job: Production Backup -Status: Warning -Started: 2026-02-09 03:00:00 -Completed: 2026-02-09 04:00:00 -Size: 250 GB +Session details: +Start time: 2026-02-07 01:00:00 +End time: 2026-02-07 02:35:00 +Total size: 140 GB +Duration: 01:35:00 -Some backup objects were skipped.""", +Processing VM-APP01 +Success + +Processing VM-DB01 +Success + +Processing VM-WEB01 +Warning +Warning: Some files were locked and skipped + +Backup completed with warnings.""", }, ], "error": [ { "from_address": "veeam@test.local", - "subject": "Backup job 'Development Environment' failed", - "body": """Job name: Development Environment -Status: Failed + "subject": 'Veeam Backup Job "Test-Backup-Job" finished with Failed', + "body": """Backup job: Test-Backup-Job + +Session details: Start time: 2026-02-09 01:00:00 End time: 2026-02-09 01:15:00 Total size: 0 GB -Objects processed: 0 +Duration: 00:15:00 + +Processing VM-APP01 +Failed +Error: Cannot create snapshot: VSS error 0x800423f4 + +Processing VM-DB01 +Success + +Processing VM-WEB01 +Success Backup failed. Please check the logs for details.""", }, { - "from_address": "synology@test.local", - "subject": "[Synology] Backup Task Archive Job failed", - "body": """Dear Administrator, + "from_address": "veeam@test.local", + "subject": 'Veeam Backup Job "Test-Backup-Job" finished with Failed', + "body": """Backup job: Test-Backup-Job -Backup task 'Archive Job' has failed. +Session details: +Start time: 2026-02-08 01:00:00 +End time: 2026-02-08 01:10:00 +Total size: 0 GB +Duration: 00:10:00 -Task: Archive Job -Status: Failed -Start: 2026-02-09 02:00:00 -Finish: 2026-02-09 02:05:00 -Data transferred: 0 GB +Processing VM-APP01 +Success -Backup failed. Please check the logs for details.""", +Processing VM-DB01 +Failed +Error: Disk space exhausted on backup repository + +Processing VM-WEB01 +Success + +Backup failed due to storage issue.""", }, { - "from_address": "nakivo@test.local", - "subject": "Job 'Critical Servers' finished with errors", - "body": """NAKIVO Backup & Replication + "from_address": "veeam@test.local", + "subject": 'Veeam Backup Job "Test-Backup-Job" finished with Failed', + "body": """Backup job: Test-Backup-Job -Job: Critical Servers -Status: Failed -Started: 2026-02-09 03:00:00 -Completed: 2026-02-09 03:10:00 -Size: 0 GB +Session details: +Start time: 2026-02-07 01:00:00 +End time: 2026-02-07 01:05:00 +Total size: 0 GB +Duration: 00:05:00 -Some backup objects failed to process.""", +Processing VM-APP01 +Success + +Processing VM-DB01 +Success + +Processing VM-WEB01 +Failed +Error: Network connection lost to ESXi host + +Backup failed. Network issue detected.""", }, ], } diff --git a/docs/changelog-claude.md b/docs/changelog-claude.md index 2334075..5e24347 100644 --- a/docs/changelog-claude.md +++ b/docs/changelog-claude.md @@ -7,7 +7,7 @@ This file documents all changes made to this project via Claude Code. ### Added - Added "Cleanup orphaned jobs" maintenance option in Settings → Maintenance to delete jobs without valid customer links and their associated emails/runs permanently from database (useful when customers are removed) - Added "Preview orphaned jobs" button to show detailed list of jobs to be deleted with run/email counts before confirming deletion (verification step for safety) -- Added "Generate test emails" feature in Settings → Maintenance with three separate buttons to create fixed test email sets (success/warning/error) in inbox for testing parsers and maintenance operations (each set contains exactly 3 emails simulating Veeam, Synology, and NAKIVO backup notifications with consistent data for reproducible testing) +- Added "Generate test emails" feature in Settings → Maintenance with three separate buttons to create fixed test email sets (success/warning/error) in inbox for testing parsers and maintenance operations (each set contains exactly 3 Veeam Backup Job emails with the same job name "Test-Backup-Job" and different dates/objects/statuses for reproducible testing and proper status flow testing) ### Changed - Removed customer name from Autotask ticket title to keep titles concise (format changed from "[Backupchecks] Customer - Job Name - Status" to "[Backupchecks] Job Name - Status") From 9197c311f2cac071e339f9929483980a386e3ce8 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 9 Feb 2026 15:11:47 +0100 Subject: [PATCH 17/18] Fix responsive navbar overlapping content on smaller screens Added dynamic padding adjustment that measures the actual navbar height and applies it to the main content padding-top. This prevents the navbar from overlapping page content when it becomes taller on narrow screens. Changes: - Removed fixed padding-top: 80px from main content - Added id="main-content" to main element for JavaScript targeting - Added JavaScript function that measures navbar.offsetHeight - Function applies dynamic padding-top with 20px buffer for spacing - Triggers on: page load, window load, window resize (debounced), navbar collapse toggle - Includes fallback to 80px if measurement fails - Updated changelog Co-Authored-By: Claude Sonnet 4.5 --- .../src/templates/layout/base.html | 54 ++++++++++++++++++- docs/changelog-claude.md | 1 + 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/containers/backupchecks/src/templates/layout/base.html b/containers/backupchecks/src/templates/layout/base.html index 346069b..4a06250 100644 --- a/containers/backupchecks/src/templates/layout/base.html +++ b/containers/backupchecks/src/templates/layout/base.html @@ -197,7 +197,7 @@
-
+
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %}
@@ -216,6 +216,58 @@ + +