- Fix leader election guard test for :undefined pid - Fix cpu_sup.util() pattern matching to handle numeric return - Fix encoding_utils to always return explicit nil when needed - Remove unused finite_float? function - Fix signal handler to match :ok return from :os.set_signal/2 - Remove redundant catch-all pattern in database metrics - Fix Gridsquare pattern matching in info_live template - Remove unnecessary nil check for calculate_course result - Fix get_packet_received_at to not check for nil (always returns DateTime) - Remove redundant catch-all pattern in weather format_weather_value - Fix query builder to use from(p in Packet) instead of bare Packet atom Reduced dialyzer errors from 49 to 6. Remaining warnings are mostly false positives from template compilation and overloaded function specs. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
710 lines
32 KiB
Text
710 lines
32 KiB
Text
<!-- Leaflet CSS for the map -->
|
|
<link
|
|
rel="stylesheet"
|
|
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
|
crossorigin=""
|
|
/>
|
|
<script
|
|
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
|
crossorigin=""
|
|
>
|
|
</script>
|
|
|
|
<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 lg:grid-rows-1">
|
|
<!-- Position information -->
|
|
<div class="card bg-base-100 shadow-xl h-full">
|
|
<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"
|
|
phx-hook="TimeAgoHook"
|
|
id="last-position-time"
|
|
data-timestamp={DateTime.to_iso8601(@packet.received_at)}
|
|
>
|
|
{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 %>
|
|
<%= if @packet.comment do %>
|
|
<div class="col-span-2">
|
|
<dt class="text-xs font-medium opacity-70">{gettext("Comment")}</dt>
|
|
<dd class="mt-1 text-sm">
|
|
<%= if is_binary(@packet.comment) do %>
|
|
{Aprsme.EncodingUtils.sanitize_string(@packet.comment)}
|
|
<% else %>
|
|
{@packet.comment}
|
|
<% end %>
|
|
</dd>
|
|
</div>
|
|
<% end %>
|
|
<%= if @packet.phg_power || @packet.phg_height || @packet.phg_gain do %>
|
|
<div class="col-span-2">
|
|
<dt class="text-xs font-medium opacity-70 mb-2">
|
|
{gettext("PHG (Power-Height-Gain)")}
|
|
</dt>
|
|
<dd class="grid grid-cols-2 gap-2 text-sm">
|
|
<%= if @packet.phg_power do %>
|
|
<div>
|
|
<span class="opacity-70">{gettext("Power:")}</span>
|
|
<span class="font-semibold">{@packet.phg_power} W</span>
|
|
</div>
|
|
<% end %>
|
|
<%= if @packet.phg_height do %>
|
|
<div>
|
|
<span class="opacity-70">{gettext("HAAT:")}</span>
|
|
<span class="font-semibold">{@packet.phg_height} ft</span>
|
|
</div>
|
|
<% end %>
|
|
<%= if @packet.phg_gain do %>
|
|
<div>
|
|
<span class="opacity-70">{gettext("Gain:")}</span>
|
|
<span class="font-semibold">{@packet.phg_gain} dBi</span>
|
|
</div>
|
|
<% end %>
|
|
<%= if @packet.phg_directivity do %>
|
|
<div>
|
|
<span class="opacity-70">{gettext("Dir:")}</span>
|
|
<span class="font-semibold">
|
|
<%= if @packet.phg_directivity == 0 or @packet.phg_directivity == 360 do %>
|
|
{gettext("Omni")}
|
|
<% else %>
|
|
{@packet.phg_directivity}°
|
|
<% end %>
|
|
</span>
|
|
</div>
|
|
<% end %>
|
|
</dd>
|
|
</div>
|
|
<% end %>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Map showing station location -->
|
|
<%= if @packet && @packet.lat && @packet.lon do %>
|
|
<div class="card bg-base-100 shadow-xl h-full">
|
|
<% {symbol_table, symbol_code} = AprsmeWeb.AprsSymbol.extract_from_packet(@packet) %>
|
|
<% symbol_html = AprsmeWeb.AprsSymbol.render_marker_html(symbol_table, symbol_code, @callsign, 32) %>
|
|
<.info_map
|
|
id="station-location-map"
|
|
lat={Aprsme.EncodingUtils.to_float(@packet.lat)}
|
|
lon={Aprsme.EncodingUtils.to_float(@packet.lon)}
|
|
callsign={@callsign}
|
|
symbol_html={symbol_html}
|
|
height="100%"
|
|
zoom={12}
|
|
/>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- Row for Device Information and Other SSIDs -->
|
|
<div class="lg:col-span-2 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
<!-- 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">
|
|
<% path_elements = parse_path_with_links(@packet.path) %>
|
|
<%= for {{type, content, display}, index} <- Enum.with_index(path_elements) do %>
|
|
<%= case type do %>
|
|
<% :link -> %>
|
|
<.link navigate={~p"/info/#{content}"} class="link link-primary">
|
|
{display}
|
|
</.link>
|
|
<% :text -> %>
|
|
{content}
|
|
<% end %>
|
|
<%= if index < length(path_elements) - 1 do %>
|
|
<span class="opacity-50">,</span>
|
|
<% end %>
|
|
<% end %>
|
|
</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>
|
|
|
|
<!-- Other SSIDs -->
|
|
<%= if length(@other_ssids) > 0 do %>
|
|
<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.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-sm 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") %>
|
|
{format_distance(dist, locale)} @ {Float.round(course, 0)}°
|
|
<% else %>
|
|
-
|
|
<% end %>
|
|
</td>
|
|
<td class="opacity-70">
|
|
<%= if ssid_info.last_heard && ssid_info.last_heard.timestamp do %>
|
|
<span
|
|
class="text-sm"
|
|
phx-hook="TimeAgoHook"
|
|
id={"ssid-time-#{ssid_info.ssid}"}
|
|
data-timestamp={ssid_info.last_heard.timestamp}
|
|
>
|
|
{ssid_info.last_heard.time_ago}
|
|
</span>
|
|
<% else %>
|
|
<span class="text-sm opacity-70">{gettext("Unknown")}</span>
|
|
<% end %>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<!-- Empty placeholder to maintain grid layout -->
|
|
<div></div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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"
|
|
phx-hook="TimeAgoHook"
|
|
id={"neighbor-time-#{neighbor.callsign}"}
|
|
data-timestamp={neighbor.last_heard.timestamp}
|
|
>
|
|
{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>
|
|
|
|
<!-- Stations that heard this callsign on RF -->
|
|
<%= if length(@heard_by_stations) > 0 do %>
|
|
<div class="card bg-base-100 shadow-xl mt-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="M9.348 14.651a3.75 3.75 0 010-5.303m5.304 0a3.75 3.75 0 010 5.303m-7.425 2.122a6.75 6.75 0 010-9.546m9.546 0a6.75 6.75 0 010 9.546M5.106 18.894c-3.808-3.808-3.808-9.98 0-13.789m13.788 0c3.808 3.808 3.808 9.981 0 13.79M12 12h.008v.007H12V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-2">
|
|
<h3 class="text-sm font-medium">
|
|
{gettext("Stations which heard %{callsign} directly on radio",
|
|
callsign: @callsign
|
|
)}
|
|
</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("First Heard")}
|
|
</th>
|
|
<th class="text-xs font-medium uppercase tracking-wider">
|
|
{gettext("Last Heard")}
|
|
</th>
|
|
<th class="text-xs font-medium uppercase tracking-wider">
|
|
{gettext("Packets")}
|
|
</th>
|
|
<th class="text-xs font-medium uppercase tracking-wider">
|
|
{gettext("Longest Path")}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<%= for station <- @heard_by_stations do %>
|
|
<tr>
|
|
<td class="font-medium">
|
|
<.link navigate={~p"/info/#{station.digipeater}"} class="link link-primary">
|
|
{station.digipeater}
|
|
</.link>
|
|
</td>
|
|
<td class="opacity-70">
|
|
<%= if station.first_heard do %>
|
|
<div class="text-xs font-mono">
|
|
{Calendar.strftime(station.first_heard, "%Y-%m-%d %H:%M")}
|
|
</div>
|
|
<% else %>
|
|
-
|
|
<% end %>
|
|
</td>
|
|
<td class="opacity-70">
|
|
<%= if station.last_heard do %>
|
|
<div class="font-semibold">
|
|
{AprsmeWeb.TimeHelpers.time_ago_in_words(station.last_heard)}
|
|
</div>
|
|
<div class="text-xs opacity-70 font-mono">
|
|
{Calendar.strftime(station.last_heard, "%Y-%m-%d %H:%M")}
|
|
</div>
|
|
<% else %>
|
|
-
|
|
<% end %>
|
|
</td>
|
|
<td>
|
|
{station.packet_count}
|
|
</td>
|
|
<td class="opacity-70">
|
|
<%= if station.longest_distance do %>
|
|
{station.longest_distance}
|
|
<%= if station.longest_path_time do %>
|
|
<div class="text-xs opacity-70 font-mono">
|
|
{Calendar.strftime(station.longest_path_time, "%Y-%m-%d %H:%M")}
|
|
</div>
|
|
<% end %>
|
|
<% else %>
|
|
-
|
|
<% end %>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- Stations heard directly by this callsign -->
|
|
<%= if length(@stations_heard_by) > 0 do %>
|
|
<div class="card bg-base-100 shadow-xl mt-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="M9.348 14.651a3.75 3.75 0 010-5.303m5.304 0a3.75 3.75 0 010 5.303m-7.425 2.122a6.75 6.75 0 010-9.546m9.546 0a6.75 6.75 0 010 9.546M5.106 18.894c-3.808-3.808-3.808-9.98 0-13.789m13.788 0c3.808 3.808 3.808 9.981 0 13.79M12 12h.008v.007H12V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-2">
|
|
<h3 class="text-sm font-medium">
|
|
{gettext("Stations heard directly by %{callsign}",
|
|
callsign: @callsign
|
|
)}
|
|
</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("First Heard")}
|
|
</th>
|
|
<th class="text-xs font-medium uppercase tracking-wider">
|
|
{gettext("Last Heard")}
|
|
</th>
|
|
<th class="text-xs font-medium uppercase tracking-wider">
|
|
{gettext("Packets")}
|
|
</th>
|
|
<th class="text-xs font-medium uppercase tracking-wider">
|
|
{gettext("Longest Path")}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<%= for station <- @stations_heard_by do %>
|
|
<tr>
|
|
<td class="font-medium">
|
|
<.link navigate={~p"/info/#{station.station}"} class="link link-primary">
|
|
{station.station}
|
|
</.link>
|
|
</td>
|
|
<td class="opacity-70">
|
|
<%= if station.first_heard do %>
|
|
<div class="text-xs font-mono">
|
|
{Calendar.strftime(station.first_heard, "%Y-%m-%d %H:%M")}
|
|
</div>
|
|
<% else %>
|
|
-
|
|
<% end %>
|
|
</td>
|
|
<td class="opacity-70">
|
|
<%= if station.last_heard do %>
|
|
<div class="font-semibold">
|
|
{AprsmeWeb.TimeHelpers.time_ago_in_words(station.last_heard)}
|
|
</div>
|
|
<div class="text-xs opacity-70 font-mono">
|
|
{Calendar.strftime(station.last_heard, "%Y-%m-%d %H:%M")}
|
|
</div>
|
|
<% else %>
|
|
-
|
|
<% end %>
|
|
</td>
|
|
<td>
|
|
{station.packet_count}
|
|
</td>
|
|
<td class="opacity-70">
|
|
<%= if station.longest_distance do %>
|
|
{station.longest_distance}
|
|
<%= if station.longest_path_time do %>
|
|
<div class="text-xs opacity-70 font-mono">
|
|
{Calendar.strftime(station.longest_path_time, "%Y-%m-%d %H:%M")}
|
|
</div>
|
|
<% end %>
|
|
<% else %>
|
|
-
|
|
<% end %>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
<% 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>
|