From b46b7fbc21b03d859177c5c811a4585c3b5e2654 Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Fri, 16 Jan 2026 12:28:07 +0100 Subject: [PATCH] Auto-commit local changes before build (2026-01-16 12:28:07) --- .last-branch | 2 +- .../app/integrations/autotask/client.py | 104 ++++ .../src/backend/app/main/routes_run_checks.py | 104 ++++ .../src/templates/main/run_checks.html | 70 ++- ...ation_functional_design_living_document.md | 412 ++++++++++++++++ ...k_integration_functional_design_phase_1.md | 464 ------------------ ...task_integration_phase_2_implementation.md | 432 ++++++++++++++++ docs/changelog.md | 11 + 8 files changed, 1128 insertions(+), 471 deletions(-) create mode 100644 docs/backupchecks_autotask_integration_functional_design_living_document.md delete mode 100644 docs/backupchecks_autotask_integration_functional_design_phase_1.md create mode 100644 docs/backupchecks_autotask_integration_phase_2_implementation.md diff --git a/.last-branch b/.last-branch index fabe856..bb1550c 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260116-03-autotask-ticket-linking-visibility +v20260116-04-runchecks-autotask-ticket-polling diff --git a/containers/backupchecks/src/backend/app/integrations/autotask/client.py b/containers/backupchecks/src/backend/app/integrations/autotask/client.py index 609dc74..722955a 100644 --- a/containers/backupchecks/src/backend/app/integrations/autotask/client.py +++ b/containers/backupchecks/src/backend/app/integrations/autotask/client.py @@ -467,6 +467,110 @@ class AutotaskClient: return data raise AutotaskError("Autotask did not return a ticket object.") + def query_tickets_by_ids( + self, + ticket_ids: List[int], + *, + max_records_per_query: int = 200, + corr_id: Optional[str] = None, + ) -> List[Dict[str, Any]]: + """Fetch multiple Tickets by id. + + Preferred path: + - Use GET Tickets/query with an 'in' filter over id. + + Fallback path: + - If the tenant does not support 'in' queries, fetch tickets individually + via GET Tickets/. + + Returns a list of ticket objects (dicts) for tickets that exist. + """ + + # Normalize ids + ids: List[int] = [] + for x in ticket_ids or []: + try: + xi = int(x) + except Exception: + continue + if xi > 0: + ids.append(xi) + + # De-duplicate while preserving order + seen = set() + dedup: List[int] = [] + for xi in ids: + if xi in seen: + continue + seen.add(xi) + dedup.append(xi) + + if not dedup: + return [] + + corr = corr_id or uuid.uuid4().hex[:10] + + def _chunk(lst: List[int], n: int) -> List[List[int]]: + return [lst[i : i + n] for i in range(0, len(lst), n)] + + out: List[Dict[str, Any]] = [] + + # Try query with op=in first (chunked) + try: + for chunk in _chunk(dedup, max(1, int(max_records_per_query))): + search_payload: Dict[str, Any] = { + "filter": [ + {"op": "in", "field": "id", "value": chunk}, + ], + "maxRecords": len(chunk), + } + params = {"search": json.dumps(search_payload)} + if self._debug_enabled(): + logger.info( + "[autotask][%s] Tickets/query ids payload=%s", + corr, + self._safe_json_preview(search_payload, max_chars=1200), + ) + data = self._request("GET", "Tickets/query", params=params) + items = self._as_items_list(data) + for it in items: + if isinstance(it, dict) and it: + out.append(it) + return out + except AutotaskError as exc: + # Common tenant behavior: reject op=in with HTTP 400. + if self._debug_enabled(): + logger.info( + "[autotask][%s] Tickets/query ids op=in failed; falling back to per-ticket GET. error=%s", + corr, + str(exc), + ) + except Exception as exc: + if self._debug_enabled(): + logger.info( + "[autotask][%s] Tickets/query ids unexpected error; falling back. error=%s", + corr, + str(exc), + ) + + # Fallback: individual GET calls (best-effort) + for tid in dedup: + try: + t = self.get_ticket(int(tid)) + if isinstance(t, dict) and t: + out.append(t) + except AutotaskError as exc: + # 404 -> deleted/missing ticket, ignore + if getattr(exc, "status_code", None) == 404: + continue + # Any other error: continue best-effort + continue + except Exception: + continue + + return out + + def _lookup_created_ticket_id( self, tracking_identifier: str, diff --git a/containers/backupchecks/src/backend/app/main/routes_run_checks.py b/containers/backupchecks/src/backend/app/main/routes_run_checks.py index 7dc5b83..9b2c06d 100644 --- a/containers/backupchecks/src/backend/app/main/routes_run_checks.py +++ b/containers/backupchecks/src/backend/app/main/routes_run_checks.py @@ -876,6 +876,110 @@ def run_checks_details(): return jsonify({"status": "ok", "job": job_payload, "runs": runs_payload}) + + +@main_bp.get("/api/run-checks/autotask-ticket-poll") +@login_required +@roles_required("admin", "operator") +def api_run_checks_autotask_ticket_poll(): + """Read-only polling of Autotask ticket state for Run Checks. + + Important: + - No Backupchecks state is modified. + - No mutations are performed in Autotask. + - This endpoint is intended to be called only from the Run Checks page. + """ + + include_reviewed = False + if get_active_role() == "admin": + include_reviewed = request.args.get("include_reviewed", "0") in ("1", "true", "yes", "on") + + # Only consider recently relevant runs to keep the payload small. + # We intentionally avoid unbounded history polling. + days = 60 + try: + days = int(request.args.get("days", "60")) + except Exception: + days = 60 + if days < 1: + days = 1 + if days > 180: + days = 180 + + now_utc = datetime.utcnow().replace(tzinfo=None) + window_start = now_utc - timedelta(days=days) + + q = JobRun.query.filter(JobRun.autotask_ticket_id.isnot(None)) + if not include_reviewed: + q = q.filter(JobRun.reviewed_at.is_(None)) + + # Only poll runs in our time window. + q = q.filter(func.coalesce(JobRun.run_at, JobRun.created_at) >= window_start) + + runs = ( + q.order_by(func.coalesce(JobRun.run_at, JobRun.created_at).desc(), JobRun.id.desc()) + .limit(400) + .all() + ) + + ticket_ids = [] + seen = set() + for r in runs: + tid = getattr(r, "autotask_ticket_id", None) + try: + tid_int = int(tid) + except Exception: + continue + if tid_int <= 0 or tid_int in seen: + continue + seen.add(tid_int) + ticket_ids.append(tid_int) + + if not ticket_ids: + return jsonify({"status": "ok", "tickets": []}) + + # If integration is disabled, do not fail the page. + settings = _get_or_create_settings() + if not getattr(settings, "autotask_enabled", False): + return jsonify({"status": "ok", "tickets": [], "autotask_enabled": False}) + + try: + client = _build_autotask_client_from_settings() + except Exception as exc: + return jsonify({"status": "ok", "tickets": [], "autotask_enabled": True, "message": str(exc)}) + + corr_id = datetime.utcnow().strftime("rcpoll-%Y%m%d%H%M%S") + + # Query tickets in Autotask (best-effort) + tickets = [] + try: + tickets = client.query_tickets_by_ids(ticket_ids, corr_id=corr_id) + except Exception: + tickets = [] + + # Build a minimal payload for UI use. + out = [] + for t in tickets or []: + if not isinstance(t, dict): + continue + tid = t.get("id") + try: + tid_int = int(tid) + except Exception: + continue + + out.append( + { + "id": tid_int, + "ticketNumber": (t.get("ticketNumber") or t.get("TicketNumber") or "") or "", + "status": t.get("status"), + "statusName": (t.get("statusName") or t.get("StatusName") or "") or "", + "title": (t.get("title") or t.get("Title") or "") or "", + "lastActivityDate": (t.get("lastActivityDate") or t.get("LastActivityDate") or t.get("lastActivity") or "") or "", + } + ) + + return jsonify({"status": "ok", "tickets": out, "autotask_enabled": True}) @main_bp.post("/api/run-checks/autotask-ticket") @login_required @roles_required("admin", "operator") diff --git a/containers/backupchecks/src/templates/main/run_checks.html b/containers/backupchecks/src/templates/main/run_checks.html index 7cfcd15..be31ea8 100644 --- a/containers/backupchecks/src/templates/main/run_checks.html +++ b/containers/backupchecks/src/templates/main/run_checks.html @@ -297,10 +297,48 @@ var currentRunId = null; var currentPayload = null; + // Phase 2.1: Read-only Autotask ticket polling (Run Checks page only) + // Cache shape: { : {id, ticketNumber, status, statusName, title, lastActivityDate} } + var autotaskTicketPollCache = {}; + + function pollAutotaskTicketsOnPageOpen() { + // Only execute on Run Checks page load. + var url = '/api/run-checks/autotask-ticket-poll'; + var qs = []; + // include_reviewed is only meaningful for admins + try { + var includeReviewed = {{ 'true' if include_reviewed else 'false' }}; + if (includeReviewed) qs.push('include_reviewed=1'); + } catch (e) {} + if (qs.length) url += '?' + qs.join('&'); + + fetch(url) + .then(function (r) { return r.json(); }) + .then(function (j) { + if (!j || j.status !== 'ok') return; + autotaskTicketPollCache = {}; + var list = (j.tickets || []); + for (var i = 0; i < list.length; i++) { + var t = list[i] || {}; + var id = parseInt(t.id, 10); + if (!Number.isFinite(id) || id <= 0) continue; + autotaskTicketPollCache[id] = t; + } + window.__rcAutotaskTicketPollCache = autotaskTicketPollCache; + }) + .catch(function () { + autotaskTicketPollCache = {}; + window.__rcAutotaskTicketPollCache = autotaskTicketPollCache; + }); + } + + var btnMarkAllReviewed = document.getElementById('rcm_mark_all_reviewed'); var btnMarkSuccessOverride = document.getElementById('rcm_mark_success_override'); - // Shift-click range selection for checkbox rows + pollAutotaskTicketsOnPageOpen(); + +// Shift-click range selection for checkbox rows var lastCheckedCb = null; @@ -864,14 +902,34 @@ table.addEventListener('change', function (e) { function renderAutotaskInfo(run) { if (!atInfo) return; var num = (run && run.autotask_ticket_number) ? String(run.autotask_ticket_number) : ''; + var tid = (run && run.autotask_ticket_id) ? parseInt(run.autotask_ticket_id, 10) : null; + var polled = (tid && autotaskTicketPollCache && autotaskTicketPollCache[tid]) ? autotaskTicketPollCache[tid] : null; + + var lines = []; if (num) { - atInfo.innerHTML = '
Ticket: ' + escapeHtml(num) + '
'; - } else if (run && run.autotask_ticket_id) { - atInfo.innerHTML = '
Ticket: created
'; + lines.push('
Ticket: ' + escapeHtml(num) + '
'); + } else if (tid) { + lines.push('
Ticket: created
'); } else { - atInfo.innerHTML = '
No Autotask ticket created for this run.
'; + lines.push('
No Autotask ticket created for this run.
'); } - } + + // Phase 2.1 visibility only: show last polled status if available + if (tid) { + if (polled) { + var statusName = (polled.statusName || '').toString().trim(); + var statusVal = (polled.status !== undefined && polled.status !== null) ? String(polled.status) : ''; + var label = statusName ? statusName : (statusVal ? ('Status ' + statusVal) : ''); + if (label) { + lines.push('
PSA status (polled): ' + escapeHtml(label) + '
'); + } + } else { + lines.push('
PSA status (polled): not available
'); + } + } + + atInfo.innerHTML = lines.join(''); +} window.__rcmRenderAutotaskInfo = renderAutotaskInfo; if (btnAutotask) { diff --git a/docs/backupchecks_autotask_integration_functional_design_living_document.md b/docs/backupchecks_autotask_integration_functional_design_living_document.md new file mode 100644 index 0000000..d544481 --- /dev/null +++ b/docs/backupchecks_autotask_integration_functional_design_living_document.md @@ -0,0 +1,412 @@ +# Backupchecks – Autotask Integration + +## Functional Design + +*Last updated: 2026-01-16* + +--- + +## 1. Scope & Goals + +This document describes the **functional design and agreed decisions** for the first phase of the Autotask integration in Backupchecks. + +Goals for phase 1: + +- Allow operators to **manually create Autotask tickets** from Backupchecks. +- Ensure **full operator control** over when a ticket is created. +- Prevent ticket spam and duplicate tickets. +- Maintain clear ownership between Backupchecks and Autotask. +- Provide a safe and auditable way to resolve tickets from Backupchecks. + +Out of scope for phase 1: + +- Automatic ticket creation +- Automatic ticket closing on success +- Issue correlation across multiple runs +- Time entry creation or modification + +--- + +## 2. Core Principles (Leading) + +These principles apply to all design and implementation choices: + +- Autotask is an **external authoritative system** (PSA). +- Backupchecks is a **consumer**, not an owner, of PSA data. +- **IDs are leading**, names are display-only. +- All PSA mappings are **explicit**, never implicit or automatic. +- Operators always retain **manual control**. +- Renaming in Autotask must **never break mappings**. + +--- + +## 3. Customer ↔ Autotask Company Mapping + +### 3.1 Mapping model + +- Mapping is configured in the **Customers** screen. +- Mapping is a **1-to-1 explicit relationship**. +- Stored values per customer: + - PSA type: autotask + - Autotask Company ID (leading) + - Autotask Company Name (cached for display) + - Last sync timestamp + - Mapping status: ok | renamed | missing | invalid + +The Autotask Company ID is the source of truth. The name exists only for UI clarity. + +### 3.2 Name synchronisation + +- If the company name is changed in Autotask: + - Backupchecks updates the cached name automatically. + - The mapping remains intact. +- Backupchecks customer names are independent and never overwritten. + +### 3.3 Failure scenarios + +- Autotask company deleted or inaccessible: + - Mapping status becomes invalid. + - Ticket creation is blocked. + - UI clearly indicates broken mapping. + +--- + +## 4. Ticket Creation Model + +### 4.1 Operator-driven creation + +- Tickets are created only via an explicit operator action. +- Location: Run Checks page. +- Manual ticket number input is removed. +- A new action replaces it: Create Autotask ticket. + +### 4.2 One ticket per run + +- Exactly one ticket per run. +- A run can never create multiple tickets. +- If a ticket exists: + - Creation action is replaced by Open ticket. + +### 4.3 Ticket contents (baseline) + +Minimum ticket fields: + +- Subject: [Backupchecks] - - +- Description: + - Run date/time + - Backup type and job + - Affected objects + - Error or warning messages + - Reference to Backupchecks + +--- + +## 5. Ticket State Tracking in Backupchecks + +Per run, Backupchecks stores: + +- Autotask Ticket ID +- Autotask Ticket Number +- Ticket URL +- Created by +- Created at timestamp +- Last known ticket status (snapshot) + +--- + +## 6. Ticket Resolution from Backupchecks + +Backupchecks may resolve a ticket only if: + +- The ticket exists +- The ticket is not already closed +- No time entries are present + +If time entries exist, the ticket is not closed and an internal system note is added. + +--- + +## 7. Backupchecks Settings + +Settings → Extensions & Integrations → Autotask + +Includes: + +- Enable integration (on/off) +- Environment +- API credentials +- Tracking Identifier +- Default queue and status +- Priority mapping + +### 7.1 Enable / disable behaviour (mandatory) + +Backupchecks must support switching the Autotask integration **on and off at any time**. + +When Autotask integration is **enabled**: +- Autotask actions are available (create ticket, resolve ticket, link existing ticket). +- Ticket polling/synchronisation (Phase 2) is active. + +When Autotask integration is **disabled**: +- Backupchecks falls back to the manual workflow: + - Operators can manually enter ticket numbers. + - No Autotask API calls are executed. + - No polling is performed. + - No resolve action is available (as it would require Autotask calls). +- Existing stored Autotask references remain visible for audit/history. + +The enable/disable switch must be reversible and must not require data deletion or migration. + +--- + +## 8. Roles & Permissions + +- Admin / Operator: create and resolve tickets +- Reporter: view-only access + +--- + +## 9. Explicit Non-Goals (Phase 1) + +- Automatic ticket creation +- Automatic ticket closing +- Time entry handling +- Multiple tickets per run +- PSA-side logic + +--- + +## 10. Phase 1 Summary + +Phase 1 delivers controlled, operator-driven PSA integration with a strong focus on auditability and predictability. + +--- + +## 11. Phase 2 – Ticket State Synchronisation (PSA-driven) + +This phase introduces **PSA-driven state awareness** for already linked tickets. The goal is to keep Backupchecks aligned with Autotask **without introducing active control or automation**. + +### 11.1 Polling strategy (Run Checks entry point) + +- Polling is executed **only when Autotask integration is enabled**. +- When the **Run Checks** page is opened, Backupchecks performs a **targeted poll** to Autotask. + +- When the **Run Checks** page is opened, Backupchecks performs a **targeted poll** to Autotask. +- Only tickets that are: + - Linked to runs shown on the page, and + - Not in a terminal state inside Backupchecks + are included. +- Tickets already marked as resolved or broken are excluded. + +This prevents unnecessary API calls and limits polling to operator-relevant context. + +### 11.2 Active-ticket-only retrieval + +- Backupchecks only queries tickets that are considered **active** in Autotask. +- Completed or closed tickets are not included in the active-ticket query. +- This ensures minimal load and avoids repeated retrieval of historical data. + +### 11.3 PSA-driven completion handling + +If Autotask reports a ticket with status **Completed**: + +- The linked run in Backupchecks is automatically marked as **Resolved**. +- The resolution is explicitly flagged as: + - **Resolved by PSA** +- Backupchecks does not add notes or modify the ticket in Autotask. + +UI behaviour: +- Operators can clearly see that the resolution originated from the PSA. +- A visual indicator highlights that the underlying backup issue may still require verification. + +### 11.4 Operator awareness and follow-up + +When a ticket is resolved by PSA: + +- Backupchecks does not assume the technical issue is resolved. +- Operators are expected to: + - Review the related run + - Decide whether further action is required inside Backupchecks + +No automatic reopening or ticket creation is performed. + +### 11.5 Deleted ticket detection + +If a linked ticket is **deleted in Autotask**: + +- Backupchecks detects this during polling. +- The ticket linkage is marked as: + - **Deleted in PSA** + +UI behaviour: +- A clear warning is shown to the operator. +- The historical ticket reference remains visible for audit purposes. +- Ticket-related actions are blocked until the operator: + - Links a replacement ticket, or + - Creates a new Autotask ticket + +### 11.6 Ticket resolution from Backupchecks (operator-driven) + +Phase 2 includes implementation of **manual ticket resolution** from Backupchecks under the already defined Phase 1 rules. + +- Resolution is always an explicit operator action (no automation). +- Resolution rules remain leading (see Chapter 6): + - Ticket must exist + - Ticket must not already be closed + - Ticket may only be closed by Backupchecks if **no time entries** exist + - If time entries exist, Backupchecks adds an internal system note and leaves the ticket open + +UI behaviour (Run Checks and Job Details): +- Show a Resolve ticket action only when a validated Autotask Ticket ID exists. +- When resolution succeeds, update the run to Resolved and store: + - Resolved timestamp + - Resolved by (operator) + - Resolution origin: Resolved by Backupchecks + +Important alignment with PSA-driven sync: +- If Autotask later reports the ticket as Completed, Backupchecks keeps the run resolved, but the origin remains: + - Resolved by Backupchecks +- If Autotask reports completion before the operator resolves it, Backupchecks sets: + - Resolved by PSA + +### 11.7 Explicit non-goals (Phase 2) + +The following remain explicitly out of scope: + +- Automatic ticket creation +- Automatic ticket reopening +- Automatic resolution without operator intent +- Status pushing from Backupchecks to Autotask (except the explicit Resolve action described above) +- Any modification of existing Autotask ticket content (except fixed-format internal system notes used during resolution rules) + +--- + +## 12. Future Design Considerations (Post-Phase 2) + +The following sections describe **future design intent only**. They are explicitly **out of scope for Phase 2** and introduce no implementation commitments. + +### 12.1 Ticket lifecycle awareness (read-only intelligence) + +Objective: +Provide better insight into the state of linked tickets without adding actions. + +Scope: +- Periodic read-only retrieval of ticket status +- Display of: + - Current status + - Queue + - Assigned resource (owner) + +Shown in: +- Run Checks +- Job Details +- Tickets / Remarks overview + +Explicitly excluded: +- No automatic actions +- No status synchronisation back to Autotask + +Value: +- Operators need to open Autotask less frequently +- Faster visibility into whether a ticket has been picked up + +### 12.2 Operator notes & context enrichment + +Objective: +Add contextual information to existing tickets without taking ownership away from the PSA. + +Scope: +- Add internal/system notes from Backupchecks +- Notes are always written in a fixed format +- Notes are added only by explicit operator action + +Rules: +- Never overwrite existing ticket content +- Never create customer-facing notes +- Never generate notes automatically + +Value: +- Tickets remain up to date without duplication +- Autotask remains the authoritative system + +### 12.3 Controlled assistance (semi-automatic support) + +Objective: +Support operator decision-making without introducing automation. + +Examples: +- Suggestion: a similar error already has an open ticket +- Suggestion: previous run was successful, consider resolving the ticket +- Highlight repeated failures without any linked ticket + +Important constraints: +- Suggestions only +- No automatic ticket creation +- No automatic ticket closure + +Value: +- Reduced human error +- No loss of operator control + +### 12.4 Cross-run correlation (analytical) + +Objective: +Provide insight into structural or recurring problems before tickets are created. + +Scope: +- Detection of repeated failures across multiple runs +- Detection of the same object with the same error over time +- Visualisation inside Backupchecks only + +Explicitly excluded: +- Ticket bundling +- Ticket merging +- Any Autotask-side modifications + +Value: +- Better decisions prior to ticket creation +- Reduced noise in the PSA + +### 12.5 Multi-PSA abstraction (design preparation) + +Objective: +Prepare the internal design for future PSA support without implementing it. + +Scope: +- PSA-agnostic internal models: + - Ticket + - Company mapping + - Resolution rules +- Autotask remains the only concrete implementation + +Why this is documented now: +- Prevents Autotask-specific technical debt +- Keeps the architecture open for other PSA platforms + +### 12.6 Governance & audit depth + +Objective: +Ensure full traceability for MSP and enterprise environments. + +Scope: +- Extended audit logging: + - Who created, linked or resolved a ticket + - When the action occurred + - From which run the action originated +- Read-only export capabilities +- Optional compliance-oriented views + +Value: +- MSP and enterprise readiness +- Supports internal and external audits + +### 12.7 Explicit non-directions + +The following are intentionally excluded unless a strategic decision is made later: + +- Automatic ticket creation +- Automatic ticket handling +- Time registration +- Mutation of existing ticket content +- PSA business logic inside Backupchecks + diff --git a/docs/backupchecks_autotask_integration_functional_design_phase_1.md b/docs/backupchecks_autotask_integration_functional_design_phase_1.md deleted file mode 100644 index 0575753..0000000 --- a/docs/backupchecks_autotask_integration_functional_design_phase_1.md +++ /dev/null @@ -1,464 +0,0 @@ -# Backupchecks – Autotask Integration - -## Functional Design – Phase 1 - -_Last updated: 2026-01-13_ - ---- - -## 1. Scope & Goals - -This document describes the **functional design and agreed decisions** for the first phase of the Autotask integration in Backupchecks. - -Goals for phase 1: -- Allow operators to **manually create Autotask tickets** from Backupchecks. -- Ensure **full operator control** over when a ticket is created. -- Prevent ticket spam and duplicate tickets. -- Maintain clear ownership between Backupchecks and Autotask. -- Provide a safe and auditable way to resolve tickets from Backupchecks. - -Out of scope for phase 1: -- Automatic ticket creation -- Automatic ticket closing on success -- Issue correlation across multiple runs -- Time entry creation or modification - ---- - -## 2. Core Principles (Leading) - -These principles apply to all design and implementation choices: - -- Autotask is an **external authoritative system** (PSA). -- Backupchecks is a **consumer**, not an owner, of PSA data. -- **IDs are leading**, names are display-only. -- All PSA mappings are **explicit**, never implicit or automatic. -- Operators always retain **manual control**. -- Renaming in Autotask must **never break mappings**. - ---- - -## 3. Customer ↔ Autotask Company Mapping - -### 3.1 Mapping model - -- Mapping is configured in the **Customers** screen. -- Mapping is a **1-to-1 explicit relationship**. -- Stored values per customer: - - PSA type: `autotask` - - Autotask Company ID (leading) - - Autotask Company Name (cached for display) - - Last sync timestamp - - Mapping status: `ok | renamed | missing | invalid` - -> **Note:** The Autotask Company ID is the source of truth. The name exists only for UI clarity. - -### 3.2 Name synchronisation - -- If the company name is changed in Autotask: - - Backupchecks updates the cached name automatically. - - The mapping remains intact. -- Backupchecks customer names are **independent** and never overwritten. - -### 3.3 Failure scenarios - -- Autotask company deleted or inaccessible: - - Mapping status becomes `invalid`. - - Ticket creation is blocked. - - UI clearly indicates broken mapping. - ---- - -## 4. Ticket Creation Model - -### 4.1 Operator-driven creation - -- Tickets are created **only** via an explicit operator action. -- Location: **Run Checks** page. -- Manual ticket number input is removed. -- A new action replaces it: - - **“Create Autotask ticket”** - -> **Rationale:** There are too many backup alerts that do not require a ticket. Human judgement remains essential. - -### 4.2 One ticket per run (Key decision) - -- **Exactly one ticket per Run**. -- A run can never create multiple tickets. -- If a ticket exists: - - Creation action is replaced by: - - “Open ticket” - - (Later) “Add note” - -> **Rationale:** Multiple errors within a run often share the same root cause. This prevents ticket flooding. - -### 4.3 Ticket contents (baseline) - -Minimum ticket fields: -- Subject: - - `[Backupchecks] - - ` -- Description: - - Run date/time - - Backup type and job - - Affected objects (e.g. HV01, USB Disk) - - Error / warning messages - - Reference to Backupchecks (URL or identifier) - -Optional (configurable later): -- Queue -- Issue type / category -- Priority mapping - ---- - -## 5. Ticket State Tracking in Backupchecks - -Per Run, Backupchecks stores: -- Autotask Ticket ID -- Autotask Ticket Number -- Ticket URL (optional) -- Created by (operator) -- Created at timestamp -- Last known ticket status (snapshot) - -This ensures: -- No duplicate tickets -- Full audit trail -n- Clear operator feedback - ---- - -## 5A. Ticket Content Composition Rules - -This chapter defines how Backupchecks determines **what content is placed in an Autotask ticket**, with the explicit goal of keeping tickets readable and actionable. - -### 5A.1 Guiding principle - -- A ticket is a **signal**, not a log file. -- The ticket must remain readable for the ticket owner. -- Full technical details always remain available in Backupchecks. - -### 5A.2 Content hierarchy (deterministic) - -Backupchecks applies the following strict hierarchy when composing ticket content: - -1. **Overall remark** (run-level summary) – if present, this is leading. -2. **Object-level messages** – used only when no overall remark exists. - -This hierarchy is fixed and non-configurable in phase 1. - -### 5A.3 Scenario A – Overall remark present - -If an overall remark exists for the run: -- The ticket description contains: - - The overall remark - - Job name, run date/time, and status -- Object-level errors are **not listed in full**. -- A short informational line is added: - - “Multiple objects reported errors. See Backupchecks for full details.” - -> **Rationale:** The overall remark already represents a consolidated summary. Listing many objects would reduce ticket clarity. - -### 5A.4 Scenario B – No overall remark - -If no overall remark exists: -- The ticket description includes object-level errors. -- Object listings are **explicitly limited**: - - A maximum of *N* objects (exact value defined during implementation) -- If more objects are present: - - “And X additional objects reported similar errors.” - -> **Rationale:** Prevents large, unreadable tickets while still providing concrete examples. - -### 5A.5 Mandatory reference to Backupchecks - -Every ticket created by Backupchecks must include a **direct link to the Job Details page of the originating run**. - -This link is intended as the primary navigation entry point for the ticket owner. - -The ticket description must include: -- Job name -- Run date/time -- A clickable URL pointing to the Job Details page of that run in Backupchecks - -> **Rationale:** The Job Details page provides the most complete and structured context for investigation. - -This ensures: -- Full traceability -- Fast access to complete technical details - ---- - -### 5A.6 Explicit exclusions - -The following content is deliberately excluded from ticket descriptions: -- Complete object lists when large -- Repeated identical error messages -- Raw technical dumps or stack traces - ---- - -## 6. Ticket Resolution from Backupchecks - -### 6.1 Resolution policy - -Backupchecks **may resolve** an Autotask ticket **only if**: -- The ticket exists -- The ticket is not already closed -- **No time entries are present on the ticket** - -This rule is **mandatory and non-configurable**. - -> **Rationale:** Prevents financial and operational conflicts inside Autotask. - -### 6.2 Behaviour when time entries exist - -If an operator clicks **Resolve ticket** but the ticket **contains time entries**: -- The ticket **must not be closed** by Backupchecks. -- Backupchecks **adds an internal system note** to the ticket stating that it was marked as resolved from Backupchecks. -- The ticket remains open for the ticket owner to review and close manually. - -Proposed internal system note text: - -> `Ticket marked as resolved in Backupchecks, but not closed automatically because time entries are present.` - -> **Rationale:** Ensures the ticket owner is explicitly informed without violating Autotask process or financial controls. - -### 6.3 Closing note (fixed text) - -When resolving a ticket **and no time entries are present**, Backupchecks always adds the following **internal system note** **before closing**: - -> `Ticket resolved via Backupchecks after verification that the backup issue is no longer present.` - -Characteristics: -- Fixed text (no operator editing in phase 1) -- **System / internal note** (never customer-facing) -- Ensures auditability and traceability - ---- - ---- - -## 6A. Handling Existing Tickets & Compatibility Mode - -### 6A.1 Existing manual ticket numbers - -In the pre-integration workflow, a run may already contain a manually entered ticket number. - -When Autotask integration is **enabled**: -- Existing ticket numbers remain visible. -- Backupchecks may offer a one-time action: - - **“Link existing Autotask ticket”** - - This validates the ticket in Autotask and stores the **Autotask Ticket ID**. - -> **Note:** Without an Autotask Ticket ID, Backupchecks must not attempt to resolve a ticket. - -When Autotask integration is **disabled**: -- The current/manual workflow applies (manual ticket number entry). - -### 6A.2 Linking existing Autotask tickets - -When integration is enabled, operators can link an existing Autotask ticket to a run: -- Search/select a ticket (preferably by ticket number) -- Store: - - Autotask Ticket ID - - Autotask Ticket Number - - Ticket URL (optional) - -After linking: -- The run behaves like an integration-created ticket for viewing and resolution rules. - -### 6A.3 Compatibility mode (optional setting) - -Optional setting (recommended for transition periods): -- **“Allow manual ticket number entry when Autotask is enabled”** (default: OFF) - -Behaviour: -- When ON, operators can still manually enter a ticket number even if integration is enabled. -- Resolve from Backupchecks is still only possible for tickets that have a validated Autotask Ticket ID. - -> **Rationale:** Provides a safe escape hatch during rollout and migration. - ---- - -## 6B. Deleted Tickets in Autotask - -Tickets may be deleted in Autotask. When a ticket referenced by Backupchecks is deleted, the linkage becomes invalid. - -### 6B.1 Detection - -When Backupchecks attempts to fetch the ticket by Autotask Ticket ID: -- If Autotask returns “not found” (deleted/missing), Backupchecks marks the linkage as **broken**. - -### 6B.2 Behaviour when a ticket is deleted - -- The run keeps the historical reference (ticket number/ID) for audit purposes. -- The ticket state is shown as: - - **“Missing in Autotask (deleted)”** -- Actions are blocked: - - No “Open ticket” (if no valid URL) - - No “Resolve ticket” -- Operators can choose: - - **Re-link to another ticket** (if the ticket was recreated or replaced) - - **Create a new Autotask ticket** (creates a new link for that run) - -> **Note:** Backupchecks should never silently remove the stored linkage, to preserve auditability. - -### 6B.3 Optional: periodic validation - -Optionally (later), Backupchecks may periodically validate linked ticket IDs and flag missing tickets. - ---- - -## 7. Backupchecks Settings - -### 7.1 New settings section - -**Settings → Extensions & Integrations → Autotask** - -### 7.2 Required settings - -- Enable Autotask integration (on/off) -- Environment: Sandbox / Production -- API Username -- API Password -- Tracking Identifier - -### 7.3 Ticket creation defaults (configurable) - -These defaults are applied when Backupchecks creates a new Autotask ticket. - -- Ticket Source (default): **Monitoring Alert** -- Default Queue (default): **Helpdesk** -- Default Status (default): **New** -- Priority mapping: - - Warning → **Medium** - - Error → **High** - -> **Note:** Issue Type / Category is intentionally **not set** by Backupchecks and will be assigned by the ticket owner or traffic manager. - ---- - -### 7.3A Backupchecks Base URL - -- Base URL of the Backupchecks instance (e.g. `https://backupchecks.example.com`) - -This value is required to construct: -- Direct links to Job Details pages -- Stable references inside Autotask tickets - -> **Note:** This setting is mandatory for ticket creation and must be validated. - ---- - -### 7.4 Dynamic reference data - -Backupchecks must retrieve the following reference data from Autotask and present it in Settings: -- Available Queues -- Available Ticket Sources - -These lists are: -- Loaded on demand (or via refresh action) -- Stored for selection in Settings - -> **Rationale:** Prevents hard-coded values and keeps Backupchecks aligned with Autotask configuration changes. - -### 7.5 Resolve configuration - -- Allow resolving tickets from Backupchecks (on/off) -- Closing note texts (read-only, fixed): - - Standard resolve note - - Time-entry-blocked resolve note - -### 7.6 Validation & diagnostics - -- Test connection -- Validate configuration (credentials, reference data access) -- Optional API logging level - ---- - -## 8. Roles & Permissions - -- Admin / Operator: - - Create tickets - - Resolve tickets (if allowed) -- Reporter: - - View ticket number and link - - No create or resolve actions - ---- - -## 9. Handling Existing, Linked and Deleted Tickets - -### 9.1 Existing tickets (pre-integration) - -- Runs that already contain a manually entered ticket number remain valid. -- When Autotask integration is enabled, operators may optionally: - - Link the run to an existing Autotask ticket (validated against Autotask). - - After linking, the run follows the same rules as integration-created tickets. - -> **Note:** This optional compatibility flow exists to support a gradual transition and avoids forced migration. - -### 9.2 Optional compatibility mode - -- Optional setting: **Allow manual ticket number entry when Autotask is enabled** -- Default: OFF -- Intended as a temporary transition mechanism. - -### 9.3 Deleted tickets in Autotask (important case) - -Tickets may be deleted directly in Autotask. Backupchecks must handle this safely and explicitly. - -Behaviour: -- Backupchecks never assumes tickets exist based on stored data alone. -- On any ticket-related action (view, resolve, open): - - Backupchecks validates the ticket ID against Autotask. - -If Autotask returns *not found*: -- The ticket is marked as **Deleted (external)**. -- The existing link is preserved as historical data but marked inactive. -- No further actions (resolve, update) are allowed on that ticket. - -UI behaviour: -- Ticket number remains visible with a clear indicator: - - “Ticket deleted in Autotask” -- Operator is offered one explicit action: - - “Create new Autotask ticket” (results in a new ticket linked to the same run) - -> **Rationale:** Ticket deletion is an external administrative decision. Backupchecks records the fact but does not attempt to repair or hide it. - -### 9.4 Why links are not silently removed - -- Silent removal would break audit trails. -- Historical runs must retain context, even if external objects no longer exist. -- Operators must explicitly decide how to proceed. - ---- - -## 10. Explicit Non-Goals (Phase 1) - -The following are explicitly excluded: -- Automatic ticket creation -- Automatic ticket closing -- Automatic re-creation of deleted tickets -- Updating ticket content after creation -- Multiple tickets per run -- Time entry handling -- Multi-PSA support - ---- - -## 11. Phase 1 Summary - -Phase 1 delivers: -- Safe, controlled PSA integration -- Operator-driven ticket lifecycle -- Explicit handling of legacy, linked and deleted tickets -- Clear audit trail -- Minimal risk to Autotask data integrity - -This design intentionally prioritises **predictability and control** over automation. - -Future phases may build on this foundation. - diff --git a/docs/backupchecks_autotask_integration_phase_2_implementation.md b/docs/backupchecks_autotask_integration_phase_2_implementation.md new file mode 100644 index 0000000..10e4b75 --- /dev/null +++ b/docs/backupchecks_autotask_integration_phase_2_implementation.md @@ -0,0 +1,432 @@ +# Backupchecks – Autotask Integration + +## Phase 2 – Implementation Design + +*Document type: Implementation design* +*Scope: Phase 2 only* + +--- + +## 1. Purpose + +This document describes the **technical implementation approach** for Phase 2 of the Autotask integration. Phase 2 focuses on **ticket state synchronisation and operator-driven resolution**, based on the approved functional design. + +No future concepts or post–Phase 2 ideas are included in this document. + +--- + +## 2. Preconditions + +Phase 2 assumes: + +- Phase 1 is fully implemented and deployed +- Autotask integration can be enabled and disabled at runtime +- Tickets are already linked to runs via Autotask Ticket ID +- Backupchecks is not the authoritative system for ticket state + +--- + +## 3. High-level Behaviour Overview + +Phase 2 is implemented in **controlled sub-phases**. Each phase introduces a limited set of actions and ends with an explicit **functional validation moment** before continuing. + +The sub-phases are: + +- Phase 2.1 – Read-only ticket polling +- Phase 2.2 – PSA-driven resolution handling +- Phase 2.3 – Deleted ticket detection +- Phase 2.4 – Manual ticket resolution from Backupchecks + +--- + +## 4. Phase 2.1 – Read-only Ticket Polling + +### 4.1 Scope + +This phase introduces **read-only polling** of Autotask ticket state. No mutations or resolution logic is applied. + +### 4.2 Trigger point + +Polling is triggered when: + +- Autotask integration is enabled +- The **Run Checks** page is opened + +There is no background scheduler or periodic task. + +### 4.3 Polling scope + +Only tickets that meet **all** of the following criteria are included: + +- Ticket is linked to a run visible on the Run Checks page +- Run is not already resolved in Backupchecks +- Ticket is not marked as deleted or invalid + +### 4.4 Autotask query strategy + +- Query only **active tickets** in Autotask +- Ticket ID is always used as the lookup key + +### 4.5 Control moment – Phase 2.1 + +Functional validation: + +- Run Checks page loads without delay +- Correct tickets are polled +- No state changes occur in Backupchecks +- No Autotask data is modified + +--- + +### 4.2 Polling scope + +Only tickets that meet **all** of the following criteria are included: + +- Ticket is linked to a run visible on the Run Checks page +- Run is not already resolved in Backupchecks +- Ticket is not marked as deleted or invalid + +This guarantees: + +- Minimal API usage +- Operator-context-only data retrieval + +--- + +### 4.3 Autotask query strategy + +- Query only **active tickets** in Autotask +- Completed / closed tickets are excluded from the active query +- Ticket ID is always used as the lookup key + +If an expected ticket is not returned: + +- A follow-up single-ticket lookup is executed +- If still not found, the ticket is treated as **deleted in PSA** + +--- + +## 5. Phase 2.2 – PSA-driven Resolution Handling + +### 5.1 Scope + +This phase adds **state interpretation** on top of read-only polling. + +### 5.2 Completed ticket handling + +If Autotask reports the ticket status as **Completed**: + +- Mark the linked run as **Resolved** +- Set resolution origin to: + - `Resolved by PSA` +- Store resolution timestamp + +No write-back to Autotask is performed. + +### 5.3 Active ticket handling + +If the ticket exists and is active: + +- Update cached ticket status snapshot only +- No state change in Backupchecks + +### 5.4 Control moment – Phase 2.2 + +Functional validation: + +- PSA-completed tickets resolve runs correctly +- Resolution origin is shown correctly +- Active tickets do not alter run state + +--- + +### 5.2 Completed ticket + +If Autotask reports the ticket status as **Completed**: + +- Mark the linked run as **Resolved** +- Set resolution origin to: + - `Resolved by PSA` +- Store resolution timestamp + +No write-back to Autotask is performed. + +--- + +### 5.3 Deleted or missing ticket + +If the ticket cannot be found in Autotask: + +- Mark ticket linkage as: + - `Deleted in PSA` +- Block ticket-related actions +- Preserve historical references (ID, number, URL) + +Operator must explicitly link or create a replacement ticket. + +--- + +## 6. Phase 2.3 – Deleted Ticket Detection + +### 6.1 Scope + +This phase introduces detection of tickets that are removed from Autotask. + +### 6.2 Detection logic + +If a linked ticket is not returned by the active-ticket query: + +- Execute a single-ticket lookup +- If still not found: + - Mark ticket linkage as `Deleted in PSA` + +### 6.3 Behaviour + +- Ticket actions are blocked +- Historical references remain visible +- Operator must explicitly relink or recreate a ticket + +### 6.4 Control moment – Phase 2.3 + +Functional validation: + +- Deleted tickets are detected reliably +- Warning is visible in UI +- No silent unlinking occurs + +--- + +## 7. Phase 2.4 – Manual Ticket Resolution (Backupchecks → Autotask) + +### 7.1 Preconditions + +The Resolve action is available only if: + +- Autotask integration is enabled +- A valid Autotask Ticket ID exists +- Ticket is not already closed in PSA + +### 7.2 Resolution flow + +1. Operator triggers **Resolve ticket** +2. Backupchecks retrieves ticket details from Autotask +3. Time entry check is executed + +#### Case A – No time entries + +- Ticket is set to completed in Autotask +- Run is marked as **Resolved** +- Resolution origin: + - `Resolved by Backupchecks` + +#### Case B – Time entries exist + +- Ticket is not closed +- Fixed-format internal system note is added +- Run remains unresolved + +### 7.3 Control moment – Phase 2.4 + +Functional validation: + +- Resolution works only under defined rules +- Time entry logic is respected +- Resolution origin is persisted correctly + +--- + +### 6.2 Resolution flow + +1. Operator triggers **Resolve ticket** +2. Backupchecks retrieves ticket details from Autotask +3. Time entry check is executed + +#### Case A – No time entries + +- Ticket is set to the configured completed status in Autotask +- Run is marked as **Resolved** +- Resolution origin: + - `Resolved by Backupchecks` + +#### Case B – Time entries exist + +- Ticket is not closed +- A fixed-format internal system note is added +- Run remains unresolved + +--- + +### 6.3 Post-resolution sync alignment + +If a ticket resolved by Backupchecks is later polled as Completed: + +- Backupchecks keeps the existing resolved state +- Resolution origin remains: + - `Resolved by Backupchecks` + +If Autotask resolves the ticket first: + +- Backupchecks sets: + - `Resolved by PSA` + +--- + +## 7. Data Model Adjustments + +Phase 2 requires the following additional fields per run: + +- `ticket_last_polled_at` +- `ticket_resolution_origin`: + - `psa` + - `backupchecks` +- `ticket_deleted_in_psa` (boolean) + +Existing Phase 1 fields remain unchanged. + +--- + +## 8. UI Behaviour + +### 8.1 Run Checks + +- Visual indicator for linked tickets +- Resolution origin badge: + - Resolved by PSA + - Resolved by Backupchecks +- Warning banner for deleted tickets + +--- + +### 8.2 Job Details + +- Same indicators as Run Checks +- Resolve action visibility follows resolution rules + +--- + +## 9. Error Handling & Logging + +- All Autotask API calls are logged with: + - Correlation ID + - Ticket ID + - Run ID + +- Polling failures do not block page rendering +- Partial polling failures are tolerated per ticket + +--- + +## 10. Explicit Non-Implementation (Phase 2) + +The following are **not implemented**: + +- Background schedulers +- Automatic ticket creation +- Automatic ticket reopening +- Status pushing without operator action +- Ticket content mutation beyond fixed system notes + +--- + +## 11. Phase-based Implementation Workflow + +For each Phase 2 sub-phase, a **dedicated chat** must be used. This ensures focus, traceability, and controlled implementation. + +The instructions below must be copied **verbatim** when starting a new chat for the corresponding phase. + +--- + +## Phase 2.1 – Chat Instructions + +Purpose: +Implement **read-only ticket polling** when the Run Checks page is opened. + +Chat instructions: + +- Scope is limited to Phase 2.1 only +- No ticket state changes are allowed +- No resolve, close, or mutation logic may be added +- Only backend polling and UI visibility updates are permitted +- All changes must be functionally testable on Run Checks + +Exit criteria: + +- Polling executes only when Run Checks is opened +- Only relevant active tickets are queried +- No Backupchecks run state is modified + +--- + +## Phase 2.2 – Chat Instructions + +Purpose: +Implement **PSA-driven resolution handling** based on polling results. + +Chat instructions: + +- Phase 2.1 must already be completed and validated +- Only Completed ticket handling may be added +- Resolution origin must be stored and displayed +- No Backupchecks-initiated ticket changes are allowed + +Exit criteria: + +- Completed tickets resolve runs correctly +- Resolution origin is accurate and persistent +- Active tickets remain unchanged + +--- + +## Phase 2.3 – Chat Instructions + +Purpose: +Implement **deleted ticket detection and operator visibility**. + +Chat instructions: + +- Phase 2.1 and 2.2 must already be completed +- Detection must be explicit and non-destructive +- Historical ticket references must remain intact +- UI must clearly indicate deleted state + +Exit criteria: + +- Deleted tickets are reliably detected +- Operators are clearly informed +- Ticket actions are blocked correctly + +--- + +## Phase 2.4 – Chat Instructions + +Purpose: +Implement **manual ticket resolution from Backupchecks to Autotask**. + +Chat instructions: + +- All previous Phase 2 sub-phases must be completed +- Resolution must be strictly operator-driven +- Time entry rules must be enforced exactly +- Resolution origin must be stored correctly + +Exit criteria: + +- Tickets are resolved only when allowed +- Time entry edge cases behave correctly +- State remains consistent with PSA polling + +--- + +## 12. Phase 2 Completion Criteria + +Phase 2 is considered complete when: + +- All Phase 2 sub-phases have passed their control moments +- No cross-phase leakage of logic exists +- Enable/disable integration behaviour is respected +- Functional behaviour matches the approved design exactly + +--- + +End of Phase 2 implementation document. + diff --git a/docs/changelog.md b/docs/changelog.md index 7c8a16a..9ed55d6 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -152,6 +152,17 @@ Changes: - Added proper ticket association to Job Details, matching the behaviour of manually entered tickets. - Updated the Run Checks view to show the ticket indicator when an Autotask ticket is linked to a run. +## v20260116-04-runchecks-autotask-ticket-polling + +### Changes: +- Added read-only Autotask ticket polling triggered on Run Checks page load +- Introduced backend endpoint to poll only relevant active Autotask tickets linked to visible runs +- Implemented ticket ID deduplication to minimize Autotask API calls +- Ensured polling is best-effort and does not block Run Checks rendering +- Added client support for bulk ticket queries with per-ticket fallback +- Updated Run Checks UI to display polled PSA ticket status without modifying run state +- Explicitly prevented any ticket mutation, resolution, or Backupchecks state changes + *** ## v0.1.21