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.
9.9 KiB
Mega Plant — Round 4
Two parts: three regressions from round 3, then the mobile standard.
The desktop layout is signed off. Do not change how any page looks at ≥1024px. Everything below is about what happens below that width, plus three data bugs.
Part A — Round 3 regressions
A.1 verify-cost-reconciliation.mjs crashes, and reports success anyway
TypeError: Failed to parse URL from /api/collections/_superusers/auth-with-password
exit=0
The script never loads .env.local, so the PocketBase URL is empty and every request
fails. It then exits 0. A script whose entire job is to catch drift currently
cannot run and claims it passed.
- Load
.env.localthe wayscripts/seed.mjsdoes. - Wrap the body in try/catch and
process.exit(1)on any failure. - Exit non-zero when the reconciliation total is off, not just when it throws.
- Run it and paste the real output.
A.2 Vehicle Costs regressed — the workbook's error was adopted
Round 3 changed Vehicle Costs from R14,996.31 to R9,673.87 by dropping
Santam versekering (R5,322.44).
That is backwards. Round 3 §B.4 said explicitly: the workbook's row-50 SUM starts one row too low and omits Santam, so the app was right and the spreadsheet was wrong. The fix was to report the spreadsheet error to the client — not to reproduce it.
- Restore
Santam versekeringto theVehicle Costssection. Correct total is R14,996.31 across 4 rows. - Add a comment in the seed and a line in the reconciliation output recording that the app deliberately differs from the workbook by +R5,322.44 here, and why.
- Never "fix" app data to match a source error that has been identified as an error. Report the mismatch instead.
A.3 The incl < excl validation isn't firing
Round 3 §B.3 asked the seed to reject any row where incl < excl or VAT falls outside
0–20%. This row is still in PocketBase:
Buildsmart Yearly update excl 14,827.91 vat 2,224.19 incl 1,421.01
incl is less than excl, which is arithmetically impossible.
- Add the validation and make it actually fail the seed.
- Confirm it trips on this row. Paste the failure output.
- Do not silently correct the value — it needs the client's confirmation (correct figure is most likely R17,052.10).
A.4 Correction to the round-3 target figure
The acceptance number in round 3 (R689,640.49) was wrong — I subtracted the R5,322.44 Santam amount twice. The workbook's grand total already excludes it via the broken subtotal.
The correct arithmetic:
Workbook grand total (Cost row 98) R1,110,486.87
less Plant Overheads (now on assets) − R415,523.94
= R694,962.93 ← reconciliation target
Current app total is R675,162.93. After restoring Santam (A.2) it becomes R680,485.37. The remaining R14,477.56 is fully explained:
| Amount | Cause |
|---|---|
| R19,800.00 | Workbook's own Personnel total (row 77 = R412,000) exceeds its two line items (157,200 + 235,000 = 392,200). Source error — ask the client. |
| −R5,322.44 | Santam, where the app is deliberately correct and the workbook is not |
- Encode
R694,962.93as the target in the reconciliation script, with both reconciling items listed explicitly so the delta is expected, not mysterious.
A.5 The "Lines" view draws filled areas, not lines
The toggle works and all three series now render with the validated colours — good.
But the "Lines" view uses recharts-area-curve with translucent fills, so the amber
Fixed area paints over the teal Income area.
The client asked for this view specifically because it matches their spreadsheet, and the spreadsheet draws lines.
- Make the "Lines" view actual lines — stroked, no fill. It is the view whose whole purpose is matching the workbook.
- Keep fills in "Cost stack", where stacking is meaningful.
Part B — The mobile standard
The client works from a phone. Desktop is signed off; mobile is not. Below is the definition of done — apply it to every page that exists now and every page added later.
B.1 The hard rule: no page scrolls sideways
A page may never be wider than the viewport. Individual tables may scroll inside their own container; the page may not.
Measured at 375px right now:
| Page | Page width | Over by | |
|---|---|---|---|
/income |
549px | +174 | ✗ |
/charts |
406px | +31 | ✗ |
/overheads |
397px | +22 | ✗ |
/running |
389px | +14 | ✗ |
/expenses |
383px | +8 | ✗ |
/ |
375px | 0 | ✓ |
/plant |
375px | 0 | ✓ |
Five of seven pages fail. Run this in the console on every page at 375px — it must
return false everywhere:
document.documentElement.scrollWidth > document.documentElement.clientWidth
The usual cause is a flex … justify-between header whose right-hand element can't
shrink. /income is the worst: the "YTD Income" gradient card and the
Sort: Profit | Income control both hang off the right edge.
- Fix all five. Headers wrap to a second row on small screens rather than overflowing.
B.2 Content must stay readable, not just fit
On /income at 375px the machine names are truncated to a single character —
"C…", "D…", "I…". The code badge and profit badge hold their width while the name
column collapses to nothing. A card you can't identify is worse than no card.
- Below
sm, stack the card contents: name on its own full-width line, badges on the line beneath. Never truncate the name to less than ~20 characters. - Same review for
/runningand the charts cards.
B.3 Filters must not bury the data
/plant has 18 type-filter pills. At 375px they wrap to 13 rows and consume the
entire first screen — you scroll past every pill before reaching a single row.
- Below
lg, collapse the type filter into a<select>dropdown, or a horizontally-scrolling single row of pills. Not a 13-row block. - Same for the
Sort:control on/incomeand the view toggle on/charts.
B.4 Wide tables: scroll inside the card, with an anchor column
/plant does this right — the table is 917px inside a 375px overflow-x-auto
container, so the page itself doesn't move. Keep that pattern. But scrolling six
columns of financials on a phone is still hard work.
- The first column (Code, or Description on expense pages) stays pinned with
position: sticky; left: 0and a background, so you always know which row you're reading. - Add
-webkit-overflow-scrolling: touchand a visible affordance that the table scrolls — a fade on the right edge is enough. - On expense pages, consider hiding
GL AccountandSupplierbelowmd. Description and the three money columns are what matter on a phone.
B.5 Tap targets and text size
Measured on /charts at 375px: 19 interactive elements under 44px tall.
On / : 10 text elements under 11px.
- Every
button,a, andinputis at least 44×44px on touch. Keep the visual size if you like — add padding to the hit area. - No text below 12px on mobile. The
text-[10px]labels need bumping.
B.6 Charts on a phone
- The legend on
/chartsis cut off at the right edge ("Income · Fixed · Ru…"). Wrap it below the title on small screens. - Charts need a minimum height on mobile — squashing to 176px with 8 x-labels makes them unreadable. Go taller on narrow screens, not shorter.
- The machine card grid must be a single column below
sm.
B.7 The PWA angle
The app is meant to install to the home screen, so mobile isn't a nice-to-have — it's the primary surface.
- Verify the installed standalone app at 375px, not just Safari/Chrome. Standalone has no browser chrome, so sticky headers and safe-area insets behave differently.
- Add
viewport-fit=coverand useenv(safe-area-inset-*)padding on the sticky header and the mobile drawer, so content clears the notch and home indicator.
B.8 How to verify — do this, and report it
Do not report mobile work as done without this. For every route
(/, /plant, /income, /running, /charts, /overheads, /expenses, and each
/expenses/*):
- Set the viewport to 375×812.
- Run the overflow check from B.1 — record the number.
- Screenshot the page.
- Count tap targets under 44px and text under 12px.
- Repeat at 768×1024 (tablet).
- Paste a table of route → overflow → tap-target count for all routes.
- Attach 375px screenshots.
- Any route that still overflows is not done.
Standing rule for future work
Add this to the project's AGENTS.md so it applies to every future change:
Every page must pass the mobile checklist before it is considered complete: no horizontal page scroll at 375px, no text under 12px, no tap target under 44px, wide tables scroll inside their own container with a sticky first column, and filter controls collapse rather than stacking into many rows. Verify at 375×812 and 768×1024 and report the measurements — do not assert it from reading the code.
- Create
AGENTS.mdwith that rule if it doesn't exist.
Acceptance criteria
npm run typecheckexits cleannpm run buildsucceedsverify-cost-reconciliation.mjsruns, and exits non-zero when it fails- Vehicle Costs is back to R14,996.31 with Santam included
- Seed rejects the Buildsmart row; failure output pasted
- Reconciliation targets R694,962.93 with both deltas itemised
- "Lines" view draws lines, not filled areas
- Zero routes report horizontal page overflow at 375px
- No tap target under 44px; no text under 12px
- Wide tables scroll in-container with a sticky first column
- Verification table + 375px screenshots pasted for every route
- Desktop (≥1024px) is visually unchanged — palette, roundness, gradients, spacing, dark mode all identical to today