defmodule Towerops.Devices.Firmware do @moduledoc """ Context for managing firmware releases and device firmware history. """ import Ecto.Query, warn: false alias Towerops.Devices.DeviceFirmwareHistory alias Towerops.Devices.FirmwareRelease alias Towerops.Repo require Logger @doc """ Creates or updates a firmware release record. Uses vendor + product_line as the unique key. If a record exists, it will be updated. Otherwise, a new record is created. ## Examples iex> upsert_firmware_release(%{vendor: "mikrotik", product_line: "routeros", version: "7.14.1", fetched_at: DateTime.utc_now()}) {:ok, %FirmwareRelease{}} iex> upsert_firmware_release(%{vendor: "mikrotik"}) {:error, %Ecto.Changeset{}} """ def upsert_firmware_release(attrs) do vendor = Map.get(attrs, :vendor) || Map.get(attrs, "vendor") product_line = Map.get(attrs, :product_line) || Map.get(attrs, "product_line") # Find existing record (handle nil product_line) existing = if vendor && product_line do Repo.one(from(f in FirmwareRelease, where: f.vendor == ^vendor and f.product_line == ^product_line)) end changeset = case existing do nil -> FirmwareRelease.changeset(%FirmwareRelease{}, attrs) record -> FirmwareRelease.changeset(record, attrs) end Repo.insert_or_update(changeset) end @doc """ Gets the latest firmware release for a vendor and product line. Returns nil if either vendor or product_line is nil. ## Examples iex> get_latest_firmware_release("mikrotik", "routeros") %FirmwareRelease{} iex> get_latest_firmware_release("unknown", "unknown") nil iex> get_latest_firmware_release("mikrotik", nil) nil """ def get_latest_firmware_release(nil, _product_line), do: nil def get_latest_firmware_release(_vendor, nil), do: nil def get_latest_firmware_release(vendor, product_line) do Repo.one(from(f in FirmwareRelease, where: f.vendor == ^vendor and f.product_line == ^product_line)) end @doc """ Lists all firmware releases. ## Examples iex> list_firmware_releases() [%FirmwareRelease{}, ...] """ def list_firmware_releases do Repo.all(FirmwareRelease) end @doc """ Lists firmware history for a device, most recent first. ## Examples iex> list_device_firmware_history(device_id, 20) [%DeviceFirmwareHistory{}, ...] """ def list_device_firmware_history(snmp_device_id, limit \\ 20) do DeviceFirmwareHistory |> where([h], h.snmp_device_id == ^snmp_device_id) |> order_by([h], desc: h.detected_at) |> limit(^limit) |> Repo.all() end @doc """ Logs a firmware version change for a device. Creates a firmware history record and broadcasts the change via PubSub. ## Examples iex> log_firmware_change(snmp_device_id, "7.12.0", "7.14.1") {:ok, %DeviceFirmwareHistory{}} """ def log_firmware_change(snmp_device_id, old_version, new_version) do attrs = %{ snmp_device_id: snmp_device_id, old_version: old_version, new_version: new_version, detected_at: DateTime.utc_now(), detection_method: "discovery" } changeset = DeviceFirmwareHistory.changeset(%DeviceFirmwareHistory{}, attrs) case Repo.insert(changeset) do {:ok, _history} = result -> Logger.info("Firmware version changed", snmp_device_id: snmp_device_id, old_version: old_version, new_version: new_version ) Phoenix.PubSub.broadcast( Towerops.PubSub, "device:#{snmp_device_id}", {:firmware_changed, snmp_device_id, old_version, new_version} ) result error -> error end end end