Add cleanup orphaned jobs maintenance option
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 <noreply@anthropic.com>
This commit is contained in:
parent
b0efa7f21d
commit
60c7e89dc2
@ -124,6 +124,123 @@ def settings_jobs_delete_all():
|
|||||||
return redirect(url_for("main.settings"))
|
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"])
|
@main_bp.route("/settings/objects/backfill", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@roles_required("admin")
|
@roles_required("admin")
|
||||||
|
|||||||
@ -549,6 +549,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<div class="card h-100 border-warning">
|
||||||
|
<div class="card-header bg-warning">Cleanup orphaned jobs</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="mb-3">Delete jobs that are no longer linked to an existing customer. Related emails and runs will be <strong>permanently deleted</strong> from the database.</p>
|
||||||
|
<form method="post" action="{{ url_for('main.settings_jobs_delete_orphaned') }}" onsubmit="return confirm('Delete orphaned jobs and their emails? This cannot be undone.');">
|
||||||
|
<button type="submit" class="btn btn-warning">Delete orphaned jobs</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
<div class="card h-100 border-danger">
|
<div class="card h-100 border-danger">
|
||||||
<div class="card-header bg-danger text-white">Jobs maintenance</div>
|
<div class="card-header bg-danger text-white">Jobs maintenance</div>
|
||||||
|
|||||||
@ -4,6 +4,9 @@ This file documents all changes made to this project via Claude Code.
|
|||||||
|
|
||||||
## [2026-02-09]
|
## [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
|
### 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")
|
- Removed customer name from Autotask ticket title to keep titles concise (format changed from "[Backupchecks] Customer - Job Name - Status" to "[Backupchecks] Job Name - Status")
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user