The UI that looked broken (and the feature I never shipped)

When the browser interface showed no projects, my first assumption was wrong — twice. A story about v1-vs-v2, SPAs, and how to debug someone else’s frontend.

The symptom

I stood up the server, opened the browser UI from my laptop, logged in, and… nothing. None of my existing projects were listed. None of my running sessions were visible. Clicking “Add project” dropped me into my home directory with no context.

I assumed I’d misconfigured something. Then I assumed the UI was just buggy.

Both wrong.

The first clue: the API disagrees with the UI

The backend clearly knew everything:

GET /project   → 10 projects
GET /session   → 100 sessions across every directory

But the UI showed none of the projects. When the backend says “10 projects” and the frontend says “add one,” the frontend isn’t asking the backend.

So I looked at what the frontend does ask for.

The second clue: the bundle

The UI is a single-page app. I fetched its JS bundle and grepped the client definitions:

path:"/api/project"          ← defined in the client...

…but:

curl /api/project → Content-Type: text/html

That’s not an API response. That’s the SPA’s catch-all serving the HTML shell — for a route whose backend isn’t implemented.

I confirmed it: an unknown /api/nonsense returns the same HTML. And the OpenAPI spec lists /api/session, /api/fs/list, /api/location, /api/health — but not /api/project.

What was actually going on

The server ships two distinct surfaces:

  • v1: the original API (/project, /session, /event). Complete. Global project and session discovery. Used by the CLI and TUI.
  • v2: a newer single-page app at / with its own /api/* namespace. The frontend is ahead of its backend: /api/project is a stub. The SPA’s home screen keeps its project list in browser storage — “Add project” is a filesystem picker, not a server-project list.

I did not build either one. opencode debug v2 is a built-in command; the @opencode/v2/* services and the SPA assets are embedded in the binary I already had. opencode serve just serves whatever UI is baked in.

The lessons

1. When UI and API disagree, the API is usually the truth. The server had 10 projects and 100 sessions the whole time. The UI just never asked.

2. “Is this my bug?” has a cheap test. I didn’t write that UI. How could I tell? The bundle is embedded in the vendor binary, the version matches npm latest, and a debug v2 subcommand exists that I never added. When you’re debugging a system, establish provenance before you try to fix it.

3. An SPA is only a frontend. “Yes, SPAs are standard” and “yes, this one is incomplete” are both true. The architecture is right; the migration isn’t finished. A 200 with Content-Type: text/html on a supposed API route is a frontend with no backend behind it.

4. Pick the stable surface for your integrations. The browser UI is a moving target. The v1 REST API is complete and stable. If you’re building anything on top, build on v1 — not the SPA.

The one-liner

A UI that “looks broken” may just be a newer, unfinished frontend; the working data can be sitting right behind an older API you haven’t tried yet.