Fix API documentation to match implementation

- REST API: Fix id type from integer to UUID, add device/equipment
  fields, correct rate limiting info (100 req/min, not "none"),
  add 429 status code
- Mobile API: Fix search_callsign response field (lon not lng),
  add path to packet fields, correct rate limiting/timeout docs,
  clarify update_bounds behavior
- Fix flaky pipeline tests (async race on global Application config)
This commit is contained in:
Graham McIntire 2026-02-19 10:22:00 -06:00
parent 33b4a35900
commit f6730b282a
No known key found for this signature in database
5 changed files with 49 additions and 24 deletions

View file

@ -111,7 +111,7 @@ Subscribe to receive packets within specific geographic bounds. Upon subscriptio
### Updating Bounds
Update the geographic bounds (e.g., when user pans/zooms the map).
Update the geographic bounds (e.g., when user pans/zooms the map). This updates the real-time streaming filter only; it does not re-send historical packets for the new bounds.
**Event:** `update_bounds`
@ -174,19 +174,21 @@ Once subscribed, you'll receive packets within your bounds. Historical packets a
| Field | Type | Description | Required |
|-------|------|-------------|----------|
| `id` | string | Unique packet identifier | Yes |
| `callsign` | string | Station callsign | Yes |
| `id` | string | Unique packet identifier (UUID) | Yes |
| `callsign` | string | Station callsign (sender). For APRS objects/items, this is still the sender callsign, not the object name. | Yes |
| `lat` | float | Latitude (-90 to 90) | Yes |
| `lng` | float | Longitude (-180 to 180) | Yes |
| `timestamp` | string | ISO 8601 timestamp | Yes |
| `symbol_table_id` | string | APRS symbol table (/, \\) | Yes |
| `symbol_code` | string | APRS symbol code | Yes |
| `comment` | string | Station comment/status | Optional |
| `altitude` | float | Altitude in meters | Optional |
| `altitude` | float | Altitude in feet | Optional |
| `speed` | float | Speed in knots | Optional |
| `course` | integer | Course in degrees (0-359) | Optional |
| `path` | string | APRS digipeater path | Optional |
**Note:** Optional fields are omitted from the payload entirely when their value is `nil`.
### Unsubscribing
Stop receiving packets.
@ -245,7 +247,7 @@ Search for callsigns matching a pattern.
"base_callsign": "W5ISP",
"last_seen": "2025-10-25T17:30:00Z",
"lat": 33.1225,
"lng": -96.124
"lon": -96.124
}
],
"count": 1
@ -461,11 +463,9 @@ struct APRSMapView: View {
}
```
## Rate Limiting
## Connection Timeout
The API is rate-limited to prevent abuse:
- 200 requests per minute per IP address
- Connections are automatically closed if idle for >60 seconds
WebSocket connections have a 60-second heartbeat timeout. If the server does not receive a heartbeat within 60 seconds, the connection is closed. Phoenix Channels clients send heartbeats automatically (every 30 seconds by default).
## Best Practices

View file

@ -44,7 +44,8 @@ defmodule AprsmeWeb.MobileChannel do
comment: string (optional),
altitude: float (optional),
speed: float (optional),
course: integer (optional)
course: integer (optional),
path: string (optional)
}
- "subscription_confirmed" - Bounds subscription successful

View file

@ -177,10 +177,22 @@ defmodule AprsmeWeb.ApiDocsLive do
end
defp format_equipment(packet) do
device =
case packet.device_identifier do
nil -> nil
"" -> nil
identifier -> Aprsme.DeviceCache.lookup_device(identifier)
end
equipment =
%{}
|> maybe_add("device_identifier", packet.device_identifier)
|> maybe_add("manufacturer", packet.manufacturer)
|> maybe_add("equipment_type", packet.equipment_type)
|> maybe_add("device_model", device && device.model)
|> maybe_add("device_vendor", device && device.vendor)
|> maybe_add("device_contact", device && device.contact)
|> maybe_add("device_class", device && device.class)
if map_size(equipment) == 0, do: nil, else: equipment
end
@ -266,7 +278,7 @@ defmodule AprsmeWeb.ApiDocsLive do
<div class="ml-3">
<h3 class="text-sm font-bold text-yellow-700 dark:text-yellow-400">Rate Limiting</h3>
<div class="text-sm text-yellow-700 dark:text-yellow-400">
Currently no rate limiting is enforced, but please be respectful and avoid excessive requests. Rate limiting may be implemented in the future.
API requests are rate limited to 100 requests per minute per IP address. Exceeding this limit will return a 429 Too Many Requests response.
</div>
</div>
</div>
@ -344,7 +356,7 @@ defmodule AprsmeWeb.ApiDocsLive do
<div class="bg-gray-900 text-gray-100 dark:bg-gray-950 p-4 rounded-lg overflow-x-auto mb-4">
<pre><code><%= raw ~s|{
"data": {
"id": 12345,
"id": "d7249877-d4a6-45c2-b314-2a8a355d2566",
"callsign": "N0CALL-9",
"base_callsign": "N0CALL",
"ssid": "9",
@ -354,7 +366,7 @@ defmodule AprsmeWeb.ApiDocsLive do
"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",
"received_at": "2024-01-15T10:30:45Z",
"region": "US-West",
"position": {
"latitude": 47.666667,
@ -372,13 +384,17 @@ defmodule AprsmeWeb.ApiDocsLive do
"aprs_messaging": false,
"weather": null,
"equipment": {
"device_identifier": "APK004",
"manufacturer": "Kenwood",
"equipment_type": "TM-D710"
"equipment_type": "TM-D710",
"device_model": "TM-D710G",
"device_vendor": "Kenwood",
"device_class": "rig"
},
"message": null,
"has_position": true,
"inserted_at": "2024-01-15T10:30:45.123456Z",
"updated_at": "2024-01-15T10:30:45.123456Z"
"inserted_at": "2024-01-15T10:30:45Z",
"updated_at": "2024-01-15T10:30:45Z"
}
}| %></code></pre>
</div>
@ -420,7 +436,7 @@ defmodule AprsmeWeb.ApiDocsLive do
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<tr>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">id</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">integer</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">string (UUID)</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">Unique packet identifier</td>
</tr>
<tr>
@ -480,7 +496,7 @@ defmodule AprsmeWeb.ApiDocsLive do
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">object</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
Equipment information (manufacturer, type)
Equipment information (device_identifier, manufacturer, equipment_type, device_model, device_vendor, device_class, device_contact)
</td>
</tr>
<tr>
@ -544,6 +560,14 @@ defmodule AprsmeWeb.ApiDocsLive do
</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">Request took too long to process</td>
</tr>
<tr>
<td class="whitespace-nowrap px-3 py-4 text-sm font-mono text-orange-600 dark:text-orange-400">
429 Too Many Requests
</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
Rate limit exceeded (100 requests per minute per IP)
</td>
</tr>
<tr>
<td class="whitespace-nowrap px-3 py-4 text-sm font-mono text-red-600 dark:text-red-400">
500 Internal Server Error

View file

@ -1,5 +1,5 @@
defmodule Aprsme.PacketConsumerPoolTest do
use ExUnit.Case, async: true
use ExUnit.Case, async: false
alias Aprsme.PacketConsumerPool
@ -13,15 +13,15 @@ defmodule Aprsme.PacketConsumerPoolTest do
end
describe "init/1" do
test "returns supervisor flags with one_for_one strategy and 3 children by default" do
{:ok, {sup_flags, children}} = PacketConsumerPool.init([])
test "returns supervisor flags with one_for_one strategy and 3 children" do
{:ok, {sup_flags, children}} = PacketConsumerPool.init(num_consumers: 3)
assert sup_flags.strategy == :one_for_one
assert length(children) == 3
end
test "each child has a unique id of {Aprsme.PacketConsumer, index}" do
{:ok, {_sup_flags, children}} = PacketConsumerPool.init([])
{:ok, {_sup_flags, children}} = PacketConsumerPool.init(num_consumers: 3)
ids = Enum.map(children, & &1.id)
@ -72,7 +72,7 @@ defmodule Aprsme.PacketConsumerPoolTest do
Application.delete_env(:aprsme, :packet_pipeline)
{:ok, {_sup_flags, children}} = PacketConsumerPool.init([])
{:ok, {_sup_flags, children}} = PacketConsumerPool.init(num_consumers: 3)
expected_demand = div(250, 3)

View file

@ -1,5 +1,5 @@
defmodule Aprsme.PacketPipelineSupervisorTest do
use ExUnit.Case, async: true
use ExUnit.Case, async: false
alias Aprsme.PacketPipelineSupervisor