A few weeks back I wrote about replacing my Haikubox with a four-camera local pipeline feeding BirdNET into TimescaleDB. The data was beautiful. The dashboards were not.

Grafana is great for whether the heat pump cycled overnight or how warm the coop got. It is not great for what showed up in the yard for the first time this week, or what the dawn chorus actually sounds like in order. I kept tweaking panels and never got there. I wanted it to be more visual with bird photos, and more contextual with details about each species. So I stopped tweaking and built a real app.

Backyard Birds

It is a small Next.js 16 app, written in TypeScript, deployed as a Docker container on the same Ubuntu box that runs Postgres. It reads from the same detections hypertable the BirdNET workers already write to. No schema changes, no second database, just a new front end on stable plumbing.

Five tabs: Overview, Live, Species, Patterns, Cameras. Two of them surprised me.

The Cameras tab is gives me an idea of where the birds are. The Front Porch mic accounts for 48% of all detections and is dominated by Common Raven. Every other camera (Shop Side, Back Patio, Garden) has American Robin as its top species. Each mic is capturing a different microhabitat in the same yard. The Haikubox had one mic in one direction so it only caught a sliver. There is also a “specialties” list of species heard at only one camera.

The Patterns tab gave me the dawn chorus in order. Virginia Rail at 4:23am, American Robin at 5:00, Western Tanager at 5:14, Townsend’s Solitaire at 5:31. I knew the robins started first. I did not know there was a Virginia Rail going off in the dark before any of them.

The rest is honest workhorse stuff. Live polls the database every five seconds, with pause-on-hover and a camera filter. Species is a photo grid pulled from iNaturalist with category filters, and clicking a species opens a detail page with a Wikipedia summary, hour-of-day chart, and per-camera bars. Overview shows totals, a 30-day chart, top species, and a “Newest arrivals” row that has flagged Caspian Tern, Olive-sided Flycatcher, and Bullock’s Oriole in the last week, all of which I would have missed in Grafana.

Vibe-coding it

The entire app is 19 commits. I described the data model, pointed Claude at the dashboards I liked, and let it scaffold. From there it was mostly conversation. “The Live tab should pause on hover.” “Color the camera labels consistently across views.” “Move the per-camera hourly chart off Patterns and onto the bottom of Cameras.” A few more iterations to add some visual tweaks, fix some bugs and a time range filter gets me to today.

I never would have shipped this with traditional dev cycles because the activation energy was too high for a hobby project. Specifically getting all of the real bird data and images, caching it effectively to reduce API calls make it next level. With Claude it stopped feeling like a frontend project and started feeling like describing what I wanted until it existed. Most of my time went into looking at it, deciding what was wrong, and asking for the next change. Which is, I think, the actual job.

What is next

Notifications for first-time species is the obvious one; the data model already supports it. Beyond that, the backend has been running unattended since the original post. It just keeps logging. That is still the part I like most. Kim says it’s better than the product it replaced. I’ll take that as a win.