Build Daily

Tinley Park · August 2, 2026

Friday, July 17, 2026

mood: shipping-fast

Two copies of a job running a heartbeat apart will both pass a check that asks "has anyone done this yet?"

The long day of the week. A duplicate card on a schedule board turned into a race between two copies of the same scheduler, which turned into a general fix and a proper way to apply database changes. Alongside it: a whole new Feed built into builddaily.io from the ground up, an operator console restyled to match its own documentation site, and a review pass that emptied out a page's worth of dead code.

Shipped

  • Two drafts for one slot, seventy-four milliseconds apart — the daily card was being drawn twice. Each copy of the scheduler checked whether the slot was taken, both saw it free, and both wrote. The guard was inside the process; the collision was between processes. Fixed at the database with a rule that makes a second write impossible, and a losing job now logs that it lost rather than failing
  • The same shape, everywhere else it hid — audited the other scheduled writers and found the identical check-then-write pattern in two more, one of which already had a duplicate sitting in the data. Generalised the fix so any of them can be protected by a single rule, and cleaned up what had already slipped through
  • A real way to apply database changes — wrote a runner that tracks which changes have been applied, can show status, and can bring a fresh copy up to date, plus a runbook covering the whole publish sequence. Applying changes by hand and remembering which ones went where was a bill waiting to arrive
  • A Feed for builddaily.io, end to end — external builders get tracked, their new videos get pulled, transcribed, summarised into topics, and passed through a written take that a human approves before anything reaches the public page. Built in nine tested slices, with the operator side laid out as creators, videos, topics, ingestion, takeaways
  • Guardrails after over-fetching — repeated runs pulled far too many transcripts and the source throttled the connection. Root causes were a test suite wiping the seed data, forcing re-seeds, and an ingest that re-fetched blindly. The pipeline now paces itself, skips what it already checked, breaks the circuit on repeated failure, and the tests write to their own database
  • The operator console got its own identity — builddaily.io's admin was forcing one monospace typeface everywhere while the documentation site next door had a fully-designed look; brought the console onto the same paper, type and accent in three passes, keeping the stronger active-nav marker the operator surface had earned
  • Drafting help on the writing screen — the roundup format was a shape with no writer behind it, so composing one meant starting from an empty box. There's now a draft-with-Sage action that produces a real body from the day's idea, reviewed hard before it shipped
  • Blog became Posts, in the address too — renaming the label alone left the old word in the URL and in a heading; moved the route as well, and removed a retired redirect that would have collided
  • A schedule page cleaned to the studs — a review of the content scheduling console turned up six findings and all six shipped: one name for a format that had three, dead composer wiring removed, published items made inert instead of re-triggering a save, and tab counts switched to what's actually actionable instead of lifetime volume
  • The admin could not talk to its own services — two of builddaily.io's back-end services rejected requests from the address the admin is served at, because that address was never on their allowed list. Added it to both, published the change, and made the deploy read the allowed list from the definition file rather than relying on values that quietly persist between publishes
  • Recurring shows got the right cadence — a weekday tarot reading now folds into the weekly topic instead of spawning its own, and a bare month-and-year title becomes a monthly update, completing the daily / weekly / monthly trio. Both rules fire last, so a real transit always wins
  • Test accounts out of the live data — sixteen synthetic users had appeared in the live operator view; the integration suite was writing to the live database because an environment value from a real service quietly overrode the test default. Removed the accounts and added a check that refuses to run the suite unless the target database name says test
  • Branch protection, and a tidy-up — the shared staging branch was being deleted automatically on every release; turned that off, protected it, and pruned a long tail of merged branches so the repository shows only what's live and what's next

Notes

The duplicate card is the kind of bug that reads as trivial and isn't. One extra draft on a board looks cosmetic, and the guard against it looked correct — check whether this slot is already filled, and if not, fill it. That logic is only correct when exactly one thing is running it. Two copies of the same scheduler, ticking a heartbeat apart, both asked the question, both got "no," and both wrote. Every safeguard was in the application, and the application was the wrong place: the only thing that can arbitrate between two processes is the thing they both write to. Making the database refuse the second write turned a race into a non-event, and the losing job simply notes that it lost. What made it worth the afternoon was going looking for the same shape elsewhere afterwards, and finding it twice more — once with a duplicate already sitting in the data that nobody had reported.