aprs.me/lib/aprsme_web/live/weather_live/callsign_view.html.heex
Graham McIntire 7dc6c616a2
Bundle all vendor JavaScript files locally instead of loading from CDNs
This change eliminates all external CDN dependencies by bundling JavaScript
libraries locally using esbuild. All vendor files are now served from the
application's own assets, improving reliability and privacy.

Changes:
- Created assets/js/vendor.js to bundle all external libraries
- Added vendor bundle configuration to esbuild in config files
- Updated all templates to use local vendor.js instead of CDN scripts
- Created download-vendor-files.sh script to fetch vendor libraries
- Added vendor files to .gitignore (they're downloaded during setup)
- Updated CSS imports to include vendor stylesheets
- Added vendor build step to assets.deploy mix task

Bundled libraries:
- Leaflet 1.9.4 (mapping library)
- Leaflet.heat 0.2.0 (heatmap plugin)
- Leaflet.markercluster 1.5.3 (marker clustering)
- OverlappingMarkerSpiderfier 0.2.6 (overlapping marker handling)
- Chart.js 4.5.0 (charting library)
- chartjs-adapter-date-fns 3.0.0 (date adapter for Chart.js)

All functionality tested and working correctly with local bundles.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 09:52:39 -05:00

247 lines
11 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 || ">" %>
<% 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>
<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>
<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>
<div>
<dt class="text-xs font-medium opacity-70">{gettext("Wind")}</dt>
<dd class="mt-1 text-sm font-semibold">
{PacketUtils.get_weather_field(@weather_packet, :wind_direction)}° @ {format_weather_value(
@weather_packet,
:wind_speed,
@locale
)}
</dd>
</div>
<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>
<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>
<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>
<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>
<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>
<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 %>