diff --git a/.sobelow-skips b/.sobelow-skips index 97678f3..24cd4fb 100644 --- a/.sobelow-skips +++ b/.sobelow-skips @@ -25,4 +25,8 @@ SQL.Query: SQL injection,lib/aprsme/packets.ex:664,17532E XSS.Raw: XSS,lib/aprsme_web/components/core_components.ex:48,1E00DD8 SQL.Query: SQL injection,lib/aprsme_web/live/info_live/show.ex:603,52B452D XSS.Raw: XSS,lib/aprsme_web/live/info_live/show.ex:661,5C70BB5 -XSS.Raw: XSS,lib/aprsme_web/live/info_live/show.ex:643,692A7EC \ No newline at end of file +XSS.Raw: XSS,lib/aprsme_web/live/info_live/show.ex:643,692A7EC +XSS.Raw: XSS,lib/aprsme_web/live/info_live/show.ex:604,12E0C65 +SQL.Query: SQL injection,lib/aprsme_web/live/info_live/show.ex:448,1662A49 +SQL.Query: SQL injection,lib/aprsme_web/live/info_live/show.ex:546,37D03BD +XSS.Raw: XSS,lib/aprsme_web/live/info_live/show.ex:586,509A015 \ No newline at end of file diff --git a/lib/aprsme/packets.ex b/lib/aprsme/packets.ex index 0f16e74..6a53170 100644 --- a/lib/aprsme/packets.ex +++ b/lib/aprsme/packets.ex @@ -859,6 +859,54 @@ defmodule Aprsme.Packets do PreparedQueries.has_weather_packets?(callsign) end + @doc """ + Gets other SSIDs for a given callsign's base callsign. + Returns a list of maps with callsign, ssid, received_at, and packet info. + Only returns SSIDs active within the last hour. + """ + @spec get_other_ssids(String.t()) :: list(map()) + def get_other_ssids(callsign) when is_binary(callsign) do + base_callsign = Aprsme.Callsign.extract_base(callsign) + one_hour_ago = DateTime.add(DateTime.utc_now(), -3600, :second) + + query = + from p in Packet, + where: p.base_callsign == ^base_callsign, + where: p.received_at >= ^one_hour_ago, + where: p.sender != ^callsign, + distinct: p.sender, + order_by: [desc: p.received_at], + limit: 10, + select: %{ + sender: p.sender, + ssid: p.ssid, + received_at: p.received_at, + id: p.id, + symbol_table_id: p.symbol_table_id, + symbol_code: p.symbol_code + } + + query + |> Repo.all() + |> Enum.map(fn row -> + packet = %Packet{ + id: row.id, + sender: row.sender, + ssid: row.ssid, + received_at: row.received_at, + symbol_table_id: row.symbol_table_id, + symbol_code: row.symbol_code + } + + %{ + callsign: row.sender, + ssid: row.ssid, + received_at: row.received_at, + packet: packet + } + end) + end + defp get_latest_weather_in_window(callsign, hours) do import Ecto.Query diff --git a/lib/aprsme_web/live/info_live/show.ex b/lib/aprsme_web/live/info_live/show.ex index 6319c76..47297a4 100644 --- a/lib/aprsme_web/live/info_live/show.ex +++ b/lib/aprsme_web/live/info_live/show.ex @@ -12,7 +12,6 @@ defmodule AprsmeWeb.InfoLive.Show do alias AprsmeWeb.AprsSymbol alias AprsmeWeb.Live.SharedPacketHandler alias AprsmeWeb.MapLive.PacketUtils - alias AprsmeWeb.TimeUtils @neighbor_limit 10 @@ -74,7 +73,7 @@ defmodule AprsmeWeb.InfoLive.Show do locale = Map.get(socket.assigns, :locale, "en") neighbors = get_neighbors(packet, normalized_callsign, locale) has_weather_packets = PacketUtils.has_weather_packets?(normalized_callsign) - other_ssids = get_other_ssids(normalized_callsign) + other_ssids = Packets.get_other_ssids(normalized_callsign) heard_by_stations = get_heard_by_stations(normalized_callsign, locale) stations_heard_by = get_stations_heard_by(normalized_callsign, locale) @@ -361,62 +360,6 @@ defmodule AprsmeWeb.InfoLive.Show do if bearing < 0, do: bearing + 360, else: bearing end - defp get_other_ssids(callsign) do - alias Aprsme.Packet - alias Aprsme.Repo - # Extract base callsign from the full callsign (remove SSID if present) - base_callsign = extract_base_callsign(callsign) - - # Get recent packets for the base callsign to find other SSIDs - one_hour_ago = TimeUtils.one_hour_ago() - - # Use a window function to get the most recent packet per sender - # This is much more efficient than fetching 100 packets and filtering in Elixir - query = """ - WITH recent_ssids AS ( - SELECT DISTINCT ON (sender) - sender, ssid, received_at, id, symbol_table_id, symbol_code - FROM packets - WHERE base_callsign = $1 - AND received_at >= $2 - AND sender != $3 - ORDER BY sender, received_at DESC - ) - SELECT * FROM recent_ssids - ORDER BY received_at DESC - LIMIT 10 - """ - - case Repo.query(query, [base_callsign, one_hour_ago, callsign]) do - {:ok, result} -> - Enum.map(result.rows, fn [sender, ssid, received_at, id, symbol_table_id, symbol_code] -> - # Create a minimal packet struct for display - packet = %Packet{ - id: id, - sender: sender, - ssid: ssid, - received_at: received_at, - symbol_table_id: symbol_table_id, - symbol_code: symbol_code - } - - %{ - callsign: sender, - ssid: ssid, - last_heard: format_timestamp_for_display(packet), - packet: packet - } - end) - - {:error, _} -> - [] - end - end - - defp extract_base_callsign(callsign) do - Callsign.extract_base(callsign) - end - defp get_heard_by_stations(callsign, locale) do alias Aprsme.Repo diff --git a/lib/aprsme_web/live/info_live/show.html.heex b/lib/aprsme_web/live/info_live/show.html.heex index 8abd466..89910ea 100644 --- a/lib/aprsme_web/live/info_live/show.html.heex +++ b/lib/aprsme_web/live/info_live/show.html.heex @@ -368,14 +368,14 @@ <% end %> - <%= if ssid_info.last_heard && ssid_info.last_heard.timestamp do %> + <%= if ssid_info.received_at do %> - {ssid_info.last_heard.time_ago} + {AprsmeWeb.TimeHelpers.time_ago_in_words(ssid_info.received_at)} <% else %> {gettext("Unknown")} diff --git a/lib/aprsme_web/live/map_live/index.ex b/lib/aprsme_web/live/map_live/index.ex index 10ff42b..4ccb978 100644 --- a/lib/aprsme_web/live/map_live/index.ex +++ b/lib/aprsme_web/live/map_live/index.ex @@ -176,20 +176,22 @@ defmodule AprsmeWeb.MapLive.Index do historical_hours = Map.get(socket.assigns, :historical_hours, "1") packet_age_threshold = Map.get(socket.assigns, :packet_age_threshold, one_hour_ago) - # If tracking a specific callsign, fetch their latest packet - tracked_callsign_latest_packet = + # If tracking a specific callsign, fetch their latest packet and other SSIDs + {tracked_callsign_latest_packet, other_ssids} = if tracked_callsign == "" do - nil + {nil, []} else try do - Packets.get_latest_packet_for_callsign(tracked_callsign) + packet = Packets.get_latest_packet_for_callsign(tracked_callsign) + ssids = Packets.get_other_ssids(tracked_callsign) + {packet, ssids} rescue # Handle database connection errors gracefully (especially in tests) DBConnection.OwnershipError -> - nil + {nil, []} _ -> - nil + {nil, []} end end @@ -206,6 +208,7 @@ defmodule AprsmeWeb.MapLive.Index do overlay_callsign: "", tracked_callsign: tracked_callsign, tracked_callsign_latest_packet: tracked_callsign_latest_packet, + other_ssids: other_ssids, trail_duration: trail_duration, historical_hours: historical_hours, packet_age_threshold: packet_age_threshold, @@ -461,13 +464,40 @@ defmodule AprsmeWeb.MapLive.Index do if normalized_callsign == "" do # Clear tracking socket - |> assign(tracked_callsign: "") + |> assign(tracked_callsign: "", other_ssids: []) |> update_url_with_current_state() else - # Set tracking and navigate to callsign URL - socket - |> assign(tracked_callsign: normalized_callsign) - |> push_patch(to: "/#{normalized_callsign}") + # Set tracking, fetch latest packet, zoom to location, and show marker + other_ssids = Packets.get_other_ssids(normalized_callsign) + latest_packet = Packets.get_latest_packet_for_callsign(normalized_callsign) + + socket = + assign(socket, + tracked_callsign: normalized_callsign, + other_ssids: other_ssids, + tracked_callsign_latest_packet: latest_packet + ) + + # Zoom to the callsign's location and display its marker + socket = + if latest_packet && latest_packet.lat && latest_packet.lon do + lat = Aprsme.EncodingUtils.to_float(latest_packet.lat) || 0.0 + lon = Aprsme.EncodingUtils.to_float(latest_packet.lon) || 0.0 + + packet_data = DataBuilder.build_packet_data(latest_packet) + + socket + |> push_event("zoom_to_location", %{lat: lat, lng: lon, zoom: 12}) + |> push_event("add_historical_packets_batch", %{ + packets: [packet_data], + batch: 0, + is_final: false + }) + else + socket + end + + push_patch(socket, to: "/#{normalized_callsign}") end {:noreply, socket} @@ -477,7 +507,7 @@ defmodule AprsmeWeb.MapLive.Index do def handle_event("clear_tracking", _params, socket) do socket = socket - |> assign(tracked_callsign: "", overlay_callsign: "") + |> assign(tracked_callsign: "", overlay_callsign: "", other_ssids: []) |> update_url_with_current_state() {:noreply, socket} @@ -1489,6 +1519,57 @@ defmodule AprsmeWeb.MapLive.Index do + + <%= if @tracked_callsign != "" and @other_ssids != [] do %> +
+ +
+ <%= for ssid_info <- @other_ssids do %> +
+ +
+ <%= if ssid_info.received_at do %> + + {time_ago_in_words(ssid_info.received_at)} + + <% end %> + <.link + navigate={~p"/info/#{ssid_info.callsign}"} + class="text-xs text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors" + title={gettext("Station info")} + > + + + + +
+
+ <% end %> +
+
+ <% end %> +