Commit graph

643 commits

Author SHA1 Message Date
2fc4706d3d
fix: stop silent packet loss, timer leak, and seq-scans
- 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.
2026-04-21 09:29:06 -05:00
31ea64aa6c
perf: reduce per-packet allocations in ingest pipeline
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.
2026-04-19 13:33:16 -05:00
5ab2ffc8f3
perf: replace per-row pg_notify trigger with elixir broadcast after batch insert 2026-04-17 13:46:51 -05:00
1788419ae0
perf: drop redundant packet indexes, compute has_weather in elixir, tighten on_conflict target 2026-04-17 13:42:19 -05:00
4854157ee9
feat: add sitemap.xml, api-catalog, and agent-discovery Link headers 2026-04-17 13:31:21 -05:00
0b178d45ba
fix: unsubscribe from pubsub topics on LiveView terminate; catch-all handle_info in ConnectionManager 2026-04-17 13:24:58 -05:00
ab29a3dca5
fix: add handle_info/2 for distributed_packet messages in PacketDistributor 2026-04-17 13:16:46 -05:00
0c50208e4f
fix: use string interpolation for SET statement_timeout instead of parameterized query 2026-04-17 13:08:25 -05:00
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
4847def0d8
Add referrer meta tag to fix OSM tile 403 errors
OSM tile servers require a Referer header. Cloudflare appears to be
stripping the Referrer-Policy HTTP header, so add a meta tag that
the browser will always respect.
2026-03-22 16:34:32 -05:00
a9cf2d3eb8
Add logging to map location determination for debugging 2026-03-22 16:27:43 -05:00
c769d89264
fix: stabilize packet broadcast tests 2026-03-22 16:17:55 -05:00
139568cd05
Log Cloudflare geolocation headers on each request 2026-03-22 16:08:52 -05:00
6988116a42
Fix Cloudflare IP geolocation not reaching LiveView on page load
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.
2026-03-22 16:08:00 -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
6d63638312
Remove dead code - unused controller actions
All pages are already LiveView, so removed:
- InfoController (replaced by InfoLive.Show)
- PageController.home and .packets (unused functions)

Remaining PageController functions are health/status JSON endpoints only.
All user-facing pages use LiveView with proper session continuity.
2026-03-22 13:11:08 -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
544d8648a0
Fix object/item packets showing sender callsign instead of object name
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.
2026-03-22 13:02:24 -05:00
732ea61a2e
docs: add weather nearby API endpoint to documentation
Adds comprehensive documentation for GET /api/v1/weather/nearby endpoint:
- Parameter descriptions (lat, lon, radius, hours, limit)
- Example requests with curl
- Success and error response formats
- Updated planned endpoints section
2026-03-22 12:16:32 -05:00
a796f8a3a9
Add Weather API controller with parameter validation
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.
2026-03-22 11:27:45 -05:00
e539dc5a3e
Add WeatherJSON view module for API responses
- 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)
2026-03-22 11:27:45 -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
018a9075ab
fix(ios): improve WebSocket stability on iPhone Safari
Increase long-poll fallback timeout to 5s for mobile networks, add
visibility change listener for proper reconnection on app foreground,
and remove backdrop blur that stalls network operations on iOS Safari.
2026-03-11 10:58:16 -05:00
6072eebe5b
vendor heroicons SVGs, remove GitHub dependency
Only the 7 SVGs actually used are vendored into priv/heroicons/.
Also fixes the icon component path resolution (was missing the 24/
size directory) and corrects hero-x-mark/hero-map-pin icon names
that included an erroneous hero- prefix.
2026-03-03 13:34:23 -06: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
9a071b6e3a
feat: refresh trail line when trail duration changes
- Regenerate trail line after trail duration update
- Trail line now respects trail duration setting
2026-02-27 13:49:18 -06:00
464ba84202
feat: clear trail line when clearing callsign tracking
- Push clear_trail_line event when user clears tracking
- Ensures trail line is removed immediately
2026-02-27 13:38:20 -06:00
2919402aa9
feat: show trail line for tracked callsigns at low zoom
- 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
2026-02-27 13:21:37 -06:00
6add35b11a
feat: add send_trail_line_for_tracked_callsign function
- 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
2026-02-27 13:10:34 -06:00
f4db38f051
update 2026-02-25 16:29:45 -06:00
d385d1eb75
Default sidebar to open on desktop via CSS
Make the map and panel CSS default to "sidebar open" on desktop
(>= 1024px) without requiring the slideover-open class. Only the
slideover-closed class now hides the sidebar. This prevents a blank
gap when the map and panel states get out of sync after LiveView
reconnects.
2026-02-25 12:25:50 -06:00