updates
This commit is contained in:
parent
8ea697f39e
commit
f6f1ad1e2f
7 changed files with 74 additions and 99 deletions
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue