towerops/lib/towerops_web/channels/mobile_channel.ex
Graham McIntire 7251930404
add mobile app websocket channel and improve QR linking UX
- add MobileSocket for token-authenticated mobile WebSocket connections
- add MobileChannel with org-level and device-level real-time events
- add /mobile/socket endpoint for iOS app connections
- add "Link Mobile App" to user dropdown menu for discoverability
- improve QR code page with numbered steps and clearer instructions
- add socket/channel tests (17 total)
2026-03-13 17:28:36 -05:00

97 lines
2.8 KiB
Elixir

defmodule ToweropsWeb.MobileChannel do
@moduledoc """
Phoenix channel for real-time mobile app communication.
Supports two topic patterns:
- `mobile:org:<org_id>` — org-level alerts and device status events
- `mobile:device:<device_id>` — 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