This is the longest day in the whole story, and it's worth telling in the order it actually happened, because none of it happened cleanly.
It started with a problem that had been quietly getting worse for days: every run, every note, every scheduled experiment was living inside Android's SharedPreferences — the storage system meant for a login flag or a "has seen onboarding" boolean, not a growing pile of structured records. It hadn't broken yet, technically. It just clearly wasn't going to survive contact with a real user's actual data — no queries, no relationship between a run and the protocol it came from, everything serialized as one blob that got slower with every experiment logged.
The fix was Isar, a real embedded database with typed models and indexes, and getting there wasn't a clean swap. isar_flutter_libs needed the Android Gradle Plugin patched — compileSdk bumped from 30 to 35, a namespace declared that had never been necessary before — the kind of thing that eats an afternoon before a single line of the actual feature gets written. Once it was in, though, real things followed fast: .eln (RO-Crate) export for a single run or a whole period, camera and gallery OCR that actually worked in the note composer, result photos with a thumbnail grid. And one number that mattered more than it looked like it should — building the release APK with --split-per-abi cut it from 83MB down to about 33MB. Nobody using the app would ever see that number. It mattered anyway.
Buried in that same commit, almost as an aside: the app's package name, app ID, labels, and preference keys all quietly changed from Labmate to Labi. No announcement. Just the rename, sitting inside a database migration, because that's genuinely how the day went — one big thing carrying another big thing along with it.
And then, on the same day, a completely different kind of fight started: calendar sync. It needed to ask for permission, read a device's calendars, and write events to one — three things that sound simple until two different Flutter packages each have their own opinion about how permission is supposed to work, and neither opinion is complete on its own.
The first attempt wasn't really a fix. It patched a wrong property name on device_calendar's Result object — real bug, wrong bug. Permission still wasn't working reliably. Next theory: stop asking device_calendar for permission at all and switch to permission_handler, a more general package — which immediately surfaced a second wrong assumption, because the code was requesting Permission.calendarFullAccess, an iOS-17-only permission that silently does nothing on Android. Swapped to plain Permission.calendar. Still not the whole story: permission could now be granted through permission_handler, and device_calendar would still return an empty list, because it keeps its own internal permission state that doesn't automatically know the OS just granted access through a different package. Fixed by calling device_calendar's own requestPermissions() right after permission_handler's request succeeds, so both sides agree access exists.
Even that had one more layer underneath it — calendars would sometimes come back empty on the very first load after granting, then populate correctly a moment later on a second call. A straightforward race: the OS hadn't finished propagating the grant before the first query ran. Added a retry after events load successfully. And somewhere in the middle of all of it, the smallest fix with the most obvious payoff: stop silently swallowing calendar errors, and just show them — including the genuinely legitimate case where there are no calendars on the device at all because no Google or Exchange account was ever added.
Eight commits to get there. None of them were wrong to try — each one was the right next question given what the previous fix had just revealed. That's what real debugging looks like more often than not: not one flash of insight, but a chain of smaller ones, each closing off a wrong theory before the next one opens up.
And the day still wasn't over. Stacked on top of a database migration, a rename, and an eight-commit permission fight: AI-assisted protocol parsing through Gemini and Groq, a first pass at RSpace integration, calendar booking, a round of export fixes. It's tempting, reading this back, to imagine it was calm. It almost certainly wasn't.