feat: Implement direct station URLs with persistent latest packet display

- Add support for station-specific URLs (e.g., /w5isp) as path parameters
- Always display the most recent packet for tracked stations, regardless of trail/historical duration
- Fetch and store tracked callsign's latest packet on mount
- Update packet filtering to preserve tracked callsign's packet even if outside time threshold
- Keep tracked callsign's latest packet updated with real-time arrivals
- Display tracked callsign's packet immediately when map loads
- Remove obsolete RedirectController in favor of direct LiveView routing

This ensures users viewing a specific station always see its most recent position,
even if it falls outside the selected historical timeframe.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Graham McIntire 2025-07-30 12:56:05 -05:00
parent f60840c0e0
commit 5c69604c32
No known key found for this signature in database
4 changed files with 53 additions and 14 deletions

View file

@ -1,7 +0,0 @@
defmodule AprsmeWeb.RedirectController do
use AprsmeWeb, :controller
def callsign_to_map(conn, %{"callsign" => callsign}) do
redirect(conn, to: "/?call=#{callsign}")
end
end

View file

@ -62,8 +62,8 @@ defmodule AprsmeWeb.MapLive.Index do
# Setup additional subscriptions if connected
socket = setup_additional_subscriptions(socket)
# Handle callsign tracking
tracked_callsign = Map.get(params, "call", "")
# Handle callsign tracking - check path params first, then query params
tracked_callsign = Map.get(params, "callsign", Map.get(params, "call", ""))
{final_map_center, final_map_zoom} =
Navigation.handle_callsign_tracking(
@ -169,6 +169,14 @@ 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 tracked_callsign == "" do
nil
else
CachedQueries.get_latest_packet_for_callsign_cached(tracked_callsign)
end
assign(socket,
map_ready: false,
map_bounds: initial_bounds,
@ -178,6 +186,7 @@ defmodule AprsmeWeb.MapLive.Index do
packet_state: PacketManager.init_packet_state(),
overlay_callsign: "",
tracked_callsign: tracked_callsign,
tracked_callsign_latest_packet: tracked_callsign_latest_packet,
trail_duration: trail_duration,
historical_hours: historical_hours,
packet_age_threshold: packet_age_threshold,
@ -270,12 +279,14 @@ defmodule AprsmeWeb.MapLive.Index do
# Filter by time and bounds
filtered_packets =
filter_packets_by_time_and_bounds(
filter_packets_by_time_and_bounds_with_tracked(
Map.new(current_packets, fn packet ->
{get_callsign_key(packet), packet}
end),
socket.assigns.map_bounds,
socket.assigns.packet_age_threshold
socket.assigns.packet_age_threshold,
socket.assigns.tracked_callsign,
socket.assigns.tracked_callsign_latest_packet
)
visible_packets_list = DataBuilder.build_packet_data_list_from_map(filtered_packets, false, socket)
@ -312,6 +323,21 @@ defmodule AprsmeWeb.MapLive.Index do
Navigation.zoom_to_current_location(socket)
end
# If we're tracking a callsign and have its latest packet, display it immediately
socket =
if socket.assigns.tracked_callsign != "" and socket.assigns.tracked_callsign_latest_packet do
packet = socket.assigns.tracked_callsign_latest_packet
packet_data = DataBuilder.build_packet_data(packet)
push_event(socket, "add_historical_packets_batch", %{
packets: [packet_data],
batch: 0,
is_final: false
})
else
socket
end
# Wait for JavaScript to send the actual map bounds before loading historical packets
# The calculated bounds might be too small/inaccurate
Logger.debug("Map ready - waiting for JavaScript to send actual bounds before loading historical packets")
@ -836,6 +862,8 @@ defmodule AprsmeWeb.MapLive.Index do
packet_sender = Map.get(packet, :sender, Map.get(packet, "sender", ""))
if String.upcase(packet_sender) == String.upcase(socket.assigns.tracked_callsign) do
# Update the tracked callsign's latest packet
socket = assign(socket, :tracked_callsign_latest_packet, packet)
process_packet_for_display(packet, socket)
else
{:noreply, socket}
@ -1554,6 +1582,25 @@ defmodule AprsmeWeb.MapLive.Index do
|> Map.new()
end
@spec filter_packets_by_time_and_bounds_with_tracked(map(), map(), DateTime.t(), String.t(), map() | nil) :: map()
defp filter_packets_by_time_and_bounds_with_tracked(
packets,
bounds,
time_threshold,
tracked_callsign,
tracked_latest_packet
) do
filtered = filter_packets_by_time_and_bounds(packets, bounds, time_threshold)
# Always include the tracked callsign's latest packet if we have one
if tracked_callsign != "" and tracked_latest_packet do
key = get_callsign_key(tracked_latest_packet)
Map.put(filtered, key, tracked_latest_packet)
else
filtered
end
end
# Helper functions for marker operations
@spec remove_markers_batch(Socket.t(), list()) :: Socket.t()

View file

@ -60,6 +60,7 @@ defmodule AprsmeWeb.MapLive.Navigation do
def handle_callsign_search(callsign, socket) do
if Utils.valid_callsign?(callsign) do
# Navigate to the new URL structure with the callsign as a path param
{:noreply, LiveView.push_navigate(socket, to: "/#{callsign}")}
else
{:noreply, LiveView.put_flash(socket, :error, "Invalid callsign format")}

View file

@ -98,10 +98,8 @@ defmodule AprsmeWeb.Router do
live "/api", ApiDocsLive, :index
live "/info/:callsign", InfoLive.Show, :show
live "/", MapLive.Index, :index
live "/:callsign", MapLive.Index, :index
end
# Redirect old callsign map URLs to new format
get "/:callsign", RedirectController, :callsign_to_map
end
# API v1 routes