fix(insights): UI contrast + DeepSeek empty-string env-var fallback

UI:
- Type-specific evidence cards (red/amber/orange/rose/purple/cyan/
  indigo/blue) had `dark:text-{color}-300` labels and
  `dark:bg-{color}-800 dark:text-{color}-100` badges that washed out on
  the dark background. Bumped 31 label shades to -200 and switched the
  three problem badges (Above manufacturer spec, Investigate upstream
  first, own fleet, crit) to solid -600 with white text.
- upstream_degradation card got brighter device-link text and a
  hover state in dark mode.

DeepSeek:
- Empty-string DEEPSEEK_MODEL / DEEPSEEK_BASE_URL env vars were not
  treated as missing — `||` only short-circuits on nil/false — so a
  blank secret would override the @default_model and the API would
  reject `model: ""`. Both runtime.exs and the client itself now treat
  blank values as missing.
This commit is contained in:
Graham McIntire 2026-05-10 12:07:53 -05:00
parent e18d877695
commit c9202744a5
3 changed files with 62 additions and 42 deletions

View file

@ -23,10 +23,20 @@ end
# DeepSeek LLM (used by Towerops.LLM.DeepSeek for insight enrichment). # DeepSeek LLM (used by Towerops.LLM.DeepSeek for insight enrichment).
# DEEPSEEK_API_KEY is optional — when missing, enrichment is skipped and # DEEPSEEK_API_KEY is optional — when missing, enrichment is skipped and
# insights still display without an LLM-generated summary. # insights still display without an LLM-generated summary.
# Helper: treat empty-string env vars as missing (the DEEPSEEK_MODEL
# secret ships as "" by default, which would otherwise bypass `||`).
deepseek_env = fn name, fallback ->
case System.get_env(name) do
nil -> fallback
"" -> fallback
value -> value
end
end
config :towerops, Towerops.LLM.DeepSeek, config :towerops, Towerops.LLM.DeepSeek,
api_key: System.get_env("DEEPSEEK_API_KEY"), api_key: System.get_env("DEEPSEEK_API_KEY"),
base_url: System.get_env("DEEPSEEK_BASE_URL") || "https://api.deepseek.com/v1", base_url: deepseek_env.("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1"),
model: System.get_env("DEEPSEEK_MODEL") || "deepseek-v4-pro" model: deepseek_env.("DEEPSEEK_MODEL", "deepseek-v4-pro")
config :towerops, ToweropsWeb.Endpoint, http: [port: String.to_integer(System.get_env("PORT", "4000"))] config :towerops, ToweropsWeb.Endpoint, http: [port: String.to_integer(System.get_env("PORT", "4000"))]

View file

@ -20,8 +20,8 @@ defmodule Towerops.LLM.DeepSeek do
def complete(messages, opts \\ []) do def complete(messages, opts \\ []) do
cfg = Application.get_env(:towerops, __MODULE__, []) cfg = Application.get_env(:towerops, __MODULE__, [])
api_key = opts[:api_key] || cfg[:api_key] api_key = opts[:api_key] || cfg[:api_key]
base_url = opts[:base_url] || cfg[:base_url] || @default_base_url base_url = present(opts[:base_url]) || present(cfg[:base_url]) || @default_base_url
model = opts[:model] || cfg[:model] || @default_model model = present(opts[:model]) || present(cfg[:model]) || @default_model
if blank?(api_key) do if blank?(api_key) do
{:error, :missing_api_key} {:error, :missing_api_key}
@ -34,6 +34,13 @@ defmodule Towerops.LLM.DeepSeek do
defp blank?(""), do: true defp blank?(""), do: true
defp blank?(s) when is_binary(s), do: String.trim(s) == "" defp blank?(s) when is_binary(s), do: String.trim(s) == ""
# Returns the value if non-empty, nil otherwise — lets `||` cascade past
# blank env-var values (a `""` env var would otherwise short-circuit
# `present(opts[:x]) || present(cfg[:x]) || @default_x`).
defp present(nil), do: nil
defp present(s) when is_binary(s), do: if(String.trim(s) == "", do: nil, else: s)
defp present(other), do: other
defp request(base_url, api_key, model, messages, opts) do defp request(base_url, api_key, model, messages, opts) do
body = %{ body = %{
model: model, model: model,

View file

@ -136,7 +136,7 @@
id="bulk-actions" id="bulk-actions"
class="mt-4 flex items-center gap-3 rounded-lg bg-indigo-50 px-4 py-3 dark:bg-indigo-900/20" class="mt-4 flex items-center gap-3 rounded-lg bg-indigo-50 px-4 py-3 dark:bg-indigo-900/20"
> >
<span class="text-sm font-medium text-indigo-700 dark:text-indigo-300"> <span class="text-sm font-medium text-indigo-700 dark:text-indigo-200">
{MapSet.size(@selected_ids)} selected {MapSet.size(@selected_ids)} selected
</span> </span>
<button <button
@ -354,7 +354,7 @@
<%= if insight.source == "gaiia" && insight.metadata["finding_type"] == "ghost" do %> <%= if insight.source == "gaiia" && insight.metadata["finding_type"] == "ghost" do %>
<div class="mt-3 rounded-md border border-amber-200 bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-900/20"> <div class="mt-3 rounded-md border border-amber-200 bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-900/20">
<p class="text-xs font-medium text-amber-800 dark:text-amber-300 mb-2"> <p class="text-xs font-medium text-amber-800 dark:text-amber-200 mb-2">
<.icon name="hero-light-bulb" class="h-3.5 w-3.5 inline" /> <.icon name="hero-light-bulb" class="h-3.5 w-3.5 inline" />
{t("To fix this:")} {t("To fix this:")}
</p> </p>
@ -396,7 +396,7 @@
<div class="mt-3 rounded-md border border-orange-200 bg-orange-50 p-3 dark:border-orange-800 dark:bg-orange-900/20"> <div class="mt-3 rounded-md border border-orange-200 bg-orange-50 p-3 dark:border-orange-800 dark:bg-orange-900/20">
<div class="flex flex-wrap items-center gap-4 text-sm"> <div class="flex flex-wrap items-center gap-4 text-sm">
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-orange-700 dark:text-orange-300"> <span class="text-xs text-orange-700 dark:text-orange-200">
{t("Free airtime")} {t("Free airtime")}
</span> </span>
<span class="font-mono text-base font-semibold text-orange-900 dark:text-orange-100"> <span class="font-mono text-base font-semibold text-orange-900 dark:text-orange-100">
@ -404,7 +404,7 @@
</span> </span>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-orange-700 dark:text-orange-300"> <span class="text-xs text-orange-700 dark:text-orange-200">
{t("Subscribers")} {t("Subscribers")}
</span> </span>
<span class="font-mono text-base font-semibold text-orange-900 dark:text-orange-100"> <span class="font-mono text-base font-semibold text-orange-900 dark:text-orange-100">
@ -413,7 +413,7 @@
</div> </div>
<%= if insight.metadata["qoe_score"] do %> <%= if insight.metadata["qoe_score"] do %>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-orange-700 dark:text-orange-300"> <span class="text-xs text-orange-700 dark:text-orange-200">
{t("QoE score")} {t("QoE score")}
</span> </span>
<span class="font-mono text-base font-semibold text-orange-900 dark:text-orange-100"> <span class="font-mono text-base font-semibold text-orange-900 dark:text-orange-100">
@ -422,7 +422,7 @@
</div> </div>
<% end %> <% end %>
<%= if insight.metadata["ap_model"] do %> <%= if insight.metadata["ap_model"] do %>
<span class="ml-auto text-xs text-orange-700 dark:text-orange-300"> <span class="ml-auto text-xs text-orange-700 dark:text-orange-200">
{insight.metadata["ap_model"]} {insight.metadata["ap_model"]}
{insight.metadata["ap_firmware"] && {insight.metadata["ap_firmware"] &&
"(" <> insight.metadata["ap_firmware"] <> ")"} "(" <> insight.metadata["ap_firmware"] <> ")"}
@ -436,7 +436,7 @@
<div class="mt-3 rounded-md border border-rose-200 bg-rose-50 p-3 dark:border-rose-800 dark:bg-rose-900/20"> <div class="mt-3 rounded-md border border-rose-200 bg-rose-50 p-3 dark:border-rose-800 dark:bg-rose-900/20">
<div class="flex flex-wrap items-center gap-4 text-sm"> <div class="flex flex-wrap items-center gap-4 text-sm">
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-rose-700 dark:text-rose-300"> <span class="text-xs text-rose-700 dark:text-rose-200">
{t("Signal")} {t("Signal")}
</span> </span>
<span class="font-mono text-base font-semibold text-rose-900 dark:text-rose-100"> <span class="font-mono text-base font-semibold text-rose-900 dark:text-rose-100">
@ -444,7 +444,7 @@
</span> </span>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-rose-700 dark:text-rose-300"> <span class="text-xs text-rose-700 dark:text-rose-200">
{t("SNR")} {t("SNR")}
</span> </span>
<span class="font-mono text-base font-semibold text-rose-900 dark:text-rose-100"> <span class="font-mono text-base font-semibold text-rose-900 dark:text-rose-100">
@ -453,7 +453,7 @@
</div> </div>
<%= if insight.metadata["tx_rate"] && insight.metadata["rx_rate"] do %> <%= if insight.metadata["tx_rate"] && insight.metadata["rx_rate"] do %>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-rose-700 dark:text-rose-300"> <span class="text-xs text-rose-700 dark:text-rose-200">
{t("TX/RX rate")} {t("TX/RX rate")}
</span> </span>
<span class="font-mono text-base font-semibold text-rose-900 dark:text-rose-100"> <span class="font-mono text-base font-semibold text-rose-900 dark:text-rose-100">
@ -463,7 +463,7 @@
<% end %> <% end %>
<%= if insight.metadata["distance_m"] do %> <%= if insight.metadata["distance_m"] do %>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-rose-700 dark:text-rose-300"> <span class="text-xs text-rose-700 dark:text-rose-200">
{t("Distance")} {t("Distance")}
</span> </span>
<span class="font-mono text-base font-semibold text-rose-900 dark:text-rose-100"> <span class="font-mono text-base font-semibold text-rose-900 dark:text-rose-100">
@ -472,7 +472,7 @@
</div> </div>
<% end %> <% end %>
<%= if insight.metadata["cpe_hostname"] do %> <%= if insight.metadata["cpe_hostname"] do %>
<span class="ml-auto text-xs text-rose-700 dark:text-rose-300"> <span class="ml-auto text-xs text-rose-700 dark:text-rose-200">
{insight.metadata["cpe_hostname"]} {insight.metadata["cpe_hostname"]}
</span> </span>
<% end %> <% end %>
@ -484,7 +484,7 @@
<div class="mt-3 rounded-md border border-cyan-200 bg-cyan-50 p-3 dark:border-cyan-800 dark:bg-cyan-900/20"> <div class="mt-3 rounded-md border border-cyan-200 bg-cyan-50 p-3 dark:border-cyan-800 dark:bg-cyan-900/20">
<div class="flex flex-wrap items-center gap-4 text-sm"> <div class="flex flex-wrap items-center gap-4 text-sm">
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-cyan-700 dark:text-cyan-300"> <span class="text-xs text-cyan-700 dark:text-cyan-200">
{t("Port")} {t("Port")}
</span> </span>
<span class="font-mono text-base font-semibold text-cyan-900 dark:text-cyan-100"> <span class="font-mono text-base font-semibold text-cyan-900 dark:text-cyan-100">
@ -492,7 +492,7 @@
</span> </span>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-cyan-700 dark:text-cyan-300"> <span class="text-xs text-cyan-700 dark:text-cyan-200">
{t("Rx power")} {t("Rx power")}
</span> </span>
<span class="font-mono text-base font-semibold text-cyan-900 dark:text-cyan-100"> <span class="font-mono text-base font-semibold text-cyan-900 dark:text-cyan-100">
@ -501,7 +501,7 @@
</div> </div>
<%= if insight.metadata["tx_power_dbm"] do %> <%= if insight.metadata["tx_power_dbm"] do %>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-cyan-700 dark:text-cyan-300"> <span class="text-xs text-cyan-700 dark:text-cyan-200">
{t("Tx power")} {t("Tx power")}
</span> </span>
<span class="font-mono text-base font-semibold text-cyan-900 dark:text-cyan-100"> <span class="font-mono text-base font-semibold text-cyan-900 dark:text-cyan-100">
@ -510,7 +510,7 @@
</div> </div>
<% end %> <% end %>
<%= if insight.metadata["transceiver_type"] do %> <%= if insight.metadata["transceiver_type"] do %>
<span class="ml-auto text-xs text-cyan-700 dark:text-cyan-300"> <span class="ml-auto text-xs text-cyan-700 dark:text-cyan-200">
{insight.metadata["transceiver_type"]} {insight.metadata["transceiver_type"]}
<%= if insight.metadata["wavelength_nm"] do %> <%= if insight.metadata["wavelength_nm"] do %>
· {insight.metadata["wavelength_nm"]} nm · {insight.metadata["wavelength_nm"]} nm
@ -522,38 +522,41 @@
<% end %> <% end %>
<%= if insight.type == "upstream_degradation" do %> <%= if insight.type == "upstream_degradation" do %>
<div class="mt-3 rounded-md border border-indigo-200 bg-indigo-50 p-3 dark:border-indigo-800 dark:bg-indigo-900/20"> <div class="mt-3 rounded-md border border-indigo-200 bg-indigo-50 p-3 dark:border-indigo-700 dark:bg-indigo-950/40">
<div class="flex flex-wrap items-center gap-4 text-sm"> <div class="flex flex-wrap items-center gap-4 text-sm">
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-indigo-700 dark:text-indigo-300"> <span class="text-xs font-medium text-indigo-700 dark:text-indigo-200">
{t("Degraded APs")} {t("Degraded APs")}
</span> </span>
<span class="font-mono text-base font-semibold text-indigo-900 dark:text-indigo-100"> <span class="font-mono text-base font-semibold text-indigo-900 dark:text-white">
{insight.metadata["degraded_ap_count"]} / {insight.metadata[ {insight.metadata["degraded_ap_count"]} / {insight.metadata[
"total_ap_count" "total_ap_count"
]} ]}
</span> </span>
</div> </div>
<span class="ml-auto rounded-md bg-indigo-200 px-2 py-1 text-xs font-medium text-indigo-900 dark:bg-indigo-800 dark:text-indigo-100"> <span class="ml-auto rounded-md bg-indigo-600 px-2 py-1 text-xs font-semibold text-white dark:bg-indigo-500">
{t("Investigate upstream first")} {t("Investigate upstream first")}
</span> </span>
</div> </div>
<%= if is_list(insight.metadata["degraded_aps"]) do %> <%= if is_list(insight.metadata["degraded_aps"]) do %>
<ul class="mt-2 space-y-1 text-xs text-indigo-900 dark:text-indigo-100"> <ul class="mt-2 space-y-1 text-xs text-indigo-900 dark:text-indigo-50">
<li <li
:for={ap <- insight.metadata["degraded_aps"]} :for={ap <- insight.metadata["degraded_aps"]}
class="flex items-center justify-between gap-2" class="flex items-center justify-between gap-2"
> >
<span class="truncate"> <span class="truncate">
<%= if ap["device_id"] do %> <%= if ap["device_id"] do %>
<.link navigate={~p"/devices/#{ap["device_id"]}"} class="underline"> <.link
navigate={~p"/devices/#{ap["device_id"]}"}
class="text-indigo-700 underline hover:text-indigo-900 dark:text-indigo-200 dark:hover:text-white"
>
{ap["name"] || "Unnamed"} {ap["name"] || "Unnamed"}
</.link> </.link>
<% else %> <% else %>
{ap["name"] || "Unnamed"} {ap["name"] || "Unnamed"}
<% end %> <% end %>
</span> </span>
<span class="font-mono text-indigo-700 dark:text-indigo-300"> <span class="font-mono text-indigo-800 dark:text-indigo-100">
QoE {Float.round(ap["qoe_score"] / 1.0, 1)} QoE {Float.round(ap["qoe_score"] / 1.0, 1)}
</span> </span>
</li> </li>
@ -566,21 +569,21 @@
<div class="mt-3 rounded-md border border-red-200 bg-red-50 p-3 dark:border-red-800 dark:bg-red-900/20"> <div class="mt-3 rounded-md border border-red-200 bg-red-50 p-3 dark:border-red-800 dark:bg-red-900/20">
<div class="flex flex-wrap items-center gap-4 text-sm"> <div class="flex flex-wrap items-center gap-4 text-sm">
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-red-700 dark:text-red-300"> <span class="text-xs text-red-700 dark:text-red-200">
{t("Hottest sensor")} {t("Hottest sensor")}
</span> </span>
<span class="font-mono text-base font-semibold text-red-900 dark:text-red-100"> <span class="font-mono text-base font-semibold text-red-900 dark:text-red-100">
{insight.metadata["temperature_c"]} °C {insight.metadata["temperature_c"]} °C
</span> </span>
<%= if insight.metadata["sensor_descr"] do %> <%= if insight.metadata["sensor_descr"] do %>
<span class="text-xs text-red-700 dark:text-red-300"> <span class="text-xs text-red-700 dark:text-red-200">
{insight.metadata["sensor_descr"]} {insight.metadata["sensor_descr"]}
</span> </span>
<% end %> <% end %>
</div> </div>
<%= if insight.metadata["qoe_score"] do %> <%= if insight.metadata["qoe_score"] do %>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-red-700 dark:text-red-300"> <span class="text-xs text-red-700 dark:text-red-200">
{t("Linked QoE")} {t("Linked QoE")}
</span> </span>
<span class="font-mono text-base font-semibold text-red-900 dark:text-red-100"> <span class="font-mono text-base font-semibold text-red-900 dark:text-red-100">
@ -589,7 +592,7 @@
</div> </div>
<% end %> <% end %>
<%= if is_number(insight.metadata["temperature_c"]) and insight.metadata["temperature_c"] >= 75 do %> <%= if is_number(insight.metadata["temperature_c"]) and insight.metadata["temperature_c"] >= 75 do %>
<span class="ml-auto rounded-md bg-red-200 px-2 py-1 text-xs font-medium text-red-900 dark:bg-red-800 dark:text-red-100"> <span class="ml-auto rounded-md bg-red-600 px-2 py-1 text-xs font-semibold text-white dark:bg-red-500">
{t("Above manufacturer spec")} {t("Above manufacturer spec")}
</span> </span>
<% end %> <% end %>
@ -601,20 +604,20 @@
<div class="mt-3 rounded-md border border-amber-200 bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-900/20"> <div class="mt-3 rounded-md border border-amber-200 bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-900/20">
<div class="flex items-center gap-3 text-sm"> <div class="flex items-center gap-3 text-sm">
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-amber-700 dark:text-amber-300"> <span class="text-xs text-amber-700 dark:text-amber-200">
{t("Channel")} {t("Channel")}
</span> </span>
<span class="font-mono text-base font-semibold text-amber-900 dark:text-amber-100"> <span class="font-mono text-base font-semibold text-amber-900 dark:text-amber-100">
{insight.metadata["channel"]} {insight.metadata["channel"]}
<%= if insight.metadata["frequency_mhz"] do %> <%= if insight.metadata["frequency_mhz"] do %>
<span class="text-xs font-normal text-amber-700 dark:text-amber-300"> <span class="text-xs font-normal text-amber-700 dark:text-amber-200">
({insight.metadata["frequency_mhz"]} MHz) ({insight.metadata["frequency_mhz"]} MHz)
</span> </span>
<% end %> <% end %>
</span> </span>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-amber-700 dark:text-amber-300"> <span class="text-xs text-amber-700 dark:text-amber-200">
{t("APs sharing")} {t("APs sharing")}
</span> </span>
<span class="font-mono text-base font-semibold text-amber-900 dark:text-amber-100"> <span class="font-mono text-base font-semibold text-amber-900 dark:text-amber-100">
@ -629,11 +632,11 @@
</div> </div>
<%= if is_list(insight.metadata["aps"]) and insight.metadata["aps"] != [] do %> <%= if is_list(insight.metadata["aps"]) and insight.metadata["aps"] != [] do %>
<div class="mt-3 overflow-x-auto"> <div class="mt-3 overflow-x-auto">
<p class="text-xs font-medium text-amber-800 dark:text-amber-300"> <p class="text-xs font-medium text-amber-800 dark:text-amber-200">
{t("Conflicting APs")} {t("Conflicting APs")}
</p> </p>
<table class="mt-1 w-full text-left text-xs text-amber-900 dark:text-amber-100"> <table class="mt-1 w-full text-left text-xs text-amber-900 dark:text-amber-100">
<thead class="text-[11px] font-medium text-amber-700 dark:text-amber-300"> <thead class="text-[11px] font-medium text-amber-700 dark:text-amber-200">
<tr> <tr>
<th class="py-1 pr-3">{t("AP")}</th> <th class="py-1 pr-3">{t("AP")}</th>
<th class="py-1 pr-3">{t("Width")}</th> <th class="py-1 pr-3">{t("Width")}</th>
@ -660,7 +663,7 @@
{ap["name"] || "Unnamed"} {ap["name"] || "Unnamed"}
<% end %> <% end %>
<%= if ap["rf_severity"] == "critical" do %> <%= if ap["rf_severity"] == "critical" do %>
<span class="ml-1 rounded bg-red-200 px-1 py-0.5 text-[10px] font-medium text-red-900 dark:bg-red-800 dark:text-red-100"> <span class="ml-1 rounded bg-red-600 px-1 py-0.5 text-[10px] font-semibold text-white dark:bg-red-500">
crit crit
</span> </span>
<% end %> <% end %>
@ -691,7 +694,7 @@
<div class="mt-3 rounded-md border border-purple-200 bg-purple-50 p-3 dark:border-purple-800 dark:bg-purple-900/20"> <div class="mt-3 rounded-md border border-purple-200 bg-purple-50 p-3 dark:border-purple-800 dark:bg-purple-900/20">
<div class="flex items-center gap-3 text-sm"> <div class="flex items-center gap-3 text-sm">
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-purple-700 dark:text-purple-300"> <span class="text-xs text-purple-700 dark:text-purple-200">
{t("Current channel")} {t("Current channel")}
</span> </span>
<span class="font-mono text-base font-semibold text-purple-900 dark:text-purple-100"> <span class="font-mono text-base font-semibold text-purple-900 dark:text-purple-100">
@ -703,7 +706,7 @@
class="h-4 w-4 text-purple-500 dark:text-purple-400" class="h-4 w-4 text-purple-500 dark:text-purple-400"
/> />
<div class="flex flex-col"> <div class="flex flex-col">
<span class="text-xs text-purple-700 dark:text-purple-300"> <span class="text-xs text-purple-700 dark:text-purple-200">
{t("Recommended")} {t("Recommended")}
</span> </span>
<span class="font-mono text-base font-semibold text-purple-900 dark:text-purple-100"> <span class="font-mono text-base font-semibold text-purple-900 dark:text-purple-100">
@ -719,7 +722,7 @@
<%= if insight.metadata["top_offenders"] && insight.metadata["top_offenders"] != [] do %> <%= if insight.metadata["top_offenders"] && insight.metadata["top_offenders"] != [] do %>
<div class="mt-3"> <div class="mt-3">
<p class="text-xs font-medium text-purple-800 dark:text-purple-300"> <p class="text-xs font-medium text-purple-800 dark:text-purple-200">
{t("Top interferers on current channel")} {t("Top interferers on current channel")}
</p> </p>
<ul class="mt-1 space-y-1"> <ul class="mt-1 space-y-1">
@ -730,17 +733,17 @@
<span class="flex items-center gap-2 truncate"> <span class="flex items-center gap-2 truncate">
<span class="font-mono">{offender["bssid"]}</span> <span class="font-mono">{offender["bssid"]}</span>
<%= if offender["ssid"] do %> <%= if offender["ssid"] do %>
<span class="truncate text-purple-700 dark:text-purple-300"> <span class="truncate text-purple-700 dark:text-purple-200">
{offender["ssid"]} {offender["ssid"]}
</span> </span>
<% end %> <% end %>
<%= if offender["is_own_fleet"] do %> <%= if offender["is_own_fleet"] do %>
<span class="rounded bg-purple-200 px-1.5 py-0.5 text-[10px] font-medium text-purple-900 dark:bg-purple-800 dark:text-purple-100"> <span class="rounded bg-purple-600 px-1.5 py-0.5 text-[10px] font-semibold text-white dark:bg-purple-500">
own fleet own fleet
</span> </span>
<% end %> <% end %>
</span> </span>
<span class="font-mono text-purple-700 dark:text-purple-300"> <span class="font-mono text-purple-700 dark:text-purple-200">
{offender["rssi_dbm"]} dBm {offender["rssi_dbm"]} dBm
</span> </span>
</li> </li>