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.
This commit is contained in:
parent
8332e5c53c
commit
b8a9b8a465
40 changed files with 372 additions and 276 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()}")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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", %{})) || %{}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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] ->
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
1
mix.exs
1
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"}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue