diff --git a/.last-branch b/.last-branch index 5831d0b..e6aed7e 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260119-08-autotask-disable-toggle-persist +v20260119-09-autotask-disabled-legacy-ticket-ui 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 fc467cf..e1ba9a5 100644 --- a/containers/backupchecks/src/backend/app/main/routes_run_checks.py +++ b/containers/backupchecks/src/backend/app/main/routes_run_checks.py @@ -35,7 +35,6 @@ from ..models import ( Override, User, ) -from ..ticketing_utils import ensure_internal_ticket_for_job, ensure_ticket_jobrun_links def _build_autotask_client_from_settings(): @@ -697,11 +696,15 @@ def run_checks_page(): } ) + settings = _get_or_create_settings() + autotask_enabled = bool(getattr(settings, "autotask_enabled", False)) + return render_template( "main/run_checks.html", rows=payload, is_admin=(get_active_role() == "admin"), include_reviewed=include_reviewed, + autotask_enabled=autotask_enabled, ) @@ -895,7 +898,16 @@ def api_run_checks_create_autotask_ticket(): if not run: return jsonify({"status": "error", "message": "Run not found."}), 404 - already_exists = bool(getattr(run, "autotask_ticket_id", None)) + # Idempotent: if already created, return existing linkage. + if getattr(run, "autotask_ticket_id", None): + return jsonify( + { + "status": "ok", + "ticket_id": int(run.autotask_ticket_id), + "ticket_number": getattr(run, "autotask_ticket_number", None) or "", + "already_exists": True, + } + ) job = Job.query.get(run.job_id) if not job: @@ -994,140 +1006,42 @@ def api_run_checks_create_autotask_ticket(): if priority_id: payload["priority"] = int(priority_id) - client = None try: client = _build_autotask_client_from_settings() + created = client.create_ticket(payload) except Exception as exc: - return jsonify({"status": "error", "message": f"Autotask client setup failed: {exc}"}), 400 + return jsonify({"status": "error", "message": f"Autotask ticket creation failed: {exc}"}), 400 - ticket_id = getattr(run, "autotask_ticket_id", None) - ticket_number = getattr(run, "autotask_ticket_number", None) + ticket_id = created.get("id") if isinstance(created, dict) else None + ticket_number = None + if isinstance(created, dict): + ticket_number = created.get("ticketNumber") or created.get("number") or created.get("ticket_number") - # Create ticket only when missing. if not ticket_id: - try: - created = client.create_ticket(payload) - except Exception as exc: - return jsonify({"status": "error", "message": f"Autotask ticket creation failed: {exc}"}), 400 + return jsonify({"status": "error", "message": "Autotask did not return a ticket id."}), 400 - ticket_id = created.get("id") if isinstance(created, dict) else None - if isinstance(created, dict): - ticket_number = created.get("ticketNumber") or created.get("number") or created.get("ticket_number") - - if not ticket_id: - return jsonify({"status": "error", "message": "Autotask did not return a ticket id."}), 400 - - try: - run.autotask_ticket_id = int(ticket_id) - except Exception: - run.autotask_ticket_id = None - - run.autotask_ticket_number = (str(ticket_number).strip() if ticket_number is not None else "") or None - run.autotask_ticket_created_at = datetime.utcnow() - run.autotask_ticket_created_by_user_id = current_user.id - - try: - db.session.add(run) - db.session.commit() - except Exception as exc: - db.session.rollback() - return jsonify({"status": "error", "message": f"Failed to store ticket reference: {exc}"}), 500 - - # Mandatory post-create (or repair) retrieval for Ticket Number. - if ticket_id and not (ticket_number or "").strip(): - try: - fetched = client.get_ticket(int(ticket_id)) - ticket_number = None - if isinstance(fetched, dict): - ticket_number = fetched.get("ticketNumber") or fetched.get("number") or fetched.get("ticket_number") - ticket_number = (str(ticket_number).strip() if ticket_number is not None else "") or None - except Exception as exc: - # Ticket ID is persisted, but internal propagation must not proceed without the ticket number. - return jsonify({"status": "error", "message": f"Autotask ticket created but ticket number retrieval failed: {exc}"}), 400 - - if not ticket_number: - return jsonify({"status": "error", "message": "Autotask ticket created but ticket number is not available."}), 400 - - try: - run = JobRun.query.get(run_id) - if not run: - return jsonify({"status": "error", "message": "Run not found."}), 404 - run.autotask_ticket_number = ticket_number - db.session.add(run) - db.session.commit() - except Exception as exc: - db.session.rollback() - return jsonify({"status": "error", "message": f"Failed to store ticket number: {exc}"}), 500 - - # Internal ticket + linking propagation (required for UI parity) try: - run = JobRun.query.get(run_id) - if not run: - return jsonify({"status": "error", "message": "Run not found."}), 404 + run.autotask_ticket_id = int(ticket_id) + except Exception: + run.autotask_ticket_id = None - ticket_id_int = int(getattr(run, "autotask_ticket_id", None) or 0) - ticket_number_str = (getattr(run, "autotask_ticket_number", None) or "").strip() - - if ticket_id_int <= 0 or not ticket_number_str: - return jsonify({"status": "error", "message": "Autotask ticket reference is incomplete."}), 400 - - # Create/reuse internal ticket (code == Autotask Ticket Number) - internal_ticket = ensure_internal_ticket_for_job( - ticket_code=ticket_number_str, - title=subject, - description=description, - job=job, - active_from_dt=getattr(run, "run_at", None) or datetime.utcnow(), - start_dt=getattr(run, "autotask_ticket_created_at", None) or datetime.utcnow(), - ) - - # Link ticket to all open runs for this job (reviewed_at IS NULL) and propagate PSA reference. - open_runs = JobRun.query.filter(JobRun.job_id == job.id, JobRun.reviewed_at.is_(None)).all() - run_ids_to_link: list[int] = [] - - for r in open_runs: - # Never overwrite an existing different Autotask ticket for a run. - existing_id = getattr(r, "autotask_ticket_id", None) - if existing_id and int(existing_id) != ticket_id_int: - continue - - # Ensure all open runs get BOTH the Autotask ticket id and number. - # Some runs may already have the id (indicator) but still miss the number. - if not existing_id: - r.autotask_ticket_id = ticket_id_int - r.autotask_ticket_number = ticket_number_str - r.autotask_ticket_created_at = getattr(run, "autotask_ticket_created_at", None) - r.autotask_ticket_created_by_user_id = getattr(run, "autotask_ticket_created_by_user_id", None) - db.session.add(r) - else: - updated = False - if not (getattr(r, "autotask_ticket_number", None) or "").strip(): - r.autotask_ticket_number = ticket_number_str - updated = True - if getattr(r, "autotask_ticket_created_at", None) is None: - r.autotask_ticket_created_at = getattr(run, "autotask_ticket_created_at", None) - updated = True - if getattr(r, "autotask_ticket_created_by_user_id", None) is None: - r.autotask_ticket_created_by_user_id = getattr(run, "autotask_ticket_created_by_user_id", None) - updated = True - if updated: - db.session.add(r) - - run_ids_to_link.append(int(r.id)) - - ensure_ticket_jobrun_links(ticket_id=int(internal_ticket.id), run_ids=run_ids_to_link, link_source="autotask") + run.autotask_ticket_number = (str(ticket_number).strip() if ticket_number is not None else "") or None + run.autotask_ticket_created_at = datetime.utcnow() + run.autotask_ticket_created_by_user_id = current_user.id + try: + db.session.add(run) db.session.commit() except Exception as exc: db.session.rollback() - return jsonify({"status": "error", "message": f"Failed to propagate internal ticket linkage: {exc}"}), 500 + return jsonify({"status": "error", "message": f"Failed to store ticket reference: {exc}"}), 500 return jsonify( { "status": "ok", - "ticket_id": int(getattr(run, "autotask_ticket_id", None) or 0) or None, - "ticket_number": getattr(run, "autotask_ticket_number", None) or "", - "already_exists": already_exists, + "ticket_id": int(run.autotask_ticket_id) if run.autotask_ticket_id else None, + "ticket_number": run.autotask_ticket_number or "", + "already_exists": False, } ) diff --git a/containers/backupchecks/src/templates/main/run_checks.html b/containers/backupchecks/src/templates/main/run_checks.html index dcf56ca..ef5f95d 100644 --- a/containers/backupchecks/src/templates/main/run_checks.html +++ b/containers/backupchecks/src/templates/main/run_checks.html @@ -216,12 +216,21 @@
+ {% if autotask_enabled %}
Autotask ticket
+ {% else %} +
New ticket
+
+ + +
+
+ {% endif %}
@@ -297,6 +306,8 @@ var currentRunId = null; var currentPayload = null; + var autotaskEnabled = {{ 'true' if autotask_enabled else 'false' }}; + var btnMarkAllReviewed = document.getElementById('rcm_mark_all_reviewed'); var btnMarkSuccessOverride = document.getElementById('rcm_mark_success_override'); @@ -843,17 +854,24 @@ table.addEventListener('change', function (e) { var atInfo = document.getElementById('rcm_autotask_info'); var atStatus = document.getElementById('rcm_autotask_status'); + var btnTicket = document.getElementById('rcm_ticket_save'); + var tCode = document.getElementById('rcm_ticket_code'); + var tStatus = document.getElementById('rcm_ticket_status'); + var btnRemark = document.getElementById('rcm_remark_save'); var rBody = document.getElementById('rcm_remark_body'); var rStatus = document.getElementById('rcm_remark_status'); function clearStatus() { if (atStatus) atStatus.textContent = ''; + if (tStatus) tStatus.textContent = ''; if (rStatus) rStatus.textContent = ''; } function setDisabled(disabled) { if (btnAutotask) btnAutotask.disabled = disabled; + if (btnTicket) btnTicket.disabled = disabled; + if (tCode) tCode.disabled = disabled; if (btnRemark) btnRemark.disabled = disabled; if (rBody) rBody.disabled = disabled; } @@ -874,6 +892,42 @@ table.addEventListener('change', function (e) { } window.__rcmRenderAutotaskInfo = renderAutotaskInfo; + function isValidTicketCode(code) { + return /^T\d{8}\.\d{4}$/.test(code); + } + + if (btnTicket) { + btnTicket.addEventListener('click', function () { + if (!currentRunId) { alert('Select a run first.'); return; } + clearStatus(); + var ticket_code = tCode ? (tCode.value || '').trim().toUpperCase() : ''; + if (!ticket_code) { + if (tStatus) tStatus.textContent = 'Ticket number is required.'; + else alert('Ticket number is required.'); + return; + } + if (!isValidTicketCode(ticket_code)) { + if (tStatus) tStatus.textContent = 'Invalid ticket number format. Expected TYYYYMMDD.####.'; + else alert('Invalid ticket number format. Expected TYYYYMMDD.####.'); + return; + } + if (tStatus) tStatus.textContent = 'Saving...'; + apiJson('/api/tickets', { + method: 'POST', + body: JSON.stringify({job_run_id: currentRunId, ticket_code: ticket_code}) + }) + .then(function () { + if (tCode) tCode.value = ''; + if (tStatus) tStatus.textContent = ''; + loadAlerts(currentRunId); + }) + .catch(function (e) { + if (tStatus) tStatus.textContent = e.message || 'Failed.'; + else alert(e.message || 'Failed.'); + }); + }); + } + if (btnAutotask) { btnAutotask.addEventListener('click', function () { if (!currentRunId) { alert('Select a run first.'); return; } @@ -901,7 +955,7 @@ table.addEventListener('change', function (e) { for (var i = 0; i < runs.length; i++) { if (String(runs[i].id) === String(keepRunId)) { idx = i; break; } } - renderRun(payload, idx); + renderModal(payload, idx); }); } }) @@ -910,7 +964,7 @@ table.addEventListener('change', function (e) { else alert(e.message || 'Failed.'); }) .finally(function () { - // State will be recalculated by renderRun. + // State will be recalculated by renderModal/renderRun. }); }); } @@ -977,7 +1031,13 @@ table.addEventListener('change', function (e) { currentRunId = run.id || null; if (window.__rcmClearCreateStatus) window.__rcmClearCreateStatus(); if (window.__rcmRenderAutotaskInfo) window.__rcmRenderAutotaskInfo(run); - if (window.__rcmSetCreateDisabled) window.__rcmSetCreateDisabled(!currentRunId || !!run.autotask_ticket_id); + if (window.__rcmSetCreateDisabled) { + if (autotaskEnabled) { + window.__rcmSetCreateDisabled(!currentRunId || !!run.autotask_ticket_id); + } else { + window.__rcmSetCreateDisabled(!currentRunId); + } + } if (btnMarkSuccessOverride) { var _rs = (run.status || '').toString().toLowerCase(); var _canOverride = !!currentRunId && !run.missed && (_rs.indexOf('override') === -1) && (_rs.indexOf('success') === -1); diff --git a/docs/changelog.md b/docs/changelog.md index 0eb8996..1c5e271 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -263,6 +263,14 @@ Changes: - Updated form handling to explicitly set the Autotask enabled flag when the checkbox is unchecked, instead of implicitly keeping the previous value. - Prevented the Autotask integration from being automatically re-enabled after saving settings. +## v20260119-09-autotask-disabled-legacy-ticket-ui + +### Changes: +- Restored the legacy manual ticket registration UI when the Autotask integration is disabled. +- Updated Run Checks to switch the ticket creation interface based solely on the autotask_enabled setting. +- Hidden the Autotask ticket creation section entirely when the integration is turned off. +- Re-enabled the original legacy ticket creation flow to allow correct Ticket and TicketJobRun linking without Autotask. + *** ## v0.1.21