Show the API reference with Redoc

Replace the hand-built reference page with Redoc rendering the generated
OpenAPI document. The bundle is fetched by package.nix (pinned hash) and
served from the UI's own address; only that page gets a style nonce and
worker-src blob: in its CSP.

The spec now explains authentication, saved-not-applied, ETags (If-Match,
409) and errors, documents the ETag header, gives each operation a curl
sample, and no longer names MCP tools. Area GETs no longer emit
"parameters": null, which Redoc could not render.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
2026-11-01 22:01:30 +01:00
co-authored by Claude Sonnet 5.5
parent 64e08fb485
commit 876080d3f5
14 changed files with 245 additions and 201 deletions
+32 -6
View File
@@ -5,6 +5,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"regexp"
"slices"
"strings"
"testing"
@@ -247,12 +248,37 @@ func TestAPIReference(t *testing.T) {
if rec.Code != 200 {
t.Fatalf("reference: %d", rec.Code)
}
for _, rt := range apiRoutes {
if !strings.Contains(body, rt.path) {
t.Errorf("%s is not on the page", rt.path)
}
csp := rec.Header().Get("Content-Security-Policy")
m := regexp.MustCompile(`data-nonce="([^"]+)"`).FindStringSubmatch(body)
if m == nil || !strings.Contains(csp, "'nonce-"+m[1]+"'") {
t.Errorf("the page's nonce is not in its CSP: %q", csp)
}
if !strings.Contains(body, "curl -sk") || !strings.Contains(body, "If-Match") || !strings.Contains(body, "unit=router-pppoe") {
t.Errorf("examples missing")
if strings.Contains(csp, "unsafe-inline") || strings.Contains(csp, "unsafe-eval") {
t.Errorf("CSP too loose: %q", csp)
}
if !strings.Contains(body, `data-spec="/api/openapi.json"`) || !strings.Contains(body, "/static/redoc.standalone.js") {
t.Errorf("the page does not load Redoc with the spec:\n%s", body)
}
}
func TestSpecExplainsETags(t *testing.T) {
spec := openAPISpec("1.2.3", nil, "router.example")
info := spec["info"].(map[string]any)["description"].(string)
for _, want := range []string{"ETag", "If-Match", "409"} {
if !strings.Contains(info, want) {
t.Errorf("%q is not explained in the spec's description", want)
}
}
patch := spec["paths"].(map[string]any)["/api/v1/settings"].(map[string]any)["patch"].(map[string]any)
if strings.Contains(patch["description"].(string), "update_settings") || strings.Contains(patch["description"].(string), "check_settings") {
t.Errorf("REST description names an MCP tool: %v", patch["description"])
}
src := patch["x-codeSamples"].([]any)[0].(map[string]any)["source"].(string)
if !strings.Contains(src, "https://router.example/api/v1/settings") || !strings.Contains(src, "If-Match") {
t.Errorf("curl sample: %s", src)
}
get := spec["paths"].(map[string]any)["/api/v1/settings"].(map[string]any)["get"].(map[string]any)
if _, ok := get["responses"].(map[string]any)["200"].(map[string]any)["headers"].(map[string]any)["ETag"]; !ok {
t.Errorf("GET settings does not document its ETag header")
}
}