diff --git a/lib/towerops/activity_feed.ex b/lib/towerops/activity_feed.ex index 6ffd6ca0..4e21c716 100644 --- a/lib/towerops/activity_feed.ex +++ b/lib/towerops/activity_feed.ex @@ -65,14 +65,14 @@ defmodule Towerops.ActivityFeed do """ def count_by_type(organization_id) do # Count recent items (last 24h) per type for badge display - since = DateTime.add(DateTime.utc_now(), -86400, :second) + since = DateTime.add(DateTime.utc_now(), -86_400, :second) - active_sources(nil) - |> Enum.map(fn source -> + nil + |> active_sources() + |> Map.new(fn source -> items = fetch_source(source, organization_id, since, nil, 1000) {source, length(items)} end) - |> Map.new() end defp maybe_filter_search(items, nil), do: items @@ -101,8 +101,7 @@ defmodule Towerops.ActivityFeed do String.contains?(searchable, search) end - defp active_sources(nil), - do: [:config_change, :alert_fired, :alert_resolved, :device_event, :sync, :device_added] + defp active_sources(nil), do: [:config_change, :alert_fired, :alert_resolved, :device_event, :sync, :device_added] defp active_sources(types) when is_list(types), do: types @@ -319,7 +318,8 @@ defmodule Towerops.ActivityFeed do device_name: nil, site_name: nil, summary: "Preseem sync #{status_label}", - detail: "#{row.records_synced || 0} records synced#{if row.duration_ms, do: " in #{row.duration_ms}ms", else: ""}", + detail: + "#{row.records_synced || 0} records synced#{if row.duration_ms, do: " in #{row.duration_ms}ms", else: ""}", severity: sync_severity(row.status), icon: "hero-arrow-path", link: nil @@ -401,8 +401,7 @@ defmodule Towerops.ActivityFeed do defp format_gaiia_impact(nil), do: nil - defp format_gaiia_impact(%{"total_subscribers" => subs, "total_mrr" => mrr}) - when is_integer(subs) and subs > 0 do + defp format_gaiia_impact(%{"total_subscribers" => subs, "total_mrr" => mrr}) when is_integer(subs) and subs > 0 do "Affects #{subs} subscribers, $#{mrr}/mo MRR at risk" end @@ -417,8 +416,8 @@ defmodule Towerops.ActivityFeed do cond do diff < 60 -> "#{diff}s" diff < 3600 -> "#{div(diff, 60)}m" - diff < 86400 -> "#{div(diff, 3600)}h #{div(rem(diff, 3600), 60)}m" - true -> "#{div(diff, 86400)}d #{div(rem(diff, 86400), 3600)}h" + diff < 86_400 -> "#{div(diff, 3600)}h #{div(rem(diff, 3600), 60)}m" + true -> "#{div(diff, 86_400)}d #{div(rem(diff, 86_400), 3600)}h" end end diff --git a/lib/towerops/config_changes.ex b/lib/towerops/config_changes.ex index 94250d4b..9d412666 100644 --- a/lib/towerops/config_changes.ex +++ b/lib/towerops/config_changes.ex @@ -122,7 +122,8 @@ defmodule Towerops.ConfigChanges do preloads = Keyword.get(opts, :preload, [:device]) device_ids = - Towerops.Devices.list_site_devices(site_id) + site_id + |> Towerops.Devices.list_site_devices() |> Enum.map(& &1.id) if device_ids == [] do diff --git a/lib/towerops/config_changes/config_change_event.ex b/lib/towerops/config_changes/config_change_event.ex index fb6d47fa..d2fbbf66 100644 --- a/lib/towerops/config_changes/config_change_event.ex +++ b/lib/towerops/config_changes/config_change_event.ex @@ -6,6 +6,8 @@ defmodule Towerops.ConfigChanges.ConfigChangeEvent do import Ecto.Changeset + alias Towerops.Devices.MikrotikBackup + @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id @@ -17,8 +19,8 @@ defmodule Towerops.ConfigChanges.ConfigChangeEvent do belongs_to :device, Towerops.Devices.Device belongs_to :organization, Towerops.Organizations.Organization - belongs_to :backup_before, Towerops.Devices.MikrotikBackup - belongs_to :backup_after, Towerops.Devices.MikrotikBackup + belongs_to :backup_before, MikrotikBackup + belongs_to :backup_after, MikrotikBackup timestamps(type: :utc_datetime, updated_at: false) end diff --git a/lib/towerops/dashboard.ex b/lib/towerops/dashboard.ex index c4fb1c75..d910a2bf 100644 --- a/lib/towerops/dashboard.ex +++ b/lib/towerops/dashboard.ex @@ -167,8 +167,6 @@ defmodule Towerops.Dashboard do duration_seconds = if device.last_status_change_at do DateTime.diff(now, device.last_status_change_at, :second) - else - nil end %{ @@ -203,18 +201,7 @@ defmodule Towerops.Dashboard do down_devices = Devices.list_organization_devices(organization_id, %{"status" => "down", "site_id" => site.id}) {subscribers_affected, mrr_at_risk} = - if down_count > 0 do - impacts = - Enum.map(down_devices, fn device -> - ImpactAnalysis.analyze_device_impact(organization_id, device.id) - end) - - subs = impacts |> Enum.map(& &1.total_subscribers) |> Enum.sum() - mrr = impacts |> Enum.map(& &1.total_mrr) |> Enum.reduce(Decimal.new("0"), &Decimal.add/2) - {subs, mrr} - else - {0, Decimal.new("0")} - end + calculate_down_impact(organization_id, down_devices, down_count) # Get average QoE from Preseem for devices at this site site_devices = Devices.list_site_devices(site.id) @@ -269,4 +256,17 @@ defmodule Towerops.Dashboard do mrr: if(gaiia_summary, do: gaiia_summary.total_mrr) } end + + defp calculate_down_impact(_organization_id, _down_devices, 0), do: {0, Decimal.new("0")} + + defp calculate_down_impact(organization_id, down_devices, _down_count) do + impacts = + Enum.map(down_devices, fn device -> + ImpactAnalysis.analyze_device_impact(organization_id, device.id) + end) + + subs = impacts |> Enum.map(& &1.total_subscribers) |> Enum.sum() + mrr = impacts |> Enum.map(& &1.total_mrr) |> Enum.reduce(Decimal.new("0"), &Decimal.add/2) + {subs, mrr} + end end diff --git a/lib/towerops/preseem.ex b/lib/towerops/preseem.ex index be687c75..efe0c8d7 100644 --- a/lib/towerops/preseem.ex +++ b/lib/towerops/preseem.ex @@ -110,14 +110,14 @@ defmodule Towerops.Preseem do capacity_scores = aps |> Enum.map(& &1.capacity_score) |> Enum.reject(&is_nil/1) avg_qoe = - if qoe_scores != [], - do: Float.round(Enum.sum(qoe_scores) / length(qoe_scores), 1), - else: nil + if qoe_scores == [], + do: nil, + else: Float.round(Enum.sum(qoe_scores) / length(qoe_scores), 1) avg_capacity = - if capacity_scores != [], - do: Float.round(Enum.sum(capacity_scores) / length(capacity_scores), 1), - else: nil + if capacity_scores == [], + do: nil, + else: Float.round(Enum.sum(capacity_scores) / length(capacity_scores), 1) total_subscribers = aps |> Enum.map(& &1.subscriber_count) |> Enum.reject(&is_nil/1) |> Enum.sum() diff --git a/lib/towerops/search.ex b/lib/towerops/search.ex index 56a4ffeb..ae0529df 100644 --- a/lib/towerops/search.ex +++ b/lib/towerops/search.ex @@ -4,11 +4,12 @@ defmodule Towerops.Search do """ import Ecto.Query - alias Towerops.Repo - alias Towerops.Devices.Device - alias Towerops.Sites.Site - alias Towerops.Gaiia.Account + alias Towerops.Alerts.Alert + alias Towerops.Devices.Device + alias Towerops.Gaiia.Account + alias Towerops.Repo + alias Towerops.Sites.Site @limit 5 @@ -76,7 +77,7 @@ defmodule Towerops.Search do |> where([a, d], d.organization_id == ^organization_id) |> where([a, _d], ilike(a.message, ^pattern)) |> where([a, _d], is_nil(a.resolved_at)) - |> order_by([a, _d], [desc: a.triggered_at]) + |> order_by([a, _d], desc: a.triggered_at) |> limit(@limit) |> select([a, d], %{id: a.id, message: a.message, device_name: d.name}) |> Repo.all() diff --git a/lib/towerops/trace.ex b/lib/towerops/trace.ex index d427a176..97c0a8c9 100644 --- a/lib/towerops/trace.ex +++ b/lib/towerops/trace.ex @@ -41,8 +41,7 @@ defmodule Towerops.Trace do devices = search_devices(organization_id, pattern) access_points = search_access_points(organization_id, pattern) - (accounts ++ inventory ++ devices ++ access_points) - |> Enum.take(25) + Enum.take(accounts ++ inventory ++ devices ++ access_points, 25) end end @@ -63,7 +62,11 @@ defmodule Towerops.Trace do defp search_inventory(organization_id, pattern) do InventoryItem |> where(organization_id: ^organization_id) - |> where([i], ilike(i.ip_address, ^pattern) or ilike(i.serial_number, ^pattern) or ilike(i.name, ^pattern) or ilike(i.mac_address, ^pattern)) + |> where( + [i], + ilike(i.ip_address, ^pattern) or ilike(i.serial_number, ^pattern) or + ilike(i.name, ^pattern) or ilike(i.mac_address, ^pattern) + ) |> limit(10) |> preload(:device) |> Repo.all() @@ -97,7 +100,8 @@ defmodule Towerops.Trace do |> preload(:device) |> Repo.all() |> Enum.map(fn ap -> - sublabel = Enum.join(Enum.reject([ap.ip_address, ap.model, "#{ap.subscriber_count || 0} subscribers"], &is_nil/1), " · ") + sublabel = + Enum.join(Enum.reject([ap.ip_address, ap.model, "#{ap.subscriber_count || 0} subscribers"], &is_nil/1), " · ") %{type: :access_point, id: ap.id, label: ap.name, sublabel: sublabel, data: ap} end) @@ -123,7 +127,7 @@ defmodule Towerops.Trace do defp trace_from_account(organization_id, account_id) do account = Repo.get_by(Account, id: account_id, organization_id: organization_id) - unless account, do: throw(:not_found) + if !account, do: throw(:not_found) subscriptions = list_account_subscriptions(organization_id, account.gaiia_id) inventory_items = list_account_inventory(organization_id, account.gaiia_id) @@ -143,7 +147,7 @@ defmodule Towerops.Trace do |> preload(:device) |> Repo.one() - unless item, do: throw(:not_found) + if !item, do: throw(:not_found) # Find account if linked account = @@ -154,7 +158,7 @@ defmodule Towerops.Trace do subscriptions = if account, do: list_account_subscriptions(organization_id, account.gaiia_id), else: [] inventory_items = if account, do: list_account_inventory(organization_id, account.gaiia_id), else: [item] - device = if item.device_id, do: Repo.get(Device, item.device_id) |> Repo.preload(:site) + device = if item.device_id, do: Device |> Repo.get(item.device_id) |> Repo.preload(:site) access_point = if device, do: Repo.get_by(AccessPoint, device_id: device.id) build_trace(account, subscriptions, inventory_items, device, access_point, organization_id) @@ -169,7 +173,7 @@ defmodule Towerops.Trace do |> preload(:site) |> Repo.one() - unless device, do: throw(:not_found) + if !device, do: throw(:not_found) access_point = Repo.get_by(AccessPoint, device_id: device.id) @@ -183,7 +187,9 @@ defmodule Towerops.Trace do case inventory_items do [first | _] when not is_nil(first.assigned_account_gaiia_id) -> Repo.get_by(Account, organization_id: organization_id, gaiia_id: first.assigned_account_gaiia_id) - _ -> nil + + _ -> + nil end subscriptions = if account, do: list_account_subscriptions(organization_id, account.gaiia_id), else: [] @@ -200,9 +206,9 @@ defmodule Towerops.Trace do |> preload(:device) |> Repo.one() - unless access_point, do: throw(:not_found) + if !access_point, do: throw(:not_found) - device = if access_point.device_id, do: access_point.device |> Repo.preload(:site) + device = if access_point.device_id, do: Repo.preload(access_point.device, :site) build_trace(nil, [], [], device, access_point, organization_id) catch @@ -321,7 +327,7 @@ defmodule Towerops.Trace do defp load_recent_config_changes(nil), do: [] defp load_recent_config_changes(device_id) do - since = DateTime.utc_now() |> DateTime.add(-24 * 3600, :second) + since = DateTime.add(DateTime.utc_now(), -24 * 3600, :second) ConfigChangeEvent |> where(device_id: ^device_id) @@ -351,7 +357,7 @@ defmodule Towerops.Trace do item_with_device = Enum.find(inventory_items, & &1.device_id) if item_with_device do - device = Repo.get(Device, item_with_device.device_id) |> Repo.preload(:site) + device = Device |> Repo.get(item_with_device.device_id) |> Repo.preload(:site) access_point = Repo.get_by(AccessPoint, device_id: device.id) {device, access_point} else @@ -377,15 +383,16 @@ defmodule Towerops.Trace do defp format_address(addr) when is_map(addr) do parts = - [ - addr["street1"] || addr["line1"], - addr["street2"] || addr["line2"], - addr["city"], - addr["state"] || addr["province"], - addr["zip"] || addr["postal_code"] - ] - |> Enum.reject(&is_nil/1) - |> Enum.reject(&(String.trim(&1) == "")) + Enum.reject( + [ + addr["street1"] || addr["line1"], + addr["street2"] || addr["line2"], + addr["city"], + addr["state"] || addr["province"], + addr["zip"] || addr["postal_code"] + ], + &(is_nil(&1) or String.trim(&1) == "") + ) if parts == [], do: nil, else: Enum.join(parts, ", ") end diff --git a/lib/towerops_web.ex b/lib/towerops_web.ex index b5bdbddd..245232a5 100644 --- a/lib/towerops_web.ex +++ b/lib/towerops_web.ex @@ -83,9 +83,9 @@ defmodule ToweropsWeb do use Gettext, backend: ToweropsWeb.Gettext import Phoenix.HTML - import ToweropsWeb.CoreComponents import ToweropsWeb.Components.Breadcrumbs import ToweropsWeb.Components.Skeletons + import ToweropsWeb.CoreComponents import ToweropsWeb.GettextHelpers # HTML escaping functionality diff --git a/lib/towerops_web/changelog_parser.ex b/lib/towerops_web/changelog_parser.ex index 14361a51..de3d0cb7 100644 --- a/lib/towerops_web/changelog_parser.ex +++ b/lib/towerops_web/changelog_parser.ex @@ -30,14 +30,7 @@ defmodule ToweropsWeb.ChangelogParser do # Bullet point String.starts_with?(String.trim(line), "* ") -> - case acc do - [current | rest] -> - item = String.trim(line) |> String.trim_leading("* ") - [%{current | items: current.items ++ [item]} | rest] - - [] -> - acc - end + add_item_to_current(acc, line |> String.trim() |> String.trim_leading("* ")) true -> acc @@ -45,4 +38,8 @@ defmodule ToweropsWeb.ChangelogParser do end) |> Enum.reverse() end + + defp add_item_to_current([current | rest], item), do: [%{current | items: current.items ++ [item]} | rest] + + defp add_item_to_current([], _item), do: [] end diff --git a/lib/towerops_web/channels/agent_channel.ex b/lib/towerops_web/channels/agent_channel.ex index 2ef6eaef..6ef8d994 100644 --- a/lib/towerops_web/channels/agent_channel.ex +++ b/lib/towerops_web/channels/agent_channel.ex @@ -31,6 +31,7 @@ defmodule ToweropsWeb.AgentChannel do alias Towerops.Agent.Validator alias Towerops.Agents alias Towerops.Alerts + alias Towerops.ConfigChanges alias Towerops.Devices alias Towerops.Devices.BackupRequests alias Towerops.Devices.MikrotikBackups @@ -1821,29 +1822,31 @@ defmodule ToweropsWeb.AgentChannel do case backups do [_new, previous | _] -> - if previous.config_hash_normalized != new_backup.config_hash_normalized do - case Towerops.ConfigChanges.record_change_event(device_id, previous, new_backup) do - {:ok, %Towerops.ConfigChanges.ConfigChangeEvent{} = event} -> - Logger.info("Config change event recorded", device_id: device_id, event_id: event.id) - - # Async correlate with performance metrics - Task.start(fn -> - Towerops.ConfigChanges.Correlator.correlate(event) - end) - - {:ok, :no_changes} -> - :ok - - {:error, reason} -> - Logger.warning("Failed to record config change event: #{inspect(reason)}", device_id: device_id) - end - end + maybe_record_change(device_id, previous, new_backup) _ -> :ok end end + defp maybe_record_change(_device_id, %{config_hash_normalized: hash}, %{config_hash_normalized: hash}), do: :ok + + defp maybe_record_change(device_id, previous, new_backup) do + case ConfigChanges.record_change_event(device_id, previous, new_backup) do + {:ok, %{id: event_id} = event} -> + Logger.info("Config change event recorded", device_id: device_id, event_id: event_id) + Task.start(fn -> ConfigChanges.Correlator.correlate(event) end) + + {:ok, :no_changes} -> + :ok + + {:error, reason} -> + Logger.warning("Failed to record config change event: #{inspect(reason)}", + device_id: device_id + ) + end + end + defp backup_needed?(device_id, config_text) do {:ok, needs_backup?} = MikrotikBackups.needs_backup?(device_id, config_text) needs_backup? diff --git a/lib/towerops_web/components/layouts/root.html.heex b/lib/towerops_web/components/layouts/root.html.heex index eba68bf4..fb40a60f 100644 --- a/lib/towerops_web/components/layouts/root.html.heex +++ b/lib/towerops_web/components/layouts/root.html.heex @@ -4,16 +4,25 @@ - + - - + + - - - - + + + + diff --git a/lib/towerops_web/components/skeletons.ex b/lib/towerops_web/components/skeletons.ex index 7bf03a11..f0525ea2 100644 --- a/lib/towerops_web/components/skeletons.ex +++ b/lib/towerops_web/components/skeletons.ex @@ -11,7 +11,10 @@ defmodule ToweropsWeb.Components.Skeletons do def skeleton_card(assigns) do ~H""" -
+
@@ -33,7 +36,10 @@ defmodule ToweropsWeb.Components.Skeletons do
-
+
@@ -49,7 +55,10 @@ defmodule ToweropsWeb.Components.Skeletons do def skeleton_stat(assigns) do ~H""" -
+
@@ -72,7 +81,10 @@ defmodule ToweropsWeb.Components.Skeletons do
-
+
@@ -82,7 +94,10 @@ defmodule ToweropsWeb.Components.Skeletons do
-
+
diff --git a/lib/towerops_web/controllers/page_html/home.html.heex b/lib/towerops_web/controllers/page_html/home.html.heex index 45030459..495b67a4 100644 --- a/lib/towerops_web/controllers/page_html/home.html.heex +++ b/lib/towerops_web/controllers/page_html/home.html.heex @@ -42,8 +42,8 @@
- - + +
@@ -81,8 +81,8 @@
- - + +
- - + +
- Gaiia - Sonar - Preseem - UISP — coming soon - Splynx — coming soon + + Gaiia + + + Sonar + + + Preseem + + + UISP — coming soon + + + Splynx — coming soon +
@@ -260,8 +270,8 @@
- - + +
@@ -300,8 +310,8 @@
- - + +
@@ -447,12 +457,15 @@

All plans include team access, email alerts, and API access. - Need a custom plan for 500+ devices? Talk to us. + Need a custom plan for 500+ devices? Talk to us.

- - + +
@@ -491,8 +504,8 @@
- - + +
diff --git a/lib/towerops_web/live/activity_feed_live.ex b/lib/towerops_web/live/activity_feed_live.ex index fee0cb7d..1cbd5cce 100644 --- a/lib/towerops_web/live/activity_feed_live.ex +++ b/lib/towerops_web/live/activity_feed_live.ex @@ -233,9 +233,9 @@ defmodule ToweropsWeb.ActivityFeedLive do diff < 5 -> "just now" diff < 60 -> "#{diff}s ago" diff < 3600 -> "#{div(diff, 60)}m ago" - diff < 86400 -> "#{div(diff, 3600)}h ago" + diff < 86_400 -> "#{div(diff, 3600)}h ago" diff < 172_800 -> "yesterday" - diff < 604_800 -> "#{div(diff, 86400)}d ago" + diff < 604_800 -> "#{div(diff, 86_400)}d ago" true -> Calendar.strftime(timestamp, "%b %d, %Y") end end diff --git a/lib/towerops_web/live/activity_feed_live.html.heex b/lib/towerops_web/live/activity_feed_live.html.heex index f80f68ec..f5396b65 100644 --- a/lib/towerops_web/live/activity_feed_live.html.heex +++ b/lib/towerops_web/live/activity_feed_live.html.heex @@ -6,19 +6,25 @@ <.header>
- + + Activity Feed
- <:subtitle>Real-time NOC operations log — config changes, alerts, events, and syncs + <:subtitle> + Real-time NOC operations log — config changes, alerts, events, and syncs + <%!-- Search Bar --%>
- <.icon name="hero-magnifying-glass" class="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 dark:text-gray-500" /> + <.icon + name="hero-magnifying-glass" + class="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 dark:text-gray-500" + /> @@ -82,7 +89,7 @@

<%= if @search != "" do %> - No results for "<%= @search %>" + No results for "{@search}" <% else %> No activity yet <% end %> @@ -100,8 +107,7 @@ phx-click="clear_search" class="btn btn-sm btn-outline gap-2" > - <.icon name="hero-x-mark" class="h-4 w-4" /> - Clear search + <.icon name="hero-x-mark" class="h-4 w-4" /> Clear search

@@ -110,11 +116,14 @@
Showing {length(@items)} events - matching "{@search}" + + matching "{@search}" + - + + Live @@ -127,7 +136,9 @@
{period} - ({length(period_items)}) + + ({length(period_items)}) +
@@ -156,8 +167,17 @@ <%!-- Content --%>
- <.icon name={item.icon} class={["h-4 w-4 flex-shrink-0", severity_icon_color(item.severity, item.type)]} /> - + <.icon + name={item.icon} + class={[ + "h-4 w-4 flex-shrink-0", + severity_icon_color(item.severity, item.type) + ]} + /> + {relative_time(item.timestamp, @now)}
-

+

<%= if item.link do %> <.link navigate={item.link} class="hover:underline"> {item.summary} @@ -194,19 +217,31 @@ <% end %>

-

+

{item.detail}

-
+
<.icon name="hero-map-pin" class="h-3 w-3" /> {item.site_name}
<%!-- Link arrow --%> -
- <.link navigate={item.link} class="text-gray-400 hover:text-blue-500 dark:text-gray-600 dark:hover:text-blue-400"> +
+ <.link + navigate={item.link} + class="text-gray-400 hover:text-blue-500 dark:text-gray-600 dark:hover:text-blue-400" + > <.icon name="hero-arrow-right" class="h-4 w-4" />
@@ -223,8 +258,7 @@ phx-click="load_more" class="btn btn-outline btn-sm gap-2" > - <.icon name="hero-arrow-down" class="h-4 w-4" /> - Load more events + <.icon name="hero-arrow-down" class="h-4 w-4" /> Load more events
<% end %> diff --git a/lib/towerops_web/live/alert_live/index.ex b/lib/towerops_web/live/alert_live/index.ex index 6ac737cb..b5b7660d 100644 --- a/lib/towerops_web/live/alert_live/index.ex +++ b/lib/towerops_web/live/alert_live/index.ex @@ -145,11 +145,9 @@ defmodule ToweropsWeb.AlertLive.Index do defp filter_alerts(alerts, "critical"), do: Enum.filter(alerts, &(&1.alert_type == :device_down and is_nil(&1.resolved_at))) - defp filter_alerts(alerts, "unresolved"), - do: Enum.filter(alerts, &is_nil(&1.resolved_at)) + defp filter_alerts(alerts, "unresolved"), do: Enum.filter(alerts, &is_nil(&1.resolved_at)) - defp filter_alerts(alerts, "resolved"), - do: Enum.filter(alerts, &(not is_nil(&1.resolved_at))) + defp filter_alerts(alerts, "resolved"), do: Enum.filter(alerts, &(not is_nil(&1.resolved_at))) defp filter_alerts(alerts, _), do: alerts @@ -188,7 +186,7 @@ defmodule ToweropsWeb.AlertLive.Index do end end) |> Enum.map(fn {{site_id, site_name}, site_alerts} -> - sub_info = if site_id, do: site_subscribers[site_id], else: nil + sub_info = if site_id, do: site_subscribers[site_id] critical_count = Enum.count(site_alerts, &(&1.alert_type == :device_down and is_nil(&1.resolved_at))) %{ diff --git a/lib/towerops_web/live/alert_live/index.html.heex b/lib/towerops_web/live/alert_live/index.html.heex index 2765f1c0..a27f8435 100644 --- a/lib/towerops_web/live/alert_live/index.html.heex +++ b/lib/towerops_web/live/alert_live/index.html.heex @@ -5,8 +5,7 @@ > <.header> - Alerts - Experimental + Alerts Experimental <:subtitle>Triage alerts by site impact — fix the biggest problems first @@ -17,29 +16,25 @@ patch={~p"/alerts?filter=unresolved&sort=#{@sort_by}"} class={["tab", @filter == "unresolved" && "tab-active"]} > - Unresolved - {@counts.unresolved} + Unresolved {@counts.unresolved} <.link patch={~p"/alerts?filter=critical&sort=#{@sort_by}"} class={["tab", @filter == "critical" && "tab-active"]} > - Critical - {@counts.critical} + Critical {@counts.critical} <.link patch={~p"/alerts?filter=all&sort=#{@sort_by}"} class={["tab", @filter == "all" && "tab-active"]} > - All - {@counts.all} + All {@counts.all} <.link patch={~p"/alerts?filter=resolved&sort=#{@sort_by}"} class={["tab", @filter == "resolved" && "tab-active"]} > - Resolved - {@counts.resolved} + Resolved {@counts.resolved}
@@ -48,19 +43,19 @@ Sort: <.link patch={~p"/alerts?filter=#{@filter}&sort=severity"} - class={["btn btn-xs", @sort_by == "severity" && "btn-primary" || "btn-ghost"]} + class={["btn btn-xs", (@sort_by == "severity" && "btn-primary") || "btn-ghost"]} > Severity <.link patch={~p"/alerts?filter=#{@filter}&sort=age"} - class={["btn btn-xs", @sort_by == "age" && "btn-primary" || "btn-ghost"]} + class={["btn btn-xs", (@sort_by == "age" && "btn-primary") || "btn-ghost"]} > Oldest First <.link patch={~p"/alerts?filter=#{@filter}&sort=impact"} - class={["btn btn-xs", @sort_by == "impact" && "btn-primary" || "btn-ghost"]} + class={["btn btn-xs", (@sort_by == "impact" && "btn-primary") || "btn-ghost"]} > Subscriber Impact @@ -71,7 +66,10 @@
- <.icon name="hero-check-circle-solid" class="h-12 w-12 text-green-500 dark:text-green-400" /> + <.icon + name="hero-check-circle-solid" + class="h-12 w-12 text-green-500 dark:text-green-400" + />

All Clear!

@@ -95,12 +93,15 @@ <%= for group <- @grouped_alerts do %>

0 && "border-red-300 dark:border-red-800 bg-red-50/50 dark:bg-red-950/30", + group.critical_count > 0 && + "border-red-300 dark:border-red-800 bg-red-50/50 dark:bg-red-950/30", group.critical_count == 0 && "border-base-300 bg-base-100" ]}> 0} + checked={ + MapSet.member?(@expanded_sites, group.site_id || "none") || group.critical_count > 0 + } phx-click="toggle_site" phx-value-site-id={group.site_id || "none"} /> @@ -110,7 +111,11 @@
<.icon - name={if group.critical_count > 0, do: "hero-exclamation-triangle", else: "hero-signal"} + name={ + if group.critical_count > 0, + do: "hero-exclamation-triangle", + else: "hero-signal" + } class={[ "h-5 w-5", group.critical_count > 0 && "text-red-500", @@ -171,18 +176,24 @@ <% color = severity_color(alert) %>
<%!-- Alert type badge --%> @@ -234,7 +245,9 @@ > {alert.device.name} - {alert.device.ip_address} + + {alert.device.ip_address} +
<%= if alert.message do %>

{alert.message}

@@ -243,11 +256,17 @@ <%!-- Timestamps --%>
- Triggered: {ToweropsWeb.TimeHelpers.format_iso8601(alert.triggered_at, @timezone)} + Triggered: {ToweropsWeb.TimeHelpers.format_iso8601( + alert.triggered_at, + @timezone + )} <%= if alert.acknowledged_at do %> - Ack'd: {ToweropsWeb.TimeHelpers.format_iso8601(alert.acknowledged_at, @timezone)} + Ack'd: {ToweropsWeb.TimeHelpers.format_iso8601( + alert.acknowledged_at, + @timezone + )} <%= if alert.acknowledged_by do %> by {alert.acknowledged_by.email} <% end %> @@ -255,7 +274,10 @@ <% end %> <%= if alert.resolved_at do %> - Resolved: {ToweropsWeb.TimeHelpers.format_iso8601(alert.resolved_at, @timezone)} + Resolved: {ToweropsWeb.TimeHelpers.format_iso8601( + alert.resolved_at, + @timezone + )} <% end %>
diff --git a/lib/towerops_web/live/changelog_live.ex b/lib/towerops_web/live/changelog_live.ex index 417eebf4..f737bab6 100644 --- a/lib/towerops_web/live/changelog_live.ex +++ b/lib/towerops_web/live/changelog_live.ex @@ -1,4 +1,5 @@ defmodule ToweropsWeb.ChangelogLive do + @moduledoc false use ToweropsWeb, :live_view @impl true @@ -23,7 +24,10 @@ defmodule ToweropsWeb.ChangelogLive do

{entry.title}

-
    +
    • {item}
diff --git a/lib/towerops_web/live/components/global_search_component.ex b/lib/towerops_web/live/components/global_search_component.ex index 55bcccb3..c414eac1 100644 --- a/lib/towerops_web/live/components/global_search_component.ex +++ b/lib/towerops_web/live/components/global_search_component.ex @@ -55,6 +55,7 @@ defmodule ToweropsWeb.Live.Components.GlobalSearchComponent do @impl true def handle_event("keydown", %{"key" => "Enter"}, socket) do flat = flat_results(socket.assigns.results) + case Enum.at(flat, socket.assigns.selected_index) do nil -> {:noreply, socket} result -> {:noreply, push_navigate(socket, to: result.url)} @@ -75,7 +76,12 @@ defmodule ToweropsWeb.Live.Components.GlobalSearchComponent do