- 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.
Dep upgrades (mix deps.update --all):
- phoenix_live_view 1.1.27 → 1.1.28
- swoosh 1.23.1 → 1.25.0
- bandit 1.10.3 → 1.10.4
- credo 1.7.17 → 1.7.18
- hammer 7.2.0 → 7.3.0
- igniter 0.7.6 → 0.7.9
- and minor: fine, lazy_html, meck, mimerl
- removed stale lock entries: geocalc, gettext_pseudolocalize, gridsquare
Bug fixes surfaced during update:
- ETS cache tables were :protected (owned by Application master), preventing
the Cache GenServer from writing; change to :public + write_concurrency
so device/symbol/query caches actually work
- historical_dot_html returned Phoenix.HTML.safe tuple instead of plain string;
symbol_html is JSON-encoded for JS so it must be binary
- String.slice always returns binary so the || "Unknown error" fallback was
unreachable dead code (dialyzer guard_fail)
- Supervisor.which_children always returns a list so the _ -> branch in
database_metrics was unreachable dead code (dialyzer pattern_match_cov)
- Add @type t :: %__MODULE__{} to User schema to resolve unknown_type in user_auth spec
- Extract nested if in leader_election to fix Credo nesting depth violation
- Update .dialyzer_ignore.exs: remove 2 stale entries, add false positives for
Ecto.Multi/Gettext opaque types and Mix.Task PLT limitations
Elixir 1.19 deprecates storing regexes in module attributes. Inline
@wx_preamble_pattern and @wx_field_patterns directly into their
respective functions to resolve the --warnings-as-errors CI failure.
https://claude.ai/code/session_01Ps7Zq3wiBur1RtRKN7JM6X
- RegexCache: change ETS table from :public to :protected for consistency
with other tables, preventing uncontrolled writes bypassing GenServer
- MobileChannel: ensure_float now returns nil instead of the original
unparsed string on parse failure, so validate_bounds properly rejects it
- map.ts: remove dead commented-out localStorage code
- map.ts: use instanceof HTMLAnchorElement instead of unsafe type cast
in popup navigation handler
- TrailManager: add destroyed flag to prevent stale RAF and debounce
callbacks from firing after destroy, and clean up hover timer on destroy
https://claude.ai/code/session_01Ps7Zq3wiBur1RtRKN7JM6X
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.
Normalize wind direction values to valid range (0-359 degrees) during
packet insertion. Invalid wind direction values (360 or > 360, or
negative) are automatically set to 0, preventing bad data from entering
the database.
This complements the fix in the APRS parser and ensures data integrity
at the application level as well.
Normalize course values to valid range (0-359 degrees) during packet
insertion. Invalid course values (negative or >= 360) are automatically
set to 0, preventing bad data from entering the database.
This complements the fix in the APRS parser and ensures data integrity
at the application level as well.
Added validation to ensure all packets with positions have valid symbols:
- symbol_code must be exactly 1 character, defaults to '>' (car) if missing
- symbol_table_id must be exactly 1 character, defaults to '/' if missing
- Validation only applies to packets with positions
Fixed 5 existing packets with empty symbol codes that would have caused
display issues on the map.
This prevents errors when rendering APRS symbols and ensures all positioned
packets can be displayed correctly.
Added proper latitude/longitude normalization to prevent invalid coordinates:
- Longitude now wraps to -180 to 180 range (e.g., 199.0 -> -161.0)
- Latitude now clamps to -90 to 90 range
- Applied normalization before creating geometry in Packet module
Fixed 7 existing packets with invalid coordinates:
- WB6WWJ-N: 199.0 -> -161.0
- BI7MCK-1: 320.3 -> -39.7
This prevents map display issues and ensures PostGIS spatial queries
work correctly.
Updated get_latest_packet_for_callsign to search by sender, object_name,
or item_name. This allows users to search for objects like "RADIOWRLD"
or "147.06-WC" and have the map center on them, just like searching for
a station callsign.
For example:
- Searching "RADIOWRLD" now finds the GPS Central store object
- Searching "147.06-WC" finds the Calgary repeater object
- Searching "CALGRY" still finds the Calgary station
This makes objects first-class searchable entities on the map.
- 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.
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
- 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
- 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.
System A (PacketManager + PacketStore ETS) had a fatal ID mismatch:
extract_packet_ids returned Ecto UUIDs but PacketStore stored under
MD5 hashes — cleanup could never find stored packets. Unified all
packet tracking on visible_packets map in LiveView assigns.
Deletes packet_manager.ex and packet_store.ex, removes from
supervision tree and PacketDistributor.
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.