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.")
|
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]:
|
def get_resource(self, resource_id: int) -> Dict[str, Any]:
|
||||||
"""Retrieve a Resource by Autotask Resource ID.
|
"""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,
|
"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")
|
@main_bp.post("/api/run-checks/mark-reviewed")
|
||||||
@login_required
|
@login_required
|
||||||
@roles_required("admin", "operator")
|
@roles_required("admin", "operator")
|
||||||
|
|||||||
@ -220,6 +220,7 @@
|
|||||||
<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>
|
||||||
<div class="d-flex gap-2">
|
<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-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>
|
<button type="button" class="btn btn-sm btn-outline-primary" id="rcm_autotask_create">Create</button>
|
||||||
</div>
|
</div>
|
||||||
@ -891,6 +892,7 @@ table.addEventListener('change', function (e) {
|
|||||||
|
|
||||||
function bindInlineCreateForms() {
|
function bindInlineCreateForms() {
|
||||||
var btnAutotask = document.getElementById('rcm_autotask_create');
|
var btnAutotask = document.getElementById('rcm_autotask_create');
|
||||||
|
var btnAutotaskResolveNote = document.getElementById('rcm_autotask_resolve_note');
|
||||||
var btnAutotaskLink = document.getElementById('rcm_autotask_link_existing');
|
var btnAutotaskLink = document.getElementById('rcm_autotask_link_existing');
|
||||||
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');
|
||||||
@ -911,6 +913,7 @@ table.addEventListener('change', function (e) {
|
|||||||
|
|
||||||
function setDisabled(disabled) {
|
function setDisabled(disabled) {
|
||||||
if (btnAutotask) btnAutotask.disabled = disabled;
|
if (btnAutotask) btnAutotask.disabled = disabled;
|
||||||
|
if (btnAutotaskResolveNote) btnAutotaskResolveNote.disabled = disabled;
|
||||||
if (btnAutotaskLink) btnAutotaskLink.disabled = disabled;
|
if (btnAutotaskLink) btnAutotaskLink.disabled = disabled;
|
||||||
if (btnTicket) btnTicket.disabled = disabled;
|
if (btnTicket) btnTicket.disabled = disabled;
|
||||||
if (tCode) tCode.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.
|
// Link existing is only meaningful when there is no PSA ticket linked to this run.
|
||||||
btnAutotaskLink.style.display = hasAtTicket ? 'none' : '';
|
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;
|
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) {
|
if (btnRemark) {
|
||||||
btnRemark.addEventListener('click', function () {
|
btnRemark.addEventListener('click', function () {
|
||||||
if (!currentRunId) { alert('Select a run first.'); return; }
|
if (!currentRunId) { alert('Select a run first.'); return; }
|
||||||
|
|||||||
@ -457,6 +457,14 @@ Changes:
|
|||||||
- Added a safe fallback chain so the UI shows:
|
- Added a safe fallback chain so the UI shows:
|
||||||
statusLabel (API) -> status_label (legacy) -> numeric status.
|
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
|
## v0.1.21
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user