diff --git a/assets/js/features/weather_charts.ts b/assets/js/features/weather_charts.ts index ece6746..0a6ef34 100644 --- a/assets/js/features/weather_charts.ts +++ b/assets/js/features/weather_charts.ts @@ -277,13 +277,13 @@ function createChartHook(configKey: string): Hook { self.el.dataset.weatherHistory, ); if (data.length === 0) { - console.log("No weather data available for chart"); + console.debug("No weather data available for chart"); return; } // Skip rendering if we have less than 2 data points (can't create a meaningful time series) if (data.length < 2) { - console.log( + console.debug( "Insufficient weather data for chart (need at least 2 data points)", ); return; diff --git a/assets/js/map.ts b/assets/js/map.ts index e666624..cfd4e44 100644 --- a/assets/js/map.ts +++ b/assets/js/map.ts @@ -80,7 +80,6 @@ const DISABLE_CLUSTERING_AT_ZOOM = 11; let MapAPRSMap = { mounted() { const self = this as unknown as LiveViewHookContext; - console.log("APRSMap hook mounted() called on element:", this.el); // Initialize error tracking self.errors = []; self.initializationAttempts = 0; @@ -346,15 +345,12 @@ let MapAPRSMap = { maxClusterRadius: 80, iconCreateFunction: function (cluster: MarkerCluster) { const count = cluster.getChildCount(); - let size = "small"; let className = "marker-cluster-small"; if (count > 10) { - size = "medium"; className = "marker-cluster-medium"; } if (count > 50) { - size = "large"; className = "marker-cluster-large"; } @@ -462,7 +458,11 @@ let MapAPRSMap = { typeof self.pushEvent === "function" && !self.isDestroyed ) { - self.pushEvent("map_ready", {}); + try { + self.pushEvent("map_ready", {}); + } catch (e) { + console.debug("Failed to push map_ready event:", e); + } if (self.map) { saveMapState(self.map, self.pushEvent.bind(self)); self.sendBoundsToServer(); @@ -492,7 +492,7 @@ let MapAPRSMap = { // Process any pending markers that were queued before map was ready if (self.pendingMarkers && self.pendingMarkers.length > 0) { - console.log( + console.debug( `Processing ${self.pendingMarkers.length} pending markers`, ); self.pendingMarkers.forEach((markerData: MarkerData) => { @@ -598,7 +598,11 @@ let MapAPRSMap = { // If zoom changed significantly (more than 2 levels), request reload of normal markers // but preserve historical ones and current position if (zoomDifference > 2) { - self.pushEvent("refresh_markers", {}); + try { + self.pushEvent("refresh_markers", {}); + } catch (e) { + console.debug("Failed to push refresh_markers event:", e); + } } self.lastZoom = currentZoom; @@ -629,7 +633,7 @@ let MapAPRSMap = { window.addEventListener("resize", self.resizeHandler); // Add a delayed size check - setTimeout(() => { + self.sizeCheckTimeout = setTimeout(() => { if (self.el && self.el.parentNode) { const rect = self.el.getBoundingClientRect(); if (self.map && rect.width > 0 && rect.height > 0) { @@ -651,7 +655,7 @@ let MapAPRSMap = { // Process any pending markers once the map is ready self.map.whenReady(() => { if (self.pendingMarkers && self.pendingMarkers.length > 0) { - console.log(`Processing ${self.pendingMarkers.length} pending markers`); + console.debug(`Processing ${self.pendingMarkers.length} pending markers`); self.pendingMarkers.forEach((markerData) => { self.addMarker(markerData); }); @@ -721,13 +725,15 @@ let MapAPRSMap = { ">

Map Loading Error

-

${message}

+

Please refresh the page or check the browser console for details.

`; + const msgEl = self.el.querySelector('#map-error-message'); + if (msgEl) msgEl.textContent = message; } }, @@ -735,18 +741,15 @@ let MapAPRSMap = { const self = this as unknown as LiveViewHookContext; // Long press timer - let longPressTimer: any = null; + let longPressTimer: ReturnType | null = null; let startX = 0; let startY = 0; const longPressDuration = 750; // 750ms for long press const movementThreshold = 10; // pixels - const handleLongPress = (e: TouchEvent) => { - if (e.touches.length !== 1) return; // Only handle single touch - - const touch = e.touches[0]; + const handleLongPress = (clientX: number, clientY: number) => { const latlng = self.map!.containerPointToLatLng( - L.point(touch.clientX, touch.clientY), + L.point(clientX, clientY), ); // Find the nearest marker @@ -781,7 +784,7 @@ let MapAPRSMap = { longPressTimer = setTimeout(() => { if (!self.isDestroyed && self.map) { - handleLongPress(e.originalEvent); + handleLongPress(touch.clientX, touch.clientY); } }, longPressDuration); }); @@ -1333,7 +1336,7 @@ let MapAPRSMap = { self.handleEvent( "add_historical_packets_batch", (data: { packets: MarkerData[]; batch: number; is_final: boolean }) => { - console.log("Received add_historical_packets_batch event:", { + console.debug("Received add_historical_packets_batch event:", { packetCount: data.packets?.length || 0, batch: data.batch, is_final: data.is_final, @@ -1726,7 +1729,7 @@ let MapAPRSMap = { sendBoundsToServer() { const self = this as unknown as LiveViewHookContext; - console.log( + console.debug( "sendBoundsToServer called, map:", !!self.map, "isDestroyed:", @@ -1758,7 +1761,7 @@ let MapAPRSMap = { }, zoom: zoom, }; - console.log("Sending bounds_changed event:", boundsData); + console.debug("Sending bounds_changed event:", boundsData); self.pushEvent("bounds_changed", boundsData); } } catch (error) { @@ -1772,8 +1775,8 @@ let MapAPRSMap = { if ( !data || !data.id || - !data.lat || - !data.lng || + data.lat == null || + data.lng == null || typeof data.lat !== "number" || typeof data.lng !== "number" ) { @@ -2500,6 +2503,12 @@ let MapAPRSMap = { self.programmaticMoveTimeout = undefined; } + // Clear size check timeout + if (self.sizeCheckTimeout !== undefined) { + clearTimeout(self.sizeCheckTimeout); + self.sizeCheckTimeout = undefined; + } + // Clear initialization timeout if (self.initializationTimeout !== undefined) { clearTimeout(self.initializationTimeout); diff --git a/config/config.exs b/config/config.exs index a6f6fb2..0d479fb 100644 --- a/config/config.exs +++ b/config/config.exs @@ -54,9 +54,6 @@ config :aprsme, :position_tracking, # Position change threshold in degrees (~100 meters at equator) change_threshold: 0.001 -config :aprsme, - ecto_repos: [Aprsme.Repo] - config :aprsme, ecto_repos: [Aprsme.Repo], aprs_is_server: System.get_env("APRS_SERVER", "dallas.aprs2.net"), diff --git a/config/dev.exs b/config/dev.exs index 55dc060..6170594 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -63,7 +63,7 @@ config :aprsme, AprsmeWeb.Endpoint, patterns: [ ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", ~r"priv/gettext/.*(po)$", - ~r"lib/temp_web/(?:controllers|live|components|router)/?.*\.(ex|heex)$" + ~r"lib/aprsme_web/(?:controllers|live|components|router)/?.*\.(ex|heex)$" ] ] diff --git a/config/runtime.exs b/config/runtime.exs index 6640821..01f3c71 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -58,7 +58,7 @@ if config_env() == :prod do config :aprsme, Aprsme.Repo, url: database_url, # Disable SSL/TLS for database connections - ssl: false, + ssl: System.get_env("DB_SSL", "false") == "true", # Increased pool size for better concurrency (was 25) pool_size: String.to_integer(System.get_env("POOL_SIZE") || "45"), # Increased timeout for ARM system under load diff --git a/lib/aprsme/application.ex b/lib/aprsme/application.ex index 8619e32..d3d5221 100644 --- a/lib/aprsme/application.ex +++ b/lib/aprsme/application.ex @@ -48,6 +48,8 @@ defmodule Aprsme.Application do # Start a worker by calling: Aprsme.Worker.start_link(arg) # {Aprsme.Worker, arg} {Registry, keys: :duplicate, name: Registry.PubSub, partitions: System.schedulers_online()}, + # Start ReplayRegistry for unique replay sessions per user + {Registry, keys: :unique, name: Aprsme.ReplayRegistry, partitions: System.schedulers_online()}, # Start cleanup scheduler for periodic packet cleanup Aprsme.CleanupScheduler, Aprsme.PostgresNotifier, @@ -130,7 +132,7 @@ defmodule Aprsme.Application do # Gettext translations are automatically compiled during Mix compilation rescue error -> - Logger.error("Failed to run migrations: #{inspect(error)}") + Logger.error("Failed to run migrations: #{inspect(error)}. Application may be in an inconsistent state.") # Don't crash the application, just log the error :ok end diff --git a/lib/aprsme/cluster/leader_election.ex b/lib/aprsme/cluster/leader_election.ex index 0736a47..e61b151 100644 --- a/lib/aprsme/cluster/leader_election.ex +++ b/lib/aprsme/cluster/leader_election.ex @@ -316,7 +316,7 @@ defmodule Aprsme.Cluster.LeaderElection do end defp pid_alive?(pid, pid_node) do - case :rpc.call(pid_node, Process, :alive?, [pid]) do + case :rpc.call(pid_node, Process, :alive?, [pid], 5000) do {:badrpc, _} -> false result -> result == true end diff --git a/lib/aprsme/connection_monitor.ex b/lib/aprsme/connection_monitor.ex index 52ba255..4f508b9 100644 --- a/lib/aprsme/connection_monitor.ex +++ b/lib/aprsme/connection_monitor.ex @@ -144,7 +144,7 @@ defmodule Aprsme.ConnectionMonitor do # Get stats from remote nodes via RPC remote_stats = Enum.reduce(Node.list(), %{}, fn remote_node, acc -> - case :rpc.call(remote_node, __MODULE__, :get_stats, []) do + case :rpc.call(remote_node, __MODULE__, :get_stats, [], 5000) do {:badrpc, _} -> acc stats -> Map.put(acc, remote_node, stats) end diff --git a/lib/aprsme/is/is.ex b/lib/aprsme/is/is.ex index f14925f..41ab633 100644 --- a/lib/aprsme/is/is.ex +++ b/lib/aprsme/is/is.ex @@ -285,6 +285,7 @@ defmodule Aprsme.Is do end end + @impl true def handle_call(:get_status, _from, state) do connected = state.socket != nil @@ -317,6 +318,7 @@ defmodule Aprsme.Is do end end + @impl true def handle_info(:send_keepalive, state) do case state.socket do nil -> @@ -345,6 +347,7 @@ defmodule Aprsme.Is do handle_socket_data(data, state) end + @impl true def handle_info({:backpressure, :activate}, %{socket: nil} = state) do {:noreply, state} end @@ -363,6 +366,7 @@ defmodule Aprsme.Is do {:noreply, %{state | backpressure_active: true, timer: nil, safety_valve_timer: safety_valve_timer}} end + @impl true def handle_info({:backpressure, :deactivate}, %{backpressure_active: false} = state) do {:noreply, state} end @@ -382,6 +386,7 @@ defmodule Aprsme.Is do {:noreply, %{state | backpressure_active: false, safety_valve_timer: nil, timer: timer}} end + @impl true def handle_info(:backpressure_safety_valve, %{backpressure_active: false} = state) do {:noreply, state} end @@ -398,6 +403,7 @@ defmodule Aprsme.Is do {:noreply, %{state | backpressure_active: false, safety_valve_timer: nil, timer: timer}} end + @impl true def handle_info({:tcp_closed, _socket}, state) do Logger.warning("Socket has been closed by remote server - will reconnect") # Cancel any existing timers @@ -421,6 +427,7 @@ defmodule Aprsme.Is do {:noreply, state} end + @impl true def handle_info({:tcp_error, _socket, reason}, state) do Logger.error("Connection error: #{inspect(reason)} - will reconnect") # Cancel any existing timers @@ -444,6 +451,7 @@ defmodule Aprsme.Is do {:noreply, state} end + @impl true def handle_info(:reconnect, state) do Logger.info("Attempting to reconnect to APRS-IS...") diff --git a/lib/aprsme/packet_replay.ex b/lib/aprsme/packet_replay.ex index aedb91b..1908346 100644 --- a/lib/aprsme/packet_replay.ex +++ b/lib/aprsme/packet_replay.ex @@ -297,7 +297,7 @@ defmodule Aprsme.PacketReplay do @impl true def handle_call(:resume, _from, %{paused: true} = state) do # Force the next packet to be sent soon - send(self(), {:continue_replay}) + send(self(), :start_replay) _ = Endpoint.broadcast(state.replay_topic, "replay_resumed", %{ diff --git a/lib/aprsme/spatial_pubsub.ex b/lib/aprsme/spatial_pubsub.ex index d6f9274..64f89c7 100644 --- a/lib/aprsme/spatial_pubsub.ex +++ b/lib/aprsme/spatial_pubsub.ex @@ -251,6 +251,12 @@ defmodule Aprsme.SpatialPubSub do } end + defp normalize_bounds(invalid) do + require Logger + Logger.warning("normalize_bounds called with invalid bounds map: #{inspect(invalid)}") + %{north: 0.0, south: 0.0, east: 0.0, west: 0.0} + end + defp ensure_float(val) when is_binary(val) do case Float.parse(val) do {f, _} -> f diff --git a/lib/aprsme/weather_cache.ex b/lib/aprsme/weather_cache.ex index be53c09..5ccc769 100644 --- a/lib/aprsme/weather_cache.ex +++ b/lib/aprsme/weather_cache.ex @@ -14,23 +14,19 @@ defmodule Aprsme.WeatherCache do The table is typically created in application.ex alongside other ETS tables. """ def setup do - _ = - case :ets.info(@cache_name) do - :undefined -> - :ets.new(@cache_name, [ - :named_table, - :public, - :set, - :protected, - read_concurrency: true, - write_concurrency: true - ]) - - _ -> - :ok - end + if :ets.whereis(@cache_name) == :undefined do + :ets.new(@cache_name, [ + :named_table, + :public, + :set, + read_concurrency: true, + write_concurrency: true + ]) + end :ok + rescue + ArgumentError -> :ok end @doc """