This commit is contained in:
Graham McIntire 2025-06-21 11:55:30 -05:00
parent 8ea697f39e
commit f6f1ad1e2f
No known key found for this signature in database
7 changed files with 74 additions and 99 deletions

View file

@ -496,13 +496,6 @@ let MapAPRSMap = {
popup: self.buildPopupContent(data),
openPopup: true,
});
// Send marker_clicked for the latest packet only
self.pushEvent("marker_clicked", {
id: data.id,
callsign: data.callsign,
lat: data.lat,
lng: data.lng,
});
});
// Handle highlighting the latest packet (open its popup)
@ -665,7 +658,7 @@ let MapAPRSMap = {
id: data.id,
callsign: data.callsign,
lat: lat,
lng: lng,
lng: lng
});
});
@ -864,6 +857,19 @@ let MapAPRSMap = {
</div>`;
}
if (data.timestamp) {
let date;
if (typeof data.timestamp === "number") {
date = new Date(data.timestamp * 1000);
} else if (typeof data.timestamp === "string") {
date = new Date(data.timestamp);
}
if (date && !isNaN(date.getTime())) {
content += `<div class="aprs-timestamp">${date.toISOString()}</div>`;
}
}
content += "</div>";
return content;
},

View file

@ -15,6 +15,7 @@ config :aprs, Aprs.Repo,
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10,
log: false,
types: Aprs.PostgresTypes
config :aprs, AprsWeb.Endpoint,

View file

@ -10,6 +10,7 @@ defmodule AprsWeb.MapLive.CallsignView do
@default_zoom 4
@default_replay_speed 1.0
@impl true
def mount(%{"callsign" => callsign}, _session, socket) do
# Normalize callsign to uppercase
normalized_callsign = String.upcase(callsign)
@ -140,7 +141,13 @@ defmodule AprsWeb.MapLive.CallsignView do
end
def handle_event("adjust_replay_speed", %{"speed" => speed}, socket) do
{:noreply, assign(socket, replay_speed: to_float(speed))}
speed_float = to_float(speed)
{:noreply, assign(socket, replay_speed: speed_float)}
end
@impl true
def handle_event("marker_clicked", _params, socket) do
{:noreply, socket}
end
def handle_event("map_ready", _params, socket) do
@ -185,10 +192,6 @@ defmodule AprsWeb.MapLive.CallsignView do
{:noreply, socket}
end
def handle_event("marker_clicked", %{"id" => _id, "callsign" => _callsign, "lat" => _lat, "lng" => _lng}, socket) do
{:noreply, socket}
end
defp handle_bounds_update(bounds, socket) do
# Convert string keys to atom keys and parse values
normalized_bounds = %{
@ -233,6 +236,7 @@ defmodule AprsWeb.MapLive.CallsignView do
end
end
@impl true
def handle_info({:zoom_to_location, lat, lng, zoom}, socket) do
socket = push_event(socket, "zoom_to_location", %{lat: lat, lng: lng, zoom: zoom})
{:noreply, socket}
@ -384,6 +388,7 @@ defmodule AprsWeb.MapLive.CallsignView do
{:noreply, socket}
end
@impl true
def render(assigns) do
~H"""
<link

View file

@ -12,6 +12,7 @@ defmodule AprsWeb.MapLive.Index do
@default_center %{lat: 39.8283, lng: -98.5795}
@default_zoom 5
@default_replay_speed 25.0
@finch_name Aprs.Finch
@impl true
@ -26,7 +27,6 @@ defmodule AprsWeb.MapLive.Index do
Endpoint.subscribe("aprs_messages")
Phoenix.PubSub.subscribe(Aprs.PubSub, "postgres:aprs_packets")
maybe_start_geolocation(socket)
# schedule_timers() # COMMENTED OUT: disables periodic replay
end
{:ok, socket}
@ -46,18 +46,18 @@ defmodule AprsWeb.MapLive.Index do
},
map_center: @default_center,
map_zoom: @default_zoom,
# replay_active: false, # COMMENTED OUT
# replay_speed: @default_replay_speed, # COMMENTED OUT
# replay_paused: false, # COMMENTED OUT
# replay_packets: [], # COMMENTED OUT
# replay_index: 0, # COMMENTED OUT
# replay_timer_ref: nil, # COMMENTED OUT
# replay_start_time: nil, # COMMENTED OUT
# replay_end_time: nil, # COMMENTED OUT
# historical_packets: %{}, # COMMENTED OUT
replay_active: false,
replay_speed: @default_replay_speed,
replay_paused: false,
replay_packets: [],
replay_index: 0,
replay_timer_ref: nil,
replay_start_time: nil,
replay_end_time: nil,
historical_packets: %{},
packet_age_threshold: one_hour_ago,
map_ready: false,
# replay_started: false, # COMMENTED OUT
replay_started: false,
pending_geolocation: nil,
bounds_update_timer: nil,
pending_bounds: nil,
@ -179,9 +179,6 @@ defmodule AprsWeb.MapLive.Index do
replay_started: false
)
# Restart replay after a short delay
Process.send_after(self(), :initialize_replay, 1000)
{:noreply, socket}
else
# If not active, the user manually requested a replay restart
@ -246,6 +243,9 @@ defmodule AprsWeb.MapLive.Index do
def handle_event("map_ready", _params, socket) do
socket = assign(socket, map_ready: true)
# Start historical replay
Process.send_after(self(), :initialize_replay, 500)
# If we have pending geolocation, zoom to it now
socket =
if socket.assigns.pending_geolocation do
@ -259,7 +259,7 @@ defmodule AprsWeb.MapLive.Index do
end
@impl true
def handle_event("marker_clicked", %{"id" => _id, "callsign" => _callsign, "lat" => _lat, "lng" => _lng}, socket) do
def handle_event("marker_clicked", _params, socket) do
{:noreply, socket}
end
@ -332,9 +332,7 @@ defmodule AprsWeb.MapLive.Index do
def handle_info({:ip_location, %{lat: lat, lng: lng}}, socket), do: handle_info_ip_location(lat, lng, socket)
# COMMENTED OUT
def handle_info(:initialize_replay, socket), do: handle_info_initialize_replay(socket)
# COMMENTED OUT
def handle_info(:replay_next_packet, socket), do: handle_replay_next_packet(socket)
def handle_info(:cleanup_old_packets, socket), do: handle_cleanup_old_packets(socket)
@ -401,9 +399,9 @@ defmodule AprsWeb.MapLive.Index do
do: to_string(packet["id"]),
else: System.unique_integer([:positive])
Logger.debug(
"[MAP] Incoming packet: id=#{inspect(callsign_key)} lat=#{inspect(lat)} lon=#{inspect(lon)} bounds=#{inspect(socket.assigns.map_bounds)} within_bounds?=#{inspect(MapHelpers.within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds))}"
)
# Logger.debug(
# "[MAP] Incoming packet: id=#{inspect(callsign_key)} lat=#{inspect(lat)} lon=#{inspect(lon)} bounds=#{inspect(socket.assigns.map_bounds)} within_bounds?=#{inspect(MapHelpers.within_bounds?(%{lat: lat, lon: lon}, socket.assigns.map_bounds))}"
# )
all_packets = Map.put(socket.assigns.all_packets, callsign_key, packet)
socket = assign(socket, all_packets: all_packets)
@ -502,8 +500,6 @@ defmodule AprsWeb.MapLive.Index do
end
defp handle_replay_end(socket) do
Process.send_after(self(), :initialize_replay, 10_000)
socket =
assign(socket,
replay_active: false,
@ -603,6 +599,13 @@ defmodule AprsWeb.MapLive.Index do
font-family: monospace;
}
.aprs-timestamp {
color: #6b7280;
font-size: 11px;
font-family: monospace;
padding-top: 4px;
}
/* Leaflet popup improvements for APRS data */
.leaflet-popup-content-wrapper {
border-radius: 8px;
@ -834,6 +837,9 @@ defmodule AprsWeb.MapLive.Index do
comment = Map.get(data_extended, :comment) || Map.get(data_extended, "comment") || ""
# Recursively convert tuples in data_extended to strings
safe_data_extended = convert_tuples_to_strings(data_extended)
to_float = fn
%Decimal{} = d ->
Decimal.to_float(d)
@ -882,7 +888,7 @@ defmodule AprsWeb.MapLive.Index do
"data_type" => to_string(Map.get(packet, :data_type, Map.get(packet, "data_type", "unknown"))),
"path" => Map.get(packet, :path, Map.get(packet, "path", "")),
"comment" => comment,
"data_extended" => data_extended || %{},
"data_extended" => safe_data_extended || %{},
"symbol_table_id" => symbol_table_id,
"symbol_code" => symbol_code,
"symbol_description" => symbol_description,
@ -891,6 +897,26 @@ defmodule AprsWeb.MapLive.Index do
}
end
defp convert_tuples_to_strings(map) when is_map(map) do
if Map.has_key?(map, :__struct__) do
map
else
Map.new(map, fn {k, v} ->
{k, convert_tuples_to_strings(v)}
end)
end
end
defp convert_tuples_to_strings(list) when is_list(list) do
Enum.map(list, &convert_tuples_to_strings/1)
end
defp convert_tuples_to_strings(tuple) when is_tuple(tuple) do
to_string(inspect(tuple))
end
defp convert_tuples_to_strings(other), do: other
defp build_weather_popup_html(packet, callsign) do
received_at =
cond do

View file

@ -66,8 +66,6 @@ defmodule Parser do
{:ok, callsign_parts} <- parse_callsign(sender),
{:ok, data_type} <- parse_datatype_safe(data),
{:ok, [destination, path]} <- split_path(path),
:ok <- validate_callsign(sender, :src),
:ok <- validate_callsign(destination, :dst),
:ok <- validate_path(path) do
data_trimmed = String.trim(data)
data_without_type = String.slice(data_trimmed, 1..-1//1)
@ -119,27 +117,6 @@ defmodule Parser do
{:error, :invalid_packet}
end
# Validate callsign for AX.25 compliance
def validate_callsign(callsign, :src) when is_binary(callsign) do
if regex_callsign_valid?(callsign) and not String.contains?(callsign, "*") do
:ok
else
{:error, "Invalid source callsign"}
end
end
def validate_callsign(_callsign, :src), do: {:error, "Invalid source callsign"}
def validate_callsign("", :dst), do: {:error, "Missing destination callsign"}
def validate_callsign(callsign, :dst) when is_binary(callsign) do
if regex_callsign_valid?(callsign), do: :ok, else: {:error, "Invalid destination callsign"}
end
def validate_callsign(_callsign, :dst), do: {:error, "Invalid destination callsign"}
defp regex_callsign_valid?(callsign), do: String.match?(callsign, ~r/^[A-Z0-9\-]+$/)
# Validate path for too many components
def validate_path(path) when is_binary(path) and path != "" do
if length(String.split(path, ",")) > 8 do

View file

@ -57,26 +57,6 @@ defmodule ParserTest do
end
end
describe "validate_callsign/2" do
property "accepts valid source callsigns" do
uppercase = Enum.map(?A..?Z, &<<&1>>)
digits = Enum.map(?0..?9, &<<&1>>)
valid_chars = uppercase ++ digits ++ ["-"]
check all cs_list <- StreamData.list_of(StreamData.member_of(valid_chars), min_length: 1),
cs = Enum.join(cs_list) do
assert :ok = Parser.validate_callsign(cs, :src)
end
end
property "rejects invalid source callsigns" do
check all cs <- StreamData.string(:printable, min_length: 1),
not String.match?(cs, ~r/^[A-Z0-9\-]+$/) or String.contains?(cs, "*") do
assert match?({:error, _}, Parser.validate_callsign(cs, :src))
end
end
end
describe "validate_path/1" do
property "rejects paths with too many components" do
check all n <- StreamData.integer(9..20) do

View file

@ -40,26 +40,6 @@ defmodule Parser.PropertyTest do
end
end
describe "validate_callsign/2" do
property "accepts valid source callsigns" do
uppercase = Enum.map(?A..?Z, &<<&1>>)
digits = Enum.map(?0..?9, &<<&1>>)
valid_chars = uppercase ++ digits ++ ["-"]
check all cs_list <- StreamData.list_of(StreamData.member_of(valid_chars), min_length: 1),
cs = Enum.join(cs_list) do
assert :ok = Parser.validate_callsign(cs, :src)
end
end
property "rejects invalid source callsigns" do
check all cs <- StreamData.string(:printable, min_length: 1),
not String.match?(cs, ~r/^[A-Z0-9\-]+$/) or String.contains?(cs, "*") do
assert match?({:error, _}, Parser.validate_callsign(cs, :src))
end
end
end
describe "validate_path/1" do
property "rejects paths with too many components" do
check all n <- StreamData.integer(9..20) do