aprs.me/lib/aprsme/postgres_notifier.ex
Graham McIntire 85c3b3d9e7
Improve info page live updates and fix APRS parser
- Fixed info page not receiving live updates by implementing callsign-specific PubSub topics
- Added automatic time counter for "Last Position" and other timestamps using JavaScript hook
- Optimized map updates to only move marker instead of reloading entire map
- Fixed APRS parser to properly handle [000/000/A=altitude format in comments
- Removed packet caching on info page for real-time updates

The info page now properly subscribes to updates for the specific callsign being viewed,
timestamps automatically count up without page refresh, and the map smoothly animates
marker position changes instead of flickering with full reloads.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-28 15:14:33 -05:00

77 lines
2.6 KiB
Elixir

defmodule Aprsme.PostgresNotifier do
@moduledoc """
Listens to PostgreSQL NOTIFY events on the "aprs_events" and "aprs_packets" channels and broadcasts
them via Phoenix.PubSub for reactive, event-driven updates.
"""
use GenServer
@event_channel "aprs_events"
@event_topic "postgres:aprsme_events"
@packet_channel "aprs_packets"
@packet_topic "postgres:aprsme_packets"
def start_link(_opts) do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
end
@impl true
def init(_) do
{:ok, conn} = Postgrex.Notifications.start_link(Aprsme.Repo.config())
{:ok, _ref1} = Postgrex.Notifications.listen(conn, @event_channel)
{:ok, _ref2} = Postgrex.Notifications.listen(conn, @packet_channel)
{:ok, %{conn: conn}}
end
@impl true
def handle_info({:notification, _conn, _pid, @event_channel, payload}, state) do
Phoenix.PubSub.broadcast(Aprsme.PubSub, @event_topic, {:postgres_notify, payload})
{:noreply, state}
end
def handle_info({:notification, _conn, _pid, @packet_channel, payload}, state) do
case Jason.decode(payload) do
{:ok, packet} ->
# Broadcast to the general packet topic
Phoenix.PubSub.broadcast(Aprsme.PubSub, @packet_topic, {:postgres_packet, packet})
# Broadcast to callsign-specific topic
callsign = Map.get(packet, "sender") || Map.get(packet, :sender)
if is_binary(callsign) and callsign != "" do
normalized_callsign = String.upcase(String.trim(callsign))
Phoenix.PubSub.broadcast(Aprsme.PubSub, "packets:#{normalized_callsign}", {:postgres_packet, packet})
end
# Also broadcast to callsign-specific weather topic if it's a weather packet
broadcast_weather_packet_if_relevant(packet)
_ ->
:noop
end
{:noreply, state}
end
def handle_info(_msg, state), do: {:noreply, state}
defp broadcast_weather_packet_if_relevant(packet) do
# Check if this is a weather packet using centralized weather fields
has_weather_data =
Enum.any?(Aprsme.EncodingUtils.weather_fields(), fn field ->
value = Map.get(packet, to_string(field))
not is_nil(value)
end)
if has_weather_data do
# Extract callsign from packet
callsign = Map.get(packet, "sender") || Map.get(packet, :sender)
if is_binary(callsign) and callsign != "" do
# Normalize callsign and broadcast to weather-specific topic
normalized_callsign = String.upcase(String.trim(callsign))
weather_topic = "weather:#{normalized_callsign}"
Phoenix.PubSub.broadcast(Aprsme.PubSub, weather_topic, {:weather_packet, packet})
end
end
end
end