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:
parent
e18d877695
commit
c9202744a5
3 changed files with 62 additions and 42 deletions
|
|
@ -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"))]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue