Jenkins shared library: Extracting the Sync blog stage - Part 2
~6 min read
With the scaffold in place, the first real candidate was obvious: the Sync blog stage that appears, near enough verbatim, in three Jenkinsfiles. This post is about what I moved into the library, what I deliberately left behind, and the two options I gave the new step.
What the stage does
Reading the three copies side by side, the work they do is the same:
1. Take two Secret text credentials from Jenkins, ghost-url and ghost-admin-key.
2. Run blog-sync --apply, which creates mi
Childcare calculator: Doing the top-up maths for sleep-deprived parents - Part 6
~4 min read
If you've ever tried to work out a payment while running on three hours of broken sleep, you'll know the feeling: you read the same number four times, and it still doesn't mean anything. Baby brain is real, and it doesn't only affect the person who gave birth. Sleep-deprived parents get things wrong in new and creative ways, and "how much do I need to pay into the childcare account?" is a surprisingly easy one to get wrong.
The problem
With Tax-Free Childcare, you don't pay the f
Marketplace Listing Helper: A full-page manager, without a bundler - Part 5
~8 min read
Thirty-six items in a 330-pixel panel on the corner of a Facebook page is a poor place to load a batch. I wanted a proper page: a table of everything, editable in place, with import and export. This post is how that page was added without a build step, and without duplicating the code that already worked.
The page
An extension can ship ordinary HTML pages that open at chrome-extension://<id>/manager.html. They share chrome.storage.local with the content script, so the manager and
Personal finance aggregator: Periods, charts, and a phone-sized UI - Part 13
~9 min read
With real categories (post 12) the Home and Transactions pages finally had something worth slicing. This post covers the next batch of changes, mostly 30 September and 1 October: choosing a reporting window, charting balances over time, making the whole app usable on a phone, and a surprisingly instructive detour through deleting a transaction. The theme is that each change started as the obvious implementation and had to be corrected once it met real data.
Rolling windows instead
House-move inventory: The SPA: small pieces, no UI library - Part 5
~8 min read
The web app is how we actually use this: standing in a half-packed bedroom with a phone, typing "duvet" into a box. That use case set the design priorities more than any architecture diagram did - fast entry, big touch targets, and nothing that needs a laptop.
It's a React + Vite + TypeScript single-page app in web/. No server-side rendering, no shared code with the Python side. It talks to the API over JSON and that's all.
Three conventions that carry the weight
1. One file tal
Jenkins shared library: Why a shared library - Part 1
~5 min read
Most of my projects have a Jenkinsfile, and over time they've started to look alike. The finance system, the inventory system and the identity system each carry their own copy of a Sync blog stage - the one that publishes the project's blog/*.md posts to Ghost. The three copies are the same block of Groovy: a when { branch 'main' } guard, a catchError that marks the stage UNSTABLE instead of failing the build, two Ghost credentials, and a small shell function that runs the blog-sync
Marketplace Listing Helper: From a spreadsheet to listings - Part 4
~6 min read
With the importer working, I fed it the real thing: a spreadsheet of Warhammer stock called warhammer-items.csv. It did not import cleanly, and the reasons are a good tour of what "my data" looks like next to what a tool expects.
What the sheet looked like
Nine columns: Item, Brand, Range, Faction, Status, Quantity, Retail, Avg eBay Sold, Market vs Retail (£). It was a stock-take, not a set of listings. Three things stood in the way.
1. No price column. The importer looks for p
Childcare calculator: Term dates, persistence, and small decisions - Part 5
~8 min read
The last stretch of work was less about the core maths and more about making the tool something you'd actually keep using: handling term-time-only funding, remembering your setup, and a handful of small decisions that were easy to get subtly wrong.
Term-time-only funding
Some funded hours only apply during school term. In the holidays you still book the childminder and still pay, but nothing is free. The calculator needed a way to say "these days are eligible, these aren't".
I c
Personal finance aggregator: Rule-based categorisation - Part 12
~10 min read
The category column has been in the transactions table since post 4. For two weeks of commits it held nothing, and the Home page's "Spending by category" chart ran on a constant called SPEND_BY_CATEGORY (post 5). This is the post where that finally became real. It's the biggest single feature in the project so far - the first commit alone is a large one - and most of the interesting decisions are about what not to store.
The shape: three sources, one precedence order
I wanted to
House-move inventory: A REST API whose docs can't go stale - Part 4
~7 min read
With the core in place (post 2), the REST API was the easy bit. FastAPI handlers are about six lines each. The interesting problem, and the one that took more commits than the endpoints themselves, was keeping the documentation true.
The thin layer
inventory.api maps HTTP to service calls:
* Pydantic request and response models in api/schemas.py, distinct from the core's dataclasses, so the wire format can evolve without touching the domain. Request bodies are extra="forbid": a
Marketplace Listing Helper: A list that lives in the browser - Part 3
~7 min read
The extension needs to remember what I'm selling. With no server allowed, the list has to live in the browser, and it has to move to and from a spreadsheet, because that is where I keep my stock. This post is the data model, and the small decisions that keep a CSV and a browser store from fighting each other.
Where it lives
Everything is in chrome.storage.local, under two keys:
* mlh_data is { headers, rows }: the column names in order, and one object per item.
* mlh_meta hold
Central login service: A two-step login wizard - Part 8
~12 min read
Part 7 closed with "that's the story to date". It wasn't quite. The login page still had one awkward detail from the very first version: a single form with three fields, the last of them labelled "2FA code (only if you have it enabled)". Everyone, including people who have never turned on two-factor, saw a box asking about something that didn't apply to them.
This post turns that form into a small wizard. Step one asks for an email and password. Step two, a code from your authenti