aprs.me/lib/aprsme_web/live/weather_live/callsign_view.html.heex
Graham McIntire e6d8894bf6
fix: Handle numeric weather values and hide N/A fields in weather view
The weather view was crashing with CaseClauseError when receiving numeric
values (like 83.0) from the database instead of strings. Also improved
the UI to only show weather fields that have valid data.

Changes:
- Added support for numeric values in format_weather_value function
- Added has_weather_field? helper to check for valid weather data
- Updated template to conditionally display weather fields
- Fields with nil, "N/A", or empty values are now hidden
- Wind field shows direction and speed independently if available

This prevents crashes and provides a cleaner UI by only showing available
weather data instead of displaying N/A values.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-03 15:33:09 -05:00

264 lines
12 KiB
Text

<%= if @weather_packet do %>
<div class="bg-base-200">
<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="flex items-center">
<h1 class="text-xl font-bold leading-7 sm:truncate sm:text-2xl">
{gettext("Weather Station")}
</h1>
<div class="ml-3 flex items-center space-x-3">
<span class="badge badge-primary">
{@callsign}
</span>
<% {symbol_table_id, symbol_code} =
AprsmeWeb.MapLive.PacketUtils.get_symbol_info(@weather_packet) %>
<% symbol_table =
if symbol_table_id in ["/", "\\", "]"], do: symbol_table_id, else: "/" %>
<% symbol_code = (symbol_code || ">") |> to_string() %>
<% table_id =
if symbol_table == "/", do: "0", else: if(symbol_table == "]", do: "2", else: "1") %>
<% symbol_code_ord =
symbol_code
|> String.to_charlist()
|> List.first()
|> (fn c -> if is_integer(c), do: c, else: 63 end).() %>
<% index = symbol_code_ord - 33 %>
<% safe_index = max(0, min(index, 93)) %>
<% column = rem(safe_index, 16) %>
<% row = div(safe_index, 16) %>
<% x = -column * 128 %>
<% y = -row * 128 %>
<% symbol_img = "/aprs-symbols/aprs-symbols-128-#{table_id}@2x.png" %>
<div
style={"width: 32px; height: 32px; background-image: url(#{symbol_img}); background-position: #{x / 4}px #{y / 4}px; background-size: 512px 192px; background-repeat: no-repeat; image-rendering: pixelated; opacity: 1.0; display: inline-block; vertical-align: middle; margin-bottom: -6px;"}
title={"Symbol: #{symbol_code} (#{symbol_code_ord}) at row #{row}, col #{column}"}
>
</div>
</div>
</div>
<div class="mt-3 flex md:ml-4 md:mt-0">
<div class="flex space-x-2">
<.link navigate={~p"/info/#{@callsign}"} class="btn btn-outline btn-sm">
{gettext("View info")}
</.link>
<.link navigate={~p"/packets/#{@callsign}"} class="btn btn-outline btn-sm">
{gettext("View packets")}
</.link>
<.link navigate={"/?call=#{@callsign}"} class="btn btn-primary btn-sm">
{gettext("View on map")}
</.link>
</div>
</div>
</div>
</div>
</div>
<div class="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4">
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<% dt =
AprsmeWeb.TimeHelpers.to_datetime(
Map.get(@weather_packet, :inserted_at) || Map.get(@weather_packet, "inserted_at")
) %>
<div class="flex items-center mb-3">
<div class="flex-shrink-0">
<svg class="h-4 w-4 text-primary" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.75.75 0 00.736-.686L11.477 4.5a.75.75 0 00-1.491-.154L9.477 9.5H9z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class="ml-2">
<h3 class="text-sm font-medium">{gettext("Weather Details")}</h3>
</div>
</div>
<dl class="grid grid-cols-2 gap-3">
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Last WX report")}</dt>
<dd class="mt-1 text-sm font-semibold">
<%= if dt do %>
{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S UTC")} ({AprsmeWeb.TimeHelpers.time_ago_in_words(dt)})<br />
<%!-- <span class="text-xs text-gray-500">{Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S %Z")} local time at {Map.get(@weather_packet, :region) || Map.get(@weather_packet, "region") || "Unknown location"} [?]</span> --%>
<% else %>
{gettext("Unknown")}
<% end %>
</dd>
</div>
<%= if has_weather_field?(@weather_packet, :temperature) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Temperature")}</dt>
<dd class="mt-1 text-sm font-semibold">
{format_weather_value(@weather_packet, :temperature, @locale)}
</dd>
</div>
<% end %>
<%= if has_weather_field?(@weather_packet, :humidity) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Humidity")}</dt>
<dd class="mt-1 text-sm font-semibold">
{PacketUtils.get_weather_field(@weather_packet, :humidity)}%
</dd>
</div>
<% end %>
<%= if has_weather_field?(@weather_packet, :wind_direction) || has_weather_field?(@weather_packet, :wind_speed) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Wind")}</dt>
<dd class="mt-1 text-sm font-semibold">
<%= if has_weather_field?(@weather_packet, :wind_direction) do %>
{PacketUtils.get_weather_field(@weather_packet, :wind_direction)}°
<% end %>
<%= if has_weather_field?(@weather_packet, :wind_speed) do %>
@ {format_weather_value(@weather_packet, :wind_speed, @locale)}
<% end %>
</dd>
</div>
<% end %>
<%= if has_weather_field?(@weather_packet, :wind_gust) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Gusts")}</dt>
<dd class="mt-1 text-sm font-semibold">
{format_weather_value(@weather_packet, :wind_gust, @locale)}
</dd>
</div>
<% end %>
<%= if has_weather_field?(@weather_packet, :pressure) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Pressure")}</dt>
<dd class="mt-1 text-sm font-semibold">
{PacketUtils.get_weather_field(@weather_packet, :pressure)} hPa
</dd>
</div>
<% end %>
<%= if has_weather_field?(@weather_packet, :rain_1h) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Rain (1h)")}</dt>
<dd class="mt-1 text-sm font-semibold">
{format_weather_value(@weather_packet, :rain_1h, @locale)}
</dd>
</div>
<% end %>
<%= if has_weather_field?(@weather_packet, :rain_24h) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Rain (24h)")}</dt>
<dd class="mt-1 text-sm font-semibold">
{format_weather_value(@weather_packet, :rain_24h, @locale)}
</dd>
</div>
<% end %>
<%= if has_weather_field?(@weather_packet, :rain_since_midnight) do %>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Rain (since midnight)")}</dt>
<dd class="mt-1 text-sm font-semibold">
{format_weather_value(@weather_packet, :rain_since_midnight, @locale)}
</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(@weather_packet.raw_packet) do %>
{Aprsme.EncodingUtils.sanitize_string(@weather_packet.raw_packet)}
<% else %>
{@weather_packet.raw_packet || ""}
<% end %>
</dd>
</div>
</dl>
<div class="mt-2 text-xs opacity-70">
<strong>{gettext("Raw comment")}:</strong> {@weather_packet.comment ||
@weather_packet["comment"]}
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h3 class="text-sm font-medium mb-2">{gettext("Weather History Graphs")}</h3>
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :temperature, nil)))) do %>
<div
id="temp-chart"
phx-hook="ChartJSTempChart"
data-weather-history={@weather_history_json}
data-chart-labels={Jason.encode!(@chart_labels)}
class="bg-base-200 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
</div>
<% end %>
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :humidity, nil)))) do %>
<div
id="humidity-chart"
phx-hook="ChartJSHumidityChart"
data-weather-history={@weather_history_json}
data-chart-labels={Jason.encode!(@chart_labels)}
class="bg-base-200 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
</div>
<% end %>
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :pressure, nil)))) do %>
<div
id="pressure-chart"
phx-hook="ChartJSPressureChart"
data-weather-history={@weather_history_json}
data-chart-labels={Jason.encode!(@chart_labels)}
class="bg-base-200 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
</div>
<% end %>
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :wind_speed, nil)))) do %>
<div
id="wind-chart"
phx-hook="ChartJSWindChart"
data-weather-history={@weather_history_json}
data-chart-labels={Jason.encode!(@chart_labels)}
class="bg-base-200 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
</div>
<% end %>
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :rain_1h, nil)))) or Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :rain_24h, nil)))) or Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :rain_since_midnight, nil)))) do %>
<div
id="rain-chart"
phx-hook="ChartJSRainChart"
data-weather-history={@weather_history_json}
data-chart-labels={Jason.encode!(@chart_labels)}
class="bg-base-200 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
</div>
<% end %>
<%= if Enum.any?(@weather_history, &(not is_nil(Map.get(&1, :luminosity, nil)))) do %>
<div
id="luminosity-chart"
phx-hook="ChartJSLuminosityChart"
data-weather-history={@weather_history_json}
data-chart-labels={Jason.encode!(@chart_labels)}
class="bg-base-200 rounded-lg p-4 mb-4"
style="height:250px;"
>
<canvas></canvas>
</div>
<% end %>
<%!-- Chart.js and adapter are now loaded from vendor.js bundle --%>
</div>
</div>
</div>
</div>
<% else %>
<div class="bg-base-200 flex items-center justify-center min-h-screen">
<div class="card bg-base-100 shadow-xl max-w-lg">
<div class="card-body text-center">
<h1 class="text-2xl font-bold mb-2">Weather for {@callsign}</h1>
<div class="opacity-70">No recent weather data available for this callsign.</div>
</div>
</div>
</div>
<% end %>