gzl.dev/blog
field notes, mostly
18 posts
August 27, 2026
7 min
Nine ways to say yes
I taught a Light Phone to speak an Oura ring's protocol over BLE. Then I spent a week failing to tap a button that doesn't exist.
Light PhoneBrightOuraBluetoothAndroid
August 21, 2026
6 min
Field notes on LightOS
What the Light Phone III's OS actually is underneath: Android with the Google removed, hardware that talks in key events, and conventions nobody voted on.
Light PhoneLightOSAndroid
August 21, 2026
1 min
A tape recorder, not a voice recorder
BrightRecorder: clips named for where and when, tapes that are just folders of WAVs, and the wheel finally doing the job it was born for.
Light PhoneBrightRecorder
August 18, 2026
2 min
Why I put an app store on a phone designed to have none
Yes, I see the irony. The argument for building it, why there's no server behind it, and the afternoon I accidentally dropped everyone's apps from the index.
Light PhoneBrightMarket
August 14, 2026
2 min
A good fork says thank you
A lot of what I ship is other people's work with my name nearer the top than it deserves. Some rules I try to fork by.
Light Phoneopen source
August 11, 2026
2 min
My bug reports file themselves
Shake the phone three times and the app files its own GitHub issue. Then an AI triages the queue every hour — under strict instructions that guessing is worse than waiting.
Light PhoneAItooling
August 8, 2026
2 min
Fifteen copies of the same file
The least interesting code I maintain is the code I'd save first in a fire. On finally building the shared library.
Light PhoneKotlin
August 8, 2026
2 min
BrightMarket is out
The app store ships: browse on a desktop, install by QR, track updates on the phone, and get your own app into the catalog.
Light PhoneBrightMarket
August 7, 2026
2 min
One column of scores
Twenty-two leagues from four keyless public APIs, in one grayscale column. How BrightSports gets scores with no account and no push.
Light PhoneBrightSports
August 6, 2026
3 min
The MetroCard inside my phone
I dissolved a transit card in acetone and moved its nervous system into the battery compartment. Tap-to-pay is back, sort of.
Light PhonehardwareNFC
August 6, 2026
2 min
Every button does what you tell it to
The follow-up to the keylayout post: BrightControl itself, why the defaults took longer than the code, and the trust problem with accessibility services.
Light PhoneBrightControldesign
August 5, 2026
1 min
Roll is out
The camera app ships: a filter wheel on real hardware, purikura with four-shot strips, date backs, and a privacy lesson from launch day.
Light PhoneRoll
August 4, 2026
2 min
The keylayout file Light left behind
The scroll wheel is just key events. Everything I learned reading Generic.kl off the phone, and the accessibility service that became BrightControl.
Light PhoneAndroid
August 2, 2026
3 min
Rewriting the camera
Roll's whole design is one gesture, stolen from a contact sheet. Plus the filters that got away from me — purikura on a monochrome phone, and datamoshing experiments that mostly produce mud.
Light Phonedesign
July 29, 2026
1 min
A QR scanner, and its desktop twin
The modern world hands you addresses as QR codes and the Light Phone couldn't read them. A scanner for the phone, a generator page for every other screen.
Light PhoneLightQR
March 22, 2026
1 min
BasilNet, a year later
What broke, what I'd trim, and how the media server quietly became infrastructure for everything else I build.
Dockerhomelabself-hosting
March 8, 2026
1 min
I built a bad DSLR on purpose
It took worse pictures than my phone. It's one of the most useful things I've ever built.
hardwarephotography
January 10, 2025
1 min
Building BasilNet: My Home Server Stack
How I built a 15-service media server with Docker Compose, Gluetun VPN, and Home Assistant integrations.
Dockerhomelabself-hosting
← all posts
August 11, 2026 · 2 min

My bug reports file themselves

Light PhoneAItooling

Every app I ship for the Light Phone has the same hidden feature: shake the phone three times and it files a bug report. Not an email, not a form — a GitHub issue, opened by the app itself, with the screen you were on, a table of build and device info, the last crash log if there was one, and a screenshot. If the app crashed outright, it asks on the next launch whether to send what it caught.

I built this because the alternative was worse. Bug reports for phone apps normally arrive as a Reddit comment three weeks later: “the camera thing broke.” Which screen? Which version? Gone. The reporter has moved on, and honestly, fair — describing a bug is work, and nobody owes me work. So the app does the describing. The person just shakes.

The 360-pixel witness

The screenshots come through grayscale and 360 pixels wide, and I expected them to be useless. They’re the most valuable part of the report. A broken layout, a stuck spinner, a date that’s obviously wrong — you can see it instantly, in a way no prose description gets close to. Most reports are diagnosable from the screenshot and the build table alone.

Then I stopped being the bottleneck

The reports land in one repo, and for a while “triage the queue” was a thing I did with coffee. Now an AI agent does the first pass, every hour. It reads each new report, clones the app’s source fresh, and works out what happened.

The rules I gave it are mostly rules about restraint:

  • Fix it only when the cause is identified — a stack trace naming the line, code that visibly does the wrong thing — and the fix is small and contained. A null guard. An off-by-one. A missing state reset.
  • Hand it to me when the cause is a guess, when the fix would change behavior someone might have an opinion about, when it needs the physical phone, or when it touches more than about three files.
  • Never reuse a stale checkout, because pushing from an old base silently deletes whatever landed in between, and it compiles perfectly afterward.
  • Branch first, always. In these repos a push to main cuts a signed APK that’s on people’s phones within minutes, with no human between a mistake and the device. CI runs the same checks on a branch and publishes nothing.

The line I keep coming back to when tuning it: guessing is worse than waiting. A wrong fix ships. A waiting issue just waits.

Does it work?

For the boring majority of reports — yes, and the boring majority is the point. Null guards and layout constraints get diagnosed, fixed, tested, and released while I’m at work, and the release notes say what changed. The interesting bugs, the ones where the report is really a design question wearing a trench coat, get labeled and handed to me with the agent’s notes attached. That split is the whole value: I only see the reports that actually need a human, and the person who shook their phone gets a fix in hours instead of whenever I next had a free evening.

The plumbing that makes this possible in every app at once — the shake detector, the report overlay, the crash handler — lives in a shared library, which is its own unglamorous story for another post.