- 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)
97 lines
2.8 KiB
Elixir
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
|