A fallback registered too early is not a fallback. It is the only route that exists.
Nineteen minutes in
FocusBottle is the only timer I've owned that I don't have to configure. No account, no settings to negotiate — pick 25, 50 or 90 and the bottle starts filling with light, dimming the moment my attention drifts off it. Yesterday I was nineteen minutes into a fifty when a price alert buzzed and the light went dim. I picked up the phone anyway.
I opened my trading dashboard to check a single position and found a red badge: broker disconnected. I spent the remaining half hour on it. The broker had been fine the entire time.
A route that answers everything
The dashboard wasn't lying, as far as it knew. Every request it made for data was coming back as a webpage — not an error page, but the app's own index.html, served with a friendly 200. Which is the most confusing possible answer to what is my account balance.
The cause was ordering. The backend registers its API routes, mounts its static assets, and then declares a catch-all route matching any path — the standard trick that lets a single-page app own its own URLs. That catch-all was registered before one small health endpoint. FastAPI matches in declaration order, so it swallowed everything, `/api/*` included. A fallback exists to catch what nothing else claims. Declared too early, it becomes the only route that exists.
Nothing was lying, which is why it took so long
No exception fired. No 500 in the log. Every layer reported success, and that is precisely why the badge said disconnected: the connection check received HTML and reported, accurately, that it had not reached the broker. The UI was the only honest component in the building.
The fix has two halves and I like it because of the second. In development, the backend no longer mounts the frontend at all — Vite's proxy owns that traffic, as it always should have. In the packaged build, where the fallback is genuinely needed, it now refuses anything starting with `api/` or `ws/` and raises a 404 instead of quietly returning a page. The first half removes the conflict. The second half removes the next version of it.
Why the timer only has three buttons
FocusBottle is mostly subtraction — no account, no setup, three durations. That isn't minimalism for its own sake. A focus session is fragile, and it dies on the first small uncertainty. Make me decide something, or worse, tell me something is broken when it isn't, and the thread I was holding is gone.
The same rule applies to the tools underneath. Every layer that can lie quietly will eventually cost you an evening. Three buttons that always work beat thirty knobs I have to trust.