defmodule ToweropsWeb.MikrotikBackupLive.Compare do @moduledoc false use ToweropsWeb, :live_view alias Towerops.Devices alias Towerops.Devices.MikrotikBackups alias Towerops.Devices.MikrotikBackups.Differ @impl true def mount(_params, _session, socket) do {:ok, socket} end @impl true def handle_params(%{"device_id" => device_id, "backup_a" => backup_a_id, "backup_b" => backup_b_id}, _uri, socket) do organization = socket.assigns.current_scope.organization # Verify device access case Devices.get_device(device_id) do nil -> {:noreply, socket |> put_flash(:error, t("Device not found")) |> push_navigate(to: ~p"/devices")} device -> if device.site.organization_id == organization.id do load_and_compare_backups(socket, device, device_id, backup_a_id, backup_b_id) else {:noreply, socket |> put_flash(:error, t("You don't have access to this device")) |> push_navigate(to: ~p"/devices")} end end end @impl true def handle_params(_params, _uri, socket) do {:noreply, socket |> put_flash(:error, t("Missing required parameters")) |> push_navigate(to: ~p"/devices")} end defp load_and_compare_backups(socket, device, device_id, backup_a_id, backup_b_id) do # Load backups backup_a = MikrotikBackups.get_backup!(backup_a_id) backup_b = MikrotikBackups.get_backup!(backup_b_id) # Verify backups belong to this device if backup_a.device_id != device_id || backup_b.device_id != device_id do {:noreply, socket |> put_flash(:error, t("Backups do not belong to this device")) |> push_navigate(to: ~p"/devices/#{device_id}?tab=backups")} else generate_and_display_diff(socket, device, backup_a, backup_b, device_id) end end defp generate_and_display_diff(socket, device, backup_a, backup_b, device_id) do # Decompress configs config_a = MikrotikBackups.decompress_config(backup_a.config_compressed) config_b = MikrotikBackups.decompress_config(backup_b.config_compressed) # Mask sensitive data config_a_masked = Differ.mask_sensitive_data(config_a) config_b_masked = Differ.mask_sensitive_data(config_b) # Generate diff case Differ.generate_unified_diff(config_a_masked, config_b_masked) do {:ok, diff_output} -> stats = Differ.calculate_diff_stats(diff_output) {:noreply, socket |> assign(:page_title, "Compare Backups - #{device.name}") |> assign(:device, device) |> assign(:backup_a, backup_a) |> assign(:backup_b, backup_b) |> assign(:diff_output, diff_output) |> assign(:stats, stats)} {:error, reason} -> {:noreply, socket |> put_flash(:error, t("Failed to generate diff: %{reason}", reason: reason)) |> push_navigate(to: ~p"/devices/#{device_id}?tab=backups")} end end @impl true def handle_event("download_config", %{"backup" => "a"}, socket) do backup = socket.assigns.backup_a config_text = MikrotikBackups.decompress_config(backup.config_compressed) device = socket.assigns.device filename = "#{device.name}_backup_#{Calendar.strftime(backup.backed_up_at, "%Y%m%d_%H%M%S")}.rsc" {:noreply, push_event(socket, "download", %{content: config_text, filename: filename, mime_type: "text/plain"})} end @impl true def handle_event("download_config", %{"backup" => "b"}, socket) do backup = socket.assigns.backup_b config_text = MikrotikBackups.decompress_config(backup.config_compressed) device = socket.assigns.device filename = "#{device.name}_backup_#{Calendar.strftime(backup.backed_up_at, "%Y%m%d_%H%M%S")}.rsc" {:noreply, push_event(socket, "download", %{content: config_text, filename: filename, mime_type: "text/plain"})} end @doc false def format_bytes(bytes) when is_integer(bytes) and bytes >= 1_073_741_824, do: "#{Float.round(bytes / 1_073_741_824, 1)} GB" def format_bytes(bytes) when is_integer(bytes) and bytes >= 1_048_576, do: "#{Float.round(bytes / 1_048_576, 1)} MB" def format_bytes(bytes) when is_integer(bytes) and bytes >= 1_024, do: "#{Float.round(bytes / 1_024, 1)} KB" def format_bytes(bytes) when is_integer(bytes) and bytes >= 0, do: "#{bytes} B" def format_bytes(_), do: "Invalid byte count" end