From 36deb77806a844a75a9aa3836bb653578ced93ec Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Mon, 19 Jan 2026 11:20:52 +0100 Subject: [PATCH] Auto-commit local changes before build (2026-01-19 11:20:52) --- .last-branch | 2 +- .../app/integrations/autotask/client.py | 43 ++- .../src/backend/app/main/routes_run_checks.py | 274 +++++++++++++++++- .../src/backend/app/main/routes_settings.py | 14 +- .../src/backend/app/migrations.py | 63 ++++ .../backupchecks/src/backend/app/models.py | 9 + .../src/templates/main/run_checks.html | 106 ++++--- .../src/templates/main/settings.html | 14 +- 8 files changed, 477 insertions(+), 48 deletions(-) diff --git a/.last-branch b/.last-branch index 54b4e7f..371b3cc 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260119-01-restoredto-v20260115-12-autotask-customers-refreshall-mappings +v20260119-02-restoredto--v20260115-15-autotask-default-ticket-status-setting diff --git a/containers/backupchecks/src/backend/app/integrations/autotask/client.py b/containers/backupchecks/src/backend/app/integrations/autotask/client.py index 6aa9981..59a62f0 100644 --- a/containers/backupchecks/src/backend/app/integrations/autotask/client.py +++ b/containers/backupchecks/src/backend/app/integrations/autotask/client.py @@ -105,7 +105,13 @@ class AutotaskClient: "Accept": "application/json", } - def _request(self, method: str, path: str, params: Optional[Dict[str, Any]] = None) -> Any: + def _request( + self, + method: str, + path: str, + params: Optional[Dict[str, Any]] = None, + json_body: Optional[Dict[str, Any]] = None, + ) -> Any: zone = self.get_zone_info() base = zone.api_url.rstrip("/") url = f"{base}/v1.0/{path.lstrip('/')}" @@ -120,6 +126,7 @@ class AutotaskClient: url=url, headers=h, params=params or None, + json=json_body if json_body is not None else None, auth=(self.username, self.password) if use_basic_auth else None, timeout=self.timeout_seconds, ) @@ -389,3 +396,37 @@ class AutotaskClient: raise AutotaskError("Tickets.priority metadata did not include picklist values.") return self._call_picklist_values(picklist_values) + + def get_ticket_statuses(self) -> List[Dict[str, Any]]: + """Return Ticket Status picklist values. + + We retrieve this from Tickets field metadata to avoid hardcoded status IDs. + """ + return self._get_ticket_picklist_values(field_names=["status", "statusid"]) + + def create_ticket(self, payload: Dict[str, Any]) -> Dict[str, Any]: + """Create a Ticket in Autotask. + + Uses POST /Tickets. + Returns the created ticket object (as returned by Autotask). + """ + if not isinstance(payload, dict) or not payload: + raise AutotaskError("Ticket payload is empty.") + + data = self._request("POST", "Tickets", json_body=payload) + # Autotask commonly returns the created object or an items list. + if isinstance(data, dict): + if "item" in data and isinstance(data.get("item"), dict): + return data["item"] + if "items" in data and isinstance(data.get("items"), list) and data.get("items"): + first = data.get("items")[0] + if isinstance(first, dict): + return first + if "id" in data: + return data + # Fallback: return normalized first item if possible + items = self._as_items_list(data) + if items: + return items[0] + + raise AutotaskError("Autotask did not return a created ticket object.") 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 b073af2..b66350c 100644 --- a/containers/backupchecks/src/backend/app/main/routes_run_checks.py +++ b/containers/backupchecks/src/backend/app/main/routes_run_checks.py @@ -4,7 +4,8 @@ import calendar from datetime import date, datetime, time, timedelta, timezone -from flask import jsonify, render_template, request +from flask import jsonify, render_template, request, url_for +from urllib.parse import urljoin from flask_login import current_user, login_required from sqlalchemy import and_, or_, func, text @@ -35,6 +36,106 @@ from ..models import ( User, ) + +def _build_autotask_client_from_settings(): + """Build an AutotaskClient from stored settings or raise a user-safe exception.""" + settings = _get_or_create_settings() + if not getattr(settings, "autotask_enabled", False): + raise RuntimeError("Autotask integration is disabled.") + + required = [ + getattr(settings, "autotask_environment", None), + getattr(settings, "autotask_api_username", None), + getattr(settings, "autotask_api_password", None), + getattr(settings, "autotask_tracking_identifier", None), + ] + if any(not (x and str(x).strip()) for x in required): + raise RuntimeError("Autotask settings incomplete.") + + from ..integrations.autotask.client import AutotaskClient + + return AutotaskClient( + username=settings.autotask_api_username, + password=settings.autotask_api_password, + api_integration_code=settings.autotask_tracking_identifier, + environment=settings.autotask_environment, + ) + + +def _determine_autotask_severity(status_text: str | None) -> str: + s = (status_text or "").strip().lower() + if "warning" in s: + return "warning" + if "error" in s or "fail" in s: + return "error" + if "missed" in s: + return "error" + return "warning" + + +def _compose_autotask_ticket_description( + *, + settings, + job: Job, + run: JobRun, + status_display: str, + overall_message: str, + objects_payload: list[dict[str, str]], +) -> str: + tz_name = _get_ui_timezone_name() or "Europe/Amsterdam" + run_dt = run.run_at + run_at_str = _format_datetime(run_dt) if run_dt else "-" + + base_url = (getattr(settings, "autotask_base_url", None) or "").strip() + job_rel = url_for("main.job_detail", job_id=job.id) + # Link to Job Details with a hint for the specific run. + job_link = urljoin(base_url.rstrip("/") + "/", job_rel.lstrip("/")) + if run.id: + job_link = f"{job_link}?run_id={int(run.id)}" + + lines: list[str] = [] + lines.append(f"Customer: {job.customer.name if job.customer else ''}") + lines.append(f"Job: {job.job_name or ''}") + lines.append(f"Backup: {job.backup_software or ''} / {job.backup_type or ''}") + lines.append(f"Run at ({tz_name}): {run_at_str}") + lines.append(f"Status: {status_display or ''}") + lines.append("") + + overall_message = (overall_message or "").strip() + if overall_message: + lines.append("Summary:") + lines.append(overall_message) + lines.append("") + lines.append("Multiple objects reported messages. See Backupchecks for full details.") + else: + # Fallback to object-level messages with a hard limit. + limit = 10 + shown = 0 + total = 0 + for o in objects_payload or []: + name = (o.get("name") or "").strip() + err = (o.get("error_message") or "").strip() + st = (o.get("status") or "").strip() + if not name: + continue + if not err and not st: + continue + total += 1 + if shown >= limit: + continue + msg = err or st + lines.append(f"- {name}: {msg}") + shown += 1 + + if total == 0: + lines.append("No detailed object messages available. See Backupchecks for full details.") + elif total > shown: + lines.append(f"And {int(total - shown)} additional objects reported similar messages.") + + lines.append("") + lines.append(f"Backupchecks details: {job_link}") + return "\n".join(lines).strip() + "\n" + # Grace window for matching real runs to an expected schedule slot. # A run within +/- 1 hour of the inferred schedule time counts as fulfilling the slot. MISSED_GRACE_WINDOW = timedelta(hours=1) @@ -753,6 +854,8 @@ def run_checks_details(): "mail": mail_meta, "body_html": body_html, "objects": objects_payload, + "autotask_ticket_id": getattr(run, "autotask_ticket_id", None), + "autotask_ticket_number": getattr(run, "autotask_ticket_number", None) or "", } ) @@ -770,6 +873,175 @@ def run_checks_details(): return jsonify({"status": "ok", "job": job_payload, "runs": runs_payload}) +@main_bp.post("/api/run-checks/autotask-ticket") +@login_required +@roles_required("admin", "operator") +def api_run_checks_create_autotask_ticket(): + """Create an Autotask ticket for a specific run. + + Enforces: exactly one ticket per run. + """ + data = request.get_json(silent=True) or {} + try: + run_id = int(data.get("run_id") or 0) + except Exception: + run_id = 0 + + if run_id <= 0: + return jsonify({"status": "error", "message": "Invalid parameters."}), 400 + + 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. + 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: + return jsonify({"status": "error", "message": "Job not found."}), 404 + + customer = Customer.query.get(job.customer_id) if getattr(job, "customer_id", None) else None + if not customer: + return jsonify({"status": "error", "message": "Customer not found."}), 404 + + if not getattr(customer, "autotask_company_id", None): + return jsonify({"status": "error", "message": "Customer has no Autotask company mapping."}), 400 + + if (getattr(customer, "autotask_mapping_status", None) or "").strip().lower() not in ("ok", "renamed"): + return jsonify({"status": "error", "message": "Autotask company mapping is not valid."}), 400 + + settings = _get_or_create_settings() + + base_url = (getattr(settings, "autotask_base_url", None) or "").strip() + if not base_url: + return jsonify({"status": "error", "message": "Autotask Base URL is not configured."}), 400 + + # Required ticket defaults + if not getattr(settings, "autotask_default_queue_id", None): + return jsonify({"status": "error", "message": "Autotask default queue is not configured."}), 400 + if not getattr(settings, "autotask_default_ticket_source_id", None): + return jsonify({"status": "error", "message": "Autotask default ticket source is not configured."}), 400 + if not getattr(settings, "autotask_default_ticket_status", None): + return jsonify({"status": "error", "message": "Autotask default ticket status is not configured."}), 400 + + # Determine display status (including overrides) for consistent subject/priority mapping. + 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 "-" + + severity = _determine_autotask_severity(status_display) + priority_id = None + if severity == "warning": + priority_id = getattr(settings, "autotask_priority_warning", None) + else: + priority_id = getattr(settings, "autotask_priority_error", None) + + # Load mail + objects for ticket composition. + 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 "", + } + ) + + subject = f"[Backupchecks] {customer.name} - {job.job_name or ''} - {status_display}" + description = _compose_autotask_ticket_description( + settings=settings, + job=job, + run=run, + status_display=status_display, + overall_message=overall_message, + objects_payload=objects_payload, + ) + + payload = { + "companyID": int(customer.autotask_company_id), + "title": subject, + "description": description, + "queueID": int(settings.autotask_default_queue_id), + "source": int(settings.autotask_default_ticket_source_id), + "status": int(settings.autotask_default_ticket_status), + } + if priority_id: + payload["priority"] = int(priority_id) + + try: + client = _build_autotask_client_from_settings() + created = client.create_ticket(payload) + except Exception as exc: + return jsonify({"status": "error", "message": f"Autotask ticket creation failed: {exc}"}), 400 + + 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") + + 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 + + return jsonify( + { + "status": "ok", + "ticket_id": int(run.autotask_ticket_id) if run.autotask_ticket_id else None, + "ticket_number": run.autotask_ticket_number or "", + "already_exists": False, + } + ) + + @main_bp.post("/api/run-checks/mark-reviewed") @login_required @roles_required("admin", "operator") diff --git a/containers/backupchecks/src/backend/app/main/routes_settings.py b/containers/backupchecks/src/backend/app/main/routes_settings.py index 5897121..2160fb5 100644 --- a/containers/backupchecks/src/backend/app/main/routes_settings.py +++ b/containers/backupchecks/src/backend/app/main/routes_settings.py @@ -657,6 +657,7 @@ def settings(): autotask_queues = [] autotask_ticket_sources = [] autotask_priorities = [] + autotask_ticket_statuses = [] autotask_last_sync_at = getattr(settings, "autotask_reference_last_sync_at", None) try: @@ -677,6 +678,12 @@ def settings(): except Exception: autotask_priorities = [] + try: + if getattr(settings, "autotask_cached_ticket_statuses_json", None): + autotask_ticket_statuses = json.loads(settings.autotask_cached_ticket_statuses_json) or [] + except Exception: + autotask_ticket_statuses = [] + return render_template( "main/settings.html", settings=settings, @@ -692,6 +699,7 @@ def settings(): autotask_queues=autotask_queues, autotask_ticket_sources=autotask_ticket_sources, autotask_priorities=autotask_priorities, + autotask_ticket_statuses=autotask_ticket_statuses, autotask_last_sync_at=autotask_last_sync_at, news_admin_items=news_admin_items, news_admin_stats=news_admin_stats, @@ -1322,6 +1330,7 @@ def settings_autotask_refresh_reference_data(): queues = client.get_queues() sources = client.get_ticket_sources() priorities = client.get_ticket_priorities() + statuses = client.get_ticket_statuses() # Store a minimal subset for dropdowns (id + name/label) # Note: Some "reference" values are exposed as picklists (value/label) @@ -1354,6 +1363,7 @@ def settings_autotask_refresh_reference_data(): settings.autotask_cached_queues_json = json.dumps(_norm(queues)) settings.autotask_cached_ticket_sources_json = json.dumps(_norm(sources)) + settings.autotask_cached_ticket_statuses_json = json.dumps(_norm(statuses)) # Priorities are returned as picklist values (value/label) pr_out = [] @@ -1377,13 +1387,13 @@ def settings_autotask_refresh_reference_data(): db.session.commit() flash( - f"Autotask reference data refreshed. Queues: {len(queues)}. Ticket Sources: {len(sources)}. Priorities: {len(pr_out)}.", + f"Autotask reference data refreshed. Queues: {len(queues)}. Ticket Sources: {len(sources)}. Ticket Statuses: {len(statuses)}. Priorities: {len(pr_out)}.", "success", ) _log_admin_event( "autotask_refresh_reference_data", "Autotask reference data refreshed.", - details=json.dumps({"queues": len(queues or []), "ticket_sources": len(sources or []), "priorities": len(pr_out)}), + details=json.dumps({"queues": len(queues or []), "ticket_sources": len(sources or []), "ticket_statuses": len(statuses or []), "priorities": len(pr_out)}), ) except Exception as exc: flash(f"Failed to refresh Autotask reference data: {exc}", "danger") diff --git a/containers/backupchecks/src/backend/app/migrations.py b/containers/backupchecks/src/backend/app/migrations.py index 6c21f5e..b1d1405 100644 --- a/containers/backupchecks/src/backend/app/migrations.py +++ b/containers/backupchecks/src/backend/app/migrations.py @@ -168,6 +168,7 @@ def migrate_system_settings_autotask_integration() -> None: ("autotask_cached_queues_json", "TEXT NULL"), ("autotask_cached_ticket_sources_json", "TEXT NULL"), ("autotask_cached_priorities_json", "TEXT NULL"), + ("autotask_cached_ticket_statuses_json", "TEXT NULL"), ("autotask_reference_last_sync_at", "TIMESTAMP NULL"), ] @@ -897,6 +898,7 @@ def run_migrations() -> None: migrate_overrides_match_columns() migrate_job_runs_review_tracking() migrate_job_runs_override_metadata() + migrate_job_runs_autotask_ticket_fields() migrate_jobs_archiving() migrate_news_tables() migrate_reporting_tables() @@ -904,6 +906,67 @@ def run_migrations() -> None: print("[migrations] All migrations completed.") +def migrate_job_runs_autotask_ticket_fields() -> None: + """Add Autotask ticket linkage fields to job_runs if missing. + + Columns: + - job_runs.autotask_ticket_id (INTEGER NULL) + - job_runs.autotask_ticket_number (VARCHAR(64) NULL) + - job_runs.autotask_ticket_created_at (TIMESTAMP NULL) + - job_runs.autotask_ticket_created_by_user_id (INTEGER NULL, FK users.id) + """ + + table = "job_runs" + try: + engine = db.get_engine() + except Exception as exc: + print(f"[migrations] Could not get engine for job_runs Autotask ticket migration: {exc}") + return + + try: + with engine.connect() as conn: + cols = _get_table_columns(conn, table) + if not cols: + return + + if "autotask_ticket_id" not in cols: + print("[migrations] Adding job_runs.autotask_ticket_id column...") + conn.execute(text('ALTER TABLE "job_runs" ADD COLUMN autotask_ticket_id INTEGER')) + + if "autotask_ticket_number" not in cols: + print("[migrations] Adding job_runs.autotask_ticket_number column...") + conn.execute(text('ALTER TABLE "job_runs" ADD COLUMN autotask_ticket_number VARCHAR(64)')) + + if "autotask_ticket_created_at" not in cols: + print("[migrations] Adding job_runs.autotask_ticket_created_at column...") + conn.execute(text('ALTER TABLE "job_runs" ADD COLUMN autotask_ticket_created_at TIMESTAMP')) + + if "autotask_ticket_created_by_user_id" not in cols: + print("[migrations] Adding job_runs.autotask_ticket_created_by_user_id column...") + conn.execute(text('ALTER TABLE "job_runs" ADD COLUMN autotask_ticket_created_by_user_id INTEGER')) + + try: + conn.execute( + text( + 'ALTER TABLE "job_runs" ' + 'ADD CONSTRAINT job_runs_autotask_ticket_created_by_user_id_fkey ' + 'FOREIGN KEY (autotask_ticket_created_by_user_id) REFERENCES users(id) ' + 'ON DELETE SET NULL' + ) + ) + except Exception as exc: + print( + f"[migrations] Could not add FK job_runs.autotask_ticket_created_by_user_id -> users.id (continuing): {exc}" + ) + + conn.execute(text('CREATE INDEX IF NOT EXISTS idx_job_runs_autotask_ticket_id ON "job_runs" (autotask_ticket_id)')) + except Exception as exc: + print(f"[migrations] job_runs table not found; skipping migrate_job_runs_autotask_ticket_fields: {exc}") + return + + print("[migrations] migrate_job_runs_autotask_ticket_fields completed.") + + def migrate_jobs_archiving() -> None: """Add archiving columns to jobs if missing. diff --git a/containers/backupchecks/src/backend/app/models.py b/containers/backupchecks/src/backend/app/models.py index e72a846..4ecba7d 100644 --- a/containers/backupchecks/src/backend/app/models.py +++ b/containers/backupchecks/src/backend/app/models.py @@ -127,6 +127,7 @@ class SystemSettings(db.Model): autotask_cached_queues_json = db.Column(db.Text, nullable=True) autotask_cached_ticket_sources_json = db.Column(db.Text, nullable=True) autotask_cached_priorities_json = db.Column(db.Text, nullable=True) + autotask_cached_ticket_statuses_json = db.Column(db.Text, nullable=True) autotask_reference_last_sync_at = db.Column(db.DateTime, nullable=True) created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) updated_at = db.Column( @@ -275,6 +276,12 @@ class JobRun(db.Model): reviewed_at = db.Column(db.DateTime, nullable=True) reviewed_by_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) + # Autotask integration (Phase 4: ticket creation from Run Checks) + autotask_ticket_id = db.Column(db.Integer, nullable=True) + autotask_ticket_number = db.Column(db.String(64), nullable=True) + autotask_ticket_created_at = db.Column(db.DateTime, nullable=True) + autotask_ticket_created_by_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) updated_at = db.Column( @@ -288,6 +295,8 @@ class JobRun(db.Model): reviewed_by = db.relationship("User", foreign_keys=[reviewed_by_user_id]) + autotask_ticket_created_by = db.relationship("User", foreign_keys=[autotask_ticket_created_by_user_id]) + class JobRunReviewEvent(db.Model): __tablename__ = "job_run_review_events" diff --git a/containers/backupchecks/src/templates/main/run_checks.html b/containers/backupchecks/src/templates/main/run_checks.html index 90de7aa..4701bb2 100644 --- a/containers/backupchecks/src/templates/main/run_checks.html +++ b/containers/backupchecks/src/templates/main/run_checks.html @@ -214,18 +214,16 @@
-
-
-
-
New ticket
- -
-
- -
-
-
-
+
+
+
+
Autotask ticket
+ +
+
+
+
+
@@ -841,56 +839,78 @@ table.addEventListener('change', function (e) { } function bindInlineCreateForms() { - var btnTicket = document.getElementById('rcm_ticket_save'); + var btnAutotask = document.getElementById('rcm_autotask_create'); + var atInfo = document.getElementById('rcm_autotask_info'); + var atStatus = document.getElementById('rcm_autotask_status'); + var btnRemark = document.getElementById('rcm_remark_save'); - var tCode = document.getElementById('rcm_ticket_code'); -var tStatus = document.getElementById('rcm_ticket_status'); var rBody = document.getElementById('rcm_remark_body'); var rStatus = document.getElementById('rcm_remark_status'); function clearStatus() { - if (tStatus) tStatus.textContent = ''; + if (atStatus) atStatus.textContent = ''; if (rStatus) rStatus.textContent = ''; } function setDisabled(disabled) { - if (btnTicket) btnTicket.disabled = disabled; + if (btnAutotask) btnAutotask.disabled = disabled; if (btnRemark) btnRemark.disabled = disabled; - if (tCode) tCode.disabled = disabled; -if (rBody) rBody.disabled = disabled; + if (rBody) rBody.disabled = disabled; } window.__rcmSetCreateDisabled = setDisabled; window.__rcmClearCreateStatus = clearStatus; - if (btnTicket) { - btnTicket.addEventListener('click', function () { + function renderAutotaskInfo(run) { + if (!atInfo) return; + var num = (run && run.autotask_ticket_number) ? String(run.autotask_ticket_number) : ''; + if (num) { + atInfo.innerHTML = '
Ticket: ' + escapeHtml(num) + '
'; + } else if (run && run.autotask_ticket_id) { + atInfo.innerHTML = '
Ticket: created
'; + } else { + atInfo.innerHTML = '
No Autotask ticket created for this run.
'; + } + } + window.__rcmRenderAutotaskInfo = renderAutotaskInfo; + + if (btnAutotask) { + btnAutotask.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 (!/^T\d{8}\.\d{4}$/.test(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', { + if (atStatus) atStatus.textContent = 'Creating ticket...'; + btnAutotask.disabled = true; + apiJson('/api/run-checks/autotask-ticket', { method: 'POST', - body: JSON.stringify({job_run_id: currentRunId, ticket_code: ticket_code}) + body: JSON.stringify({run_id: currentRunId}) }) - .then(function () { - if (tCode) tCode.value = ''; -if (tStatus) tStatus.textContent = ''; - loadAlerts(currentRunId); + .then(function (j) { + if (!j || j.status !== 'ok') throw new Error((j && j.message) || 'Failed.'); + if (atStatus) atStatus.textContent = ''; + + // Refresh modal data so UI reflects stored ticket linkage. + var keepRunId = currentRunId; + if (currentJobId) { + return fetch('/api/run-checks/details?job_id=' + encodeURIComponent(currentJobId)) + .then(function (r) { return r.json(); }) + .then(function (payload) { + currentPayload = payload; + // Find the same run index + var idx = 0; + var runs = (payload && payload.runs) || []; + for (var i = 0; i < runs.length; i++) { + if (String(runs[i].id) === String(keepRunId)) { idx = i; break; } + } + renderModal(payload, idx); + }); + } }) .catch(function (e) { - if (tStatus) tStatus.textContent = e.message || 'Failed.'; + if (atStatus) atStatus.textContent = e.message || 'Failed.'; else alert(e.message || 'Failed.'); + }) + .finally(function () { + // State will be recalculated by renderModal/renderRun. }); }); } @@ -956,7 +976,8 @@ if (tStatus) tStatus.textContent = ''; currentRunId = run.id || null; if (window.__rcmClearCreateStatus) window.__rcmClearCreateStatus(); - if (window.__rcmSetCreateDisabled) window.__rcmSetCreateDisabled(!currentRunId); + if (window.__rcmRenderAutotaskInfo) window.__rcmRenderAutotaskInfo(run); + if (window.__rcmSetCreateDisabled) window.__rcmSetCreateDisabled(!currentRunId || !!run.autotask_ticket_id); if (btnMarkSuccessOverride) { var _rs = (run.status || '').toString().toLowerCase(); var _canOverride = !!currentRunId && !run.missed && (_rs.indexOf('override') === -1) && (_rs.indexOf('success') === -1); @@ -1144,9 +1165,10 @@ if (tStatus) tStatus.textContent = ''; var dot = run.missed ? "dot-missed" : statusDotClass(run.status); var dotHtml = dot ? ('') : ''; var reviewedMark = run.is_reviewed ? ' ' : ''; + var ticketMark = run.autotask_ticket_id ? ' 🎫' : ''; a.title = run.status || ''; - a.innerHTML = dotHtml + '' + escapeHtml(run.run_at || 'Run') + '' + reviewedMark; + a.innerHTML = dotHtml + '' + escapeHtml(run.run_at || 'Run') + '' + reviewedMark + ticketMark; a.addEventListener('click', function (ev) { ev.preventDefault(); renderRun(data, idx); diff --git a/containers/backupchecks/src/templates/main/settings.html b/containers/backupchecks/src/templates/main/settings.html index 6c52acb..3b848e0 100644 --- a/containers/backupchecks/src/templates/main/settings.html +++ b/containers/backupchecks/src/templates/main/settings.html @@ -397,6 +397,17 @@
Requires refreshed reference data.
+
+ + +
Required for Autotask ticket creation. Requires refreshed reference data.
+
+