Fix QSO show page weather lookup and add expandable sounding profiles

Fix pos1 longitude key mismatch (lng vs lon) that caused empty weather
results for all QSOs. Bump search radius to 300km for sounding coverage.
Soundings now show as expandable cards with full elevation profile data.
This commit is contained in:
Graham McIntire 2026-03-29 14:33:14 -05:00
parent c11e6f06f4
commit 480480bde3
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59

View file

@ -22,7 +22,8 @@ defmodule MicrowavepropWeb.QsoLive.Show do
obs_sort_by: "station_name",
obs_sort_order: "asc",
sounding_sort_by: "station_name",
sounding_sort_order: "asc"
sounding_sort_order: "asc",
expanded_soundings: MapSet.new()
)}
end
@ -41,6 +42,17 @@ defmodule MicrowavepropWeb.QsoLive.Show do
)}
end
def handle_event("toggle_profile", %{"id" => id}, socket) do
expanded =
if MapSet.member?(socket.assigns.expanded_soundings, id) do
MapSet.delete(socket.assigns.expanded_soundings, id)
else
MapSet.put(socket.assigns.expanded_soundings, id)
end
{:noreply, assign(socket, expanded_soundings: expanded)}
end
def handle_event("sort", %{"field" => field, "table" => "soundings"}, socket) do
{sort_by, sort_order} =
toggle_sort(socket.assigns.sounding_sort_by, socket.assigns.sounding_sort_order, field)
@ -91,12 +103,15 @@ defmodule MicrowavepropWeb.QsoLive.Show do
defp sounding_sort_key(_), do: fn s -> s.station.name || s.station.station_code end
defp load_weather(qso) do
case qso.pos1 do
%{"lat" => lat, "lon" => lon} ->
Weather.weather_for_qso(%{lat: lat, lon: lon, timestamp: qso.qso_timestamp})
lat = qso.pos1 && qso.pos1["lat"]
lon = qso.pos1 && (qso.pos1["lon"] || qso.pos1["lng"])
_ ->
%{surface_observations: [], soundings: []}
if lat && lon do
Weather.weather_for_qso(%{lat: lat, lon: lon, timestamp: qso.qso_timestamp},
radius_km: 300
)
else
%{surface_observations: [], soundings: []}
end
end
@ -175,32 +190,67 @@ defmodule MicrowavepropWeb.QsoLive.Show do
<%= if @soundings == [] do %>
<p class="text-sm text-base-content/50 italic">No soundings found nearby.</p>
<% else %>
<.table
id="soundings"
rows={@soundings}
row_id={fn s -> "sounding-#{s.id}" end}
sort_by={@sounding_sort_by}
sort_order={@sounding_sort_order}
sort_target="soundings"
>
<:col :let={s} label="Station" sort_field="station_name">
{s.station.name || s.station.station_code}
</:col>
<:col :let={s} label="Time" sort_field="observed_at">
{Calendar.strftime(s.observed_at, "%H:%M")}
</:col>
<:col :let={s} label="Sfc Temp (C)" sort_field="surface_temp_c">
{s.surface_temp_c || ""}
</:col>
<:col :let={s} label="Refractivity" sort_field="surface_refractivity">
{format_number(s.surface_refractivity)}
</:col>
<:col :let={s} label="Ducting">{if s.ducting_detected, do: "Yes", else: "No"}</:col>
<:col :let={s} label="K-Index" sort_field="k_index">{format_number(s.k_index)}</:col>
<:col :let={s} label="Lifted Index" sort_field="lifted_index">
{format_number(s.lifted_index)}
</:col>
</.table>
<%= for s <- @soundings do %>
<div class="mb-6 border border-base-300 rounded-lg">
<button
phx-click="toggle_profile"
phx-value-id={s.id}
class="w-full flex items-center justify-between p-4 hover:bg-base-200 transition-colors"
>
<div class="flex items-center gap-4 text-sm">
<span class="font-semibold">{s.station.name || s.station.station_code}</span>
<span>{Calendar.strftime(s.observed_at, "%H:%M UTC")}</span>
<span>{s.level_count} levels</span>
<span>Sfc: {format_number(s.surface_temp_c)}°C</span>
<span>N: {format_number(s.surface_refractivity)}</span>
<span>K: {format_number(s.k_index)}</span>
<span>LI: {format_number(s.lifted_index)}</span>
<%= if s.ducting_detected do %>
<span class="badge badge-warning badge-sm">Ducting</span>
<% end %>
</div>
<.icon
name={
if MapSet.member?(@expanded_soundings, s.id),
do: "hero-chevron-up",
else: "hero-chevron-down"
}
class="w-5 h-5"
/>
</button>
<%= if MapSet.member?(@expanded_soundings, s.id) do %>
<div class="px-4 pb-4">
<div class="overflow-x-auto">
<table class="table table-xs table-zebra">
<thead>
<tr>
<th>Pressure (mb)</th>
<th>Height (m)</th>
<th>Temp (°C)</th>
<th>Dewpoint (°C)</th>
<th>Wind Dir (°)</th>
<th>Wind Spd (kts)</th>
</tr>
</thead>
<tbody>
<%= for level <- filter_profile(s.profile) do %>
<tr>
<td>{format_number(level["pres"])}</td>
<td>{format_number(level["hght"])}</td>
<td>{format_number(level["tmpc"])}</td>
<td>{format_number(level["dwpc"])}</td>
<td>{format_number(level["drct"])}</td>
<td>{format_number(level["sknt"])}</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% end %>
</div>
<% end %>
<% end %>
<div class="divider" />
@ -231,6 +281,12 @@ defmodule MicrowavepropWeb.QsoLive.Show do
end
end
defp filter_profile(profile) do
Enum.filter(profile, fn level ->
level["tmpc"] != nil || level["dwpc"] != nil
end)
end
defp format_kp(nil), do: ""
defp format_kp(values) when is_list(values), do: Enum.map_join(values, ", ", &format_number/1)
end