# This is NOT the Next.js you know This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. # PocketBase select fields are not self-service The `order_items.item_status` field is a `select` with a fixed list of values. If you want to set a new value (e.g. `delivery_note` for the new delivery-note flow), you must add it to the schema in the PB admin **first** — the API will 500 with "Failed to update record." otherwise. The local `pb-exports/pb_schema-2.json` is a snapshot for reference only; the source of truth is the running PB instance. If a new code path sets a select value that doesn't exist yet, ask the user to add it via the PB admin before debugging the API. They do this manually (it's a UI-only change in the PB admin). # Reuse existing multi-file fields for new attachment types The `order_items` collection has a multi-file field `order_item_attachements` (`maxSelect: 10`, accepts any MIME). When adding a new document type to attach to an order item (e.g. delivery note PDF), upload the file to this field rather than creating a new attachment collection. The operator queries by some key field on the item (e.g. `delivery_note_number`) to find the items, then reads the attachments off each item. Pattern in code: 1. POST endpoint: generate the PDF, upload via multipart/form-data using the `order_item_attachements` field on each item in the group 2. GET endpoint: query `order_items` by the key field, find a file whose name matches the expected pattern (e.g. `DN26071401.pdf`), stream the bytes back via PB's `/api/files/...` URL 3. Stable filename per document type so the retrieval side can disambiguate from other attachments on the same item # File URLs go through `/api/v1/files/`, never `/api/files/` The WFDASH file-serving proxy lives at `/api/v1/files/{collection}/{recordId}/{filename}` (route file: `src/app/api/v1/files/[collection]/[recordId]/[filename]/route.ts`). Every `` and every fetch from a client component that opens a repair/quote/order attachment must use the `/api/v1/` prefix. `/api/files/{collection}/{recordId}/{filename}` (no `v1/`) 404s — that's the *PocketBase* convention, not a route in this app. The PB instance serves files at that path, and the WFDASH proxy forwards to it server-side, but a client that points directly at `/api/files/...` gets the Next.js 404 page. **Gotcha:** PocketBase record responses (e.g. `record.delivery_note` as a `string[]` of filenames) sometimes come back as fully-qualified PB URLs like `http://pb-host/api/files/...` — those should be re-written to `/api/v1/files/...` on the client before rendering. The existing detail page builds the path from scratch from the record id + filename, which sidesteps the issue. Don't blindly trust the URL PB hands back. The `src/app/api/invoicing/delivery-note-pdf/route.test.ts` test file intentionally matches `/api/files/...` in an expect — that's asserting the *raw PB response* shape, not the URL the client should use. Different concern. # Data access layer discipline The app has two coexisting data layers: - **Legacy**: `src/lib/pb-helpers.ts` — used directly by Server Components under `src/app/(app)/**/page.tsx` and form components. - **Current**: `src/lib/server/*-service.ts` — exposed via `/api/v1/*` route handlers and `/api/mcp`, used by the agent API and external integrations. Both layers read the same PocketBase collections. Pick the right one depending on what you're doing. ## Rules **For new endpoints / features**: put business logic in `src/lib/server/-service.ts` and expose it via a thin `src/app/api/v1///route.ts`. Do NOT extend `pb-helpers.ts`. **For new pages / form components**: call `/api/v1/*` (or have the Server Component call the corresponding `lib/server/*-service.ts` function directly). Do NOT import from `pb-helpers.ts`. **When touching a page that already uses `pb-helpers.ts`**: migrate its data calls to the service layer in the same commit. One resource at a time, no big-bang rewrites. Lowest-risk order: customers → orders → quotes → suppliers → companies → everything else. ## How to spot you're reaching for the legacy layer `pb-helpers.ts` is the file in question. Grep for it: ```bash grep -rn 'from "@/lib/pb-helpers"' src/ --include="*.ts" --include="*.tsx" ``` Each hit is one caller to migrate when you're in the area. ## Migration cost indicator A single page rewrite is typically: replace `getX(...)` import + call with `await fetch('/api/v1/x?...')` (or a direct service-layer call), then update the page to await. Half-day budget for 5 pages if no weird cross-resource joins are involved. # Audit trail (`audit_log`) is written by a PB hook, not by app code Every create / update / delete on `quotes`, `quote_items`, `quote_attachments`, `orders`, `order_items` and `delivery_notes` is recorded by `pocketbase/pb_hooks/audit_log.pb.js` (+ `audit_log_utils.js`) into the `audit_log` collection. Don't add hand-written "audit" notes for field changes — the hook already captures from → to per field. Attribution: dashboard users writing straight to PB are credited automatically. Server-side writes go through `getAdminPocketBase()`, which attaches `X-Audit-*` headers from `src/lib/server/audit-context.ts` (the browser forwards its PB token as `x-wfdash-auth`; PB verifies it). If a server path knows the actor better than the request does (MCP `actor` args), wrap it in `runWithAuditActor(...)`. n8n writes appear as "Automation" unless the workflow sends `X-Audit-Actor-Name`. Read side: `src/lib/server/audit-log-service.ts` → `/api/v1/audit/{quote|order|delivery_note}/{id}` → ``. Noisy fields (search/AI/timers) are skipped via `IGNORED_FIELDS` in the hook utils — add new auto-updated fields there. The hook files are deployed by hand into the PB server's `pb_hooks/` (like select values, it's a manual step on the live instance). # Giellie agent (`/giellie`) — tools come from the MCP, policy lives in one file The sidebar "Giellie Agent" is a MiniMax chat agent (`src/lib/server/giellie-*.ts`, routes under `/api/v1/giellie/*`, chats in the `giellie_chats` PB collection). It calls the **same MCP handler** as Hermes/n8n (`src/app/api/mcp/route.ts`), in-process — a new MCP tool is automatically available to Giellie. - Safety lives in `src/lib/server/giellie-tool-policy.ts`: `*.delete` and secret/base64 tools are denied; only tools in `READ_TOOLS` run without approval; everything else (including brand-new tools) pauses for an Approve/Reject card; email/PO sends always ask. **When you add a read-only MCP tool, add it to `READ_TOOLS`** or Giellie will ask approval for every call. - Every `/api/v1/giellie/*` route authenticates with `getBrowserSessionUser(req)` (PB token in `Authorization`). These routes hold `AGENT_API_KEY`; never expose one without that check. - Config (key, model, base URL, on/off) is the existing `/settings/ai` row; Giellie uses its own system prompt in `giellie-system-prompt.ts`. - Classification is **args-aware**: use `classifyToolCall(name, input)` (not bare `classifyTool`) for routing. A plain write whose input carries a contact-routing key (`email*`, `bcc`, `cc`, `sender`, `recipient`, any depth ≤ 4) is escalated to "confirm". `quotes.re_run_ai` is denied (it wipes every quote item); `orders.cancel`, `delivery_notes.cancel` and `delivery_notes.combine` always ask. - The "Trust this chat" toggle is **per chat** and only auto-approves ordinary writes. It never covers confirm-class actions: email/PO sends, cancels/merges, and changes to email/contact fields. - Env: `AGENT_API_KEY` (server-side MCP auth, required) and, in containers, `AGENT_API_BASE_URL` (a full `https://` origin) so the MCP's loopback calls reach the app; scheme-less values like `localhost:4001` are ignored. - The `giellie_chats` collection is created by `npm run pb:migrate:giellie-chats` (manual step on the live PB, like select values and pb_hooks). Its API rules are stricter than the original spec: users may list/view their own chats only; create/update/delete are null (all writes go through the admin client in `giellie-chat-store.ts`). # AI quote assistant — fills unpriced quote lines from the POS Runbook: `docs/quote-assistant.md`. Files: `src/lib/server/quote-assistant-{core,shortlist,ai,prompt,service}.ts` (logic, POS shortlist, MiniMax call, default prompt, orchestration) and `src/lib/quote-assistant-{review,save}.ts` (client-safe review/save helpers). Routes: `POST /api/v1/quotes/assistant/tick` (n8n, `assertAgentAuth`), `POST /api/v1/quotes/{id}/assistant/run` (browser), MCP `quotes.assistant_fill`. Settings live in Settings → AI. - A line is **pending** (purple) only when `ai_fill.status === "filled" && !ai_fill.reviewed_at && pricing_source === "ai_pos"` (`isAiFillPending`). - AI writes **never set `is_processed`**, never change quote status, never send, and never touch a line with a price, `user_edited` or `is_processed`. - `pricing_source "ai_pos"` must survive human saves: route every save through `nextPricingSourceOnSave` / `withAiReview`, or a stale page turns the line back to `manual`. - Manual live steps (like select values and pb_hooks): run `npm run pb:migrate:quote-assistant` against the live PB before deploying, then activate the n8n workflow "WFDASH Quote assistant tick" (every 5 min, POSTs the tick route). See the runbook.