defmodule ToweropsWeb.MobileChannel do @moduledoc """ Phoenix channel for real-time mobile app communication. Supports two topic patterns: - `mobile:org:` — org-level alerts and device status events - `mobile:device:` — device-level sensor and interface events """ use ToweropsWeb, :channel alias Towerops.Devices alias Towerops.Organizations @impl true def join("mobile:org:" <> org_id, _params, socket) do user_id = socket.assigns.user_id if Organizations.user_has_access?(user_id, org_id) do _ = Phoenix.PubSub.subscribe(Towerops.PubSub, "organization:#{org_id}:alerts") _ = Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:org:#{org_id}") {:ok, socket} else {:error, %{reason: "unauthorized"}} end end def join("mobile:device:" <> device_id, _params, socket) do user_id = socket.assigns.user_id case Devices.get_device(device_id) do nil -> {:error, %{reason: "not_found"}} device -> if Organizations.user_has_access?(user_id, device.organization_id) do _ = Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device_id}") {:ok, socket} else {:error, %{reason: "unauthorized"}} end end end # Org-level: alert changed @impl true def handle_info({:alert_changed, org_id}, socket) do push(socket, "alert:changed", %{organization_id: org_id}) {:noreply, socket} end # Org-level: device events def handle_info({event, org_id}, socket) when event in [:device_created, :device_updated, :device_status_changed, :device_deleted] do push(socket, "device:changed", %{ event: to_string(event), organization_id: org_id }) {:noreply, socket} end # Device-level: sensor and interface events from SensorChangeDetector/DevicePollerWorker def handle_info({:device_event, event}, socket) when is_map(event) do push(socket, "device:event", %{ device_id: event.device_id, event_type: event.event_type, severity: event.severity, message: event.message, metadata: event.metadata, occurred_at: format_datetime(event.occurred_at) }) {:noreply, socket} end # Device-level: bulk data updates def handle_info({update_type, device_id}, socket) when update_type in [:sensors_updated, :state_sensors_updated, :interfaces_updated, :neighbors_updated] and is_binary(device_id) do push(socket, "device:updated", %{ device_id: device_id, update_type: to_string(update_type) }) {:noreply, socket} end # Catch-all for unhandled PubSub messages def handle_info(_msg, socket) do {:noreply, socket} end defp format_datetime(%DateTime{} = dt), do: DateTime.to_iso8601(dt) defp format_datetime(other), do: to_string(other) end