diff --git a/.github/workflows/elixir.yaml b/.github/workflows/elixir.yaml index c7b712e..88337b0 100644 --- a/.github/workflows/elixir.yaml +++ b/.github/workflows/elixir.yaml @@ -48,23 +48,18 @@ jobs: otp: ["27.3.4"] elixir: ["1.18.4"] steps: - # Step: Setup Elixir + Erlang image as the base. - - name: Set up Elixir + # Step: Setup Elixir + Erlang + Gleam using erlef/setup-beam + - name: Set up Elixir, Erlang, and Gleam uses: erlef/setup-beam@v1 with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} + gleam-version: "1.5.1" # Step: Check out the code. - name: Checkout code uses: actions/checkout@v4 - # Step: Install Gleam - - name: Set up Gleam - uses: gleam-lang/setup-gleam@v1 - with: - gleam-version: "1.5.1" - # Step: Install mix_gleam archive - name: Install mix_gleam run: mix archive.install hex mix_gleam 0.6.2 --force diff --git a/assets/js/features/trail_manager.ts b/assets/js/features/trail_manager.ts index 925d5b0..4810807 100644 --- a/assets/js/features/trail_manager.ts +++ b/assets/js/features/trail_manager.ts @@ -78,6 +78,24 @@ export class TrailManager { } } + setTrailDuration(durationHours: number) { + this.trailDuration = durationHours * 60 * 60 * 1000; // Convert hours to milliseconds + + // Clean up existing trails based on new duration + const cutoffTime = Date.now() - this.trailDuration; + + this.trails.forEach((trailState, baseCallsign) => { + // Filter positions based on new duration (skip historical dots) + trailState.positions = trailState.positions.filter((pos) => { + const posTimestamp = typeof pos.timestamp === "string" ? new Date(pos.timestamp).getTime() : pos.timestamp; + return posTimestamp >= cutoffTime; + }); + + // Update trail visualization + this.updateTrailVisualization(baseCallsign, trailState, true); + }); + } + addPosition( markerId: string, lat: number, diff --git a/assets/js/map.ts b/assets/js/map.ts index 73e1815..ea8d23a 100644 --- a/assets/js/map.ts +++ b/assets/js/map.ts @@ -663,6 +663,13 @@ let MapAPRSMap = { } }); + // Handle trail duration updates from LiveView + self.handleEvent("update_trail_duration", (data: { duration_hours: number }) => { + if (self.trailManager) { + self.trailManager.setTrailDuration(data.duration_hours); + } + }); + // Handle new packets from LiveView self.handleEvent("new_packet", (data: MarkerData) => { try { diff --git a/assets/js/map_helpers.ts b/assets/js/map_helpers.ts index 04b7232..caf1950 100644 --- a/assets/js/map_helpers.ts +++ b/assets/js/map_helpers.ts @@ -34,8 +34,27 @@ export function parseTimestamp(timestamp: string | number | Date | undefined): n /** * Get trail ID from marker data */ -export function getTrailId(data: { callsign_group?: string; callsign?: string; id: string }): string { - return data.callsign_group || data.callsign || data.id; +export function getTrailId(data: { callsign_group?: string; callsign?: string; id: string | number }): string { + // Prioritize callsign_group and callsign over extracting from ID + if (data.callsign_group) { + return data.callsign_group; + } + + if (data.callsign) { + return data.callsign; + } + + // For historical markers like "hist_CALLSIGN_123", extract the base callsign + const id = String(data.id); + if (id.startsWith("hist_")) { + // Remove hist_ prefix and any trailing numeric index + const withoutPrefix = id.replace(/^hist_/, ""); + // Remove trailing _digits pattern + return withoutPrefix.replace(/_\d+$/, ""); + } + + // For regular IDs, return as string + return String(data.id); } /** diff --git a/lib/aprsme_web/live/map_live/index.ex b/lib/aprsme_web/live/map_live/index.ex index d5e74e8..9fb3f9f 100644 --- a/lib/aprsme_web/live/map_live/index.ex +++ b/lib/aprsme_web/live/map_live/index.ex @@ -360,6 +360,9 @@ defmodule AprsmeWeb.MapLive.Index do socket = assign(socket, trail_duration: duration, packet_age_threshold: new_threshold) + # Update client-side TrailManager with new duration + socket = push_event(socket, "update_trail_duration", %{duration_hours: hours}) + # Trigger cleanup to remove packets that are now outside the new duration send(self(), :cleanup_old_packets) diff --git a/test/assets/js/map_helpers_test.ts b/test/assets/js/map_helpers_test.ts index 3eae93d..6181248 100644 --- a/test/assets/js/map_helpers_test.ts +++ b/test/assets/js/map_helpers_test.ts @@ -62,6 +62,50 @@ describe('map_helpers', () => { }; expect(getTrailId(data)).toBe('ID-1'); }); + + test('extracts base callsign from historical ID format', () => { + const data = { + id: 'hist_MYCALL_123' + }; + expect(getTrailId(data)).toBe('MYCALL'); + }); + + test('handles historical ID with complex callsign', () => { + const data = { + id: 'hist_KD8ABC-9_456' + }; + expect(getTrailId(data)).toBe('KD8ABC-9'); + }); + + test('prioritizes callsign_group over historical ID extraction', () => { + const data = { + callsign_group: 'GROUP-1', + id: 'hist_MYCALL_123' + }; + expect(getTrailId(data)).toBe('GROUP-1'); + }); + + test('prioritizes callsign over historical ID extraction', () => { + const data = { + callsign: 'REALCALL', + id: 'hist_MYCALL_123' + }; + expect(getTrailId(data)).toBe('REALCALL'); + }); + + test('handles numeric ID', () => { + const data = { + id: 12345 + }; + expect(getTrailId(data)).toBe('12345'); + }); + + test('handles numeric historical ID', () => { + const data = { + id: 'hist_MYCALL_12345' + }; + expect(getTrailId(data)).toBe('MYCALL'); + }); }); describe('saveMapState', () => {