updates with parser updates
This commit is contained in:
parent
4e00c3cf5d
commit
9b207e0d67
17 changed files with 317 additions and 324 deletions
|
|
@ -1,84 +0,0 @@
|
||||||
# Claude Session Summary - August 2, 2025
|
|
||||||
|
|
||||||
## Session Overview
|
|
||||||
This session focused on updating the APRS.me codebase to handle improved APRS parser features based on updates from the vendor/aprs library.
|
|
||||||
|
|
||||||
## Completed Tasks
|
|
||||||
|
|
||||||
### 1. Enhanced APRS Parser Integration
|
|
||||||
- **Initial Request**: "the parser has been improved again with these updates, please update the code to handle them"
|
|
||||||
- **Parser Improvements Implemented**:
|
|
||||||
- Added 11 new standard parser fields for better compatibility
|
|
||||||
- Implemented comprehensive weather data extraction with dedicated wx field
|
|
||||||
- Fixed PHG parsing to return string format instead of map structure
|
|
||||||
- Added radiorange (RNG) field parsing from comments
|
|
||||||
- Enhanced comment processing with proper data extraction and cleaning
|
|
||||||
- Improved compressed position parsing with APRS messaging capability
|
|
||||||
|
|
||||||
### 2. Database Schema Updates
|
|
||||||
- Created migration `20250801231447_add_enhanced_parser_fields.exs`
|
|
||||||
- Added 20 new fields to packets table:
|
|
||||||
- Standard parser fields: srccallsign, dstcallsign, body, origpacket, header, alive, posambiguity, symboltable, symbolcode, messaging
|
|
||||||
- Radio range field: radiorange
|
|
||||||
- Weather fields: rain_midnight, has_weather (some already existed)
|
|
||||||
|
|
||||||
### 3. Code Updates in lib/aprsme/packet.ex
|
|
||||||
- Added `put_standard_parser_fields/2` function to extract new parser compatibility fields
|
|
||||||
- Added `put_radio_range_field/2` function for radiorange extraction
|
|
||||||
- Enhanced `extract_weather_data/2` to prioritize new wx field from improved parser
|
|
||||||
- Updated `put_phg_fields/2` to handle both string format ("1060") and legacy map structure
|
|
||||||
- Added `parse_phg_string/2` for parsing 4-character PHG strings
|
|
||||||
|
|
||||||
### 4. Comprehensive Testing
|
|
||||||
- Created `test/aprsme/enhanced_parser_test.exs` with 6 tests
|
|
||||||
- Tests cover:
|
|
||||||
- Standard parser field extraction
|
|
||||||
- Radio range field extraction
|
|
||||||
- Weather data from wx field
|
|
||||||
- PHG data in both string and map formats
|
|
||||||
- Full packet storage with enhanced fields
|
|
||||||
- All 467 tests passing with 0 failures
|
|
||||||
|
|
||||||
### 5. Previous Work in Session
|
|
||||||
Before the parser update task, the session included:
|
|
||||||
- Improving test coverage from 41.62% to meet 90% threshold
|
|
||||||
- Writing comprehensive test suites for Aprsme.Packets, Cluster.LeaderElection, and Cluster.ConnectionManager modules
|
|
||||||
- Fixing production issues including:
|
|
||||||
- Buffer full errors (actually PacketConsumer crashes)
|
|
||||||
- Telemetry_vals data type mismatches
|
|
||||||
- Missing position_ambiguity column
|
|
||||||
- Optimizing test suite performance (48% speed improvement)
|
|
||||||
- Multiple git commits and pushes throughout
|
|
||||||
|
|
||||||
## Key Technical Details
|
|
||||||
|
|
||||||
### Parser Field Mappings
|
|
||||||
The enhanced parser now provides these additional fields that are mapped to database columns:
|
|
||||||
- `posambiguity` → position_ambiguity (0-4 level indicator)
|
|
||||||
- `wx` → dedicated weather data field (prioritized over other weather fields)
|
|
||||||
- `radiorange` → RNG field from comments (e.g., "RNG0050")
|
|
||||||
- PHG data now comes as string "1060" instead of map structure
|
|
||||||
|
|
||||||
### Backward Compatibility
|
|
||||||
All changes maintain backward compatibility:
|
|
||||||
- Weather extraction checks multiple field locations (wx, weather, weather_report, raw_weather_data)
|
|
||||||
- PHG parsing handles both string and map formats
|
|
||||||
- Standard fields are extracted from top-level attributes if present
|
|
||||||
|
|
||||||
## Current State
|
|
||||||
- All code changes committed and pushed to main branch
|
|
||||||
- Database migration successfully applied
|
|
||||||
- Test suite fully passing
|
|
||||||
- Code formatted and no compilation warnings
|
|
||||||
- Ready for deployment to production
|
|
||||||
|
|
||||||
## Remaining Tasks
|
|
||||||
From the todo list:
|
|
||||||
1. Write tests for Aprsme.Is module (APRS-IS connection) - pending
|
|
||||||
2. Run final coverage report to verify improvements - pending
|
|
||||||
|
|
||||||
## Notes for Next Session
|
|
||||||
- The enhanced parser integration is complete and tested
|
|
||||||
- Consider deploying to production and monitoring for any issues with the new fields
|
|
||||||
- The Aprsme.Is module still needs test coverage
|
|
||||||
- May want to verify that the parser improvements are working correctly with live APRS data
|
|
||||||
|
|
@ -28,6 +28,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- Supports ambiguity levels 0-4 as defined in APRS specification
|
- Supports ambiguity levels 0-4 as defined in APRS specification
|
||||||
- Enhanced APRS parser integration with latest improvements
|
- Enhanced APRS parser integration with latest improvements
|
||||||
- Added `posresolution` field showing position accuracy in meters (18.52m for uncompressed, 0.291m for compressed)
|
- Added `posresolution` field showing position accuracy in meters (18.52m for uncompressed, 0.291m for compressed)
|
||||||
|
- Upgraded vendor/aprs parser to version 0.1.5+ with comprehensive compatibility improvements
|
||||||
|
- Added standard APRS parser fields: posambiguity, format, alive, symboltable, symbolcode, messaging
|
||||||
|
- Enhanced weather data extraction with dedicated `wx` field
|
||||||
|
- Added radio range (RNG) parsing from comments
|
||||||
|
- Improved PHG data format handling (now returns string representation)
|
||||||
|
- Added telemetry fields (seq, vals, bits) extraction
|
||||||
|
- Better UTF-8 handling and error messages for invalid position data
|
||||||
|
- Fixed compressed latitude calculation and third-party traffic parsing
|
||||||
|
- Added support for alternate compressed position formats
|
||||||
- Added 20 new database fields for enhanced parser compatibility
|
- Added 20 new database fields for enhanced parser compatibility
|
||||||
- Implemented weather data extraction with dedicated `wx` field support
|
- Implemented weather data extraction with dedicated `wx` field support
|
||||||
- Enhanced PHG parsing to handle both string format and legacy map structure
|
- Enhanced PHG parsing to handle both string format and legacy map structure
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
console.log("app.js loading...");
|
||||||
|
|
||||||
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
|
// If you want to use Phoenix channels, run `mix help phx.gen.channel`
|
||||||
// to get started and then uncomment the line below.
|
// to get started and then uncomment the line below.
|
||||||
// import "./user_socket.js"
|
// import "./user_socket.js"
|
||||||
|
|
@ -124,23 +126,28 @@ let BodyClassHook = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// APRS Map Hook
|
// APRS MapAPRSMap Hook
|
||||||
let Hooks = {};
|
let Hooks = {};
|
||||||
|
|
||||||
// Map hooks - load map bundle when needed
|
// Map hooks - load map bundle when needed
|
||||||
|
// Store original mounted function before creating wrapper
|
||||||
|
const originalMapMounted = MapAPRSMap.mounted;
|
||||||
Hooks.APRSMap = {
|
Hooks.APRSMap = {
|
||||||
...MapAPRSMap,
|
...MapAPRSMap,
|
||||||
mounted() {
|
mounted() {
|
||||||
|
console.log("APRSMap wrapper mounted() called");
|
||||||
const self = this;
|
const self = this;
|
||||||
if (window.VendorLoader && !window.mapBundleLoaded) {
|
if (window.VendorLoader && !window.mapBundleLoaded) {
|
||||||
|
console.log("Loading map bundle...");
|
||||||
// Load map bundle and wait for it to complete
|
// Load map bundle and wait for it to complete
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.src = window.VendorLoader.mapBundleUrl;
|
script.src = window.VendorLoader.mapBundleUrl;
|
||||||
script.onload = () => {
|
script.onload = () => {
|
||||||
|
console.log("Map bundle loaded, calling original mounted");
|
||||||
window.mapBundleLoaded = true;
|
window.mapBundleLoaded = true;
|
||||||
// Now call the original mounted function
|
// Now call the original mounted function
|
||||||
if (MapAPRSMap.mounted) {
|
if (originalMapMounted) {
|
||||||
MapAPRSMap.mounted.call(self);
|
originalMapMounted.call(self);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
script.onerror = () => {
|
script.onerror = () => {
|
||||||
|
|
@ -148,9 +155,10 @@ Hooks.APRSMap = {
|
||||||
};
|
};
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
} else {
|
} else {
|
||||||
|
console.log("Map bundle already loaded, calling original mounted");
|
||||||
// Map bundle already loaded, proceed immediately
|
// Map bundle already loaded, proceed immediately
|
||||||
if (MapAPRSMap.mounted) {
|
if (originalMapMounted) {
|
||||||
MapAPRSMap.mounted.call(this);
|
originalMapMounted.call(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -176,7 +184,7 @@ Hooks.InfoMap = {
|
||||||
};
|
};
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
} else {
|
} else {
|
||||||
// Map bundle already loaded, proceed immediately
|
// MapAPRSMap bundle already loaded, proceed immediately
|
||||||
if (InfoMap.mounted) {
|
if (InfoMap.mounted) {
|
||||||
InfoMap.mounted.call(this);
|
InfoMap.mounted.call(this);
|
||||||
}
|
}
|
||||||
|
|
@ -263,7 +271,7 @@ window.localStorage.setItem("theme", theme);
|
||||||
window.reRenderAllCharts = () => {
|
window.reRenderAllCharts = () => {
|
||||||
// Store all chart instances globally so we can access them
|
// Store all chart instances globally so we can access them
|
||||||
if (!window.chartInstances) {
|
if (!window.chartInstances) {
|
||||||
window.chartInstances = new Map();
|
window.chartInstances = new MapAPRSMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-render all stored chart instances
|
// Re-render all stored chart instances
|
||||||
|
|
@ -310,6 +318,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("Creating LiveSocket with hooks:", Object.keys(Hooks));
|
||||||
let liveSocket = new LiveSocket("/live", Socket, {
|
let liveSocket = new LiveSocket("/live", Socket, {
|
||||||
longPollFallbackMs: 2500,
|
longPollFallbackMs: 2500,
|
||||||
params: { _csrf_token: csrfToken },
|
params: { _csrf_token: csrfToken },
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ import {
|
||||||
let MapAPRSMap = {
|
let MapAPRSMap = {
|
||||||
mounted() {
|
mounted() {
|
||||||
const self = this as unknown as LiveViewHookContext;
|
const self = this as unknown as LiveViewHookContext;
|
||||||
|
console.log("APRSMap hook mounted() called on element:", this.el);
|
||||||
// Initialize error tracking
|
// Initialize error tracking
|
||||||
self.errors = [];
|
self.errors = [];
|
||||||
self.initializationAttempts = 0;
|
self.initializationAttempts = 0;
|
||||||
|
|
@ -438,6 +439,7 @@ let MapAPRSMap = {
|
||||||
if (self.map) {
|
if (self.map) {
|
||||||
saveMapState(self.map, self.pushEvent.bind(self));
|
saveMapState(self.map, self.pushEvent.bind(self));
|
||||||
// Also send bounds to server to trigger historical loading
|
// Also send bounds to server to trigger historical loading
|
||||||
|
console.log("Calling sendBoundsToServer after map_ready (retry)");
|
||||||
self.sendBoundsToServer();
|
self.sendBoundsToServer();
|
||||||
}
|
}
|
||||||
// Also trigger map state update after a delay
|
// Also trigger map state update after a delay
|
||||||
|
|
@ -1059,6 +1061,11 @@ let MapAPRSMap = {
|
||||||
self.handleEvent(
|
self.handleEvent(
|
||||||
"add_historical_packets_batch",
|
"add_historical_packets_batch",
|
||||||
(data: { packets: MarkerData[]; batch: number; is_final: boolean }) => {
|
(data: { packets: MarkerData[]; batch: number; is_final: boolean }) => {
|
||||||
|
console.log("Received add_historical_packets_batch event:", {
|
||||||
|
packetCount: data.packets?.length || 0,
|
||||||
|
batch: data.batch,
|
||||||
|
is_final: data.is_final
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
if (data.packets && Array.isArray(data.packets)) {
|
if (data.packets && Array.isArray(data.packets)) {
|
||||||
// Process all packets immediately for maximum speed
|
// Process all packets immediately for maximum speed
|
||||||
|
|
@ -1325,6 +1332,7 @@ let MapAPRSMap = {
|
||||||
|
|
||||||
sendBoundsToServer() {
|
sendBoundsToServer() {
|
||||||
const self = this as unknown as LiveViewHookContext;
|
const self = this as unknown as LiveViewHookContext;
|
||||||
|
console.log("sendBoundsToServer called, map:", !!self.map, "isDestroyed:", self.isDestroyed);
|
||||||
if (!self.map || self.isDestroyed) return;
|
if (!self.map || self.isDestroyed) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -1351,6 +1359,7 @@ let MapAPRSMap = {
|
||||||
},
|
},
|
||||||
zoom: zoom,
|
zoom: zoom,
|
||||||
};
|
};
|
||||||
|
console.log("Sending bounds_changed event:", boundsData);
|
||||||
self.pushEvent("bounds_changed", boundsData);
|
self.pushEvent("bounds_changed", boundsData);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -435,6 +435,10 @@ defmodule Aprsme.Packets do
|
||||||
@impl true
|
@impl true
|
||||||
@spec get_recent_packets(map()) :: [struct()]
|
@spec get_recent_packets(map()) :: [struct()]
|
||||||
def get_recent_packets(opts \\ %{}) do
|
def get_recent_packets(opts \\ %{}) do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
Logger.debug("Packets.get_recent_packets called with opts: #{inspect(opts)}")
|
||||||
|
|
||||||
# Use hours_back from opts if provided, otherwise default to 24 hours
|
# Use hours_back from opts if provided, otherwise default to 24 hours
|
||||||
hours_back = Map.get(opts, :hours_back, 24)
|
hours_back = Map.get(opts, :hours_back, 24)
|
||||||
time_ago = DateTime.add(DateTime.utc_now(), -hours_back * 3600, :second)
|
time_ago = DateTime.add(DateTime.utc_now(), -hours_back * 3600, :second)
|
||||||
|
|
@ -475,7 +479,9 @@ defmodule Aprsme.Packets do
|
||||||
|> offset(^offset)
|
|> offset(^offset)
|
||||||
|> QueryBuilder.with_coordinates()
|
|> QueryBuilder.with_coordinates()
|
||||||
|
|
||||||
Repo.all(query)
|
result = Repo.all(query)
|
||||||
|
Logger.debug("Packets.get_recent_packets returning #{length(result)} packets")
|
||||||
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- App scripts -->
|
<!-- App scripts -->
|
||||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
<script phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
(function(c,u,v,n,p,e,z,A,w){function k(a){if(!x){x=!0;var l=u.getElementsByTagName(v)[0],d=u.createElement(v);d.src=A;d.crossOrigin="anonymous";d.addEventListener("load",function(){try{c[n]=r;c[p]=t;var b=c[e],d=b.init;b.init=function(a){for(var b in a)Object.prototype.hasOwnProperty.call(a,b)&&(w[b]=a[b]);d(w)};B(a,b)}catch(g){console.error(g)}});l.parentNode.insertBefore(d,l)}}function B(a,l){try{for(var d=m.data,b=0;b<a.length;b++)if("function"===typeof a[b])a[b]();var e=!1,g=c.__SENTRY__;"undefined"!==typeof g&&g.hub&&g.hub.getClient()&&(e=!0);g=!1;for(b=0;b<d.length;b++)if(d[b].f){g=!0;var f=d[b];!1===e&&"init"!==f.f&&l.init();e=!0;l[f.f].apply(l,f.a)}!1===e&&!1===g&&l.init();var h=c[n],k=c[p];for(b=0;b<d.length;b++)d[b].e&&h?h.apply(c,d[b].e):d[b].p&&k&&k.apply(c,[d[b].p])}catch(C){console.error(C)}}for(var f=!0,y=!1,q=0;q<document.scripts.length;q++)if(-1<document.scripts[q].src.indexOf(z)){f="no"!==document.scripts[q].getAttribute("data-lazy");break}var x=!1,h=[],m=function(a){(a.e||a.p||a.f&&-1<a.f.indexOf("capture")||a.f&&-1<a.f.indexOf("showReportDialog"))&&f&&k(h);m.data.push(a)};m.data=[];c[e]=c[e]||{};c[e].onLoad=function(a){h.push(a);f&&!y||k(h)};c[e].forceLoad=function(){y=!0;f&&setTimeout(function(){k(h)})};"init addBreadcrumb captureMessage captureException captureEvent configureScope withScope showReportDialog".split(" ").forEach(function(a){c[e][a]=function(){m({f:a,a:arguments})}});var r=c[n];c[n]=function(a,e,d,b,f){m({e:[].slice.call(arguments)});r&&r.apply(c,arguments)};var t=c[p];c[p]=function(a){m({p:a.reason});t&&t.apply(c,arguments)};f||setTimeout(function(){k(h)})})(window,document,"script","onerror","onunhandledrejection","Sentry","be4b53768e7c243cc72fa78ee7b7ec8c","https://js.sentry-cdn.com/be4b53768e7c243cc72fa78ee7b7ec8c.min.js",{"dsn":"https://337ece4c07ff53c6719d900adfddd6e4@o4509627566063616.ingest.us.sentry.io/4509691336785920"});
|
(function(c,u,v,n,p,e,z,A,w){function k(a){if(!x){x=!0;var l=u.getElementsByTagName(v)[0],d=u.createElement(v);d.src=A;d.crossOrigin="anonymous";d.addEventListener("load",function(){try{c[n]=r;c[p]=t;var b=c[e],d=b.init;b.init=function(a){for(var b in a)Object.prototype.hasOwnProperty.call(a,b)&&(w[b]=a[b]);d(w)};B(a,b)}catch(g){console.error(g)}});l.parentNode.insertBefore(d,l)}}function B(a,l){try{for(var d=m.data,b=0;b<a.length;b++)if("function"===typeof a[b])a[b]();var e=!1,g=c.__SENTRY__;"undefined"!==typeof g&&g.hub&&g.hub.getClient()&&(e=!0);g=!1;for(b=0;b<d.length;b++)if(d[b].f){g=!0;var f=d[b];!1===e&&"init"!==f.f&&l.init();e=!0;l[f.f].apply(l,f.a)}!1===e&&!1===g&&l.init();var h=c[n],k=c[p];for(b=0;b<d.length;b++)d[b].e&&h?h.apply(c,d[b].e):d[b].p&&k&&k.apply(c,[d[b].p])}catch(C){console.error(C)}}for(var f=!0,y=!1,q=0;q<document.scripts.length;q++)if(-1<document.scripts[q].src.indexOf(z)){f="no"!==document.scripts[q].getAttribute("data-lazy");break}var x=!1,h=[],m=function(a){(a.e||a.p||a.f&&-1<a.f.indexOf("capture")||a.f&&-1<a.f.indexOf("showReportDialog"))&&f&&k(h);m.data.push(a)};m.data=[];c[e]=c[e]||{};c[e].onLoad=function(a){h.push(a);f&&!y||k(h)};c[e].forceLoad=function(){y=!0;f&&setTimeout(function(){k(h)})};"init addBreadcrumb captureMessage captureException captureEvent configureScope withScope showReportDialog".split(" ").forEach(function(a){c[e][a]=function(){m({f:a,a:arguments})}});var r=c[n];c[n]=function(a,e,d,b,f){m({e:[].slice.call(arguments)});r&&r.apply(c,arguments)};var t=c[p];c[p]=function(a){m({p:a.reason});t&&t.apply(c,arguments)};f||setTimeout(function(){k(h)})})(window,document,"script","onerror","onunhandledrejection","Sentry","be4b53768e7c243cc72fa78ee7b7ec8c","https://js.sentry-cdn.com/be4b53768e7c243cc72fa78ee7b7ec8c.min.js",{"dsn":"https://337ece4c07ff53c6719d900adfddd6e4@o4509627566063616.ingest.us.sentry.io/4509691336785920"});
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ defmodule AprsmeWeb.MapLive.HistoricalLoader do
|
||||||
def start_progressive_historical_loading(socket) do
|
def start_progressive_historical_loading(socket) do
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
Logger.debug("HistoricalLoader: Starting progressive historical loading")
|
||||||
|
|
||||||
Logger.debug(
|
Logger.debug(
|
||||||
"start_progressive_historical_loading called with zoom: #{socket.assigns.map_zoom}, bounds: #{inspect(socket.assigns.map_bounds)}"
|
"start_progressive_historical_loading called with zoom: #{socket.assigns.map_zoom}, bounds: #{inspect(socket.assigns.map_bounds)}"
|
||||||
)
|
)
|
||||||
|
|
@ -128,6 +130,8 @@ defmodule AprsmeWeb.MapLive.HistoricalLoader do
|
||||||
defp do_load_historical_batch(%{assigns: %{map_bounds: nil}} = socket, _batch_offset), do: socket
|
defp do_load_historical_batch(%{assigns: %{map_bounds: nil}} = socket, _batch_offset), do: socket
|
||||||
|
|
||||||
defp do_load_historical_batch(%{assigns: %{map_bounds: bounds}} = socket, batch_offset) do
|
defp do_load_historical_batch(%{assigns: %{map_bounds: bounds}} = socket, batch_offset) do
|
||||||
|
require Logger
|
||||||
|
|
||||||
bounds_list = [
|
bounds_list = [
|
||||||
bounds.west,
|
bounds.west,
|
||||||
bounds.south,
|
bounds.south,
|
||||||
|
|
@ -172,7 +176,9 @@ defmodule AprsmeWeb.MapLive.HistoricalLoader do
|
||||||
params = Map.put(params, :hours_back, historical_hours)
|
params = Map.put(params, :hours_back, historical_hours)
|
||||||
|
|
||||||
# Get recent packets within time filter
|
# Get recent packets within time filter
|
||||||
|
Logger.debug("HistoricalLoader: Querying packets with params: #{inspect(params)}")
|
||||||
recent_packets = Packets.get_recent_packets(params)
|
recent_packets = Packets.get_recent_packets(params)
|
||||||
|
Logger.debug("HistoricalLoader: Got #{length(recent_packets)} packets")
|
||||||
|
|
||||||
# If tracking a callsign and this is the first batch, ensure we always include
|
# If tracking a callsign and this is the first batch, ensure we always include
|
||||||
# the most recent packet for that callsign, even if it's older than the time filter
|
# the most recent packet for that callsign, even if it's older than the time filter
|
||||||
|
|
@ -300,6 +306,12 @@ defmodule AprsmeWeb.MapLive.HistoricalLoader do
|
||||||
|
|
||||||
# Handle high zoom (markers)
|
# Handle high zoom (markers)
|
||||||
defp handle_zoom_based_display(socket, _historical_packets, packet_data_list, is_final_batch, batch_offset) do
|
defp handle_zoom_based_display(socket, _historical_packets, packet_data_list, is_final_batch, batch_offset) do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
Logger.debug(
|
||||||
|
"HistoricalLoader: Pushing #{length(packet_data_list)} packets to client, batch #{batch_offset}, is_final: #{is_final_batch}"
|
||||||
|
)
|
||||||
|
|
||||||
# Use LiveView's efficient push_event for incremental updates
|
# Use LiveView's efficient push_event for incremental updates
|
||||||
LiveView.push_event(socket, "add_historical_packets_batch", %{
|
LiveView.push_event(socket, "add_historical_packets_batch", %{
|
||||||
packets: packet_data_list,
|
packets: packet_data_list,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ defmodule AprsmeWeb.MapLive.Index do
|
||||||
"""
|
"""
|
||||||
use AprsmeWeb, :live_view
|
use AprsmeWeb, :live_view
|
||||||
|
|
||||||
import AprsmeWeb.Components.ErrorBoundary
|
|
||||||
import AprsmeWeb.Live.Shared.PacketUtils, only: [get_callsign_key: 1]
|
import AprsmeWeb.Live.Shared.PacketUtils, only: [get_callsign_key: 1]
|
||||||
import AprsmeWeb.TimeHelpers, only: [time_ago_in_words: 1]
|
import AprsmeWeb.TimeHelpers, only: [time_ago_in_words: 1]
|
||||||
import Phoenix.LiveView, only: [connected?: 1, push_event: 3, push_patch: 2, put_flash: 3]
|
import Phoenix.LiveView, only: [connected?: 1, push_event: 3, push_patch: 2, put_flash: 3]
|
||||||
|
|
@ -266,6 +265,9 @@ defmodule AprsmeWeb.MapLive.Index do
|
||||||
# Handle both bounds_changed and update_bounds events
|
# Handle both bounds_changed and update_bounds events
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event(event, %{"bounds" => bounds}, socket) when event in ["bounds_changed", "update_bounds"] do
|
def handle_event(event, %{"bounds" => bounds}, socket) when event in ["bounds_changed", "update_bounds"] do
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
Logger.debug("Received #{event} event with bounds: #{inspect(bounds)}")
|
||||||
handle_bounds_update(bounds, socket)
|
handle_bounds_update(bounds, socket)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -362,6 +364,17 @@ defmodule AprsmeWeb.MapLive.Index do
|
||||||
# The calculated bounds might be too small/inaccurate
|
# The calculated bounds might be too small/inaccurate
|
||||||
Logger.debug("Map ready - waiting for JavaScript to send actual bounds before loading historical packets")
|
Logger.debug("Map ready - waiting for JavaScript to send actual bounds before loading historical packets")
|
||||||
|
|
||||||
|
# If we need initial historical load and have bounds, trigger it
|
||||||
|
socket =
|
||||||
|
if socket.assigns.needs_initial_historical_load and socket.assigns.map_bounds do
|
||||||
|
Logger.debug("Map ready with needs_initial_historical_load=true, triggering historical loading")
|
||||||
|
# Send a message to trigger bounds processing
|
||||||
|
send(self(), {:process_bounds_update, socket.assigns.map_bounds})
|
||||||
|
socket
|
||||||
|
else
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -1257,19 +1270,17 @@ defmodule AprsmeWeb.MapLive.Index do
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<.error_boundary id="map-error-boundary">
|
<div
|
||||||
<div
|
id="aprs-map"
|
||||||
id="aprs-map"
|
class={if @slideover_open, do: "slideover-open", else: "slideover-closed"}
|
||||||
class={if @slideover_open, do: "slideover-open", else: "slideover-closed"}
|
phx-hook="APRSMap"
|
||||||
phx-hook="APRSMap"
|
phx-update="ignore"
|
||||||
phx-update="ignore"
|
data-center={Jason.encode!(@map_center)}
|
||||||
data-center={Jason.encode!(@map_center)}
|
data-zoom={@map_zoom}
|
||||||
data-zoom={@map_zoom}
|
role="application"
|
||||||
role="application"
|
aria-label={gettext("APRS packet map showing real-time amateur radio stations")}
|
||||||
aria-label={gettext("APRS packet map showing real-time amateur radio stations")}
|
>
|
||||||
>
|
</div>
|
||||||
</div>
|
|
||||||
</.error_boundary>
|
|
||||||
|
|
||||||
<button class="locate-button" phx-click="locate_me" title={Gettext.gettext(AprsmeWeb.Gettext, "Find my location")}>
|
<button class="locate-button" phx-click="locate_me" title={Gettext.gettext(AprsmeWeb.Gettext, "Find my location")}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="#374151" stroke="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="#374151" stroke="none">
|
||||||
|
|
@ -1947,6 +1958,11 @@ defmodule AprsmeWeb.MapLive.Index do
|
||||||
|
|
||||||
# Load historical packets for the new bounds (now socket.assigns.map_bounds is correct)
|
# Load historical packets for the new bounds (now socket.assigns.map_bounds is correct)
|
||||||
Logger.debug("Starting progressive historical loading for new bounds")
|
Logger.debug("Starting progressive historical loading for new bounds")
|
||||||
|
|
||||||
|
Logger.debug(
|
||||||
|
"Current assigns: historical_loading=#{socket.assigns.historical_loading}, map_bounds=#{inspect(socket.assigns.map_bounds)}"
|
||||||
|
)
|
||||||
|
|
||||||
socket = HistoricalLoader.start_progressive_historical_loading(socket)
|
socket = HistoricalLoader.start_progressive_historical_loading(socket)
|
||||||
|
|
||||||
# Mark initial historical as completed if this was the initial load
|
# Mark initial historical as completed if this was the initial load
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,5 @@ defmodule AprsmeWeb.MapLive.PacketBatcher do
|
||||||
|
|
||||||
# Send batch to parent LiveView
|
# Send batch to parent LiveView
|
||||||
send(parent_pid, {:packet_batch, packets})
|
send(parent_pid, {:packet_batch, packets})
|
||||||
|
|
||||||
Logger.debug("Processing batch of #{length(packets)} packets")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
defmodule Aprsme.Repo.Migrations.AddParserCompatibilityFields do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:packets) do
|
||||||
|
# Position resolution and format fields from enhanced parser
|
||||||
|
add_if_not_exists :posresolution, :float
|
||||||
|
add_if_not_exists :format, :string
|
||||||
|
|
||||||
|
# Additional message field
|
||||||
|
add_if_not_exists :addressee, :string
|
||||||
|
add_if_not_exists :message_text, :string
|
||||||
|
add_if_not_exists :message_number, :string
|
||||||
|
|
||||||
|
# Telemetry fields
|
||||||
|
add_if_not_exists :telemetry_seq, :integer
|
||||||
|
add_if_not_exists :telemetry_vals, {:array, :integer}
|
||||||
|
add_if_not_exists :telemetry_bits, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "Testing Docker build..."
|
|
||||||
|
|
||||||
# Build the Docker image
|
|
||||||
echo "Building Docker image..."
|
|
||||||
docker build -t aprsme-test:latest .
|
|
||||||
|
|
||||||
echo "Docker build completed successfully!"
|
|
||||||
|
|
||||||
# Test if the image runs
|
|
||||||
echo "Testing if the image can run..."
|
|
||||||
docker run --rm aprsme-test:latest bin/aprsme eval "IO.puts(:ok)"
|
|
||||||
|
|
||||||
echo "All tests passed!"
|
|
||||||
|
|
@ -31,11 +31,17 @@ defmodule Aprsme.Cluster.LeaderElectionTest do
|
||||||
assert {:ok, pid} = LeaderElection.start_link([])
|
assert {:ok, pid} = LeaderElection.start_link([])
|
||||||
assert Process.alive?(pid)
|
assert Process.alive?(pid)
|
||||||
assert Process.whereis(LeaderElection) == pid
|
assert Process.whereis(LeaderElection) == pid
|
||||||
|
|
||||||
|
# Clean up after this specific test
|
||||||
|
GenServer.stop(pid)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "registers with the given name" do
|
test "registers with the given name" do
|
||||||
{:ok, _pid} = LeaderElection.start_link([])
|
{:ok, pid} = LeaderElection.start_link([])
|
||||||
assert Process.whereis(LeaderElection)
|
assert Process.whereis(LeaderElection)
|
||||||
|
|
||||||
|
# Clean up after this specific test
|
||||||
|
GenServer.stop(pid)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,12 @@ defmodule Aprsme.PacketsOldestTest do
|
||||||
test "returns the timestamp of the oldest packet" do
|
test "returns the timestamp of the oldest packet" do
|
||||||
# Create packets with different timestamps relative to now
|
# Create packets with different timestamps relative to now
|
||||||
now = DateTime.utc_now()
|
now = DateTime.utc_now()
|
||||||
oldest_time = DateTime.add(now, -365 * 24 * 60 * 60, :second) # 1 year ago
|
# 1 year ago
|
||||||
middle_time = DateTime.add(now, -180 * 24 * 60 * 60, :second) # 6 months ago
|
oldest_time = DateTime.add(now, -365 * 24 * 60 * 60, :second)
|
||||||
newest_time = DateTime.add(now, -30 * 24 * 60 * 60, :second) # 1 month ago
|
# 6 months ago
|
||||||
|
middle_time = DateTime.add(now, -180 * 24 * 60 * 60, :second)
|
||||||
|
# 1 month ago
|
||||||
|
newest_time = DateTime.add(now, -30 * 24 * 60 * 60, :second)
|
||||||
|
|
||||||
# Insert packets with different timestamps
|
# Insert packets with different timestamps
|
||||||
{:ok, _} = create_test_packet("OLD-1", oldest_time)
|
{:ok, _} = create_test_packet("OLD-1", oldest_time)
|
||||||
|
|
@ -23,9 +26,9 @@ defmodule Aprsme.PacketsOldestTest do
|
||||||
|
|
||||||
# Should return the oldest timestamp
|
# Should return the oldest timestamp
|
||||||
result = Packets.get_oldest_packet_timestamp()
|
result = Packets.get_oldest_packet_timestamp()
|
||||||
|
|
||||||
# Allow for small time differences due to database precision
|
# Allow for small time differences due to database precision (±1 second)
|
||||||
assert DateTime.diff(result, oldest_time, :second) == 0
|
assert abs(DateTime.diff(result, oldest_time, :second)) <= 1
|
||||||
end
|
end
|
||||||
|
|
||||||
test "handles packets with microsecond precision" do
|
test "handles packets with microsecond precision" do
|
||||||
|
|
@ -33,7 +36,7 @@ defmodule Aprsme.PacketsOldestTest do
|
||||||
now = DateTime.utc_now()
|
now = DateTime.utc_now()
|
||||||
# Truncate to microseconds to match database precision
|
# Truncate to microseconds to match database precision
|
||||||
timestamp_with_microseconds = DateTime.truncate(now, :microsecond)
|
timestamp_with_microseconds = DateTime.truncate(now, :microsecond)
|
||||||
|
|
||||||
{:ok, _} = create_test_packet("TEST-1", timestamp_with_microseconds)
|
{:ok, _} = create_test_packet("TEST-1", timestamp_with_microseconds)
|
||||||
|
|
||||||
result = Packets.get_oldest_packet_timestamp()
|
result = Packets.get_oldest_packet_timestamp()
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ defmodule AprsmeWeb.MapLive.HistoricalLoadingTest do
|
||||||
packet_ids = Enum.map(recent_packets, & &1.id)
|
packet_ids = Enum.map(recent_packets, & &1.id)
|
||||||
assert packet1.id in packet_ids
|
assert packet1.id in packet_ids
|
||||||
assert packet2.id in packet_ids
|
assert packet2.id in packet_ids
|
||||||
refute Enum.any?(packet_ids, fn id -> id == _old_packet.id end)
|
refute _old_packet.id in packet_ids
|
||||||
end
|
end
|
||||||
|
|
||||||
test "loads historical packets with custom historical hours setting", %{conn: conn} do
|
test "loads historical packets with custom historical hours setting", %{conn: conn} do
|
||||||
|
|
@ -239,26 +239,27 @@ defmodule AprsmeWeb.MapLive.HistoricalLoadingTest do
|
||||||
describe "historical packet push events" do
|
describe "historical packet push events" do
|
||||||
test "sends add_historical_packets_batch events to client", %{conn: conn} do
|
test "sends add_historical_packets_batch events to client", %{conn: conn} do
|
||||||
now = DateTime.utc_now()
|
now = DateTime.utc_now()
|
||||||
|
|
||||||
# Create test packets
|
# Create test packets
|
||||||
packets = for i <- 1..5 do
|
packets =
|
||||||
packet_fixture(%{
|
for i <- 1..5 do
|
||||||
sender: "PUSH#{i}",
|
packet_fixture(%{
|
||||||
base_callsign: "PUSH#{i}",
|
sender: "PUSH#{i}",
|
||||||
ssid: "0",
|
base_callsign: "PUSH#{i}",
|
||||||
lat: 40.7 + (i * 0.01),
|
ssid: "0",
|
||||||
lon: -74.0 + (i * 0.01),
|
lat: 40.7 + i * 0.01,
|
||||||
received_at: DateTime.add(now, -(i * 5 * 60), :second),
|
lon: -74.0 + i * 0.01,
|
||||||
comment: "Push test station #{i}"
|
received_at: DateTime.add(now, -(i * 5 * 60), :second),
|
||||||
})
|
comment: "Push test station #{i}"
|
||||||
end
|
})
|
||||||
|
end
|
||||||
|
|
||||||
# Connect to the map at high zoom (should use marker mode, not heat map)
|
# Connect to the map at high zoom (should use marker mode, not heat map)
|
||||||
{:ok, view, _html} = live(conn, "/?z=12")
|
{:ok, view, _html} = live(conn, "/?z=12")
|
||||||
|
|
||||||
# Send map_ready event
|
# Send map_ready event
|
||||||
assert render_hook(view, "map_ready", %{})
|
assert render_hook(view, "map_ready", %{})
|
||||||
|
|
||||||
# Send bounds that include all test packets
|
# Send bounds that include all test packets
|
||||||
bounds = %{
|
bounds = %{
|
||||||
"north" => 40.8,
|
"north" => 40.8,
|
||||||
|
|
@ -266,29 +267,30 @@ defmodule AprsmeWeb.MapLive.HistoricalLoadingTest do
|
||||||
"east" => -73.9,
|
"east" => -73.9,
|
||||||
"west" => -74.1
|
"west" => -74.1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Send bounds_changed event
|
# Send bounds_changed event
|
||||||
assert render_hook(view, "bounds_changed", %{"bounds" => bounds})
|
assert render_hook(view, "bounds_changed", %{"bounds" => bounds})
|
||||||
|
|
||||||
# Wait for historical loading
|
# Wait for historical loading
|
||||||
Process.sleep(200)
|
Process.sleep(200)
|
||||||
|
|
||||||
# Verify all packets would be included in the query
|
# Verify all packets would be included in the query
|
||||||
recent_packets = Packets.get_recent_packets(%{
|
recent_packets =
|
||||||
bounds: [bounds["west"], bounds["south"], bounds["east"], bounds["north"]],
|
Packets.get_recent_packets(%{
|
||||||
hours_back: 1,
|
bounds: [bounds["west"], bounds["south"], bounds["east"], bounds["north"]],
|
||||||
limit: 500
|
hours_back: 1,
|
||||||
})
|
limit: 500
|
||||||
|
})
|
||||||
|
|
||||||
packet_ids = Enum.map(recent_packets, & &1.id)
|
packet_ids = Enum.map(recent_packets, & &1.id)
|
||||||
assert length(packet_ids) >= 5
|
assert length(packet_ids) >= 5
|
||||||
|
|
||||||
# Verify all our test packets are included
|
# Verify all our test packets are included
|
||||||
for packet <- packets do
|
for packet <- packets do
|
||||||
assert packet.id in packet_ids
|
assert packet.id in packet_ids
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
test "does not send historical packets when bounds are invalid", %{conn: conn} do
|
test "does not send historical packets when bounds are invalid", %{conn: conn} do
|
||||||
# Create a test packet
|
# Create a test packet
|
||||||
packet_fixture(%{
|
packet_fixture(%{
|
||||||
|
|
@ -300,13 +302,13 @@ defmodule AprsmeWeb.MapLive.HistoricalLoadingTest do
|
||||||
received_at: DateTime.add(DateTime.utc_now(), -30 * 60, :second),
|
received_at: DateTime.add(DateTime.utc_now(), -30 * 60, :second),
|
||||||
comment: "Test for invalid bounds"
|
comment: "Test for invalid bounds"
|
||||||
})
|
})
|
||||||
|
|
||||||
# Connect to the map
|
# Connect to the map
|
||||||
{:ok, view, _html} = live(conn, "/")
|
{:ok, view, _html} = live(conn, "/")
|
||||||
|
|
||||||
# Send map_ready event
|
# Send map_ready event
|
||||||
assert render_hook(view, "map_ready", %{})
|
assert render_hook(view, "map_ready", %{})
|
||||||
|
|
||||||
# Send invalid bounds (north < south)
|
# Send invalid bounds (north < south)
|
||||||
invalid_bounds = %{
|
invalid_bounds = %{
|
||||||
"north" => 40.5,
|
"north" => 40.5,
|
||||||
|
|
@ -314,13 +316,13 @@ defmodule AprsmeWeb.MapLive.HistoricalLoadingTest do
|
||||||
"east" => -73.7,
|
"east" => -73.7,
|
||||||
"west" => -74.3
|
"west" => -74.3
|
||||||
}
|
}
|
||||||
|
|
||||||
# This should not cause an error, but bounds won't be processed
|
# This should not cause an error, but bounds won't be processed
|
||||||
assert render_hook(view, "bounds_changed", %{"bounds" => invalid_bounds})
|
assert render_hook(view, "bounds_changed", %{"bounds" => invalid_bounds})
|
||||||
|
|
||||||
# Wait briefly
|
# Wait briefly
|
||||||
Process.sleep(100)
|
Process.sleep(100)
|
||||||
|
|
||||||
# Since we can't check internal state, we verify the view is still functional
|
# Since we can't check internal state, we verify the view is still functional
|
||||||
# by sending valid bounds and checking it still works
|
# by sending valid bounds and checking it still works
|
||||||
valid_bounds = %{
|
valid_bounds = %{
|
||||||
|
|
@ -329,11 +331,11 @@ defmodule AprsmeWeb.MapLive.HistoricalLoadingTest do
|
||||||
"east" => -73.7,
|
"east" => -73.7,
|
||||||
"west" => -74.3
|
"west" => -74.3
|
||||||
}
|
}
|
||||||
|
|
||||||
assert render_hook(view, "bounds_changed", %{"bounds" => valid_bounds})
|
assert render_hook(view, "bounds_changed", %{"bounds" => valid_bounds})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "progressive historical loading" do
|
describe "progressive historical loading" do
|
||||||
test "loads packets in batches for low zoom levels", %{conn: conn} do
|
test "loads packets in batches for low zoom levels", %{conn: conn} do
|
||||||
now = DateTime.utc_now()
|
now = DateTime.utc_now()
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ defmodule AprsmeWeb.TimeHelpersTest do
|
||||||
# Use a fixed reference time for all tests to ensure consistency
|
# Use a fixed reference time for all tests to ensure consistency
|
||||||
# This prevents tests from failing due to timing issues
|
# This prevents tests from failing due to timing issues
|
||||||
reference_time = ~U[2024-01-15 12:00:00Z]
|
reference_time = ~U[2024-01-15 12:00:00Z]
|
||||||
|
|
||||||
# Mock DateTime.utc_now() to return our reference time
|
# Mock DateTime.utc_now() to return our reference time
|
||||||
# Note: Since time_ago_in_words likely uses DateTime.utc_now() internally,
|
# Note: Since time_ago_in_words likely uses DateTime.utc_now() internally,
|
||||||
# we need to test with actual time differences
|
# we need to test with actual time differences
|
||||||
|
|
|
||||||
|
|
@ -6,246 +6,248 @@ defmodule AprsmeWeb.HistoricalLoadingIntegrationTest do
|
||||||
use ExUnit.Case, async: false
|
use ExUnit.Case, async: false
|
||||||
use Wallaby.Feature
|
use Wallaby.Feature
|
||||||
|
|
||||||
|
import Aprsme.PacketsFixtures
|
||||||
import Wallaby.Browser
|
import Wallaby.Browser
|
||||||
import Wallaby.Query
|
import Wallaby.Query
|
||||||
import Aprsme.PacketsFixtures
|
|
||||||
|
|
||||||
@tag :integration
|
|
||||||
describe "historical packet loading on page load" do
|
describe "historical packet loading on page load" do
|
||||||
test "displays historical packets immediately after map loads", %{session: session} do
|
@describetag :integration
|
||||||
|
feature "displays historical packets immediately after map loads", %{session: session} do
|
||||||
# Create test packets with known positions
|
# Create test packets with known positions
|
||||||
now = DateTime.utc_now()
|
now = DateTime.utc_now()
|
||||||
|
|
||||||
packet1 = packet_fixture(%{
|
_packet1 =
|
||||||
sender: "INTTEST1",
|
packet_fixture(%{
|
||||||
base_callsign: "INTTEST1",
|
sender: "INTTEST1",
|
||||||
ssid: "0",
|
base_callsign: "INTTEST1",
|
||||||
lat: 40.7128,
|
ssid: "0",
|
||||||
lon: -74.0060,
|
lat: 40.7128,
|
||||||
received_at: DateTime.add(now, -20 * 60, :second),
|
lon: -74.0060,
|
||||||
comment: "Integration test station 1",
|
received_at: DateTime.add(now, -20 * 60, :second),
|
||||||
symbol_code: "k",
|
comment: "Integration test station 1",
|
||||||
symbol_table_id: "/"
|
symbol_code: "k",
|
||||||
})
|
symbol_table_id: "/"
|
||||||
|
})
|
||||||
packet2 = packet_fixture(%{
|
|
||||||
sender: "INTTEST2",
|
_packet2 =
|
||||||
base_callsign: "INTTEST2",
|
packet_fixture(%{
|
||||||
ssid: "0",
|
sender: "INTTEST2",
|
||||||
lat: 40.7580,
|
base_callsign: "INTTEST2",
|
||||||
lon: -73.9855,
|
ssid: "0",
|
||||||
received_at: DateTime.add(now, -10 * 60, :second),
|
lat: 40.7580,
|
||||||
comment: "Integration test station 2",
|
lon: -73.9855,
|
||||||
symbol_code: "-",
|
received_at: DateTime.add(now, -10 * 60, :second),
|
||||||
symbol_table_id: "/"
|
comment: "Integration test station 2",
|
||||||
})
|
symbol_code: "-",
|
||||||
|
symbol_table_id: "/"
|
||||||
|
})
|
||||||
|
|
||||||
# Navigate to the map
|
# Navigate to the map
|
||||||
session
|
session
|
||||||
|> visit("/")
|
|> visit("/")
|
||||||
|> assert_has(css("#map", text: ""))
|
|> assert_has(css("#map", text: ""))
|
||||||
|
|
||||||
# Wait for map to initialize and markers to appear
|
# Wait for map to initialize and markers to appear
|
||||||
# The map should automatically load historical packets
|
# The map should automatically load historical packets
|
||||||
Process.sleep(3000)
|
Process.sleep(3000)
|
||||||
|
|
||||||
# Check that markers are present on the map
|
# Check that markers are present on the map
|
||||||
# Look for Leaflet marker elements
|
# Look for Leaflet marker elements
|
||||||
assert_has(session, css(".leaflet-marker-icon"))
|
assert_has(session, css(".leaflet-marker-icon"))
|
||||||
|
|
||||||
# Verify at least 2 markers are present (our test packets)
|
# Verify at least 2 markers are present (our test packets)
|
||||||
marker_count =
|
marker_count =
|
||||||
session
|
session
|
||||||
|> all(css(".leaflet-marker-icon"))
|
|> all(css(".leaflet-marker-icon"))
|
||||||
|> length()
|
|> length()
|
||||||
|
|
||||||
assert marker_count >= 2, "Expected at least 2 markers, found #{marker_count}"
|
assert marker_count >= 2, "Expected at least 2 markers, found #{marker_count}"
|
||||||
|
|
||||||
# Click on a marker to verify it's one of our test packets
|
# Click on a marker to verify it's one of our test packets
|
||||||
session
|
click(session, css(".leaflet-marker-icon", at: 0))
|
||||||
|> click(css(".leaflet-marker-icon", at: 0))
|
|
||||||
|
|
||||||
# Wait for popup to appear
|
# Wait for popup to appear
|
||||||
Process.sleep(1000)
|
Process.sleep(1000)
|
||||||
|
|
||||||
# Verify popup contains one of our test callsigns
|
# Verify popup contains one of our test callsigns
|
||||||
assert_has(session, css(".leaflet-popup-content", text: ~r/INTTEST[12]/))
|
assert_has(session, css(".leaflet-popup-content", text: ~r/INTTEST[12]/))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "loads packets within specified historical time range", %{session: session} do
|
feature "loads packets within specified historical time range", %{session: session} do
|
||||||
now = DateTime.utc_now()
|
now = DateTime.utc_now()
|
||||||
|
|
||||||
# Create packets at different times
|
# Create packets at different times
|
||||||
_old_packet = packet_fixture(%{
|
_old_packet =
|
||||||
sender: "OLDTEST",
|
packet_fixture(%{
|
||||||
base_callsign: "OLDTEST",
|
sender: "OLDTEST",
|
||||||
ssid: "0",
|
base_callsign: "OLDTEST",
|
||||||
lat: 40.7128,
|
ssid: "0",
|
||||||
lon: -74.0060,
|
lat: 40.7128,
|
||||||
received_at: DateTime.add(now, -3 * 60 * 60, :second), # 3 hours ago
|
lon: -74.0060,
|
||||||
comment: "Old packet - should not appear with hist=1"
|
# 3 hours ago
|
||||||
})
|
received_at: DateTime.add(now, -3 * 60 * 60, :second),
|
||||||
|
comment: "Old packet - should not appear with hist=1"
|
||||||
recent_packet = packet_fixture(%{
|
})
|
||||||
sender: "RECENTTEST",
|
|
||||||
base_callsign: "RECENTTEST",
|
_recent_packet =
|
||||||
ssid: "0",
|
packet_fixture(%{
|
||||||
lat: 40.7128,
|
sender: "RECENTTEST",
|
||||||
lon: -74.0060,
|
base_callsign: "RECENTTEST",
|
||||||
received_at: DateTime.add(now, -30 * 60, :second), # 30 minutes ago
|
ssid: "0",
|
||||||
comment: "Recent packet - should appear"
|
lat: 40.7128,
|
||||||
})
|
lon: -74.0060,
|
||||||
|
# 30 minutes ago
|
||||||
|
received_at: DateTime.add(now, -30 * 60, :second),
|
||||||
|
comment: "Recent packet - should appear"
|
||||||
|
})
|
||||||
|
|
||||||
# Navigate to map with 1 hour historical range (default)
|
# Navigate to map with 1 hour historical range (default)
|
||||||
session
|
session
|
||||||
|> visit("/?hist=1")
|
|> visit("/?hist=1")
|
||||||
|> assert_has(css("#map"))
|
|> assert_has(css("#map"))
|
||||||
|
|
||||||
# Wait for historical loading
|
# Wait for historical loading
|
||||||
Process.sleep(3000)
|
Process.sleep(3000)
|
||||||
|
|
||||||
# Click on the marker (should be the recent one)
|
# Click on the marker (should be the recent one)
|
||||||
session
|
click(session, css(".leaflet-marker-icon", at: 0))
|
||||||
|> click(css(".leaflet-marker-icon", at: 0))
|
|
||||||
|
|
||||||
Process.sleep(1000)
|
Process.sleep(1000)
|
||||||
|
|
||||||
# Verify it's the recent packet, not the old one
|
# Verify it's the recent packet, not the old one
|
||||||
assert_has(session, css(".leaflet-popup-content", text: "RECENTTEST"))
|
assert_has(session, css(".leaflet-popup-content", text: "RECENTTEST"))
|
||||||
refute_has(session, css(".leaflet-popup-content", text: "OLDTEST"))
|
refute_has(session, css(".leaflet-popup-content", text: "OLDTEST"))
|
||||||
|
|
||||||
# Now test with extended historical range
|
# Now test with extended historical range
|
||||||
session
|
session
|
||||||
|> visit("/?hist=6") # 6 hours
|
# 6 hours
|
||||||
|
|> visit("/?hist=6")
|
||||||
|> assert_has(css("#map"))
|
|> assert_has(css("#map"))
|
||||||
|
|
||||||
Process.sleep(3000)
|
Process.sleep(3000)
|
||||||
|
|
||||||
# Now both packets should be visible
|
# Now both packets should be visible
|
||||||
marker_count =
|
marker_count =
|
||||||
session
|
session
|
||||||
|> all(css(".leaflet-marker-icon"))
|
|> all(css(".leaflet-marker-icon"))
|
||||||
|> length()
|
|> length()
|
||||||
|
|
||||||
assert marker_count >= 2, "Expected at least 2 markers with 6-hour range"
|
assert marker_count >= 2, "Expected at least 2 markers with 6-hour range"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "updates historical packets when bounds change", %{session: session} do
|
feature "updates historical packets when bounds change", %{session: session} do
|
||||||
now = DateTime.utc_now()
|
now = DateTime.utc_now()
|
||||||
|
|
||||||
# Create packets in different locations
|
# Create packets in different locations
|
||||||
nyc_packet = packet_fixture(%{
|
_nyc_packet =
|
||||||
sender: "NYC1",
|
packet_fixture(%{
|
||||||
base_callsign: "NYC1",
|
sender: "NYC1",
|
||||||
ssid: "0",
|
base_callsign: "NYC1",
|
||||||
lat: 40.7128,
|
ssid: "0",
|
||||||
lon: -74.0060,
|
lat: 40.7128,
|
||||||
received_at: DateTime.add(now, -30 * 60, :second),
|
lon: -74.0060,
|
||||||
comment: "NYC packet"
|
received_at: DateTime.add(now, -30 * 60, :second),
|
||||||
})
|
comment: "NYC packet"
|
||||||
|
})
|
||||||
la_packet = packet_fixture(%{
|
|
||||||
sender: "LA1",
|
_la_packet =
|
||||||
base_callsign: "LA1",
|
packet_fixture(%{
|
||||||
ssid: "0",
|
sender: "LA1",
|
||||||
lat: 34.0522,
|
base_callsign: "LA1",
|
||||||
lon: -118.2437,
|
ssid: "0",
|
||||||
received_at: DateTime.add(now, -30 * 60, :second),
|
lat: 34.0522,
|
||||||
comment: "LA packet"
|
lon: -118.2437,
|
||||||
})
|
received_at: DateTime.add(now, -30 * 60, :second),
|
||||||
|
comment: "LA packet"
|
||||||
|
})
|
||||||
|
|
||||||
# Start focused on NYC
|
# Start focused on NYC
|
||||||
session
|
session
|
||||||
|> visit("/?lat=40.7128&lng=-74.0060&z=10")
|
|> visit("/?lat=40.7128&lng=-74.0060&z=10")
|
||||||
|> assert_has(css("#map"))
|
|> assert_has(css("#map"))
|
||||||
|
|
||||||
Process.sleep(3000)
|
Process.sleep(3000)
|
||||||
|
|
||||||
# Should see NYC packet
|
# Should see NYC packet
|
||||||
session
|
click(session, css(".leaflet-marker-icon", at: 0))
|
||||||
|> click(css(".leaflet-marker-icon", at: 0))
|
|
||||||
|
|
||||||
Process.sleep(1000)
|
Process.sleep(1000)
|
||||||
|
|
||||||
assert_has(session, css(".leaflet-popup-content", text: "NYC1"))
|
assert_has(session, css(".leaflet-popup-content", text: "NYC1"))
|
||||||
|
|
||||||
# Close popup
|
# Close popup
|
||||||
session
|
send_keys(session, [:escape])
|
||||||
|> send_keys([:escape])
|
|
||||||
|
|
||||||
# Pan to LA (this would be done via map interaction in real usage)
|
# Pan to LA (this would be done via map interaction in real usage)
|
||||||
# For testing, we'll navigate to new URL
|
# For testing, we'll navigate to new URL
|
||||||
session
|
visit(session, "/?lat=34.0522&lng=-118.2437&z=10")
|
||||||
|> visit("/?lat=34.0522&lng=-118.2437&z=10")
|
|
||||||
|
|
||||||
Process.sleep(3000)
|
Process.sleep(3000)
|
||||||
|
|
||||||
# Should now see LA packet instead
|
# Should now see LA packet instead
|
||||||
session
|
click(session, css(".leaflet-marker-icon", at: 0))
|
||||||
|> click(css(".leaflet-marker-icon", at: 0))
|
|
||||||
|
|
||||||
Process.sleep(1000)
|
Process.sleep(1000)
|
||||||
|
|
||||||
assert_has(session, css(".leaflet-popup-content", text: "LA1"))
|
assert_has(session, css(".leaflet-popup-content", text: "LA1"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@tag :integration
|
|
||||||
describe "historical loading with tracked callsigns" do
|
describe "historical loading with tracked callsigns" do
|
||||||
test "loads all packets for tracked callsign regardless of bounds", %{session: session} do
|
@describetag :integration
|
||||||
|
feature "loads all packets for tracked callsign regardless of bounds", %{session: session} do
|
||||||
now = DateTime.utc_now()
|
now = DateTime.utc_now()
|
||||||
|
|
||||||
# Create packets for tracked station at different locations
|
# Create packets for tracked station at different locations
|
||||||
packet1 = packet_fixture(%{
|
_packet1 =
|
||||||
sender: "TRACK1-9",
|
packet_fixture(%{
|
||||||
base_callsign: "TRACK1",
|
sender: "TRACK1-9",
|
||||||
ssid: "9",
|
base_callsign: "TRACK1",
|
||||||
lat: 40.7128,
|
ssid: "9",
|
||||||
lon: -74.0060,
|
lat: 40.7128,
|
||||||
received_at: DateTime.add(now, -45 * 60, :second),
|
lon: -74.0060,
|
||||||
comment: "NYC position"
|
received_at: DateTime.add(now, -45 * 60, :second),
|
||||||
})
|
comment: "NYC position"
|
||||||
|
})
|
||||||
packet2 = packet_fixture(%{
|
|
||||||
sender: "TRACK1-9",
|
_packet2 =
|
||||||
base_callsign: "TRACK1",
|
packet_fixture(%{
|
||||||
ssid: "9",
|
sender: "TRACK1-9",
|
||||||
lat: 41.8781,
|
base_callsign: "TRACK1",
|
||||||
lon: -87.6298,
|
ssid: "9",
|
||||||
received_at: DateTime.add(now, -30 * 60, :second),
|
lat: 41.8781,
|
||||||
comment: "Chicago position"
|
lon: -87.6298,
|
||||||
})
|
received_at: DateTime.add(now, -30 * 60, :second),
|
||||||
|
comment: "Chicago position"
|
||||||
packet3 = packet_fixture(%{
|
})
|
||||||
sender: "TRACK1-9",
|
|
||||||
base_callsign: "TRACK1",
|
_packet3 =
|
||||||
ssid: "9",
|
packet_fixture(%{
|
||||||
lat: 34.0522,
|
sender: "TRACK1-9",
|
||||||
lon: -118.2437,
|
base_callsign: "TRACK1",
|
||||||
received_at: DateTime.add(now, -15 * 60, :second),
|
ssid: "9",
|
||||||
comment: "LA position"
|
lat: 34.0522,
|
||||||
})
|
lon: -118.2437,
|
||||||
|
received_at: DateTime.add(now, -15 * 60, :second),
|
||||||
|
comment: "LA position"
|
||||||
|
})
|
||||||
|
|
||||||
# Navigate to tracked callsign URL
|
# Navigate to tracked callsign URL
|
||||||
session
|
session
|
||||||
|> visit("/TRACK1-9")
|
|> visit("/TRACK1-9")
|
||||||
|> assert_has(css("#map"))
|
|> assert_has(css("#map"))
|
||||||
|
|
||||||
# Wait for map to load and center on latest position
|
# Wait for map to load and center on latest position
|
||||||
Process.sleep(3000)
|
Process.sleep(3000)
|
||||||
|
|
||||||
# Should see trail connecting all positions
|
# Should see trail connecting all positions
|
||||||
# Verify we have markers (latest position + trail points)
|
# Verify we have markers (latest position + trail points)
|
||||||
marker_count =
|
marker_count =
|
||||||
session
|
session
|
||||||
|> all(css(".leaflet-marker-icon"))
|
|> all(css(".leaflet-marker-icon"))
|
||||||
|> length()
|
|> length()
|
||||||
|
|
||||||
# Should have at least 1 marker for current position
|
# Should have at least 1 marker for current position
|
||||||
assert marker_count >= 1, "Expected markers for tracked station"
|
assert marker_count >= 1, "Expected markers for tracked station"
|
||||||
|
|
||||||
# Verify polyline trail exists
|
# Verify polyline trail exists
|
||||||
assert_has(session, css(".leaflet-pane .leaflet-overlay-pane polyline"))
|
assert_has(session, css(".leaflet-pane .leaflet-overlay-pane polyline"))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
2
vendor/aprs
vendored
2
vendor/aprs
vendored
|
|
@ -1 +1 @@
|
||||||
Subproject commit 7e120409975f8e76af0f1c7b5446978685151a52
|
Subproject commit b072eb7b693e5bc48a24867aeebbc928df877e59
|
||||||
Loading…
Add table
Reference in a new issue