diff --git a/lib/aprs/bad_packet.ex b/lib/aprs/bad_packet.ex index bf2d63f..de2bac6 100644 --- a/lib/aprs/bad_packet.ex +++ b/lib/aprs/bad_packet.ex @@ -3,6 +3,7 @@ defmodule Aprs.BadPacket do use Ecto.Schema import Ecto.Changeset + import Ecto.Query schema "badpackets" do field :raw_packet, :string @@ -19,4 +20,35 @@ defmodule Aprs.BadPacket do |> cast(attrs, [:raw_packet, :error_message, :error_type, :attempted_at]) |> validate_required([:raw_packet]) end + + @doc """ + Returns recent bad packets, ordered by attempted_at descending + """ + def recent(query \\ __MODULE__, limit \\ 100) do + from(b in query, + order_by: [desc: b.attempted_at], + limit: ^limit + ) + end + + @doc """ + Returns bad packets by error type + """ + def by_error_type(query \\ __MODULE__, error_type) do + from(b in query, + where: b.error_type == ^error_type + ) + end + + @doc """ + Returns count of bad packets in the last N hours + """ + def count_recent(hours \\ 24) do + cutoff = DateTime.add(DateTime.utc_now(), -hours * 3600, :second) + + from(b in __MODULE__, + where: b.attempted_at > ^cutoff, + select: count(b.id) + ) + end end diff --git a/lib/aprs_web/live/bad_packets_live/index.ex b/lib/aprs_web/live/bad_packets_live/index.ex new file mode 100644 index 0000000..b2ca404 --- /dev/null +++ b/lib/aprs_web/live/bad_packets_live/index.ex @@ -0,0 +1,63 @@ +defmodule AprsWeb.BadPacketsLive.Index do + @moduledoc false + use AprsWeb, :live_view + + import Ecto.Query + + alias Aprs.BadPacket + alias Aprs.Repo + + @impl true + def mount(_params, _session, socket) do + if connected?(socket) do + # Load initial bad packets + bad_packets = fetch_bad_packets() + # Extra safeguard to ensure we never show more than 100 + limited_packets = Enum.take(bad_packets, 100) + # Schedule automatic refresh every 5 seconds + :timer.send_interval(5000, self(), :refresh_bad_packets) + {:ok, assign(socket, bad_packets: limited_packets, loading: false, last_updated: DateTime.utc_now())} + else + {:ok, assign(socket, bad_packets: [], loading: false, last_updated: nil)} + end + end + + @impl true + def handle_params(params, _url, socket) do + {:noreply, apply_action(socket, socket.assigns.live_action, params)} + end + + defp apply_action(socket, :index, _params) do + assign(socket, :page_title, "Bad Packets") + end + + @impl true + def handle_event("refresh", _params, socket) do + send(self(), :do_refresh) + {:noreply, assign(socket, loading: true)} + end + + @impl true + def handle_info(:refresh_bad_packets, socket) do + send(self(), :do_refresh) + {:noreply, socket} + end + + @impl true + def handle_info(:do_refresh, socket) do + bad_packets = fetch_bad_packets() + # Extra safeguard to ensure we never show more than 100 + limited_packets = Enum.take(bad_packets, 100) + {:noreply, assign(socket, bad_packets: limited_packets, loading: false, last_updated: DateTime.utc_now())} + end + + defp fetch_bad_packets(limit \\ 100) do + # Hard cap at 100 to prevent showing too many records + actual_limit = min(limit, 100) + + BadPacket + |> order_by([b], desc: b.attempted_at) + |> limit(^actual_limit) + |> Repo.all() + end +end diff --git a/lib/aprs_web/live/bad_packets_live/index.html.heex b/lib/aprs_web/live/bad_packets_live/index.html.heex new file mode 100644 index 0000000..dbcd9f9 --- /dev/null +++ b/lib/aprs_web/live/bad_packets_live/index.html.heex @@ -0,0 +1,163 @@ +
+ Packets that failed to parse or process (limited to 100 most recent) + <%= if @last_updated do %> + + Last updated: {Calendar.strftime(@last_updated, "%H:%M:%S UTC")} + + <% end %> +
+All packets are parsing successfully!
+Live and recent APRS packets from the network