The IPGeolocation plug was setting session data, but the live_session
wasn't forwarding it to the LiveView mount. Also removed the root-path
restriction so geolocation works on any initial page load.
Objects and items were being displayed at the origin station's location
instead of their actual coordinates because the broadcast used the
sender (origin callsign) as the identifier instead of object_name/item_name.
For example, object CALGRY sent by VE6RWB-15 was appearing at VE6RWB-15's
location instead of CALGRY's actual coordinates.
Changes:
- Modified broadcast_single_packet to use object_name for objects
and item_name for items as the identifier instead of sender
- Added tests verifying objects and items broadcast with correct identifier
- Objects/items now display at their specified coordinates, not origin station
Fixes the positioning bug where objects appeared at sender location.
Implements error email notifications that:
- Send email only for first occurrence of each unique error
- Only send in production environment (dev/test are silent)
- Include error details, location, and link to ErrorTracker UI
- Use existing Resend/Swoosh configuration for delivery
- Email sent to graham@mcintire.me
Integrates with ErrorTracker via telemetry events.
Objects and items now display with their own names on the map instead of
the originating station's callsign. This prevents confusion where stations
creating objects (like repeaters) appear at multiple locations.
For example, CALGRY creates repeater objects (147.06-WC, 146.85-NH) which
were appearing on the map labeled as "CALGRY" at the repeater locations
instead of showing the actual object names.
Changes:
- Updated map_label() to return object_name for object packets
- Updated map_label() to return item_name for item packets
- Position packets continue to show the sender callsign
- Updated tests to expect object/item names instead of sender
This matches APRS.fi behavior where objects are displayed separately
from their originating station.
Implements GET /api/v1/weather/nearby endpoint with comprehensive parameter
validation and error handling. The controller validates required parameters
(lat, lon, radius) and optional parameters (hours, limit) with appropriate
range checking. Integrates with PreparedQueries for efficient database access
and WeatherJSON for response serialization.
Key features:
- Required params: lat (-90 to 90), lon (-180 to 180), radius (0-1000 miles)
- Optional params: hours (1-168, default 6), limit (1-100, default 50)
- Proper error responses via FallbackController (400 for bad requests, 422 for validation)
- Full test coverage (16 tests) including edge cases and error scenarios
Also updates FallbackController to handle {:error, status, message} tuples
and fixes WeatherJSON to support both field name formats (lat/lon and latitude/longitude)
for compatibility with existing tests and actual query results.
- Implements nearby/1 function to format weather station data
- Returns structured JSON with data and meta fields
- Handles null weather fields gracefully
- Includes distance, position, weather data, and symbols
- Formats DateTime to ISO8601 strings
- All tests passing (3/3)
- Fix PacketConsumerTest timing issues by adding Process.sleep(50) after handle_events
to wait for async broadcast tasks to complete before assertions
- Update Renovate workflow to use Docker container directly instead of GitHub Action
- Use full GitHub URL for checkout action to work with Forgejo Actions
- Modified handle_zoom_threshold_crossing to detect tracked_callsign
- Show trail line instead of heat map when tracking at zoom ≤ 8
- Maintain heat map behavior when not tracking
- Maintain marker behavior at zoom > 8
- Filters packets by tracked callsign and time threshold
- Sorts packets chronologically by received_at
- Pushes show_trail_line event with coordinates
- Returns socket unchanged when no packets found
- Strip Mic-E telemetry from comments before database storage
- Show sender callsign on map labels instead of object names for clarity
- Object names still used for grouping to maintain trail organization
- Add comprehensive tests for new encoding utilities
Fixes display issues where weather balloon "X3234025" was shown
instead of station "WA0YMH", and Mic-E telemetry bytes like "!w>`!"
were appearing in comments.
The email and callsign changesets add "did not change" errors when
created with empty attrs (on mount). Guard all field error displays
on changeset.action being set so errors only appear after user
interaction.
Cover to_float, to_decimal, normalize_data_type, weather_fields,
sanitize_packet_strings, to_float_safe, has_weather_data, create_point,
extract_coordinates, lat/lon helpers, PHG extraction, and altitude
extraction with 79 new tests across 4 test files.
Cover weather data with spaces/dots for missing values, negative
temperatures, negative altitudes, combined PHG+altitude+RNG
stripping, nil comments, comments that become empty after cleaning,
and false-match protection for comments starting with P or h. Also
fix strip_weather_data to require at least one weather field after
the preamble to avoid false-matching course/speed in non-weather
comments.
Weather packets had raw data strings (e.g. 007/000g000t054...)
stored as the comment instead of the trailing human-readable text.
Now iteratively strips fixed-width APRS weather fields in any order,
handling both position-included (XXX/XXX) and positionless (cXXXsXXX)
formats. Also strips RNG data and fixes altitude regex to handle
negative values. Empty comments after cleaning return nil.
Drop 10 dead parser-compat columns (srccallsign, dstcallsign,
origpacket, body, header, alive, posambiguity, symboltable,
symbolcode, messaging) and move 14 display-only columns into a
single `data` JSONB column (PHG, telemetry, radiorange,
information_field, format, posresolution, position_ambiguity,
luminosity, rain_midnight).
Reduces row width from ~70 to ~48 columns. Table is ephemeral
so migration uses DROP/recreate. Also fixes snow_24h -> snow bug
in weather check query and adds null byte stripping for JSONB
string sanitization.
Convert the packets table to a time-partitioned table with daily
partitions (PARTITION BY RANGE on received_at). This replaces the
expensive CTE-based batch DELETE cleanup with instant DROP TABLE
partition drops — no dead tuples, no WAL pressure, no VACUUM needed.
- Add PartitionManager GenServer to create/drop daily partitions
- Simplify PacketCleanupWorker to delegate to PartitionManager
- Replace gridsquare/geocalc deps with local Maidenhead module
- Remove gettext_pseudolocalize dependency
- Fix Decimal comparison in packet consumer test
- Replace N+1 get_latest_packet_for_callsign calls in RF path loading
with single get_latest_packets_for_callsigns batch query (DISTINCT ON)
- Change batch packet processing to collect marker removal IDs and push
one remove_markers_batch event instead of individual remove_marker calls
- Add callsignIndex reverse map (callsign_group → Set<markerID>) for O(1)
lookup in new_packet/new_packets handlers instead of scanning all markers
- Server tracks callsign replacements in prepare_packet_state, sends
convert_to_historical list so JS skips full markerStates scan
- Historical packets get red dot HTML server-side in build_minimal_packet_data
instead of JS createMarkerIcon generating it
- Add callsign_group field to all packet data output; server pre-sorts
by group with chronological order so JS iterates directly
- Normalize heat map intensity in Clustering.cluster_packets (min/50, cap 1.0)
instead of JS doing it per-point
- Remove buildPopupContent JS fallback and unused escapeHtml import;
server already provides popup in all data paths
- Batch real-time packet push_events: collect marker data across the
packet batch and send a single "new_packets" WebSocket message instead
of one per packet. JS handler processes all live→historical conversions
before adding new markers to avoid intra-batch conflicts.
- Batch RF path station queries: replace N+1 get_latest_packet_for_callsign
calls with a single DISTINCT ON query selecting only callsign + lat/lng.
- Add read_concurrency: true to streaming_packets_subscribers and :aprsme
ETS tables (read-heavy, rarely written).
- Remove dead GC code in PacketConsumer (threshold > 1000 unreachable with
max batch_size of 100).
Delete 5 unused modules: Presence (untracked), LiveView ShutdownHandler
(unused mixin), RateLimiterWrapper (pointless delegation), Passcode
(only used by own test), MapLive.Utils (pure delegates).
Fix ShutdownHandler to not call deleted Presence. Inline ParamUtils
at call sites. Remove cachex dep and 27 stale entries from mix.lock.
Clean commented-out Exq/Redis config from application.ex.
Historical map loading previously fetched all 73 columns (SELECT *) from
the packets table (~2068 bytes/row). Added get_recent_packets_for_map/1
with select_map_fields/1 that fetches only the 22 columns needed for map
rendering (~200 bytes/row), yielding a ~3x query speedup.
Also removes unused all_packets tracking from PacketProcessor and
simplifies visible_packets cleanup in Index.
The object name regex only matched [A-Z0-9 ] which missed names with
hyphens (P-K5SGD), hash symbols (#146.760), and other printable ASCII.
Changed to .{9}[*_] to match all valid object names including killed
objects. Also merged the two cond branches so data_extended is tried
first with information_field parsing as fallback.
Removed dead copy_insert/3 tests from db_optimizer_test.
ConnectionMonitor.gather_cluster_stats was calling :rpc.call(Node.self())
which GenServer.call'd back into itself while blocked in handle_info,
causing a 5-second deadlock timeout every 30 seconds. Fixed by computing
local stats directly from state.
LeaderElection never verified that a leader still held the :global
registration. After :global conflict resolution (two isolated nodes
merging), the loser was silently unregistered but stayed is_leader: true
forever, causing both pods to connect to APRS-IS with the same callsign
in a reconnect loop. Fixed by adding verify_leadership/1 to
check_leadership which detects lost registrations and steps down.
parse_rf_path was filtering out all paths containing qA* constructs,
which is virtually every APRS-IS packet. The qA* element delimits the
RF path from the APRS-IS metadata — it should be split on, not
discarded. Now properly extracts digipeaters (before qA*) and the
igate (after qA*), while filtering APRS aliases like WIDE/RELAY/TRACE.
Also adds hover handlers to trail polylines so hovering anywhere on a
call's track shows the RF path, not just on the dot markers.
When the PacketProducer buffer crosses 80% capacity, signal Aprsme.Is
to switch its TCP socket to passive mode, leveraging TCP flow control
to slow APRS-IS intake. Resume active mode when buffer drains below 30%.
A 30-second safety valve timer auto-resumes if backpressure gets stuck.
Disconnect/reconnect/terminate paths all clear backpressure state.
Extracts client IP from CF-Connecting-IP or X-Forwarded-For headers
and sets conn.remote_ip before request logging, so K8s logs show the
actual client address instead of the internal cluster/ingress IP.
Use CF-IPLatitude/CF-IPLongitude headers instead of making HTTP calls
to ip-api.com on every page load. Eliminates ~500ms latency, external
API dependency, and flaky tests.
Also disable test server to prevent port conflicts during development.
- Switch PacketProducer buffer from list to :queue for O(1) overflow handling
- Add cached leadership check via :persistent_term to eliminate GenServer.call
bottleneck in packet distribution hot path
- Simplify packets_live coordinate extraction from 70 lines of nested
conditionals to helper functions (extract_coordinate/2, format_coordinate/1)
- Remove debug logging from hot paths (packets.ex queries, PacketDistributor,
app.js console.logs)
- Remove sobelow dependency (false positives blocking commits)
- Add 25 new tests for PacketProducer, coordinate helpers, and cached leadership
- Handle login send failure in AprsIsConnection instead of crashing
- Close leaked sockets on login failure in Aprsme.Is (init + reconnect)
- Keep rescheduling keepalive timer when disconnected to prevent permanent loss
- Use supervised BroadcastTaskSupervisor instead of unsupervised Task.start
for packet broadcasting
- Remove racy Process.alive? check in StreamingPacketsPubSub; send/2 to
dead PIDs is a no-op, :DOWN monitors handle cleanup
- Add dateline wrapping to SpatialPubSub.point_in_bounds? matching
StreamingPacketsPubSub behavior
- Add bad_packets table cleanup to PacketCleanupWorker
- Add TODO.md with remaining improvements