defmodule ToweropsWeb.TraceLive.Index do @moduledoc """ LiveView for the Subscriber Trace / Help Desk Mode. Provides a unified search across subscribers, devices, and access points, then assembles a full "patient chart" trace view showing all relevant network health information at a glance. """ use ToweropsWeb, :live_view alias Towerops.Trace attr :trace, :map, required: true defp issues_callout(assigns) do issues = [] issues = if assigns.trace[:device] && assigns.trace.device.status == :down, do: [t("Device is DOWN") | issues], else: issues issues = if assigns.trace[:qoe_metrics] && assigns.trace.qoe_metrics[:qoe_score] && assigns.trace.qoe_metrics.qoe_score < 50, do: ["QoE score is critical (#{assigns.trace.qoe_metrics.qoe_score})" | issues], else: issues issues = if assigns.trace[:active_alerts] && assigns.trace.active_alerts != [], do: ["#{length(assigns.trace.active_alerts)} active alert(s)" | issues], else: issues assigns = assign(assigns, :issues, Enum.reverse(issues)) ~H"""
<.icon name="hero-exclamation-triangle" class="h-5 w-5" />

Issues Detected

""" end attr :label, :string, required: true attr :value, :string, required: true attr :quality, :atom, default: :unknown defp metric_card(assigns) do color = case assigns.quality do :good -> "border-green-200 dark:border-green-800" :warning -> "border-yellow-200 dark:border-yellow-800" :bad -> "border-red-200 dark:border-red-800" _ -> "border-base-200" end assigns = assign(assigns, :color, color) ~H"""
{@label}
{@value}
""" end @impl true def mount(_params, _session, socket) do {:ok, socket |> assign(:page_title, t("Trace")) |> assign(:query, "") |> assign(:results, []) |> assign(:trace, nil) |> assign(:trace_type, nil) |> assign(:trace_id, nil) |> assign(:searching, false)} end @impl true def handle_params(params, _url, socket) do organization = socket.assigns.current_scope.organization socket = case params do %{"type" => type, "id" => id} -> type_atom = safe_type(type) trace = Trace.assemble_trace(organization.id, type_atom, id) socket |> assign(:trace, trace) |> assign(:trace_type, type_atom) |> assign(:trace_id, id) _ -> socket end {:noreply, socket} end @impl true def handle_event("search", %{"query" => query}, socket) do organization = socket.assigns.current_scope.organization results = if String.trim(query) == "" do [] else Trace.search(organization.id, query) end {:noreply, socket |> assign(:query, query) |> assign(:results, results) |> assign(:trace, nil) |> assign(:trace_type, nil) |> assign(:trace_id, nil)} end @impl true def handle_event("select_result", %{"type" => type, "id" => id}, socket) do {:noreply, push_patch(socket, to: ~p"/trace?type=#{type}&id=#{id}")} end @impl true def handle_event("clear", _params, socket) do {:noreply, socket |> assign(:query, "") |> assign(:results, []) |> assign(:trace, nil) |> assign(:trace_type, nil) |> assign(:trace_id, nil) |> push_patch(to: ~p"/trace")} end defp type_badge_class(:account), do: "badge-primary" defp type_badge_class(:inventory_item), do: "badge-secondary" defp type_badge_class(:site), do: "badge-warning" defp type_badge_class(:device), do: "badge-accent" defp type_badge_class(:access_point), do: "badge-info" defp type_badge_class(_), do: "badge-ghost" defp type_badge_label(:account), do: "Account" defp type_badge_label(:inventory_item), do: "Inventory" defp type_badge_label(:site), do: "Site" defp type_badge_label(:device), do: "Device" defp type_badge_label(:access_point), do: "AP" defp type_badge_label(_), do: "Unknown" defp status_badge_class("active"), do: "badge-success" defp status_badge_class("suspended"), do: "badge-warning" defp status_badge_class("cancelled"), do: "badge-error" defp status_badge_class(_), do: "badge-ghost" defp device_status_color(:up), do: "text-green-600 dark:text-green-400" defp device_status_color(:down), do: "text-red-600 dark:text-red-400" defp device_status_color(_), do: "text-gray-500 dark:text-gray-400" defp format_speed(nil), do: "—" defp format_speed(speed) when is_number(speed) and speed >= 1000, do: "#{Float.round(speed / 1000, 1)} Gbps" defp format_speed(speed) when is_number(speed), do: "#{speed} Mbps" defp format_speed(speed), do: "#{speed}" defp format_relative_time(nil), do: "—" defp format_relative_time(dt) do diff = DateTime.diff(DateTime.utc_now(), dt, :second) cond do diff < 60 -> "just now" diff < 3600 -> "#{div(diff, 60)}m ago" diff < 86_400 -> "#{div(diff, 3600)}h ago" true -> "#{div(diff, 86_400)}d ago" end end defp format_score(nil), do: "—" defp format_score(score) when is_number(score), do: "#{Float.round(score * 1.0, 1)}" defp format_score(score), do: "#{score}" defp format_pct(nil), do: "—" defp format_pct(val) when is_number(val), do: "#{Float.round(val * 1.0, 1)}%" defp format_pct(val), do: "#{val}" defp format_ms(nil), do: "—" defp format_ms(val) when is_number(val), do: "#{Float.round(val * 1.0, 1)} ms" defp format_ms(val), do: "#{val}" defp format_throughput(nil), do: "—" defp format_throughput(val) when is_number(val) and val >= 1000, do: "#{Float.round(val / 1000, 1)} Gbps" defp format_throughput(val) when is_number(val), do: "#{Float.round(val * 1.0, 1)} Mbps" defp format_throughput(val), do: "#{val}" defp format_alert_type(nil), do: "—" defp format_alert_type(type) when is_binary(type), do: type |> String.replace("_", " ") |> String.capitalize() defp format_alert_type(type), do: "#{type}" defp score_quality(nil), do: :unknown defp score_quality(s) when s >= 80, do: :good defp score_quality(s) when s >= 50, do: :warning defp score_quality(_), do: :bad defp score_color(nil), do: "text-gray-400" defp score_color(s) when s >= 80, do: "text-green-600 dark:text-green-400" defp score_color(s) when s >= 50, do: "text-yellow-600 dark:text-yellow-400" defp score_color(_), do: "text-red-600 dark:text-red-400" defp latency_color(nil), do: "text-gray-400" defp latency_color(v) when v <= 30, do: "text-green-600 dark:text-green-400" defp latency_color(v) when v <= 80, do: "text-yellow-600 dark:text-yellow-400" defp latency_color(_), do: "text-red-600 dark:text-red-400" defp jitter_color(nil), do: "text-gray-400" defp jitter_color(v) when v <= 10, do: "text-green-600 dark:text-green-400" defp jitter_color(v) when v <= 30, do: "text-yellow-600 dark:text-yellow-400" defp jitter_color(_), do: "text-red-600 dark:text-red-400" defp loss_color(nil), do: "text-gray-400" defp loss_color(v) when v <= 1, do: "text-green-600 dark:text-green-400" defp loss_color(v) when v <= 5, do: "text-yellow-600 dark:text-yellow-400" defp loss_color(_), do: "text-red-600 dark:text-red-400" defp device_status_dot(:up), do: "bg-green-500" defp device_status_dot(:down), do: "bg-red-500" defp device_status_dot(_), do: "bg-gray-400" defp alert_dot(%{severity: "critical"}), do: "bg-red-500" defp alert_dot(%{severity: "warning"}), do: "bg-yellow-500" defp alert_dot(%{alert_type: "down"}), do: "bg-red-500" defp alert_dot(_), do: "bg-orange-400" defp urgency_dot("critical"), do: "bg-red-500" defp urgency_dot("high"), do: "bg-orange-500" defp urgency_dot("medium"), do: "bg-yellow-500" defp urgency_dot(_), do: "bg-blue-400" defp airtime_quality(nil), do: :unknown defp airtime_quality(v) when v <= 50, do: :good defp airtime_quality(v) when v <= 80, do: :warning defp airtime_quality(_), do: :bad defp safe_type("account"), do: :account defp safe_type("inventory_item"), do: :inventory_item defp safe_type("site"), do: :site defp safe_type("device"), do: :device defp safe_type("access_point"), do: :access_point defp safe_type(_), do: :account end