Serve dashboard at /, version the REST API under /api/v1

The SvelteKit build was always made for the root (no base path); it
now mounts at / — registered last so /api/v1, /ws, /health, and /mcp
match first — and the JSON root endpoint is gone (its info lives in
/health and gateway_status). REST routers move from /api/* to
/api/v1/*; /ws and /health stay put; /mcp/ unchanged. Dashboard API
client, tests, README, and docs updated; dashboard rebuilt (build/ is
gitignored).

Verified live: / serves the UI, /api/v1 answers 200/401, the old
/api paths 404, MCP still lists 15 tools.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 14:57:45 -04:00
parent a8f06462e6
commit dff21f7d5c
9 changed files with 96 additions and 114 deletions

View File

@@ -30,31 +30,31 @@ async def client():
class TestBearerToken:
async def test_missing_token_rejected(self, token_enabled, client):
resp = await client.get("/api/calls/active")
resp = await client.get("/api/v1/calls/active")
assert resp.status_code == 401
assert resp.headers["www-authenticate"] == "Bearer"
async def test_wrong_token_rejected(self, token_enabled, client):
resp = await client.get(
"/api/calls/active", headers={"Authorization": "Bearer wrong"}
"/api/v1/calls/active", headers={"Authorization": "Bearer wrong"}
)
assert resp.status_code == 401
async def test_valid_token_reaches_handler(self, token_enabled, client):
resp = await client.get(
"/api/calls/active", headers={"Authorization": f"Bearer {TOKEN}"}
"/api/v1/calls/active", headers={"Authorization": f"Bearer {TOKEN}"}
)
# No lifespan ran, so the handler itself 503s — auth was accepted
assert resp.status_code == 503
async def test_empty_token_disables_auth(self, monkeypatch, client):
monkeypatch.setattr(get_settings(), "api_token", SecretStr(""))
resp = await client.get("/api/calls/active")
resp = await client.get("/api/v1/calls/active")
assert resp.status_code == 503
async def test_all_api_routers_protected(self, token_enabled, client):
for path in ("/api/calls/active", "/api/call-flows/", "/api/devices/",
"/api/routing/rules", "/api/calls/history"):
for path in ("/api/v1/calls/active", "/api/v1/call-flows/", "/api/v1/devices/",
"/api/v1/routing/rules", "/api/v1/calls/history"):
resp = await client.get(path)
assert resp.status_code == 401, path
@@ -76,12 +76,12 @@ class TestRouteOrder:
return None
def test_history_not_shadowed_by_call_id(self):
route = self._resolve("/api/calls/history")
route = self._resolve("/api/v1/calls/history")
assert route is not None
assert route.endpoint.__name__ == "list_history"
def test_call_id_still_matches(self):
route = self._resolve("/api/calls/call_abc123")
route = self._resolve("/api/v1/calls/call_abc123")
assert route is not None
assert route.endpoint.__name__ == "get_call"

View File

@@ -163,38 +163,38 @@ FLOW_PAYLOAD = {
class TestCallFlowRoutes:
async def test_crud_round_trip(self, client):
resp = await client.post("/api/call-flows/", json=FLOW_PAYLOAD)
resp = await client.post("/api/v1/call-flows/", json=FLOW_PAYLOAD)
assert resp.status_code == 200, resp.text
flow_id = resp.json()["id"]
assert flow_id == "acme-main-line"
resp = await client.post("/api/call-flows/", json=FLOW_PAYLOAD)
resp = await client.post("/api/v1/call-flows/", json=FLOW_PAYLOAD)
assert resp.status_code == 409
resp = await client.get("/api/call-flows/")
resp = await client.get("/api/v1/call-flows/")
assert [f["id"] for f in resp.json()] == [flow_id]
resp = await client.get(f"/api/call-flows/{flow_id}")
resp = await client.get(f"/api/v1/call-flows/{flow_id}")
assert resp.json()["steps"][0]["action_value"] == "2"
resp = await client.get("/api/call-flows/by-number/+18005551234")
resp = await client.get("/api/v1/call-flows/by-number/+18005551234")
assert resp.json()["id"] == flow_id
resp = await client.put(
f"/api/call-flows/{flow_id}", json={"notes": "updated"}
f"/api/v1/call-flows/{flow_id}", json={"notes": "updated"}
)
assert resp.json()["notes"] == "updated"
resp = await client.delete(f"/api/call-flows/{flow_id}")
resp = await client.delete(f"/api/v1/call-flows/{flow_id}")
assert resp.json()["status"] == "deleted"
resp = await client.get(f"/api/call-flows/{flow_id}")
resp = await client.get(f"/api/v1/call-flows/{flow_id}")
assert resp.status_code == 404
class TestCallHistoryRoutes:
async def test_history_and_record(self, client):
resp = await client.get("/api/calls/history")
resp = await client.get("/api/v1/calls/history")
assert resp.status_code == 200
assert resp.json() == []
@@ -211,17 +211,17 @@ class TestCallHistoryRoutes:
))
await session.commit()
resp = await client.get("/api/calls/history")
resp = await client.get("/api/v1/calls/history")
assert [r["id"] for r in resp.json()] == ["call_hist1"]
resp = await client.get("/api/calls/history?number=%2B18005551234")
resp = await client.get("/api/v1/calls/history?number=%2B18005551234")
assert len(resp.json()) == 1
resp = await client.get("/api/calls/call_hist1/record")
resp = await client.get("/api/v1/calls/call_hist1/record")
assert resp.json()["intent"] == "dispute charge"
resp = await client.get("/api/calls/call_missing/record")
resp = await client.get("/api/v1/calls/call_missing/record")
assert resp.status_code == 404
resp = await client.get("/api/calls/call_hist1/transcript")
resp = await client.get("/api/v1/calls/call_hist1/transcript")
assert resp.json() == []