aprs.me/lib/aprsme_web/live/info_live/show.html.heex
2025-07-09 16:22:58 -05:00

426 lines
18 KiB
Text

<div class="min-h-screen bg-base-200">
<!-- Page header -->
<div class="bg-base-100 shadow-sm">
<div class="container mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-4 md:flex md:items-center md:justify-between">
<div class="min-w-0 flex-1">
<div class="flex items-center">
<h1 class="text-xl font-bold leading-7 sm:truncate sm:text-2xl">
{gettext("APRS Station")}
</h1>
<div class="ml-3 flex items-center space-x-3">
<span class="badge badge-primary">
{@callsign}
</span>
<%= if @packet do %>
<% {symbol_table, symbol_code} = AprsmeWeb.AprsSymbol.extract_from_packet(@packet) %>
<% display_symbol =
if symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/),
do: symbol_table,
else:
"#{AprsmeWeb.AprsSymbol.normalize_symbol_table(symbol_table)}#{AprsmeWeb.AprsSymbol.normalize_symbol_code(symbol_code)}" %>
<div title={display_symbol}>
{render_symbol_html(@packet)}
</div>
<% end %>
</div>
</div>
</div>
<div class="mt-3 flex md:ml-4 md:mt-0">
<div class="flex space-x-2">
<.link navigate={~p"/packets/#{@callsign}"} class="btn btn-outline btn-sm">
{gettext("View packets")}
</.link>
<%= if @has_weather_packets do %>
<.link navigate={~p"/weather/#{@callsign}"} class="btn btn-primary btn-sm">
{gettext("Weather charts")}
</.link>
<% end %>
</div>
</div>
</div>
</div>
</div>
<div class="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4">
<%= if @packet do %>
<!-- Station details -->
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 mb-6">
<!-- Position information -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg
class="h-4 w-4 opacity-70"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z"
/>
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-medium">{gettext("Position Information")}</h3>
</div>
</div>
<dl class="grid grid-cols-2 gap-3">
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Location")}</dt>
<dd class="mt-1 text-sm font-semibold">
{@packet.lat || ""}, {@packet.lon || ""}
</dd>
</div>
<%= if @packet.lat && @packet.lon do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Grid Square")}</dt>
<dd class="mt-1 text-sm font-semibold">
<%= case Gridsquare.encode(@packet.lon, @packet.lat) do %>
<% %Gridsquare.EncodeResult{grid_reference: grid_ref} -> %>
{grid_ref}
<% _ -> %>
-
<% end %>
</dd>
</div>
<% end %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Last Position")}</dt>
<dd class="mt-1 text-sm">
<%= if @packet.received_at do %>
<div class="font-semibold">
{AprsmeWeb.TimeHelpers.time_ago_in_words(@packet.received_at)}
</div>
<div class="text-xs opacity-70 font-mono">
{Calendar.strftime(@packet.received_at, "%Y-%m-%d %H:%M:%S UTC")}
</div>
<% else %>
<span class="opacity-70">{gettext("Unknown")}</span>
<% end %>
</dd>
</div>
<%= if @packet.altitude do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Altitude")}</dt>
<dd class="mt-1 text-sm font-semibold">{@packet.altitude} ft</dd>
</div>
<% end %>
<%= if @packet.course do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Course")}</dt>
<dd class="mt-1 text-sm font-semibold">{@packet.course}°</dd>
</div>
<% end %>
<%= if @packet.speed do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Speed")}</dt>
<dd class="mt-1 text-sm font-semibold">{@packet.speed} MPH</dd>
</div>
<% end %>
</dl>
</div>
</div>
<!-- Device information -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg
class="h-4 w-4 opacity-70"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0020.75 3H3.75A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25"
/>
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-medium">{gettext("Device Information")}</h3>
</div>
</div>
<dl class="grid grid-cols-2 gap-3">
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Device")}</dt>
<dd class="mt-1 text-sm font-semibold">
<%= if @packet.device_model || @packet.device_vendor do %>
{[
@packet.device_model,
@packet.device_vendor,
@packet.device_contact
]
|> Enum.reject(&is_nil/1)
|> Enum.join(" ")}
<%= if @packet.device_class do %>
({@packet.device_class})
<% end %>
<% else %>
<span class="opacity-70 font-mono">
{@packet.device_identifier || gettext("Unknown")}
</span>
<% end %>
</dd>
</div>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Path")}</dt>
<dd class="mt-1 text-sm font-semibold">{@packet.path || ""}</dd>
</div>
<%= if @packet.path && @packet.path != "" do %>
<div class="col-span-2">
<dt class="text-xs font-medium opacity-70">{gettext("Decoded Path")}</dt>
<dd class="mt-1 text-sm">
{decode_aprs_path(@packet.path)}
</dd>
</div>
<% end %>
<div class="col-span-2">
<dt class="text-xs font-medium opacity-70">{gettext("Raw Packet")}</dt>
<dd class="mt-1 text-xs font-mono break-all">
<%= if is_binary(@packet.raw_packet) do %>
{Aprsme.EncodingUtils.sanitize_string(@packet.raw_packet)}
<% else %>
{@packet.raw_packet || ""}
<% end %>
</dd>
</div>
</dl>
</div>
</div>
</div>
<!-- Other SSIDs -->
<%= if length(@other_ssids) > 0 do %>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg
class="h-4 w-4 opacity-70"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
/>
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-medium">{gettext("Other SSIDs")}</h3>
</div>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Callsign")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Distance & Direction")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Last Heard")}
</th>
</tr>
</thead>
<tbody>
<%= for ssid_info <- @other_ssids do %>
<tr>
<td class="font-medium">
<div class="flex items-center space-x-2">
<.link
navigate={~p"/info/#{ssid_info.callsign}"}
class="link link-primary"
>
{ssid_info.callsign}
</.link>
<%= if ssid_info.packet do %>
<% {symbol_table, symbol_code} =
AprsmeWeb.AprsSymbol.extract_from_packet(ssid_info.packet) %>
<% display_symbol =
if symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/),
do: symbol_table,
else:
"#{AprsmeWeb.AprsSymbol.normalize_symbol_table(symbol_table)}#{AprsmeWeb.AprsSymbol.normalize_symbol_code(symbol_code)}" %>
<div title={display_symbol}>
{render_symbol_html(ssid_info.packet)}
</div>
<% end %>
</div>
</td>
<td class="opacity-70">
<%= if ssid_info.packet && ssid_info.packet.lat && ssid_info.packet.lon && @packet && @packet.lat && @packet.lon do %>
<% dist =
haversine(
@packet.lat,
@packet.lon,
ssid_info.packet.lat,
ssid_info.packet.lon
) %>
<% course =
calculate_course(
@packet.lat,
@packet.lon,
ssid_info.packet.lat,
ssid_info.packet.lon
) %>
<% locale = Map.get(assigns, :locale, "en") %>
<%= if course do %>
{format_distance(dist, locale)} @ {Float.round(course, 0)}°
<% else %>
{format_distance(dist, locale)}
<% end %>
<% else %>
-
<% end %>
</td>
<td class="opacity-70">
<%= if ssid_info.last_heard do %>
<div class="font-semibold">
{ssid_info.last_heard.time_ago}
</div>
<div class="text-xs opacity-70 font-mono">
{ssid_info.last_heard.formatted}
</div>
<% else %>
<span class="opacity-70">{gettext("Unknown")}</span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
<% end %>
<!-- Neighboring stations -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg
class="h-4 w-4 opacity-70"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z"
/>
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-medium">{gettext("Stations Near Current Position")}</h3>
</div>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Callsign")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Distance & Course")}
</th>
<th class="text-xs font-medium uppercase tracking-wider">
{gettext("Last Heard")}
</th>
</tr>
</thead>
<tbody>
<%= for neighbor <- @neighbors do %>
<tr>
<td class="font-medium">
<div class="flex items-center space-x-2">
<.link navigate={~p"/info/#{neighbor.callsign}"} class="link link-primary">
{neighbor.callsign}
</.link>
<%= if neighbor.packet do %>
<% {symbol_table, symbol_code} =
AprsmeWeb.AprsSymbol.extract_from_packet(neighbor.packet) %>
<% display_symbol =
if symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/),
do: symbol_table,
else:
"#{AprsmeWeb.AprsSymbol.normalize_symbol_table(symbol_table)}#{AprsmeWeb.AprsSymbol.normalize_symbol_code(symbol_code)}" %>
<div title={display_symbol}>
{render_symbol_html(neighbor.packet)}
</div>
<% end %>
</div>
</td>
<td class="opacity-70">
<%= if neighbor.course do %>
{neighbor.distance || ""} @ {Float.round(neighbor.course, 0)}°
<% else %>
{neighbor.distance || ""}
<% end %>
</td>
<td class="opacity-70">
<%= if neighbor.last_heard do %>
<div class="font-semibold">
{neighbor.last_heard.time_ago}
</div>
<div class="text-xs opacity-70 font-mono">
{neighbor.last_heard.formatted}
</div>
<% else %>
<span class="opacity-70">{gettext("Unknown")}</span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
<% else %>
<div class="text-center py-8">
<svg
class="mx-auto h-8 w-8 opacity-70"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM16.894 20.567L16.5 21.75l-.394-1.183a2.25 2.25 0 00-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 001.423-1.423L16.5 15.75l.394 1.183a2.25 2.25 0 001.423 1.423L19.5 18.75l-1.183.394a2.25 2.25 0 00-1.423 1.423z"
/>
</svg>
<h3 class="mt-2 text-sm font-semibold">{gettext("No data available")}</h3>
<p class="mt-1 text-sm opacity-70">
{gettext("No recent packet data available for this callsign.")}
</p>
</div>
<% end %>
</div>
</div>