# WFDASH — What We've Shipped > **The one-month build log.** 24 May → 22 Jun 2026. > 15 commits · ~30K lines of real code · ~57K net lines added (incl. lockfile churn). One Next.js 16 CRM that took a legacy Django app and gave it AI, an email pipeline, and a real supplier map. --- ## The Project **WFDASH** is a business CRM — customers, companies, suppliers, quotes, orders, driver collection pool, delivery notes, repairs. It's a re-platform of an older Django app, rebuilt on a modern stack: | Layer | Choice | |---|---| | Framework | Next.js 16 (App Router) + React 19 + TypeScript | | UI | shadcn/ui (base-nova style) + Tailwind 4 + Lucide | | Maps | Leaflet + react-leaflet (clustering, marker edit, fullscreen, dark tiles) | | Backend | PocketBase (DB + auth + file storage + realtime) | | Automation | n8n (email monitoring, AI quote rewrite) | | Agent API | in-app JSON-RPC MCP endpoint + REST `/api/v1/*` | | Packaging | Multi-stage Docker (Node 20 alpine, non-root) + Nixpacks | | Tests | Vitest (unit) + Playwright (E2E) | > **North star:** *"One scheduler, one workflow, one source of truth — no long-running process in the Next.js app, no racy counters, no fabricated cost prices."* --- ## The Timeline At A Glance ``` 24 May ─── Foundation sprint (UI kit, PWA, PocketBase collections) 25 May ─── PocketBase APIs + migration docs 27 May ─── Supplier map (Leaflet) ships 11 Jun ─── Test scaffolding (Vitest + Playwright + line endings) 16 Jun ─── Email-to-quote pipeline (v3.1 agentic + PDF text) 22 Jun ─── Big batch: atomic numbering → email cards → AI rewriter → pricing lock → UX polish ``` **By the numbers:** - **15 commits**, ~57K net lines added (78K insertions / 22K deletions; ~27K of that is real code in `src/`) - **29,582 lines** currently living in `src/`, `n8n_workfows/`, `scripts/`, `docs/`, `e2e/` - **5 n8n workflow files** (Quote Email Monitor v3.1 → v4 → v5, Quote Item Rewriter, Error Handler) - **14 PocketBase collections** (12 from setup + 2 added with the email pipeline) - **9 REST endpoints** under `/api/v1/*` + **8 MCP tools** under `/api/mcp` - **15 Vitest unit suites** + **3 Playwright E2E specs** The MCP tools list (the AI agent uses these): `orders.list` · `orders.create_from_quote` · `collection_pool.list` · `order_items.update_status` · `quotes.create_from_email` · `quotes.append_reply` · `quotes.find_by_message_id` · `attachments.upload_for_quote` --- ## Phase 1 — Foundation (24 May) **Commit `363786e` — UI kit shipped** The bones of the app. shadcn/ui components that the rest of the build rests on: - `separator`, `sheet`, `sonner`, `switch`, `table`, `tabs`, `textarea`, `tooltip` - `data-table.tsx` — generic table with sort/filter/pagination (became the workhorse for every list page) - `line-items-table.tsx` — quote & order line items **Commit `a828432` — Responsive & accessibility pass** Dashboard and quotes pages get a proper mobile-card layout, keyboard nav, and ARIA labels. **Commit `954dc26` — PWA scaffold** Service worker, offline page, manifest. The app installs to a phone home screen and survives flaky connections in the warehouse. --- ## Phase 2 — Backend Wiring (25 May) **Commit `fd1571e` — PocketBase collections, APIs, migration docs** The schema + the agent surface area come online together: - Base collections: `customers`, `companies`, `suppliers`, `quotes`, `quote_items`, `orders`, `purchase_orders`, `order_items`, `collection_pool`, `delivery_notes`, `delivery_note_items`, `repairs`, `stock_items` - `scripts/setup-pocketbase.ts` — one-shot setup from `npm run setup` - MCP endpoint (`/api/mcp`) with `tools/list` + `tools/call` - REST endpoints under `/api/v1/*` (auth via `Authorization: Bearer AGENT_API_KEY`) - Migration docs: `core-recheck-matrix.md`, `django-nextjs-parity-backlog.md`, `order-workflow-rules.md` --- ## Phase 3 — Supplier Map (27 May) **Commits `565b1a6` → `54644c7` → `83935c9` → `a6ac88a`** A real, interactive supplier map. Five commits in one day because each fix surfaced the next: | Commit | What landed | |---|---| | `565b1a6` | Leaflet + react-leaflet, supplier markers, clustering | | `54644c7` | Better clustering algorithm + readable labels | | `83935c9` | Dark tile layer + fullscreen mode + click-to-edit marker coordinates | | `a6ac88a` | Node 20.9.0 enforced via `engines` + `nixpacks.toml` (so CI builds don't surprise us with Node 18) | Why it matters: the warehouse team can see supplier locations at a glance, click any pin to fix a wrong coordinate, and the map works in dark mode in the cab at night. --- ## Phase 4 — Test Scaffolding (11 Jun) **Commit `c4e1c62` — Standardize line endings + full test + config scaffolding** A boring-but-critical commit: - Vitest + jsdom + Testing Library, with path aliases matching `tsconfig.json` - Unit suites for: `cn()`, `quote-status`, `order-item-status` (incl. legacy `driver_status` alias), `agent-auth`, `orders-service`, `pocketbase-admin`, every `/api/v1/*` route, every MCP tool - Playwright E2E: health probe, agent auth (skips if no API key), login (skips if no creds) - VSCode workspace + extensions + MCP config examples - `.gitignore` extended for test artifacts The "skips if no creds" behaviour is intentional — fresh checkout gets `1 passed, N skipped`, never a red CI. --- ## Phase 5 — Email-to-Quote Pipeline (16 Jun → 22 Jun) **This is the headline feature.** Three workflow versions, one PocketBase schema, one atomic counter, and a long list of "no, really, it works this time" fixes. ### The flow ``` IMAP poll (quotes@wfsales.co.za, every 5 min, UNSEEN only) ↓ Parse Email + binary preservation ↓ Drop blacklists / auto-replies ↓ Is reply to existing quote? ├── yes → MCP: quotes.append_reply → terminal └── no → Carry ctx → Split Binary → Extract from File (PDF) ↓ AI Agent (MiniMax) with two tools: ├─ create_quote_tool (MCP: quotes.create_from_email) └─ upload_attachment_tool (MCP: attachments.upload_for_quote) ↓ PocketBase record + per-attachment rows linked back ``` ### Commit `41b5fc4` — Pipeline v3.1 (agentic + PDF text) Replaces the brittle v3 "Information Extractor → Code" pair with a **langchain AI Agent** that directly calls the MCP tools. PDF attachments go through `Split Binary → Extract from File (PDF) → Aggregate` so the LLM sees the line items as text. Flips `passthroughBinaryImages: false` so PNG attachments survive to the upload tool — the v3 bug where `attachment_1` was eaten by LLM vision is fixed. Side effects of this commit: - `pb_quote_attachments.collection.json` + `quote_counters.collection.json` + `quote_replies.collection.json` - Migration scripts: `create-quote-attachments-and-replies.ts`, `broaden-quote-attachments-mime-types.ts` - `scripts/n8n-v2-main.ts`, `scripts/n8n-v2-error.ts` — the n8n Code nodes lifted into TypeScript for type-checking - Smoke scripts: `counter.ts`, `counter-concurrent.ts`, `create-from-email.ts`, `upload-attachment.ts` - `src/lib/server/quotes-service.ts` — 747 lines of business logic, fully unit-tested ### Commit `3359195` — Atomic numbering + v5 catch-and-create **The bug:** in v4, by the time the workflow reached "Upload Attachment", the binary had already been stripped → silent upload failure. **The fix (v5 "Catch and Create"):** upload attachments to PocketBase *immediately* after auth (binary still fresh), then do all the rest using record IDs, then PATCH each attachment to link it to the quote. The old node is renamed `DISABLED - Upload Attachment` so it's a one-click rollback. Atomic numbering: - `/api/v1/quotes/next-number` returns `Q-YYYY-MM-NNN` - Backed by `quote_counters` collection with `findOrCreateByMessageId` dedup - Concurrent smoke test (`counter-concurrent.ts`) proves the counter doesn't double-issue under load Files in this drop: - `n8n_workfows/WFDASH — Quote Email Monitor (v5 catch-and-create).json` - `pb_quote_attachments.schema.json` - `scripts/build_v5_workflow.py` (regenerates v5 from v4) - `scripts/deploy_v5_to_n8n.py` + `scripts/verify_v5.py` A separate `WFDASH — Error Handler` workflow catches every failure in the main workflow and posts a Telegram alert so nothing falls into a black hole. --- ## Phase 6 — Quote Cards & Color Coding (22 Jun) **Commit `a18ab0f`** The first thing a user sees when they open a quote is now genuinely useful: - **`QuoteEmailCard` overhaul** — proper HTML/plain text body sanitization, automatic linkification, inline PDF/image previews, full metadata display, auth-aware attachment previews via blob URLs - **`email_received_at`** field added to `quotes` collection + types + setup script - **`/api/v1/users/lookup`** server endpoint — resolves user IDs to display names - **User color coding** (`src/lib/user-color.ts`) — stable hash → color so claimed quotes are visually grouped per claimant - **Quotes list** — tab filters reordered, claimer names shown, rows color-coded per claimant - **Quote detail page** — claimer info with user icon - **`DataTable`** — now accepts per-row custom class + style props (powers the color coding) --- ## Phase 7 — AI Quote Item Rewriter (22 Jun) **Commit `2c6155a`** The user pastes in a wall of supplier-speak ("1 x ABC pump model XYZ-9000 c/w 2" flanged inlet, 415V, IP55 rated...") and the rewriter turns it into clean, structured line items. **What landed:** - **Backend:** `src/app/api/v1/quotes/items/rewrite/route.ts` — 228 lines, calls n8n webhook, validates response, updates PB - **Frontend:** `src/components/rewrite-quote-modal.tsx` — 716 lines, diff preview, per-item accept/reject - **n8n:** `WFDASH — Quote Item Rewriter.json` — the workflow definition - **`user_edited` field** — gates AI rewrites on user-edited content (don't overwrite what the user already cleaned up) - **Migration:** `scripts/migrations/add-quote-items-user-edited.ts` (idempotent + backfills existing items) **Bonus features that rode along:** - **`SupplierSearch` combobox** — searchable combobox using `cmdk`, replaces plain `