Auto-commit local changes before build (2026-02-03 10:31:44)

This commit is contained in:
Ivo Oskamp 2026-02-03 10:31:44 +01:00
parent 3400af58d7
commit 3c7f4c7926
5 changed files with 187 additions and 1 deletions

View File

@ -1 +1 @@
v20260120-11-runchecks-autotask-status-label
v20260203-01-autotask-resolve-note

View File

@ -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.

View File

@ -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")

View File

@ -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; }

View File

@ -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