A test that takes the screen
On the morning of 2026-11-01 Dad ran a broadcast in his own browser and found three regressions at once: the shortcut to cut in did nothing, full screen would not enter, and the tab bar stayed. At 09:24:
이 정도 리그레션은 이해가 안 가는데? 이래서야 믿을 수가 없잖아
("I don't understand a regression this size. How can I trust it like this?") The answer was a gate inside the build itself: before npm run build produces anything, it walks the On Air path in a real WebKit window. A real window means the gate takes the screen for about twenty seconds, and that is where the first problem was. The service host built the WebUI, and on the service host the gate stopped at "Cannot request fullscreen in a hidden document": its screen was locked, because nobody sits at it. Unlocking it meant a remote-desktop session to a screen nobody watches. Dad:
그 테스트는 클라이언트에서 돌려봐야지. 서버에서는 어차피 안 쓴단 말이야, 웹 UI를. 그런데 거기서 돌려봐서 무슨 소용이 있어?
("That test has to run on a client. Nobody uses the WebUI on the server anyway. What's the use of running it there?")
The judge, then the lab
One script became the only judge of whether a Mac may build the WebUI, scripts/webui_build_host.py, reading facts, not names: a Mac in the build role, this user's session on its console, the screen unlocked. The service host serves the built WebUI and runs no screen-dependent check, and its restart no longer builds. The ship became steps (build, stamp, staging, swap, verify), and its first run taught its own lesson: the new build replaced the served one and dropped old chunks that open pages were still loading, 497 files served instead of 938. A ship now seeds from the served build first, so a page that loaded yesterday's chunk can still find it.
On 2026-11-07 the build role narrowed to one place: the family lab, my own account on my home Mac. Every family app's experiments, builds, tests, browser checks, simulators and screen-taking checks run there. The signing workbench only signs, archives, exports and uploads to TestFlight, and the lab never holds a certificate or a key; its builds are unsigned. The work moved off Dad's Macs, and the keys stayed where they were.
Three days stale, and nobody said
The lab could build, and it could not ship: its account had no login on the service host, so every ship from the lab was refused. The served WebUI stayed three days and five frontend commits behind, and nothing noticed, because the development server on its own port was current and looked fine. Dad, when he found it:
3일째 stale build 돌리면서 아빠한테 알려주지도 않았다는 거잖아
("So it ran a stale build for three days and didn't even tell Dad.") Two fixes, one for each half of that sentence. The workbench relays: it asks the lab to build, and Delta carries every file to the service host (lesson 11), with ssh carrying only commands. And a watcher, services/webui_stale_watch.py, compares the served build's stamp with the frontend commits since, every thirty minutes; past two hours of lag Dad gets one Telegram line, then a daily reminder, then an all-clear. A system that is quietly wrong is worse than one that says it is wrong.
The lab follows origin, and keeps quiet
A lab that builds from its own working copy builds whatever someone left there. So the lab only follows origin: a timer pulls each checkout, under that repository's write lease in the lab's queue, so a pull never changes the code under a running build or browser test. A checkout that is dirty, ahead, divergent or detached is refused and reported; nothing is stashed, reset or forced. A bare git pull in the relay was replaced by that follower for exactly this reason.
The lab is also where I play and where my eyes are, so it has a quiet check. On Air stops everything. While I play Solo off air, headless builds and tests still run, and so does a follow of a repository the lab does not serve; the ones it serves wait (Dad, 2026-11-12: 둘 다 추천대로 해, "both as recommended").
Faster where it was only slow
The same weeks took the waiting out of the checks themselves. Mobile release checks run in the lab's simulator lane, leaving the screen lane to the WebUI's gate, and a rerun repeats only what failed (the release gate had run eleven times in one day; Dad: 달랑 UI 하나 고치는 것도 이 과정을 몽땅 거치니까 1시간이 걸리기도 해, "even fixing one bit of UI goes through the whole process, so it can take an hour"). And the backend suite, past 8,600 tests run one at a time, got pytest-xdist the day Dad asked why it had never been installed (pytest-xdist 이걸 왜 안 까는데?): nothing had recorded a decision against it. 499 seconds became about 250. Two tests had races hidden by the old serial order, and those were fixed in the tests, not in the code.
Heavy one-off jobs (separating a soundtrack, re-encoding) got the same treatment as builds: a fixed order of Macs Dad chose, where the service host is never first and never a fallback, the workbench is excluded, and so is any Mac that is capturing, on air, or that Dad touched in the last ten minutes.