fix: resolve 19 bugs across Elixir, JS/TS, and config
Critical: - Remove :protected from WeatherCache ETS (conflicted with :public) - Register Aprsme.ReplayRegistry in supervision tree - Route :continue_replay dead message to :start_replay handler - Add 5000ms timeout to unbounded :rpc.call calls - Fix falsy lat/lng check rejecting valid 0 coordinates Medium: - Fix live_reload pattern (temp_web -> aprsme_web) - Remove duplicate ecto_repos config - Make DB SSL configurable via DB_SSL env var - Track sizeCheckTimeout on self for cleanup in destroyed() - Wrap pushEvent calls in try/catch - Remove unused size variable in marker cluster icon - Capture touch coords at touchstart instead of stale TouchEvent - Demote console.log to console.debug in production code - Add catch-all to normalize_bounds preventing GenServer crash Style: - Add missing @impl true annotations in is.ex - Improve migration failure error message - Use textContent instead of innerHTML for error messages - Use proper type for longPressTimer instead of any
This commit is contained in:
parent
b86153cd27
commit
6eda1c0290
12 changed files with 66 additions and 48 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = {
|
|||
">
|
||||
<div>
|
||||
<h3 style="margin: 0 0 10px 0;">Map Loading Error</h3>
|
||||
<p style="margin: 0; font-size: 14px;">${message}</p>
|
||||
<p id="map-error-message" style="margin: 0; font-size: 14px;"></p>
|
||||
<p style="margin: 10px 0 0 0; font-size: 12px; color: #856404;">
|
||||
Please refresh the page or check the browser console for details.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
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<typeof setTimeout> | 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);
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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)$"
|
||||
]
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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...")
|
||||
|
||||
|
|
|
|||
|
|
@ -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", %{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 """
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue