Implements automatic connection draining to balance load across cluster nodes:
- ConnectionMonitor tracks CPU usage and connection counts across nodes
- Triggers draining when node is overloaded (>70% CPU or 2x avg connections)
- LiveViews gracefully disconnect and reconnect to less loaded nodes
- Helps prevent single pod from handling all WebSocket connections
Also fixes Mic-E packet parsing to properly extract altitude data and
remove telemetry suffixes from comments.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Fix zero coordinate handling to distinguish between invalid coordinates and valid 0.0 (equator/prime meridian)
- Add to_float_safe() function that preserves nil for proper coordinate validation
- Add comprehensive coordinate validation with descriptive logging in JavaScript
- Make position change threshold configurable via application config (default: 0.001°)
- Add extensive test coverage for edge cases including nil coordinates and equator crossing
- Improve robustness for real-world APRS coordinate edge cases
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Optimize LiveView to only update position data when coordinates actually change (>100m threshold)
- Hide loading spinner after map initialization to prevent flash on updates
- Add position change detection logic with proper nil/Decimal handling
- Add comprehensive test coverage for position change detection
- Maintain smooth map animations and existing functionality
Resolves map flashing and unnecessary reloads when new packets arrive on /info/:call pages.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Handle both DateTime and NaiveDateTime structs from database
- Convert NaiveDateTime to UTC DateTime before formatting
- Add fallback for string timestamps
- Fixes FunctionClauseError when processing packets
Update PostgreSQL notify trigger to send ALL packet fields including:
- Device information (model, vendor, contact, class)
- Weather data fields for weather-enabled stations
- SSID and base_callsign for proper SSID grouping
- data_type for packet type identification
Also added debug logging to verify live updates are working correctly.
This ensures the info page displays complete, real-time information
for all sections including position, device info, nearby stations,
and raw packet data.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Remove Cachex caching from the has_weather_packets function to ensure
real-time data display. The function now queries the database directly
for weather packet information.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Set map height to 100% instead of fixed 280px
- Removed card-body padding to eliminate gap
- Added h-full class to ensure cards fill available height
- Added min-height of 320px for better UX
- Removed redundant styling from map container
The map now properly fills the entire card height and matches
the position information card height on larger screens.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Fixed info page not receiving live updates by implementing callsign-specific PubSub topics
- Added automatic time counter for "Last Position" and other timestamps using JavaScript hook
- Optimized map updates to only move marker instead of reloading entire map
- Fixed APRS parser to properly handle [000/000/A=altitude format in comments
- Removed packet caching on info page for real-time updates
The info page now properly subscribes to updates for the specific callsign being viewed,
timestamps automatically count up without page refresh, and the map smoothly animates
marker position changes instead of flickering with full reloads.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Added debug logging to track packet reception and processing in the
info page LiveView to help diagnose why live updates aren't working.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
When a new deployment occurs in k8s, all connected LiveViews now receive
a notification to update their deployment timestamp display.
Changes:
- Added DeploymentNotifier GenServer that broadcasts deployment events
- LiveViews subscribe to "deployment_events" PubSub topic
- When DEPLOYED_AT env var is set (k8s deployments), a notification is
sent after app startup
- LiveViews handle the deployment event and update their timestamp
This ensures that when a new version is deployed, users see the
deployment timestamp update in real-time without needing to refresh
their browser.
🤖 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>
The 'Last Update' timestamp now correctly shows when the last real-time
packet was received, not when the map bounds changed or historical data
was loaded.
Changes:
- Removed timestamp update when map bounds change (was incorrectly
resetting the timer)
- Added periodic UI update timer (every 30 seconds) to refresh the
'time ago' display so it counts up properly (e.g., "2 minutes ago",
"5 minutes ago", etc.)
- The timestamp only updates when actual real-time packets are received
via PacketProcessor
Now the sidebar will show how long it's been since the last packet was
received, updating every 30 seconds to keep the display current.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Previously, the deployment timestamp was set to DateTime.utc_now() when
the application started, which meant it would always show as recent in
k8s environments where pods are recreated on each deployment.
This commit:
- Updates Release.init() to check for DEPLOYED_AT environment variable
- Falls back to current time if env var is not set (backwards compatible)
- Adds proper ISO8601 timestamp parsing with error handling
- Updates GitHub Actions deploy workflow to set DEPLOYED_AT during deployment
- Creates k8s patch file for future manual deployments
The deployment timestamp will now persist across pod restarts in k8s,
showing the actual deployment time rather than pod startup time.
🤖 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>
- Remove APRS server port from status page server field
- Add comprehensive cluster node list to cluster status section
- Highlight leader node with primary badge and indicator dot
- Display all cluster nodes in a responsive flex layout
- Include all_nodes field in cluster_info for complete node visibility
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add SKIP_DB_CREATE env var to skip database creation when using PgBouncer
- Fix redis_children() to return proper child specs
- Implement zero-downtime graceful shutdown without user notifications
- Add health check plug for Kubernetes probes
- Remove user-facing shutdown notifications for seamless deployments
Co-Authored-By: Claude <noreply@anthropic.com>
- Remove static Redis PubSub configuration from runtime.exs
- Add dynamic pubsub_config/0 function in application.ex
- Configure PubSub adapter based on cluster_enabled and REDIS_URL at startup
- Ensures proper initialization order for distributed deployments
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This adds a "Last Update" section below the navigation that shows:
- Relative time ("less than a minute ago")
- Exact UTC timestamp ("2025-07-25 19:04 UTC")
Updates automatically whenever LiveView receives packet updates or map bounds changes.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Both /status and /status.json endpoints now show cluster-wide APRS-IS
connection status instead of just local node status.
Features:
- get_cluster_aprs_status() checks all nodes in cluster via RPC
- Returns status from any connected node (usually the leader)
- Shows cluster info: total nodes, connected nodes, leader node
- Graceful fallback to local status in non-clustered mode
- Enhanced status page UI with cluster information display
This ensures status pages accurately reflect the true APRS-IS
connection state across the entire cluster.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Added missing CSS for slideover panel with proper width and positioning
- Fixed overflow issues by adding box-sizing: border-box
- Adjusted Track button padding from px-6 to px-4 to prevent cutoff
- Added whitespace-nowrap and flex-shrink-0 to prevent button text wrapping
- Ensured proper panel width of 352px on desktop with transform animations
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Added base CSS styles for slideover panel with proper positioning
- Increased right padding (pr-8) in content area to prevent button cutoff
- Set overflow to hidden on panel container to prevent scrollbar issues
- Ensures Track button and other content are fully visible
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add comprehensive mobile viewport meta tags for better mobile experience
- Implement touch gesture support with long-press to show station info
- Create mobile-specific CSS with touch target optimization (44px minimum)
- Add safe area insets support for notched devices
- Position zoom controls at bottom-right on mobile for thumb accessibility
- Optimize map performance on mobile with canvas renderer
- Fix desktop sidebar toggle button clipping when slideover is open
- Improve responsive design for slideover panels and popups
- Add landscape mode and high DPI screen optimizations
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed issue where stationary stations like K5SGD-D were showing multiple
positions due to minor GPS accuracy variations. The previous 15-meter
threshold was too strict for typical GPS drift which can vary 5-30 meters.
- Increased default threshold in GeoUtils from 10m to 50m
- Updated packet processor to use 50m threshold for movement detection
- Verified with K5SGD-D showing ~24m variations that are now ignored
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This change eliminates all external CDN dependencies by bundling JavaScript
libraries locally using esbuild. All vendor files are now served from the
application's own assets, improving reliability and privacy.
Changes:
- Created assets/js/vendor.js to bundle all external libraries
- Added vendor bundle configuration to esbuild in config files
- Updated all templates to use local vendor.js instead of CDN scripts
- Created download-vendor-files.sh script to fetch vendor libraries
- Added vendor files to .gitignore (they're downloaded during setup)
- Updated CSS imports to include vendor stylesheets
- Added vendor build step to assets.deploy mix task
Bundled libraries:
- Leaflet 1.9.4 (mapping library)
- Leaflet.heat 0.2.0 (heatmap plugin)
- Leaflet.markercluster 1.5.3 (marker clustering)
- OverlappingMarkerSpiderfier 0.2.6 (overlapping marker handling)
- Chart.js 4.5.0 (charting library)
- chartjs-adapter-date-fns 3.0.0 (date adapter for Chart.js)
All functionality tested and working correctly with local bundles.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implemented URL persistence for map settings to preserve user preferences:
- Added `trail` parameter for trail duration (1, 6, 12, 24, 48, 168 hours)
- Added `hist` parameter for historical data hours (1, 3, 6, 12, 24 hours)
- Parameters only appear in URL when different from default value (1 hour)
Changes:
- Updated handle_url_update to include trail and hist parameters
- Modified mount to parse these parameters on page load
- Updated handle_params to parse parameters on URL changes
- Added update_url_with_current_state helper function
- Modified update_trail_duration and update_historical_hours event handlers
to update URL when settings change
- Fixed finalize_mount_assigns to preserve parsed values
This allows users to share or bookmark URLs with specific trail duration
and historical data settings, improving the user experience.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
When changing trail duration and historical data settings, historical positions
would show up on the map but trail lines wouldn't connect them. This was caused
by a mismatch between trail ID generation and callsign extraction.
## Changes
- Fix getTrailId() to properly extract base callsigns from historical marker IDs
- Add setTrailDuration() method to TrailManager for dynamic duration updates
- Add LiveView event to synchronize trail duration changes with client
- Add comprehensive tests for historical ID handling
## Problem
Historical markers have IDs like "hist_CALLSIGN_123" but getTrailId() was
returning the full ID while TrailManager.extractBaseCallsign() extracted just
"CALLSIGN", creating separate trails instead of connected ones.
## Solution
Updated getTrailId() to extract base callsigns from historical IDs using the
same logic as TrailManager, ensuring all positions from the same station use
the same trail ID and connect properly.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Removed three logger.info statements that were cluttering the logs:
- "RF path hover start - path: ..."
- "Parsed path stations: ..."
- "Found x station positions"
These were debug statements that are no longer needed in production.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
The get_other_ssids function wasn't fetching symbol information from
the database, causing icons to display incorrectly. Updated the query
to include symbol_table_id and symbol_code fields and added them to
the packet struct so render_symbol_html can properly display the icons.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Fix "this.__view is undefined" error when calling pushEvent in event handlers
- Bind pushEvent context properly in all marker click and hover handlers
- Replace undefined sendBoundsToServer calls with saveMapState
- Ensure all event handlers check for pushEvent availability and type
- Use .bind(self) to preserve LiveView hook context in callbacks
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add explicit import for Phoenix.HTML to ensure raw/1 function is available.
This helps prevent 'view crashed - undefined' errors that may occur in
certain environments.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Change from {render_symbol_html(...)} to <%= render_symbol_html(...) %>
in three places where APRS symbols are rendered. This fixes the
'view crashed - undefined' error on the info page.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Reduce map height from 400px to 280px to better match position info card
- Add Leaflet CSS/JS includes for map functionality
- Reorganize layout: Position Info and map on top row
- Move Device Info and Other SSIDs to second row in a grid
- Make Other SSIDs table more compact with table-sm class
- Simplify last heard display to show only time ago (remove timestamp)
- Add empty placeholder div to maintain grid layout when no other SSIDs
- Improve responsive layout with lg:col-span-2 and nested grids
The layout now makes better use of horizontal space on larger screens
while maintaining mobile responsiveness.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Increase debounce time from 300ms to 400ms for bounds updates
- Cancel in-progress historical loading when new bounds are scheduled
- Add stale update detection to skip outdated bounds updates
- Clear timer reference and pending bounds when processing updates
- Add debug logging for cancelled loads and stale updates
This prevents overlapping historical data loads during rapid zoom
changes, eliminating duplicate packets and improving performance.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
The function was sending a message but not returning the socket,
causing an ArgumentError when the result was passed to assign/3.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add missing :historical_packets initialization to prevent KeyError when
HistoricalLoader tries to access it during zoom-based display handling.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Remove duplicate received_at indexes (packets_received_at_asc_idx and packets_recent_hour_idx)
that were consuming 423 MB, keeping only packets_received_at_idx
- Fix KeyError by initializing missing :all_packets and :visible_packets in socket assigns
- All tests now pass successfully
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Renamed predicate functions to follow Elixir conventions:
- is_finite? → finite?
- is_finite_number? → finite_number?
- is_finite_float? → finite_float?
- Fixed all function returns to ensure proper nil handling
- Applied mix format to all files
- All tests pass with no compilation warnings
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Security improvements:
- Added sanitize_numeric_string to remove dangerous characters
- Limited input string length to prevent DoS attacks (20 chars for numbers)
- Added is_finite? checks to prevent infinity/NaN values
- Validate coordinate ranges (lat: -90 to 90, lng: -180 to 180)
- Added safe_parse_coordinate with proper validation
- Updated to_float in EncodingUtils with security validations
- Protected against integer overflow in coordinate conversions
- Added sanitize_path_string for APRS path validation
All coordinate inputs from users are now:
1. Sanitized to remove injection characters
2. Length-limited to prevent resource exhaustion
3. Validated for finite values (no infinity/NaN)
4. Checked against valid geographic ranges
5. Given safe fallback defaults
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Implemented dual-strategy pruning based on overage size
- Small overages (<10 packets): Only remove exactly what's needed
- Large overages: Batch prune with 20% buffer to reduce frequency
- Used Map.drop for more efficient bulk deletion
- Added Stream for lazy evaluation in small overage case
- Reduces complexity from O(n log n) on every insertion to:
- O(k log k) for small overages where k << n
- Amortized O(n log n) for large overages with buffering
- This significantly improves performance when managing thousands of packets
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>