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:
parent
a0258c0ff0
commit
047cd3a7ce
6 changed files with 238 additions and 8 deletions
|
|
@ -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
|
- BRIN indexes consideration for time-series data
|
||||||
- Connection draining and load balancing for clustered deployments
|
- Connection draining and load balancing for clustered deployments
|
||||||
- Monitors CPU usage and connection counts across cluster nodes
|
- 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)
|
- Automatically drains connections when node is overloaded (>70% CPU or 2x average connections)
|
||||||
- Gracefully reconnects clients to less loaded nodes
|
- Gracefully reconnects clients to less loaded nodes
|
||||||
- Improves overall cluster stability and performance
|
- 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
|
- Weather queries now use indexed `has_weather` column
|
||||||
- Spatial queries optimized with geography cast index
|
- 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
|
### Fixed
|
||||||
- PostgreSQL notify trigger now sends all required fields for info page updates
|
- PostgreSQL notify trigger now sends all required fields for info page updates
|
||||||
- Info page real-time updates now display all packet details correctly
|
- Info page real-time updates now display all packet details correctly
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,16 @@ This document tracks potential improvements identified during the multi-replica
|
||||||
- Created `Aprsme.ShutdownHandler` with configurable drain timeout
|
- Created `Aprsme.ShutdownHandler` with configurable drain timeout
|
||||||
- Created `Aprsme.SignalHandler` for proper SIGTERM handling
|
- Created `Aprsme.SignalHandler` for proper SIGTERM handling
|
||||||
- **Silent shutdowns**: Removed all user notifications during graceful shutdown
|
- **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)
|
- Updated health endpoint to return 503 when draining (15s delay)
|
||||||
- Added preStop lifecycle hook with 15s sleep
|
- Added preStop lifecycle hook with 15s sleep
|
||||||
- Set terminationGracePeriodSeconds to 60 seconds
|
- Set terminationGracePeriodSeconds to 60 seconds
|
||||||
|
|
|
||||||
81
docs/tracked-callsign-behavior.md
Normal file
81
docs/tracked-callsign-behavior.md
Normal 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.
|
||||||
|
|
@ -56,8 +56,6 @@ defmodule Aprsme.EncodingUtils do
|
||||||
def to_float(value) when is_integer(value) do
|
def to_float(value) when is_integer(value) do
|
||||||
if value >= -9.0e15 and value <= 9.0e15 do
|
if value >= -9.0e15 and value <= 9.0e15 do
|
||||||
value * 1.0
|
value * 1.0
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -77,7 +75,6 @@ defmodule Aprsme.EncodingUtils do
|
||||||
|
|
||||||
def to_float(_), do: nil
|
def to_float(_), do: nil
|
||||||
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Converts various types to Decimal for database storage.
|
Converts various types to Decimal for database storage.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -448,12 +448,21 @@ defmodule Aprsme.Packets do
|
||||||
limit = Map.get(opts, :limit, 200)
|
limit = Map.get(opts, :limit, 200)
|
||||||
offset = Map.get(opts, :offset, 0)
|
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 =
|
base_query =
|
||||||
from(p in Packet,
|
if Map.has_key?(opts, :callsign) and opts[:callsign] != nil and opts[:callsign] != "" do
|
||||||
where: p.has_position == true,
|
# When tracking a callsign, show all their packets regardless of position
|
||||||
where: p.received_at >= ^time_ago
|
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
|
# Apply spatial and other filters BEFORE limiting
|
||||||
# This ensures we get the most recent packets within the specified bounds
|
# This ensures we get the most recent packets within the specified bounds
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Add table
Reference in a new issue