diff --git a/lib/aprs_web/live/api_docs_live.ex b/lib/aprs_web/live/api_docs_live.ex index 81c3cf1..670be32 100644 --- a/lib/aprs_web/live/api_docs_live.ex +++ b/lib/aprs_web/live/api_docs_live.ex @@ -6,9 +6,204 @@ defmodule AprsWeb.ApiDocsLive do @impl true def mount(_params, _session, socket) do - {:ok, assign(socket, page_title: "API Documentation")} + {:ok, + socket + |> assign(page_title: "API Documentation") + |> assign(test_callsign: "") + |> assign(loading: false) + |> assign(api_result: nil) + |> assign(error: nil)} end + @impl true + def handle_event("update_callsign", %{"callsign" => callsign}, socket) do + {:noreply, + socket + |> assign(test_callsign: callsign) + |> assign(error: nil)} + end + + @impl true + def handle_event("test_api", %{"callsign" => callsign}, socket) do + normalized_callsign = callsign |> String.trim() |> String.upcase() + + if normalized_callsign == "" do + {:noreply, assign(socket, error: "Please enter a callsign")} + else + socket = + socket + |> assign(loading: true) + |> assign(error: nil) + |> assign(api_result: nil) + + # Make the API call + send(self(), {:call_api, normalized_callsign}) + {:noreply, socket} + end + end + + @impl true + def handle_info({:call_api, callsign}, socket) do + case call_api(callsign) do + {:ok, result} -> + {:noreply, + socket + |> assign(loading: false) + |> assign(api_result: result)} + + {:error, error_message} -> + {:noreply, + socket + |> assign(loading: false) + |> assign(error: error_message)} + end + end + + defp call_api(callsign) do + # Use the same validation and lookup logic as the API controller + alias Aprs.Packets + + # Validate callsign format + if validate_callsign_format(callsign) do + # Try to get the most recent packet for this callsign + thirty_days_ago = DateTime.add(DateTime.utc_now(), -30, :day) + + opts = %{ + callsign: callsign, + start_time: thirty_days_ago, + limit: 1 + } + + case Packets.get_recent_packets(opts) do + [] -> + {:ok, %{"data" => nil, "message" => "No packets found for callsign #{callsign}"}} + + [packet | _] -> + # Format the packet data similar to the JSON view + packet_data = format_packet_for_display(packet) + {:ok, %{"data" => packet_data}} + + {:error, _reason} -> + {:error, "Database error occurred while fetching packet data"} + end + else + {:error, "Invalid callsign format"} + end + rescue + error -> + {:error, "An error occurred: #{inspect(error)}"} + end + + defp validate_callsign_format(callsign) do + # Same validation as in the controller + callsign_regex = ~r/^[A-Z0-9]{1,3}[0-9][A-Z]{1,4}(-[0-9]{1,2})?$/ + String.match?(callsign, callsign_regex) and String.length(callsign) <= 12 + end + + defp format_packet_for_display(%Aprs.Packet{} = packet) do + %{ + "id" => packet.id, + "callsign" => format_callsign(packet.base_callsign, packet.ssid), + "base_callsign" => packet.base_callsign, + "ssid" => packet.ssid, + "sender" => packet.sender, + "destination" => packet.destination, + "path" => packet.path, + "data_type" => packet.data_type, + "information_field" => packet.information_field, + "raw_packet" => packet.raw_packet, + "received_at" => packet.received_at, + "region" => packet.region, + "position" => position_data(packet), + "symbol" => symbol_data(packet), + "comment" => packet.comment, + "timestamp" => packet.timestamp, + "aprs_messaging" => packet.aprs_messaging, + "weather" => weather_data(packet), + "equipment" => equipment_data(packet), + "message" => message_data(packet), + "has_position" => packet.has_position, + "inserted_at" => packet.inserted_at, + "updated_at" => packet.updated_at + } + end + + defp format_callsign(base_callsign, nil), do: base_callsign + defp format_callsign(base_callsign, "0"), do: base_callsign + defp format_callsign(base_callsign, ssid), do: "#{base_callsign}-#{ssid}" + + defp position_data(%Aprs.Packet{has_position: false}), do: nil + defp position_data(%Aprs.Packet{lat: nil, lon: nil}), do: nil + + defp position_data(%Aprs.Packet{} = packet) do + base_position = %{ + "latitude" => to_float(packet.lat), + "longitude" => to_float(packet.lon) + } + + base_position + |> maybe_add("course", packet.course) + |> maybe_add("speed", packet.speed) + |> maybe_add("altitude", packet.altitude) + end + + defp symbol_data(%Aprs.Packet{symbol_code: nil, symbol_table_id: nil}), do: nil + + defp symbol_data(%Aprs.Packet{} = packet) do + %{ + "code" => packet.symbol_code, + "table_id" => packet.symbol_table_id + } + end + + defp weather_data(%Aprs.Packet{} = packet) do + weather_data = + %{} + |> maybe_add("temperature", packet.temperature) + |> maybe_add("humidity", packet.humidity) + |> maybe_add("wind_speed", packet.wind_speed) + |> maybe_add("wind_direction", packet.wind_direction) + |> maybe_add("wind_gust", packet.wind_gust) + |> maybe_add("pressure", packet.pressure) + |> maybe_add("rain_1h", packet.rain_1h) + |> maybe_add("rain_24h", packet.rain_24h) + |> maybe_add("rain_since_midnight", packet.rain_since_midnight) + + case weather_data do + empty when empty == %{} -> nil + data -> data + end + end + + defp equipment_data(%Aprs.Packet{} = packet) do + equipment_data = + %{} + |> maybe_add("manufacturer", packet.manufacturer) + |> maybe_add("equipment_type", packet.equipment_type) + + case equipment_data do + empty when empty == %{} -> nil + data -> data + end + end + + defp message_data(%Aprs.Packet{addressee: nil, message_text: nil, message_number: nil}), do: nil + + defp message_data(%Aprs.Packet{} = packet) do + %{} + |> maybe_add("addressee", packet.addressee) + |> maybe_add("text", packet.message_text) + |> maybe_add("number", packet.message_number) + end + + defp maybe_add(map, _key, nil), do: map + defp maybe_add(map, _key, ""), do: map + defp maybe_add(map, key, value), do: Map.put(map, key, value) + + defp to_float(%Decimal{} = decimal), do: Decimal.to_float(decimal) + defp to_float(value) when is_number(value), do: value + defp to_float(_), do: nil + @impl true def render(assigns) do ~H""" @@ -434,6 +629,192 @@ defmodule AprsWeb.ApiDocsLive do + +
+
+

Test the API

+
+ +
+

+ Try the API directly from this page. Enter a callsign to see the most recent packet data. +

+ +
+
+
+ +
+ + +
+
+
+ + + <%= if @error do %> +
+
+
+ + + +
+
+

{@error}

+
+
+
+ <% end %> + + + <%= if @api_result do %> +
+

API Response

+ + <%= if @api_result["data"] do %> + +
+
+ + + + Packet Found! +
+ +
+
+
+ Callsign: + {@api_result["data"]["callsign"]} +
+
+ Received: + + {format_datetime(@api_result["data"]["received_at"])} + +
+ <%= if @api_result["data"]["position"] do %> +
+ Position: + + {Float.round(@api_result["data"]["position"]["latitude"], 4)}, {Float.round( + @api_result["data"]["position"]["longitude"], + 4 + )} + +
+ <% end %> + <%= if @api_result["data"]["comment"] do %> +
+ Comment: + {@api_result["data"]["comment"]} +
+ <% end %> +
+
+ + +
+ + View Raw JSON Response + +
+
<%= Jason.encode!(@api_result, pretty: true) %>
+
+
+
+ <% else %> + +
+
+ + + + No Packets Found +
+

{@api_result["message"]}

+ + +
+ + View Raw JSON Response + +
+
<%= Jason.encode!(@api_result, pretty: true) %>
+
+
+
+ <% end %> +
+ <% end %> +
+
+
+
@@ -463,4 +844,20 @@ defmodule AprsWeb.ApiDocsLive do
""" end + + defp format_datetime(nil), do: "N/A" + + defp format_datetime(datetime_string) do + case DateTime.from_iso8601(datetime_string) do + {:ok, datetime, _} -> + datetime + |> DateTime.shift_zone!("America/New_York") + |> Calendar.strftime("%Y-%m-%d %H:%M:%S %Z") + + _ -> + datetime_string + end + rescue + _ -> datetime_string + end end