Six of the 37 blocked notes fail on one pair of units. Concrete pipes are delivered by the pipe and ordered by the metre: the slip reads "U PIPES-450 X 2.5(100D) | 34 ea", the order line reads "IJ Pipes 450x2.5 100D | 222.5 m". There is no ea -> m conversion — one "each" is not a fixed number of metres — so units.ts refused it, which was the right answer to the wrong question. The missing number is a property of the product and it is printed on both documents: the 2.5. The rule only fires when the SLIP and the ORDER independently state the same length. Two documents written by different people agreeing on 2.5 is the standard the rest of this pipeline already holds to — gross minus tare equals nett, qty times rate equals amount. One description alone would not do, because a misread digit would multiply what we pay. The order corroborates the reading: five of its six pipe lines are exact multiples of 2.5 m — 222.5, 237.5, 150, 27.5 and 252.5 are 89, 95, 60, 11 and 101 pipes. Those figures are not lengths by coincidence. units.ts stays the closed table it was built as. It is tried first and its answer is final when it has one; only a pair it genuinely cannot express reaches the pack rule, so nothing that converted before converts differently now. The bore floor (>= 100) and length ceiling (<= 20 m) keep it off thread pitches and panel dimensions, and a disagreement between the two documents falls through to the existing failure rather than picking a side. Wired through convertLineQty so the check and the ledger cannot diverge: checks.ts and dn-item-row.ts both call it, for the same reason resolveNoteLines exists. qty_plausible now judges the converted figure, which is what makes it bite — 400 pipes is 1000 m against 222.5 ordered and blocks, where the raw count of 400 would have passed. Mutation-tested: disabling the fallback fails all three integration tests (the check, the ledger row, and the plausibility bound). 993 tests, typecheck clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|---|---|---|
| .claude | ||
| .freebuff | ||
| .opencode/openwork/inbox/chat-attachments | ||
| .playwright-mcp | ||
| docs | ||
| public | ||
| RAW_DOCS | ||
| scripts | ||
| src | ||
| .dockerignore | ||
| .env.coolify | ||
| .env.local.example | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| cooler.env | ||
| COOLIFY.md | ||
| Copy of THE HILLS WAGES HRS AUGUST 2026.xlsx | ||
| deployment-vptid1uo0jnz2q4t1mcbqjj9-all-logs-2026-08-19-00-23-06.txt | ||
| docker-compose.yml | ||
| Dockerfile | ||
| fuel_logs_duplicates_2026-09-10.csv | ||
| next.config.js | ||
| package-lock.json | ||
| package.json | ||
| postcss.config.mjs | ||
| README.md | ||
| tsconfig.json | ||
| vitest.config.ts | ||
MEGA INFRASTRUCTURE — Operations Dashboard
A Next.js 16 + TypeScript + Tailwind v4 operations dashboard for a South African civil construction company, backed by PocketBase. Tracks, per construction site: contracts, employees & timesheets, vehicles (fuel + plant), fuel logs, plant hour meters, fixed costs, income claims, purchase orders + line items, and delivery notes with OCR-driven receipting. Includes an LLM chat ("Ask") with a read-only tool layer over the same data, and a read-only MCP server.
Default login PIN is
2468. ChangeNEXT_PUBLIC_APP_PINin production. Append?demo=1to any URL to bypass the PIN during development.
Quick start
cp .env.local.example .env.local
# edit NEXT_PUBLIC_POCKETBASE_URL, POCKETBASE_ADMIN_*, N8N_*
npm install
npm run dev # next dev -H 0.0.0.0 -p 6004 --webpack
Production build / start:
npm run build # icons + og-image + next build --webpack
npm run start # next start -H 0.0.0.0 -p 3000
Docker / Coolify: see docs/COOLIFY.md and the
Dockerfile.
MCP server (stdio, read-only):
npm run mcp # uses the PocketBase creds in .env.local
Modules
The sidebar lists seven modules. Each is documented in
docs/modules.md.
| Path | Module | What it shows |
|---|---|---|
/dashboard |
Overview | KPI tiles, fuel-attention rail grouped by site, every site summary |
/dashboard/sites and /dashboard/sites/[id] |
Sites | Per-site profitability, burn, P&L, payroll, weekly & monthly trends |
/dashboard/employees |
Workforce | Per-employee hours, rates, weekly trend, casual-matrix import |
/dashboard/plant and /dashboard/plant/[vehicleId] |
Plant & Equipment | Hour-meter history, weekly logs, billable rate |
/dashboard/fuel and /dashboard/fuel/[vehicleId] |
Fleet & Fuel | Variance per log, over-limit flags, dismissable attention |
/dashboard/purchasing and /dashboard/purchasing/[poId] |
Purchasing | Every PO, DN fulfilment %, OCR exception inbox |
/dashboard/chat |
Ask | Read-only LLM chat with the same metrics the dashboard uses |
Documentation
| Document | What's in it |
|---|---|
docs/architecture.md |
Runtime topology, the two PB clients, rendering model, auth model, build/deploy |
docs/data-model.md |
Every PocketBase collection, fields, relations, the project_code denormalisation, indexes, API rules |
docs/modules.md |
One section per dashboard module — what it reads, computes, lets the user do |
docs/data-flows.md |
End-to-end ingestion pipelines with sequence diagrams (timesheets, fuel, plant, PO, DN) |
docs/api-reference.md |
Every API route — method, auth, request/response, error cases |
docs/operations.md |
Env vars, scripts, tests, feature flags, deploy / smoke test |
docs/multi-site-assessment.md |
The main deliverable — will this app hold up at 50 sites? Where does it break, and what to fix in what order? |
The docs/superpowers/ directory has older design notes; treat as
historical, not authoritative.
Tech stack
- Next.js 16 (App Router, modified build — see
AGENTS.md) - React 19 + TypeScript 5.7
- Tailwind v4 (via
@tailwindcss/postcss) - PocketBase 0.27 (PocketBase v0.22+ schema shape, see
scripts/pb-bootstrap-schema.py) - SheetJS (xlsx) + PapaParse (csv) for imports
- Recharts for charts
- Vitest for tests
@ducanh2912/next-pwa(Webpack-only —next dev/buildpass--webpackto support it)- Anthropic or MiniMax as the chat LLM provider
API surface (summary)
| Route group | Auth | Notes |
|---|---|---|
GET /api/health |
none | Docker healthcheck |
POST /api/sites, PATCH /api/sites/[id], POST /api/sites/[id]/fixed-costs |
server-only | Sites CRUD |
POST / PATCH / DELETE /api/vehicles[ /[id]] |
server-only | Vehicles CRUD (unified fuel + plant) |
POST /api/plant-hours/import, GET /api/plant-hours/template, GET /api/plant-hours/exception-report/[batchId], DELETE /api/plant-hours/[id] |
server-only | Plant hours ingest + reporting |
POST /api/timesheets/import |
server-only | Timesheet ingest (three formats auto-detected) |
POST /api/fuel-logs/import |
server-only | Fuel log ingest |
POST /api/purchase-orders/upload, POST /api/purchase-orders/process-batch (n8n), GET /api/purchase-orders/by-number/[poNumber]/items, PATCH /api/purchase-orders/[id]/supplier |
server-only / x-ingest-secret |
PO ingest pipeline |
POST /api/delivery-notes/upload, GET /api/delivery-notes/[id]/slip (n8n), POST /api/delivery-notes/[id]/ocr-result (n8n), POST /api/delivery-notes/[id]/assign-po |
server-only / x-ingest-secret |
DN pipeline + manual fallback |
GET / POST /api/data-import-batches/[id]/status|retry |
server-only | Batch lifecycle |
POST /api/records/[collection]/[id]/verify |
server-only | Verify toggle for POs and DNs |
POST /api/chat |
ENABLE_CHAT=true (no user auth) |
SSE chat stream |
Full reference: docs/api-reference.md.
Tests
npm run typecheck # tsc --noEmit
npm run test:run # vitest run
The metric helpers under src/lib/metrics/*.test.ts pin every
computation the dashboard exposes. Run after any change to the
financial roll-up.
License
Proprietary. Internal use only.