The client asked for two things on the running-cost pages: drop the two prose
notes, and show the year-to-date figure next to the total for the current month.
- /running loses the "Shown to Jan, Feb..." and "A re-cut of costs..."
paragraphs. The tiles carry the year and the month instead.
- /running shows "Nov so far" beside "YTD running cost", and the "made of"
line is printed for the month as well as for the year, so the month's total
is explained on the page and not only in the grid.
- The cards follow the income cards: YTD spend | Nov so far side by side from
md, the month on its own line below md, and the sparkline yields its width
below xl so the vehicle NAME is never truncated to pay for a decorative
trend. Below sm the name wraps instead of being cut short.
- /running/insurance shows the premium in force this month, and its
year-to-date figure now stops at the months that have happened. December is
a rate on file, not money spent, so including it made this page a full year
while the Overview showed eleven months - the same cost kind reading as two
different numbers on two screens.
- /running/financing shows "Committed for Nov" beside a new year-to-date
instalment figure.
One rule, three screens. elapsedMonths() in lib/income-month.ts counts the
months that have happened and is the same clock the income pages use, and
financeByAsset() / fleetFinanceByMonth() in lib/running-costs.ts are the only
finance rule the Overview and the financing register both read. The Overview's
insurance and finance year-to-date figures now equal the insurance and
financing pages' figures exactly.
lib/running-month.test.ts asserts it, including the insurance cross-check
between the register and the Overview.
A settled agreement still contributes nothing to any month - the instalments
TLB06 was paying in January-October are not in the Overview's finance figure.
The client confirmed the financing is correct as it stands, so that is left
alone and recorded in AGENTS.md rather than quietly changed.
Splits the Running Costs group into six purpose-built pages, each with its own
collection and its own source data, plus a coordinate parser for purchase orders.
Insurance (R503 330/yr, 20 vehicles)
A premium can move every month or three, so it is stored as a RATE PERIOD in
insurance_rates and each month's cost is resolved against the rate in force
that month. cost_lines insurance rows are derived from those periods. An absent
rate is null, never 0 — nobody has entered the date, which is not the same as
being insured for free.
Financing (20 agreements, R428 585/mo)
Instalment from the sheet; start date, paid-off date and the settled toggle are
human-entered and preserved across re-seeds. Months-left and outstanding are
null until a date is entered, never R0 on a live debt. The two paid-off
agreements (WB02, DT01) show as settled and are excluded from the run-rate.
Purchases + Oil Stock
Uploads PO PDFs the way /income/invoices does. The parser reads columns by
x-position, because plain text extraction reorders them, and refuses any PO
whose lines do not sum to the sub-total. A cost code either names a machine
("SS-02" -> SS02) or a stock store ("MPLANT, ENGOIL") — never both, never
guessed. Oil POs move litres into oil_stock_movements instead of onto a machine;
stock is the sum of movements so the first usage is a subtraction from history.
Operators (19) and Mechanics (2)
Both are people with an employee number, separately reconcilable by the seed so
someone added in the app survives. Mechanics get a badge QR; a mechanic with no
employee number cannot be printed, because the code is what the QR encodes.
Cross-cutting
- The seed no longer deletes operators, mechanics or oil items: people are added
in the app, and those rows exist in no file.
- Re-pointing allocations that PocketBase clears because `assets` is rebuilt
with new ids each seed (operators, purchase lines).
- One QR encoder for screen and PDF, so a printed code cannot drift from the one
on screen. pdf-lib writes real vector PDFs; pdfjs-dist only reads them.
- Every figure is null-safe: unknown is never rendered as zero.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>