Building Inventory System

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
Andrew Townsend
Andrew Townsend
3 min read
Building Jenkins Shared Library

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
Andrew Townsend
Andrew Townsend
2 min read
Building Marketplace Listing Helper

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
Andrew Townsend
Andrew Townsend
3 min read
Building a Childcare Calculator

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
Andrew Townsend
Andrew Townsend
4 min read
Building Finance System

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
Andrew Townsend
Andrew Townsend
5 min read
Building Inventory System

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
Andrew Townsend
Andrew Townsend
3 min read
Building Marketplace Listing Helper

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
Andrew Townsend
Andrew Townsend
3 min read
Building Identity System

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
Andrew Townsend
Andrew Townsend
5 min read
Building a Childcare Calculator

Childcare calculator: Spreading hours across the week, and the partial-week problem - Part 4

~9 min read Splitting free hours gives each childminder a weekly allocation. This post is about turning that into the day-by-day numbers on the calendar, adding a weekly breakdown, and fixing the most interesting bug in the project: a month rarely starts on a Monday. Free hours are weekly, the calendar is daily The first version applied each week's free hours chronologically: Monday's hours were free until the allowance ran out, then the rest were paid. The totals were right, but the calenda
Andrew Townsend
Andrew Townsend
4 min read
Building Finance System

Personal finance aggregator: Seeing everything in one place - Part 11

~9 min read By post 8 the app could pull balances and transactions from every linked bank. What it couldn't do was make that data legible - or hold the parts of my financial picture that no bank API will ever give me, like a car loan. This post is a run of smaller commits, spread over 28-29 September, that were each about the same thing: making the Accounts and Home pages answer "what do I actually have, and what do I owe?" without squinting. It's less a single design story than five related f
Andrew Townsend
Andrew Townsend
5 min read
Building Inventory System

House-move inventory: Money, currencies and an approximate euro - Part 3

~7 min read Every item in the inventory can have a value, and values are the part of this project where a quiet bug does the most harm: the number ends up on a customs declaration and in an insurance conversation. So the money rules were written down before the first item was added, and they have barely changed since. The rules From CLAUDE.md: * Money is Decimal end to end, and a string on the wire ("45.00"). Never float - the service actively rejects one. * New items default to GBP. * V
Andrew Townsend
Andrew Townsend
3 min read
Building Marketplace Listing Helper

Marketplace Listing Helper: Filling in Facebook's form - Part 2

~7 min read Filling in a form sounds like the easy half of the job. It's the half that breaks. Facebook's Create listing form is built from generated markup with no stable ids, its inputs are controlled by React, and its dropdowns are custom widgets. This post is about how the extension copes, and about the one rule that keeps it honest when it can't. Finding a field without an id There's nothing like #title to select. What is stable is the text a person reads, so findField() looks fields up
Andrew Townsend
Andrew Townsend
3 min read