fix: Show all packets for tracked callsigns including non-position data

- Modified get_recent_packets to not filter by has_position when tracking a specific callsign
- Ensures users can see status updates, messages, and other non-position packets
- Fixes issue where callsigns without recent position data wouldn't show any packets
- Updated documentation to reflect this behavior

🤖 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 14:02:44 -05:00
parent a0258c0ff0
commit 047cd3a7ce
No known key found for this signature in database
6 changed files with 238 additions and 8 deletions

View file

@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- BRIN indexes consideration for time-series data
- Connection draining and load balancing for clustered deployments
- Monitors CPU usage and connection counts across cluster nodes
- Documentation for tracked callsign behavior confirming last packet is always shown regardless of age
- Automatically drains connections when node is overloaded (>70% CPU or 2x average connections)
- Gracefully reconnects clients to less loaded nodes
- Improves overall cluster stability and performance
@ -27,6 +28,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Weather queries now use indexed `has_weather` column
- Spatial queries optimized with geography cast index
### Fixed
- Tracked callsigns now show all packets including those without position data
- Previously filtered out packets with `has_position == false`
- Now shows all packets when tracking a specific callsign via /:callsign URL
- Ensures users can see status updates, messages, and other non-position packets
### Fixed
- PostgreSQL notify trigger now sends all required fields for info page updates
- Info page real-time updates now display all packet details correctly

View file

@ -48,6 +48,16 @@ This document tracks potential improvements identified during the multi-replica
- Created `Aprsme.ShutdownHandler` with configurable drain timeout
- Created `Aprsme.SignalHandler` for proper SIGTERM handling
- **Silent shutdowns**: Removed all user notifications during graceful shutdown
### ✅ Tracked Callsign Always Shows Last Packet (2025-07-30)
- **Status**: Fixed - Now Shows All Packets Including Non-Position
- **Impact**: High - User experience for tracking specific stations
- **Implementation**:
- Fixed `get_recent_packets` to not filter by `has_position` when tracking callsign
- Confirmed `get_latest_packet_for_callsign` fetches without time restrictions
- Verified `filter_packets_by_time_and_bounds_with_tracked` always includes tracked packet
- Added documentation explaining the behavior
- Users can now see status updates, messages, and other non-position packets
- Updated health endpoint to return 503 when draining (15s delay)
- Added preStop lifecycle hook with 15s sleep
- Set terminationGracePeriodSeconds to 60 seconds

View file

@ -0,0 +1,81 @@
# Tracked Callsign Behavior Documentation
## Overview
When viewing a specific callsign at the `/:callsign` route (e.g., `/KE0GB`), the APRS.me application displays the last received packet from that callsign **regardless of age**.
## Implementation Details
### 1. Route Handling
- Route defined in `router.ex`: `live "/:callsign", MapLive.Index, :index`
- The callsign is extracted from the URL and set as `tracked_callsign` in the LiveView socket
### 2. Latest Packet Retrieval
- `Packets.get_latest_packet_for_callsign/1` fetches the most recent packet for a callsign
- Uses `QueryBuilder.callsign_history/2` with only a limit parameter (no time filtering)
- **No time restrictions** are applied when fetching the latest packet
### 3. Map Centering
- If the tracked callsign has a known position, the map centers on that location at zoom level 12
- Handled by `Navigation.handle_callsign_tracking/4` in the MapLive.Navigation module
### 4. Packet Display Logic
The key function ensuring old packets are always displayed is `filter_packets_by_time_and_bounds_with_tracked/5` in MapLive.Index:
```elixir
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
```
This function:
1. First filters packets by time threshold and map bounds
2. Then **always adds** the tracked callsign's latest packet to the filtered results
3. This ensures the tracked callsign is visible even if their last position is older than configured time windows
### 5. Trail Duration Independence
- Even with the shortest trail duration (1 hour), the tracked callsign's latest packet is displayed
- The trail duration setting only affects which other packets are shown, not the tracked callsign
## User Experience
When a user navigates to `/:callsign`:
1. The callsign appears in the search box
2. A "Tracking: CALLSIGN" indicator shows below the search
3. The map centers on the callsign's last known position (if available)
4. The callsign's latest packet is displayed on the map, no matter how old
5. Only packets from that callsign are shown (real-time filtering applies)
## Summary
The system is correctly implemented to **always show the last received packet** from a tracked callsign, regardless of:
- How old the packet is (could be days, weeks, or months old)
- The selected trail duration setting
- The historical data load setting
- Whether the packet is within the current map bounds
- **Whether the packet has position data or not** (as of 2025-07-30)
This ensures users can always find and view the last activity of any callsign they're tracking, including:
- Position packets with lat/lon coordinates
- Status updates without position
- Messages and telemetry data
- Weather reports
- Any other APRS packet types
## Fix Applied (2025-07-30)
The `get_recent_packets` function was modified to not filter by `has_position == true` when a specific callsign is being tracked. This ensures that all packet types are visible when viewing a callsign's activity, not just those with position data.

View file

@ -56,8 +56,6 @@ defmodule Aprsme.EncodingUtils do
def to_float(value) when is_integer(value) do
if value >= -9.0e15 and value <= 9.0e15 do
value * 1.0
else
nil
end
end
@ -77,7 +75,6 @@ defmodule Aprsme.EncodingUtils do
def to_float(_), do: nil
@doc """
Converts various types to Decimal for database storage.

View file

@ -448,12 +448,21 @@ defmodule Aprsme.Packets do
limit = Map.get(opts, :limit, 200)
offset = Map.get(opts, :offset, 0)
# Build base query with time and position filters
# Build base query with time filter
# Only filter by position if not tracking a specific callsign
base_query =
from(p in Packet,
where: p.has_position == true,
where: p.received_at >= ^time_ago
)
if Map.has_key?(opts, :callsign) and opts[:callsign] != nil and opts[:callsign] != "" do
# When tracking a callsign, show all their packets regardless of position
from(p in Packet,
where: p.received_at >= ^time_ago
)
else
# For general map view, only show packets with positions
from(p in Packet,
where: p.has_position == true,
where: p.received_at >= ^time_ago
)
end
# Apply spatial and other filters BEFORE limiting
# This ensures we get the most recent packets within the specified bounds

View file

@ -0,0 +1,126 @@
defmodule AprsmeWeb.MapLive.TrackedCallsignOldPacketTest do
use AprsmeWeb.ConnCase
import Aprsme.PacketsFixtures
import Phoenix.LiveViewTest
describe "tracked callsign shows last packet regardless of age" do
test "displays old packet for tracked callsign", %{conn: conn} do
# Create a packet that's 30 days old
thirty_days_ago = DateTime.add(DateTime.utc_now(), -30 * 24 * 60 * 60, :second)
old_packet =
packet_fixture(%{
sender: "TEST-OLD",
lat: Decimal.new("35.1234"),
lon: Decimal.new("-97.5678"),
raw_packet: "TEST-OLD>APRS:!3507.40N/09734.07W>Test old packet",
received_at: thirty_days_ago,
has_position: true
})
# Navigate to the tracked callsign URL
{:ok, view, _html} = live(conn, "/TEST-OLD")
# Wait for map to be ready
send(view.pid, %{
event: "map_ready",
data: %{
center: %{lat: 35.0, lng: -97.0},
zoom: 10,
bounds: %{
north: 36.0,
south: 34.0,
east: -96.0,
west: -98.0
}
}
})
# Allow time for the view to process
:timer.sleep(100)
# Check that the tracked callsign is set
assert render(view) =~ "TEST-OLD"
# The packet should be in the view's assigns
# Since filter_packets_by_time_and_bounds_with_tracked always includes
# the tracked callsign's latest packet, it should be present
state = :sys.get_state(view.pid)
socket = elem(state, 1).socket
# Verify the tracked callsign is set
assert socket.assigns.tracked_callsign == "TEST-OLD"
# Verify the tracked latest packet is the old packet
assert socket.assigns.tracked_latest_packet.id == old_packet.id
assert socket.assigns.tracked_latest_packet.sender == "TEST-OLD"
# Cleanup handled by test transaction
end
test "includes old packet even with 1-hour trail duration", %{conn: conn} do
# Create a packet that's 48 hours old
two_days_ago = DateTime.add(DateTime.utc_now(), -48 * 60 * 60, :second)
old_packet =
packet_fixture(%{
sender: "OLD-STATION",
lat: Decimal.new("40.7128"),
lon: Decimal.new("-74.0060"),
raw_packet: "OLD-STATION>APRS:!4042.77N/07400.36W>Old position",
received_at: two_days_ago,
has_position: true
})
# Navigate to the tracked callsign URL
{:ok, view, _html} = live(conn, "/OLD-STATION")
# Set trail duration to 1 hour (shortest option)
send(view.pid, {:update_trail_duration, 1})
# Wait for map to be ready
send(view.pid, %{
event: "map_ready",
data: %{
center: %{lat: 40.7, lng: -74.0},
zoom: 10,
bounds: %{
north: 41.0,
south: 40.0,
east: -73.0,
west: -75.0
}
}
})
:timer.sleep(100)
# The old packet should still be tracked despite 1-hour trail setting
state = :sys.get_state(view.pid)
socket = elem(state, 1).socket
assert socket.assigns.tracked_callsign == "OLD-STATION"
# 1 hour trail duration
assert socket.assigns.packet_age_threshold == 1
assert socket.assigns.tracked_latest_packet.id == old_packet.id
# Cleanup handled by test transaction
end
test "shows no packet message when callsign has no packets", %{conn: conn} do
# Navigate to a callsign that doesn't exist
{:ok, view, _html} = live(conn, "/NOPACKETS-1")
# Check that the tracked callsign is set
assert render(view) =~ "NOPACKETS-1"
# But there should be no packet data
state = :sys.get_state(view.pid)
socket = elem(state, 1).socket
assert socket.assigns.tracked_callsign == "NOPACKETS-1"
assert socket.assigns.tracked_latest_packet == nil
end
end
end