From d1dfa6df90aa68f700251bcacb9bcd144531d6f7 Mon Sep 17 00:00:00 2001 From: Mike Wichers Date: Fri, 4 Sep 2026 20:26:45 -0400 Subject: [PATCH] API.md ships in the image; GET /api/docs/approved serves it behind api:docs Moved to app/API.md so the build context carries it. The console renders it at /leaders/api. tests/smoke_admin.py 162 -> 164. --- API.md => app/API.md | 0 app/admin_api.py | 16 +++++++++++++++- tests/http_drive.py | 4 ++-- tests/smoke_admin.py | 5 +++++ 4 files changed, 22 insertions(+), 3 deletions(-) rename API.md => app/API.md (100%) diff --git a/API.md b/app/API.md similarity index 100% rename from API.md rename to app/API.md diff --git a/app/admin_api.py b/app/admin_api.py index 6378e41..efa4bb3 100644 --- a/app/admin_api.py +++ b/app/admin_api.py @@ -45,7 +45,7 @@ import os import re from fastapi import APIRouter, Body, Header, HTTPException, Query, Request -from fastapi.responses import HTMLResponse +from fastapi.responses import HTMLResponse, PlainTextResponse import auth import calendar_write @@ -511,6 +511,20 @@ def whoami(request: Request, x_admin_token: str = Header(None)): _CAP_RE = re.compile(r"""_(?:auth|people_actor)\([^)]*?["']([a-z_]+:[a-z_]+)["']""") +@docs_router.get("/docs/approved", response_class=PlainTextResponse) +def api_docs_approved(request: Request): + """API.md: the approved reference, generated from this registry plus a + full HTTP drive of every route (tests/http_drive.py). Markdown; the + console renders it at /leaders/api. Same gate as /api/docs.""" + _auth(request, None, "api:docs") + import pathlib + p = pathlib.Path(__file__).with_name("API.md") + if not p.exists(): + raise HTTPException(404, "API.md is not in this build") + return p.read_text() + + + def route_capability(endpoint): try: src = inspect.getsource(endpoint) diff --git a/tests/http_drive.py b/tests/http_drive.py index b705a8f..ec3c1b1 100644 --- a/tests/http_drive.py +++ b/tests/http_drive.py @@ -1,9 +1,9 @@ """Drive every admin API route over real HTTP. NOT a unit test: it needs a running throwaway site on a COPY of the database (container name sw-test on arrstack_arr_net, reachable at the URL in /tmp/apitest/base) and sessions for four accounts in -/tmp/apitest/setup.json - see projects/scout-website/state.md for the recipe. +/tmp/apitest/setup.json - see projects/scout-website/state.md for the recipe. Output goes to app/API.md so it ships in the image and the console can render it. Calendar checks write 2036 probes to the REAL Radicale store and delete them. -Writes results to /tmp/apitest/results.json; API.md is generated from that plus the +Writes results to /tmp/apitest/results.json; app/API.md is generated from that plus the router registry. 123 checks on 2026-09-04, all passing.""" import json, urllib.request, urllib.error, urllib.parse, hashlib, struct, zlib, base64, datetime, sys diff --git a/tests/smoke_admin.py b/tests/smoke_admin.py index 329f17f..51855b9 100644 --- a/tests/smoke_admin.py +++ b/tests/smoke_admin.py @@ -420,6 +420,11 @@ check("fbposts caps: read for leaders, ingest admin-only and scopable", "fbposts con = S.connect(); con.execute("DELETE FROM fb_posts"); con.commit(); con.close() import os as _os; _os.remove(S.image_path(r)) +print("\napproved doc") +import pathlib as _pl +check("API.md ships beside the app", (_pl.Path(A.__file__).with_name("API.md")).exists() and "## How to call it" in _pl.Path(A.__file__).with_name("API.md").read_text()) +check("/api/docs/approved is gated like /api/docs", any(r.path == "/api/docs/approved" for r in A.docs_router.routes)) + print("\napi docs registry") import admin_api as A reg = A.describe_routes()