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
85621c4f95
@ -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")
|
||||
|
||||
@ -549,6 +549,18 @@
|
||||
</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="card h-100 border-danger">
|
||||
<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]
|
||||
|
||||
### 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")
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user