The coord-validation and antimeridian-unwrap logic added in the last
pass had been duplicated across map.ts, trail_manager.ts, and
info_map.ts. Move both into map_helpers.ts and import from there so
there is one authoritative implementation.
- Heatmap: filter points with invalid coords or out-of-range intensity
so malformed rows can't distort the layer.
- InfoMap: validate coords for range (not just NaN) and use an epsilon
when comparing positions, so float noise doesn't cause needless
re-renders or accept bogus data.
- TrailManager: getTrailCenter returns null for empty input instead of
the ambiguous (0, 0) null-island; callers guard accordingly.
- Normalize longitude delta in trail_manager Haversine so wraps across
180/-180 no longer compute a near-global distance and spuriously
split the trail into short-segment-filtered pieces.
- Unwrap longitudes before building segments and drawing polylines in
both the per-station TrailManager and the show_trail_line handler so
a date-line crossing renders as a short hop instead of a line that
wraps around the world.
- Raise maxHopDistance (10 -> 100 km) and drop minSegmentPoints (3 -> 2)
so legitimate fast movers (aircraft, balloons) and short valid hops
are drawn instead of being dropped.
- Unbind mousemove/mouseout handlers on a trail before removing it so
queued events can't fire against discarded state.
- Fix self.isValidCoordinate TypeError in updateMarker (function is
module-level, not on the hook) and add coord validation to
show_trail_line.
- Null boundsTimer after clearTimeout for cleanup consistency.
- 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
- 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.
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.
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.
- 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
- 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
- Added check for Chart.js availability before rendering
- Updated chart hooks to wait for Chart.js to load completely
- Prevents 'window.Chart is not a constructor' errors
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixed the esbuild errors during Docker build where npm packages couldn't
be resolved. The issue was caused by node_modules being excluded from
the Docker build context in .dockerignore.
Implemented a proper vendor bundle strategy following Phoenix 1.7+ patterns:
- Created vendor.js that bundles all npm dependencies (Leaflet, Chart.js,
topbar) and their CSS files into a single pre-built bundle
- Added vendor esbuild profile in config.exs with proper CSS/image loaders
- Updated mix.exs to build vendor bundle before app bundle in assets.deploy
- Modified JavaScript imports to use globally loaded libraries from vendor
bundle instead of importing from node_modules
- Added vendor.js script tag to root.html.heex before app.js
This approach avoids Docker build failures by pre-bundling all vendor
dependencies, eliminating the need for node_modules during production builds.
🤖 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>
- Add comprehensive type definitions for Leaflet plugins (heatmap, marker clustering, overlapping marker spiderfier)
- Create event payload interfaces for all LiveView events
- Add marker extension types for APRS-specific properties
- Define Chart.js dataset and configuration types
- Replace 74 instances of 'any' type across all TypeScript files
- Improve type safety for event handlers and state management
- Add proper typing for external library integrations
This refactoring enhances type safety throughout the codebase, making it easier to catch errors at compile time and improving IDE support for autocompletion and refactoring.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>