2026-07-26

Sprint 12 Closes: The Pipe Carries Its First Real Message, and the Owner Gets a Second Dashboard

Sprint 11 built a whole notification pipeline and proved it worked with a single throwaway test message sent to nobody who needed it. That was deliberate — proving the pipe holds before connecting anything real to it. This sprint is where something real finally flows through it: a customer saves a beer to their want list, an admin flips it to "on tap," and that customer gets a push telling them so. Alongside it, a second and unrelated piece of work: a new screen for the tavern owner, built to answer a question the existing dashboard was never meant to answer.

Finishing what was deliberately left broken

The service worker's push and notificationclick handlers were literal no-ops in Sprint 11 — the pipe existed, but nothing was listening on the other end. This sprint fills them in. The push handler parses whatever payload arrives and shows a notification; if the payload is missing or malformed, it falls back to a plain default rather than failing silently, since a push handler that throws just means nothing appears and nobody knows why. The notificationclick handler closes the notification and either focuses an already-open tab and sends it to the right page, or opens a new one there if nothing's open.

The interesting part was how to test any of this. A service worker isn't a module you can import into a test file — it's a plain script that runs against a global self the browser provides. So the test reads the actual file that ships, evaluates it against a small mocked self that records whatever listeners it registers, and then fires fake push and click events at those listeners directly. That matters more than it sounds: it's testing the file that actually goes out the door, not a parallel copy of the same logic that could quietly drift from what's really shipped.

The trigger nobody has to remember to pull

The other half of the alert is simpler than it sounds: when an admin flips a beer's status to "on tap," the API looks up everyone who'd saved that beer to their want list and queues one push per customer. The one deliberate choice here is what it doesn't do — it doesn't build its own rate limit. Sprint 11 already has a per-customer cap on how many pushes anyone gets in a given window, so this trigger just uses it. Building a second cap here would have meant a second place that rule could quietly fall out of sync with the first one, for no real benefit.

A second dashboard for a different question

The existing Admin Dashboard answers "how's today going" — counts, anomalies, the operational pulse of the place. It was never built to answer the tavern owner's actual weekly question: who's drifting away, and what should I put on tap next. This sprint gives that question its own screen.

A lapsed member, here, is specifically someone who confirmed a beer at some point but hasn't in the last 30 days — a different signal from someone who's never engaged at all, which isn't really "lapsed" so much as never arrived. The other half is a beer-demand list: want-count and average rating per beer. The rating side only ever surfaces an aggregate — the screen can tell the owner a beer averages 4.2 stars across six raters, but it never says whose rating that includes. That's not an incidental detail; it's the same rule this app has followed everywhere ratings show up, and this screen was built to keep following it rather than carve out a quiet exception because it's "just for the owner."

Proving it against the real thing, not the stand-in

Both new queries behind that screen are riskier than they look. One groups confirmations by customer and then filters on the most recent date in each group; the other averages a rating across everyone who left one. Both pass cleanly against the fast in-memory database the test suite normally runs on — and that's exactly the problem, because that stand-in doesn't actually enforce the same SQL translation a real Postgres database does. So both got a second, independent check: run the same numbers by hand against a live database and compare. It's the same lesson a post from several sprints back was built entirely around — the test suite passing and the feature actually working aren't the same claim, and the gap between them is exactly where the fast test double can't see.

The lesson from last time held

Last sprint's merge order had a real trap in it: deleting a branch partway through a stack of dependent pull requests silently closes whatever was based on it, without any error to notice. This sprint's pull requests stacked the same way, and this time the fix from last sprint's retro got applied on purpose — re-point every dependent request at the main branch first, then merge from the bottom up, deleting nothing until the whole stack is safely landed.

One more honest note from testing this sprint: re-seeding a stale local database surfaced a real gap in the local setup — a freshly started database container and the API racing each other, with the API sometimes trying to run its startup migration before the database is actually ready to accept a connection. That's a real thing worth fixing. It also isn't this sprint's job, so it got written down for the deployment work that will eventually own it, instead of quietly expanding this sprint to include a fix nobody asked for.

The PM angle

Reusing the frequency cap and reusing last sprint's merge-order lesson are the same instinct in two different outfits. A completed sprint's retro is only worth something if the next sprint actually stands on top of what it learned, instead of quietly re-deciding the same question from scratch. And noticing a real gap without immediately patching it is its own kind of discipline — writing it down for whoever's sprint it actually belongs to is often the more useful move than closing it out on the spot, because it keeps this sprint's scope honest instead of letting "while I'm in here" quietly take over.

Where the project stands

Five issues (#128–#132), five pull requests (#133–#137) across two independent stacks, all merged in order onto master, milestone closed. Backend suite: 429 passing. Frontend suite: 278 passing. Clean production build. The riskiest parts — the analytics queries and the on-tap trigger's full path from an availability change to a queued push — were verified live against a real Postgres database, not just the test suite. The one thing still marked as a manual step is the same class flagged before: a real phone actually receiving and tapping a notification. Next up is the rest of the engagement and retention work this pipeline was built to carry — badges, automated notifications, the owner's own message composer, the social layer — all unblocked by this sprint, and all still deliberately ungroomed until each one is actually its turn.

Comments

Loading comments...