diff --git a/.last-branch b/.last-branch index 502f598..a1dfda1 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260116-07-autotask-ticket-link-all-runs-ticketjobrun-fix +v20260116-08-autotask-ticket-backfill-ticketjobrun 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 24fbf75..61b37d6 100644 --- a/containers/backupchecks/src/backend/app/main/routes_run_checks.py +++ b/containers/backupchecks/src/backend/app/main/routes_run_checks.py @@ -1000,15 +1000,149 @@ def api_run_checks_create_autotask_ticket(): run = JobRun.query.get(run_id) if not run: return jsonify({"status": "error", "message": "Run not found."}), 404 - - # Idempotent: if already created, return existing linkage. + # Idempotent: if already created, ensure internal Ticket/JobRun links exist and return existing linkage. if getattr(run, "autotask_ticket_id", None): + linked_run_ids: list[int] = [] + try: + rows = ( + JobRun.query.filter(JobRun.job_id == run.job_id) + .filter(JobRun.reviewed_at.is_(None)) + .with_entities(JobRun.id) + .order_by(JobRun.id.asc()) + .all() + ) + linked_run_ids = [int(rid) for (rid,) in rows if rid is not None] + except Exception: + linked_run_ids = [] + + # Safety: always include the explicitly selected run. + try: + if run.id and int(run.id) not in linked_run_ids: + linked_run_ids.append(int(run.id)) + except Exception: + pass + + # Best-effort: backfill internal Ticket + TicketScope + TicketJobRun links so Tickets/Remarks and Job Details stay consistent. + try: + ticket_code = (getattr(run, "autotask_ticket_number", None) or "").strip().upper() + if ticket_code: + job = Job.query.get(run.job_id) + customer = Customer.query.get(job.customer_id) if job and getattr(job, "customer_id", None) else None + + # Compose a sensible title/description (best-effort). + subject = None + description = None + try: + status_display = run.status or "-" + try: + status_display, _, _, _ov_id, _ov_reason = _apply_overrides_to_run(job, run) + except Exception: + status_display = run.status or "-" + + cname = getattr(customer, "name", None) or "" + jname = getattr(job, "job_name", None) or "" + subject = f"[Backupchecks] {cname} - {jname} - {status_display}" + + settings = _get_or_create_settings() + msg = MailMessage.query.get(run.mail_message_id) if run.mail_message_id else None + overall_message = (getattr(msg, "overall_message", None) or "") if msg else "" + + objects_payload: list[dict[str, str]] = [] + try: + objs = run.objects.order_by(JobObject.object_name.asc()).all() + except Exception: + objs = list(run.objects or []) + for o in objs or []: + objects_payload.append( + { + "name": getattr(o, "object_name", "") or "", + "type": getattr(o, "object_type", "") or "", + "status": getattr(o, "status", "") or "", + "error_message": getattr(o, "error_message", "") or "", + } + ) + + if (not objects_payload) and msg: + try: + mos = MailObject.query.filter_by(mail_message_id=msg.id).order_by(MailObject.object_name.asc()).all() + except Exception: + mos = [] + for mo in mos or []: + objects_payload.append( + { + "name": getattr(mo, "object_name", "") or "", + "type": getattr(mo, "object_type", "") or "", + "status": getattr(mo, "status", "") or "", + "error_message": getattr(mo, "error_message", "") or "", + } + ) + + description = _compose_autotask_ticket_description( + settings=settings, + job=job, + run=run, + status_display=status_display, + overall_message=overall_message, + objects_payload=objects_payload, + ) + except Exception: + subject = subject or f"[Backupchecks] Autotask {ticket_code}" + description = description or "" + + internal_ticket = Ticket.query.filter_by(ticket_code=ticket_code).first() + if not internal_ticket: + now = datetime.utcnow() + internal_ticket = Ticket( + ticket_code=ticket_code, + title=subject, + description=description or "", + active_from_date=_to_amsterdam_date(run.run_at) 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 + + # Ensure a job scope exists (used by popups / job details / tickets page) + scope = None + if job and job.id and internal_ticket and internal_ticket.id: + scope = TicketScope.query.filter_by(ticket_id=internal_ticket.id, scope_type="job", job_id=job.id).first() + if not scope and internal_ticket and internal_ticket.id: + scope = TicketScope( + ticket_id=internal_ticket.id, + scope_type="job", + customer_id=job.customer_id if job else None, + backup_software=job.backup_software if job else None, + backup_type=job.backup_type if job else None, + job_id=job.id if job else None, + job_name_match=job.job_name if job else None, + job_name_match_mode="exact", + resolved_at=None, + ) + db.session.add(scope) + elif scope: + scope.resolved_at = None + + # Link ticket to all relevant job runs (idempotent) + for rid in linked_run_ids or []: + if not TicketJobRun.query.filter_by(ticket_id=internal_ticket.id, job_run_id=rid).first(): + db.session.add(TicketJobRun(ticket_id=internal_ticket.id, job_run_id=rid, link_source="autotask")) + + db.session.commit() + except Exception: + db.session.rollback() + return jsonify( { "status": "ok", "ticket_id": int(run.autotask_ticket_id), "ticket_number": getattr(run, "autotask_ticket_number", None) or "", "already_exists": True, + "linked_run_ids": linked_run_ids or [], } ) diff --git a/docs/changelog.md b/docs/changelog.md index cf5724d..957a184 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -189,6 +189,14 @@ Changes: - Made the list of runs to link deterministic by collecting run IDs first, then applying both run field updates and internal ticket linking across that stable set - No changes made to polling logic or PSA status interpretation +## v20260116-08-autotask-ticket-backfill-ticketjobrun + +- Fixed inconsistent ticket linking when creating Autotask tickets from the Run Checks page. +- Ensured that newly created Autotask tickets are linked to all related job runs, not only the selected run. +- Backfilled ticket-to-run associations so tickets appear correctly in the Tickets overview. +- 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. + *** ## v0.1.21