This commit is contained in:
Graham McIntire 2025-06-22 16:02:59 -05:00
parent 77276eb2a1
commit 16209269e7
No known key found for this signature in database
8 changed files with 896 additions and 9 deletions

View file

@ -411,6 +411,9 @@ defmodule AprsWeb.CoreComponents do
</div>
<div class="flex items-center space-x-6">
<a href="/" class="text-gray-700 hover:text-blue-700 font-medium transition-colors">Home</a>
<a href="/api" class="text-gray-700 hover:text-blue-700 font-medium transition-colors">
API
</a>
<a href="/about" class="text-gray-700 hover:text-blue-700 font-medium transition-colors">
About
</a>

View file

@ -0,0 +1,100 @@
defmodule AprsWeb.Api.V1.CallsignController do
@moduledoc """
API v1 controller for callsign-related endpoints.
"""
use AprsWeb, :controller
alias Aprs.Packets
alias AprsWeb.Api.V1.CallsignJSON
action_fallback AprsWeb.Api.V1.FallbackController
@doc """
Get the most recent packet for a given callsign.
## Parameters
* `callsign` - The callsign to search for, with optional SSID (e.g., "N0CALL" or "N0CALL-9")
## Returns
* 200 - Success with packet data
* 404 - No packets found for the callsign
* 400 - Invalid callsign format
"""
def show(conn, %{"callsign" => callsign}) do
with {:ok, normalized_callsign} <- validate_callsign(callsign),
{:ok, packet} <- get_latest_packet(normalized_callsign) do
conn
|> put_status(:ok)
|> put_view(json: CallsignJSON)
|> render(:show, packet: packet)
else
{:error, :invalid_callsign} ->
{:error, "Invalid callsign format"}
{:error, :not_found} ->
conn
|> put_status(:ok)
|> put_view(json: CallsignJSON)
|> render(:not_found, callsign: callsign)
{:error, reason} ->
{:error, reason}
end
end
defp validate_callsign(callsign) when is_binary(callsign) do
# Remove any whitespace and convert to uppercase
normalized = callsign |> String.trim() |> String.upcase()
if validate_callsign_format(normalized) do
{:ok, normalized}
else
{:error, :invalid_callsign}
end
end
defp validate_callsign(nil), do: {:error, :invalid_callsign}
defp validate_callsign(_), do: {:error, :invalid_callsign}
defp validate_callsign_format(callsign) do
# Basic callsign validation
# Matches patterns like: N0CALL, N0CALL-9, W1ABC-15, etc.
# This is a simplified regex - a full implementation might be more comprehensive
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 get_latest_packet(callsign) do
# Try to get the most recent packet for this callsign
# We'll limit to packets from the last 30 days to keep queries efficient
thirty_days_ago = DateTime.add(DateTime.utc_now(), -30, :day)
# Use get_recent_packets which orders by desc received_at
opts = %{
callsign: callsign,
start_time: thirty_days_ago,
limit: 1
}
case Packets.get_recent_packets(opts) do
[] ->
{:error, :not_found}
[packet | _] ->
{:ok, packet}
{:error, reason} ->
{:error, reason}
end
rescue
Ecto.QueryError ->
{:error, :database_error}
error ->
require Logger
Logger.error("Error fetching packet for callsign #{callsign}: #{inspect(error)}")
{:error, :internal_error}
end
end

View file

@ -0,0 +1,72 @@
defmodule AprsWeb.Api.V1.FallbackController do
@moduledoc """
Translates controller action results into valid `Plug.Conn` responses.
See `Phoenix.Controller.action_fallback/1` for more details.
"""
use AprsWeb, :controller
alias AprsWeb.Api.V1.ErrorJSON
# This clause handles errors returned by Ecto's insert/update/delete.
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
conn
|> put_status(:unprocessable_entity)
|> put_view(json: AprsWeb.Api.V1.ChangesetJSON)
|> render(:error, changeset: changeset)
end
# This clause is an example of how to handle resources that cannot be found.
def call(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> put_view(json: ErrorJSON)
|> render(:"404")
end
# Handle generic errors
def call(conn, {:error, reason}) when is_atom(reason) do
status =
case reason do
:unauthorized -> :unauthorized
:forbidden -> :forbidden
:bad_request -> :bad_request
_ -> :internal_server_error
end
conn
|> put_status(status)
|> put_view(json: ErrorJSON)
|> render(:error, message: format_error_message(reason))
end
# Handle string error messages
def call(conn, {:error, message}) when is_binary(message) do
conn
|> put_status(:bad_request)
|> put_view(json: ErrorJSON)
|> render(:error, message: message)
end
# Handle timeout errors
def call(conn, :timeout) do
conn
|> put_status(:request_timeout)
|> put_view(json: ErrorJSON)
|> render(:error, message: "Request timeout")
end
# Default fallback for unexpected errors
def call(conn, _error) do
conn
|> put_status(:internal_server_error)
|> put_view(json: ErrorJSON)
|> render(:"500")
end
defp format_error_message(:not_found), do: "Resource not found"
defp format_error_message(:unauthorized), do: "Unauthorized access"
defp format_error_message(:forbidden), do: "Access forbidden"
defp format_error_message(:bad_request), do: "Bad request"
defp format_error_message(reason), do: "An error occurred: #{reason}"
end

View file

@ -0,0 +1,127 @@
defmodule AprsWeb.Api.V1.CallsignJSON do
@moduledoc """
Renders callsign and packet data for API v1.
"""
alias Aprs.Packet
def render("show.json", %{packet: packet}) do
%{data: packet_json(packet)}
end
def render("index.json", %{packets: packets}) do
%{data: Enum.map(packets, &packet_json/1)}
end
def render("not_found.json", %{callsign: callsign}) do
%{
data: nil,
message: "No packets found for callsign #{callsign}"
}
end
defp packet_json(%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_json(packet),
symbol: symbol_json(packet),
comment: packet.comment,
timestamp: packet.timestamp,
aprs_messaging: packet.aprs_messaging,
weather: weather_json(packet),
equipment: equipment_json(packet),
message: message_json(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_json(%Packet{has_position: false}), do: nil
defp position_json(%Packet{lat: nil, lon: nil}), do: nil
defp position_json(%Packet{} = packet) do
base_position = %{
latitude: to_float(packet.lat),
longitude: to_float(packet.lon)
}
# Add course, speed, and altitude if available
base_position
|> maybe_add(:course, packet.course)
|> maybe_add(:speed, packet.speed)
|> maybe_add(:altitude, packet.altitude)
end
defp symbol_json(%Packet{symbol_code: nil, symbol_table_id: nil}), do: nil
defp symbol_json(%Packet{} = packet) do
%{
code: packet.symbol_code,
table_id: packet.symbol_table_id
}
end
defp weather_json(%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_json(%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_json(%Packet{addressee: nil, message_text: nil, message_number: nil}), do: nil
defp message_json(%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
end

View file

@ -0,0 +1,30 @@
defmodule AprsWeb.Api.V1.ChangesetJSON do
@moduledoc """
Renders changeset errors for API v1.
"""
@doc """
Renders changeset errors.
"""
def render("error.json", %{changeset: changeset}) do
%{
error: %{
message: "Validation failed",
code: "validation_error",
details: translate_errors(changeset)
}
}
end
defp translate_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
end
defp translate_error({msg, opts}) do
# Because the error messages we show in our forms and APIs
# are defined inside Ecto, we need to translate them dynamically.
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end
end

View file

@ -0,0 +1,86 @@
defmodule AprsWeb.Api.V1.ErrorJSON do
@moduledoc """
Renders error responses for API v1.
"""
def render("error.json", %{message: message}) do
%{
error: %{
message: message,
code: "generic_error"
}
}
end
def render("404.json", _assigns) do
%{
error: %{
message: "Resource not found",
code: "not_found"
}
}
end
def render("500.json", _assigns) do
%{
error: %{
message: "Internal server error",
code: "internal_server_error"
}
}
end
def render("422.json", _assigns) do
%{
error: %{
message: "Unprocessable entity",
code: "unprocessable_entity"
}
}
end
def render("400.json", _assigns) do
%{
error: %{
message: "Bad request",
code: "bad_request"
}
}
end
def render("401.json", _assigns) do
%{
error: %{
message: "Unauthorized",
code: "unauthorized"
}
}
end
def render("403.json", _assigns) do
%{
error: %{
message: "Forbidden",
code: "forbidden"
}
}
end
def render("408.json", _assigns) do
%{
error: %{
message: "Request timeout",
code: "request_timeout"
}
}
end
def render(template, _assigns) do
%{
error: %{
message: Phoenix.Controller.status_message_from_template(template),
code: "unknown_error"
}
}
end
end

View file

@ -0,0 +1,466 @@
defmodule AprsWeb.ApiDocsLive do
@moduledoc """
LiveView for API documentation page.
"""
use AprsWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, page_title: "API Documentation")}
end
@impl true
def render(assigns) do
~H"""
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-8">
<h1 class="text-4xl font-bold text-gray-900 mb-4">APRS.me API Documentation</h1>
<p class="text-lg text-gray-600">
RESTful JSON API for accessing APRS packet data and station information.
</p>
</div>
<!-- API Overview -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 mb-8">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-2xl font-semibold text-gray-900">Overview</h2>
</div>
<div class="px-6 py-4">
<div class="prose max-w-none">
<p class="text-gray-700 mb-4">
The APRS.me API provides programmatic access to Amateur Radio APRS (Automatic Packet Reporting System)
data. All API endpoints return JSON data and follow RESTful conventions.
</p>
<div class="grid md:grid-cols-2 gap-6 mt-6">
<div class="bg-blue-50 p-4 rounded-lg">
<h3 class="font-semibold text-blue-900 mb-2">Base URL</h3>
<code class="text-sm bg-white px-2 py-1 rounded border">
https://aprs.me/api/v1
</code>
</div>
<div class="bg-green-50 p-4 rounded-lg">
<h3 class="font-semibold text-green-900 mb-2">Content Type</h3>
<code class="text-sm bg-white px-2 py-1 rounded border">
application/json
</code>
</div>
</div>
<div class="mt-6 p-4 bg-yellow-50 rounded-lg">
<h3 class="font-semibold text-yellow-900 mb-2">Rate Limiting</h3>
<p class="text-yellow-800">
Currently no rate limiting is enforced, but please be respectful and avoid excessive requests.
Rate limiting may be implemented in the future.
</p>
</div>
</div>
</div>
</div>
<!-- API Endpoints -->
<div class="space-y-8">
<!-- Callsign Endpoint -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold text-gray-900">Get Latest Packet by Callsign</h2>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
GET
</span>
</div>
</div>
<div class="px-6 py-4">
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-900 mb-2">Endpoint</h3>
<div class="bg-gray-50 p-3 rounded-lg">
<code class="text-sm">GET /api/v1/callsign/{"{callsign}"}</code>
</div>
</div>
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-900 mb-2">Description</h3>
<p class="text-gray-700">
Retrieves the most recent APRS packet for the specified callsign. The callsign can include
an SSID (e.g., N0CALL-9) or just the base callsign (e.g., N0CALL).
</p>
</div>
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-900 mb-2">Parameters</h3>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Parameter
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Type
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Required
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Description
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
callsign
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
string
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
Yes
</td>
<td class="px-6 py-4 text-sm text-gray-900">
Amateur radio callsign with optional SSID (e.g., N0CALL or N0CALL-9)
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-900 mb-2">Example Request</h3>
<div class="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto">
<pre><code>curl -X GET "https://aprs.me/api/v1/callsign/N0CALL-9" \
-H "Accept: application/json"</code></pre>
</div>
</div>
<div class="mb-4">
<h3 class="text-lg font-medium text-gray-900 mb-2">Response Format</h3>
<h4 class="text-md font-medium text-gray-800 mb-2">Success Response (200 OK)</h4>
<div class="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto mb-4">
<pre><code><%= raw ~s|{
"data": {
"id": 12345,
"callsign": "N0CALL-9",
"base_callsign": "N0CALL",
"ssid": "9",
"sender": "N0CALL-9",
"destination": "APRS",
"path": "WIDE1-1,WIDE2-1",
"data_type": "position",
"information_field": "!4740.00N/12200.00W>Mobile Station",
"raw_packet": "N0CALL-9>APRS,WIDE1-1,WIDE2-1:!4740.00N/12200.00W>Mobile Station",
"received_at": "2024-01-15T10:30:45.123456Z",
"region": "US-West",
"position": {
"latitude": 47.666667,
"longitude": -122.0,
"course": 90,
"speed": 35.5,
"altitude": 152.4
},
"symbol": {
"code": ">",
"table_id": "/"
},
"comment": "Mobile Station",
"timestamp": null,
"aprs_messaging": false,
"weather": null,
"equipment": {
"manufacturer": "Kenwood",
"equipment_type": "TM-D710"
},
"message": null,
"has_position": true,
"inserted_at": "2024-01-15T10:30:45.123456Z",
"updated_at": "2024-01-15T10:30:45.123456Z"
}
}| %></code></pre>
</div>
<h4 class="text-md font-medium text-gray-800 mb-2">Not Found Response (404)</h4>
<div class="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto mb-4">
<pre><code><%= raw ~s|{
"data": null,
"message": "No packets found for callsign N0CALL-9"
}| %></code></pre>
</div>
<h4 class="text-md font-medium text-gray-800 mb-2">Error Response (400)</h4>
<div class="bg-gray-900 text-white p-4 rounded-lg overflow-x-auto">
<pre><code><%= raw ~s|{
"error": {
"message": "Invalid callsign format",
"code": "bad_request"
}
}| %></code></pre>
</div>
</div>
</div>
</div>
<!-- Response Fields Documentation -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-900">Response Fields</h2>
</div>
<div class="px-6 py-4">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Field
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Type
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Description
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">id</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">integer</td>
<td class="px-6 py-4 text-sm text-gray-900">Unique packet identifier</td>
</tr>
<tr class="bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
callsign
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">string</td>
<td class="px-6 py-4 text-sm text-gray-900">
Full callsign with SSID (e.g., "N0CALL-9")
</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
base_callsign
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">string</td>
<td class="px-6 py-4 text-sm text-gray-900">Base callsign without SSID</td>
</tr>
<tr class="bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">ssid</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">string</td>
<td class="px-6 py-4 text-sm text-gray-900">
SSID (Secondary Station Identifier), null if not present
</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
received_at
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">datetime</td>
<td class="px-6 py-4 text-sm text-gray-900">
When the packet was received (ISO 8601 format)
</td>
</tr>
<tr class="bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
position
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">object</td>
<td class="px-6 py-4 text-sm text-gray-900">
Position data (latitude, longitude, course, speed, altitude)
</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
symbol
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">object</td>
<td class="px-6 py-4 text-sm text-gray-900">
APRS symbol information (code and table_id)
</td>
</tr>
<tr class="bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
weather
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">object</td>
<td class="px-6 py-4 text-sm text-gray-900">
Weather data if present (temperature, humidity, wind, etc.)
</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
equipment
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">object</td>
<td class="px-6 py-4 text-sm text-gray-900">
Equipment information (manufacturer, type)
</td>
</tr>
<tr class="bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
message
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">object</td>
<td class="px-6 py-4 text-sm text-gray-900">
Message data if the packet is a message
</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900">
raw_packet
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">string</td>
<td class="px-6 py-4 text-sm text-gray-900">
Original raw APRS packet as received
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- HTTP Status Codes -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-900">HTTP Status Codes</h2>
</div>
<div class="px-6 py-4">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status Code
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Description
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-green-600">
200 OK
</td>
<td class="px-6 py-4 text-sm text-gray-900">
Request successful, packet data returned
</td>
</tr>
<tr class="bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-yellow-600">
400 Bad Request
</td>
<td class="px-6 py-4 text-sm text-gray-900">
Invalid callsign format or malformed request
</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-red-600">
404 Not Found
</td>
<td class="px-6 py-4 text-sm text-gray-900">
No packets found for the specified callsign
</td>
</tr>
<tr class="bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-red-600">
408 Request Timeout
</td>
<td class="px-6 py-4 text-sm text-gray-900">Request took too long to process</td>
</tr>
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-red-600">
500 Internal Server Error
</td>
<td class="px-6 py-4 text-sm text-gray-900">
Server error occurred while processing the request
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Future Endpoints -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-900">Planned Endpoints</h2>
</div>
<div class="px-6 py-4">
<p class="text-gray-700 mb-4">
The following endpoints are planned for future releases:
</p>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div>
<code class="text-sm font-mono">GET /api/v1/callsign/{"{callsign}"}/history</code>
<p class="text-sm text-gray-600 mt-1">Get historical packets for a callsign</p>
</div>
<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">Planned</span>
</div>
<div class="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div>
<code class="text-sm font-mono">GET /api/v1/packets/recent</code>
<p class="text-sm text-gray-600 mt-1">Get recent packets with filtering options</p>
</div>
<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">Planned</span>
</div>
<div class="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div>
<code class="text-sm font-mono">GET /api/v1/packets/area</code>
<p class="text-sm text-gray-600 mt-1">Get packets within a geographic area</p>
</div>
<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">Planned</span>
</div>
<div class="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div>
<code class="text-sm font-mono">GET /api/v1/weather/{"{callsign}"}</code>
<p class="text-sm text-gray-600 mt-1">Get weather data from weather stations</p>
</div>
<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">Planned</span>
</div>
</div>
</div>
</div>
<!-- Contact and Support -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-900">Support</h2>
</div>
<div class="px-6 py-4">
<div class="prose max-w-none">
<p class="text-gray-700 mb-4">
This API is provided free of charge for amateur radio and educational purposes.
If you encounter issues or have suggestions for improvements, please reach out.
</p>
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="font-semibold text-gray-900 mb-2">Guidelines</h3>
<ul class="text-gray-700 space-y-1">
<li> Use reasonable request rates to avoid overwhelming the service</li>
<li> Cache responses when appropriate to reduce server load</li>
<li> Include a User-Agent header identifying your application</li>
<li> This service is for amateur radio and educational use</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
"""
end
end

View file

@ -30,11 +30,6 @@ defmodule AprsWeb.Router do
live_dashboard "/dashboard", metrics: AprsWeb.Telemetry
live_session :map_pages, layout: {AprsWeb.Layouts, :map} do
live "/", MapLive.Index, :index
live "/:callsign", MapLive.CallsignView, :index
end
live_session :regular_pages do
live "/status", StatusLive.Index, :index
live "/packets", PacketsLive.Index, :index
@ -42,13 +37,21 @@ defmodule AprsWeb.Router do
live "/badpackets", BadPacketsLive.Index, :index
live "/weather/:callsign", WeatherLive.CallsignView, :index
live "/about", AboutLive, :index
live "/api", ApiDocsLive, :index
end
live_session :map_pages, layout: {AprsWeb.Layouts, :map} do
live "/", MapLive.Index, :index
live "/:callsign", MapLive.CallsignView, :index
end
end
# Other scopes may use custom stacks.
# scope "/api", AprsWeb do
# pipe_through :api
# end
# API v1 routes
scope "/api/v1", AprsWeb.Api.V1, as: :api_v1 do
pipe_through :api
get "/callsign/:callsign", CallsignController, :show
end
# Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:aprs, :dev_routes) do