← All posts

June 16 — a kernel reboot, two apps becoming one, and an enrichment layer pulled from Apple

This was the day the project stopped being two experiments and became one thing with a name. It opened with a recovery I didn't ask for and ended with the app — now called Loupe — running as a real service, backed up to the cloud, writing prose summaries of where I'd been, and carrying an enrichment layer pulled straight out of Apple Photos. A lot of threads. Here's the whole day.

Built / shipped

Problems & fixes

The box had silently rebooted. I came back to a dropped connection and a dead stack and assumed the worst — a crash, a storage fault, a power event. It was none of those. The reboot log plus a bumped kernel version told the real story: an unattended security upgrade had cleanly rebooted the machine overnight, and because the whole stack was hand-started with no auto-start, everything died with it. Checking reboot history before assuming a fault saved me an hour of chasing a phantom. Everything on disk was intact; I just had to restart it. This is the exact pain that motivated making Loupe a managed service later the same day.

The thumbnail generator was failing 100% — and it was OOM. It wasn't a code bug in the obvious sense. It was trying to decode up to 200 large HEIC images at once on a machine with only about 8 GB of RAM, and the box ran out of memory. The fix was to cap concurrency hard (twelve image workers) and split the work into an image phase and a video phase. That cap is now load-bearing — every path that touches decoding respects it, and I've written down: never raise it.

Two silent write-path failures. Both the OpenCV image writer and FFmpeg infer the output format from the file extension. Writing to a temp name ending in .jpg.tmp makes both of them silently fail — no error, no file. The fix: encode to a JPEG in memory and write the bytes, or write to a …tmp.jpg name and rename afterward.

The public site was returning 502. The fix was binding the app to all interfaces and pointing the tunnel's ingress at the right origin — the tunnel runs on a different host than the app, so pointing it at localhost could never have worked.

Summaries kept naming home after a nearby business. The venue resolver was labeling clusters by whatever shop sat a few meters away, so a quiet day at home came out labeled after some random nearby café or restaurant. And an earlier "discount far outliers" heuristic had erased a real trip — a genuine drive out to a ranch got filtered as noise. Two lessons collided here: an erased real place is invisible and unrecoverable, while a wrong label is at least catchable. So I ripped the outlier discount out entirely and kept only a minimum-cluster-size filter — under-filter, never over-filter. For home specifically, I suppress to the suburb name plus a distance gate, rather than fighting it with radius tuning that the geometry didn't support anyway.

The location provider rejected the call. The legacy geocoding API returned a flat denial for a new project. The newer nearby-search API worked, and skipping sub-feature points of interest (bathrooms, parking lots) let the real venue resolve.

Decisions

Learned

Still open / next