7.5 KiB
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:
- POST endpoint: generate the PDF, upload via multipart/form-data
using the
order_item_attachementsfield on each item in the group - GET endpoint: query
order_itemsby 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 - 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 <a href={...}> 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 undersrc/app/(app)/**/page.tsxand 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/<feature>-service.ts and expose it via a thin
src/app/api/v1/<feature>/<op>/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:
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} → <AuditTimeline>.
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:*.deleteand secret/base64 tools are denied; only tools inREAD_TOOLSrun 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 toREAD_TOOLSor Giellie will ask approval for every call. - Every
/api/v1/giellie/*route authenticates withgetBrowserSessionUser(req)(PB token inAuthorization). These routes holdAGENT_API_KEY; never expose one without that check. - Config (key, model, base URL, on/off) is the existing
/settings/airow; Giellie uses its own system prompt ingiellie-system-prompt.ts. - The
giellie_chatscollection is created bynpm run pb:migrate:giellie-chats(manual step on the live PB, like select values and pb_hooks).