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 @@ +
+
+ <.header> +
+
+

+ Bad APRS Packets + + (Showing {length(@bad_packets)} most recent) + +

+

+ 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 %> +

+
+
+ + <.link navigate={~p"/packets"} class="text-sm text-blue-600 hover:text-blue-800"> + View Good Packets → + + <.link navigate={~p"/"} class="text-sm text-blue-600 hover:text-blue-800"> + ← Back to Map + +
+
+ +
+ +
+
+ + Auto-refreshes every 5 seconds + + <%= if @loading do %> + + + + + + + + Updating... + + <% end %> +
+ <.table id="bad_packets" rows={@bad_packets}> + <:col :let={bad_packet} label="Attempted At"> + + {Calendar.strftime( + bad_packet.attempted_at || bad_packet.inserted_at, + "%Y-%m-%d %H:%M:%S UTC" + )} + + + <:col :let={bad_packet} label="Error Type"> + + {bad_packet.error_type || "Unknown"} + + + <:col :let={bad_packet} label="Error Message"> + + {bad_packet.error_message || "No error message"} + + + <:col :let={bad_packet} label="Raw Packet"> +
+
+ + + {String.slice(bad_packet.raw_packet || "", 0..50)}{if String.length( + bad_packet.raw_packet || + "" + ) > 50, + do: "..."} + + +
+ {bad_packet.raw_packet} +
+
+
+ + +
+ + <%= if length(@bad_packets) == 0 do %> +
+
+ + + +
+

No bad packets

+

All packets are parsing successfully!

+
+ <% end %> +
diff --git a/lib/aprs_web/live/packets_live/index.html.heex b/lib/aprs_web/live/packets_live/index.html.heex index 6b491fe..82db31a 100644 --- a/lib/aprs_web/live/packets_live/index.html.heex +++ b/lib/aprs_web/live/packets_live/index.html.heex @@ -7,6 +7,9 @@

Live and recent APRS packets from the network

+ <.link navigate={~p"/badpackets"} class="text-sm text-blue-600 hover:text-blue-800"> + View Bad Packets → + <.link navigate={~p"/"} class="text-sm text-blue-600 hover:text-blue-800"> ← Back to Map diff --git a/lib/aprs_web/router.ex b/lib/aprs_web/router.ex index 3b36dcf..c04c7f8 100644 --- a/lib/aprs_web/router.ex +++ b/lib/aprs_web/router.ex @@ -34,6 +34,7 @@ defmodule AprsWeb.Router do live "/packets", PacketsLive.Index, :index live "/packets/:callsign", PacketsLive.CallsignView, :index + live "/badpackets", BadPacketsLive.Index, :index live "/:callsign", MapLive.CallsignView, :index end