mega-plant/HANDOFF-PROMPT-4.md
Mega Plant 0516220110 Round 4: regressions fixed + mobile standard
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.
2026-09-12 12:05:43 +02:00

243 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.local` the way `scripts/seed.mjs` does.
- [ ] 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 versekering` to the `Vehicle Costs` section. 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.93` as 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:
```js
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 `/running` and 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 `/income` and 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: 0` and a background, so you always know which row
you're reading.
- [ ] Add `-webkit-overflow-scrolling: touch` and a visible affordance that the
table scrolls — a fade on the right edge is enough.
- [ ] On expense pages, consider hiding `GL Account` and `Supplier` below `md`.
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`, and `input` is 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 `/charts` is 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=cover` and use `env(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/*`):
1. Set the viewport to **375×812**.
2. Run the overflow check from B.1 — record the number.
3. Screenshot the page.
4. Count tap targets under 44px and text under 12px.
5. 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.md` with that rule if it doesn't exist.
---
# Acceptance criteria
- [ ] `npm run typecheck` exits clean
- [ ] `npm run build` succeeds
- [ ] `verify-cost-reconciliation.mjs` runs, 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**