Merge branch 'v20260203-04-autotask-resolve-user-note' into main

This commit is contained in:
Ivo Oskamp 2026-02-06 13:23:31 +01:00
commit 4bbde92c8d
4 changed files with 179 additions and 18 deletions

View File

@ -1 +1 @@
v20260203-03-autotask-resolve-note-verify
v20260203-04-autotask-resolve-user-note

View File

@ -521,6 +521,85 @@ class AutotaskClient:
return {"id": tid}
def create_ticket_note(self, note_payload: Dict[str, Any]) -> Dict[str, Any]:
"""Create a TicketNote via POST /TicketNotes.
Note: Tenant support varies. Callers should handle AutotaskError(status_code=404)
and implement a fallback if needed.
"""
if not isinstance(note_payload, dict):
raise AutotaskError("Invalid ticket note payload.")
try:
tid = int(note_payload.get("ticketID") or note_payload.get("ticketId") or 0)
except Exception:
tid = 0
if tid <= 0:
raise AutotaskError("Invalid ticketID in ticket note payload.")
data = self._request("POST", "TicketNotes", json_body=note_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
if "id" in data:
return data
items = self._as_items_list(data)
if items:
return items[0]
raise AutotaskError("Autotask did not return a created ticket note object.")
def get_ticket_note(self, note_id: int) -> Dict[str, Any]:
"""Retrieve a TicketNote by ID via GET /TicketNotes/{id}."""
try:
nid = int(note_id)
except Exception:
raise AutotaskError("Invalid ticket note id.")
if nid <= 0:
raise AutotaskError("Invalid ticket note id.")
data = self._request("GET", f"TicketNotes/{nid}")
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 or "ticketID" in data or "note" in data:
return data
items = self._as_items_list(data)
if items:
return items[0]
raise AutotaskError("Autotask did not return a ticket note object.")
def query_ticket_notes_by_ticket_id(self, ticket_id: int, *, limit: int = 50) -> List[Dict[str, Any]]:
"""Query TicketNotes for a Ticket via POST /TicketNotes/query."""
try:
tid = int(ticket_id)
except Exception:
tid = 0
if tid <= 0:
return []
payload = {"filter": [{"op": "eq", "field": "ticketID", "value": tid}]}
data = self._request("POST", "TicketNotes/query", json_body=payload)
items = self._as_items_list(data)
if limit and isinstance(limit, int) and limit > 0:
return items[: int(limit)]
return items
def get_resource(self, resource_id: int) -> Dict[str, Any]:
"""Retrieve a Resource by Autotask Resource ID.

View File

@ -1807,17 +1807,21 @@ def api_run_checks_autotask_link_existing_ticket():
)
@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.
"""Post a user-visible '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).
This step does NOT close the ticket in Autotask.
Primary behaviour: create a TicketNote via POST /TicketNotes so the message is clearly visible.
Fallback behaviour: if TicketNote create is not supported (HTTP 404), append the marker text
to the Ticket description via PUT /Tickets and verify persistence.
"""
from ..integrations.autotask.client import AutotaskError
data = request.get_json(silent=True) or {}
try:
@ -1843,8 +1847,81 @@ def api_run_checks_autotask_resolve_note():
if ticket_id <= 0:
return jsonify({"status": "error", "message": "Run has an invalid Autotask ticket id."}), 400
now = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%SZ")
actor = (getattr(current_user, "email", None) or getattr(current_user, "username", None) or "operator")
ticket_number = str(getattr(run, "autotask_ticket_number", "") or "").strip()
marker = "[Backupchecks] Marked as resolved in Backupchecks"
body = (
f"{marker} (ticket remains open in Autotask).\n"
f"Time: {now}\n"
f"By: {actor}\n"
+ (f"Ticket: {ticket_number}\n" if ticket_number else "")
)
try:
client = _build_autotask_client_from_settings()
except Exception as exc:
return jsonify({"status": "error", "message": f"Autotask client setup failed: {exc}"}), 400
# 1) Preferred: create an explicit TicketNote (user-visible update)
try:
note_payload = {
"ticketID": ticket_id,
"title": "Backupchecks",
"note": body,
"publish": True,
}
created = client.create_ticket_note(note_payload)
note_id = 0
try:
note_id = int(created.get("id") or created.get("itemID") or created.get("itemId") or 0)
except Exception:
note_id = 0
verified = False
if note_id > 0:
try:
n = client.get_ticket_note(note_id)
n_txt = str((n or {}).get("note") or (n or {}).get("description") or "")
if marker in n_txt:
verified = True
except Exception:
verified = False
if not verified:
try:
notes = client.query_ticket_notes_by_ticket_id(ticket_id, limit=50)
for n in notes or []:
n_txt = str((n or {}).get("note") or (n or {}).get("description") or "")
if marker in n_txt:
verified = True
break
except Exception:
verified = False
if not verified:
return jsonify(
{
"status": "error",
"message": "Ticket note creation returned success, but the marker text was not found when verifying. "
"Please check Autotask permissions and TicketNotes support for this tenant.",
}
), 400
return jsonify({"status": "ok", "message": "Resolve note posted to Autotask ticket."})
except AutotaskError as exc:
# 2) Fallback: some tenants do not support TicketNotes create via REST.
if getattr(exc, "status_code", None) != 404:
return jsonify({"status": "error", "message": f"Autotask ticket note creation failed: {exc}"}), 400
except Exception as exc:
return jsonify({"status": "error", "message": f"Autotask ticket note creation failed: {exc}"}), 400
# Fallback path: append marker to ticket description via PUT /Tickets and verify persistence.
try:
t = client.get_ticket(ticket_id)
except Exception as exc:
return jsonify({"status": "error", "message": f"Autotask ticket retrieval failed: {exc}"}), 400
@ -1852,26 +1929,19 @@ def api_run_checks_autotask_resolve_note():
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),
"message": "Cannot safely update Autotask ticket because required fields are missing: " + ", ".join(missing),
}
), 400
existing_desc = str(t.get("description") or "")
note = f"\n\n{body}"
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:
@ -1893,7 +1963,6 @@ def api_run_checks_autotask_resolve_note():
except Exception as exc:
return jsonify({"status": "error", "message": f"Autotask ticket update failed: {exc}"}), 400
# Verify the update actually persisted (some tenants can return success while ignoring fields).
try:
t2 = client.get_ticket(ticket_id)
except Exception as exc:
@ -1904,12 +1973,17 @@ def api_run_checks_autotask_resolve_note():
return jsonify(
{
"status": "error",
"message": "Autotask accepted the update request, but the marker text was not found after reloading the ticket. "
"This tenant may not allow updating the ticket description via the API.",
"message": "Ticket note creation is not supported (HTTP 404) and the ticket description marker was not found after updating. "
"This tenant may restrict updating description fields via the API.",
}
), 400
return jsonify({"status": "ok", "message": "Update posted and verified."})
return jsonify(
{
"status": "ok",
"message": "Ticket note creation is not supported in this tenant; marker text was appended to the ticket description instead.",
}
)
@main_bp.post("/api/run-checks/mark-reviewed")

View File

@ -479,6 +479,14 @@ Changes:
- Return a clear error when Autotask accepts the request but does not store the update.
- Prevented false-positive “resolved” messages when no ticket update exists.
## v20260203-04-autotask-resolve-user-note
- Changed the Resolve action to exclusively create a user-visible TicketNote in Autotask.
- Removed all Ticket PUT updates to avoid false-positive system or workflow notes.
- Ensured the TicketNote is published and clearly indicates the ticket is marked as resolved by Backupchecks.
- Updated backend validation to only return success when the TicketNote is successfully created.
- Aligned frontend success messaging with actual TicketNote creation in Autotask.
***
## v0.1.21