Convert app.js, info_map.js, error_boundary.js, and time_ago_hook.js
to TypeScript with proper typing for Phoenix hooks, Leaflet, DOM
interactions, and timer management. Update esbuild hex package to
~> 0.10 and binary to 0.25.4. Entry point changed to app.ts.
- Remove CSS `bottom: 50px !important` on popups that pushed them too
high above markers
- Hide trails when markers are clustered (zoom < 11) so trails don't
point to invisible markers inside cluster bubbles
- Fix locate button overlap with zoom controls and keep it always
light mode
- Remove duplicate slideover/toggle CSS from layout files (single
source of truth in component)
- Remove dead CSS classes and standardize breakpoints to 1024px
- Add isDestroyed guards on async callbacks to prevent race conditions
- Escape callsigns in marker HTML to prevent XSS
- Replace Leaflet _container private API with getContainer()
- Clean up about page placeholder text
Replace trail color palette with bold, high-saturation colors that avoid
yellows, oranges, and grays (common road colors on map tiles). Interleave
warm/cool tones so consecutive color assignments are maximally distinct.
Change color assignment to prefer globally unique colors first, falling
back to proximity-based deconfliction only when all 15 colors are in use.
- 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).
Historical loading overrides all markers to historical:true, including
the most-recent marker. When a new live packet arrives, the conversion
check only looked for non-historical markers, missing the existing
"current" marker. Broaden the check to also convert markers with
is_most_recent_for_callsign set.
Use mousemove instead of mouseover so the RF path updates as the
cursor moves along the trail. Send the mouse position rather than
the nearest-position's coordinates so the path line anchors where
the user is actually hovering. Debounce and deduplicate events to
avoid flooding the server.
The slideover toggle button now uses a fixed light theme (white bg,
dark arrow) so it's always visible on the map regardless of OS dark
mode. Removed prefers-color-scheme overrides since the button overlays
the always-light map tiles.
Fixed map not resizing when closing the sidebar — the #aprs-map div
has phx-update="ignore" so server-rendered classes never updated.
Now uses LiveView JS commands (JS.toggle_class + JS.dispatch) to
toggle classes client-side and trigger map.invalidateSize(), replacing
the custom push_event/handleEvent JS handler.
parse_rf_path was filtering out all paths containing qA* constructs,
which is virtually every APRS-IS packet. The qA* element delimits the
RF path from the APRS-IS metadata — it should be split on, not
discarded. Now properly extracts digipeaters (before qA*) and the
igate (after qA*), while filtering APRS aliases like WIDE/RELAY/TRACE.
Also adds hover handlers to trail polylines so hovering anywhere on a
call's track shows the RF path, not just on the dot markers.
- 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
Replace all DaisyUI component classes with pure Tailwind CSS utility
classes following TailwindUI conventions. Use dark: variants for dark
mode instead of DaisyUI's data-theme semantic color system.
- Remove DaisyUI plugin and theme vendor files
- Configure Tailwind v4 custom dark variant for data-theme attribute
- Migrate all components: cards, tables, badges, buttons, forms,
navigation, dropdowns, modals, alerts, star ratings, spinners
- Update all auth pages (login, register, settings, password reset,
confirmation) to TailwindUI card layout
- Update content pages (about, API docs, status, packets, bad packets,
info, weather) to TailwindUI patterns
- Replace opacity-based text styling with semantic gray colors
- Update test assertions to match new class names
- Remove BodyClassHook: use CSS :has() selectors on data-map-page attribute
instead of JS toggling body.map-page class
- Remove ResponsiveSlideoverHook: pass viewport_width via LiveSocket connect
params and determine initial slideover state server-side
- Convert theme selector from data-set-theme attributes with DOMContentLoaded
listeners to JS.dispatch("phx:set-theme") with a single event handler
- Fix buggy reRenderAllCharts that incorrectly used `new MapAPRSMap()` as a
Map constructor; charts now re-render via themeChanged event directly
Widen nearbyDistance, spiderfyDistanceMultiplier, and foot separation
to accommodate 120x32px marker labels that were overlapping when
spidered at close zoom levels.
- Add minimum movement threshold (100m) to skip stationary stations
- Add max hop distance (10km) to break trails at igate-hopping artifacts
- Require 3+ points per segment to filter noise from 2-point jumps
Add isNaN() checks in addition to isFinite() to catch NaN values before
they reach Leaflet's polyline renderer. This prevents 'can't access
property x, h is undefined' errors when invalid coordinates slip through.
Generated with Claude Code https://claude.com/claude-code
- Add extractCoordinate helper to recursively extract coordinates
- Handle coordinates that come as nested arrays like [[lat]]
- Log raw coordinate values when validation fails for debugging
- Prevents Invalid coordinates errors in console
This handles edge cases where coordinates are wrapped in arrays
before being passed to the marker creation code.
- Check if data object exists before accessing properties
- Validate lat/lng are numbers, not just truthy values
- Prevents TypeError when clustering tries to access undefined coordinates
- Fixes: Error adding historical packet: TypeError: can't access property x of undefined
- Set websocket timeout to 60000ms in endpoint configuration
- Add timeout option to LiveSocket client configuration
- Fixes timeout errors during initial map load with large packet count
- Remove duplicate time formatting function in components.ex
- Use existing time_ago_in_words from TimeHelpers instead of custom format_time_ago
- Remove unnecessary Float multiplication in format_coordinates
- Changed Float.round(lat * 1.0, 4) to Float.round(lat, 4)
- Fix TypeScript type safety issues
- Add pendingMarkers to LiveViewHookContext interface
- Add initializationTimeout property for proper cleanup
- Add cleanup for map initialization retry timeout
- Prevent memory leak by clearing timeout in destroyed() method
- Format code with mix format
All changes maintain backward compatibility while improving code quality.
- Check if markerClusterGroup is ready before removing markers
- Verify _map and _topClusterLevel exist in cluster group
- Add try-catch blocks to handle removal errors gracefully
- Clean up tracking maps even if layer removal fails
- Prevents 'can't access property "_childClusters"' errors
- Add marker queuing system for markers that arrive before map is ready
- Check map._container and getZoom to ensure internal structures exist
- Process pending markers after map.whenReady() callback
- Fix typo: bindTooltip -> bindTooltip
The errorTileUrl was set to a base64 data URL of a transparent 1x1 PNG,
which caused browsers to fail when trying to use it as a tile replacement.
Removing this option allows Leaflet to handle tile errors with its
default behavior, which is more robust and prevents console errors.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Combine Chart.js and date adapter into single bundle to ensure proper loading order
- Add validation to skip chart rendering when insufficient weather data (< 2 points)
- Update vendor loader to not load date adapter separately
- Prevents date adapter errors when weather stations have limited data
The function was being called before it was defined, causing a runtime error.
Moving the function definition before the whenReady callback ensures it's
available when the callback executes.
- Add clearRfPathLines() method for centralized cleanup logic
- Clear RF paths when clearing all markers
- Clear RF paths in destroyed() lifecycle hook
- Add error handling to prevent failures during cleanup
- Fix issue where path lines could persist after hover ends
- Add DOM existence check before reinitializing map
- Implement initialization flag to prevent race conditions
- Improve cleanup in destroyed() to prevent memory leaks
- Map now only updates marker position when location changes
- Added sendBoundsToServer() call after map_ready event
- Previously only saveMapState was called which doesn't send bounds
- This ensures historical packets are loaded on initial page load
- Also added to retry logic for reliability
The issue was that bounds_changed event was never triggered on initial load, preventing historical packets from being fetched.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Increased spiderfyDistanceMultiplier from 2 to 3.5
- Provides much better separation when overlapping markers are clicked
- Markers now spread out 3.5x the default distance for easier clicking
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Reduced nearbyDistance from 100 to 20 pixels to only spider truly overlapping markers
- Added spiderfyDistanceMultiplier: 2 to increase separation when markers do spider
- Added leg color configuration for better visibility
- Fixes issue where distant markers like K5FDT-R were incorrectly grouped with AE5PL stations
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Increased nearbyDistance from 60 to 100 pixels for maximum clickability
- Increased circleSpiralSwitchover from 12 to 15 markers
- Provides even better separation when clicking on densely packed APRS packets
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Increased nearbyDistance from 40 to 60 pixels for better clickability
- Increased circleSpiralSwitchover from 9 to 12 markers before switching to spiral layout
- Improves user experience when clicking on overlapping APRS packets
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
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>
Improves robustness by adding fallback logic when is_most_recent_for_callsign
is undefined or null. In edge cases where this server-side property might not
be reliably set, falls back to the previous behavior of excluding historical
markers using the _isHistorical flag.
Changes:
- Added explicit boolean checks for is_most_recent_for_callsign === true
- Added fallback logic: if property is null/undefined, use \!_isHistorical
- Applied to both addMarker() and zoom handler functions
- Ensures spidering works even if server-side property is inconsistent
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
The OverlappingMarkerSpiderfier (OMS) was incorrectly configured to exclude
historical markers but wasn't properly filtering for only the most recent
packets with icons. This caused spidering functionality to break.
Changes:
- Updated addMarker() to check data.is_most_recent_for_callsign instead of \!_isHistorical
- Updated zoom handler to check markerState.is_most_recent_for_callsign when re-adding markers to OMS
- Ensures only current position markers (with APRS icons) participate in spidering
- Historical markers (red dots) are excluded from spidering as intended
🤖 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 workaround to clear the longpoll fallback timer after WebSocket
connection is established. The issue was causing hundreds of unnecessary
longpoll requests per second even when WebSocket was working properly.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>