Add deployment notification system to update timestamp in LiveViews

When a new deployment occurs in k8s, all connected LiveViews now receive
a notification to update their deployment timestamp display.

Changes:
- Added DeploymentNotifier GenServer that broadcasts deployment events
- LiveViews subscribe to "deployment_events" PubSub topic
- When DEPLOYED_AT env var is set (k8s deployments), a notification is
  sent after app startup
- LiveViews handle the deployment event and update their timestamp

This ensures that when a new version is deployed, users see the
deployment timestamp update in real-time without needing to refresh
their browser.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Graham McIntire 2025-07-28 13:19:38 -05:00
parent 94c0f1a48c
commit 478e7b1936
No known key found for this signature in database
4 changed files with 97 additions and 1 deletions

View file

@ -49,6 +49,8 @@ defmodule Aprsme.Application do
Aprsme.CleanupScheduler,
Aprsme.Presence,
Aprsme.PostgresNotifier,
# Start deployment notifier
Aprsme.DeploymentNotifier,
# Start the packet processing pipeline
Aprsme.PacketPipelineSupervisor
]

View file

@ -0,0 +1,62 @@
defmodule Aprsme.DeploymentNotifier do
@moduledoc """
Monitors for deployment changes and notifies connected clients.
In k8s, this detects when the DEPLOYED_AT environment variable changes.
"""
use GenServer
require Logger
@check_interval :timer.seconds(30)
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(_opts) do
# Schedule first check
Process.send_after(self(), :check_deployment, @check_interval)
# Get initial deployment timestamp
deployed_at = Aprsme.Release.deployed_at()
{:ok, %{deployed_at: deployed_at}}
end
def handle_info(:check_deployment, state) do
# Schedule next check
Process.send_after(self(), :check_deployment, @check_interval)
# Get current deployment timestamp
current_deployed_at = Aprsme.Release.deployed_at()
# Check if deployment timestamp changed (shouldn't happen in same process, but useful for monitoring)
if current_deployed_at == state.deployed_at do
{:noreply, state}
# Broadcast the new deployment
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}}
)
{:noreply, %{state | deployed_at: current_deployed_at}}
end
end
@doc """
Notify about a new deployment immediately.
This can be called from the release module when deployment is detected.
"""
def notify_deployment(deployed_at) do
Phoenix.PubSub.broadcast(
Aprsme.PubSub,
"deployment_events",
{:new_deployment, %{deployed_at: deployed_at}}
)
end
end

View file

@ -100,6 +100,27 @@ defmodule Aprsme.Release do
# Store in application config
Application.put_env(:aprsme, :deployed_at, deployed_at)
# 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
spawn(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 ->
require Logger
Logger.warning("Failed to send deployment notification: #{inspect(error)}")
end
end)
end
deployed_at
end

View file

@ -28,6 +28,7 @@ defmodule AprsmeWeb.MapLive.Index do
alias AprsmeWeb.MapLive.UrlParams
alias AprsmeWeb.TimeUtils
alias Phoenix.LiveView.Socket
alias Phoenix.Socket.Broadcast
@impl true
def mount(params, session, socket) do
@ -107,6 +108,9 @@ defmodule AprsmeWeb.MapLive.Index do
# Still subscribe to bad packets (they don't have location)
Phoenix.PubSub.subscribe(Aprsme.PubSub, "bad_packets")
# Subscribe to deployment events
Phoenix.PubSub.subscribe(Aprsme.PubSub, "deployment_events")
# Subscribe to StreamingPacketsPubSub with initial bounds
Aprsme.StreamingPacketsPubSub.subscribe_to_bounds(self(), default_bounds)
@ -671,6 +675,13 @@ defmodule AprsmeWeb.MapLive.Index do
def handle_info({:streaming_packet, packet}, socket), do: handle_info_postgres_packet(packet, socket)
def handle_info(
%Broadcast{topic: "deployment_events", payload: {:new_deployment, %{deployed_at: deployed_at}}},
socket
) do
{:noreply, assign(socket, :deployed_at, deployed_at)}
end
def handle_info({:load_rf_path_station_packets, stations}, socket) do
# Load the most recent packet for each RF path station
station_packets =
@ -722,7 +733,7 @@ defmodule AprsmeWeb.MapLive.Index do
end
end
def handle_info(%Phoenix.Socket.Broadcast{topic: "aprs_messages", event: "packet", payload: packet}, socket),
def handle_info(%Broadcast{topic: "aprs_messages", event: "packet", payload: packet}, socket),
do: handle_info({:postgres_packet, packet}, socket)
def handle_info({:show_error, message}, socket) do