# AGENTS.md — Mega Plant ## Vehicle sites from Fuel Logs `/plant` and `/plant/map` import Fuel Logs CSVs by exact QR and show current/previous distinct site plus observed dates. Current means last recorded site, not GPS. `plant_site_observations` is durable QR-keyed history; never wipe it or manufacture a previous site. Older/repeated snapshots must not move a vehicle backwards. Untimed conflicting sites on one day are held for review. Manual site changes share the history and lock. Current/previous metadata and QR are captured BEFORE asset deletion in seed; preserve explicit empty values too. `node scripts/setup-plant-site-history.mjs` adds the schema. `node --import tsx scripts/import-plant-sites.ts --help` documents preview and hash-bound apply. `docs/plant-site-import.md` documents the existing scheduled-download bridge. Imports change location metadata only, never income/cost/fuel measurements. New named sites are created only for matched Plant vehicles; missing coordinates mean awaiting a pin, never a headquarters fallback. Add pins in the map sidebar. Owner-confirmed 2026-11-04: TLB06 QR is **1089**, not the old meter reading 4704. `lib/plant-site-policy.json` contains confirmed aliases Equestria Bastion Job → Equestria 215 and Cosmo City / Lion Park → Cosmo City. Do not guess additional identities or aliases. ## Income by site — posted income is not billing `/income` has Vehicles / Sites views. `/sites` defaults to the SAME posted-income view, with a separate Internal billed invoices view. `buildSiteIncomeRows` in `lib/site-income.ts` is the shared calculation: actual `monthly_income` supplies the amount/year/month, linked invoice supplies its historical site. It must NEVER use `assets.site`, fuel-log locations or today's map assignment. Site detail pages show the same posted amount separately from explicitly labelled all-time billing. Unposted invoice lines are billing, not posted income. Historical workbook income without site evidence stays in Unallocated historical income. External invoices' `site` field means SUPPLIER, so their posted income stays in Unallocated external income until a separate historical-site allocation exists. Missing source references remain visible as unallocated invoice income. Do not drop these groups to make a site list look cleaner, or guess sites to force totals. No site profit is calculated without an explicit cost-allocation rule. Invoice writes use `revalidateInvoicePages`, which invalidates both registers, Income, Sites/details, Charts and Dashboard. Clearing a booking must delete its prior postings and refresh completion; site-only allocation accounts for a line but does not invent machine income. Existing billing summaries remain invoice-line-based and may differ from posted income, deliberately. ## The month on `/income` — LIVE, and decided on the server The income page shows the year-to-date figures AND the current month. `lib/income-month.ts` is the single rule that decides what "the month" is; the vehicle cards, the site cards and the tiles all read from it, so they cannot disagree. - **The month is the live CALENDAR month**, and it is shown even at R0. Client ruling, 2026-11-04: income for a month is usually entered near the END of it, so the current month is a live figure that starts at R0 and grows — the same rule the running-costs grid already follows ("The current month IS shown and is expected to grow"). A missing month is a dash; the live month is R0. - **The month index is computed on the SERVER** (`currentMonthIndex(rows, year)`) and passed down as a plain number. The browser must never work it out for itself — two clocks disagreeing would show two different months on the same page. Both `/income` and `/sites` do this. - **Once the page's year is not the current year**, the calendar month belongs to another year, so the last month that carries a figure is used instead. **A month's PROFIT is a dash until that month's costs are posted** (`monthProfit` returns `null`, never `0`). September and October carry invoice income with NO posted fixed costs — their costs exist only as forecast rows. A profit there would be income minus zero, which reads as a 100% margin and does not tie back to the year-to-date profit. It becomes a number by itself the moment the workbook posts that month's costs; no code change is needed. The month figure sits BESIDE the year figure on each card (`YTD income` | `Nov so far`), not above it. Below `md` there is no room for it beside the others, so it gets its own line. The 12-month strip shows the live month as `R 0` rather than `—`, so a card cannot contradict itself; months AFTER it stay `—` because they have not happened yet. ⚠️ **The sparkline on a collapsed card is hidden below `xl`.** The content column is only ~700px wide between `md` and `xl` (the sidebar eats the rest), and the vehicle NAME must not be truncated to pay for a decorative trend. The same 12 months are in the expanded panel's strip, one click away. ## Mobile checklist (Round 4 standing rule) Every page must pass the mobile checklist before it is considered complete: 1. **No horizontal page scroll at 375px.** Individual tables may scroll inside their own container; the page itself may not. 2. **No text under 12px on mobile.** Bump `text-[10px]` labels up. 3. **No tap target under 44×44px.** Keep the visual size if you like — add padding to the hit area. 4. **Wide tables scroll inside their own container** with a sticky first column and `-webkit-overflow-scrolling: touch`. Add a visible scroll affordance (e.g. right-edge fade). Modern Chrome does not SUPPORT that property any more (`CSS.supports` returns false), so it is inert either way. React's client renderer drops the literal string when a grid is rendered client-side (a Cards/Grid toggle), while a server-rendered grid keeps it - so the audit script can report `touchScroll: false` on a grid that is functionally correct. Judge it on the container, the sticky column and the affordance, not on that string. 5. **Filter controls collapse** below `lg` — pills become a `