Pressing ▶ or ⏱ on a routine killed the app outright, with no error and no red screen. Nothing was wrong with your database, and nothing was wrong with the app's code.
A deploy failed, and nobody noticed.
The API on staging was nine hours out of date. It was missing one field the app had just started depending on. Reading that missing field threw an error, and in a release build an error like that doesn't show a red screen. It terminates the app.
So a two-minute network blip on a deploy looked, on your phone, like a broken app.
Six links. Every one is verified: the failed workflow run, the image ID on the server, the source of the deployed commit, the error on the device, and the crash report.
It added routinePath to GET /routines/:id/chain and taught the player to read it, in the same round. From that moment the app required a server new enough to send it.
Not on code. GitHub Actions couldn't reach the VPS over SSH. It made twelve attempts across about two minutes, then it gave up.
Image 4af6259, from PR #41. Its copy of chain.ts contains the string routinePath exactly zero times.
The build on your phone was fine. It was talking to a server nine hours behind it.
chain[index].routinePath was undefined, and calling .find() on it threw while the player was mid-render. That's the white flash you saw.
React Native routes an uncaught render error to RCTFatal, which calls abort(). The process dies on SIGABRT. No red screen, because release builds don't have one.
// lib/chain-gate.ts:29 const boundary = chain[index].routinePath.find((r) => !entered.has(r.routineId)); ^^^^^^^^^^^ undefined on a stale API // what the device reported Render Error: Cannot read property 'find' of undefined // what the crash report showed underneath it -[RCTExceptionsManager reportFatal:stack:exceptionId:] RCTGetFatalHandler objc_exception_throw → abort() SIGABRT (signal 6)
The same error that shows a red screen with a file and line number in development just closes the app in release. All the diagnostic information exists, but it simply isn't displayed.
The simulator ran a dev build against your local API, which was up to date and shows errors. Two reasons it stayed healthy while the phone died.
The breakthrough was pointing a dev build at staging: same data as the phone, but a build that could still speak. That produced the red screen naming chain-gate.ts:29.
The deployed server returned nine fields. The app expected twelve.
| Field | Sent by stale API | Effect on the app |
|---|---|---|
| routinePath | No | Crash. undefined.find() during render. |
| taskType | No | Silent. A dump step stopped being recognised as one. |
| routineName | No | Cosmetic. The sub-routine crumb rendered blank. |
Only the first one was fatal, but one is enough.
Now running 67cb974, which is master. routinePath is served again. This alone fixed your phone. No rebuild was needed.
Both reads now go through a guard. No path means no sub-routine gate, so the step simply plays. A stale server is a smaller feature set now, not a crash.
The second fix matters more than the first. The app and the API deploy on separate schedules, so an app meeting an older server is ordinary, not exceptional. TypeScript can't catch it either: routinePath crosses the network, so the type describes what you hope arrived.
The deploy is the fragile part, not the code.
Widening that window is a small change. It's the one that stops this shape of problem recurring.