Implements the subset of the isitagentready.com checklist that maps to
real capabilities of this site:
- RFC 8288 Link headers on every browser response advertising
service-doc (/algo), about, privacy-policy, and sitemap.
- Markdown-for-Agents content negotiation: requests with
Accept: text/markdown for / or /algo return real markdown
(curated summary + verbatim algo.md) with x-markdown-tokens hint.
Other paths still 406 — no synthesizing markdown from LiveView HTML.
- Content-Signal directive in robots.txt declaring
search=yes, ai-train=no, ai-input=no.
- RFC 9727 API catalog at /.well-known/api-catalog with the sole
public endpoint (/api/contacts/map) linking status -> /health and
service-desc -> /openapi.json (a new minimal OpenAPI 3.0 spec).
- Agent Skills Discovery index at /.well-known/agent-skills/index.json
listing two skill documents (fetch-contacts, read-algorithm) with
sha256 digests computed at compile time; documents served at
/.well-known/agent-skills/{name}/SKILL.md.
Skipped (don't match actual site capabilities): Web Bot Auth JWKS,
OAuth/OIDC discovery, MCP server card.
83 lines
2.6 KiB
Elixir
83 lines
2.6 KiB
Elixir
defmodule MicrowavepropWeb.ApiCatalogController do
|
|
@moduledoc """
|
|
RFC 9727 API catalog + companion OpenAPI spec for automated API discovery.
|
|
|
|
`/api/contacts/map` is currently the only public JSON endpoint. The
|
|
catalog lists it with link relations for `service-desc` (OpenAPI) and
|
|
`status` (health check). If more endpoints are added, extend
|
|
`linkset_entries/1` and the OpenAPI `paths` map.
|
|
"""
|
|
use MicrowavepropWeb, :controller
|
|
|
|
@openapi_type "application/vnd.oai.openapi+json;version=3.0"
|
|
|
|
@spec catalog(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
|
def catalog(conn, _params) do
|
|
body = Jason.encode!(%{linkset: linkset_entries(conn)})
|
|
|
|
conn
|
|
|> put_resp_content_type("application/linkset+json")
|
|
|> put_resp_header("cache-control", "public, max-age=3600")
|
|
|> send_resp(200, body)
|
|
end
|
|
|
|
@spec openapi(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
|
def openapi(conn, _params) do
|
|
body = Jason.encode!(openapi_spec(conn))
|
|
|
|
conn
|
|
|> put_resp_content_type("application/json")
|
|
|> put_resp_header("cache-control", "public, max-age=3600")
|
|
|> send_resp(200, body)
|
|
end
|
|
|
|
defp linkset_entries(conn) do
|
|
[
|
|
%{
|
|
anchor: url(conn, ~p"/api/contacts/map"),
|
|
"service-desc": [%{href: url(conn, ~p"/openapi.json"), type: @openapi_type}],
|
|
status: [%{href: url(conn, ~p"/health")}]
|
|
}
|
|
]
|
|
end
|
|
|
|
defp openapi_spec(conn) do
|
|
%{
|
|
openapi: "3.0.3",
|
|
info: %{
|
|
title: "Microwaveprop Public API",
|
|
description: "Public JSON endpoints for the NTMS microwave propagation prediction service.",
|
|
version: "1.0.0"
|
|
},
|
|
servers: [%{url: url(conn, ~p"/")}],
|
|
paths: %{
|
|
"/api/contacts/map" => %{
|
|
get: %{
|
|
summary: "All recorded amateur-radio contacts as compact tuples",
|
|
description:
|
|
"Returns every contact in the calibration dataset as an array of fixed-shape tuples. " <>
|
|
"Response is pre-gzipped when the client sends Accept-Encoding: gzip. " <>
|
|
"Tuple shape: [lat1, lon1, lat2, lon2, band_mhz, callsign1, callsign2, mode, distance_km, qso_timestamp, id].",
|
|
responses: %{
|
|
"200" => %{
|
|
description: "Array of contact tuples",
|
|
content: %{
|
|
"application/json" => %{
|
|
schema: %{
|
|
type: "array",
|
|
items: %{
|
|
type: "array",
|
|
minItems: 11,
|
|
maxItems: 11
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
end
|
|
end
|