diff --git a/CLAUDE.md b/CLAUDE.md index d8c26594..1cf76d3d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -939,4 +939,5 @@ When writing new LiveView code or JavaScript hooks: - Take another snapshot - Compare - memory should not continuously grow - assets are rebuilt automatically on filesystem change -- never try to run npm, it's not included in phoenix \ No newline at end of file +- never try to run npm, it's not included in phoenix +- assets build themselves on file change, you do not need to ever run mix assets.build \ No newline at end of file diff --git a/assets/js/app.ts b/assets/js/app.ts index aacee415..7f32d3e5 100644 --- a/assets/js/app.ts +++ b/assets/js/app.ts @@ -822,15 +822,23 @@ const NetworkMap = { // MikroTik Port Sync - automatically switches port when SSL checkbox is toggled const MikrotikPortSync = { handleSslChange: null as ((e: Event) => void) | null, + sslCheckbox: null as HTMLInputElement | null, - mounted(this: any) { - const sslCheckbox = this.el.querySelector('[name="device[mikrotik_use_ssl]"]') + setupListener(this: any) { + // Clean up existing listener first + if (this.sslCheckbox && this.handleSslChange) { + this.sslCheckbox.removeEventListener('change', this.handleSslChange) + } + + const sslCheckbox = this.el.querySelector('[name="device[mikrotik_use_ssl]"]') as HTMLInputElement const portInput = this.el.querySelector('[name="device[mikrotik_port]"]') as HTMLInputElement if (!sslCheckbox || !portInput) return + this.sslCheckbox = sslCheckbox + this.handleSslChange = () => { - const isChecked = (sslCheckbox as HTMLInputElement).checked + const isChecked = sslCheckbox.checked const currentPort = portInput.value.trim() // Only update port if it's empty or set to one of the default values @@ -844,11 +852,20 @@ const MikrotikPortSync = { sslCheckbox.addEventListener('change', this.handleSslChange) }, + mounted(this: any) { + this.setupListener() + }, + + updated(this: any) { + // Re-attach listener when LiveView updates the DOM (e.g., when mikrotik_enabled changes) + this.setupListener() + }, + destroyed(this: any) { - const sslCheckbox = this.el.querySelector('[name="device[mikrotik_use_ssl]"]') - if (sslCheckbox && this.handleSslChange) { - sslCheckbox.removeEventListener('change', this.handleSslChange) + if (this.sslCheckbox && this.handleSslChange) { + this.sslCheckbox.removeEventListener('change', this.handleSslChange) this.handleSslChange = null + this.sslCheckbox = null } } } diff --git a/lib/towerops_web/live/device_live/form.html.heex b/lib/towerops_web/live/device_live/form.html.heex index b7d4d774..44d5f666 100644 --- a/lib/towerops_web/live/device_live/form.html.heex +++ b/lib/towerops_web/live/device_live/form.html.heex @@ -3,465 +3,592 @@ current_scope={@current_scope} >
{if @live_action == :new, do: "Add new device to monitor", else: "Update device details"} - - -
- You have {remaining} {if remaining == 1, do: "slot", else: "slots"} remaining. -
++ You have {remaining} {if remaining == 1, do: "slot", else: "slots"} remaining. +
+- A device with this IP address already exists: - <.link - navigate={~p"/devices/#{@duplicate_device.id}"} - class="font-medium underline hover:text-amber-900 dark:hover:text-amber-100" - > - {@duplicate_device.name || @duplicate_device.ip_address} - + <.form for={@form} id="device-form" phx-change="validate" phx-submit="save"> +
+ Core device identification and location details.
Non-routable IP address detected
-- This IP address (private network or CGNAT range) cannot be reached from the cloud. - To monitor this device, assign it to a - <.link - navigate={~p"/agents"} - class="font-medium underline" +
+ A device with this IP address already exists: + <.link + navigate={~p"/devices/#{@duplicate_device.id}"} + class="font-medium underline hover:text-amber-900 dark:hover:text-amber-100" + > + {@duplicate_device.name || @duplicate_device.ip_address} + +
+- <.icon name="hero-exclamation-triangle" class="h-4 w-4" /> - Overriding site/organization defaults - - this device is explicitly assigned -
- <% :site -> %> -- <.icon name="hero-building-office" class="h-4 w-4" /> Inherited from site: - {@effective_agent_name} -
- <% :organization -> %> -- <.icon name="hero-building-office-2" class="h-4 w-4" /> - Inherited from organization: {@effective_agent_name} -
- <% :global -> %> -- <.icon name="hero-globe-alt" class="h-4 w-4" /> - Using global default cloud poller: {@effective_agent_name} -
- <% :none -> %> -- <.icon name="hero-cloud" class="h-4 w-4" /> No agent assigned - cloud polling -
- <% end %> - <% else %> -- Assign this device to a remote agent for local SNMP polling. Leave empty to inherit from site or organization defaults. -
- <% end %> - <% end %> - -- Monitor device availability with ICMP pings and collect detailed information via SNMP - (interfaces, sensors, system info). -
- <% else %> -Monitor device availability with ICMP pings only (no SNMP discovery).
- <% end %> -Non-routable IP address detected
++ This IP address (private network or CGNAT range) cannot be reached from the cloud. + To monitor this device, assign it to a + <.link navigate={~p"/agents"} class="font-medium underline"> + remote agent + + deployed on the same network. +
+- <.icon name="hero-exclamation-triangle" class="h-4 w-4" /> - Overriding site/organization SNMP defaults -
- <% :site -> %> -- <.icon name="hero-building-office" class="h-4 w-4" /> - Inherited from site (v{@effective_snmp_version}, community: {@effective_snmp_community}) -
- <% :organization -> %> -- <.icon name="hero-building-office-2" class="h-4 w-4" /> - Inherited from organization (v{@effective_snmp_version}, community: {@effective_snmp_community}) -
- <% :default -> %> -- <.icon name="hero-information-circle" class="h-4 w-4" /> - Using default (v2c, no community set - configure at organization or site level) -
- <% end %> - <% end %> - - <.input - field={@form[:snmp_port]} - type="number" - label="SNMP Port" - min="1" - max="65535" - value={@form[:snmp_port].value || 161} - /> - -+ Configure how and how often this device should be monitored. +
++ <.icon name="hero-exclamation-triangle" class="h-4 w-4" /> + Overriding + site/organization defaults - this device is explicitly assigned +
+ <% :site -> %> ++ <.icon name="hero-building-office" class="h-4 w-4" /> + Inherited from site: {@effective_agent_name} +
+ <% :organization -> %> ++ <.icon name="hero-building-office-2" class="h-4 w-4" /> + Inherited from organization: {@effective_agent_name} +
+ <% :global -> %> ++ <.icon name="hero-globe-alt" class="h-4 w-4" /> + Using global default cloud poller: + {@effective_agent_name} +
+ <% :none -> %> ++ <.icon name="hero-cloud" class="h-4 w-4" /> + No agent assigned - cloud polling +
+ <% end %> + <% else %> ++ Assign this device to a remote agent for local SNMP polling. Leave empty to inherit from site or organization defaults. +
+ <% end %> ++ Choose monitoring mode and configure SNMP settings for detailed device information. +
++ Monitor device availability with ICMP pings and collect detailed information via SNMP + (interfaces, sensors, system info). +
+ <% else %> +Monitor device availability with ICMP pings only (no SNMP discovery).
+ <% end %> ++ <.icon name="hero-exclamation-triangle" class="h-4 w-4" /> + Overriding site/organization SNMP defaults +
+ <% :site -> %> ++ <.icon name="hero-building-office" class="h-4 w-4" /> + Inherited from site (v{@effective_snmp_version}, community: {String.slice( + @effective_snmp_community, + 0..1 + )}{"*" + |> String.duplicate( + max(0, String.length(@effective_snmp_community) - 2) + )}) +
+ <% :organization -> %> ++ <.icon name="hero-building-office-2" class="h-4 w-4" /> + Inherited from organization (v{@effective_snmp_version}, community: {String.slice( + @effective_snmp_community, + 0..1 + )}{"*" + |> String.duplicate( + max(0, String.length(@effective_snmp_community) - 2) + )}) +
+ <% :default -> %> ++ <.icon name="hero-information-circle" class="h-4 w-4" /> + Using default (v2c, no community set - configure at organization or site level) +
+ <% end %> + <% end %> ++ +
Configure MikroTik RouterOS API access. Works alongside SNMP for enhanced device management.
- Inheriting credentials from {@mikrotik_config_source}. - Username: {@effective_mikrotik_username} -
++ Inheriting credentials from {@mikrotik_config_source}. + Username: {@effective_mikrotik_username} +
++ 🚨 Critical Security Warning: + Plain API (port 8728) sends credentials unencrypted over the network. + This setting is blocked when using cloud pollers + and should only be used with local agents on trusted networks. +
++ ⚠️ Security Warning: + Plain API (port 8728) sends credentials unencrypted. Use SSL (port 8729) whenever possible. +
+- 🚨 Critical Security Warning: - Plain API (port 8728) sends credentials unencrypted over the network. - This setting is blocked when using cloud pollers - and should only be used with local agents on trusted networks. -
-- ⚠️ Security Warning: - Plain API (port 8728) sends credentials unencrypted. Use SSL (port 8729) whenever possible. -
-- Once you delete this device, there is no going back. All monitoring history and alerts will be permanently deleted. +
+ Permanently delete this device and all its data.
- <.button - phx-click="delete" - data-confirm="Are you sure you want to delete this device? All monitoring history and alerts will be permanently deleted." - variant="danger" - > - <.icon name="hero-trash" class="h-4 w-4" /> Delete Device -+ Once you delete this device, there is no going back. All monitoring history and alerts will be permanently deleted. +
+ <.button + phx-click="delete" + data-confirm="Are you sure you want to delete this device? All monitoring history and alerts will be permanently deleted." + variant="danger" + > + <.icon name="hero-trash" class="h-4 w-4" /> Delete Device + +