defmodule ToweropsWeb.Api.MobileController do @moduledoc """ API controller for mobile app data access. All endpoints require mobile authentication via bearer token. """ use ToweropsWeb, :controller alias Towerops.Alerts alias Towerops.Devices alias Towerops.Organizations alias Towerops.Sites @doc """ GET /api/v1/mobile/organizations Returns list of organizations the user has access to. Response: { "organizations": [ { "id": "uuid", "name": "Acme Corp", "sites_count": 3, "device_count": 15, "active_alerts_count": 2 } ] } """ def list_organizations(conn, _params) do user = conn.assigns.current_user orgs = Organizations.list_user_organizations(user.id) org_ids = Enum.map(orgs, & &1.id) site_counts = Sites.batch_count_organization_sites(org_ids) device_counts = Devices.batch_count_organization_devices(org_ids) alert_counts = Alerts.batch_count_active_alerts(org_ids) organizations = Enum.map(orgs, fn org -> %{ id: org.id, name: org.name, sites_count: Map.get(site_counts, org.id, 0), device_count: Map.get(device_counts, org.id, 0), active_alerts_count: Map.get(alert_counts, org.id, 0) } end) json(conn, %{organizations: organizations}) end @doc """ GET /api/v1/mobile/organizations/:id/sites Returns list of sites for an organization. Response: { "sites": [ { "id": "uuid", "name": "Main Office", "location": "New York, NY", "device_count": 5, "equipment_down_count": 1 } ] } """ def list_sites(conn, %{"organization_id" => org_id}) do user = conn.assigns.current_user case verify_organization_access(user, org_id) do {:ok, _org} -> site_list = Sites.list_organization_sites(org_id) site_ids = Enum.map(site_list, & &1.id) counts = Devices.batch_count_site_devices(site_ids) sites = Enum.map(site_list, fn site -> %{total: total, down: down} = Map.get(counts, site.id, %{total: 0, down: 0}) %{ id: site.id, name: site.name, location: site.location, device_count: total, equipment_down_count: down } end) json(conn, %{sites: sites}) {:error, :unauthorized} -> conn |> put_status(:forbidden) |> json(%{error: "Access denied to this organization"}) end end @doc """ GET /api/v1/mobile/organizations/:id/devices Returns list of devices for an organization with current status. Query params: - site_id: Filter by site (optional) - status: Filter by status: "up", "down", "unknown" (optional) Response: { "device": [ { "id": "uuid", "name": "Core Router", "ip_address": "192.168.1.1", "site_name": "Main Office", "status": "up", "uptime": "15 days, 4 hours", "last_seen_at": "2026-01-15T19:44:25Z" } ] } """ def list_devices(conn, %{"organization_id" => org_id} = params) do user = conn.assigns.current_user case verify_organization_access(user, org_id) do {:ok, _org} -> equipment_list = org_id |> Devices.list_organization_devices(params) |> Enum.map(&format_equipment/1) json(conn, %{device: equipment_list}) {:error, :unauthorized} -> conn |> put_status(:forbidden) |> json(%{error: "Access denied to this organization"}) end end @doc """ GET /api/v1/mobile/devices/:id Returns detailed device information including interfaces and sensors. Response: { "id": "uuid", "name": "Core Router", "ip_address": "192.168.1.1", "status": "up", "uptime": "15 days, 4 hours", "site": {"id": "uuid", "name": "Main Office"}, "interfaces": [...], "sensors": [...] } """ def get_device(conn, %{"id" => device_id}) do user = conn.assigns.current_user case Devices.get_device_with_details(device_id) do nil -> conn |> put_status(:not_found) |> json(%{error: "Device not found"}) device -> case verify_organization_access(user, device.organization_id) do {:ok, _org} -> json(conn, format_equipment_details(device)) {:error, :unauthorized} -> conn |> put_status(:forbidden) |> json(%{error: "Access denied to this device"}) end end end @doc """ GET /api/v1/mobile/organizations/:id/alerts Returns list of alerts for an organization. Query params: - severity: Filter by severity: "critical", "warning", "info" (optional) - status: Filter by status: "active", "acknowledged", "resolved" (optional) - limit: Number of alerts to return (default 50, max 200) Response: { "alerts": [ { "id": "uuid", "severity": "critical", "status": "active", "message": "Device Down: Core Router", "equipment_name": "Core Router", "device_id": "uuid", "occurred_at": "2026-01-15T19:44:25Z" } ] } """ def list_alerts(conn, %{"organization_id" => org_id} = params) do user = conn.assigns.current_user case verify_organization_access(user, org_id) do {:ok, _org} -> limit = case Integer.parse(params["limit"] || "50") do {n, _} when n > 0 and n <= 200 -> n _ -> 50 end alerts = org_id |> Alerts.list_organization_alerts(Map.put(params, "limit", limit)) |> Enum.map(&format_alert/1) json(conn, %{alerts: alerts}) {:error, :unauthorized} -> conn |> put_status(:forbidden) |> json(%{error: "Access denied to this organization"}) end end # Private helpers defp verify_organization_access(user, org_id) do if Organizations.user_has_access?(user.id, org_id) do {:ok, org_id} else {:error, :unauthorized} end end defp format_equipment(device) do %{ id: device.id, name: device.name, ip_address: device.ip_address, site_name: device.site && device.site.name, status: device.status || "unknown", uptime: format_uptime(device), last_seen_at: device.last_seen_at } end defp format_equipment_details(device) do %{ id: device.id, name: device.name, ip_address: device.ip_address, status: device.status || "unknown", uptime: format_uptime(device), site: %{ id: device.site.id, name: device.site.name }, interfaces: Enum.map(device.snmp_device.interfaces || [], fn interface -> %{ id: interface.id, name: interface.if_name, alias: interface.if_alias, status: interface.if_oper_status, admin_status: interface.if_admin_status, speed: interface.if_speed, mac_address: interface.if_phys_address } end), sensors: Enum.map(device.snmp_device.sensors || [], fn sensor -> %{ id: sensor.id, name: sensor.sensor_descr, type: sensor.sensor_type, unit: sensor.sensor_unit, current_value: sensor.last_value, status: nil } end) } end defp format_alert(alert) do %{ id: alert.id, status: format_alert_status(alert), message: alert.message, equipment_name: alert.device && alert.device.name, device_id: alert.device_id, occurred_at: alert.triggered_at } end @doc false def format_alert_status(%{resolved_at: t}) when not is_nil(t), do: "resolved" def format_alert_status(%{acknowledged_at: t}) when not is_nil(t), do: "acknowledged" def format_alert_status(_), do: "active" @doc false def timeticks_to_string(timeticks) do seconds = div(timeticks, 100) days = div(seconds, 86_400) hours = div(rem(seconds, 86_400), 3600) minutes = div(rem(seconds, 3600), 60) format_uptime_components(days, hours, minutes) end defp format_uptime_components(days, hours, _min) when days > 0, do: "#{days}d #{hours}h" defp format_uptime_components(_days, hours, minutes) when hours > 0, do: "#{hours}h #{minutes}m" defp format_uptime_components(_, _, minutes), do: "#{minutes}m" defp format_uptime(device) do if device.snmp_device && device.snmp_device.sys_uptime do timeticks_to_string(device.snmp_device.sys_uptime) end end end