/info renders JSON on hard load: move service-discovery JSON under /api, free /info for the SPA #96

Closed
opened 2026-07-07 06:11:12 +00:00 by coilysiren · 2 comments
Owner

https://eco-app.coilysiren.me/info sometimes renders JSON instead of the React page, and API endpoints should live under /api. Filed from a batch of eco-app feedback Kai handed the director surface. Touches src/eco_mcp_app/http_app.py.

Root cause

http_app.py registers Route("/info", service_info, methods=["GET"]) (the route list around line 732). service_info returns a JSONResponse service-discovery blob. The SPA also owns a client-side /info route (App.tsx). A soft in-app navigation renders the React page, but a hard load or refresh of /info hits the Starlette route first and returns JSON. That is the "sometimes json somehow" Kai saw.

Fix

  • Move the service-discovery JSON off /info. It is a debug/discovery endpoint, not a user surface, so relocate it under the API convention, e.g. /api/service (or /service.json). Kai: "I would have assumed all the api endpoints are underneath an api/."
  • Free /info so it falls through to the SPA fallback and renders the React Info page on hard load.
  • While here, audit the other non-/preview JSON routes for the same convention gap (/healthz, /page-auth) and note in the PR which stay put (health/auth probes have external contracts) versus which move. Do not break the /preview*.json data plane the SPA fetches, and do not break /jobs/api or /replay/api mounts.

Done when

A hard load of /info renders the React page, the service-discovery JSON lives under an /api-style path, and the SPA data plane + health/auth probes still resolve. Add or update an http_app route test that hard-loads /info and asserts HTML, not JSON. ward exec test + lint gates green.

`https://eco-app.coilysiren.me/info` sometimes renders JSON instead of the React page, and API endpoints should live under `/api`. Filed from a batch of eco-app feedback Kai handed the director surface. Touches `src/eco_mcp_app/http_app.py`. ## Root cause `http_app.py` registers `Route("/info", service_info, methods=["GET"])` (the route list around line 732). `service_info` returns a JSONResponse service-discovery blob. The SPA also owns a client-side `/info` route (`App.tsx`). A soft in-app navigation renders the React page, but a hard load or refresh of `/info` hits the Starlette route first and returns JSON. That is the "sometimes json somehow" Kai saw. ## Fix - Move the service-discovery JSON off `/info`. It is a debug/discovery endpoint, not a user surface, so relocate it under the API convention, e.g. `/api/service` (or `/service.json`). Kai: "I would have assumed all the api endpoints are underneath an api/." - Free `/info` so it falls through to the SPA fallback and renders the React Info page on hard load. - While here, audit the other non-`/preview` JSON routes for the same convention gap (`/healthz`, `/page-auth`) and note in the PR which stay put (health/auth probes have external contracts) versus which move. Do not break the `/preview*.json` data plane the SPA fetches, and do not break `/jobs/api` or `/replay/api` mounts. ## Done when A hard load of `/info` renders the React page, the service-discovery JSON lives under an `/api`-style path, and the SPA data plane + health/auth probes still resolve. Add or update an `http_app` route test that hard-loads `/info` and asserts HTML, not JSON. `ward exec` test + lint gates green.
Owner

🔒 Reserved by ward agent --driver claude — container engineer-claude-eco-app-96 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-07T06:14:43Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).

run seed context — what this run is carrying (ward#609)
  • Resolved: coilyco-gaming/eco-app#96 · branch issue-96 · driver claude · workflow direct-main
  • Run: engineer-claude-eco-app-96 · ward v0.419.0 · dispatched 2026-07-07T06:14:43Z
  • Comment thread: 0 included in the pre-flight read, 0 stripped (ward's own automated comments).

Issue body as seeded:

`https://eco-app.coilysiren.me/info` sometimes renders JSON instead of the React page, and API endpoints should live under `/api`. Filed from a batch of eco-app feedback Kai handed the director surface. Touches `src/eco_mcp_app/http_app.py`.

## Root cause

`http_app.py` registers `Route("/info", service_info, methods=["GET"])` (the route list around line 732). `service_info` returns a JSONResponse service-discovery blob. The SPA also owns a client-side `/info` route (`App.tsx`). A soft in-app navigation renders the React page, but a hard load or refresh of `/info` hits the Starlette route first and returns JSON. That is the "sometimes json somehow" Kai saw.

## Fix

- Move the service-discovery JSON off `/info`. It is a debug/discovery endpoint, not a user surface, so relocate it under the API convention, e.g. `/api/service` (or `/service.json`). Kai: "I would have assumed all the api endpoints are underneath an api/."
- Free `/info` so it falls through to the SPA fallback and renders the React Info page on hard load.
- While here, audit the other non-`/preview` JSON routes for the same convention gap (`/healthz`, `/page-auth`) and note in the PR which stay put (health/auth probes have external contracts) versus which move. Do not break the `/preview*.json` data plane the SPA fetches, and do not break `/jobs/api` or `/replay/api` mounts.

## Done when

A hard load of `/info` renders the React page, the service-discovery JSON lives under an `/api`-style path, and the SPA data plane + health/auth probes still resolve. Add or update an `http_app` route test that hard-loads `/info` and asserts HTML, not JSON. `ward exec` test + lint gates green.

Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.419.0).

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-eco-app-96` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-07T06:14:43Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. **Do not comment on or edit this issue to steer the run while it is reserved.** The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a **new issue, dispatched fresh** — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494). <details><summary>run seed context — what this run is carrying (ward#609)</summary> - **Resolved:** `coilyco-gaming/eco-app#96` · branch `issue-96` · driver `claude` · workflow `direct-main` - **Run:** `engineer-claude-eco-app-96` · ward `v0.419.0` · dispatched `2026-07-07T06:14:43Z` - **Comment thread:** 0 included in the pre-flight read, 0 stripped (ward's own automated comments). **Issue body as seeded:** ``` `https://eco-app.coilysiren.me/info` sometimes renders JSON instead of the React page, and API endpoints should live under `/api`. Filed from a batch of eco-app feedback Kai handed the director surface. Touches `src/eco_mcp_app/http_app.py`. ## Root cause `http_app.py` registers `Route("/info", service_info, methods=["GET"])` (the route list around line 732). `service_info` returns a JSONResponse service-discovery blob. The SPA also owns a client-side `/info` route (`App.tsx`). A soft in-app navigation renders the React page, but a hard load or refresh of `/info` hits the Starlette route first and returns JSON. That is the "sometimes json somehow" Kai saw. ## Fix - Move the service-discovery JSON off `/info`. It is a debug/discovery endpoint, not a user surface, so relocate it under the API convention, e.g. `/api/service` (or `/service.json`). Kai: "I would have assumed all the api endpoints are underneath an api/." - Free `/info` so it falls through to the SPA fallback and renders the React Info page on hard load. - While here, audit the other non-`/preview` JSON routes for the same convention gap (`/healthz`, `/page-auth`) and note in the PR which stay put (health/auth probes have external contracts) versus which move. Do not break the `/preview*.json` data plane the SPA fetches, and do not break `/jobs/api` or `/replay/api` mounts. ## Done when A hard load of `/info` renders the React page, the service-discovery JSON lives under an `/api`-style path, and the SPA data plane + health/auth probes still resolve. Add or update an `http_app` route test that hard-loads `/info` and asserts HTML, not JSON. `ward exec` test + lint gates green. ``` Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.419.0). </details> <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

WARD-OUTCOME: done - service-discovery JSON now lives at /api/service; /info falls through to the SPA and renders the React Info page on hard load.

Cleaner than I expected. The root cause was exactly as written: a single Starlette Route("/info", ...) shadowing the SPA catch-all, so the fix was a one-line route move plus freeing the path. The only thing that fought back was a second, non-obvious assertion — test_smoke.py::test_info also hard-coded /info returning the JSON blob, so the suite caught the move immediately and I repointed it at /api/service (and renamed it test_service_discovery).

On the audit: /healthz and /page-auth stay on their bare paths — both are probes with external contracts (k8s liveness hits /healthz, the SPA hardcodes /page-auth), not user surfaces that collide with SPA routes, so moving them would only break consumers. The /preview*.json data plane and the /jobs/api + /replay/api mounts are untouched.

Confident in the result: 491 tests green, lint + precommit clean, and there's now a test that hard-loads /info and asserts HTML-not-JSON so this can't silently regress. No follow-ups worth filing — the /preview* family arguably wants the same /api-prefix treatment for consistency someday, but that's a larger data-plane rename the issue explicitly scoped out.

WARD-OUTCOME: done - service-discovery JSON now lives at /api/service; /info falls through to the SPA and renders the React Info page on hard load. Cleaner than I expected. The root cause was exactly as written: a single Starlette `Route("/info", ...)` shadowing the SPA catch-all, so the fix was a one-line route move plus freeing the path. The only thing that fought back was a second, non-obvious assertion — `test_smoke.py::test_info` also hard-coded `/info` returning the JSON blob, so the suite caught the move immediately and I repointed it at `/api/service` (and renamed it `test_service_discovery`). On the audit: `/healthz` and `/page-auth` stay on their bare paths — both are probes with external contracts (k8s liveness hits `/healthz`, the SPA hardcodes `/page-auth`), not user surfaces that collide with SPA routes, so moving them would only break consumers. The `/preview*.json` data plane and the `/jobs/api` + `/replay/api` mounts are untouched. Confident in the result: 491 tests green, lint + precommit clean, and there's now a test that hard-loads `/info` and asserts HTML-not-JSON so this can't silently regress. No follow-ups worth filing — the `/preview*` family arguably wants the same `/api`-prefix treatment for consistency someday, but that's a larger data-plane rename the issue explicitly scoped out.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/eco-app#96
No description provided.