Auto-commit local changes before build (2026-01-15 15:05:42)
This commit is contained in:
parent
49fd29a6f2
commit
3564bcf62f
@ -1 +1 @@
|
||||
v20260115-12-autotask-customers-refreshall-mappings
|
||||
v20260115-13-autotask-runchecks-create-ticket
|
||||
|
||||
@ -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,30 @@ class AutotaskClient:
|
||||
raise AutotaskError("Tickets.priority metadata did not include picklist values.")
|
||||
|
||||
return self._call_picklist_values(picklist_values)
|
||||
|
||||
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.")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -897,6 +897,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 +905,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.
|
||||
|
||||
|
||||
@ -275,6 +275,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 +294,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"
|
||||
|
||||
@ -217,13 +217,11 @@
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="border rounded p-2">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div class="fw-semibold">New ticket</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="rcm_ticket_save">Add</button>
|
||||
<div class="fw-semibold">Autotask ticket</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="rcm_autotask_create">Create</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<input class="form-control form-control-sm" id="rcm_ticket_code" type="text" placeholder="Ticket number (e.g., T20260106.0001)" />
|
||||
</div>
|
||||
<div class="mt-2 small text-muted" id="rcm_ticket_status"></div>
|
||||
<div class="mt-2 small" id="rcm_autotask_info"></div>
|
||||
<div class="mt-2 small text-muted" id="rcm_autotask_status"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
@ -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;
|
||||
}
|
||||
|
||||
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 = '<div><strong>Ticket:</strong> ' + escapeHtml(num) + '</div>';
|
||||
} else if (run && run.autotask_ticket_id) {
|
||||
atInfo.innerHTML = '<div><strong>Ticket:</strong> created</div>';
|
||||
} else {
|
||||
atInfo.innerHTML = '<div class="text-muted">No Autotask ticket created for this run.</div>';
|
||||
}
|
||||
}
|
||||
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 ? ('<span class="status-dot ' + dot + ' me-2" aria-hidden="true"></span>') : '';
|
||||
var reviewedMark = run.is_reviewed ? ' <span class="ms-2" title="Reviewed" aria-label="Reviewed">✔</span>' : '';
|
||||
var ticketMark = run.autotask_ticket_id ? ' <span class="ms-2" title="Autotask ticket created" aria-label="Autotask ticket">🎫</span>' : '';
|
||||
|
||||
a.title = run.status || '';
|
||||
a.innerHTML = dotHtml + '<span class="text-nowrap">' + escapeHtml(run.run_at || 'Run') + '</span>' + reviewedMark;
|
||||
a.innerHTML = dotHtml + '<span class="text-nowrap">' + escapeHtml(run.run_at || 'Run') + '</span>' + reviewedMark + ticketMark;
|
||||
a.addEventListener('click', function (ev) {
|
||||
ev.preventDefault();
|
||||
renderRun(data, idx);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user