Two stats, two definitions of "captured"
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
The empty database that looked perfectly healthy
SQLite treats a zero-byte file as a valid fresh database, so every corruption check passed - and the backup pruning would have deleted the good copies within a week.
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.
A test that proved nothing, and the bug it was supposed to catch
A five-finding cleanup batch where the smallest item mattered most: a regression test that failed because the bug it targeted is structurally unreachable.