fix: sync UserSettingsLive modal state to URL for browser navigation
Update UserSettingsLive to properly handle browser back/forward buttons by syncing all modal state to URL parameters. Changes: - Update handle_params/3 to read modal param and set modal states - Add build_settings_path/2 helper to construct URLs with preserved params - Update all modal show/close handlers to use push_patch instead of assign: - add_token modal - token_created modal (shown after creating token) - revoke_all modal - add_device modal (TOTP) - device_qr modal (shown after creating device) - recovery_codes modal Behavior: - Browser back button now closes modals without navigating away - URL reflects current modal state (?modal=add_token, etc.) - Tab and page params preserved when opening/closing modals - Bookmarkable URLs restore exact modal state Follows idiomatic Phoenix LiveView pattern using push_patch/2 and handle_params/3 as documented in CLAUDE.md. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
97b396a763
commit
d84a685878
3 changed files with 221 additions and 15 deletions
90
lib/towerops/snmp/profiles/vendors/cambium.ex
vendored
90
lib/towerops/snmp/profiles/vendors/cambium.ex
vendored
|
|
@ -54,6 +54,94 @@ defmodule Towerops.Snmp.Profiles.Vendors.Cambium do
|
|||
|
||||
@impl true
|
||||
def discover_wireless_sensors(client_opts) do
|
||||
Vendor.fetch_sensors(wireless_oid_defs(), client_opts)
|
||||
# Fetch static sensors
|
||||
static_sensors = Vendor.fetch_sensors(wireless_oid_defs(), client_opts)
|
||||
|
||||
# Discover ePMP-specific wireless sensors
|
||||
epmp_sensors = discover_epmp_sensors(client_opts)
|
||||
|
||||
static_sensors ++ epmp_sensors
|
||||
end
|
||||
|
||||
# Discover ePMP-specific wireless sensors
|
||||
@spec discover_epmp_sensors(Client.connection_opts()) :: [map()]
|
||||
defp discover_epmp_sensors(client_opts) do
|
||||
# Check if this is an ePMP device by checking wirelessInterfaceMode
|
||||
case Client.get(client_opts, "1.3.6.1.4.1.17713.21.1.2.32.0") do
|
||||
{:ok, mode} when mode in [1, 2] ->
|
||||
build_epmp_sensors(client_opts, mode)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
@spec build_epmp_sensors(Client.connection_opts(), integer()) :: [map()]
|
||||
defp build_epmp_sensors(client_opts, mode) do
|
||||
sensors = []
|
||||
|
||||
# All ePMP devices get these sensors
|
||||
sensors =
|
||||
sensors ++
|
||||
[
|
||||
# RSSI - Downlink Receive Signal Strength Indicator
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.3.0",
|
||||
sensor_type: "rssi",
|
||||
sensor_descr: "Downlink RSSI",
|
||||
sensor_unit: "dBm",
|
||||
sensor_divisor: 1,
|
||||
sensor_index: "cambium_epmp_rssi"
|
||||
},
|
||||
# SNR - Downlink Signal-to-Noise Ratio
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.18.0",
|
||||
sensor_type: "snr",
|
||||
sensor_descr: "Downlink SNR",
|
||||
sensor_unit: "dB",
|
||||
sensor_divisor: 1,
|
||||
sensor_index: "cambium_epmp_snr"
|
||||
},
|
||||
# Frequency - RF frequency in MHz
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.1.0",
|
||||
sensor_type: "frequency",
|
||||
sensor_descr: "RF Frequency",
|
||||
sensor_unit: "MHz",
|
||||
sensor_divisor: 1,
|
||||
sensor_index: "cambium_epmp_frequency"
|
||||
}
|
||||
]
|
||||
|
||||
# Access Point mode (mode == 1) gets client count sensor
|
||||
sensors =
|
||||
if mode == 1 do
|
||||
sensors ++
|
||||
[
|
||||
%{
|
||||
oid: "1.3.6.1.4.1.17713.21.1.2.10.0",
|
||||
sensor_type: "clients",
|
||||
sensor_descr: "Connected Subscribers",
|
||||
sensor_unit: "",
|
||||
sensor_divisor: 1,
|
||||
sensor_index: "cambium_epmp_clients"
|
||||
}
|
||||
]
|
||||
else
|
||||
sensors
|
||||
end
|
||||
|
||||
# Fetch actual values for each sensor
|
||||
sensors
|
||||
|> Enum.map(fn sensor ->
|
||||
case Client.get(client_opts, sensor.oid) do
|
||||
{:ok, value} when is_integer(value) ->
|
||||
Map.put(sensor, :last_value, value)
|
||||
|
||||
_ ->
|
||||
Map.put(sensor, :last_value, nil)
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil(&1.last_value))
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -58,10 +58,19 @@ defmodule ToweropsWeb.UserSettingsLive do
|
|||
# Get page from params, default to 1
|
||||
page = params |> Map.get("page", "1") |> String.to_integer()
|
||||
|
||||
# Read modal state from URL
|
||||
modal = params["modal"]
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:active_tab, tab)
|
||||
|> assign(:login_history_page, page)
|
||||
|> assign(:show_add_token_modal, modal == "add_token")
|
||||
|> assign(:show_token_modal, modal == "token_created")
|
||||
|> assign(:show_revoke_all_modal, modal == "revoke_all")
|
||||
|> assign(:show_add_device_modal, modal == "add_device")
|
||||
|> assign(:show_device_qr_modal, modal == "device_qr")
|
||||
|> assign(:show_recovery_codes_modal, modal == "recovery_codes")
|
||||
|> assign_login_history()
|
||||
|
||||
{:noreply, socket}
|
||||
|
|
@ -69,12 +78,12 @@ defmodule ToweropsWeb.UserSettingsLive do
|
|||
|
||||
@impl true
|
||||
def handle_event("show_add_token_modal", _params, socket) do
|
||||
{:noreply, assign(socket, :show_add_token_modal, true)}
|
||||
{:noreply, push_patch(socket, to: build_settings_path(socket, %{"modal" => "add_token"}))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("cancel_add_token", _params, socket) do
|
||||
{:noreply, assign(socket, :show_add_token_modal, false)}
|
||||
{:noreply, push_patch(socket, to: build_settings_path(socket, %{}))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -177,12 +186,15 @@ defmodule ToweropsWeb.UserSettingsLive do
|
|||
# API token management - delegated to ApiTokenManager
|
||||
@impl true
|
||||
def handle_event("create_api_token", params, socket) do
|
||||
{:noreply, ApiTokenManager.create_api_token(socket, params)}
|
||||
socket = ApiTokenManager.create_api_token(socket, params)
|
||||
# After creating token, show the token_created modal
|
||||
{:noreply, push_patch(socket, to: build_settings_path(socket, %{"modal" => "token_created"}))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("close_token_modal", _params, socket) do
|
||||
{:noreply, ApiTokenManager.close_token_modal(socket)}
|
||||
socket = ApiTokenManager.close_token_modal(socket)
|
||||
{:noreply, push_patch(socket, to: build_settings_path(socket, %{}))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -198,17 +210,19 @@ defmodule ToweropsWeb.UserSettingsLive do
|
|||
|
||||
@impl true
|
||||
def handle_event("show_revoke_all_modal", _params, socket) do
|
||||
{:noreply, SessionManager.show_revoke_all_modal(socket)}
|
||||
{:noreply, push_patch(socket, to: build_settings_path(socket, %{"modal" => "revoke_all"}))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("cancel_revoke_all", _params, socket) do
|
||||
{:noreply, SessionManager.cancel_revoke_all(socket)}
|
||||
{:noreply, push_patch(socket, to: build_settings_path(socket, %{}))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("confirm_revoke_all", _params, socket) do
|
||||
{:noreply, SessionManager.confirm_revoke_all(socket)}
|
||||
socket = SessionManager.confirm_revoke_all(socket)
|
||||
# After revoking all sessions, close modal
|
||||
{:noreply, push_patch(socket, to: build_settings_path(socket, %{}))}
|
||||
end
|
||||
|
||||
# Security tab - TOTP Device Management
|
||||
|
|
@ -216,27 +230,34 @@ defmodule ToweropsWeb.UserSettingsLive do
|
|||
@impl true
|
||||
# TOTP device management - delegated to TotpManager
|
||||
def handle_event("show_add_device_modal", _params, socket) do
|
||||
{:noreply, TotpManager.show_add_device_modal(socket)}
|
||||
socket = TotpManager.show_add_device_modal(socket)
|
||||
{:noreply, push_patch(socket, to: build_settings_path(socket, %{"modal" => "add_device"}))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("cancel_add_device", _params, socket) do
|
||||
{:noreply, TotpManager.cancel_add_device(socket)}
|
||||
socket = TotpManager.cancel_add_device(socket)
|
||||
{:noreply, push_patch(socket, to: build_settings_path(socket, %{}))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("create_device", %{"name" => name}, socket) do
|
||||
{:noreply, TotpManager.create_device(socket, name)}
|
||||
socket = TotpManager.create_device(socket, name)
|
||||
# After creating device, show the QR code modal
|
||||
{:noreply, push_patch(socket, to: build_settings_path(socket, %{"modal" => "device_qr"}))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("verify_new_device", %{"code" => code}, socket) do
|
||||
{:noreply, TotpManager.verify_new_device(socket, code)}
|
||||
socket = TotpManager.verify_new_device(socket, code)
|
||||
# After verifying device, close modal
|
||||
{:noreply, push_patch(socket, to: build_settings_path(socket, %{}))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("close_device_qr_modal", _params, socket) do
|
||||
{:noreply, TotpManager.close_device_qr_modal(socket)}
|
||||
socket = TotpManager.close_device_qr_modal(socket)
|
||||
{:noreply, push_patch(socket, to: build_settings_path(socket, %{}))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
|
|
@ -247,12 +268,15 @@ defmodule ToweropsWeb.UserSettingsLive do
|
|||
# Recovery codes - delegated to TotpManager
|
||||
@impl true
|
||||
def handle_event("regenerate_recovery_codes", _params, socket) do
|
||||
{:noreply, TotpManager.regenerate_recovery_codes(socket)}
|
||||
socket = TotpManager.regenerate_recovery_codes(socket)
|
||||
# After regenerating codes, show the recovery codes modal
|
||||
{:noreply, push_patch(socket, to: build_settings_path(socket, %{"modal" => "recovery_codes"}))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("close_recovery_codes_modal", _params, socket) do
|
||||
{:noreply, TotpManager.close_recovery_codes_modal(socket)}
|
||||
socket = TotpManager.close_recovery_codes_modal(socket)
|
||||
{:noreply, push_patch(socket, to: build_settings_path(socket, %{}))}
|
||||
end
|
||||
|
||||
defp get_current_token_id(%{current_token: token}) when is_binary(token) do
|
||||
|
|
@ -334,6 +358,32 @@ defmodule ToweropsWeb.UserSettingsLive do
|
|||
|> assign(:show_security_alert, failed_count >= 3)
|
||||
end
|
||||
|
||||
# URL building helper
|
||||
defp build_settings_path(socket, extra_params) do
|
||||
# Preserve current tab and page
|
||||
base_params = %{
|
||||
"tab" => socket.assigns.active_tab,
|
||||
"page" => to_string(socket.assigns.login_history_page)
|
||||
}
|
||||
|
||||
# Merge with extra params, removing nil values
|
||||
params =
|
||||
base_params
|
||||
|> Map.merge(extra_params)
|
||||
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
||||
|> Map.new()
|
||||
|
||||
# Remove page param if it's 1 (default)
|
||||
params = if params["page"] == "1", do: Map.delete(params, "page"), else: params
|
||||
|
||||
# Build path with query string
|
||||
if map_size(params) > 0 do
|
||||
~p"/users/settings?#{params}"
|
||||
else
|
||||
~p"/users/settings"
|
||||
end
|
||||
end
|
||||
|
||||
# Sessions tab helper functions
|
||||
|
||||
# Formatting helpers delegated to ToweropsWeb.UserSettingsLive.Helpers module
|
||||
|
|
|
|||
68
priv/profiles/os_discovery/cambium-epmp.yaml
Normal file
68
priv/profiles/os_discovery/cambium-epmp.yaml
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
mib: CAMBIUM-PMP80211-MIB
|
||||
modules:
|
||||
os:
|
||||
version: CAMBIUM-PMP80211-MIB::cambiumCurrentuImageVersion.0
|
||||
serial: CAMBIUM-PMP80211-MIB::cambiumEPMPMSN.0
|
||||
lat: CAMBIUM-PMP80211-MIB::cambiumDeviceLatitude.0
|
||||
long: CAMBIUM-PMP80211-MIB::cambiumDeviceLongitude.0
|
||||
processors:
|
||||
data:
|
||||
-
|
||||
oid: sysCPUUsage
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.2.1.64.{{ $index }}'
|
||||
descr: CPU
|
||||
divisor: 10
|
||||
sensors:
|
||||
count:
|
||||
data:
|
||||
-
|
||||
oid: sysDFSDetectedCount
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.2.1.36.{{ $index }}'
|
||||
index: sysDFSDetectedCount
|
||||
descr: DFS Detected Count
|
||||
-
|
||||
oid: cambiumGPSNumTrackedSat
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.1.2.84.{{ $index }}'
|
||||
index: cambiumGPSNumTrackedSat
|
||||
descr: GPS Satellites Tracked
|
||||
-
|
||||
oid: cambiumGPSNumVisibleSat
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.1.2.85.{{ $index }}'
|
||||
index: cambiumGPSNumVisibleSat
|
||||
descr: GPS Satellites Visible
|
||||
state:
|
||||
data:
|
||||
-
|
||||
oid: cambiumEffectiveSyncSource
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.1.1.7.{{ $index }}'
|
||||
index: cambiumEffectiveSyncSource
|
||||
descr: GPS Sync Status
|
||||
state_name: cambiumEffectiveSyncSource
|
||||
states:
|
||||
- { value: 1, generic: 0, graph: 1, descr: GPS Sync Up }
|
||||
- { value: 2, generic: 2, graph: 1, descr: GPS Sync Down }
|
||||
- { value: 3, generic: 0, graph: 1, descr: CMM4 Sync }
|
||||
- { value: 4, generic: 0, graph: 1, descr: CMM3 Sync }
|
||||
-
|
||||
oid: cambiumDFSStatus
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.1.1.6.{{ $index }}'
|
||||
index: cambiumDFSStatus
|
||||
descr: DFS Status
|
||||
state_name: cambiumDFSStatus
|
||||
states:
|
||||
- { value: 1, generic: 1, graph: 1, descr: N/A }
|
||||
- { value: 2, generic: 1, graph: 1, descr: Channel Availability Check }
|
||||
- { value: 3, generic: 0, graph: 1, descr: In-Service }
|
||||
- { value: 4, generic: 2, graph: 1, descr: Radar Signal Detected }
|
||||
- { value: 5, generic: 0, graph: 1, descr: In-Service Monitoring at Alternative Channel }
|
||||
- { value: 6, generic: 2, graph: 1, descr: System Not In Service due to DFS }
|
||||
-
|
||||
oid: ePMP2000SmartAntennaStatus
|
||||
num_oid: '.1.3.6.1.4.1.17713.21.1.1.41.{{ $index }}'
|
||||
index: ePMP2000SmartAntennaStatus
|
||||
descr: Smart Antenna Status
|
||||
state_name: ePMP2000SmartAntennaStatus
|
||||
states:
|
||||
- { value: 0, generic: 1, graph: 0, descr: Not Powered }
|
||||
- { value: 1, generic: 3, graph: 0, descr: Powered / Not Detected }
|
||||
- { value: 3, generic: 0, graph: 0, descr: Detected }
|
||||
Loading…
Add table
Reference in a new issue