diff --git a/.last-branch b/.last-branch index fe28a48..dad6799 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260115-11-autotask-companyname-unwrap +v20260115-12-autotask-customers-refreshall-mappings diff --git a/containers/backupchecks/src/backend/app/main/routes_customers.py b/containers/backupchecks/src/backend/app/main/routes_customers.py index f57ca44..e54ab83 100644 --- a/containers/backupchecks/src/backend/app/main/routes_customers.py +++ b/containers/backupchecks/src/backend/app/main/routes_customers.py @@ -259,6 +259,73 @@ def api_customer_autotask_mapping_refresh(customer_id: int): return jsonify({"status": "error", "message": str(exc) or "Refresh failed."}), 400 +@main_bp.post("/api/customers/autotask-mapping/refresh-all") +@login_required +@roles_required("admin", "operator") +def api_customers_autotask_mapping_refresh_all(): + """Refresh mapping status for all customers that have an Autotask company ID.""" + + from ..integrations.autotask.client import AutotaskError + + customers = Customer.query.filter(Customer.autotask_company_id.isnot(None)).all() + if not customers: + return jsonify({"status": "ok", "refreshed": 0, "counts": {"ok": 0, "renamed": 0, "missing": 0, "invalid": 0}}) + + try: + client = _get_autotask_client_or_raise() + except Exception as exc: + return jsonify({"status": "error", "message": str(exc) or "Autotask is not configured."}), 400 + + counts = {"ok": 0, "renamed": 0, "missing": 0, "invalid": 0} + refreshed = 0 + + for c in customers: + company_id = getattr(c, "autotask_company_id", None) + if not company_id: + continue + try: + company = client.get_company(int(company_id)) + name = _normalize_company_name(company) + + prev = (getattr(c, "autotask_company_name", None) or "").strip() + if prev and name and prev != name: + c.autotask_company_name = name + c.autotask_mapping_status = "renamed" + counts["renamed"] += 1 + else: + c.autotask_company_name = name + c.autotask_mapping_status = "ok" + counts["ok"] += 1 + c.autotask_last_sync_at = datetime.utcnow() + refreshed += 1 + except AutotaskError as exc: + try: + code = getattr(exc, "status_code", None) + except Exception: + code = None + + if code == 404: + c.autotask_mapping_status = "invalid" + counts["invalid"] += 1 + else: + c.autotask_mapping_status = "missing" + counts["missing"] += 1 + c.autotask_last_sync_at = datetime.utcnow() + refreshed += 1 + except Exception: + c.autotask_mapping_status = "missing" + c.autotask_last_sync_at = datetime.utcnow() + counts["missing"] += 1 + refreshed += 1 + + try: + db.session.commit() + return jsonify({"status": "ok", "refreshed": refreshed, "counts": counts}) + except Exception as exc: + db.session.rollback() + return jsonify({"status": "error", "message": str(exc) or "Failed to refresh all mappings."}), 400 + + @main_bp.route("/customers/create", methods=["POST"]) @login_required @roles_required("admin", "operator") diff --git a/containers/backupchecks/src/templates/main/customers.html b/containers/backupchecks/src/templates/main/customers.html index 91ec891..024fb51 100644 --- a/containers/backupchecks/src/templates/main/customers.html +++ b/containers/backupchecks/src/templates/main/customers.html @@ -19,6 +19,11 @@ Export CSV + + {% if autotask_enabled and autotask_configured %} + + + {% endif %} {% endif %} @@ -219,6 +224,10 @@ var nameInput = document.getElementById("edit_customer_name"); var activeInput = document.getElementById("edit_customer_active"); + // Top-level refresh-all (only present when integration is enabled/configured) + var refreshAllBtn = document.getElementById("autotaskRefreshAllMappingsBtn"); + var refreshAllMsg = document.getElementById("autotaskRefreshAllMappingsMsg"); + // Autotask mapping UI (only present when integration is enabled/configured) var atCurrent = document.getElementById("autotaskCurrentMapping"); var atCurrentMeta = document.getElementById("autotaskCurrentMappingMeta"); @@ -233,6 +242,20 @@ var currentCustomerId = null; var selectedCompanyId = null; + function setRefreshAllMsg(text, isError) { + if (!refreshAllMsg) { + return; + } + refreshAllMsg.textContent = text || ""; + if (isError) { + refreshAllMsg.classList.remove("text-muted"); + refreshAllMsg.classList.add("text-danger"); + } else { + refreshAllMsg.classList.remove("text-danger"); + refreshAllMsg.classList.add("text-muted"); + } + } + function setMsg(text, isError) { if (!atMsg) { return; @@ -302,6 +325,32 @@ return data; } + if (refreshAllBtn) { + refreshAllBtn.addEventListener("click", async function () { + if (!confirm("Refresh mapping status for all mapped customers?")) { + return; + } + refreshAllBtn.disabled = true; + setRefreshAllMsg("Refreshing...", false); + try { + var data = await postJson("/api/customers/autotask-mapping/refresh-all", {}); + var counts = (data && data.counts) ? data.counts : null; + if (counts) { + setRefreshAllMsg( + "Done. OK: " + (counts.ok || 0) + ", Renamed: " + (counts.renamed || 0) + ", Missing: " + (counts.missing || 0) + ", Invalid: " + (counts.invalid || 0) + ".", + false + ); + } else { + setRefreshAllMsg("Done.", false); + } + window.location.reload(); + } catch (e) { + setRefreshAllMsg(e && e.message ? e.message : "Refresh failed.", true); + refreshAllBtn.disabled = false; + } + }); + } + var editButtons = document.querySelectorAll(".customer-edit-btn"); editButtons.forEach(function (btn) { btn.addEventListener("click", function () { diff --git a/docs/changelog.md b/docs/changelog.md index ec0cd13..454b22e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -94,6 +94,12 @@ Changes: - Improved company lookup handling to support different response shapes (single item and collection wrappers). - Ensured the cached Autotask company name is stored and displayed consistently after mapping and refresh. +## v20260115-12-autotask-customers-refreshall-mappings + +- Added a “Refresh all Autotask mappings” button on the Customers page to validate all mapped customers in one action. +- Implemented a new backend endpoint to refresh mapping status for all customers with an Autotask Company ID and return a status summary (ok/renamed/missing/invalid). +- Updated the Customers UI to call the refresh-all endpoint, show a short result summary, and reload to reflect updated mapping states. + *** ## v0.1.21