Converts the inline state maps in Is, PacketConsumer, LeaderElection, and
CircuitBreaker into structs with @type t specs. Enforced keys catch typos
on state updates and give dialyzer a concrete type to check against
instead of map() in every handle_* clause.
Is' nested login_params and packet_stats are extracted to their own
modules (Aprsme.Is.LoginParams, Aprsme.Is.PacketStats) rather than
defined inline, per the project's no-nested-modules rule.
Turned on :error_handling, :underspecs, and :unmatched_returns in
mix.exs dialyzer config. The 97 warnings this surfaced were fixed in
place rather than suppressed:
- unmatched_return (79): explicit discard with `_ = ...` for
fire-and-forget side effects (Process.cancel_timer, :ets.new,
send/2), and pattern-matched `:ok = ...` for control-plane
Phoenix.PubSub subscribe/unsubscribe/broadcast calls so a future
return-shape change fails loud.
- contract_supertype (18): tightened @spec arg and return types on
data_builder, historical_loader, url_params, packet_utils,
encoding_utils, aprs_symbol, weather_controller, packet_replay to
match each function's actual success typing.
No behavioural change. mix compile clean, 1008 tests pass, dialyzer
count is now 0.
- packet_consumer: when a batch exceeds max_batch_size, carry the
excess over to the next cycle instead of dropping it. Previously
any overage was split off into a drop list that was only logged,
losing packets the producer had already acknowledged.
- map_live: cancel the hover_end_timer in terminate/2 so a user
disconnecting while a marker is still highlighted doesn't leave
an orphaned timer behind.
- packet_consumer: rescue/log exceptions inside the async broadcast
task so PubSub serialization bugs or outages are observable
instead of silently killing the Task.
- prepared_queries: swap ST_Y/ST_X BETWEEN predicates for the && /
ST_MakeEnvelope pattern so bounds queries hit the GIST index on
packets.location instead of sequentially scanning.
Drop the pre-insert aprs_messages broadcast in Is.dispatch (subscribers
already get a richer payload via postgres:aprsme_packets after insert).
Switch PacketsLive.CallsignView to the per-callsign packets:<CS> topic
so it only receives relevant packets instead of filtering every one.
In PacketConsumer: reuse the received_at stamped in Is.dispatch instead
of calling DateTime.utc_now/0 per packet; drop the duplicate struct_to_map
pass (Is.dispatch already handled it); fold coordinate validation and
Geo.Point construction into set_lat_lon + create_location_geometry so
coords are normalized once; extract device_identifier from the already-
normalized attrs; pre-build the broadcast payload once (identifier pick,
lat/lon aliases, routing callsign) so the async broadcast task no longer
does Map.drop/Map.merge per packet.
Rewrite PacketSanitizer.sanitize_packet / sanitize_data_map with Map.new/2
instead of Enum.reduce + Map.put — one allocation per packet instead of N.
Also compute has_weather in Packet.changeset/2 so direct-changeset inserts
(tests, backfills) populate it the same way the GenStage pipeline does.
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.
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.
- Extract prepare_markers_for_push/2 to reduce nesting depth in
handle_info
- Split detect_item_or_object into smaller predicate and application
functions to reduce cyclomatic complexity
- Coerce Decimal/nil values to native numbers in telemetry metrics
(PostgreSQL sum() returns numeric type mapped to Decimal by Postgrex)
- Align locate button horizontally with zoom controls
- 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).
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.
- 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
APRS object packets (e.g. DAPNET transmitters) were showing the
sender's callsign on the map instead of the object name. For example,
DB0SDA (Germany) sending an object for P-K5SGD (Texas) would display
as "DB0SDA" at the Texas coordinates.
- Add display_name/1 to PacketUtils that returns object_name for
objects, item_name for items, falling back to sender
- Update DataBuilder to use display_name in all callsign display paths
- Fix object/item detection order in packet_consumer to prevent
objects from being incorrectly flagged as items
- Add DataBuilder tests for object/item display name behavior
- Fix flaky PacketPipelineSupervisor test (ensure module loaded)
Add posresolution and rain_midnight to the list of float fields in
normalize_numeric_types to ensure integer values are converted to floats
before database insertion. This fixes the Ecto.ChangeError that was
preventing many APRS packets from being stored in the database.
- Create migration to change string fields from VARCHAR(255) to TEXT type
- Add PacketSanitizer module for UTF-8 safe string truncation
- Update PacketConsumer to sanitize data before database insertion
- Prevents database errors from overly long packet data fields
The APRS parser is sending timestamp as integer (Unix timestamp) but our
schema expects a string. This was causing Ecto.ChangeError in production
preventing packet insertion.
Convert integer timestamps to strings in the field conversion pipeline.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Instead of playing whack-a-mole with individual fields, this implements a
proper whitelist approach that only allows fields defined in the Packet schema.
- Created PacketFieldWhitelist module with all 78 valid packet fields
- Filters out ANY field not in the schema, preventing future errors
- Handles both atom and string keys
- Includes debug helpers to identify filtered fields
This definitively fixes packet insertion failures by ensuring only valid
database fields are passed to Repo.insert_all, regardless of what the
APRS parser sends.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
The parser is sending fields with different names than our schema:
- aprs_messaging? (with question mark) -> convert to aprs_messaging
- raw_data -> remove (not in schema)
This should fix the remaining packet insertion failures in production.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Added item_name, object_name fields to store names
- Added is_item, is_object boolean flags for filtering
- Added dao field to store DAO extension data for extra position precision
- Automatic detection during packet processing to populate these fields
- Added database indexes for efficient queries
- Now preserving itemname as item_name and dao data instead of discarding
This enables future features like:
- Showing/hiding items and objects separately from stations
- Querying for specific objects (events, repeaters, etc.)
- Using DAO data for high-precision applications
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Production logs showed additional fields causing insertion failures:
- dao: DAO extension data
- itemname: Item name field
- longitude/latitude: Parser uses these but our schema uses lon/lat
Added conversion step to map latitude->lat and longitude->lon before processing.
This ensures packets can be properly saved to the database.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
The updated APRS parser (commit 69d0ab5) adds several fields that don't exist in our database schema:
- type, digipeaters, daodatumbyte, mbits, message (duplicate of message_text)
- phg (we store individual PHG fields), wx (weather object)
- resultcode, resultmsg, gpsfixstatus
Created a centralized helper function to remove all non-schema fields before database insertion.
Also enhanced error logging to capture sample packet data for easier debugging.
This fixes the production issue where packets were failing to save to the database.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
The recent APRS parser update added a gpsfixstatus field that doesn't exist
in our database schema. This was causing all packet insertions to fail in
production with ArgumentError, preventing any packets from being stored and
making historical packet loading impossible.
Changes:
- Added explicit removal of :gpsfixstatus field in packet_consumer.ex
- Added both atom and string key deletion for safety
- Placed removal alongside other field removals like raw_weather_data
This fixes the critical production issue where no packets were being stored,
which prevented historical packet loading from working.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Optimized list concatenation in PacketConsumer from O(n) to O(1) by prepending events
- Refactored coordinate rounding to avoid recreating anonymous functions on each call
- Added database indexes for case-insensitive searches on upper(sender) and upper(base_callsign)
- Implemented RegexCache to avoid recompiling regex patterns for wildcard device matching
- Added compound index on upper(sender) with received_at for efficient sorted queries
These changes improve performance in hot paths:
- Packet batching is now more efficient with better list operations
- Coordinate processing avoids function allocation overhead
- Database queries using UPPER() now use functional indexes
- Device wildcard matching no longer recompiles regex on every lookup
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
The map wasn't receiving real-time packet updates because:
1. LiveView wasn't subscribed to StreamingPacketsPubSub
2. LiveView wasn't handling {:streaming_packet, packet} messages
3. Packets weren't being broadcast to both PubSub systems
Changes:
- Added StreamingPacketsPubSub subscription with viewport bounds filtering
- Added handler for {:streaming_packet, packet} messages
- Update subscription bounds when map viewport changes
- Properly unsubscribe on LiveView termination
- Also broadcast packets to SpatialPubSub (in addition to StreamingPacketsPubSub)
Now the map only receives packets within its current viewport bounds,
reducing unnecessary data transfer and improving performance.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit addresses two major areas:
1. Crash Prevention & Error Handling:
- Added error handling to all database queries to prevent crashes
- Implemented circuit breaker pattern for APRS-IS connection
- Added timeouts to all Redis operations (5 second timeout)
- Fixed unsafe pattern matches that could cause MatchError crashes
- Added try/rescue blocks around background tasks
- Improved socket operation error handling with proper cleanup
- Enhanced packet cleanup worker with comprehensive error handling
2. RF Path Station Markers:
- Fixed issue where digipeater/iGate icons weren't shown when searching for callsigns
- Now loads and displays position markers for all stations in the RF path
- Extracts RF path stations from tracked callsign packets
- Fetches latest position for each digipeater/iGate
- Properly displays complete RF path visualization with station positions
Technical details:
- Added Process.alive? checks before sending messages
- Fixed resource cleanup in terminate functions
- Added proper error handling to File.read operations
- Implemented safe wrapper for ShutdownHandler health checks
- All changes ensure the app continues running even when errors occur
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Fixed File.read crash in devices_seeder.ex with proper error handling
- Added timeout to TCP connect in aprs_is_connection.ex (10s timeout)
- Fixed unsafe socket send operations with error handling
- Added Process.alive? check before sending to PIDs in streaming_packets_pubsub.ex
- Fixed stream materialization memory issue in packet_consumer.ex
- Added error handling for datetime parsing errors
- Prevented crashes from dead processes and closed sockets
These fixes ensure the application is more resilient to:
- Missing files
- Network timeouts
- Dead processes
- Closed sockets
- Invalid data parsing
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement leader election to ensure only one APRS-IS connection across
multiple Kubernetes replicas. This prevents duplicate packet processing
and respects APRS-IS usage policies.
Key changes:
- Add leader election using :global registry
- Create connection manager for dynamic APRS-IS management
- Implement packet distribution from leader to all nodes
- Add Kubernetes headless service for node discovery
- Configure DNS-based clustering with libcluster
- Update deployment to support 3 replicas with clustering
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add nil check before canceling timer in PacketConsumer to prevent errors
when timer reference doesn't exist (fixes test failures)
- Simplify packet counter reset migration by removing unsupported event
triggers, keeping only manual reset function
- All tests now passing
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed "ParseError does not implement the Access behaviour" error that
occurred during packet batch insert when packets contained ParseError
structs in their data_extended field.
Added special handling for Aprs.Types.ParseError structs in the
struct_to_map function to convert them to simple maps with their
error_code and message fields, preventing the Access behavior error
when the struct is accessed during processing.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
The memory monitoring was incorrectly using total VM memory instead of
per-process memory, causing false positives whenever any process in the
system allocated memory.
Changes:
- Changed memory monitoring to per-process instead of total VM memory
- Reduced thresholds to reasonable per-process values (50MB diff, 100MB total)
- Increased minor GC trigger from 100 to 1000 packets to prevent GC after
every normal batch
This prevents detecting memory changes from other processes and eliminates
the constant GC warnings while still maintaining proper memory management.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Reduced batch processing parameters to prevent excessive GC:
- Reduced batch_size from 500 to 100 packets
- Adjusted batch_timeout from 2000ms to 1000ms for faster processing
- Reduced max_demand from 750 to 300 to match smaller batches
- Updated hardcoded fallback from 500 to 100 in packet_consumer.ex
- Changed minor GC trigger from 500 to 100 packets to match batch size
These smaller batch sizes were proven to work well in production and
should significantly reduce the frequency of garbage collection while
maintaining good performance.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Increased memory thresholds from 10MB/20MB to 500MB/1GB since the server
has 15GB available memory. This prevents constant GC warnings while
maintaining memory safety.
Configured VM garbage collection settings in vm.args.eex:
- Enabled concurrent ports/sockets limit (+Q 65536)
- Set heap sizes and GC parameters optimized for better performance
- Added scheduler and binary heap settings
These changes will reduce unnecessary GC overhead and improve overall
application performance.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Create PacketConsumerPool to run 3 parallel consumers (configurable)
- Increase max_demand from 250 to 750 total (250 per consumer)
- Fix timer management to properly cancel/restart on batch processing
- Configure proper GenStage subscriptions with backpressure control
- Allow unnamed consumers for pool usage
This 3x increase in processing capacity prevents the "Packet buffer full"
warnings by ensuring consumers can keep up with incoming packet rate.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit introduces several major performance improvements and adds
robust error handling for malformed HTTP requests:
Performance Optimizations:
- Add database migration with BRIN indexes for time-series data and partial
indexes for recent queries, significantly improving query performance
- Create global StreamingPacketsPubSub system for real-time packet distribution
with geographic bounds filtering using ETS for fast lookups
- Refactor PacketConsumer to use Stream module for memory-efficient processing,
preventing memory accumulation during batch operations
- Implement dedicated BroadcastTaskSupervisor pool for async broadcast operations,
preventing GenServer blocking on I/O
- Increase database connection pool from 25 to 45 (production) and 15 to 30 (dev)
for better concurrency support
Error Handling:
- Add SentryFilter to prevent Bandit.HTTPError from missing Host headers from
cluttering Sentry (common with bots/scanners)
- Configure custom 400 Bad Request error handling
- Filter out common bot/scanner paths from error reporting
All changes include comprehensive test coverage following TDD practices.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Restored original PacketConsumer implementation from before optimizations
- Reverted to fixed batch size of 50 packets (original value)
- Removed SystemMonitor and InsertOptimizer from application startup
- Restored original config with buffer size 1000 and batch timeout 1000ms
- Removed all dynamic batch sizing and optimization logic
The recent performance optimizations were causing packet buffer overflows
because the dynamic batch sizing was actually slowing down processing.
Reverting to the simpler, working implementation that processes packets
consistently without buffer overflows.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Increased packet buffer size from 1,000 to 10,000 to handle traffic spikes
- Removed InsertOptimizer dynamic batch sizing that was slowing processing
- Reverted to fixed batch size of 200 for consistent performance
- Simplified insert options to use static configuration
- Added telemetry metrics for buffer overflow and utilization monitoring
- Fixed unused variable warning in packet_consumer.ex
The root cause was the recent performance optimizations that introduced
dynamic batch sizing, which actually degraded performance and caused
the producer buffer to overflow when the consumer couldn't keep up.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Fixed InsertOptimizer returning map instead of keyword list for Ecto
- Increased batch size ranges to handle high load (100-800 packets)
- Improved consumer responsiveness:
- Reduced batch timeout from 1000ms to 500ms
- Process batches at 80% capacity for better throughput
- Reduced adjustment interval from 10s to 5s
- Added comprehensive logging for debugging buffer status
- Created integration tests for packet pipeline
- Coordinated batch sizing between SystemMonitor and InsertOptimizer
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>