Jenkins shared library: Shared defaults and a package name - Part 3
~5 min read
The first version of syncBlog had its release tag written straight into the step: v0.2.0 as a default, the Ghost repository URL inside the shell snippet, and the two credential IDs as string literals. That works for one step. This post is about moving those values somewhere central, and about the package name I put them under.
Where to keep a shared value
I weighed three places:
* The default in the step. Simplest. Bump it, tag the library, and every project that doesn't pass v
Marketplace Listing Helper: Photos that don't lag or vanish - Part 6
~8 min read
The manager's photo picker had two problems. Selecting photos in the edit form was slow, and every new session started with reloading the whole photos folder. Both come down to the same thing: phone photos are large, and a browser extension can't keep hold of files it was shown once. This post is how each was handled.
The lag
The edit form shows every photo in the folder as a small tile, and you click the ones that belong to the item, in order. Clicking was visibly slow. There we
Personal finance aggregator: Hargreaves Lansdown, with no API - Part 14
~9 min read
The project's very first description listed a stretch goal: Hargreaves Lansdown, for the investment side of the picture. Post 8 assumed that meant a CSV import. It didn't turn out that way. This post covers the commits that made HL a real provider, and mostly it's about how an integration with no API, no OAuth and a real lockout risk changes the design you'd copy from TrueLayer.
What's different about HL
TrueLayer gave me an OAuth flow, a refresh token, and an API. Hargreaves Lan
House-move inventory: Schema migrations with PRAGMA user_version - Part 6
~9 min read
For the first day of this project, changing the schema meant deleting the database. That is fine until the database contains a week of real packing. The fix was a migration system, which took about a hundred lines, and then over the following weeks it taught me three things I didn't expect.
The runner
SQLite has a spare integer in the file header, PRAGMA user_version, that the engine itself never uses. That's all the bookkeeping a forward-only migration runner needs. In storage/d
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