add badpackets page
This commit is contained in:
parent
4c0433d32e
commit
4d2dedd751
5 changed files with 262 additions and 0 deletions
|
|
@ -3,6 +3,7 @@ defmodule Aprs.BadPacket do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
schema "badpackets" do
|
schema "badpackets" do
|
||||||
field :raw_packet, :string
|
field :raw_packet, :string
|
||||||
|
|
@ -19,4 +20,35 @@ defmodule Aprs.BadPacket do
|
||||||
|> cast(attrs, [:raw_packet, :error_message, :error_type, :attempted_at])
|
|> cast(attrs, [:raw_packet, :error_message, :error_type, :attempted_at])
|
||||||
|> validate_required([:raw_packet])
|
|> validate_required([:raw_packet])
|
||||||
end
|
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
|
end
|
||||||
|
|
|
||||||
63
lib/aprs_web/live/bad_packets_live/index.ex
Normal file
63
lib/aprs_web/live/bad_packets_live/index.ex
Normal file
|
|
@ -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
|
||||||
163
lib/aprs_web/live/bad_packets_live/index.html.heex
Normal file
163
lib/aprs_web/live/bad_packets_live/index.html.heex
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 my-8">
|
||||||
|
<div class="mb-8">
|
||||||
|
<.header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-semibold text-gray-900">
|
||||||
|
Bad APRS Packets
|
||||||
|
<span class="ml-2 text-base font-normal text-gray-500">
|
||||||
|
(Showing {length(@bad_packets)} most recent)
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
Packets that failed to parse or process (limited to 100 most recent)
|
||||||
|
<%= if @last_updated do %>
|
||||||
|
<span class="text-xs text-gray-400 ml-2">
|
||||||
|
Last updated: {Calendar.strftime(@last_updated, "%H:%M:%S UTC")}
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<button
|
||||||
|
phx-click="refresh"
|
||||||
|
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
disabled={@loading}
|
||||||
|
>
|
||||||
|
<%= if @loading do %>
|
||||||
|
<svg
|
||||||
|
class="animate-spin -ml-1 mr-2 h-4 w-4 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
>
|
||||||
|
</circle>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
>
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
Refreshing...
|
||||||
|
<% else %>
|
||||||
|
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Refresh
|
||||||
|
<% end %>
|
||||||
|
</button>
|
||||||
|
<.link navigate={~p"/packets"} class="text-sm text-blue-600 hover:text-blue-800">
|
||||||
|
View Good Packets →
|
||||||
|
</.link>
|
||||||
|
<.link navigate={~p"/"} class="text-sm text-blue-600 hover:text-blue-800">
|
||||||
|
← Back to Map
|
||||||
|
</.link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</.header>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 bg-white shadow-sm rounded-lg overflow-hidden">
|
||||||
|
<div class="px-4 py-2 bg-gray-50 border-b border-gray-200 flex items-center justify-between">
|
||||||
|
<span class="text-xs text-gray-500">
|
||||||
|
Auto-refreshes every 5 seconds
|
||||||
|
</span>
|
||||||
|
<%= if @loading do %>
|
||||||
|
<span class="flex items-center text-xs text-blue-600">
|
||||||
|
<svg
|
||||||
|
class="animate-spin -ml-1 mr-1 h-3 w-3"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
>
|
||||||
|
</circle>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
>
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
Updating...
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<.table id="bad_packets" rows={@bad_packets}>
|
||||||
|
<:col :let={bad_packet} label="Attempted At">
|
||||||
|
<span class="text-sm text-gray-900">
|
||||||
|
{Calendar.strftime(
|
||||||
|
bad_packet.attempted_at || bad_packet.inserted_at,
|
||||||
|
"%Y-%m-%d %H:%M:%S UTC"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</:col>
|
||||||
|
<:col :let={bad_packet} label="Error Type">
|
||||||
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||||
|
{bad_packet.error_type || "Unknown"}
|
||||||
|
</span>
|
||||||
|
</:col>
|
||||||
|
<:col :let={bad_packet} label="Error Message">
|
||||||
|
<span class="text-sm text-red-600 font-mono">
|
||||||
|
{bad_packet.error_message || "No error message"}
|
||||||
|
</span>
|
||||||
|
</:col>
|
||||||
|
<:col :let={bad_packet} label="Raw Packet">
|
||||||
|
<div class="max-w-md">
|
||||||
|
<details class="group">
|
||||||
|
<summary class="cursor-pointer text-sm text-gray-500 hover:text-gray-700">
|
||||||
|
<span class="font-mono">
|
||||||
|
{String.slice(bad_packet.raw_packet || "", 0..50)}{if String.length(
|
||||||
|
bad_packet.raw_packet ||
|
||||||
|
""
|
||||||
|
) > 50,
|
||||||
|
do: "..."}
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div class="mt-2 p-2 bg-gray-50 rounded text-xs font-mono text-gray-700 break-all">
|
||||||
|
{bad_packet.raw_packet}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</:col>
|
||||||
|
</.table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= if length(@bad_packets) == 0 do %>
|
||||||
|
<div class="mt-8 text-center">
|
||||||
|
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-green-100">
|
||||||
|
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="mt-2 text-sm font-medium text-gray-900">No bad packets</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">All packets are parsing successfully!</p>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
@ -7,6 +7,9 @@
|
||||||
<p class="mt-1 text-sm text-gray-500">Live and recent APRS packets from the network</p>
|
<p class="mt-1 text-sm text-gray-500">Live and recent APRS packets from the network</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-4">
|
<div class="flex items-center space-x-4">
|
||||||
|
<.link navigate={~p"/badpackets"} class="text-sm text-blue-600 hover:text-blue-800">
|
||||||
|
View Bad Packets →
|
||||||
|
</.link>
|
||||||
<.link navigate={~p"/"} class="text-sm text-blue-600 hover:text-blue-800">
|
<.link navigate={~p"/"} class="text-sm text-blue-600 hover:text-blue-800">
|
||||||
← Back to Map
|
← Back to Map
|
||||||
</.link>
|
</.link>
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ defmodule AprsWeb.Router do
|
||||||
|
|
||||||
live "/packets", PacketsLive.Index, :index
|
live "/packets", PacketsLive.Index, :index
|
||||||
live "/packets/:callsign", PacketsLive.CallsignView, :index
|
live "/packets/:callsign", PacketsLive.CallsignView, :index
|
||||||
|
live "/badpackets", BadPacketsLive.Index, :index
|
||||||
live "/:callsign", MapLive.CallsignView, :index
|
live "/:callsign", MapLive.CallsignView, :index
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue