Auto-commit local changes before build (2026-02-03 10:31:44)
This commit is contained in:
parent
3400af58d7
commit
3c7f4c7926
@ -1 +1 @@
|
||||
v20260120-11-runchecks-autotask-status-label
|
||||
v20260203-01-autotask-resolve-note
|
||||
|
||||
@ -483,6 +483,44 @@ class AutotaskClient:
|
||||
raise AutotaskError("Autotask did not return a ticket object.")
|
||||
|
||||
|
||||
def update_ticket(self, ticket_payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Update a Ticket via PUT /Tickets.
|
||||
|
||||
Autotask does not support PATCH for Tickets. PUT behaves as a full update.
|
||||
Callers must construct a valid payload (typically by copying required fields
|
||||
from a fresh GET /Tickets/{id} response) and changing only intended fields.
|
||||
"""
|
||||
|
||||
if not isinstance(ticket_payload, dict):
|
||||
raise AutotaskError("Invalid ticket payload.")
|
||||
|
||||
try:
|
||||
tid = int(ticket_payload.get("id") or 0)
|
||||
except Exception:
|
||||
tid = 0
|
||||
if tid <= 0:
|
||||
raise AutotaskError("Invalid ticket id in payload.")
|
||||
|
||||
data = self._request("PUT", "Tickets", json_body=ticket_payload)
|
||||
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
|
||||
# Some environments return the raw object
|
||||
if "id" in data:
|
||||
return data
|
||||
|
||||
items = self._as_items_list(data)
|
||||
if items:
|
||||
return items[0]
|
||||
|
||||
# PUT may return an empty body in some tenants; treat as success.
|
||||
return {"id": tid}
|
||||
|
||||
|
||||
def get_resource(self, resource_id: int) -> Dict[str, Any]:
|
||||
"""Retrieve a Resource by Autotask Resource ID.
|
||||
|
||||
|
||||
@ -1805,6 +1805,97 @@ def api_run_checks_autotask_link_existing_ticket():
|
||||
"internal_ticket_id": int(getattr(internal_ticket, "id", 0) or 0) if internal_ticket else 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@main_bp.post("/api/run-checks/autotask-resolve-note")
|
||||
@login_required
|
||||
@roles_required("admin", "operator")
|
||||
def api_run_checks_autotask_resolve_note():
|
||||
"""Post a 'should be resolved' update to an existing Autotask ticket.
|
||||
|
||||
This first-step implementation does NOT close the ticket in Autotask.
|
||||
It updates the Ticket description via PUT /Tickets (TicketNotes create is
|
||||
not reliably supported across tenants).
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
if not getattr(run, "autotask_ticket_id", None):
|
||||
return jsonify({"status": "error", "message": "Run has no Autotask ticket linked."}), 400
|
||||
|
||||
try:
|
||||
ticket_id = int(run.autotask_ticket_id)
|
||||
except Exception:
|
||||
ticket_id = 0
|
||||
|
||||
if ticket_id <= 0:
|
||||
return jsonify({"status": "error", "message": "Run has an invalid Autotask ticket id."}), 400
|
||||
|
||||
try:
|
||||
client = _build_autotask_client_from_settings()
|
||||
t = client.get_ticket(ticket_id)
|
||||
except Exception as exc:
|
||||
return jsonify({"status": "error", "message": f"Autotask ticket retrieval failed: {exc}"}), 400
|
||||
|
||||
if not isinstance(t, dict):
|
||||
return jsonify({"status": "error", "message": "Autotask did not return a ticket object."}), 400
|
||||
|
||||
# Build an update payload based on known required fields from Postman validation.
|
||||
required_fields = ["id", "companyID", "queueID", "title", "priority", "status", "dueDateTime"]
|
||||
missing = [f for f in required_fields if t.get(f) in (None, "")]
|
||||
if missing:
|
||||
return jsonify(
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Cannot safely update Autotask ticket because required fields are missing: "
|
||||
+ ", ".join(missing),
|
||||
}
|
||||
), 400
|
||||
|
||||
existing_desc = str(t.get("description") or "")
|
||||
|
||||
now = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%SZ")
|
||||
actor = (getattr(current_user, "email", None) or getattr(current_user, "username", None) or "operator")
|
||||
marker = "[Backupchecks] Marked as resolved in Backupchecks"
|
||||
note = f"\n\n{marker} (ticket remains open in Autotask). {now} by {actor}"
|
||||
|
||||
# Avoid runaway growth if the same action is clicked multiple times.
|
||||
if marker in existing_desc:
|
||||
new_desc = existing_desc
|
||||
else:
|
||||
new_desc = (existing_desc or "") + note
|
||||
|
||||
payload = {
|
||||
"id": int(t.get("id")),
|
||||
"companyID": t.get("companyID"),
|
||||
"queueID": t.get("queueID"),
|
||||
"title": t.get("title"),
|
||||
"priority": t.get("priority"),
|
||||
"status": t.get("status"),
|
||||
"dueDateTime": t.get("dueDateTime"),
|
||||
"description": new_desc,
|
||||
}
|
||||
|
||||
try:
|
||||
client.update_ticket(payload)
|
||||
except Exception as exc:
|
||||
return jsonify({"status": "error", "message": f"Autotask ticket update failed: {exc}"}), 400
|
||||
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
@main_bp.post("/api/run-checks/mark-reviewed")
|
||||
@login_required
|
||||
@roles_required("admin", "operator")
|
||||
|
||||
@ -220,6 +220,7 @@
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div class="fw-semibold">Autotask ticket</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-success" id="rcm_autotask_resolve_note">Resolve</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="rcm_autotask_link_existing">Link existing</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="rcm_autotask_create">Create</button>
|
||||
</div>
|
||||
@ -891,6 +892,7 @@ table.addEventListener('change', function (e) {
|
||||
|
||||
function bindInlineCreateForms() {
|
||||
var btnAutotask = document.getElementById('rcm_autotask_create');
|
||||
var btnAutotaskResolveNote = document.getElementById('rcm_autotask_resolve_note');
|
||||
var btnAutotaskLink = document.getElementById('rcm_autotask_link_existing');
|
||||
var atInfo = document.getElementById('rcm_autotask_info');
|
||||
var atStatus = document.getElementById('rcm_autotask_status');
|
||||
@ -911,6 +913,7 @@ table.addEventListener('change', function (e) {
|
||||
|
||||
function setDisabled(disabled) {
|
||||
if (btnAutotask) btnAutotask.disabled = disabled;
|
||||
if (btnAutotaskResolveNote) btnAutotaskResolveNote.disabled = disabled;
|
||||
if (btnAutotaskLink) btnAutotaskLink.disabled = disabled;
|
||||
if (btnTicket) btnTicket.disabled = disabled;
|
||||
if (tCode) tCode.disabled = disabled;
|
||||
@ -963,6 +966,12 @@ table.addEventListener('change', function (e) {
|
||||
// Link existing is only meaningful when there is no PSA ticket linked to this run.
|
||||
btnAutotaskLink.style.display = hasAtTicket ? 'none' : '';
|
||||
}
|
||||
|
||||
if (btnAutotaskResolveNote) {
|
||||
var hasTicket = !!(run && run.autotask_ticket_id);
|
||||
// Resolve note is only meaningful when there is an active linked PSA ticket.
|
||||
btnAutotaskResolveNote.style.display = (hasTicket && !isResolved && !isDeleted) ? '' : 'none';
|
||||
}
|
||||
}
|
||||
window.__rcmRenderAutotaskInfo = renderAutotaskInfo;
|
||||
|
||||
@ -1177,6 +1186,46 @@ table.addEventListener('change', function (e) {
|
||||
});
|
||||
}
|
||||
|
||||
if (btnAutotaskResolveNote) {
|
||||
btnAutotaskResolveNote.addEventListener('click', function () {
|
||||
if (!currentRunId) { alert('Select a run first.'); return; }
|
||||
clearStatus();
|
||||
if (!confirm('Add an update to the existing Autotask ticket that it should be resolved?\n\nThis will NOT close the ticket in Autotask.')) return;
|
||||
if (atStatus) atStatus.textContent = 'Posting update...';
|
||||
btnAutotaskResolveNote.disabled = true;
|
||||
apiJson('/api/run-checks/autotask-resolve-note', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({run_id: currentRunId})
|
||||
})
|
||||
.then(function (j) {
|
||||
if (!j || j.status !== 'ok') throw new Error((j && j.message) || 'Failed.');
|
||||
if (atStatus) atStatus.textContent = 'Update posted.';
|
||||
// Keep UI consistent (button visibility depends on run state).
|
||||
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;
|
||||
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; }
|
||||
}
|
||||
renderRun(payload, idx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function (e) {
|
||||
if (atStatus) atStatus.textContent = e.message || 'Failed.';
|
||||
else alert(e.message || 'Failed.');
|
||||
})
|
||||
.finally(function () {
|
||||
btnAutotaskResolveNote.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (btnRemark) {
|
||||
btnRemark.addEventListener('click', function () {
|
||||
if (!currentRunId) { alert('Select a run first.'); return; }
|
||||
|
||||
@ -457,6 +457,14 @@ Changes:
|
||||
- Added a safe fallback chain so the UI shows:
|
||||
statusLabel (API) -> status_label (legacy) -> numeric status.
|
||||
|
||||
## v20260203-01-autotask-resolve-note
|
||||
|
||||
- Added a Resolve button to the Autotask ticket section on the Run Checks page.
|
||||
- Resolve action does NOT close the Autotask ticket.
|
||||
- Implemented functionality to add a note/update to the existing Autotask ticket indicating it is marked as resolved from Backupchecks.
|
||||
- Added backend API endpoint to handle the resolve-note action.
|
||||
- Extended Autotask client with a helper to update existing tickets via PUT.
|
||||
|
||||
***
|
||||
|
||||
## v0.1.21
|
||||
|
||||
Loading…
Reference in New Issue
Block a user