# 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 `