Auto-commit local changes before build (2026-01-19 14:06:42)
This commit is contained in:
parent
c595c165ed
commit
c5ff1e11a3
@ -1 +1 @@
|
|||||||
v20260119-08-autotask-disable-toggle-persist
|
v20260119-09-autotask-disabled-legacy-ticket-ui
|
||||||
|
|||||||
@ -35,7 +35,6 @@ from ..models import (
|
|||||||
Override,
|
Override,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from ..ticketing_utils import ensure_internal_ticket_for_job, ensure_ticket_jobrun_links
|
|
||||||
|
|
||||||
|
|
||||||
def _build_autotask_client_from_settings():
|
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(
|
return render_template(
|
||||||
"main/run_checks.html",
|
"main/run_checks.html",
|
||||||
rows=payload,
|
rows=payload,
|
||||||
is_admin=(get_active_role() == "admin"),
|
is_admin=(get_active_role() == "admin"),
|
||||||
include_reviewed=include_reviewed,
|
include_reviewed=include_reviewed,
|
||||||
|
autotask_enabled=autotask_enabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -895,7 +898,16 @@ def api_run_checks_create_autotask_ticket():
|
|||||||
if not run:
|
if not run:
|
||||||
return jsonify({"status": "error", "message": "Run not found."}), 404
|
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)
|
job = Job.query.get(run.job_id)
|
||||||
if not job:
|
if not job:
|
||||||
@ -994,140 +1006,42 @@ def api_run_checks_create_autotask_ticket():
|
|||||||
if priority_id:
|
if priority_id:
|
||||||
payload["priority"] = int(priority_id)
|
payload["priority"] = int(priority_id)
|
||||||
|
|
||||||
client = None
|
|
||||||
try:
|
try:
|
||||||
client = _build_autotask_client_from_settings()
|
client = _build_autotask_client_from_settings()
|
||||||
|
created = client.create_ticket(payload)
|
||||||
except Exception as exc:
|
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_id = created.get("id") if isinstance(created, dict) else None
|
||||||
ticket_number = getattr(run, "autotask_ticket_number", 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:
|
if not ticket_id:
|
||||||
try:
|
return jsonify({"status": "error", "message": "Autotask did not return a ticket id."}), 400
|
||||||
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
|
|
||||||
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:
|
try:
|
||||||
run = JobRun.query.get(run_id)
|
run.autotask_ticket_id = int(ticket_id)
|
||||||
if not run:
|
except Exception:
|
||||||
return jsonify({"status": "error", "message": "Run not found."}), 404
|
run.autotask_ticket_id = None
|
||||||
|
|
||||||
ticket_id_int = int(getattr(run, "autotask_ticket_id", None) or 0)
|
run.autotask_ticket_number = (str(ticket_number).strip() if ticket_number is not None else "") or None
|
||||||
ticket_number_str = (getattr(run, "autotask_ticket_number", None) or "").strip()
|
run.autotask_ticket_created_at = datetime.utcnow()
|
||||||
|
run.autotask_ticket_created_by_user_id = current_user.id
|
||||||
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")
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.add(run)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
db.session.rollback()
|
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(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"ticket_id": int(getattr(run, "autotask_ticket_id", None) or 0) or None,
|
"ticket_id": int(run.autotask_ticket_id) if run.autotask_ticket_id else None,
|
||||||
"ticket_number": getattr(run, "autotask_ticket_number", None) or "",
|
"ticket_number": run.autotask_ticket_number or "",
|
||||||
"already_exists": already_exists,
|
"already_exists": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -216,12 +216,21 @@
|
|||||||
<div class="row g-2 align-items-start">
|
<div class="row g-2 align-items-start">
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
<div class="border rounded p-2">
|
<div class="border rounded p-2">
|
||||||
|
{% if autotask_enabled %}
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
<div class="fw-semibold">Autotask ticket</div>
|
<div class="fw-semibold">Autotask ticket</div>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" id="rcm_autotask_create">Create</button>
|
<button type="button" class="btn btn-sm btn-outline-primary" id="rcm_autotask_create">Create</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 small" id="rcm_autotask_info"></div>
|
<div class="mt-2 small" id="rcm_autotask_info"></div>
|
||||||
<div class="mt-2 small text-muted" id="rcm_autotask_status"></div>
|
<div class="mt-2 small text-muted" id="rcm_autotask_status"></div>
|
||||||
|
{% else %}
|
||||||
|
<div class="fw-semibold">New ticket</div>
|
||||||
|
<div class="d-flex gap-2 mt-1">
|
||||||
|
<input class="form-control form-control-sm" id="rcm_ticket_code" type="text" placeholder="Ticket number (e.g., T20260106.0001)" />
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" id="rcm_ticket_save">Add</button>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 small text-muted" id="rcm_ticket_status"></div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-6">
|
<div class="col-12 col-lg-6">
|
||||||
@ -297,6 +306,8 @@
|
|||||||
var currentRunId = null;
|
var currentRunId = null;
|
||||||
var currentPayload = null;
|
var currentPayload = null;
|
||||||
|
|
||||||
|
var autotaskEnabled = {{ 'true' if autotask_enabled else 'false' }};
|
||||||
|
|
||||||
var btnMarkAllReviewed = document.getElementById('rcm_mark_all_reviewed');
|
var btnMarkAllReviewed = document.getElementById('rcm_mark_all_reviewed');
|
||||||
var btnMarkSuccessOverride = document.getElementById('rcm_mark_success_override');
|
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 atInfo = document.getElementById('rcm_autotask_info');
|
||||||
var atStatus = document.getElementById('rcm_autotask_status');
|
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 btnRemark = document.getElementById('rcm_remark_save');
|
||||||
var rBody = document.getElementById('rcm_remark_body');
|
var rBody = document.getElementById('rcm_remark_body');
|
||||||
var rStatus = document.getElementById('rcm_remark_status');
|
var rStatus = document.getElementById('rcm_remark_status');
|
||||||
|
|
||||||
function clearStatus() {
|
function clearStatus() {
|
||||||
if (atStatus) atStatus.textContent = '';
|
if (atStatus) atStatus.textContent = '';
|
||||||
|
if (tStatus) tStatus.textContent = '';
|
||||||
if (rStatus) rStatus.textContent = '';
|
if (rStatus) rStatus.textContent = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDisabled(disabled) {
|
function setDisabled(disabled) {
|
||||||
if (btnAutotask) btnAutotask.disabled = disabled;
|
if (btnAutotask) btnAutotask.disabled = disabled;
|
||||||
|
if (btnTicket) btnTicket.disabled = disabled;
|
||||||
|
if (tCode) tCode.disabled = disabled;
|
||||||
if (btnRemark) btnRemark.disabled = disabled;
|
if (btnRemark) btnRemark.disabled = disabled;
|
||||||
if (rBody) rBody.disabled = disabled;
|
if (rBody) rBody.disabled = disabled;
|
||||||
}
|
}
|
||||||
@ -874,6 +892,42 @@ table.addEventListener('change', function (e) {
|
|||||||
}
|
}
|
||||||
window.__rcmRenderAutotaskInfo = renderAutotaskInfo;
|
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) {
|
if (btnAutotask) {
|
||||||
btnAutotask.addEventListener('click', function () {
|
btnAutotask.addEventListener('click', function () {
|
||||||
if (!currentRunId) { alert('Select a run first.'); return; }
|
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++) {
|
for (var i = 0; i < runs.length; i++) {
|
||||||
if (String(runs[i].id) === String(keepRunId)) { idx = i; break; }
|
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.');
|
else alert(e.message || 'Failed.');
|
||||||
})
|
})
|
||||||
.finally(function () {
|
.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;
|
currentRunId = run.id || null;
|
||||||
if (window.__rcmClearCreateStatus) window.__rcmClearCreateStatus();
|
if (window.__rcmClearCreateStatus) window.__rcmClearCreateStatus();
|
||||||
if (window.__rcmRenderAutotaskInfo) window.__rcmRenderAutotaskInfo(run);
|
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) {
|
if (btnMarkSuccessOverride) {
|
||||||
var _rs = (run.status || '').toString().toLowerCase();
|
var _rs = (run.status || '').toString().toLowerCase();
|
||||||
var _canOverride = !!currentRunId && !run.missed && (_rs.indexOf('override') === -1) && (_rs.indexOf('success') === -1);
|
var _canOverride = !!currentRunId && !run.missed && (_rs.indexOf('override') === -1) && (_rs.indexOf('success') === -1);
|
||||||
|
|||||||
@ -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.
|
- 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.
|
- 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
|
## v0.1.21
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user