towerops/lib/towerops/uisp/sync.ex

129 lines
4.3 KiB
Elixir

defmodule Towerops.Uisp.Sync do
@moduledoc """
Orchestrates syncing data from the UISP API into the local database.
Runs site sync followed by device sync, logs each operation, and updates
the integration's sync status.
"""
alias Towerops.Integrations
alias Towerops.Integrations.Integration
alias Towerops.Repo
alias Towerops.Uisp.ConfigSnapshot
alias Towerops.Uisp.DeviceSync
alias Towerops.Uisp.GpsSync
alias Towerops.Uisp.SiteSync
alias Towerops.Uisp.StatisticsSync
alias Towerops.Uisp.SyncLog
require Logger
@doc """
Main entry point: syncs sites and devices for the given UISP integration.
Returns `{:ok, %{sites_synced: count, devices_matched: count, devices_created: count}}`
or `{:error, reason}`.
"""
def sync_organization(%Integration{} = integration) do
start_time = System.monotonic_time(:millisecond)
with {:ok, site_result} <- SiteSync.sync_sites(integration),
site_map = build_full_site_map(integration, site_result.site_map),
{:ok, device_result} <- DeviceSync.sync_devices(integration, site_map) do
# Phase 2: GPS enrichment, statistics sync, config snapshots
# These are best-effort — failures don't block the main sync
gps_result = safe_sync(:gps, fn -> GpsSync.sync_gps(device_result.device_pairs || []) end)
stats_result =
safe_sync(:stats, fn -> StatisticsSync.sync_statistics(integration, device_result.device_map || %{}) end)
config_result =
safe_sync(:config, fn -> ConfigSnapshot.sync_configs(integration, device_result.device_map || %{}) end)
duration_ms = System.monotonic_time(:millisecond) - start_time
total_synced = site_result.synced + device_result.matched + device_result.created
message =
"Synced #{site_result.synced} sites, matched #{device_result.matched} devices, created #{device_result.created} devices"
log_sync(
integration,
"success",
total_synced,
%{
gps: inspect(gps_result),
stats: inspect(stats_result),
config: inspect(config_result)
},
duration_ms
)
Integrations.update_sync_status(integration, "success", message)
{:ok,
%{
sites_synced: site_result.synced,
devices_matched: device_result.matched,
devices_created: device_result.created
}}
else
{:error, reason} ->
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", message)
{:error, reason}
end
end
defp build_full_site_map(integration, uisp_id_map) do
# The uisp_id_map has UISP site IDs -> local sites.
# Also build a name-based map for any sites already in the DB.
name_map = SiteSync.build_site_map_by_name(integration.organization_id)
Map.merge(name_map, uisp_id_map)
end
defp log_sync(integration, status, records_synced, errors, duration_ms) do
%SyncLog{}
|> SyncLog.changeset(%{
organization_id: integration.organization_id,
integration_id: integration.id,
status: status,
records_synced: records_synced,
errors: errors,
duration_ms: duration_ms,
inserted_at: Towerops.Time.now()
})
|> Repo.insert()
end
defp safe_sync(label, fun) do
case fun.() do
{:ok, result} ->
result
{:error, reason} ->
Logger.warning("UISP #{label} sync failed: #{inspect(reason)}")
%{error: reason}
end
rescue
e ->
Logger.warning("UISP #{label} sync crashed: #{Exception.message(e)}")
%{error: Exception.message(e)}
end
defp humanize_sync_error(:unauthorized), do: "Authentication failed — check your API key"
defp humanize_sync_error(:timeout), do: "Connection timed out — UISP may be temporarily unavailable"
defp humanize_sync_error(reason) when is_binary(reason) do
if String.contains?(reason, ["CA trust store", "cacert", "certificate"]) do
"SSL certificate verification failed — unable to establish a secure connection"
else
Logger.warning("UISP sync failed with raw error: #{reason}")
"An unexpected error occurred during sync"
end
end
defp humanize_sync_error(_reason), do: "An unexpected error occurred during sync"
end