mikrotik version upgrade tracking

This commit is contained in:
Graham McIntire 2026-02-01 12:35:41 -06:00
parent 7ca07010ab
commit 612bed2dd5
No known key found for this signature in database
3 changed files with 140 additions and 68 deletions

View file

@ -84,6 +84,10 @@ defmodule Towerops.Workers.FirmwareVersionFetcherWorker do
if version == "" or is_nil(version) do
{:error, :invalid_data}
else
# Extract clean version number from RSS title
# "RouterOS 7.21.2 [stable]" -> "7.21.2"
clean_version = extract_version_number(version)
# Parse RFC822 date
release_date = parse_rfc822_date(pub_date_string)
@ -91,14 +95,15 @@ defmodule Towerops.Workers.FirmwareVersionFetcherWorker do
%{
vendor: "mikrotik",
product_line: "routeros",
version: version,
version: clean_version,
release_date: release_date,
download_url: download_url,
changelog_url: @changelog_url,
fetched_at: DateTime.utc_now(),
metadata: %{
"rss_title" => channel_title,
"rss_description" => description
"rss_description" => description,
"raw_version" => version
}
}}
end
@ -111,6 +116,20 @@ defmodule Towerops.Workers.FirmwareVersionFetcherWorker do
{:error, :parse_error}
end
# Extract clean version number from RSS title
# Examples:
# "RouterOS 7.21.2 [stable]" -> "7.21.2"
# "7.14.1" -> "7.14.1"
defp extract_version_number(version_string) do
# Match pattern: one or more digits, followed by dot, followed by digits
# This extracts semantic version numbers like "7.21.2"
case Regex.run(~r/\d+\.\d+(?:\.\d+)?/, version_string) do
[version] -> version
# Fallback to original if no match
_ -> version_string
end
end
# Parse RFC822 date format used in RSS feeds
# Example: "Wed, 15 Jan 2025 12:00:00 +0000"
defp parse_rfc822_date(date_string) when is_binary(date_string) and date_string != "" do

View file

@ -286,12 +286,27 @@ defmodule ToweropsWeb.DeviceLive.Show do
end
end
defp determine_vendor("MikroTik"), do: "mikrotik"
defp determine_vendor("Cisco"), do: "cisco"
defp determine_vendor("Ubiquiti"), do: "ubiquiti"
defp determine_vendor(manufacturer) when is_binary(manufacturer) do
manufacturer_lower = String.downcase(manufacturer)
cond do
String.contains?(manufacturer_lower, "mikrotik") -> "mikrotik"
String.contains?(manufacturer_lower, "cisco") -> "cisco"
String.contains?(manufacturer_lower, "ubiquiti") -> "ubiquiti"
true -> nil
end
end
defp determine_vendor(_), do: nil
defp determine_product_line("MikroTik"), do: "routeros"
defp determine_product_line(manufacturer) when is_binary(manufacturer) do
manufacturer_lower = String.downcase(manufacturer)
if String.contains?(manufacturer_lower, "mikrotik") do
"routeros"
end
end
defp determine_product_line(_), do: nil
defp firmware_update_available?(nil, _), do: false
@ -301,7 +316,29 @@ defmodule ToweropsWeb.DeviceLive.Show do
current = snmp_device.firmware_version
available = available_firmware.version
current && available && VersionComparator.newer?(current, available)
# Extract clean version numbers for comparison
# "RouterOS 7.20.6 (Level 6)" -> "7.20.6"
# "7.21.2" -> "7.21.2"
current_clean = extract_version_number(current)
available_clean = extract_version_number(available)
current_clean && available_clean && VersionComparator.newer?(current_clean, available_clean)
end
# Extract semantic version number from version string
# Examples:
# "RouterOS 7.20.6 (Level 6)" -> "7.20.6"
# "7.21.2" -> "7.21.2"
# "v7.14.1" -> "7.14.1"
defp extract_version_number(nil), do: nil
defp extract_version_number(""), do: nil
defp extract_version_number(version_string) when is_binary(version_string) do
# Match semantic version pattern: X.Y or X.Y.Z
case Regex.run(~r/\d+\.\d+(?:\.\d+)?/, version_string) do
[version] -> version
_ -> nil
end
end
defp format_date(nil), do: ""

View file

@ -205,57 +205,6 @@
<div class="mt-6">
<%= case @active_tab do %>
<% "overview" -> %>
<!-- Firmware Update Indicator -->
<%= if firmware_update_available?(@snmp_device, @available_firmware) do %>
<div class="mb-6 rounded-md bg-blue-50 dark:bg-blue-900/20 p-4 border border-blue-200 dark:border-blue-800">
<div class="flex">
<div class="flex-shrink-0">
<.icon name="hero-arrow-up-circle" class="h-5 w-5 text-blue-400" />
</div>
<div class="ml-3 flex-1">
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">
Firmware Update Available
</h3>
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
<p>
Current version:
<span class="font-mono font-semibold">{@snmp_device.firmware_version}</span>
<br /> Latest version:
<span class="font-mono font-semibold">{@available_firmware.version}</span>
<%= if @available_firmware.release_date do %>
<span class="text-xs">
(released {format_date(@available_firmware.release_date)})
</span>
<% end %>
</p>
</div>
<div class="mt-3">
<a
href={@available_firmware.download_url}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Download Firmware
<.icon name="hero-arrow-top-right-on-square" class="ml-1.5 h-4 w-4" />
</a>
<%= if @available_firmware.changelog_url do %>
<a
href={@available_firmware.changelog_url}
target="_blank"
rel="noopener noreferrer"
class="ml-3 inline-flex items-center text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500"
>
View Release Notes
<.icon name="hero-arrow-top-right-on-square" class="ml-1 h-4 w-4" />
</a>
<% end %>
</div>
</div>
</div>
</div>
<% end %>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
<!-- Left Column: Device Information & Traffic -->
<div class="space-y-6">
@ -293,16 +242,83 @@
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="text-sm text-gray-600 dark:text-gray-400">Operating System</dt>
<dd class="text-sm font-medium text-gray-900 dark:text-white">
{cond do
@snmp_device.manufacturer && @snmp_device.firmware_version ->
"#{@snmp_device.manufacturer} #{@snmp_device.firmware_version}"
<div class="flex items-center gap-2">
<span>
{cond do
@snmp_device.manufacturer && @snmp_device.firmware_version ->
"#{@snmp_device.manufacturer} #{@snmp_device.firmware_version}"
@snmp_device.manufacturer ->
@snmp_device.manufacturer
@snmp_device.manufacturer ->
@snmp_device.manufacturer
true ->
@snmp_device.sys_descr || "N/A"
end}
true ->
@snmp_device.sys_descr || "N/A"
end}
</span>
<%= if firmware_update_available?(@snmp_device, @available_firmware) do %>
<div class="group/firmware relative inline-flex items-center">
<.icon
name="hero-arrow-up-circle"
class="h-5 w-5 text-blue-500 cursor-help dark:text-blue-400"
/>
<!-- Invisible bridge to prevent gap -->
<div class="absolute left-0 top-0 w-full h-8 z-40"></div>
<!-- Tooltip popup -->
<div class="invisible group-hover/firmware:visible hover:visible absolute left-0 top-7 z-50 w-80 rounded-lg bg-white dark:bg-gray-800 shadow-xl ring-1 ring-black ring-opacity-5 dark:ring-white dark:ring-opacity-10 p-4">
<div class="flex items-start gap-3">
<.icon
name="hero-arrow-up-circle"
class="h-5 w-5 text-blue-500 dark:text-blue-400 flex-shrink-0 mt-0.5"
/>
<div class="flex-1 min-w-0">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
Firmware Update Available
</h4>
<div class="space-y-1.5 text-xs text-gray-600 dark:text-gray-400">
<div>
<span class="font-medium">Current:</span>
<span class="font-mono ml-1">{@snmp_device.firmware_version}</span>
</div>
<div>
<span class="font-medium">Latest:</span>
<span class="font-mono ml-1 text-blue-600 dark:text-blue-400">
{@available_firmware.version}
</span>
<%= if @available_firmware.release_date do %>
<span class="text-gray-500 dark:text-gray-500">
(released {format_date(@available_firmware.release_date)})
</span>
<% end %>
</div>
</div>
<div class="mt-3 flex gap-2">
<a
href={@available_firmware.download_url}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded px-2 py-1 text-xs font-medium bg-blue-600 text-white hover:bg-blue-700"
>
Download
<.icon name="hero-arrow-top-right-on-square" class="h-3 w-3" />
</a>
<%= if @available_firmware.changelog_url do %>
<a
href={@available_firmware.changelog_url}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded px-2 py-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20"
>
Release Notes
<.icon name="hero-arrow-top-right-on-square" class="h-3 w-3" />
</a>
<% end %>
</div>
</div>
</div>
</div>
</div>
<% end %>
</div>
</dd>
</div>
@ -790,7 +806,7 @@
</.link>
</dt>
<dd class="text-sm font-medium text-gray-900 dark:text-white">
<%= if sensor.latest_reading do %>
<%= if sensor.latest_reading && sensor.latest_reading.value do %>
{trunc(sensor.latest_reading.value)}
<%= if sensor.sensor_unit && sensor.sensor_unit != "" do %>
{sensor.sensor_unit}
@ -826,7 +842,7 @@
</.link>
</dt>
<dd class="text-sm font-medium text-gray-900 dark:text-white">
<%= if sensor.latest_reading do %>
<%= if sensor.latest_reading && sensor.latest_reading.value do %>
<%= if sensor.sensor_type in ["clients", "ccq", "utilization"] do %>
{trunc(sensor.latest_reading.value)}
<% else %>