Skip to content
Development

Two stats, two definitions of "captured"

By Victor Da Luz
rustsqlitedev-loggreenhouse

Greenhouse’s dashboard shows two related stats: a “Captured today” badge and a consecutive-day streak. They’re supposed to agree, and mostly they did, until you captured an idea and promoted it to an active project on the same day. Then “Captured today” would quietly flip back off, even though you’d captured something that same morning.

The bug was two functions computing the same concept two different ways. The streak function pulled every item’s created_at regardless of what status it was in now, on purpose, with a doc comment explaining that a capture day shouldn’t vanish just because the idea got promoted later. But the “captured today” count filtered for status = 'fresh_idea' only. The moment you promoted an idea, it stopped being a fresh idea, and stopped counting, even though the streak still credited the day correctly. Same underlying data, two different lenses, two different answers.

While tracing this I found a second, related question sitting unresolved in the same corner of the code: importing an existing folder from disk stamps created_at at import time and quietly fed the streak too. Import a folder full of six-month-old song ideas and Greenhouse would credit you with capturing something today. The PRD means something narrower by “captured” though, and it’s explicit: “Captured an idea today?” is about the Capture flow, the actual creative act of sitting with a new idea. Adopting a folder someone already created isn’t that.

Fixed both at once: added a column marking rows as imported vs. genuinely captured, and pointed both stats at the same underlying query, filtered to exclude imports. Existing rows backfill to “not imported” by default, which means old imports from before this fix still count retroactively. I decided that was fine. It’s a motivational nudge, not a ledger, and the PRD is explicit that streaks are stats, never locks.

The whole fix lived in the database layer, no UI to click through, so the verification was four new tests exercising the real production functions directly rather than a mocked render. Sometimes the right amount of ceremony for a bug is a migration and a few unit tests, not a browser.

Related reading

Development

The guard that was never there

Adding one status check to a Rust function meant it could no longer call another function the same way - and that constraint quietly opened a hole neither function had on its own.

Read