The R 100 000 "Maintenance" section never had a home. On the workbook's Cost
sheet it is cell I97 — an orphaned figure with no section header above it and
no breakdown under it, sitting inside the sheet's own grand total of
R 1 110 486,87. Round 3 gave it a section, a card, a page and a sidebar entry
(7194e78). It was never a cost: nothing was ever attached to it.
The client ruled on 2026-11-05 that the app works from LIVE data and the
workbook is only history. So Maintenance comes out completely:
- the live PocketBase row (section "Maintenance", R 100 000) is deleted
- the seed row is removed from data/source/overheads.json, so a re-seed
cannot bring it back
- the card entry, the /expenses/maintenance page and the sidebar link
are removed
- data/source/overheads-extra.json — a DEAD file nothing imported (zero
references anywhere in the repo; the seed reads overheads.json
explicitly and only globs invoice-*.json) holding the round-2
Insurance/Tracking sections plus a mislabelled duplicate
"{ section: Cash Floats, description: Maintenance, incl: 100000 }" —
is trashed
Both pages now read the same money: /expenses and /overheads both show
R 580 485,37, over 6 categories and 25 lines. The vehicle/overhead split
under the headline is R 143 326,36 tied to a vehicle and R 437 159,01
company overheads.
The reconciliation against the workbook gains a third itemised delta, and it
is a SUBTRACTION this time, because the workbook's grand total still counts
the orphan cell:
R 1 110 486,87 workbook grand total (Cost sheet row 98)
- R 415 523,94 machine instalments — they live on each asset
- R 19 800,00 the workbook's own Personnel total is R 19 800 too high
+ R 5 322,44 Santam — the app keeps it, the workbook's subtotal skips it
- R 100 000,00 Maintenance — an orphan cell, removed from the app
= R 580 485,37 the roll-up
scripts/verify-cost-reconciliation.mjs now carries KNOWN_MAINTENANCE_REMOVED
and still exits 0 with R 0.00 unexplained. The note on /overheads is
generated from the constants, never typed, so it cannot drift. The three
signs matter: Personnel comes OFF, Santam goes back ON, Maintenance comes OFF.
AGENTS.md records the live-data ruling and renames the section heading from
"the cards are built FROM the sheet" to "FROM THE ROWS", since the sheet is
history and the rows are the truth.
The Subscriptions VAT errors (Charl - CCS, Buildsmart Yearly update) are
untouched — still flagged, still failing the seed loudly, still the client's
to fix.
Verified: typecheck clean; 162 tests (156 pass, 0 fail, 6 skipped);
production build clean and /expenses/maintenance gone from the route list;
reconciliation exits 0 with R 0.00 unexplained; /expenses and /overheads
both read R 580 485,37 at 375x812 with no overflow and no dead links.
/plant and /plant/map import Fuel Logs CSVs by exact QR and show each
vehicle's current and previous distinct site with the dates they were
observed.
- plant_site_observations is durable, QR-keyed history. It is never wiped
and a previous site is never manufactured.
- An older or repeated snapshot must not move a vehicle backwards; untimed
conflicting sites on one day are held for review.
- A manual site change shares the same history and the same lock.
- Current/previous metadata and the sticker QR are captured BEFORE the asset
rows are deleted in the seed, so a re-seed cannot wipe them. Explicit
empty values are preserved too: clearing a site is a deliberate manual
observation, not permission to restore an old sheet value.
- Imports change location metadata only. They never touch income, cost or
fuel measurements.
- A new named site is created only for a matched Plant vehicle. Missing
coordinates mean "awaiting a pin", never a headquarters fallback.
Owner-confirmed 2026-11-04: TLB06's QR is 1089, not the old meter reading
4704. lib/plant-site-policy.json holds the confirmed aliases (Equestria
Bastion Job -> Equestria 215, Cosmo City / Lion Park -> Cosmo City); no
other identity is guessed.
- Job cards and Fuel: vehicle search above a compact camera
- Fuel: optional "who is fuelling" employee search, recorded in Booked by
- Remove the codeless "Diesel Truck" / "Water truck" register rows
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
Single line, TLB07-CASE 570 ST at 87.00 Hr x R280.00 = R24,360.00 ex VAT,
plus R3,654.00 VAT (15%) for R28,014.00. Escalation is zero, as on every
invoice in this run.
Extracted by coordinate and confirmed against the rendered page.
TLB07 is also on PHI 171 (Cosmo City) over the same period, which the
multi-site ruling covers, so both book to TLB07.
Verified through the real buildIncomeRows: September now totals R369,055.13
across 11 machines, matching the sum of all five invoice totals, with zero
unbooked lines and every machine's month cells equal to its own YTD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fifth line set from the September 2026 plant-hire run. All five codes map
cleanly to register assets, so nothing is left unbooked this time:
MPLANT05 to SC01, RV-01-CAT to RV01, TLB06-CASE to TLB06, TP601-TATA to
TP601, TP602-TATA to TP602.
Extracted by coordinate and confirmed against both rendered pages. The
R14,548.50 column is PLUS VAT (15%), not escalation - escalation is R0.00.
SC01 and TP601 each pick up a second site this period, which the multi-site
ruling covers, so those lines book to the same machine as before.
Verified through the real buildIncomeRows: September now totals R341,041.13
across 11 machines, matching the sum of all four invoice totals, and every
machine's month cells still equal its own YTD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WIP carried over from the working tree, committed separately so it does not
sit inside the invoice work.
- inline Edit cells for code / name / type and an active toggle
- GET /api/assets/:id/impact returns the cascade counts so the client can
confirm before a delete
- DELETE /api/assets/:id hard delete; PocketBase cascade clears the dependent
income / running / cost-line rows
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an Invoices sub-menu under /income with a summary table (one row per
invoice, click to expand the full detail) and the data model behind it.
Data:
- invoices + invoice_lines PocketBase collections, created idempotently by
scripts/setup-invoice-collections.mjs
- line items transcribed from PHI 166/167/171 with per-invoice arithmetic
validation in the seed; a bad invoice is refused without blocking the rest
- invoice-*.json in data/source are auto-imported, so adding an invoice is
one file
Booking:
- manual book-in per line (Booked to), grouped by category
- POST /api/invoice-lines/:id/book moves the income row rather than adding a
second one, so a booking can be changed freely
- scripts/book-invoice.mjs is idempotent: rows are stamped with
monthly_income.invoice_line and replaced on re-run
- booked amount follows lib/invoice-booking-policy.json (bookInclusiveVat),
read by both the app and the CLI so they cannot disagree
Money:
- the R24,205.13 column on these invoices is PLUS VAT, not escalation. The
seed now rejects VAT that disagrees with the stated rate.
- booked amounts are VAT-inclusive per the client ruling of 2026-10-01; the
VAT stays visible rather than hidden.
Uploads:
- drag-and-drop PDF attached to invoices.source_pdf
- GET /api/invoices/:id/document proxies it — the collection is rule-locked,
so a direct PocketBase /api/files link 404s in a browser
Fixes a month-aggregation bug this introduced: buildIncomeRows assigned into
months[i], so a machine with two income rows in one month (one per invoice,
which is normal multi-site use) showed only the last. SS01 September read
R20,700 instead of R35,305 while its own YTD included both. Months now
accumulate, per-month profit is computed once after accumulation, and
lib/rows.test.ts pins it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Part A — Round 3 regressions:
- A.1: verify-cost-reconciliation.mjs now loads .env.local, wraps in try/catch, exits 1 on any failure
- A.2: Vehicle Costs restored Santam — back to R 14,996.31 (matches row-by-row sum)
- A.3: incl<excl / VAT math / VAT>20% now FATAL — seed exits 2 with full report (4 anomalies on Charl-CCS + Buildsmart)
- A.4: Reconciliation target R 694,962.93 with both deltas itemised (Personnel +R19,800, Santam -R5,322.44, unexplained R 0.00)
- A.5: 'Lines' view uses actual lines (recharts LineChart), no fills; fills only in 'Cost stack'
Part B — Mobile standard:
- B.1: header wraps to 2 rows on mobile; YTD badge cards stack; page-wide overflow eliminated on /income, /charts, /overheads, /running, /expenses
- B.2: IncomeView card row layout stacked — name gets its own line on mobile, never truncated to single char
- B.3: Plant type filter is a <select> dropdown below lg (was 18 pills wrapping to 13 rows); same for sort on /income and /running
- B.4: Plant table first column sticky; -webkit-overflow-scrolling: touch; tables scroll inside their own container
- B.5: All buttons/links use min-h-[44px] tap targets; text below 12px is bumped where it matters
- B.6: Charts legend wraps below title; chart min-height h-44 → h-52 on mobile
- B.7: paddingTop env(safe-area-inset-top) on sticky header for notch/home indicator clearance
AGENTS.md created with the standing mobile checklist rule.
- A.1: fixed_expenses per month now threaded through to chart bucket
- A.2: null = no data (forecast), 0 = real plotted zero — distinguishable
- A.3: x-axis clipped to last actual month (IncomeAreaChart + IncomeBarChart + MachineLineChart)
- A.4: chart hues as CSS vars (--color-chart-income/fixed/running) — deeper steps of same hues, validated for CVD
- A.5: chart-type toggle (Lines / Cost stack / Profit) + localStorage persistence + dynamic legend
- A.6: profit on charts page comes from computeProfit()
- B.1: deleted /expenses/plant-overheads, /expenses/insurance, /expenses/tracking (all empty); added /expenses/vehicles; ExpenseSectionPage now has an empty state
- B.2: captured R 157,200 Personnel block, R 100,000 Maintenance, R 10,000 Petty Cash; roll-up reconciles to R 675,162.93 (= sum of non-Plant non-Santam rows; workbook grand total R 1,110,486.87 is internally off by R 14,477.56 vs its own rows)
- B.3: seed flags incl<excl, VAT>20%, broken VAT math — does NOT silently correct; 4 anomalies reported on Charl-CCS + Buildsmart
- B.4: noted that workbook Vehicle Costs subtotal skips Santam (app R 9,673.87 is correct; workbook is wrong)
- B.2 reconciliation assertion added to verify-cost-reconciliation.mjs
- Sidebar updated: Vehicles added, Insurance/Tracking/Plant-Overheads removed