From b8a9b8a4658b1021e7498ee4d470d5c1e7f48263 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Tue, 21 Apr 2026 10:07:00 -0500 Subject: [PATCH] chore(dialyzer): enable stricter flags and fix 97 resulting findings Turned on :error_handling, :underspecs, and :unmatched_returns in mix.exs dialyzer config. The 97 warnings this surfaced were fixed in place rather than suppressed: - unmatched_return (79): explicit discard with `_ = ...` for fire-and-forget side effects (Process.cancel_timer, :ets.new, send/2), and pattern-matched `:ok = ...` for control-plane Phoenix.PubSub subscribe/unsubscribe/broadcast calls so a future return-shape change fails loud. - contract_supertype (18): tightened @spec arg and return types on data_builder, historical_loader, url_params, packet_utils, encoding_utils, aprs_symbol, weather_controller, packet_replay to match each function's actual success typing. No behavioural change. mix compile clean, 1008 tests pass, dialyzer count is now 0. --- lib/aprsme/application.ex | 25 +++-- lib/aprsme/circuit_breaker.ex | 2 +- lib/aprsme/cleanup_scheduler.ex | 17 +-- lib/aprsme/cluster/connection_manager.ex | 4 +- lib/aprsme/cluster/leader_election.ex | 37 ++++--- lib/aprsme/cluster/packet_distributor.ex | 4 +- lib/aprsme/cluster/packet_receiver.ex | 2 +- lib/aprsme/connection_monitor.ex | 11 +- lib/aprsme/db_optimizer.ex | 4 +- lib/aprsme/deployment_notifier.ex | 11 +- lib/aprsme/encoding_utils.ex | 21 +++- lib/aprsme/error_notifier.ex | 7 +- lib/aprsme/packet_consumer.ex | 27 +++-- lib/aprsme/packet_replay.ex | 101 +++++++++-------- lib/aprsme/partition_manager.ex | 26 +++-- lib/aprsme/postgres_notifier.ex | 2 +- lib/aprsme/regex_cache.ex | 2 +- lib/aprsme/release.ex | 35 +++--- lib/aprsme/shutdown_handler.ex | 2 +- lib/aprsme/spatial_pubsub.ex | 9 +- lib/aprsme/streaming_packets_pubsub.ex | 6 +- lib/aprsme_web/aprs_symbol.ex | 2 +- .../controllers/api/v1/weather_controller.ex | 4 +- lib/aprsme_web/controllers/page_controller.ex | 2 +- lib/aprsme_web/live/bad_packets_live/index.ex | 2 +- lib/aprsme_web/live/info_live/show.ex | 7 +- lib/aprsme_web/live/locale_hook.ex | 7 +- lib/aprsme_web/live/map_live/data_builder.ex | 50 +++++++-- .../live/map_live/historical_loader.ex | 13 ++- lib/aprsme_web/live/map_live/index.ex | 104 ++++++++++-------- .../live/map_live/packet_batcher.ex | 2 +- lib/aprsme_web/live/map_live/url_params.ex | 6 +- .../live/packets_live/callsign_view.ex | 7 +- lib/aprsme_web/live/packets_live/index.ex | 7 +- lib/aprsme_web/live/shared/packet_utils.ex | 4 +- lib/aprsme_web/live/status_live/index.ex | 64 +++++------ lib/aprsme_web/plugs/health_check.ex | 2 +- lib/aprsme_web/plugs/set_locale.ex | 2 +- lib/aprsme_web/user_auth.ex | 7 +- mix.exs | 1 + 40 files changed, 372 insertions(+), 276 deletions(-) diff --git a/lib/aprsme/application.ex b/lib/aprsme/application.ex index c9f9be4..137a074 100644 --- a/lib/aprsme/application.ex +++ b/lib/aprsme/application.ex @@ -10,7 +10,7 @@ defmodule Aprsme.Application do @impl true def start(_type, _args) do # Initialize deployment timestamp - Aprsme.Release.init() + _ = Aprsme.Release.init() # Run migrations on startup migrate() @@ -83,17 +83,18 @@ defmodule Aprsme.Application do {:ok, sup} = Supervisor.start_link(children, opts) # Attach error notification telemetry handler - Aprsme.ErrorNotifier.attach() + _ = Aprsme.ErrorNotifier.attach() # Now that the Repo is started, run the refresh in a background task # Skip in test environment to avoid DBConnection.OwnershipError env = Application.get_env(:aprsme, :env) - if env != :test do - Aprsme.BroadcastTaskSupervisor.async_execute(fn -> - Aprsme.DeviceIdentification.maybe_refresh_devices() - end) - end + _ = + if env != :test do + Aprsme.BroadcastTaskSupervisor.async_execute(fn -> + Aprsme.DeviceIdentification.maybe_refresh_devices() + end) + end {:ok, sup} end @@ -191,11 +192,11 @@ defmodule Aprsme.Application do # Create ETS tables for caching — use :public so the Cache GenServer (a separate process) # can write to these tables; write_concurrency improves throughput under concurrent writes. - :ets.new(:query_cache, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true]) - :ets.new(:device_cache, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true]) - :ets.new(:symbol_cache, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true]) - :ets.new(:aprsme, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true]) - :ets.insert(:aprsme, {:message_number, 0}) + _ = :ets.new(:query_cache, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true]) + _ = :ets.new(:device_cache, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true]) + _ = :ets.new(:symbol_cache, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true]) + _ = :ets.new(:aprsme, [:set, :public, :named_table, read_concurrency: true, write_concurrency: true]) + _ = :ets.insert(:aprsme, {:message_number, 0}) [ # ETS-based rate limiter diff --git a/lib/aprsme/circuit_breaker.ex b/lib/aprsme/circuit_breaker.ex index fa50710..680f6c9 100644 --- a/lib/aprsme/circuit_breaker.ex +++ b/lib/aprsme/circuit_breaker.ex @@ -180,7 +180,7 @@ defmodule Aprsme.CircuitBreaker do {:ok, result} catch :exit, {:timeout, _} -> - Task.shutdown(task, :brutal_kill) + _ = Task.shutdown(task, :brutal_kill) record_failure(service_name) Logger.warning("Service call timed out", diff --git a/lib/aprsme/cleanup_scheduler.ex b/lib/aprsme/cleanup_scheduler.ex index cc32ef6..f77125c 100644 --- a/lib/aprsme/cleanup_scheduler.ex +++ b/lib/aprsme/cleanup_scheduler.ex @@ -37,14 +37,15 @@ defmodule Aprsme.CleanupScheduler do def handle_info(:schedule_cleanup, %{interval: interval} = state) when is_integer(interval) do Logger.info("Running packet cleanup task") - Aprsme.BroadcastTaskSupervisor.async_execute(fn -> - try do - PacketCleanupWorker.perform(%{}) - rescue - error -> - Logger.error("Packet cleanup task failed: #{inspect(error)}") - end - end) + _ = + Aprsme.BroadcastTaskSupervisor.async_execute(fn -> + try do + PacketCleanupWorker.perform(%{}) + rescue + error -> + Logger.error("Packet cleanup task failed: #{inspect(error)}") + end + end) schedule_next_cleanup(interval) {:noreply, state} diff --git a/lib/aprsme/cluster/connection_manager.ex b/lib/aprsme/cluster/connection_manager.ex index f142fdc..14ec32e 100644 --- a/lib/aprsme/cluster/connection_manager.ex +++ b/lib/aprsme/cluster/connection_manager.ex @@ -17,11 +17,11 @@ defmodule Aprsme.Cluster.ConnectionManager do @impl true def init(_opts) do # Subscribe to leadership changes - Phoenix.PubSub.subscribe(Aprsme.PubSub, "cluster:leadership") + :ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, "cluster:leadership") # Check initial leadership state delay = Application.get_env(:aprsme, :connection_manager_init_delay, 1000) - Process.send_after(self(), :check_initial_state, delay) + _ = Process.send_after(self(), :check_initial_state, delay) {:ok, %{connection_started: false}} end diff --git a/lib/aprsme/cluster/leader_election.ex b/lib/aprsme/cluster/leader_election.ex index 19a474f..ae17987 100644 --- a/lib/aprsme/cluster/leader_election.ex +++ b/lib/aprsme/cluster/leader_election.ex @@ -127,11 +127,12 @@ defmodule Aprsme.Cluster.LeaderElection do case :global.whereis_name(@election_key) do pid when pid == self() -> - if !state.is_leader do - Logger.info("Re-confirming leadership on node #{node()}") - :persistent_term.put({__MODULE__, :is_leader}, true) - notify_leadership_change(true) - end + _ = + if !state.is_leader do + Logger.info("Re-confirming leadership on node #{node()}") + :persistent_term.put({__MODULE__, :is_leader}, true) + :ok = notify_leadership_change(true) + end {:noreply, %{state | is_leader: true, leader_node: node()}} @@ -145,12 +146,13 @@ defmodule Aprsme.Cluster.LeaderElection do state = verify_leadership(state) # Re-attempt election if we're not leader - if not state.is_leader do - Process.send_after(self(), :attempt_election, 100) - end + _ = + if not state.is_leader do + Process.send_after(self(), :attempt_election, 100) + end # Schedule next check - Process.send_after(self(), :check_leadership, @check_interval) + _ = Process.send_after(self(), :check_leadership, @check_interval) {:noreply, state} end @@ -173,12 +175,13 @@ defmodule Aprsme.Cluster.LeaderElection do @impl true def terminate(reason, state) do - if state.is_leader do - Logger.info("Leader stepping down due to: #{inspect(reason)}") - :persistent_term.put({__MODULE__, :is_leader}, false) - :global.unregister_name(@election_key) - notify_leadership_change(false) - end + _ = + if state.is_leader do + Logger.info("Leader stepping down due to: #{inspect(reason)}") + :persistent_term.put({__MODULE__, :is_leader}, false) + _ = :global.unregister_name(@election_key) + notify_leadership_change(false) + end :ok end @@ -194,7 +197,7 @@ defmodule Aprsme.Cluster.LeaderElection do _ -> Logger.warning("Lost global leadership registration, stepping down") :persistent_term.put({__MODULE__, :is_leader}, false) - notify_leadership_change(false) + _ = notify_leadership_change(false) %{state | is_leader: false, leader_node: nil} end end @@ -217,7 +220,7 @@ defmodule Aprsme.Cluster.LeaderElection do :yes -> Logger.info("Elected as APRS-IS connection leader on node #{node()}") :persistent_term.put({__MODULE__, :is_leader}, true) - notify_leadership_change(true) + _ = notify_leadership_change(true) {:noreply, %{state | is_leader: true, leader_node: node()}} :no -> diff --git a/lib/aprsme/cluster/packet_distributor.ex b/lib/aprsme/cluster/packet_distributor.ex index 2cb3842..a46c42a 100644 --- a/lib/aprsme/cluster/packet_distributor.ex +++ b/lib/aprsme/cluster/packet_distributor.ex @@ -45,13 +45,13 @@ defmodule Aprsme.Cluster.PacketDistributor do @impl true def init(_opts) do - Phoenix.PubSub.subscribe(Aprsme.PubSub, @pubsub_topic) + :ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, @pubsub_topic) {:ok, %{}} end @impl true def terminate(_reason, _state) do - Phoenix.PubSub.unsubscribe(Aprsme.PubSub, @pubsub_topic) + _ = Phoenix.PubSub.unsubscribe(Aprsme.PubSub, @pubsub_topic) :ok end end diff --git a/lib/aprsme/cluster/packet_receiver.ex b/lib/aprsme/cluster/packet_receiver.ex index c45da4f..a4ce773 100644 --- a/lib/aprsme/cluster/packet_receiver.ex +++ b/lib/aprsme/cluster/packet_receiver.ex @@ -18,7 +18,7 @@ defmodule Aprsme.Cluster.PacketReceiver do @impl true def init(_opts) do # Subscribe to distributed packets - PacketDistributor.subscribe() + :ok = PacketDistributor.subscribe() Logger.info("Started packet receiver on node #{node()}") diff --git a/lib/aprsme/connection_monitor.ex b/lib/aprsme/connection_monitor.ex index 6f9df5e..969bf31 100644 --- a/lib/aprsme/connection_monitor.ex +++ b/lib/aprsme/connection_monitor.ex @@ -188,11 +188,12 @@ defmodule Aprsme.ConnectionMonitor do Logger.info("Draining #{to_drain} connections from node #{Node.self()}") # Broadcast drain event to LiveViews - Phoenix.PubSub.broadcast( - Aprsme.PubSub, - "connection:drain:#{Node.self()}", - {:drain_connections, to_drain} - ) + _ = + Phoenix.PubSub.broadcast( + Aprsme.PubSub, + "connection:drain:#{Node.self()}", + {:drain_connections, to_drain} + ) state end diff --git a/lib/aprsme/db_optimizer.ex b/lib/aprsme/db_optimizer.ex index f79d99d..479de90 100644 --- a/lib/aprsme/db_optimizer.ex +++ b/lib/aprsme/db_optimizer.ex @@ -77,7 +77,7 @@ defmodule Aprsme.DbOptimizer do def analyze_table(table_name) do validate_identifier!(table_name) quoted_name = quote_identifier(table_name) - SQL.query!(Repo, "ANALYZE #{quoted_name}", []) + _ = SQL.query!(Repo, "ANALYZE #{quoted_name}", []) :ok rescue error -> @@ -100,7 +100,7 @@ defmodule Aprsme.DbOptimizer do query = "#{vacuum_type}#{analyze_clause} #{quoted_name}" - SQL.query!(Repo, query, [], timeout: :infinity) + _ = SQL.query!(Repo, query, [], timeout: :infinity) :ok rescue error -> diff --git a/lib/aprsme/deployment_notifier.ex b/lib/aprsme/deployment_notifier.ex index 1aed43d..5c14d0f 100644 --- a/lib/aprsme/deployment_notifier.ex +++ b/lib/aprsme/deployment_notifier.ex @@ -38,11 +38,12 @@ defmodule Aprsme.DeploymentNotifier do else Logger.info("Deployment timestamp changed from #{state.deployed_at} to #{current_deployed_at}") - Phoenix.PubSub.broadcast( - Aprsme.PubSub, - "deployment_events", - {:new_deployment, %{deployed_at: current_deployed_at}} - ) + _ = + Phoenix.PubSub.broadcast( + Aprsme.PubSub, + "deployment_events", + {:new_deployment, %{deployed_at: current_deployed_at}} + ) {:noreply, %{state | deployed_at: current_deployed_at}} end diff --git a/lib/aprsme/encoding_utils.ex b/lib/aprsme/encoding_utils.ex index ae033a6..36e131e 100644 --- a/lib/aprsme/encoding_utils.ex +++ b/lib/aprsme/encoding_utils.ex @@ -210,7 +210,19 @@ defmodule Aprsme.EncodingUtils do iex> :foo in Aprsme.EncodingUtils.weather_fields() false """ - @spec weather_fields() :: [atom()] + @spec weather_fields() :: [ + :temperature + | :humidity + | :wind_speed + | :wind_direction + | :wind_gust + | :pressure + | :rain_1h + | :rain_24h + | :rain_since_midnight + | :snow, + ... + ] def weather_fields do [ :temperature, @@ -371,7 +383,12 @@ defmodule Aprsme.EncodingUtils do iex> Aprsme.EncodingUtils.encoding_info(<<72, 101, 211, 108, 111>>) %{valid_utf8: false, byte_count: 5, char_count: nil, invalid_at: 2} """ - @spec encoding_info(binary()) :: map() + @spec encoding_info(binary()) :: %{ + valid_utf8: boolean(), + byte_count: non_neg_integer(), + char_count: non_neg_integer() | nil, + invalid_at: non_neg_integer() | nil + } def encoding_info(binary) when is_binary(binary) do Encoding.encoding_info(binary) end diff --git a/lib/aprsme/error_notifier.ex b/lib/aprsme/error_notifier.ex index 1acb025..0d7316d 100644 --- a/lib/aprsme/error_notifier.ex +++ b/lib/aprsme/error_notifier.ex @@ -31,9 +31,10 @@ defmodule Aprsme.ErrorNotifier do occurrence = metadata.occurrence # Only send email for first occurrence in production - if should_send_email?(occurrence) do - send_error_email(occurrence) - end + _ = + if should_send_email?(occurrence) do + send_error_email(occurrence) + end :ok end diff --git a/lib/aprsme/packet_consumer.ex b/lib/aprsme/packet_consumer.ex index 40284d6..372f90c 100644 --- a/lib/aprsme/packet_consumer.ex +++ b/lib/aprsme/packet_consumer.ex @@ -142,7 +142,7 @@ defmodule Aprsme.PacketConsumer do end defp reset_batch_timer(%{timer: timer} = state) do - Process.cancel_timer(timer) + _ = Process.cancel_timer(timer) new_timer = Process.send_after(self(), :process_batch, state.batch_timeout) %{state | batch: [], batch_length: 0, timer: new_timer} end @@ -294,7 +294,7 @@ defmodule Aprsme.PacketConsumer do try do {inserted_count, _} = Repo.insert_all(Aprsme.Packet, valid_inserts, insert_opts) - broadcast_packets_async(Enum.map(valid_pairs, fn {_insert, bcast} -> bcast end)) + _ = broadcast_packets_async(Enum.map(valid_pairs, fn {_insert, bcast} -> bcast end)) {inserted_count, invalid_count} rescue @@ -305,9 +305,10 @@ defmodule Aprsme.PacketConsumer do {fallback_inserted, fallback_bcasts} = insert_individually(valid_pairs) # Broadcast whatever was successfully inserted - if fallback_bcasts != [] do - broadcast_packets_async(fallback_bcasts) - end + _ = + if fallback_bcasts != [] do + broadcast_packets_async(fallback_bcasts) + end {fallback_inserted, invalid_count + length(valid_pairs) - fallback_inserted} end @@ -384,15 +385,19 @@ defmodule Aprsme.PacketConsumer do # Replaces the `aprs_packets` pg_notify path (removed as a DB trigger). defp broadcast_legacy_topics(packet, routing_callsign, has_weather?) do - Phoenix.PubSub.broadcast(Aprsme.PubSub, "postgres:aprsme_packets", {:postgres_packet, packet}) + # Hot path: PubSub.broadcast failures tolerated (logged by PubSub itself). + _ = Phoenix.PubSub.broadcast(Aprsme.PubSub, "postgres:aprsme_packets", {:postgres_packet, packet}) - if is_binary(routing_callsign) and routing_callsign != "" do - Phoenix.PubSub.broadcast(Aprsme.PubSub, "packets:#{routing_callsign}", {:postgres_packet, packet}) + _ = + if is_binary(routing_callsign) and routing_callsign != "" do + _ = Phoenix.PubSub.broadcast(Aprsme.PubSub, "packets:#{routing_callsign}", {:postgres_packet, packet}) - if has_weather? do - Phoenix.PubSub.broadcast(Aprsme.PubSub, "weather:#{routing_callsign}", {:weather_packet, packet}) + if has_weather? do + Phoenix.PubSub.broadcast(Aprsme.PubSub, "weather:#{routing_callsign}", {:weather_packet, packet}) + end end - end + + :ok end defp test_env? do diff --git a/lib/aprsme/packet_replay.ex b/lib/aprsme/packet_replay.ex index 351912d..fb6fcaa 100644 --- a/lib/aprsme/packet_replay.ex +++ b/lib/aprsme/packet_replay.ex @@ -197,13 +197,14 @@ defmodule Aprsme.PacketReplay do ) # Send notification to client that replay is starting - Endpoint.broadcast(state.replay_topic, "replay_started", %{ - total_packets: Packets.get_historical_packet_count(Map.new(replay_opts)), - start_time: state.start_time, - end_time: state.end_time, - replay_speed: state.replay_speed, - bounds: state.bounds - }) + _ = + Endpoint.broadcast(state.replay_topic, "replay_started", %{ + total_packets: Packets.get_historical_packet_count(Map.new(replay_opts)), + start_time: state.start_time, + end_time: state.end_time, + replay_speed: state.replay_speed, + bounds: state.bounds + }) # Get packets and start streaming replay_opts_map = @@ -224,10 +225,11 @@ defmodule Aprsme.PacketReplay do [] -> # No packets found, end replay immediately - Endpoint.broadcast(state.replay_topic, "replay_complete", %{ - packets_sent: 0, - message: "No matching packets found for replay" - }) + _ = + Endpoint.broadcast(state.replay_topic, "replay_complete", %{ + packets_sent: 0, + message: "No matching packets found for replay" + }) {:stop, :normal, state} end @@ -243,11 +245,12 @@ defmodule Aprsme.PacketReplay do @impl true def handle_info({:send_packet, packet, stream}, state) do # Send the packet to the client - Endpoint.broadcast(state.replay_topic, "historical_packet", %{ - packet: sanitize_packet_for_transport(packet), - timestamp: packet.received_at, - is_historical: true - }) + _ = + Endpoint.broadcast(state.replay_topic, "historical_packet", %{ + packet: sanitize_packet_for_transport(packet), + timestamp: packet.received_at, + is_historical: true + }) # Update counter new_packets_sent = state.packets_sent + 1 @@ -264,10 +267,11 @@ defmodule Aprsme.PacketReplay do [] -> # No more packets, end replay - Endpoint.broadcast(state.replay_topic, "replay_complete", %{ - packets_sent: new_packets_sent, - message: "Replay complete" - }) + _ = + Endpoint.broadcast(state.replay_topic, "replay_complete", %{ + packets_sent: new_packets_sent, + message: "Replay complete" + }) {:stop, :normal, state} end @@ -276,14 +280,16 @@ defmodule Aprsme.PacketReplay do @impl true @spec handle_call(any(), {pid(), any()}, state()) :: {:reply, any(), state()} def handle_call(:pause, _from, state) do - if state.replay_timer do - Process.cancel_timer(state.replay_timer) - end + _ = + if state.replay_timer do + Process.cancel_timer(state.replay_timer) + end - Endpoint.broadcast(state.replay_topic, "replay_paused", %{ - packets_sent: state.packets_sent, - last_packet_time: state.last_packet_time - }) + _ = + Endpoint.broadcast(state.replay_topic, "replay_paused", %{ + packets_sent: state.packets_sent, + last_packet_time: state.last_packet_time + }) {:reply, :ok, %{state | paused: true, replay_timer: nil}} end @@ -293,10 +299,11 @@ defmodule Aprsme.PacketReplay do # Force the next packet to be sent soon send(self(), {:continue_replay}) - Endpoint.broadcast(state.replay_topic, "replay_resumed", %{ - packets_sent: state.packets_sent, - last_packet_time: state.last_packet_time - }) + _ = + Endpoint.broadcast(state.replay_topic, "replay_resumed", %{ + packets_sent: state.packets_sent, + last_packet_time: state.last_packet_time + }) {:reply, :ok, %{state | paused: false}} end @@ -310,9 +317,10 @@ defmodule Aprsme.PacketReplay do @impl true def handle_call({:set_speed, speed}, _from, state) do # Update speed and notify client - Endpoint.broadcast(state.replay_topic, "replay_speed_changed", %{ - replay_speed: speed - }) + _ = + Endpoint.broadcast(state.replay_topic, "replay_speed_changed", %{ + replay_speed: speed + }) {:reply, :ok, %{state | replay_speed: speed}} end @@ -320,9 +328,10 @@ defmodule Aprsme.PacketReplay do @impl true def handle_call({:update_filters, filters}, _from, state) do # Update filters - this requires restarting the replay - if state.replay_timer do - Process.cancel_timer(state.replay_timer) - end + _ = + if state.replay_timer do + Process.cancel_timer(state.replay_timer) + end # Update state with new filters new_state = @@ -378,15 +387,17 @@ defmodule Aprsme.PacketReplay do @spec terminate(any(), state()) :: :ok def terminate(_reason, state) do # Clean up any timers - if state.replay_timer do - Process.cancel_timer(state.replay_timer) - end + _ = + if state.replay_timer do + Process.cancel_timer(state.replay_timer) + end # Notify client that replay has ended - Endpoint.broadcast(state.replay_topic, "replay_stopped", %{ - packets_sent: state.packets_sent, - message: "Replay stopped" - }) + _ = + Endpoint.broadcast(state.replay_topic, "replay_stopped", %{ + packets_sent: state.packets_sent, + message: "Replay stopped" + }) :ok end @@ -398,7 +409,7 @@ defmodule Aprsme.PacketReplay do {:via, Registry, {Aprsme.ReplayRegistry, "replay:#{user_id}"}} end - @spec sanitize_packet_for_transport(any()) :: map() + @spec sanitize_packet_for_transport(struct()) :: map() defp sanitize_packet_for_transport(packet) do # Convert to map and ensure all fields are JSON-safe packet diff --git a/lib/aprsme/partition_manager.ex b/lib/aprsme/partition_manager.ex index 9ca297b..e02ae27 100644 --- a/lib/aprsme/partition_manager.ex +++ b/lib/aprsme/partition_manager.ex @@ -56,7 +56,7 @@ defmodule Aprsme.PartitionManager do if partition_exists?(name) do acc else - create_partition_with_lock(date) + _ = create_partition_with_lock(date) [name | acc] end end @@ -68,13 +68,14 @@ defmodule Aprsme.PartitionManager do name = partition_name(date) lock_key = :erlang.phash2(name) - Repo.transaction(fn -> - Repo.query!("SELECT pg_advisory_xact_lock($1)", [lock_key]) + _ = + Repo.transaction(fn -> + _ = Repo.query!("SELECT pg_advisory_xact_lock($1)", [lock_key]) - if !partition_exists?(name) do - create_partition(date) - end - end) + if !partition_exists?(name) do + create_partition(date) + end + end) name end @@ -96,7 +97,7 @@ defmodule Aprsme.PartitionManager do end end) |> Enum.map(fn name -> - drop_partition(name) + _ = drop_partition(name) name end) @@ -181,9 +182,10 @@ defmodule Aprsme.PartitionManager do validate_partition_name!(name) - Repo.query!( - "CREATE TABLE IF NOT EXISTS #{quote_identifier(name)} PARTITION OF packets FOR VALUES FROM ('#{from_str}') TO ('#{to_str}')" - ) + _ = + Repo.query!( + "CREATE TABLE IF NOT EXISTS #{quote_identifier(name)} PARTITION OF packets FOR VALUES FROM ('#{from_str}') TO ('#{to_str}')" + ) Logger.debug("Created partition #{name} [#{from_str}, #{to_str})") name @@ -193,7 +195,7 @@ defmodule Aprsme.PartitionManager do # Validate partition name to prevent SQL injection validate_partition_name!(name) - Repo.query!("DROP TABLE IF EXISTS #{quote_identifier(name)}") + _ = Repo.query!("DROP TABLE IF EXISTS #{quote_identifier(name)}") Logger.debug("Dropped partition #{name}") name end diff --git a/lib/aprsme/postgres_notifier.ex b/lib/aprsme/postgres_notifier.ex index 8d1dd16..2469ac2 100644 --- a/lib/aprsme/postgres_notifier.ex +++ b/lib/aprsme/postgres_notifier.ex @@ -26,7 +26,7 @@ defmodule Aprsme.PostgresNotifier do @impl true def handle_info({:notification, _conn, _pid, @event_channel, payload}, state) do - Phoenix.PubSub.broadcast(Aprsme.PubSub, @event_topic, {:postgres_notify, payload}) + _ = Phoenix.PubSub.broadcast(Aprsme.PubSub, @event_topic, {:postgres_notify, payload}) {:noreply, state} end diff --git a/lib/aprsme/regex_cache.ex b/lib/aprsme/regex_cache.ex index 488963d..2e1bfcc 100644 --- a/lib/aprsme/regex_cache.ex +++ b/lib/aprsme/regex_cache.ex @@ -25,7 +25,7 @@ defmodule Aprsme.RegexCache do @impl true def init(_) do # Table stores {pattern, regex, last_access_counter} - :ets.new(@table_name, [:set, :protected, :named_table, read_concurrency: true]) + _table = :ets.new(@table_name, [:set, :protected, :named_table, read_concurrency: true]) {:ok, %{counter: 0}} end diff --git a/lib/aprsme/release.ex b/lib/aprsme/release.ex index 5cd4ad7..6d9d39e 100644 --- a/lib/aprsme/release.ex +++ b/lib/aprsme/release.ex @@ -64,7 +64,7 @@ defmodule Aprsme.Release do end def rollback(repo, version) do - load_app() + _ = load_app() {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version)) end @@ -104,24 +104,25 @@ defmodule Aprsme.Release do # Notify about deployment after a short delay to ensure PubSub is started # In k8s, this will notify all connected clients about the new deployment - if System.get_env("DEPLOYED_AT") do - Task.start(fn -> - # Wait for application to start - Process.sleep(10_000) + _ = + if System.get_env("DEPLOYED_AT") do + Task.start(fn -> + # Wait for application to start + Process.sleep(10_000) - try do - require Logger - - Aprsme.DeploymentNotifier.notify_deployment(deployed_at) - Logger.info("Deployment notification sent for timestamp: #{deployed_at}") - rescue - error -> + try do require Logger - Logger.warning("Failed to send deployment notification: #{inspect(error)}") - end - end) - end + _ = Aprsme.DeploymentNotifier.notify_deployment(deployed_at) + Logger.info("Deployment notification sent for timestamp: #{deployed_at}") + rescue + error -> + require Logger + + Logger.warning("Failed to send deployment notification: #{inspect(error)}") + end + end) + end deployed_at end @@ -158,7 +159,7 @@ defmodule Aprsme.Release do Aprsme.Repo, fn repo -> timeout_seconds = div(timeout, 1000) - SQL.query!(repo, "SET statement_timeout = '#{timeout_seconds}s'", []) + _ = SQL.query!(repo, "SET statement_timeout = '#{timeout_seconds}s'", []) Ecto.Migrator.run(repo, :up, all: true) end, timeout: timeout diff --git a/lib/aprsme/shutdown_handler.ex b/lib/aprsme/shutdown_handler.ex index 56d4393..86fba09 100644 --- a/lib/aprsme/shutdown_handler.ex +++ b/lib/aprsme/shutdown_handler.ex @@ -122,7 +122,7 @@ defmodule Aprsme.ShutdownHandler do log_connection_stats() # Terminate remaining connections - terminate_remaining_connections() + _ = terminate_remaining_connections() # Exit the application System.stop(0) diff --git a/lib/aprsme/spatial_pubsub.ex b/lib/aprsme/spatial_pubsub.ex index 3a5c29f..223abe3 100644 --- a/lib/aprsme/spatial_pubsub.ex +++ b/lib/aprsme/spatial_pubsub.ex @@ -190,10 +190,11 @@ defmodule Aprsme.SpatialPubSub do |> Enum.map(& &1.topic) # Use dedicated broadcast task supervisor for better performance - Aprsme.BroadcastTaskSupervisor.broadcast_async( - topics, - {:spatial_packet, packet} - ) + _ = + Aprsme.BroadcastTaskSupervisor.broadcast_async( + topics, + {:spatial_packet, packet} + ) # Update statistics (immediately return control to GenServer) state = diff --git a/lib/aprsme/streaming_packets_pubsub.ex b/lib/aprsme/streaming_packets_pubsub.ex index 475f8a2..9913c49 100644 --- a/lib/aprsme/streaming_packets_pubsub.ex +++ b/lib/aprsme/streaming_packets_pubsub.ex @@ -69,10 +69,10 @@ defmodule Aprsme.StreamingPacketsPubSub do @impl true def init(_opts) do # Create ETS table for fast lookups - :ets.new(@table_name, [:set, :protected, :named_table, read_concurrency: true]) + _ = :ets.new(@table_name, [:set, :protected, :named_table, read_concurrency: true]) # Monitor subscribers for cleanup - Process.flag(:trap_exit, true) + _ = Process.flag(:trap_exit, true) # pid => monitor_ref — track monitors to demonitor on unsubscribe/update {:ok, %{monitors: %{}}} @@ -105,7 +105,7 @@ defmodule Aprsme.StreamingPacketsPubSub do @impl true def handle_cast({:broadcast, packet}, state) do - maybe_broadcast_packet(packet) + _ = maybe_broadcast_packet(packet) {:noreply, state} end diff --git a/lib/aprsme_web/aprs_symbol.ex b/lib/aprsme_web/aprs_symbol.ex index 4cceda9..8adb715 100644 --- a/lib/aprsme_web/aprs_symbol.ex +++ b/lib/aprsme_web/aprs_symbol.ex @@ -346,7 +346,7 @@ defmodule AprsmeWeb.AprsSymbol do end # Helper function to safely extract a value from a packet or data_extended map - @spec get_packet_field(map(), atom(), any()) :: any() + @spec get_packet_field(map(), atom(), String.t()) :: String.t() defp get_packet_field(packet, field, default) do data_extended = Map.get(packet, :data_extended, Map.get(packet, "data_extended", %{})) || %{} diff --git a/lib/aprsme_web/controllers/api/v1/weather_controller.ex b/lib/aprsme_web/controllers/api/v1/weather_controller.ex index d163ace..8b020ac 100644 --- a/lib/aprsme_web/controllers/api/v1/weather_controller.ex +++ b/lib/aprsme_web/controllers/api/v1/weather_controller.ex @@ -105,8 +105,8 @@ defmodule AprsmeWeb.Api.V1.WeatherController do end end - @spec validate_optional_integer(map(), String.t(), integer()) :: - {:ok, integer()} | {:error, :bad_request, String.t()} + @spec validate_optional_integer(map(), <<_::40>>, 6 | 50) :: + {:ok, integer()} | {:error, :bad_request, <<_::64, _::_*8>>} defp validate_optional_integer(params, key, default) do case Map.get(params, key) do nil -> diff --git a/lib/aprsme_web/controllers/page_controller.ex b/lib/aprsme_web/controllers/page_controller.ex index 446be6c..891791b 100644 --- a/lib/aprsme_web/controllers/page_controller.ex +++ b/lib/aprsme_web/controllers/page_controller.ex @@ -34,7 +34,7 @@ defmodule AprsmeWeb.PageController do # Normal health check db_healthy = try do - Aprsme.Repo.query!("SELECT 1") + _ = Aprsme.Repo.query!("SELECT 1") true rescue _ -> false diff --git a/lib/aprsme_web/live/bad_packets_live/index.ex b/lib/aprsme_web/live/bad_packets_live/index.ex index 9198d42..cb5a1da 100644 --- a/lib/aprsme_web/live/bad_packets_live/index.ex +++ b/lib/aprsme_web/live/bad_packets_live/index.ex @@ -11,7 +11,7 @@ defmodule AprsmeWeb.BadPacketsLive.Index do def mount(_params, _session, socket) do if connected?(socket) do # Subscribe to Postgres notifications for bad packets - Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_events") + :ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_events") # Load initial bad packets bad_packets = fetch_bad_packets() # Extra safeguard to ensure we never show more than 100 diff --git a/lib/aprsme_web/live/info_live/show.ex b/lib/aprsme_web/live/info_live/show.ex index dc42ecc..5aaf436 100644 --- a/lib/aprsme_web/live/info_live/show.ex +++ b/lib/aprsme_web/live/info_live/show.ex @@ -63,9 +63,10 @@ defmodule AprsmeWeb.InfoLive.Show do normalized_callsign = Callsign.normalize(callsign) # Subscribe to callsign-specific topic for live updates - if connected?(socket) do - Phoenix.PubSub.subscribe(Aprsme.PubSub, "packets:#{normalized_callsign}") - end + _ = + if connected?(socket) do + Phoenix.PubSub.subscribe(Aprsme.PubSub, "packets:#{normalized_callsign}") + end packet = get_latest_packet(normalized_callsign) packet = if packet, do: SharedPacketHandler.enrich_with_device_info(packet) diff --git a/lib/aprsme_web/live/locale_hook.ex b/lib/aprsme_web/live/locale_hook.ex index cac4c9d..aedb185 100644 --- a/lib/aprsme_web/live/locale_hook.ex +++ b/lib/aprsme_web/live/locale_hook.ex @@ -15,9 +15,10 @@ defmodule AprsmeWeb.LocaleHook do get_locale_from_session(session) || "en" end - if locale do - Gettext.put_locale(AprsmeWeb.Gettext, locale) - end + _ = + if locale do + Gettext.put_locale(AprsmeWeb.Gettext, locale) + end # Set map_page assign based on the view module map_page = map_page?(socket, params) diff --git a/lib/aprsme_web/live/map_live/data_builder.ex b/lib/aprsme_web/live/map_live/data_builder.ex index 637a61e..497c99d 100644 --- a/lib/aprsme_web/live/map_live/data_builder.ex +++ b/lib/aprsme_web/live/map_live/data_builder.ex @@ -86,16 +86,16 @@ defmodule AprsmeWeb.MapLive.DataBuilder do @doc """ Build packet data with default locale. """ - @spec build_packet_data(map(), boolean()) :: map() | nil - def build_packet_data(packet, is_most_recent_for_callsign) do + @spec build_packet_data(map(), boolean()) :: nil | %{<<_::16, _::_*8>> => any()} + def build_packet_data(packet, is_most_recent_for_callsign) when is_boolean(is_most_recent_for_callsign) do build_packet_data(packet, is_most_recent_for_callsign, "en") end @doc """ Build packet data with default parameters. """ - @spec build_packet_data(map()) :: map() | nil - def build_packet_data(packet) do + @spec build_packet_data(map()) :: nil | %{<<_::16, _::_*8>> => any()} + def build_packet_data(packet) when is_map(packet) do build_packet_data(packet, false, "en") end @@ -161,7 +161,7 @@ defmodule AprsmeWeb.MapLive.DataBuilder do end # Validate coordinates are numeric and within valid ranges - @spec valid_coordinates?(any(), any()) :: boolean() + @spec valid_coordinates?(number() | nil, number() | nil) :: boolean() defp valid_coordinates?(lat, lon) when is_number(lat) and is_number(lon) do # Range checks ensure finite values; infinity would be outside these ranges lat >= -90 and lat <= 90 and lon >= -180 and lon <= 180 @@ -363,7 +363,20 @@ defmodule AprsmeWeb.MapLive.DataBuilder do end end - @spec build_standard_popup_html(map(), float(), float()) :: String.t() + @spec build_standard_popup_html( + %{ + :callsign => binary(), + :callsign_group => binary(), + :comment => any(), + :is_weather_packet => boolean(), + :safe_data_extended => any(), + :symbol_code => any(), + :symbol_table_id => any(), + :timestamp => binary() + }, + float(), + float() + ) :: binary() defp build_standard_popup_html(packet_info, _lat, _lon) do timestamp_dt = TimeHelpers.to_datetime(packet_info.timestamp) cache_buster = System.system_time(:millisecond) @@ -435,7 +448,22 @@ defmodule AprsmeWeb.MapLive.DataBuilder do |> IO.iodata_to_binary() end - @spec build_packet_result(map(), map(), float(), float(), String.t()) :: map() + @spec build_packet_result( + map(), + %{ + :callsign => binary(), + :callsign_group => binary(), + :comment => any(), + :is_weather_packet => boolean(), + :safe_data_extended => any(), + :symbol_code => nil | binary(), + :symbol_table_id => nil | binary(), + :timestamp => binary() + }, + float(), + float(), + binary() + ) :: %{<<_::16, _::_*8>> => any()} defp build_packet_result(packet, packet_info, lat, lon, popup) do # Generate unique ID for live packets to prevent conflicts packet_id = @@ -479,7 +507,11 @@ defmodule AprsmeWeb.MapLive.DataBuilder do # Utility functions for packet field access - @spec get_packet_field(map(), atom() | String.t(), any()) :: any() + @spec get_packet_field( + map(), + :base_callsign | :comment | :data_type | :path | :ssid | :symbol_code | :symbol_table_id, + nil | binary() + ) :: any() defp get_packet_field(packet, field, default) do SharedPacketUtils.get_packet_field(packet, field, default) end @@ -489,7 +521,7 @@ defmodule AprsmeWeb.MapLive.DataBuilder do SharedPacketUtils.get_timestamp(packet) end - @spec to_float(any()) :: float() + @spec to_float(number() | nil) :: float() defp to_float(value) do ParamUtils.to_float(value) end diff --git a/lib/aprsme_web/live/map_live/historical_loader.ex b/lib/aprsme_web/live/map_live/historical_loader.ex index c445251..61ca9d9 100644 --- a/lib/aprsme_web/live/map_live/historical_loader.ex +++ b/lib/aprsme_web/live/map_live/historical_loader.ex @@ -384,13 +384,13 @@ defmodule AprsmeWeb.MapLive.HistoricalLoader do # Very zoomed out - smaller batches, more of them defp get_loading_params_for_zoom(_), do: {50, 5} - @spec calculate_batch_size_for_zoom(integer()) :: integer() + @spec calculate_batch_size_for_zoom(integer()) :: 50 | 75 | 100 | 500 defp calculate_batch_size_for_zoom(zoom) do {batch_size, _} = get_loading_params_for_zoom(zoom) batch_size end - @spec calculate_batch_count_for_zoom(integer()) :: integer() + @spec calculate_batch_count_for_zoom(integer()) :: 2 | 3 | 4 | 5 defp calculate_batch_count_for_zoom(zoom) do {_, batch_count} = get_loading_params_for_zoom(zoom) batch_count @@ -437,10 +437,11 @@ defmodule AprsmeWeb.MapLive.HistoricalLoader do # Limit to prevent too many queries |> Enum.take(20) - if Enum.any?(rf_path_stations) do - # Schedule loading of RF path station packets - Process.send_after(self(), {:load_rf_path_station_packets, rf_path_stations}, 100) - end + _ = + if Enum.any?(rf_path_stations) do + # Schedule loading of RF path station packets + Process.send_after(self(), {:load_rf_path_station_packets, rf_path_stations}, 100) + end socket end diff --git a/lib/aprsme_web/live/map_live/index.ex b/lib/aprsme_web/live/map_live/index.ex index aafb9ca..e813278 100644 --- a/lib/aprsme_web/live/map_live/index.ex +++ b/lib/aprsme_web/live/map_live/index.ex @@ -114,11 +114,12 @@ defmodule AprsmeWeb.MapLive.Index do client_id = "liveview_#{:erlang.phash2(self())}" # Register with connection monitor - if Application.get_env(:aprsme, :cluster_enabled, false) do - Aprsme.ConnectionMonitor.register_connection() - # Subscribe to drain events for this node - Phoenix.PubSub.subscribe(Aprsme.PubSub, "connection:drain:#{Node.self()}") - end + _ = + if Application.get_env(:aprsme, :cluster_enabled, false) do + _ = Aprsme.ConnectionMonitor.register_connection() + # Subscribe to drain events for this node + Phoenix.PubSub.subscribe(Aprsme.PubSub, "connection:drain:#{Node.self()}") + end # Register with spatial PubSub (will get viewport later) # Start with a default viewport that will be updated when we get actual bounds @@ -126,20 +127,20 @@ defmodule AprsmeWeb.MapLive.Index do {:ok, spatial_topic} = Aprsme.SpatialPubSub.register_viewport(client_id, default_bounds) # Subscribe to the spatial topic for this client - Phoenix.PubSub.subscribe(Aprsme.PubSub, spatial_topic) + :ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, spatial_topic) # Still subscribe to bad packets (they don't have location) - Phoenix.PubSub.subscribe(Aprsme.PubSub, "bad_packets") + :ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, "bad_packets") # Subscribe to deployment events - Phoenix.PubSub.subscribe(Aprsme.PubSub, "deployment_events") + :ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, "deployment_events") # Subscribe to StreamingPacketsPubSub with initial bounds - Aprsme.StreamingPacketsPubSub.subscribe_to_bounds(self(), default_bounds) + _ = Aprsme.StreamingPacketsPubSub.subscribe_to_bounds(self(), default_bounds) - Process.send_after(self(), :cleanup_old_packets, 60_000) + _ = Process.send_after(self(), :cleanup_old_packets, 60_000) # Schedule UI update timer to refresh "time ago" display every 30 seconds - Process.send_after(self(), :update_time_display, 30_000) + _ = Process.send_after(self(), :update_time_display, 30_000) socket |> assign(:spatial_client_id, client_id) @@ -155,7 +156,7 @@ defmodule AprsmeWeb.MapLive.Index do end defp do_setup_additional_subscriptions(socket, true) do - Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets") + :ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets") socket end @@ -410,7 +411,7 @@ defmodule AprsmeWeb.MapLive.Index do # Cancel any pending hover end timer socket = if socket.assigns[:hover_end_timer] do - Process.cancel_timer(socket.assigns.hover_end_timer) + _ = Process.cancel_timer(socket.assigns.hover_end_timer) assign(socket, hover_end_timer: nil) else socket @@ -833,33 +834,36 @@ defmodule AprsmeWeb.MapLive.Index do def handle_info({:postgres_packet, packet}, socket) do # Add packet to batcher instead of processing immediately - if socket.assigns[:batcher_pid] do - PacketBatcher.add_packet(socket.assigns.batcher_pid, packet) - else - handle_info_postgres_packet(packet, socket) - end + _ = + if socket.assigns[:batcher_pid] do + PacketBatcher.add_packet(socket.assigns.batcher_pid, packet) + else + handle_info_postgres_packet(packet, socket) + end {:noreply, socket} end def handle_info({:spatial_packet, packet}, socket) do # Add packet to batcher instead of processing immediately - if socket.assigns[:batcher_pid] do - PacketBatcher.add_packet(socket.assigns.batcher_pid, packet) - else - handle_info_postgres_packet(packet, socket) - end + _ = + if socket.assigns[:batcher_pid] do + PacketBatcher.add_packet(socket.assigns.batcher_pid, packet) + else + handle_info_postgres_packet(packet, socket) + end {:noreply, socket} end def handle_info({:streaming_packet, packet}, socket) do # Add packet to batcher instead of processing immediately - if socket.assigns[:batcher_pid] do - PacketBatcher.add_packet(socket.assigns.batcher_pid, packet) - else - handle_info_postgres_packet(packet, socket) - end + _ = + if socket.assigns[:batcher_pid] do + PacketBatcher.add_packet(socket.assigns.batcher_pid, packet) + else + handle_info_postgres_packet(packet, socket) + end {:noreply, socket} end @@ -1860,39 +1864,45 @@ defmodule AprsmeWeb.MapLive.Index do @impl true def terminate(_reason, socket) do # Unregister from connection monitor - if Application.get_env(:aprsme, :cluster_enabled, false) and - not socket.assigns[:connection_draining] do - Aprsme.ConnectionMonitor.unregister_connection() - end + _ = + if Application.get_env(:aprsme, :cluster_enabled, false) and + not socket.assigns[:connection_draining] do + Aprsme.ConnectionMonitor.unregister_connection() + end # Cleanup spatial registration if we have a client ID - if socket.assigns[:spatial_client_id] do - Aprsme.SpatialPubSub.unregister_client(socket.assigns.spatial_client_id) - end + _ = + if socket.assigns[:spatial_client_id] do + Aprsme.SpatialPubSub.unregister_client(socket.assigns.spatial_client_id) + end # Unsubscribe from StreamingPacketsPubSub - Aprsme.StreamingPacketsPubSub.unsubscribe(self()) + _ = Aprsme.StreamingPacketsPubSub.unsubscribe(self()) # Unsubscribe from PubSub topic subscribed in mount - Phoenix.PubSub.unsubscribe(Aprsme.PubSub, "postgres:aprsme_packets") + _ = Phoenix.PubSub.unsubscribe(Aprsme.PubSub, "postgres:aprsme_packets") + + _ = if socket.assigns.buffer_timer, do: Process.cancel_timer(socket.assigns.buffer_timer) - if socket.assigns.buffer_timer, do: Process.cancel_timer(socket.assigns.buffer_timer) # Clean up any pending bounds update timer - if socket.assigns[:bounds_update_timer] do - Process.cancel_timer(socket.assigns.bounds_update_timer) - end + _ = + if socket.assigns[:bounds_update_timer] do + Process.cancel_timer(socket.assigns.bounds_update_timer) + end # Clean up pending hover-end timer — set when the mouse leaves a marker # and only cleared when it re-enters; if the LV terminates between the two, # the timer would otherwise outlive the socket. - if socket.assigns[:hover_end_timer] do - Process.cancel_timer(socket.assigns.hover_end_timer) - end + _ = + if socket.assigns[:hover_end_timer] do + Process.cancel_timer(socket.assigns.hover_end_timer) + end # Clean up any pending batch tasks - if socket.assigns[:pending_batch_tasks] do - Enum.each(socket.assigns.pending_batch_tasks, &Process.cancel_timer/1) - end + _ = + if socket.assigns[:pending_batch_tasks] do + Enum.each(socket.assigns.pending_batch_tasks, &Process.cancel_timer/1) + end :ok end diff --git a/lib/aprsme_web/live/map_live/packet_batcher.ex b/lib/aprsme_web/live/map_live/packet_batcher.ex index 72fa1b8..131b9a3 100644 --- a/lib/aprsme_web/live/map_live/packet_batcher.ex +++ b/lib/aprsme_web/live/map_live/packet_batcher.ex @@ -101,7 +101,7 @@ defmodule AprsmeWeb.MapLive.PacketBatcher do defp cancel_timer(%{timer_ref: nil} = state), do: state defp cancel_timer(%{timer_ref: ref} = state) do - Process.cancel_timer(ref) + _ = Process.cancel_timer(ref) %{state | timer_ref: nil} end diff --git a/lib/aprsme_web/live/map_live/url_params.ex b/lib/aprsme_web/live/map_live/url_params.ex index 9636034..efd7a71 100644 --- a/lib/aprsme_web/live/map_live/url_params.ex +++ b/lib/aprsme_web/live/map_live/url_params.ex @@ -13,7 +13,7 @@ defmodule AprsmeWeb.MapLive.UrlParams do Parse map state from URL parameters. Returns {map_center, zoom} tuple with validated coordinates. """ - @spec parse_map_params(map()) :: {map(), integer()} + @spec parse_map_params(map()) :: {%{lat: float(), lng: float()}, integer()} def parse_map_params(params) do lat = parse_latitude(Map.get(params, "lat")) lng = parse_longitude(Map.get(params, "lng")) @@ -72,12 +72,12 @@ defmodule AprsmeWeb.MapLive.UrlParams do @doc """ Get default map center coordinates. """ - @spec default_center() :: map() + @spec default_center() :: %{lat: float(), lng: float()} def default_center, do: @default_center @doc """ Get default zoom level. """ - @spec default_zoom() :: integer() + @spec default_zoom() :: 5 def default_zoom, do: @default_zoom end diff --git a/lib/aprsme_web/live/packets_live/callsign_view.ex b/lib/aprsme_web/live/packets_live/callsign_view.ex index c9fe0cc..c39eb1d 100644 --- a/lib/aprsme_web/live/packets_live/callsign_view.ex +++ b/lib/aprsme_web/live/packets_live/callsign_view.ex @@ -18,9 +18,10 @@ defmodule AprsmeWeb.PacketsLive.CallsignView do if Callsign.valid?(normalized_callsign) do # Subscribe to live packet updates for this callsign only - if connected?(socket) do - Phoenix.PubSub.subscribe(Aprsme.PubSub, "packets:#{normalized_callsign}") - end + _ = + if connected?(socket) do + Phoenix.PubSub.subscribe(Aprsme.PubSub, "packets:#{normalized_callsign}") + end # Get stored packets for this callsign (up to 100) stored_packets = get_stored_packets(normalized_callsign, 100) diff --git a/lib/aprsme_web/live/packets_live/index.ex b/lib/aprsme_web/live/packets_live/index.ex index c7c2d5b..160484f 100644 --- a/lib/aprsme_web/live/packets_live/index.ex +++ b/lib/aprsme_web/live/packets_live/index.ex @@ -7,9 +7,10 @@ defmodule AprsmeWeb.PacketsLive.Index do @impl true def mount(_params, _session, socket) do - if connected?(socket) do - Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets") - end + _ = + if connected?(socket) do + Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets") + end {:ok, assign(socket, :packets, [])} end diff --git a/lib/aprsme_web/live/shared/packet_utils.ex b/lib/aprsme_web/live/shared/packet_utils.ex index 7843452..dcfd399 100644 --- a/lib/aprsme_web/live/shared/packet_utils.ex +++ b/lib/aprsme_web/live/shared/packet_utils.ex @@ -48,7 +48,7 @@ defmodule AprsmeWeb.Live.Shared.PacketUtils do @doc """ Parse trail duration with validation and bounds checking. """ - @spec parse_trail_duration(binary() | any()) :: integer() + @spec parse_trail_duration(any()) :: 1 | 6 | 12 | 24 | 48 | 168 def parse_trail_duration(duration) when is_binary(duration) do case Integer.parse(duration) do {hours, ""} when hours in [1, 6, 12, 24, 48, 168] -> @@ -65,7 +65,7 @@ defmodule AprsmeWeb.Live.Shared.PacketUtils do @doc """ Parse historical hours with validation. """ - @spec parse_historical_hours(binary() | any()) :: integer() + @spec parse_historical_hours(any()) :: 1 | 3 | 6 | 12 | 24 def parse_historical_hours(hours) when is_binary(hours) do case Integer.parse(hours) do {h, ""} when h in [1, 3, 6, 12, 24] -> diff --git a/lib/aprsme_web/live/status_live/index.ex b/lib/aprsme_web/live/status_live/index.ex index dce9ffe..8aefbbf 100644 --- a/lib/aprsme_web/live/status_live/index.ex +++ b/lib/aprsme_web/live/status_live/index.ex @@ -25,13 +25,14 @@ defmodule AprsmeWeb.StatusLive.Index do loading: false ) - if connected?(socket) do - # Subscribe to status updates via PubSub - Phoenix.PubSub.subscribe(Aprsme.PubSub, "aprs_status") + _ = + if connected?(socket) do + # Subscribe to status updates via PubSub + :ok = Phoenix.PubSub.subscribe(Aprsme.PubSub, "aprs_status") - # Schedule the first refresh with a slight delay - Process.send_after(self(), :refresh_status, 500) - end + # Schedule the first refresh with a slight delay + Process.send_after(self(), :refresh_status, 500) + end {:ok, socket} end @@ -41,31 +42,32 @@ defmodule AprsmeWeb.StatusLive.Index do # Refresh status asynchronously using supervised task self_pid = self() - Task.Supervisor.start_child(Aprsme.BroadcastTaskSupervisor, fn -> - try do - status = get_aprs_status() - send(self_pid, {:status_updated, status}) - rescue - error -> - Logger.error("Failed to refresh APRS status: #{inspect(error)}") - # Send empty/default status on error - send( - self_pid, - {:status_updated, - %{ - connected: false, - uptime_seconds: 0, - data_rate: 0, - packet_stats: %{ - packets_per_second: 0, - last_packet_at: nil - }, - stored_packet_count: 0, - error: "Failed to fetch status" - }} - ) - end - end) + _ = + Task.Supervisor.start_child(Aprsme.BroadcastTaskSupervisor, fn -> + try do + status = get_aprs_status() + send(self_pid, {:status_updated, status}) + rescue + error -> + Logger.error("Failed to refresh APRS status: #{inspect(error)}") + # Send empty/default status on error + send( + self_pid, + {:status_updated, + %{ + connected: false, + uptime_seconds: 0, + data_rate: 0, + packet_stats: %{ + packets_per_second: 0, + last_packet_at: nil + }, + stored_packet_count: 0, + error: "Failed to fetch status" + }} + ) + end + end) # Schedule next refresh schedule_refresh() diff --git a/lib/aprsme_web/plugs/health_check.ex b/lib/aprsme_web/plugs/health_check.ex index b8c120a..afa5a5b 100644 --- a/lib/aprsme_web/plugs/health_check.ex +++ b/lib/aprsme_web/plugs/health_check.ex @@ -86,7 +86,7 @@ defmodule AprsmeWeb.Plugs.HealthCheck do end defp check_pubsub do - Phoenix.PubSub.broadcast(Aprsme.PubSub, "health_check", :ping) + _ = Phoenix.PubSub.broadcast(Aprsme.PubSub, "health_check", :ping) :ok rescue _ -> {:error, "PubSub check failed"} diff --git a/lib/aprsme_web/plugs/set_locale.ex b/lib/aprsme_web/plugs/set_locale.ex index 237a7b2..d56b308 100644 --- a/lib/aprsme_web/plugs/set_locale.ex +++ b/lib/aprsme_web/plugs/set_locale.ex @@ -10,7 +10,7 @@ defmodule AprsmeWeb.Plugs.SetLocale do def call(conn, _opts) do locale = get_locale_from_header(conn) || "en" # Set the backend's locale for the current process - Gettext.put_locale(AprsmeWeb.Gettext, locale) + _ = Gettext.put_locale(AprsmeWeb.Gettext, locale) # Store locale in session for LiveView to access conn = put_session(conn, :locale, locale) conn diff --git a/lib/aprsme_web/user_auth.ex b/lib/aprsme_web/user_auth.ex index 1c953be..acb9a80 100644 --- a/lib/aprsme_web/user_auth.ex +++ b/lib/aprsme_web/user_auth.ex @@ -79,9 +79,10 @@ defmodule AprsmeWeb.UserAuth do user_token = get_session(conn, :user_token) user_token && Accounts.delete_user_session_token(user_token) - if live_socket_id = get_session(conn, :live_socket_id) do - AprsmeWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{}) - end + _ = + if live_socket_id = get_session(conn, :live_socket_id) do + AprsmeWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{}) + end conn |> renew_session() diff --git a/mix.exs b/mix.exs index b9c6809..c3c9165 100644 --- a/mix.exs +++ b/mix.exs @@ -15,6 +15,7 @@ defmodule Aprsme.MixProject do listeners: [Phoenix.CodeReloader], dialyzer: [ ignore_warnings: ".dialyzer_ignore.exs", + flags: [:error_handling, :underspecs, :unmatched_returns], plt_file: {:no_warn, "_build/dev/dialyxir_erlang-#{:erlang.system_info(:otp_release)}_elixir-#{System.version()}_deps-dev.plt"}