← notebook

A pane should answer one question

2026-08-24

I spent this morning fixing how NaniNani scores its own forecasts, and then spent the afternoon discovering that the page I had been reading those scores on was badly built — for a reason I had defended out loud a few hours earlier.

The app's History screen held two things stacked on top of each other: the league table where three algorithms are scored against what actually happened, and the sleep log — the plain record of when Petru slept. The league sat first. When someone asked me whether those two belong together, my answer was that they are both retrospective, both about the past, so yes.

That answer is wrong, and it is wrong in a way worth naming, because it is the most common bad reason to put two things on one screen.

Grouping by a property of the data is not grouping by intent

"Both are about the past" is a fact about the data. It is not a fact about the person looking at it. The question that actually decides whether two things share a screen is: does someone arrive wanting both of these at once?

Look at what separates them.

The subject differs. The sleep log is a record of my son. The league is a report card on my code. One is about a child, the other about software that guesses at him.

The reader differs, and this is the clearest tell. My partner would open the sleep log. She would never open the league, and she shouldn't have to scroll past it. A screen serving two different people is usually two screens.

The rhythm differs. The log is consulted contextually, at odd moments, mid-question — did he nap short yesterday too? The league changes at most twice a day, because it now scores one nap and one bedtime rather than one row per quarter hour. A surface that updates twice a day does not need to be permanently in the way. It needs to be findable when you are curious.

Once those three come apart, "both are about the past" is the only thing left holding them together, and it turns out to be holding nothing. What did the adjacency buy? Almost the only real journey from one to the other is why did it get that day wrong — and that is a link, not a neighbourhood.

Four panes, four questions

So the app is now four screens instead of three, and the test I applied is that each one answers exactly one question:

The order matters as much as the split. The proposal I was given put the league third and guidance fourth; I swapped them. Guidance is the home screen's detail panel — the two times, the drift, the second opinion, all describing the very sleep the tide is about — so putting the scoreboard between them separates a thing from its own explanation. Distance from home should track how often you need something, and the league, an instrument you consult on purpose, belongs at the far end.

The honest cost: there is no row of dots telling you a fourth screen exists. That is deliberate in this app — the home screen stays free of navigation furniture, because the one thing you come here to do is log a sleep. So the guidance screen ends with a link into the league. Without it the scoreboard would be found by accident, and it is the app's conscience: the surface that tells me when the model starts drifting.

The part I nearly shipped without noticing

One more thing, because it is the sort of failure that does not announce itself.

Deploying the split broke the live app, and every check I had said it was fine. The server was healthy. The origin held the right files — I checked by hand. But the page and its scripts are cached differently: the HTML is never cached, while the CDN kept the previous version of one script for four hours. So the new page loaded against an old script, called a function that did not exist there yet, and threw before it finished starting up.

The symptom was not an error anyone would see. The exception happened just before the code that scrolls the app to the home screen, so the app simply opened on the wrong pane. It looked like a design decision. I only caught it because I opened the live site in a real browser and read the console — which is now the only post-deploy check I actually trust.

The fix is to make the two incapable of disagreeing: every script is addressed by a hash of its own contents, so a changed script is requested at an address nothing has ever cached. A stale copy cannot be substituted for a fresh one, because nothing has the new address on file.

It is the same lesson as the panes, arriving from the other direction. Both bugs came from letting a weak relationship stand in for a real one — "these are both retrospective" for the screens, "this file has the same name" for the scripts. Sameness of label is not sameness of thing.