diff --git a/lib/towerops/gaiia/sync.ex b/lib/towerops/gaiia/sync.ex
index 9988056a..7baf706b 100644
--- a/lib/towerops/gaiia/sync.ex
+++ b/lib/towerops/gaiia/sync.ex
@@ -32,7 +32,11 @@ defmodule Towerops.Gaiia.Sync do
items_count = upsert_nodes(org_id, items, :inventory_item),
{:ok, subs} <- Client.list_billing_subscriptions(api_key, opts) do
subs_count = upsert_nodes(org_id, subs, :billing_subscription)
- Integrations.update_sync_status(integration, "success")
+
+ message =
+ "Synced #{accounts_count} accounts, #{sites_count} sites, #{items_count} inventory items, #{subs_count} subscriptions"
+
+ Integrations.update_sync_status(integration, "success", message)
{:ok,
%{
@@ -43,7 +47,7 @@ defmodule Towerops.Gaiia.Sync do
}}
else
{:error, reason} ->
- Integrations.update_sync_status(integration, "failed")
+ Integrations.update_sync_status(integration, "failed", humanize_sync_error(reason))
{:error, reason}
end
end
@@ -170,4 +174,19 @@ defmodule Towerops.Gaiia.Sync do
endpoint -> [endpoint: endpoint]
end
end
+
+ defp humanize_sync_error(:unauthorized), do: "Authentication failed — check your API key"
+
+ defp humanize_sync_error(:forbidden), do: "Access denied — your API key may not have sufficient permissions"
+
+ defp humanize_sync_error(:not_found), do: "API endpoint not found — the Gaiia API may have changed"
+
+ defp humanize_sync_error(:timeout), do: "Connection timed out — Gaiia may be temporarily unavailable"
+
+ defp humanize_sync_error(%Req.TransportError{reason: :econnrefused}), do: "Connection refused — unable to reach Gaiia"
+
+ defp humanize_sync_error(%Req.TransportError{reason: reason}), do: "Connection error: #{reason}"
+
+ defp humanize_sync_error(reason) when is_binary(reason), do: reason
+ defp humanize_sync_error(_reason), do: "An unexpected error occurred during sync"
end
diff --git a/lib/towerops/integrations.ex b/lib/towerops/integrations.ex
index 0aa35cc0..d4129465 100644
--- a/lib/towerops/integrations.ex
+++ b/lib/towerops/integrations.ex
@@ -37,11 +37,12 @@ defmodule Towerops.Integrations do
|> Repo.update()
end
- def update_sync_status(%Integration{} = integration, status) do
+ def update_sync_status(%Integration{} = integration, status, message \\ nil) do
integration
|> Integration.changeset(%{
last_sync_status: status,
- last_synced_at: DateTime.truncate(DateTime.utc_now(), :second)
+ last_synced_at: DateTime.truncate(DateTime.utc_now(), :second),
+ last_sync_message: message
})
|> Repo.update()
end
diff --git a/lib/towerops/integrations/integration.ex b/lib/towerops/integrations/integration.ex
index 3e77f545..d57cb46b 100644
--- a/lib/towerops/integrations/integration.ex
+++ b/lib/towerops/integrations/integration.ex
@@ -24,6 +24,7 @@ defmodule Towerops.Integrations.Integration do
field :sync_interval_minutes, :integer, default: 10
field :last_synced_at, :utc_datetime
field :last_sync_status, :string, default: "never"
+ field :last_sync_message, :string
belongs_to :organization, Towerops.Organizations.Organization
@@ -39,7 +40,8 @@ defmodule Towerops.Integrations.Integration do
:credentials,
:sync_interval_minutes,
:last_synced_at,
- :last_sync_status
+ :last_sync_status,
+ :last_sync_message
])
|> validate_required([:organization_id, :provider])
|> validate_inclusion(:provider, @valid_providers)
diff --git a/lib/towerops/preseem/sync.ex b/lib/towerops/preseem/sync.ex
index 35180c10..67c25b75 100644
--- a/lib/towerops/preseem/sync.ex
+++ b/lib/towerops/preseem/sync.ex
@@ -117,8 +117,9 @@ defmodule Towerops.Preseem.Sync do
status = if synced_count == length(ap_list), do: "success", else: "partial"
+ message = "Synced #{synced_count} access points, matched #{matched_count} devices"
log_sync(integration, status, synced_count, %{}, duration_ms)
- Integrations.update_sync_status(integration, status)
+ Integrations.update_sync_status(integration, status, message)
{:ok, %{synced: synced_count, matched: matched_count}}
end
@@ -126,8 +127,9 @@ defmodule Towerops.Preseem.Sync do
defp handle_failed_sync(integration, reason, start_time) do
duration_ms = System.monotonic_time(:millisecond) - start_time
+ message = humanize_sync_error(reason)
log_sync(integration, "failed", 0, %{error: inspect(reason)}, duration_ms)
- Integrations.update_sync_status(integration, "failed")
+ Integrations.update_sync_status(integration, "failed", message)
{:error, reason}
end
@@ -167,4 +169,11 @@ defmodule Towerops.Preseem.Sync do
defp cast_to_float(nil), do: nil
defp cast_to_float(val) when is_float(val), do: val
defp cast_to_float(val) when is_integer(val), do: val / 1
+
+ defp humanize_sync_error(:unauthorized), do: "Authentication failed — check your API key"
+
+ defp humanize_sync_error(:timeout), do: "Connection timed out — Preseem may be temporarily unavailable"
+
+ defp humanize_sync_error(reason) when is_binary(reason), do: reason
+ defp humanize_sync_error(_reason), do: "An unexpected error occurred during sync"
end
diff --git a/lib/towerops_web/controllers/api/v1/integrations_controller.ex b/lib/towerops_web/controllers/api/v1/integrations_controller.ex
index f6fc7e82..49e433d9 100644
--- a/lib/towerops_web/controllers/api/v1/integrations_controller.ex
+++ b/lib/towerops_web/controllers/api/v1/integrations_controller.ex
@@ -103,6 +103,7 @@ defmodule ToweropsWeb.Api.V1.IntegrationsController do
sync_interval_minutes: integration.sync_interval_minutes,
last_synced_at: integration.last_synced_at,
last_sync_status: integration.last_sync_status,
+ last_sync_message: integration.last_sync_message,
inserted_at: integration.inserted_at,
updated_at: integration.updated_at
}
diff --git a/lib/towerops_web/live/org/settings_live.html.heex b/lib/towerops_web/live/org/settings_live.html.heex
index 8c2ca4cd..accfa4d3 100644
--- a/lib/towerops_web/live/org/settings_live.html.heex
+++ b/lib/towerops_web/live/org/settings_live.html.heex
@@ -866,6 +866,18 @@
<% end %>
+ <%= if integration.last_sync_message && integration.last_sync_message != "" do %>
+
+ {integration.last_sync_message}
+
+ <% end %>
+
<%= if integration.last_synced_at do %>
· Next sync in ~{next_sync_minutes(
diff --git a/priv/repo/migrations/20260214200600_add_last_sync_message_to_integrations.exs b/priv/repo/migrations/20260214200600_add_last_sync_message_to_integrations.exs
new file mode 100644
index 00000000..b49061b4
--- /dev/null
+++ b/priv/repo/migrations/20260214200600_add_last_sync_message_to_integrations.exs
@@ -0,0 +1,9 @@
+defmodule Towerops.Repo.Migrations.AddLastSyncMessageToIntegrations do
+ use Ecto.Migration
+
+ def change do
+ alter table(:integrations) do
+ add :last_sync_message, :text
+ end
+ end
+end