Auto-commit local changes before build (2026-01-16 12:28:07)

This commit is contained in:
Ivo Oskamp 2026-01-16 12:28:07 +01:00
parent 9399082231
commit b46b7fbc21
8 changed files with 1128 additions and 471 deletions

View File

@ -1 +1 @@
v20260116-03-autotask-ticket-linking-visibility v20260116-04-runchecks-autotask-ticket-polling

View File

@ -467,6 +467,110 @@ class AutotaskClient:
return data return data
raise AutotaskError("Autotask did not return a ticket object.") 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/<id>.
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( def _lookup_created_ticket_id(
self, self,
tracking_identifier: str, tracking_identifier: str,

View File

@ -876,6 +876,110 @@ def run_checks_details():
return jsonify({"status": "ok", "job": job_payload, "runs": runs_payload}) 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") @main_bp.post("/api/run-checks/autotask-ticket")
@login_required @login_required
@roles_required("admin", "operator") @roles_required("admin", "operator")

View File

@ -297,9 +297,47 @@
var currentRunId = null; var currentRunId = null;
var currentPayload = null; var currentPayload = null;
// Phase 2.1: Read-only Autotask ticket polling (Run Checks page only)
// Cache shape: { <ticketId>: {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 btnMarkAllReviewed = document.getElementById('rcm_mark_all_reviewed');
var btnMarkSuccessOverride = document.getElementById('rcm_mark_success_override'); var btnMarkSuccessOverride = document.getElementById('rcm_mark_success_override');
pollAutotaskTicketsOnPageOpen();
// Shift-click range selection for checkbox rows // Shift-click range selection for checkbox rows
var lastCheckedCb = null; var lastCheckedCb = null;
@ -864,13 +902,33 @@ table.addEventListener('change', function (e) {
function renderAutotaskInfo(run) { function renderAutotaskInfo(run) {
if (!atInfo) return; if (!atInfo) return;
var num = (run && run.autotask_ticket_number) ? String(run.autotask_ticket_number) : ''; 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) { if (num) {
atInfo.innerHTML = '<div><strong>Ticket:</strong> ' + escapeHtml(num) + '</div>'; lines.push('<div><strong>Ticket:</strong> ' + escapeHtml(num) + '</div>');
} else if (run && run.autotask_ticket_id) { } else if (tid) {
atInfo.innerHTML = '<div><strong>Ticket:</strong> created</div>'; lines.push('<div><strong>Ticket:</strong> created</div>');
} else { } else {
atInfo.innerHTML = '<div class="text-muted">No Autotask ticket created for this run.</div>'; lines.push('<div class="text-muted">No Autotask ticket created for this run.</div>');
} }
// 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('<div class="text-muted">PSA status (polled): ' + escapeHtml(label) + '</div>');
}
} else {
lines.push('<div class="text-muted">PSA status (polled): not available</div>');
}
}
atInfo.innerHTML = lines.join('');
} }
window.__rcmRenderAutotaskInfo = renderAutotaskInfo; window.__rcmRenderAutotaskInfo = renderAutotaskInfo;

View File

@ -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] - -&#x20;
- 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

View File

@ -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] <Customer> - <Job> - <Status>`
- 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.

View File

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

View File

@ -152,6 +152,17 @@ Changes:
- Added proper ticket association to Job Details, matching the behaviour of manually entered tickets. - 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. - 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 ## v0.1.21