misc fixes

This commit is contained in:
Graham McIntire 2025-06-22 17:11:58 -05:00
parent 01ee6eada5
commit e7214f5064
No known key found for this signature in database
8 changed files with 197 additions and 153 deletions

View file

@ -64,6 +64,7 @@ interface MarkerState {
historical?: boolean; historical?: boolean;
is_most_recent_for_callsign?: boolean; is_most_recent_for_callsign?: boolean;
callsign_group?: string; callsign_group?: string;
callsign?: string;
} }
interface MapEventData { interface MapEventData {
@ -503,69 +504,60 @@ let MapAPRSMap = {
const incomingCallsign = data.callsign_group || data.callsign || data.id; const incomingCallsign = data.callsign_group || data.callsign || data.id;
if (incomingCallsign) { if (incomingCallsign) {
// Find existing marker for this callsign - check both live and historical markers // Find existing live markers for this callsign and convert them to historical dots
let existingMarkerId: string | null = null; const markersToConvert: string[] = [];
let existingCallsign: string | null = null;
self.markerStates!.forEach((state, id) => { self.markerStates!.forEach((state, id) => {
// Extract callsign from various sources const stateCallsign = state.callsign_group || state.callsign;
const stateCallsign = // Only convert non-historical markers for the same callsign, but exclude the incoming packet's ID
state.callsign_group || if (
(typeof id === "string" && !id.startsWith("hist_") ? id : null) || stateCallsign === incomingCallsign &&
(typeof id === "string" && id.startsWith("hist_") !state.historical &&
? id.replace(/^hist_/, "").replace(/_\d+$/, "") String(id) !== String(data.id)
: null); ) {
markersToConvert.push(String(id));
// Match callsigns (only consider non-historical markers for conversion)
if (stateCallsign === incomingCallsign && !state.historical) {
existingMarkerId = id;
existingCallsign = stateCallsign;
} }
}); });
// If we found an existing live marker, convert it to a historical dot // Convert existing live markers to historical dots by updating their icon
if (existingMarkerId && existingCallsign) { markersToConvert.forEach((id) => {
const existingMarker = self.markers!.get(existingMarkerId); const existingMarker = self.markers!.get(id);
const existingState = self.markerStates!.get(existingMarkerId); const existingState = self.markerStates!.get(id);
if (existingMarker && existingState) { if (existingMarker && existingState) {
// Create historical dot data from existing marker // Update the state to historical
const historicalData = { existingState.historical = true;
id: `hist_${existingCallsign}_${Date.now()}`, existingState.is_most_recent_for_callsign = false;
// Create new historical dot icon
const historicalIconData = {
id: String(id),
lat: existingState.lat, lat: existingState.lat,
lng: existingState.lng, lng: existingState.lng,
callsign: existingCallsign, callsign: existingState.callsign || incomingCallsign,
callsign_group: existingCallsign, callsign_group: existingState.callsign_group || incomingCallsign,
symbol_table_id: existingState.symbol_table, symbol_table_id: existingState.symbol_table,
symbol_code: existingState.symbol_code, symbol_code: existingState.symbol_code,
historical: true, historical: true,
is_most_recent_for_callsign: false, is_most_recent_for_callsign: false,
timestamp: data.timestamp
? typeof data.timestamp === "string"
? new Date(data.timestamp).getTime() - 1000
: data.timestamp - 1000
: Date.now() - 1000,
popup: existingState.popup, popup: existingState.popup,
}; };
// Remove the existing marker (this will also clean up its trail reference) // Update the marker's icon to show as a dot
self.removeMarker(existingMarkerId); existingMarker.setIcon(self.createMarkerIcon(historicalIconData));
// Add the historical dot marker // Mark as historical
self.addMarker({ (existingMarker as any)._isHistorical = true;
...historicalData,
popup: self.buildPopupContent(historicalData),
});
} }
} });
} }
// Add the new marker with proper callsign grouping // Add the new marker as the most recent for this callsign
self.addMarker({ self.addMarker({
...data, ...data,
historical: false, historical: false,
is_most_recent_for_callsign: true, is_most_recent_for_callsign: true,
callsign_group: data.callsign_group || data.callsign || incomingCallsign, // Ensure callsign_group is set for trail grouping callsign_group: data.callsign_group || data.callsign || incomingCallsign,
popup: self.buildPopupContent(data), popup: self.buildPopupContent(data),
openPopup: true, openPopup: true,
}); });
@ -655,14 +647,46 @@ let MapAPRSMap = {
// Handle clearing historical packets // Handle clearing historical packets
self.handleEvent("clear_historical_packets", () => { self.handleEvent("clear_historical_packets", () => {
// Remove all historical markers // Remove only historical markers (preserve live markers and their trails)
const markersToRemove: string[] = []; const markersToRemove: string[] = [];
self.markers!.forEach((marker: any, id: any) => { self.markers!.forEach((marker: any, id: any) => {
if ((marker as any)._isHistorical) { const markerState = self.markerStates!.get(String(id));
// Only remove markers that are explicitly historical
if ((marker as any)._isHistorical || (markerState && markerState.historical)) {
markersToRemove.push(String(id)); markersToRemove.push(String(id));
} }
}); });
markersToRemove.forEach((id) => self.removeMarker(id));
// Remove historical markers without affecting trails of live markers
markersToRemove.forEach((id) => {
const marker = self.markers!.get(id);
const markerState = self.markerStates!.get(id);
if (marker) {
// Remove from map and storage
self.markerLayer!.removeLayer(marker);
self.markers!.delete(id);
self.markerStates!.delete(id);
// Only remove trail if this was the only marker for this callsign
if (self.trailManager && markerState) {
const callsign = markerState.callsign_group || markerState.callsign || id;
// Check if there are any live markers left for this callsign
let hasLiveMarkers = false;
self.markerStates!.forEach((state) => {
const stateCallsign = state.callsign_group || state.callsign;
if (stateCallsign === callsign && !state.historical) {
hasLiveMarkers = true;
}
});
// Only remove trail if no live markers remain
if (!hasLiveMarkers) {
self.trailManager.removeTrail(callsign);
}
}
}
});
}); });
// Handle bounds-based marker filtering // Handle bounds-based marker filtering
@ -681,6 +705,11 @@ let MapAPRSMap = {
self.handleEvent("clear_and_reload_markers", () => { self.handleEvent("clear_and_reload_markers", () => {
// This event is just a trigger - the server will handle clearing and adding markers // This event is just a trigger - the server will handle clearing and adding markers
}); });
// Handle clear_all_markers event
self.handleEvent("clear_all_markers", () => {
self.clearAllMarkers();
});
}, },
sendBoundsToServer() { sendBoundsToServer() {
@ -763,8 +792,8 @@ let MapAPRSMap = {
} }
} }
// Remove existing marker if it exists // Remove existing marker if it exists (without affecting trails since we're just replacing it)
self.removeMarker(data.id); self.removeMarkerWithoutTrail(data.id);
// Create marker - use simple dot for older historical positions, APRS icon for most recent // Create marker - use simple dot for older historical positions, APRS icon for most recent
const marker = L.marker([lat, lng], { const marker = L.marker([lat, lng], {
@ -934,8 +963,24 @@ let MapAPRSMap = {
} }
}); });
// Remove out-of-bounds markers // Remove out-of-bounds markers without affecting trails
markersToRemove.forEach((id) => self.removeMarker(String(id))); markersToRemove.forEach((id) => self.removeMarkerWithoutTrail(String(id)));
},
removeMarkerWithoutTrail(id: string | any) {
const self = this as unknown as LiveViewHookContext;
// Ensure id is a string
const markerId = typeof id === "string" ? id : String(id);
const marker = self.markers!.get(markerId);
if (marker) {
// Remove marker from map but don't touch trails
self.markerLayer!.removeLayer(marker);
self.markers!.delete(markerId);
self.markerStates!.delete(markerId);
// Note: We intentionally don't remove trails here
}
}, },
createMarkerIcon(data: MarkerData): L.DivIcon { createMarkerIcon(data: MarkerData): L.DivIcon {

View file

@ -394,20 +394,8 @@ defmodule Aprs.Packets do
defp filter_by_region(query, _), do: query defp filter_by_region(query, _), do: query
defp filter_by_callsign(query, %{callsign: callsign}) do defp filter_by_callsign(query, %{callsign: callsign}) do
# Support both exact match and partial match # Use sender field for exact callsign matching
# For exact match, check if callsign contains a hyphen (has SSID) from p in query, where: ilike(p.sender, ^callsign)
if String.contains?(callsign, "-") do
# Exact match for callsign with SSID
[base_call, ssid] = String.split(callsign, "-", parts: 2)
from p in query,
where:
ilike(p.base_callsign, ^base_call) and
((is_nil(p.ssid) and ^ssid == "0") or p.ssid == ^ssid)
else
# Match base callsign exactly, regardless of SSID
from p in query, where: ilike(p.base_callsign, ^callsign)
end
end end
defp filter_by_callsign(query, _), do: query defp filter_by_callsign(query, _), do: query

View file

@ -26,13 +26,8 @@ defmodule AprsWeb.MapLive.CallsignView do
page_title: "APRS Map - #{normalized_callsign}", page_title: "APRS Map - #{normalized_callsign}",
# Track visible packets by callsign # Track visible packets by callsign
visible_packets: %{}, visible_packets: %{},
# Default bounds - will be updated based on packet locations # Map bounds - will be set when first bounds update is received
map_bounds: %{ map_bounds: nil,
north: 49.0,
south: 24.0,
east: -66.0,
west: -125.0
},
map_center: @default_center, map_center: @default_center,
map_zoom: @default_zoom, map_zoom: @default_zoom,
default_center: @default_center, default_center: @default_center,
@ -62,7 +57,9 @@ defmodule AprsWeb.MapLive.CallsignView do
packets_loaded: false, packets_loaded: false,
# Latest symbol table ID and code # Latest symbol table ID and code
latest_symbol_table_id: "/", latest_symbol_table_id: "/",
latest_symbol_code: ">" latest_symbol_code: ">",
# Flag to track if latest marker was already pushed
latest_marker_pushed: false
) )
if connected?(socket) do if connected?(socket) do
@ -164,14 +161,7 @@ defmodule AprsWeb.MapLive.CallsignView do
|> assign(packets_loaded: true) |> assign(packets_loaded: true)
end end
# Auto-start replay if it hasn't been started yet # Don't start historical replay yet - wait for bounds to be available
socket =
if socket.assigns.replay_started do
socket
else
socket = start_historical_replay(socket)
assign(socket, replay_started: true, replay_active: true)
end
# If we have a pending geolocation, zoom to it after a delay # If we have a pending geolocation, zoom to it after a delay
socket = socket =
@ -208,21 +198,32 @@ defmodule AprsWeb.MapLive.CallsignView do
|> Enum.filter(fn {_k, packet} -> MapHelpers.within_bounds?(packet, normalized_bounds) end) |> Enum.filter(fn {_k, packet} -> MapHelpers.within_bounds?(packet, normalized_bounds) end)
|> Map.new() |> Map.new()
markers_to_remove = packets_to_remove =
socket.assigns.visible_packets socket.assigns.visible_packets
|> Enum.reject(fn {_k, packet} -> MapHelpers.within_bounds?(packet, normalized_bounds) end) |> Enum.reject(fn {_k, packet} -> MapHelpers.within_bounds?(packet, normalized_bounds) end)
|> Enum.map(fn {k, _} -> k end) |> Enum.map(fn {k, _} -> k end)
socket = socket =
if markers_to_remove == [] do if packets_to_remove == [] do
socket socket
else else
Enum.reduce(markers_to_remove, socket, fn k, acc -> Enum.reduce(packets_to_remove, socket, fn k, acc ->
push_event(acc, "remove_marker", %{id: k}) push_event(acc, "remove_marker", %{id: k})
end) end)
end end
socket = assign(socket, map_bounds: normalized_bounds, visible_packets: new_visible_packets) socket = assign(socket, map_bounds: normalized_bounds, visible_packets: new_visible_packets)
# Start historical replay now that we have bounds (if not already started)
socket =
if socket.assigns.map_ready and not socket.assigns.replay_started and
not is_nil(socket.assigns.map_bounds) do
socket = start_historical_replay(socket)
assign(socket, replay_started: true, replay_active: true)
else
socket
end
{:noreply, socket} {:noreply, socket}
end end
@ -249,11 +250,12 @@ defmodule AprsWeb.MapLive.CallsignView do
end end
def handle_info(:auto_start_replay, socket) do def handle_info(:auto_start_replay, socket) do
if not socket.assigns.replay_started and socket.assigns.map_ready do if not socket.assigns.replay_started and socket.assigns.map_ready and
not is_nil(socket.assigns.map_bounds) do
socket = start_historical_replay(socket) socket = start_historical_replay(socket)
{:noreply, assign(socket, replay_started: true, replay_active: true)} {:noreply, assign(socket, replay_started: true, replay_active: true)}
else else
if not socket.assigns.map_ready do if not socket.assigns.map_ready or is_nil(socket.assigns.map_bounds) do
Process.send_after(self(), :auto_start_replay, 1000) Process.send_after(self(), :auto_start_replay, 1000)
end end
@ -266,7 +268,9 @@ defmodule AprsWeb.MapLive.CallsignView do
def handle_info(%Phoenix.Socket.Broadcast{topic: "aprs_messages", event: "packet", payload: packet}, socket) do def handle_info(%Phoenix.Socket.Broadcast{topic: "aprs_messages", event: "packet", payload: packet}, socket) do
# Only process packets for the specific callsign being viewed # Only process packets for the specific callsign being viewed
if PacketUtils.generate_callsign(packet) == socket.assigns.callsign do packet_sender = Map.get(packet, :sender, "")
if String.upcase(packet_sender) == String.upcase(socket.assigns.callsign) do
handle_info_postgres_packet(packet, socket) handle_info_postgres_packet(packet, socket)
else else
{:noreply, socket} {:noreply, socket}
@ -280,11 +284,12 @@ defmodule AprsWeb.MapLive.CallsignView do
updated_visible_packets = Map.put(socket.assigns.visible_packets, key, packet) updated_visible_packets = Map.put(socket.assigns.visible_packets, key, packet)
socket = assign(socket, visible_packets: updated_visible_packets) socket = assign(socket, visible_packets: updated_visible_packets)
packet_data = PacketUtils.build_packet_data(packet) # Live packets are always the most recent for their callsign
packet_data = PacketUtils.build_packet_data(packet, true)
socket = socket =
if packet_data, if packet_data,
do: push_event(socket, "add_markers", %{markers: [packet_data]}), do: push_event(socket, "new_packet", packet_data),
else: socket else: socket
{:noreply, socket} {:noreply, socket}
@ -672,7 +677,8 @@ defmodule AprsWeb.MapLive.CallsignView do
fetch_historical_packets_for_callsign( fetch_historical_packets_for_callsign(
socket.assigns.callsign, socket.assigns.callsign,
one_hour_ago, one_hour_ago,
now now,
socket.assigns.map_bounds
) )
# Always fetch the latest packet with a position, regardless of age # Always fetch the latest packet with a position, regardless of age
@ -752,23 +758,23 @@ defmodule AprsWeb.MapLive.CallsignView do
historical_callsigns = MapSet.new(unique_position_packets, &PacketUtils.generate_callsign/1) historical_callsigns = MapSet.new(unique_position_packets, &PacketUtils.generate_callsign/1)
socket = {socket, latest_marker_pushed} =
if latest_packet && !MapSet.member?(historical_callsigns, latest_packet_id) do if latest_packet && !MapSet.member?(historical_callsigns, latest_packet_id) do
# Push the latest marker as a regular marker (not historical) # Push the latest marker as a regular marker (not historical)
packet_data = PacketUtils.build_packet_data(latest_packet) packet_data = PacketUtils.build_packet_data(latest_packet, true)
if packet_data do if packet_data do
push_event(socket, "add_markers", %{markers: [packet_data]}) {push_event(socket, "new_packet", packet_data), true}
else else
socket {socket, false}
end end
else else
socket {socket, false}
end end
if Enum.empty?(packet_data_list) do if Enum.empty?(packet_data_list) do
# No historical packets found (but latest marker may have been pushed above) # No historical packets found (but latest marker may have been pushed above)
socket assign(socket, latest_marker_pushed: latest_marker_pushed)
else else
# Clear any previous historical packets from the map # Clear any previous historical packets from the map
socket = push_event(socket, "clear_historical_packets", %{}) socket = push_event(socket, "clear_historical_packets", %{})
@ -787,18 +793,28 @@ defmodule AprsWeb.MapLive.CallsignView do
replay_packets: [], replay_packets: [],
replay_index: 0, replay_index: 0,
replay_start_time: one_hour_ago, replay_start_time: one_hour_ago,
replay_end_time: now replay_end_time: now,
latest_marker_pushed: latest_marker_pushed
) )
end end
end end
defp fetch_historical_packets_for_callsign(callsign, start_time, end_time) do defp fetch_historical_packets_for_callsign(callsign, start_time, end_time, bounds) do
Packets.get_packets_for_replay(%{ params = %{
callsign: callsign, callsign: callsign,
start_time: start_time, start_time: start_time,
end_time: end_time, end_time: end_time,
limit: 1000 limit: 1000
}) }
params =
if bounds do
Map.put(params, :bounds, [bounds.west, bounds.south, bounds.east, bounds.north])
else
params
end
Packets.get_packets_for_replay(params)
end end
# Filter packets to only include those with unique positions (lat/lon changed) # Filter packets to only include those with unique positions (lat/lon changed)
@ -886,7 +902,12 @@ defmodule AprsWeb.MapLive.CallsignView do
defp maybe_push_latest_marker(socket, nil), do: socket defp maybe_push_latest_marker(socket, nil), do: socket
defp maybe_push_latest_marker(socket, packet) do defp maybe_push_latest_marker(socket, packet) do
packet_data = PacketUtils.build_packet_data(packet) # Only push if we haven't already pushed it during historical replay
if packet_data, do: push_event(socket, "add_markers", %{markers: [packet_data]}), else: socket if Map.get(socket.assigns, :latest_marker_pushed, false) do
socket
else
packet_data = PacketUtils.build_packet_data(packet, true)
if packet_data, do: push_event(socket, "new_packet", packet_data), else: socket
end
end end
end end

View file

@ -361,10 +361,10 @@ defmodule AprsWeb.MapLive.Index do
end) end)
end end
# Clear existing historical packets # Remove only out-of-bounds historical packets instead of clearing all
socket = push_event(socket, "clear_historical_packets", %{}) socket = push_event(socket, "filter_markers_by_bounds", %{bounds: map_bounds})
# Load historical packets for the new bounds # Load additional historical packets for the new bounds if needed
socket = load_historical_packets_for_bounds(socket, map_bounds) socket = load_historical_packets_for_bounds(socket, map_bounds)
# Update map bounds and visible packets # Update map bounds and visible packets
@ -496,11 +496,13 @@ defmodule AprsWeb.MapLive.Index do
else: System.unique_integer([:positive]) else: System.unique_integer([:positive])
new_visible_packets = Map.put(socket.assigns.visible_packets, callsign_key, packet) new_visible_packets = Map.put(socket.assigns.visible_packets, callsign_key, packet)
marker_data = PacketUtils.build_packet_data(packet)
# Live packets are always the most recent for their callsign
marker_data = PacketUtils.build_packet_data(packet, true)
socket = socket =
if marker_data do if marker_data do
push_event(socket, "add_markers", %{markers: [marker_data]}) push_event(socket, "new_packet", marker_data)
else else
socket socket
end end
@ -965,8 +967,8 @@ defmodule AprsWeb.MapLive.Index do
# Clear existing historical packets # Clear existing historical packets
socket = assign(socket, historical_packets: %{}) socket = assign(socket, historical_packets: %{})
# Clear all markers on the client # Clear only historical markers on the client (preserve live markers)
socket = push_event(socket, "clear_all_markers", %{}) socket = push_event(socket, "clear_historical_packets", %{})
# Load historical packets with new time range # Load historical packets with new time range
socket = load_historical_packets_for_bounds(socket, socket.assigns.map_bounds) socket = load_historical_packets_for_bounds(socket, socket.assigns.map_bounds)

View file

@ -81,9 +81,9 @@ defmodule AprsWeb.MapLive.PacketUtils do
@spec generate_callsign(map()) :: String.t() @spec generate_callsign(map()) :: String.t()
def generate_callsign(packet) do def generate_callsign(packet) do
base_callsign = get_packet_field(packet, :base_callsign, "") base_callsign = get_packet_field(packet, :base_callsign, "")
ssid = get_packet_field(packet, :ssid, 0) ssid = get_packet_field(packet, :ssid, nil)
if ssid != 0 and ssid != "" and ssid != nil do if ssid != nil and ssid != "" do
"#{base_callsign}-#{ssid}" "#{base_callsign}-#{ssid}"
else else
base_callsign base_callsign
@ -138,15 +138,20 @@ defmodule AprsWeb.MapLive.PacketUtils do
Map.get(data_extended, to_string(key)) || "N/A" Map.get(data_extended, to_string(key)) || "N/A"
end end
def build_packet_data(packet) do def build_packet_data(packet, is_most_recent_for_callsign) when is_boolean(is_most_recent_for_callsign) do
{lat, lon, data_extended} = MapHelpers.get_coordinates(packet) {lat, lon, data_extended} = MapHelpers.get_coordinates(packet)
callsign = generate_callsign(packet) callsign = generate_callsign(packet)
if lat && lon && callsign != "" && callsign != nil do if lat && lon && callsign != "" && callsign != nil do
build_packet_map(packet, lat, lon, data_extended) packet_data = build_packet_map(packet, lat, lon, data_extended)
Map.put(packet_data, "is_most_recent_for_callsign", is_most_recent_for_callsign)
end end
end end
def build_packet_data(packet) do
build_packet_data(packet, false)
end
defp build_packet_map(packet, lat, lon, data_extended) do defp build_packet_map(packet, lat, lon, data_extended) do
data_extended = data_extended || %{} data_extended = data_extended || %{}
packet_info = extract_packet_info(packet, data_extended) packet_info = extract_packet_info(packet, data_extended)
@ -157,7 +162,7 @@ defmodule AprsWeb.MapLive.PacketUtils do
defp extract_packet_info(packet, data_extended) do defp extract_packet_info(packet, data_extended) do
%{ %{
callsign: generate_callsign(packet), callsign: get_packet_field(packet, :sender, ""),
symbol_table_id: get_packet_field(packet, :symbol_table_id, "/"), symbol_table_id: get_packet_field(packet, :symbol_table_id, "/"),
symbol_code: get_packet_field(packet, :symbol_code, ">"), symbol_code: get_packet_field(packet, :symbol_code, ">"),
timestamp: get_timestamp(packet), timestamp: get_timestamp(packet),
@ -197,7 +202,7 @@ defmodule AprsWeb.MapLive.PacketUtils do
""" """
<div class="aprs-popup"> <div class="aprs-popup">
<div class="aprs-callsign"><strong><a href="/map/callsign/#{packet_info.callsign}">#{packet_info.callsign}</a></strong></div> <div class="aprs-callsign"><strong><a href="/#{packet_info.callsign}">#{packet_info.callsign}</a></strong></div>
#{comment_html} #{comment_html}
<div class="aprs-coords">#{Float.round(to_float(lat), 4)}, #{Float.round(to_float(lon), 4)}</div> <div class="aprs-coords">#{Float.round(to_float(lat), 4)}, #{Float.round(to_float(lon), 4)}</div>
#{timestamp_html} #{timestamp_html}
@ -205,7 +210,7 @@ defmodule AprsWeb.MapLive.PacketUtils do
""" """
end end
defp build_weather_popup_html(packet, callsign) do defp build_weather_popup_html(packet, sender) do
received_at = get_received_at(packet) received_at = get_received_at(packet)
timestamp_dt = TimeHelpers.to_datetime(received_at) timestamp_dt = TimeHelpers.to_datetime(received_at)
@ -222,7 +227,7 @@ defmodule AprsWeb.MapLive.PacketUtils do
end end
""" """
<strong>#{callsign} - Weather Report</strong> <strong>#{sender} - Weather Report</strong>
#{timestamp_html} #{timestamp_html}
<hr style="margin-top: 4px; margin-bottom: 4px;"> <hr style="margin-top: 4px; margin-bottom: 4px;">
Temperature: #{get_weather_field(packet, :temperature)}°F<br> Temperature: #{get_weather_field(packet, :temperature)}°F<br>
@ -236,11 +241,19 @@ defmodule AprsWeb.MapLive.PacketUtils do
end end
defp build_packet_result(packet, packet_info, lat, lon, popup) do defp build_packet_result(packet, packet_info, lat, lon, popup) do
# Generate unique ID for live packets to prevent conflicts
packet_id =
if Map.has_key?(packet, :id) do
"live_#{packet.id}_#{System.unique_integer([:positive])}"
else
"live_#{packet_info.callsign}_#{System.unique_integer([:positive])}"
end
%{ %{
"id" => packet_info.callsign, "id" => packet_id,
"callsign" => packet_info.callsign, "callsign" => packet_info.callsign,
"base_callsign" => get_packet_field(packet, :base_callsign, ""), "base_callsign" => get_packet_field(packet, :base_callsign, ""),
"ssid" => get_packet_field(packet, :ssid, 0), "ssid" => get_packet_field(packet, :ssid, nil),
"lat" => to_float(lat), "lat" => to_float(lat),
"lng" => to_float(lon), "lng" => to_float(lon),
"data_type" => to_string(get_packet_field(packet, :data_type, "unknown")), "data_type" => to_string(get_packet_field(packet, :data_type, "unknown")),

View file

@ -95,20 +95,8 @@ defmodule AprsWeb.PacketsLive.CallsignView do
order_by: [desc: p.received_at], order_by: [desc: p.received_at],
limit: ^limit limit: ^limit
# Apply callsign filter # Apply callsign filter using sender field for exact matching
filtered_query = filtered_query = from p in query, where: ilike(p.sender, ^callsign)
if String.contains?(callsign, "-") do
# Exact match for callsign with SSID
[base_call, ssid] = String.split(callsign, "-", parts: 2)
from p in query,
where:
ilike(p.base_callsign, ^base_call) and
((is_nil(p.ssid) and ^ssid == "0") or p.ssid == ^ssid)
else
# Match base callsign exactly, regardless of SSID
from p in query, where: ilike(p.base_callsign, ^callsign)
end
filtered_query filtered_query
|> Repo.all() |> Repo.all()
@ -122,20 +110,15 @@ defmodule AprsWeb.PacketsLive.CallsignView do
end end
defp packet_matches_callsign?(packet, target_callsign) do defp packet_matches_callsign?(packet, target_callsign) do
# Check if packet sender or base_callsign matches the target callsign # Check exact match for sender field only
# Supports both exact matches and SSID variants (e.g., "N0CALL" matches "N0CALL-1")
sender = packet.sender || "" sender = packet.sender || ""
base_callsign = packet.base_callsign || ""
# Convert to uppercase for case-insensitive comparison # Convert to uppercase for case-insensitive comparison
sender_upper = String.upcase(sender) sender_upper = String.upcase(sender)
base_upper = String.upcase(base_callsign)
target_upper = String.upcase(target_callsign) target_upper = String.upcase(target_callsign)
# Check exact match for sender or base_callsign # Exact match only
# Also check if the sender starts with the target callsign (for SSID variants) sender_upper == target_upper
sender_upper == target_upper || base_upper == target_upper ||
String.starts_with?(sender_upper, target_upper <> "-")
end end
# Helper to get all packets (stored + live) in chronological order # Helper to get all packets (stored + live) in chronological order

View file

@ -74,19 +74,16 @@ defmodule Parser do
ssid = ssid =
case List.last(callsign_parts) do case List.last(callsign_parts) do
nil -> nil ->
0 nil
s when is_binary(s) -> s when is_binary(s) ->
case Integer.parse(s) do s
{i, _} -> i
:error -> 0
end
i when is_integer(i) -> i when is_integer(i) ->
i to_string(i)
_ -> _ ->
0 nil
end end
{:ok, {:ok,
@ -1182,17 +1179,12 @@ defmodule Parser do
end end
end end
defp extract_ssid(nil), do: 0 defp extract_ssid(nil), do: nil
defp extract_ssid(s) when is_binary(s) do defp extract_ssid(s) when is_binary(s), do: s
case Integer.parse(s) do
{i, _} -> i
:error -> 0
end
end
defp extract_ssid(i) when is_integer(i), do: i defp extract_ssid(i) when is_integer(i), do: to_string(i)
defp extract_ssid(_), do: 0 defp extract_ssid(_), do: nil
defp split_path_for_tunnel(path) do defp split_path_for_tunnel(path) do
split_path(path) split_path(path)

View file

@ -136,7 +136,7 @@ defmodule Aprs.Integration.HistoricalPacketsTest do
test "only loads packets within map bounds", %{conn: conn, packets: mock_packets} do test "only loads packets within map bounds", %{conn: conn, packets: mock_packets} do
# Mock to return only packets within the specified bounds # Mock to return only packets within the specified bounds
# TEST2 is at lat: 38.8, lon: -97.5, which is outside these bounds # TEST2 is at lat: 38.8, lon: -97.5, which is outside these bounds
test1_packets = Enum.filter(mock_packets, fn packet -> packet.base_callsign == "TEST1" end) test1_packets = Enum.filter(mock_packets, fn packet -> packet.sender == "TEST1" end)
expect_packets_for_replay_with_bounds(test1_packets) expect_packets_for_replay_with_bounds(test1_packets)
{:ok, view, _html} = live(conn, "/") {:ok, view, _html} = live(conn, "/")