2026-07-23
Sprint 5 Closes: One Audit Trail, Three Screens, and the Bugs You Only Find by Using the Thing
Sprint 5 of the beer app closed today. Admin Experience — role assignment, user management, audited beer edits, an anomaly panel, a dashboard tying it together — was scoped as seven issues two days ago, and all seven shipped, each as its own pull request. That part went the way the plan said it would. What's more interesting is what happened when the plan met the actual running app: a generalization that paid for itself three times over, and three separate bugs that no unit test caught, because none of them were about logic being wrong — they were about assumptions nobody had stated yet.
Building the shared piece once, then actually reusing it
The grooming session that scoped this sprint made a bet: instead of writing three separate "admin corrects a record, with a reason, with an audit trail" implementations — one for beers, one for accounts, one more if anything else needed it — build the shape once as a generalized AdminAudit trail and have everything else build on top of it. Betting on reuse before any of the reusing code exists is the kind of call that can go either way.
It went the way you'd hope. Role assignment wrote to it. Account deactivation wrote to it. Beer edits wrote to it — including a late addition, extending it to beer creation too, once it became clear that catching a suspicious burst of new beers is useless if you can't say which admin account added them. Four different actions, one audit table, one pattern to test and reason about instead of four. The alternative — a ConfirmationAudit-shaped table for every entity that ever needed correcting — would have worked too, just as four times the surface area for the same guarantee.
Deciding what a "diff" means before writing one
The audit trail's existing rows only ever held one changed value each — a role name, an "Active"/"Deactivated" flag. A beer edit touches up to ten fields at once, and there wasn't an established answer for what an audit row should look like when the thing being audited isn't a single value. The tempting shortcut was to just serialize the whole before-and-after beer object into the row as JSON and call it done — no comparison logic needed, captures everything.
It's also the wrong instinct for this table. Every other row in it is a short, human-readable line an admin can actually read without decoding JSON, and most of a beer's ten fields don't change on a typical edit — a JSON dump would be mostly noise. So the diff logic compares old and new field by field and records only what actually changed: "Style: Amber Ale; ABV: 5.2" becomes "Style: Belgian Pale Ale; ABV: 5.5". More logic to write, a format that stays legible, and it never had to become someone's problem to parse later.
The bug that only showed up because I logged in
Unit tests covered the user list endpoint thoroughly — role gating, pagination, the whole shape. It still crashed the first time I actually clicked through the finished screen, because bootstrapping a test admin account by hand left that account with two role rows instead of one, and the endpoint's role lookup assumed exactly one. No unit test had ever put a user in that state, because the app's own code never creates it — only a manual database fix does. It's a small thing, and it's also exactly the kind of thing that only surfaces when a real person does something slightly outside what the automated tests imagined, which is the actual argument for clicking through a feature yourself instead of trusting the green checkmark.
Naming versus guessing
Two smaller decisions came up mid-sprint that were easy to guess at and expensive to guess wrong. The dashboard needed an "active members" count, and the sprint's own issue never defined "active" — but an earlier planning document had already defined it, as members who'd confirmed a beer recently, not as accounts that simply hadn't been deactivated. Those are genuinely different numbers, and only one of them means what an owner would assume it means glancing at a dashboard. Separately, the dashboard was supposed to "become the landing page for the admin role," and the actual home screen had no idea an admin was even signed in — it would have kept showing an admin their own "0 of 200" beer progress, which makes no sense for someone who doesn't drink on the clock. Both got resolved as explicit decisions instead of assumptions baked silently into the code.
The PM angle
The theme underneath all four of these isn't "write more tests" — the tests were fine, and more of them wouldn't have caught any of this. It's that a green test suite proves the logic does what you told it to do; it says nothing about whether what you told it to do was actually right. That gap gets closed by clicking through the feature as a real user, and by pausing to ask what a word like "active" is actually supposed to mean before writing a query around it. Both are cheap. Skipping either one is cheap too, right up until the feature ships with the wrong number on it.
Where the project stands
Five sprints in, and the Admin Experience epic — dashboard, anomaly detection, full user and catalog data correction with an audit trail — is done. Test suites stand at 236 backend and 149 frontend, all green. Mug Club Progress, Customer Phone Experience, and Admin Experience are all fully shipped now; Auth is done for both password and social sign-in. Next up: grooming the Engagement, Retention & Social epic — badges, push notifications, the social layer — into its own sprint. That session hasn't happened yet.
Comments
Loading comments...