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 """