Files
zeekayandHanzo Dev 5ea9ae272a web: the console comes off gorilla — it is an http.Handler now
pkg/web was the last package importing gorilla/mux for something that is not
routing. web.Web carried AddToRouter(*mux.Router): the console's mounting rule,
braided into one router's type. It bought nothing. Every host registered the
same terminal catch-all, so the rule was the HOST's — spelled once, where the
host composes its handler chain — not the console's.

So the interface is the value: web.Web is http.Handler. The net/http host in
pkg/query-service/app/server.go spells r.PathPrefix("/").Handler(web) after
every API route; a zip host spells app.All("/*", zip.AdaptNetHTTP(web)). Same
value, one implementation, mounted where the host mounts things.

Serving stays stdlib rather than zip.Static, and that is a choice not an
omission. zip.Static exists and is good, but it serves bytes out of an fs.FS,
and our shell is NOT a file in the tree: it is rendered once at boot with this
deployment's base href and its settings JSON. WithIndex/WithFallback would serve
the raw index.html and hand the browser a console with no base href and no boot
data. Nothing in this repo hosts the console on a zip app either — cmd/community
(the shipped image) serves a net/http chain, and in the composed cloud binary
webui.Mount already owns /*, so a second /* from o11y would shadow the cloud
console for every path. A zip-only console here would be dark code or a
collision; hanzoai/cloud's own webui is stdlib + AdaptNetHTTP for the same
reason.

noopweb answers 404 instead of registering nothing. Mounting is unconditional
now, so "this deployment has no console" has to be a status — and 404 is the
exact answer it already gave, since gorilla's default NotFoundHandler is what
served the paths the null provider left unregistered. Same bytes, same header.

And the fix the move exposed: the catch-all was swallowing the API. gorilla
resumes the parent router when a subrouter's prefix matches but none of its
routes do, so every miss inside /v1/o11y — a typo, a route this build does not
serve, a method the route does not take — fell through to the console and came
back 200 text/html. A JSON client got the SPA shell. routerweb now refuses /v1
and /ws, segment-exact so a console route that merely starts with the same
letters (/v1beta-…) stays the console's.

Proven by bodies, not by status: GET / -> 200 text/html + the templated shell;
GET /assets/index-a1b2c3d4.js -> 200 text/javascript with the bundle's bytes,
never the shell; GET /services/frontend/overview -> 200 the shell (deep link);
GET /v1/o11y/pods/bogus and /ws/query_progress -> 404 text/plain with no "<html"
in the body. Fixture is a temp dir, so the test needs no frontend build. Deleting
apiPrefixes turns the boundary test red with the exact 200 text/html it is there
to catch. Route table unmoved: 367 routes, 0 catch-alls, 353 MCP tools.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 18:24:13 -07:00
..