Commit graph

258 commits

Author SHA1 Message Date
adaf9599e2
chore: update dependencies and fix pre-existing issues
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
2026-04-15 14:07:29 -05:00
Claude
51d5107983
fix: inline regex module attributes to fix deprecation warning
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
2026-03-27 16:18:27 -05:00
Claude
a8051ce247
fix: code review refinements
- 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
2026-03-27 16:18:27 -05:00
2300dcd69c
fix: stabilize moving station tracking 2026-03-26 18:48:43 -05:00
bc82777d34
fix: close review findings 2026-03-26 12:36:50 -05:00
288b9fbbb2
fix: address security vulnerabilities and concurrency issues
- Fix SQL injection in partition_manager, db_optimizer, and release.ex
- Fix XSS vulnerabilities with proper HTML escaping in LiveViews
- Add proper error handling for email delivery functions
- Fix race conditions with advisory locks and atomic operations
- Replace unsupervised spawn/Task.start with supervised alternatives
- Convert ETS operations to GenServer serialization for thread safety
- Change ETS tables from :public to :protected access
- Add client limits to prevent unbounded memory growth
- Add PubSub cleanup in GenServer terminate callbacks
- Fix device upsert to use atomic Repo.insert_all
2026-03-23 16:59:09 -05:00
d234c313ca
fix: harden mobile channel numeric params 2026-03-22 17:36:09 -05:00
35962c195b
fix: guard device cache refresh when stopped 2026-03-22 17:31:54 -05:00
21d28afe8e
refactor: reduce map live complexity 2026-03-22 17:20:47 -05:00
f840e8fd34
fix: guard APRS send_message when disconnected 2026-03-22 17:16:44 -05:00
4d45c8351a
fix: guard pubsub APIs when not running 2026-03-22 17:14:24 -05:00
7c0861f216
fix: replace duplicate spatial registrations 2026-03-22 17:12:20 -05:00
1631352786
fix: guard connection monitor stats lookup 2026-03-22 17:09:07 -05:00
c769d89264
fix: stabilize packet broadcast tests 2026-03-22 16:17:55 -05:00
8937c1fb57
fix: handle APRS status DB ownership exits 2026-03-22 16:02:03 -05:00
0d4314b880
Fix object positioning to use object name instead of origin callsign
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.
2026-03-22 15:19:52 -05:00
0c9087b7f1
Add email notifications for production errors
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.
2026-03-22 15:12:59 -05:00
b5c9388399
fix: add wind direction normalization in changeset
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.
2026-03-22 13:50:06 -05:00
ca9bcf21a7
fix: add course value normalization in changeset
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.
2026-03-22 13:44:51 -05:00
ae8f2acc48
Fix symbol validation - prevent empty symbol codes
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.
2026-03-22 13:28:03 -05:00
57f243d5b6
Fix coordinate normalization bug
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.
2026-03-22 13:15:57 -05:00
fdf116971d
Allow searching for objects and items by name
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.
2026-03-22 13:07:37 -05:00
47fc071263
Add get_nearby_weather_stations/4 query function with spatial filtering
Implements PostGIS-based query to find weather stations within a radius:
- Uses ST_DWithin for efficient spatial filtering within radius (miles to meters)
- Orders results by ST_Distance (closest first)
- Filters by has_weather, has_position, and time window (default 6h)
- Deduplicates by base_callsign using DISTINCT ON (avoids duplicate SSIDs)
- Returns all weather fields: temp, humidity, pressure, wind, rain
- Configurable limit (default 50) and hours options

Comprehensive test coverage (10 tests):
- Spatial filtering within/outside radius
- Time window filtering
- Base callsign deduplication (most recent SSID)
- Distance ordering verification
- Required field validation
- Edge cases (empty results, no weather data)
2026-03-22 11:27:45 -05:00
ac42896271
fix: performance improvements and bug fixes across backend and frontend
Backend:
- Use indexed has_weather boolean for weather queries instead of 6 OR conditions
- Enable gzip compression for static assets
- Re-enable security headers (CSP, X-Frame-Options, X-Content-Type-Options)
- Supervise cleanup task via BroadcastTaskSupervisor instead of Task.start
- Relax IsSupervisor restart limits (5/60s) to survive transient network issues
- Remove dead code: empty cache invalidation block, commented-out logging

Frontend:
- Use callsignIndex for O(1) marker dedup instead of O(n) forEach scan
- Consolidate triple packet loop into single pass in new_packets handler
- Batch trail visualization updates via requestAnimationFrame
- Replace DOM querySelectorAll z-index scan with simple counter
- Only rebuild OMS markers when crossing clustering zoom threshold
- Clean up trail manager on hook destroy
- Remove dead commented-out localStorage code
2026-03-19 12:41:36 -05:00
7897fe8a1a
perf: optimize slow tests - 40% reduction in top 10 slowest tests
- Skip expensive scheduler utilization sampling in test env (saves 1s per call)
- Reduce Process.sleep times from 100-300ms to 50-100ms
- Reduce refute_receive timeouts from 500ms to 100ms
- GPS drift test: 1371ms → 431ms (68.5% faster)
- BroadcastTaskSupervisor tests removed from slowest 10 (1000ms+ saved each)
- PacketDistributor tests: 33% faster
- StreamingPacketsPubSub tests removed from slowest 10 (400ms+ saved each)

Top 10 slowest tests reduced from 6.8s to 4.1s (40% improvement)
2026-03-03 10:47:40 -06:00
b45993a8be
Fix APRS object display and comment encoding issues
- 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.
2026-02-20 18:06:32 -06:00
bcb00801ee
Remove noisy leader election log
The "Not elected as leader" info log was being emitted every 5 seconds
by non-leader nodes, filling up logs unnecessarily in clustered deployments.
2026-02-20 13:43:13 -06:00
50544c8354
Remove dead PostGIS code, deduplicate utilities, and simplify module hierarchy
- Delete ~170 lines of commented-out PostGIS spatial query functions
- Consolidate coordinate validation into CoordinateUtils
- Replace private normalize_data_type with EncodingUtils delegation
- Inline to_float/to_decimal wrappers at call sites
- Fix has_weather_data? missing wind_gust field
- Remove BoundsManager passthrough delegation module
- Simplify MapLive.PacketUtils by removing pure-passthrough delegations
2026-02-20 13:39:07 -06:00
673eb55e97
Add test coverage for comment cleaning edge cases
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.
2026-02-20 13:17:27 -06:00
ddf26a99ce
Strip weather data, RNG, and negative altitudes from packet 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.
2026-02-20 13:15:19 -06:00
ca05b497b9
Consolidate 24 packet columns into JSONB data field
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.
2026-02-20 13:02:47 -06:00
b08eba2c6b
Partition packets table by day and simplify cleanup
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
2026-02-20 11:25:16 -06:00
d09e015857
Fix Credo issues and telemetry Decimal coercion
- 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
2026-02-20 09:55:56 -06:00
84e65232e5
Batch RF path queries, batch marker removals, and JS callsign index
- 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
2026-02-20 09:04:22 -06:00
b4bbfda620
Move client-side logic to server-side LiveView
- 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
2026-02-20 08:54:34 -06:00
dcabb744df
Batch push_events, RF path queries, and ETS tuning for hot 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).
2026-02-20 08:30:54 -06:00
781fdd88a3
Remove dead modules, unused deps, and commented-out code
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.
2026-02-20 08:09:22 -06:00
4153f66b86
Remove broken dual packet state system (PacketManager/PacketStore)
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.
2026-02-20 07:47:39 -06:00
8755bba9f0
Optimize historical packet loading by selecting only needed columns
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.
2026-02-20 07:41:14 -06:00
bcb3556bf6
Use PostGIS GiST index for spatial bounds queries 2026-02-20 07:29:27 -06:00
6c2b664333
Fix code review batch 3: auth, performance, dedup, cache TTL 2026-02-20 07:29:27 -06:00
7b488e9fd7
Fix code review findings batch 2: dead code, encoding, correctness
- Delete dead modules: Archiver, PacketPipelineSetup, SystemMonitor
- Encoding: fast-path for clean ASCII, fix latin1 C1 control chars (128-159)
- SpatialPubSub: fix date line grid cell wrapping
- QueryBuilder: use indexed has_weather column instead of 10-way OR
- PacketProcessor/HistoricalLoader: wire up heat map via DisplayManager
- MapLive: add PacketBatcher crash recovery with Process.monitor
- MapLive: fix time display re-render by touching assign
- DeviceCache: remove unreachable outer try/rescue
- Remove dead stubs: RateLimiterWrapper count/reset, DeviceIdentification enqueue_refresh_job
- Simplify Application.pubsub_config/0
- Add tests for SpatialPubSub, PacketBatcher, encoding, weather_only
2026-02-20 07:29:27 -06:00
d9c04f7e0c
Fix code review findings: security, correctness, performance, dead code
Security:
- Add SQL identifier validation to DbOptimizer.analyze_table/vacuum_table
- Move health/readiness routes to non-rate-limited pipeline

Correctness:
- Fix monitor reference leak in StreamingPacketsPubSub (track refs, demonitor on unsubscribe)
- Persist circuit breaker half-open state transitions
- Simplify ShutdownHandler.terminate to avoid pointless Process.send_after

Performance:
- Batch marker removal into single WebSocket event (remove_markers_batch)
- Use Task.Supervisor.start_child for fire-and-forget broadcasts

Dead code removal:
- Remove copy_insert/regular_batch_insert/rows_to_csv from DbOptimizer
- Remove dead packet_pipeline_integration tests
- Remove unused telemetry event detach

Other:
- Fix Callsign.valid? docstring to match permissive behavior
- Fix DatabaseMetrics to delegate instead of hardcoding pool stats
- Add circuit breaker and encoding test coverage
2026-02-19 18:49:27 -06:00
118a55b26e
Fix APRS object name extraction for names with special characters
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.
2026-02-19 18:41:37 -06:00
ac51ecaada
Fix multiple TODO bugs: drain handler, callsign dedup, packet pipeline
- Fix drain_connections missing {:noreply, socket} wrapper
- Fix get_callsign_key to match atom keys from Ecto structs
- Add SpatialPubSub.broadcast_packet to PacketDistributor
- Fix pid_alive? treating {:badrpc, _} as truthy
- Fix SpatialPubSub ensure_float crash on integer strings
- Fix inverted memory calculation in ConnectionMonitor
- Remove dead SSL handler from Is module, consolidate into handle_socket_data
- Remove duplicate packet transformation in Is.dispatch
- Remove invalid placeholders option from insert_all calls
- Change PacketPipelineSupervisor to :rest_for_one strategy
- Update TODO.md marking fixed items
2026-02-19 18:27:25 -06:00
753fa50463
Fix ConnectionMonitor deadlock and LeaderElection silent leadership loss
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.
2026-02-19 18:18:31 -06:00
66061a5abe
Fix leader election: prevent self-deregistration on re-election
:global.register_name returns :no even when the name is already
registered to the calling PID. This caused the leader to clear its
own leadership state every 5 seconds after cluster formation triggered
a redundant re-election attempt.

Now checks if we already hold the registration before attempting to
re-register, preserving leadership state across cluster topology changes.
2026-02-19 17:21:26 -06:00
fa89c6d3ca
Add TCP backpressure to packet pipeline
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.
2026-02-19 15:31:51 -06:00
871620223d
Performance improvements, code cleanup, remove sobelow
- 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
2026-02-19 14:51:01 -06:00
509f271eac
Improve packet pipeline reliability, security, and cleanup efficiency
- Fix XSS vulnerability in buildPopupContent fallback (escape callsign/comment)
- Add batch insert retry with individual fallback in PacketConsumer
- Add APRS-IS login response validation (logresp dispatch handler)
- Fix stale generation check bypass in HistoricalLoader
- Replace two-step SELECT+DELETE cleanup with single CTE-based batch DELETE
- Change default packet retention from 365 to 7 days
- Delete MapHelpers (100% duplicate of CoordinateUtils + BoundsUtils)
- Delete AprsIsConnection (dead code, second unused APRS-IS connection)
- Fix InfoMap hook memory leak (store/cancel setTimeout ref)
- Extract singleton loadMapBundle with callback queue in app.js
- Extract @heat_map_max_zoom constant in DisplayManager
- Add telemetry to cleanup worker and PacketProducer buffer overflow
- Track PacketProducer buffer_size as integer for O(1) length checks
- Remove duplicate bounds functions from index.ex
2026-02-19 14:11:48 -06:00