Auto-commit local changes before build (2026-01-16 14:13:31)

This commit is contained in:
Ivo Oskamp 2026-01-16 14:13:31 +01:00
parent ef8d12065b
commit 9025d70b8e
3 changed files with 180 additions and 1 deletions

View File

@ -1 +1 @@
v20260116-08-autotask-ticket-backfill-ticketjobrun v20260116-09-autotask-ticket-propagate-active-runs

View File

@ -143,6 +143,148 @@ def _compose_autotask_ticket_description(
# A run within +/- 1 hour of the inferred schedule time counts as fulfilling the slot. # A run within +/- 1 hour of the inferred schedule time counts as fulfilling the slot.
MISSED_GRACE_WINDOW = timedelta(hours=1) MISSED_GRACE_WINDOW = timedelta(hours=1)
def _propagate_autotask_ticket_to_active_runs(job_id: int) -> None:
"""Ensure Autotask ticket linkage is consistent across all active (unreviewed) runs of a job.
Rules:
- Only affects runs where reviewed_at is NULL (i.e. visible as active on Run Checks).
- If an active Autotask ticket exists on any run for this job, copy it to all other active runs.
- Keep internal Ticket + TicketScope + TicketJobRun relations in sync so Tickets/Remarks and Job Details stay consistent.
- Do nothing if no active ticket exists.
Best-effort: errors must never break page load.
"""
try:
# Find a "source" run with an Autotask ticket among active runs.
source = (
JobRun.query.filter(JobRun.job_id == job_id)
.filter(JobRun.reviewed_at.is_(None))
.filter(JobRun.autotask_ticket_id.isnot(None))
.order_by(func.coalesce(JobRun.run_at, JobRun.created_at).desc(), JobRun.id.desc())
.first()
)
if not source:
return
try:
ticket_id = int(getattr(source, "autotask_ticket_id", None) or 0)
except Exception:
ticket_id = 0
if ticket_id <= 0:
return
ticket_number = (getattr(source, "autotask_ticket_number", None) or "").strip() or None
# Collect all active runs for this job.
active_runs = (
JobRun.query.filter(JobRun.job_id == job_id)
.filter(JobRun.reviewed_at.is_(None))
.order_by(func.coalesce(JobRun.run_at, JobRun.created_at).desc(), JobRun.id.desc())
.all()
)
if not active_runs:
return
# Update run fields (but never overwrite a different ticket).
changed = False
now = datetime.utcnow()
for r in active_runs:
existing = getattr(r, "autotask_ticket_id", None)
if existing is not None:
try:
if int(existing) != int(ticket_id):
continue
except Exception:
continue
if getattr(r, "autotask_ticket_id", None) is None:
try:
r.autotask_ticket_id = int(ticket_id)
changed = True
except Exception:
pass
# Always propagate ticket number if we have one and the run doesn't.
if ticket_number and not (getattr(r, "autotask_ticket_number", None) or "").strip():
r.autotask_ticket_number = ticket_number
changed = True
# Preserve created-by metadata if already present; otherwise best-effort fill.
if getattr(r, "autotask_ticket_created_at", None) is None:
r.autotask_ticket_created_at = now
changed = True
# Ensure internal ticket linkage when we have a ticket number.
if ticket_number:
ticket_code = ticket_number.strip().upper()
job = Job.query.get(job_id)
if job:
internal_ticket = Ticket.query.filter_by(ticket_code=ticket_code).first()
if not internal_ticket:
# Minimal record; title/description may be refined elsewhere.
internal_ticket = Ticket(
ticket_code=ticket_code,
title=f"[Backupchecks] Autotask {ticket_code}",
description="",
active_from_date=_to_amsterdam_date(getattr(source, "run_at", None)) or _to_amsterdam_date(now) or now.date(),
start_date=now,
resolved_at=None,
)
db.session.add(internal_ticket)
db.session.flush()
else:
# Keep it active if it was previously resolved globally.
if internal_ticket.resolved_at is not None:
internal_ticket.resolved_at = None
# If this ticket is already resolved for this job (or globally), do not propagate it to new runs.
if internal_ticket.resolved_at is not None:
return
existing_scope = TicketScope.query.filter_by(ticket_id=internal_ticket.id, scope_type="job", job_id=job.id).first()
if existing_scope and existing_scope.resolved_at is not None:
return
# Ensure a job scope exists.
scope = existing_scope
if not scope:
scope = TicketScope(
ticket_id=internal_ticket.id,
scope_type="job",
customer_id=job.customer_id,
backup_software=job.backup_software,
backup_type=job.backup_type,
job_id=job.id,
job_name_match=job.job_name,
job_name_match_mode="exact",
resolved_at=None,
)
db.session.add(scope)
else:
scope.resolved_at = None
# Link ticket to all active job runs (idempotent).
for r in active_runs:
if not TicketJobRun.query.filter_by(ticket_id=internal_ticket.id, job_run_id=r.id).first():
db.session.add(TicketJobRun(ticket_id=internal_ticket.id, job_run_id=r.id, link_source="autotask"))
if changed:
for r in active_runs:
db.session.add(r)
if changed or ticket_number:
db.session.commit()
except Exception:
try:
db.session.rollback()
except Exception:
pass
return
def _status_is_success(status: str | None) -> bool: def _status_is_success(status: str | None) -> bool:
s = (status or "").strip().lower() s = (status or "").strip().lower()
@ -529,6 +671,27 @@ def run_checks_page():
rows = q.limit(2000).all() rows = q.limit(2000).all()
# Ensure Autotask tickets remain visible on all active runs until the ticket is resolved.
# This also guarantees that newly arrived runs inherit the existing ticket while it is still open.
try:
job_ids_for_page = [int(r.job_id) for r in rows]
if job_ids_for_page and not include_reviewed:
jobs_with_autotask = (
db.session.query(JobRun.job_id)
.filter(JobRun.job_id.in_(job_ids_for_page))
.filter(JobRun.reviewed_at.is_(None))
.filter(JobRun.autotask_ticket_id.isnot(None))
.group_by(JobRun.job_id)
.all()
)
for (jid,) in jobs_with_autotask or []:
try:
_propagate_autotask_ticket_to_active_runs(int(jid))
except Exception:
pass
except Exception:
pass
# Ensure override flags are up-to-date for the runs shown in this overview. # Ensure override flags are up-to-date for the runs shown in this overview.
# The Run Checks modal computes override status on-the-fly, but the overview # The Run Checks modal computes override status on-the-fly, but the overview
# aggregates by persisted JobRun.override_applied. Keep those flags aligned # aggregates by persisted JobRun.override_applied. Keep those flags aligned
@ -731,6 +894,14 @@ def run_checks_details():
runs = q.order_by(func.coalesce(JobRun.run_at, JobRun.created_at).desc(), JobRun.id.desc()).limit(400).all() runs = q.order_by(func.coalesce(JobRun.run_at, JobRun.created_at).desc(), JobRun.id.desc()).limit(400).all()
# Ensure Autotask tickets are consistently linked to all active runs of this job (until resolved).
if not include_reviewed:
_propagate_autotask_ticket_to_active_runs(job.id)
# Re-load runs so the response reflects any propagated ticket fields immediately.
q2 = JobRun.query.filter(JobRun.job_id == job.id)
q2 = q2.filter(JobRun.reviewed_at.is_(None))
runs = q2.order_by(func.coalesce(JobRun.run_at, JobRun.created_at).desc(), JobRun.id.desc()).limit(400).all()
runs_payload = [] runs_payload = []
for run in runs: for run in runs:
msg = MailMessage.query.get(run.mail_message_id) if run.mail_message_id else None msg = MailMessage.query.get(run.mail_message_id) if run.mail_message_id else None

View File

@ -197,6 +197,14 @@ Changes:
- Corrected Job Details visibility so open runs linked to the same ticket now display the ticket number consistently. - Corrected Job Details visibility so open runs linked to the same ticket now display the ticket number consistently.
- Aligned Run Checks, Tickets, and Job Details views to use the same ticket-jobrun linkage logic. - Aligned Run Checks, Tickets, and Job Details views to use the same ticket-jobrun linkage logic.
## v20260116-09-autotask-ticket-propagate-active-runs
- Updated ticket propagation logic so Autotask tickets are linked to all active job runs (non-Reviewed) visible on the Run Checks page.
- Ensured ticket remarks and ticket-jobrun entries are created for each active run, not only the initially selected run.
- Implemented automatic ticket inheritance for newly incoming runs of the same job while the ticket remains unresolved.
- Stopped ticket propagation once the ticket or job is marked as Resolved to prevent incorrect linking to closed incidents.
- Aligned Run Checks, Tickets overview, and Job Details to consistently reflect ticket presence across all active runs.
*** ***
## v0.1.21 ## v0.1.21