Compare commits

..

No commits in common. "702de680987242141a9b4a7aaeb71e2304ba158e" and "64f686e275eb9fff63acf28893af9e2493a25c38" have entirely different histories.

4 changed files with 100 additions and 40 deletions

View File

@ -17,4 +17,4 @@ ENV APP_PORT=8080
EXPOSE 8080
# Use the application factory from backend.app
CMD ["gunicorn", "-b", "0.0.0.0:8080", "--timeout", "120", "backend.app:create_app()"]
CMD ["gunicorn", "-b", "0.0.0.0:8080", "backend.app:create_app()"]

View File

@ -31,7 +31,6 @@ def cloud_connect_accounts():
)
customers = Customer.query.filter_by(active=True).order_by(Customer.name.asc()).all()
customer_rows = [{"id": c.id, "name": c.name} for c in customers]
jobs = Job.query.filter_by(archived=False).order_by(Job.job_name.asc()).all()
# Attach derived fields for the template
@ -46,7 +45,7 @@ def cloud_connect_accounts():
"main/cloud_connect_accounts.html",
unmatched=unmatched,
matched=matched,
customers=customer_rows,
customers=customers,
jobs=jobs,
)

View File

@ -8,57 +8,118 @@ from datetime import datetime
@roles_required("admin")
def settings_jobs_delete_all():
try:
job_count = db.session.execute(text("SELECT COUNT(*) FROM jobs")).scalar() or 0
jobs = Job.query.all()
if not job_count:
if not jobs:
flash("No jobs to delete.", "info")
return redirect(url_for("main.settings"))
return redirect(url_for("main.settings", section="general"))
def _try(stmt):
"""Best-effort: skip if table/column doesn't exist in this schema version."""
# Collect run ids for FK cleanup in auxiliary tables that may not have ON DELETE CASCADE
run_ids = []
mail_message_ids = []
for job in jobs:
for run in job.runs:
if run.id is not None:
run_ids.append(run.id)
if run.mail_message_id:
mail_message_ids.append(run.mail_message_id)
# Return related mails back to inbox and unlink from job
if mail_message_ids:
msgs = MailMessage.query.filter(MailMessage.id.in_(mail_message_ids)).all()
for msg in msgs:
if hasattr(msg, "location"):
msg.location = "inbox"
msg.job_id = None
def _safe_execute(stmt, params):
try:
db.session.execute(text(stmt))
except Exception as exc:
print(f"[settings-jobs] Skipped: {exc}")
db.session.execute(stmt, params)
except Exception as cleanup_exc:
# Best-effort cleanup for differing DB schemas
print(f"[settings-jobs] Cleanup skipped: {cleanup_exc}")
# All deletions via direct SQL — no ORM loading into Python memory.
# Order: unlink → FK-dependent tables → job_runs → jobs.
# 1. Unlink staging accounts that have a nullable FK to jobs.
_try("UPDATE cove_accounts SET job_id = NULL WHERE job_id IS NOT NULL")
_try("UPDATE cloud_connect_accounts SET job_id = NULL WHERE job_id IS NOT NULL")
# Ensure run_object_links doesn't block job_runs deletion (older schemas may miss ON DELETE CASCADE)
if run_ids:
db.session.execute(
text("DELETE FROM run_object_links WHERE run_id IN :run_ids").bindparams(
bindparam("run_ids", expanding=True)
),
{"run_ids": run_ids},
)
# 2. Return mail messages linked to jobs back to the inbox.
db.session.execute(text(
"UPDATE mail_messages SET job_id = NULL, location = 'inbox' WHERE job_id IS NOT NULL"
))
# 3. Delete tables that FK → job_runs.
_try("DELETE FROM remark_job_runs")
_try("DELETE FROM ticket_job_runs")
_try("DELETE FROM run_object_links")
# 4. Delete tables that FK → jobs.
_try("DELETE FROM job_object_links")
_try("DELETE FROM ticket_scopes WHERE job_id IS NOT NULL")
_try("DELETE FROM remark_scopes WHERE job_id IS NOT NULL OR job_run_id IS NOT NULL")
_try("DELETE FROM overrides WHERE job_id IS NOT NULL")
# Ensure job_object_links doesn't block jobs deletion (older schemas may miss ON DELETE CASCADE)
job_ids = [j.id for j in jobs]
if job_ids:
db.session.execute(
text("DELETE FROM job_object_links WHERE job_id IN :job_ids").bindparams(
bindparam("job_ids", expanding=True)
),
{"job_ids": job_ids},
)
# 5. Delete runs, then jobs.
db.session.execute(text("DELETE FROM job_runs"))
db.session.execute(text("DELETE FROM jobs"))
# Clean up auxiliary FK tables that may reference job_runs/jobs without ON DELETE CASCADE (older schemas)
if 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},
)
_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},
)
# Some schemas use remark_scopes for per-run remarks
_safe_execute(
text("DELETE FROM remark_scopes WHERE job_run_id IN :run_ids").bindparams(
bindparam("run_ids", expanding=True)
),
{"run_ids": run_ids},
)
if job_ids:
# ticket_scopes.job_id is a FK without ON DELETE CASCADE in some schemas
_safe_execute(
text("DELETE FROM ticket_scopes WHERE job_id IN :job_ids").bindparams(
bindparam("job_ids", expanding=True)
),
{"job_ids": job_ids},
)
# Some schemas use remark_scopes for per-job remarks
_safe_execute(
text("DELETE FROM remark_scopes WHERE job_id IN :job_ids").bindparams(
bindparam("job_ids", expanding=True)
),
{"job_ids": job_ids},
)
# Overrides may reference jobs directly
_safe_execute(
text("DELETE FROM overrides WHERE job_id IN :job_ids").bindparams(
bindparam("job_ids", expanding=True)
),
{"job_ids": job_ids},
)
# Delete all jobs (runs/objects are cascaded via ORM relationships)
for job in jobs:
db.session.delete(job)
db.session.commit()
_log_admin_event(
event_type="jobs_delete_all",
message=f"Deleted all {job_count} job(s) and all related data.",
)
flash(f"All {job_count} jobs deleted. Related mails are returned to the inbox.", "success")
flash("All jobs deleted. Related mails are returned to the inbox.", "success")
except Exception as exc:
db.session.rollback()
print(f"[settings-jobs] Failed to delete all jobs: {exc}")
flash(f"Failed to delete all jobs: {exc}", "danger")
flash("Failed to delete all jobs.", "danger")
return redirect(url_for("main.settings"))

View File

@ -163,7 +163,7 @@
autocomplete="off" />
<datalist id="ccCustomerList">
{% for c in customers %}
<option value="{{ c.name | e }}"></option>
<option value="{{ c.name }}"></option>
{% endfor %}
</datalist>
</div>