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