diff --git a/lib/towerops_web/controllers/api/v1/devices_controller.ex b/lib/towerops_web/controllers/api/v1/devices_controller.ex index 6b456e86..20a9d276 100644 --- a/lib/towerops_web/controllers/api/v1/devices_controller.ex +++ b/lib/towerops_web/controllers/api/v1/devices_controller.ex @@ -88,37 +88,11 @@ defmodule ToweropsWeb.Api.V1.DevicesController do organization_id = conn.assigns.current_organization_id current_user = conn.assigns[:current_user] - # Default organization_id to authenticated org if not provided or empty - device_params = - case Map.get(device_params, "organization_id") do - nil -> Map.put(device_params, "organization_id", organization_id) - "" -> Map.put(device_params, "organization_id", organization_id) - _provided_id -> device_params - end + device_params = default_organization_id(device_params, organization_id) - # Verify site belongs to organization if site_id is provided case verify_site_access(device_params["site_id"], organization_id) do :ok -> - # Superusers bypass device quota limits - opts = if current_user && current_user.is_superuser, do: [bypass_limits: true], else: [] - - case Devices.create_device(device_params, opts) do - {:ok, device} -> - # Trigger SNMP discovery if enabled - _ = - if device.snmp_enabled do - DiscoveryWorker.enqueue(device.id) - end - - conn - |> put_status(:created) - |> json(format_device(device)) - - {:error, %Ecto.Changeset{} = changeset} -> - conn - |> put_status(:unprocessable_entity) - |> json(%{errors: translate_errors(changeset)}) - end + create_and_discover_device(conn, device_params, current_user) {:error, reason} -> conn @@ -265,6 +239,38 @@ defmodule ToweropsWeb.Api.V1.DevicesController do # Private helpers + defp default_organization_id(device_params, organization_id) do + case Map.get(device_params, "organization_id") do + nil -> Map.put(device_params, "organization_id", organization_id) + "" -> Map.put(device_params, "organization_id", organization_id) + _provided_id -> device_params + end + end + + defp create_and_discover_device(conn, device_params, current_user) do + opts = if current_user && current_user.is_superuser, do: [bypass_limits: true], else: [] + + case Devices.create_device(device_params, opts) do + {:ok, device} -> + maybe_enqueue_discovery(device) + + conn + |> put_status(:created) + |> json(format_device(device)) + + {:error, %Ecto.Changeset{} = changeset} -> + conn + |> put_status(:unprocessable_entity) + |> json(%{errors: translate_errors(changeset)}) + end + end + + defp maybe_enqueue_discovery(device) do + if device.snmp_enabled do + DiscoveryWorker.enqueue(device.id) + end + end + defp verify_site_access(nil, _organization_id), do: :ok defp verify_site_access(site_id, organization_id) do diff --git a/lib/towerops_web/live/device_live/index.ex b/lib/towerops_web/live/device_live/index.ex index 32682b3a..2dfb6bb8 100644 --- a/lib/towerops_web/live/device_live/index.ex +++ b/lib/towerops_web/live/device_live/index.ex @@ -25,6 +25,7 @@ defmodule ToweropsWeb.DeviceLive.Index do |> assign(:timezone, socket.assigns.current_scope.timezone) |> assign(:device, device) |> assign(:grouped_devices, grouped_devices) + |> assign(:sites_enabled, organization.use_sites) |> assign(:has_sites, sites != []) |> assign(:reorder_mode, false) |> assign(:device_quota, %{current: current, limit: limit})} diff --git a/lib/towerops_web/live/device_live/index.html.heex b/lib/towerops_web/live/device_live/index.html.heex index 2657b652..f2572055 100644 --- a/lib/towerops_web/live/device_live/index.html.heex +++ b/lib/towerops_web/live/device_live/index.html.heex @@ -79,7 +79,7 @@ <% end %> <.button - :if={@has_sites} + :if={!@sites_enabled || @has_sites} navigate={~p"/devices/new"} variant="primary" > @@ -137,227 +137,229 @@ <%= case @active_tab do %> <% "existing" -> %> - <%= if !@has_sites do %> + <%= if @sites_enabled && !@has_sites && @grouped_devices != [] do %> +
+
+
+ <.icon name="hero-information-circle" class="h-5 w-5 text-blue-400" /> +
+
+

+ Organize your devices with sites +

+
+

+ Sites help you organize devices by physical location. Create a site to assign your devices and keep things organized. +

+
+
+ <.button navigate={~p"/sites/new"}> + <.icon name="hero-plus" class="h-4 w-4" /> Create Your First Site + +
+
+
+
+ <% end %> + + <%= if @grouped_devices == [] do %>
- <.icon - name="hero-building-office" - class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" - /> -

- Create a site first -

+ <.icon name="hero-server" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" /> +

No devices

- Before adding device, you need to create at least one site location. -

-

- Sites help you organize your devices by physical location. + Get started by adding your first device.

- <.button navigate={~p"/sites/new"} variant="primary"> - <.icon name="hero-plus" class="h-5 w-5" /> Create Your First Site + <.button navigate={~p"/devices/new"} variant="primary"> + <.icon name="hero-plus" class="h-5 w-5" /> New Device
<% else %> - <%= if @grouped_devices == [] do %> -
- <.icon name="hero-server" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" /> -

No devices

-

- Get started by adding your first device. -

-
- <.button navigate={~p"/devices/new"} variant="primary"> - <.icon name="hero-plus" class="h-5 w-5" /> New Device - -
-
- <% else %> -
-
-
- - - -
+
+
+ + + + + + + + + + + + <%= for {{site, devices, stats}, index} <- Enum.with_index(@grouped_devices) do %> + + - - - - - - <%= for {{site, devices, stats}, index} <- Enum.with_index(@grouped_devices) do %> + <%= for {device, device_index} <- Enum.with_index(devices) do %> - - - - <%= for {device, device_index} <- Enum.with_index(devices) do %> - + - - - - - - <% end %> + {device.ip_address} + + + + <% end %> - -
+ Drag handle + + Name + + IP Address + + Status + + Last Checked +
- Drag handle - + + - Name - - IP Address - - Status - - Last Checked +
+
+ <%= if site do %> + <.link + navigate={~p"/sites/#{site.id}"} + class="text-sm font-semibold text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400" + > + {site.name} + + <% else %> + + {if @sites_enabled, do: "Unassigned Devices", else: "All Devices"} + + <% end %> + · + + {stats.total} {if stats.total == 1, do: "device", else: "devices"} + + 0} + class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200" + > + {stats.up} up + + 0} + class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200" + > + {stats.down} down + +
+

+ <.icon name="hero-map-pin" class="inline h-3 w-3" /> {site.location} +

+
+ -
-
- <%= if site do %> - <.link - navigate={~p"/sites/#{site.id}"} - class="text-sm font-semibold text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400" - > - {site.name} - - <% else %> - - All Devices - - <% end %> - · - - {stats.total} {if stats.total == 1, do: "device", else: "devices"} - - 0} - class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200" - > - {stats.up} up - - 0} - class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200" - > - {stats.down} down - -
-

- <.icon name="hero-map-pin" class="inline h-3 w-3" /> {site.location} -

-
-
- - - - {device.name} - - {device.ip_address} - - - {device.status |> to_string() |> String.upcase()} - - - <.timestamp datetime={device.last_checked_at} timezone={@timezone} /> -
+ + {device.status |> to_string() |> String.upcase()} + + + <.timestamp datetime={device.last_checked_at} timezone={@timezone} /> +
-
+ <% end %> + +
- <% end %> +
<% end %> <% "discovered" -> %> <%= if @discovered_devices == [] do %> diff --git a/priv/static/changelog.txt b/priv/static/changelog.txt index c2a864f2..f929479b 100644 --- a/priv/static/changelog.txt +++ b/priv/static/changelog.txt @@ -6,6 +6,9 @@ Devices Tested & Working 2026-02-04 * Feature: SNMP v3 support * Sites are now optional and not enabled by default +* Ability to set an org as default +* Poller: snmp v3 support +* Poller: ensure credentials do not remain in memory 2026-02-03 * More tests