live update info page

This commit is contained in:
Graham McIntire 2025-07-04 11:45:00 -05:00
parent 7a36960eb6
commit a78fb8a385
No known key found for this signature in database
8 changed files with 181 additions and 3 deletions

View file

@ -12,12 +12,16 @@ defmodule Aprsme.DeviceParser do
cond do
Map.has_key?(packet_data, :destination) and not is_nil(packet_data[:destination]) ->
packet_data[:destination]
Map.has_key?(packet_data, "destination") and not is_nil(packet_data["destination"]) ->
packet_data["destination"]
Map.has_key?(packet_data, :device_identifier) and not is_nil(packet_data[:device_identifier]) ->
packet_data[:device_identifier]
Map.has_key?(packet_data, "device_identifier") and not is_nil(packet_data["device_identifier"]) ->
packet_data["device_identifier"]
true ->
extract_from_data_extended(packet_data)
end

View file

@ -49,6 +49,7 @@ defmodule Aprsme.Packets do
parsed_device_id = Aprsme.DeviceParser.extract_device_identifier(packet_data)
device_id = parsed_device_id || Map.get(packet_attrs, :destination)
packet_attrs = Map.put(packet_attrs, :device_identifier, device_id)
# Logger.debug("Inserting packet with device_identifier=#{inspect(device_id)}, destination=#{inspect(Map.get(packet_attrs, :destination))}")
insert_packet(packet_attrs, packet_data)
rescue

View file

@ -11,6 +11,12 @@ defmodule AprsmeWeb.InfoLive.Show do
@impl true
def mount(%{"callsign" => callsign}, _session, socket) do
normalized_callsign = String.upcase(String.trim(callsign))
# Subscribe to Postgres notifications for live updates
if connected?(socket) do
Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets")
end
packet = get_latest_packet(normalized_callsign)
neighbors = get_neighbors(packet, normalized_callsign)
@ -24,6 +30,32 @@ defmodule AprsmeWeb.InfoLive.Show do
{:ok, socket}
end
@impl true
def handle_info({:postgres_packet, packet}, socket) do
# Only update if the packet is for our callsign
if packet_matches_callsign?(packet, socket.assigns.callsign) do
# Refresh data when new packet arrives
packet = get_latest_packet(socket.assigns.callsign)
neighbors = get_neighbors(packet, socket.assigns.callsign)
socket =
socket
|> assign(:packet, packet)
|> assign(:neighbors, neighbors)
{:noreply, socket}
else
{:noreply, socket}
end
end
def handle_info(_message, socket), do: {:noreply, socket}
defp packet_matches_callsign?(packet, callsign) do
packet_sender = Map.get(packet, "sender") || Map.get(packet, :sender, "")
String.upcase(packet_sender) == String.upcase(callsign)
end
defp get_latest_packet(callsign) do
%{callsign: callsign, limit: 1}
|> Packets.get_recent_packets()

View file

@ -180,7 +180,9 @@
<%= if @packet.manufacturer && @packet.manufacturer != "" do %>
{@packet.manufacturer} {@packet.equipment_type || ""}
<% else %>
<span class="text-gray-500 font-mono">{@packet.device_identifier || "Unknown"}</span>
<span class="text-gray-500 font-mono">
{@packet.device_identifier || "Unknown"}
</span>
<% end %>
</dd>
</div>

View file

@ -8,6 +8,12 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do
@impl true
def mount(%{"callsign" => callsign}, _session, socket) do
normalized_callsign = String.upcase(String.trim(callsign))
# Subscribe to Postgres notifications for live updates
if connected?(socket) do
Phoenix.PubSub.subscribe(Aprsme.PubSub, "postgres:aprsme_packets")
end
weather_packet = get_latest_weather_packet(normalized_callsign)
{start_time, end_time} = default_time_range()
weather_history = get_weather_history(normalized_callsign, start_time, end_time)
@ -47,6 +53,78 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do
{:ok, socket}
end
@impl true
def handle_info({:postgres_packet, packet}, socket) do
# Only update if the packet is for our callsign and contains weather data
if packet_matches_callsign?(packet, socket.assigns.callsign) and has_weather_data?(packet) do
# Refresh weather data when new weather packet arrives
weather_packet = get_latest_weather_packet(socket.assigns.callsign)
{start_time, end_time} = default_time_range()
weather_history = get_weather_history(socket.assigns.callsign, start_time, end_time)
weather_history_json =
weather_history
|> Enum.map(fn pkt ->
dew_point =
if is_number(pkt.temperature) and is_number(pkt.humidity) do
calc_dew_point(pkt.temperature, pkt.humidity)
end
%{
timestamp: pkt.received_at,
temperature: pkt.temperature,
dew_point: dew_point,
humidity: pkt.humidity,
pressure: pkt.pressure,
wind_direction: pkt.wind_direction,
wind_speed: pkt.wind_speed,
rain_1h: pkt.rain_1h,
rain_24h: pkt.rain_24h,
rain_since_midnight: pkt.rain_since_midnight,
luminosity: pkt.luminosity
}
end)
|> Jason.encode!()
socket =
socket
|> assign(:weather_packet, weather_packet)
|> assign(:weather_history, weather_history)
|> assign(:weather_history_json, weather_history_json)
{:noreply, socket}
else
{:noreply, socket}
end
end
def handle_info(_message, socket), do: {:noreply, socket}
defp packet_matches_callsign?(packet, callsign) do
packet_sender = Map.get(packet, "sender") || Map.get(packet, :sender, "")
String.upcase(packet_sender) == String.upcase(callsign)
end
defp has_weather_data?(packet) do
# Check if packet contains any weather-related fields
weather_fields = [
"temperature",
"humidity",
"pressure",
"wind_speed",
"wind_direction",
"rain_1h",
"rain_24h",
"rain_since_midnight",
"luminosity"
]
Enum.any?(weather_fields, fn field ->
value = Map.get(packet, field) || Map.get(packet, String.to_atom(field))
not is_nil(value)
end)
end
defp get_latest_weather_packet(callsign) do
# Get weather packets from the last 7 days to find the most recent one
end_time = DateTime.utc_now()

View file

@ -152,7 +152,7 @@
<dd class="mt-1 text-sm font-semibold text-gray-900">
{get_weather_field_zero(@weather_packet, :rain_since_midnight)} in
</dd>
</div>
</div>
<div class="col-span-2">
<dt class="text-xs font-medium text-gray-500">Raw Packet</dt>
<dd class="mt-1 text-xs font-mono text-gray-900 break-all">
@ -163,7 +163,7 @@
<% end %>
</dd>
</div>
</dl>
</dl>
<div class="mt-2 text-xs text-gray-500">
<strong>Raw comment:</strong> {@weather_packet.comment || @weather_packet["comment"]}
</div>

View file

@ -12,6 +12,7 @@ defmodule Aprsme.Repo.Migrations.RecreateNotifyPacketsInsertFunction do
BEGIN
payload := json_build_object(
'id', NEW.id,
'sender', NEW.sender,
'lat', NEW.lat,
'lon', NEW.lon,
'inserted_at', NEW.inserted_at

View file

@ -0,0 +1,60 @@
defmodule Aprsme.Repo.Migrations.UpdatePacketsNotificationPayload do
use Ecto.Migration
def up do
execute "DROP TRIGGER IF EXISTS packets_notify_insert ON packets;"
execute "DROP FUNCTION IF EXISTS notify_packets_insert();"
execute """
CREATE OR REPLACE FUNCTION notify_packets_insert() RETURNS trigger AS $$
DECLARE
payload TEXT;
BEGIN
payload := json_build_object(
'id', NEW.id,
'sender', NEW.sender,
'lat', NEW.lat,
'lon', NEW.lon,
'inserted_at', NEW.inserted_at
)::text;
PERFORM pg_notify('aprs_packets', payload);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
"""
execute """
CREATE TRIGGER packets_notify_insert
AFTER INSERT ON packets
FOR EACH ROW EXECUTE FUNCTION notify_packets_insert();
"""
end
def down do
execute "DROP TRIGGER IF EXISTS packets_notify_insert ON packets;"
execute "DROP FUNCTION IF EXISTS notify_packets_insert();"
execute """
CREATE OR REPLACE FUNCTION notify_packets_insert() RETURNS trigger AS $$
DECLARE
payload TEXT;
BEGIN
payload := json_build_object(
'id', NEW.id,
'lat', NEW.lat,
'lon', NEW.lon,
'inserted_at', NEW.inserted_at
)::text;
PERFORM pg_notify('aprs_packets', payload);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
"""
execute """
CREATE TRIGGER packets_notify_insert
AFTER INSERT ON packets
FOR EACH ROW EXECUTE FUNCTION notify_packets_insert();
"""
end
end