diff --git a/lib/aprs_web/components/core_components.ex b/lib/aprs_web/components/core_components.ex index 75e3824..644cea3 100644 --- a/lib/aprs_web/components/core_components.ex +++ b/lib/aprs_web/components/core_components.ex @@ -393,26 +393,29 @@ defmodule AprsWeb.CoreComponents do end @doc """ - Renders a header with title. + Renders a modern site header with navigation links. """ - attr :class, :string, default: nil - - slot :inner_block, required: true - slot :subtitle - slot :actions + attr :class, :string, default: "" def header(assigns) do ~H""" -
-
-

- {render_slot(@inner_block)} -

-

- {render_slot(@subtitle)} -

-
-
{render_slot(@actions)}
+
+
""" end diff --git a/lib/aprs_web/components/layouts/app.html.heex b/lib/aprs_web/components/layouts/app.html.heex index 17da568..30deda0 100644 --- a/lib/aprs_web/components/layouts/app.html.heex +++ b/lib/aprs_web/components/layouts/app.html.heex @@ -1,3 +1,4 @@ +<.header />
<.flash kind={:info} title="Success!" flash={@flash} /> diff --git a/lib/aprs_web/live/about_live.ex b/lib/aprs_web/live/about_live.ex new file mode 100644 index 0000000..304b611 --- /dev/null +++ b/lib/aprs_web/live/about_live.ex @@ -0,0 +1,9 @@ +defmodule AprsWeb.AboutLive do + @moduledoc false + use AprsWeb, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, page_title: "About aprs.me")} + end +end diff --git a/lib/aprs_web/live/about_live.html.heex b/lib/aprs_web/live/about_live.html.heex new file mode 100644 index 0000000..e69de29 diff --git a/lib/aprs_web/live/bad_packets_live/index.html.heex b/lib/aprs_web/live/bad_packets_live/index.html.heex index dbcd9f9..56c3672 100644 --- a/lib/aprs_web/live/bad_packets_live/index.html.heex +++ b/lib/aprs_web/live/bad_packets_live/index.html.heex @@ -1,74 +1,6 @@
- <.header> -
-
-

- Bad APRS Packets - - (Showing {length(@bad_packets)} most recent) - -

-

- Packets that failed to parse or process (limited to 100 most recent) - <%= if @last_updated do %> - - Last updated: {Calendar.strftime(@last_updated, "%H:%M:%S UTC")} - - <% end %> -

-
-
- - <.link navigate={~p"/packets"} class="text-sm text-blue-600 hover:text-blue-800"> - View Good Packets → - - <.link navigate={~p"/"} class="text-sm text-blue-600 hover:text-blue-800"> - ← Back to Map - -
-
- + <.header />
diff --git a/lib/aprs_web/live/map_live/index.ex b/lib/aprs_web/live/map_live/index.ex index dc14e4e..5193422 100644 --- a/lib/aprs_web/live/map_live/index.ex +++ b/lib/aprs_web/live/map_live/index.ex @@ -52,7 +52,7 @@ defmodule AprsWeb.MapLive.Index do overlay_callsign: "", trail_duration: "1", historical_hours: "1", - packet_age_threshold: 3600, + packet_age_threshold: one_hour_ago, slideover_open: true, replay_active: false, replay_start_time: nil, @@ -1016,7 +1016,26 @@ defmodule AprsWeb.MapLive.Index do defp packet_within_time_threshold?(packet, threshold) do case packet do %{received_at: received_at} when not is_nil(received_at) -> - DateTime.compare(received_at, threshold) in [:gt, :eq] + threshold_dt = + cond do + is_integer(threshold) -> + # Assume seconds since epoch + DateTime.from_unix!(threshold) + + is_binary(threshold) -> + case DateTime.from_iso8601(threshold) do + {:ok, dt, _} -> dt + _ -> DateTime.utc_now() + end + + match?(%DateTime{}, threshold) -> + threshold + + true -> + DateTime.utc_now() + end + + DateTime.compare(received_at, threshold_dt) in [:gt, :eq] _ -> # If no timestamp, treat as current @@ -1381,7 +1400,7 @@ defmodule AprsWeb.MapLive.Index do #{comment_html}
#{Float.round(PacketUtils.to_float(lat), 4)}, #{Float.round(PacketUtils.to_float(lon), 4)}
-
#{packet_info.timestamp}
+
#{format_popup_timestamp(packet_info.timestamp)}
""" end @@ -1408,11 +1427,11 @@ defmodule AprsWeb.MapLive.Index do defp build_weather_popup_html(packet, callsign) do received_at = get_received_at(packet) - timestamp_str = format_timestamp(received_at) + timestamp_str = format_popup_timestamp(received_at) """ #{callsign} - Weather Report
- #{timestamp_str} UTC + #{timestamp_str}
Temperature: #{PacketUtils.get_weather_field(packet, :temperature)}°F
Humidity: #{PacketUtils.get_weather_field(packet, :humidity)}%
@@ -1432,10 +1451,30 @@ defmodule AprsWeb.MapLive.Index do end end - defp format_timestamp(received_at) do - if received_at, - do: Calendar.strftime(received_at, "%Y-%m-%d %H:%M:%S"), - else: "N/A" + defp format_popup_timestamp(ts) do + cond do + is_binary(ts) -> + case DateTime.from_iso8601(ts) do + {:ok, dt, _} -> Calendar.strftime(dt, "%Y-%m-%d %H:%M:%S UTC") + _ -> ts + end + + is_integer(ts) -> + ts + |> DateTime.from_unix!(:millisecond) + |> Calendar.strftime("%Y-%m-%d %H:%M:%S UTC") + + match?(%DateTime{}, ts) -> + Calendar.strftime(ts, "%Y-%m-%d %H:%M:%S UTC") + + match?(%NaiveDateTime{}, ts) -> + ts + |> DateTime.from_naive!("Etc/UTC") + |> Calendar.strftime("%Y-%m-%d %H:%M:%S UTC") + + true -> + to_string(ts) + end end @spec generate_callsign(map() | struct()) :: String.t() diff --git a/lib/aprs_web/live/packets_live/callsign_view.html.heex b/lib/aprs_web/live/packets_live/callsign_view.html.heex index 0a5161a..b93c30b 100644 --- a/lib/aprs_web/live/packets_live/callsign_view.html.heex +++ b/lib/aprs_web/live/packets_live/callsign_view.html.heex @@ -1,23 +1,6 @@
- <.header> -
-
-

Packets for {@callsign}

-

- Showing up to 100 packets (stored and live) for callsign {@callsign} -

-
-
- <.link navigate={~p"/"} class="text-sm text-blue-600 hover:text-blue-800"> - ← Back to Map - - <.link navigate={~p"/packets"} class="text-sm text-blue-600 hover:text-blue-800"> - All Packets - -
-
- + <.header />
<%= if @error do %> diff --git a/lib/aprs_web/live/packets_live/index.html.heex b/lib/aprs_web/live/packets_live/index.html.heex index 0f4bfeb..0cc8acb 100644 --- a/lib/aprs_web/live/packets_live/index.html.heex +++ b/lib/aprs_web/live/packets_live/index.html.heex @@ -1,21 +1,6 @@
- <.header> -
-
-

APRS Packets

-

Live and recent APRS packets from the network

-
-
- <.link navigate={~p"/badpackets"} class="text-sm text-blue-600 hover:text-blue-800"> - View Bad Packets → - - <.link navigate={~p"/"} class="text-sm text-blue-600 hover:text-blue-800"> - ← Back to Map - -
-
- + <.header />
diff --git a/lib/aprs_web/live/user_confirmation_instructions_live.ex b/lib/aprs_web/live/user_confirmation_instructions_live.ex index 8b16b03..9fdd6b8 100644 --- a/lib/aprs_web/live/user_confirmation_instructions_live.ex +++ b/lib/aprs_web/live/user_confirmation_instructions_live.ex @@ -6,7 +6,7 @@ defmodule AprsWeb.UserConfirmationInstructionsLive do def render(assigns) do ~H""" - <.header>Resend confirmation instructions + <.header /> <.simple_form :let={f} diff --git a/lib/aprs_web/live/user_confirmation_live.ex b/lib/aprs_web/live/user_confirmation_live.ex index 1072e9f..3b8f7a6 100644 --- a/lib/aprs_web/live/user_confirmation_live.ex +++ b/lib/aprs_web/live/user_confirmation_live.ex @@ -7,7 +7,7 @@ defmodule AprsWeb.UserConfirmationLive do def render(%{live_action: :edit} = assigns) do ~H"""
- <.header class="text-center">Confirm Account + <.header class="text-center" /> <.simple_form :let={f} for={%{}} as={:user} id="confirmation_form" phx-submit="confirm_account"> <.input field={{f, :token}} type="hidden" value={@token} /> diff --git a/lib/aprs_web/live/user_forgot_password_live.ex b/lib/aprs_web/live/user_forgot_password_live.ex index d42a717..5c26e48 100644 --- a/lib/aprs_web/live/user_forgot_password_live.ex +++ b/lib/aprs_web/live/user_forgot_password_live.ex @@ -7,6 +7,8 @@ defmodule AprsWeb.UserForgotPasswordLive do def render(assigns) do ~H"""
+ <.header class="text-center" /> + <.header class="text-center"> Forgot your password? <:subtitle>We'll send a password reset link to your inbox diff --git a/lib/aprs_web/live/user_login_live.ex b/lib/aprs_web/live/user_login_live.ex index 74024c9..2f4ae18 100644 --- a/lib/aprs_web/live/user_login_live.ex +++ b/lib/aprs_web/live/user_login_live.ex @@ -8,6 +8,8 @@ defmodule AprsWeb.UserLoginLive do def render(assigns) do ~H"""
+ <.header class="text-center" /> + <.header class="text-center"> Sign in to account <:subtitle> diff --git a/lib/aprs_web/live/user_registration_live.ex b/lib/aprs_web/live/user_registration_live.ex index b8d84c9..78209ec 100644 --- a/lib/aprs_web/live/user_registration_live.ex +++ b/lib/aprs_web/live/user_registration_live.ex @@ -8,6 +8,8 @@ defmodule AprsWeb.UserRegistrationLive do def render(assigns) do ~H"""
+ <.header class="text-center" /> + <.header class="text-center"> Register for an account <:subtitle> diff --git a/lib/aprs_web/live/user_reset_password_live.ex b/lib/aprs_web/live/user_reset_password_live.ex index c782cd0..87bc649 100644 --- a/lib/aprs_web/live/user_reset_password_live.ex +++ b/lib/aprs_web/live/user_reset_password_live.ex @@ -7,7 +7,7 @@ defmodule AprsWeb.UserResetPasswordLive do def render(assigns) do ~H"""
- <.header class="text-center">Reset Password + <.header class="text-center" /> <.simple_form :let={f} diff --git a/lib/aprs_web/live/user_settings_live.ex b/lib/aprs_web/live/user_settings_live.ex index 120de75..f6b6039 100644 --- a/lib/aprs_web/live/user_settings_live.ex +++ b/lib/aprs_web/live/user_settings_live.ex @@ -6,7 +6,7 @@ defmodule AprsWeb.UserSettingsLive do def render(assigns) do ~H""" - <.header>Change Email + <.header /> <.simple_form :let={f} @@ -35,7 +35,7 @@ defmodule AprsWeb.UserSettingsLive do - <.header>Change Password + <.header /> <.simple_form :let={f} diff --git a/lib/aprs_web/live/weather_live/callsign_view.ex b/lib/aprs_web/live/weather_live/callsign_view.ex new file mode 100644 index 0000000..acdcb16 --- /dev/null +++ b/lib/aprs_web/live/weather_live/callsign_view.ex @@ -0,0 +1,27 @@ +defmodule AprsWeb.WeatherLive.CallsignView do + use AprsWeb, :live_view + + alias Aprs.Packets + alias AprsWeb.MapLive.PacketUtils + + @impl true + def mount(%{"callsign" => callsign}, _session, socket) do + normalized_callsign = String.upcase(String.trim(callsign)) + weather_packet = get_latest_weather_packet(normalized_callsign) + + socket = + socket + |> assign(:callsign, normalized_callsign) + |> assign(:weather_packet, weather_packet) + |> assign(:page_title, "Weather for #{normalized_callsign}") + + {:ok, socket} + end + + defp get_latest_weather_packet(callsign) do + # Get the most recent packet for this callsign that is a weather report + %{callsign: callsign, limit: 10} + |> Packets.get_recent_packets() + |> Enum.find(&PacketUtils.is_weather_packet?/1) + end +end diff --git a/lib/aprs_web/live/weather_live/callsign_view.html.heex b/lib/aprs_web/live/weather_live/callsign_view.html.heex new file mode 100644 index 0000000..7643fd2 --- /dev/null +++ b/lib/aprs_web/live/weather_live/callsign_view.html.heex @@ -0,0 +1,53 @@ +<%= if @weather_packet do %> +
+

Weather for {@callsign}

+
+ {PacketUtils.get_timestamp(@weather_packet)} +
+
+
+ Temperature: + {PacketUtils.get_weather_field(@weather_packet, :temperature)}°F +
+
+ Humidity: + {PacketUtils.get_weather_field(@weather_packet, :humidity)}% +
+
+ Wind: + {PacketUtils.get_weather_field(@weather_packet, :wind_direction)}° @ {PacketUtils.get_weather_field( + @weather_packet, + :wind_speed + )} mph +
+
+ Gusts: + {PacketUtils.get_weather_field(@weather_packet, :wind_gust)} mph +
+
+ Pressure: + {PacketUtils.get_weather_field(@weather_packet, :pressure)} hPa +
+
+ Rain (1h): + {PacketUtils.get_weather_field(@weather_packet, :rain_1h)} in +
+
+ Rain (24h): + {PacketUtils.get_weather_field(@weather_packet, :rain_24h)} in +
+
+ Rain (since midnight): + {PacketUtils.get_weather_field(@weather_packet, :rain_since_midnight)} in +
+
+
+ Raw comment: {@weather_packet.comment || @weather_packet["comment"]} +
+
+<% else %> +
+

Weather for {@callsign}

+
No recent weather data available for this callsign.
+
+<% end %> diff --git a/lib/aprs_web/router.ex b/lib/aprs_web/router.ex index 2f1706e..46a4b14 100644 --- a/lib/aprs_web/router.ex +++ b/lib/aprs_web/router.ex @@ -40,6 +40,8 @@ defmodule AprsWeb.Router do live "/packets", PacketsLive.Index, :index live "/packets/:callsign", PacketsLive.CallsignView, :index live "/badpackets", BadPacketsLive.Index, :index + live "/weather/:callsign", WeatherLive.CallsignView, :index + live "/about", AboutLive, :index end end