defmodule AprsmeWeb.StatusLive.Index do
@moduledoc """
LiveView for displaying real-time APRS-IS connection status
"""
use AprsmeWeb, :live_view
# 5 seconds - reduced from 1 second to improve performance
@refresh_interval 5_000
@impl true
def mount(_params, _session, socket) do
# Load cached status on mount for fast initial load
aprs_status = get_cached_aprs_status()
socket =
assign(socket,
page_title: "System Status",
aprs_status: aprs_status,
current_time: DateTime.utc_now(),
health_score: calculate_health_score(aprs_status),
loading: false
)
if connected?(socket) do
# Subscribe to status updates via PubSub
Phoenix.PubSub.subscribe(Aprsme.PubSub, "aprs_status")
# Schedule the first refresh with a slight delay
Process.send_after(self(), :refresh_status, 500)
end
{:ok, socket}
end
@impl true
def handle_info(:refresh_status, socket) do
# Refresh status asynchronously
self_pid = self()
Task.start(fn ->
status = get_aprs_status()
send(self_pid, {:status_updated, status})
end)
# Schedule next refresh
schedule_refresh()
{:noreply, assign(socket, loading: true)}
end
@impl true
def handle_info({:status_updated, status}, socket) do
socket =
assign(socket,
aprs_status: status,
current_time: DateTime.utc_now(),
health_score: calculate_health_score(status),
loading: false
)
{:noreply, socket}
end
@impl true
def handle_info({:aprs_status_update, status}, socket) do
# Handle real-time status updates via PubSub
socket =
assign(socket,
aprs_status: status,
current_time: DateTime.utc_now(),
health_score: calculate_health_score(status)
)
{:noreply, socket}
end
@impl true
def render(assigns) do
~H"""
{gettext("Last Packet:")}
<%= if @aprs_status.packet_stats.last_packet_at do %>
{format_time_ago(@aprs_status.packet_stats.last_packet_at)}
<% else %>
{gettext("None")}
<% end %>
"""
end
# Private functions
defp get_aprs_status do
Aprsme.Is.get_status()
rescue
error ->
require Logger
Logger.error("Error getting APRS status: #{inspect(error)}")
# Return a default status when database is unavailable
default_status()
end
defp get_cached_aprs_status do
# Try to get cached status for instant load
case Cachex.get(:query_cache, "aprs_status") do
{:ok, status} when not is_nil(status) ->
status
_ ->
# Fallback to direct query if cache miss
status = get_aprs_status()
Cachex.put(:query_cache, "aprs_status", status, ttl: to_timeout(second: 5))
status
end
end
defp default_status do
%{
connected: false,
server: "unknown",
port: 0,
connected_at: nil,
uptime_seconds: 0,
login_id: "unknown",
filter: "unknown",
packet_stats: %{
total_packets: 0,
packets_per_second: 0,
last_packet_at: nil
},
stored_packet_count: 0,
oldest_packet_timestamp: nil
}
end
defp schedule_refresh do
Process.send_after(self(), :refresh_status, @refresh_interval)
end
def format_uptime(seconds) when seconds <= 0, do: gettext("Not connected")
def format_uptime(seconds) do
days = div(seconds, 86_400)
hours = div(rem(seconds, 86_400), 3600)
minutes = div(rem(seconds, 3600), 60)
secs = rem(seconds, 60)
cond do
days > 0 -> "#{days}d #{hours}h #{minutes}m #{secs}s"
hours > 0 -> "#{hours}h #{minutes}m #{secs}s"
minutes > 0 -> "#{minutes}m #{secs}s"
true -> "#{secs}s"
end
end
defp calculate_health_score(aprs_status) do
cond do
not aprs_status.connected -> 1
# Less than 5 minutes
aprs_status.uptime_seconds < 300 -> 2
# Less than 1 hour
aprs_status.uptime_seconds < 3600 -> 3
# Less than 1 day
aprs_status.uptime_seconds < 86_400 -> 4
# More than 1 day
true -> 5
end
end
def get_health_description(score, connected) do
case {score, connected} do
{1, false} -> gettext("Disconnected - Connection issues detected")
{2, true} -> gettext("Recently connected - Monitoring stability")
{3, true} -> gettext("Good - Connection stable for less than 1 hour")
{4, true} -> gettext("Very good - Connection stable for less than 1 day")
{5, true} -> gettext("Excellent - Long-term stable connection")
_ -> gettext("Unknown status")
end
end
def format_time_ago(nil), do: gettext("Never")
def format_time_ago(datetime) do
diff_seconds = DateTime.diff(DateTime.utc_now(), datetime)
cond do
diff_seconds < 60 -> gettext("%{count} seconds ago", count: diff_seconds)
diff_seconds < 3600 -> gettext("%{count} minutes ago", count: div(diff_seconds, 60))
diff_seconds < 86_400 -> gettext("%{count} hours ago", count: div(diff_seconds, 3600))
true -> gettext("%{count} days ago", count: div(diff_seconds, 86_400))
end
end
def format_number(number) when is_integer(number) do
number
|> Integer.to_string()
|> String.reverse()
|> String.replace(~r/(\d{3})(?=\d)/, "\\1,")
|> String.reverse()
end
def format_number(number), do: to_string(number)
end