diff --git a/assets/js/map.ts b/assets/js/map.ts index e2748c7..c3452fc 100644 --- a/assets/js/map.ts +++ b/assets/js/map.ts @@ -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 = { `; } + 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 += `
${date.toISOString()}
`; + } + } + content += ""; return content; }, diff --git a/config/dev.exs b/config/dev.exs index 366cabb..c181b1a 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -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, diff --git a/lib/aprs_web/live/map_live/callsign_view.ex b/lib/aprs_web/live/map_live/callsign_view.ex index 2eeb4d5..6809ce3 100644 --- a/lib/aprs_web/live/map_live/callsign_view.ex +++ b/lib/aprs_web/live/map_live/callsign_view.ex @@ -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""" _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 diff --git a/lib/parser.ex b/lib/parser.ex index d4a747e..b6923a5 100644 --- a/lib/parser.ex +++ b/lib/parser.ex @@ -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 diff --git a/test/parser/parser_test.exs b/test/parser/parser_test.exs index 452cdf5..5339e11 100644 --- a/test/parser/parser_test.exs +++ b/test/parser/parser_test.exs @@ -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 diff --git a/test/parser/property_test.exs b/test/parser/property_test.exs index 6aeef45..a54a26c 100644 --- a/test/parser/property_test.exs +++ b/test/parser/property_test.exs @@ -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