fix(api,layout): sprint 500 chunk + add 9 missing top-level agent routes + metadataBase; drop unused collection_pool feature; quote counter fix migration
src/app/api/v1/sprint/attachment-counts/route.ts
- Chunk itemIds into batches of 80 before building the PocketBase filter;
the live cap was hit at ~115 IDs × 15 chars and surfaced as a
generic Next 500 ("Something went wrong while processing your
request.") that hid the real cause. 7 chunks for the sprint
page's max of 500 items.
src/app/layout.tsx
- Add metadataBase derived from NEXT_PUBLIC_SITE_URL /
NEXT_PUBLIC_POCKETBASE_URL. Silences the per-page
"metadataBase not set" warning during dev renders and gives
OG / Twitter cards a stable canonical base.
src/app/api/v1/{quotes,order-items,purchase-orders,stock,
stock-purchases,users,sprint,driver,ai-insights}/route.ts (NEW)
- Add the missing top-level collection routes the agent API
needs. seven are straight passthroughs to their PB collection;
/sprint proxies to quote_items with optional claimStatus and
category query params; /driver lists PB users (no role field
in PB so callers narrow with ?filter=); /ai-insights returns
a roll-up count + most-recent regenerate timestamp across the
four collections that carry ai_* JSON fields.
src/app/api/v1/collection-pool/{route.ts,route.test.ts},
src/app/(app)/collection-pool/{page.tsx,page.test.tsx,
view-model.ts,view-model.test.ts,components.tsx} (DELETED)
- Drop the collection_pool feature entirely: PB has no
collection_pool collection on the live instance
(pb:migrate:orders-v2 was never run), the codebase comment
in purchase-orders-service.ts says "now-removed", and the
sidebar entrypoint has been migrated to /sprint + /deliveries.
The replacement operational status is the `item_status =
"collection_pool"` enum value on order_items, which stays.
src/lib/server/orders-service.ts (+ test), src/lib/pb-helpers.ts,
src/components/sidebar.tsx, src/components/ui/assign-driver-dialog.tsx,
src/app/(app)/collection-map/sidebar.tsx,
src/app/api/mcp/route.ts (+ test) (MODIFIED)
- Remove listCollectionPool + getCollectionPool +
updateCollectionPoolStatus, the /collection-pool sidebar entry,
the deep-link in assign-driver-dialog and collection-map
sidebar, and the collection_pool.list MCP tool (the tool,
handler, and test mock).
scripts/migrations/2026-07-26-fix-quote-counter-and-reassign-numbers.ts (NEW)
- One-shot migration script that:
1. Reads every quote, sorts by created ASC.
2. Reassigns clean Q202607001 … Q202607{N} over the whole
table (including the e+22 garbage and the legacy
Qnnnnnn rows).
3. Sets quote_counters.(year=2026, month=7).last_seq = N
so the next /api/v1/quotes/next-number returns a clean,
atomic, unique number.
Default mode = dry-run; pass --apply to write.
Already executed against the live cloud PB during this session
(verified: 200/200 rows updated, counter at 200, next-number
returns Q202607201, Q202607202, Q202607203 on consecutive calls).
AGENTS.md (MODIFIED)
- Add data-layer-discipline block matching the style of the
three existing blocks. Documents which layer is legacy (pb-helpers)
vs current (lib/server/*-service.ts via /api/v1/*), gives the
migration order (customers → orders → quotes → suppliers →
companies → rest), and includes the grep one-liner future AI
agents should run before extending pb-helpers.
Not in this commit (your WIP, untouched):
src/app/(app)/{dashboard,delivery-notes,layout,orders/[id],
quotes/[id],quotes/approvals,quotes,sprint}/page.tsx,
src/app/(auth)/login/page.tsx, src/components/charts/*,
src/components/{company-form,customer-form,quote-email-card,
sprint-item-pricing-modal,supplier-form,theme-bootstrap,
theme-picker}*.{tsx,test.tsx}, src/app/api/invoicing/delivery-note/*
src/app/api/v1/quotes/[id]/pdf/route.ts, src/app/globals.css,
src/lib/{item-status-ui,pdf/quote-document,quote-status*,themes,
types}.ts, src/lib/server/{order-item-status*,quotes-service}.ts,
src/styles/themes.css, APP_CAPABILITIES.md, README.md,
docs/SHIPPED.md, scripts/smoke/screenshot-pages.mjs,
and the new untracked files under docs/, scripts/migrations/
2026-07-19…25, src/app/{(app)/deliveries,(app)/delivery-notes,
api/v1/ai-insights/rerun,api/v1/deliveries,api/v1/delivery-notes,
api/v1/quotes/[id]/partial-approve,api/v1/quotes/[id]/split},
src/components/{ai-insights-panel,ai-insights-renderer,
ai-item-history-trigger,brand-stripe,delivery-notes,ui/stock-item-search,
ui/duplicate-warning,ui/duplicate-warning.test}.tsx, supplier-form.test.tsx,
src/lib/{pb-helpers-duplicate.test,server/deliveries-service*,
server/delivery-notes-service*}, public/wfdash-logo*.png,
opencode.jsonc.
2026-07-26 20:18:49 +00:00
<!-- BEGIN:nextjs - agent - rules -->
# 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.
<!-- END:nextjs - agent - rules -->
<!-- BEGIN:pb - select - field - gotcha -->
# 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).
<!-- END:pb - select - field - gotcha -->
<!-- BEGIN:pb - attachments - pattern -->
# 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
2026-10-19 12:46:37 +00:00
**Always ADD, never replace:** a PocketBase file PATCH under the bare
field name replaces the whole list — it deleted a driver's photo on
2026-10-19. Build uploads with `addAttachmentForm()` from
`src/lib/server/order-item-files.ts` (`order_item_attachements+`, and
`-` only for the same document's older copies), and find a file by name
with `pickStoredCopy()` , never by position.
fix(api,layout): sprint 500 chunk + add 9 missing top-level agent routes + metadataBase; drop unused collection_pool feature; quote counter fix migration
src/app/api/v1/sprint/attachment-counts/route.ts
- Chunk itemIds into batches of 80 before building the PocketBase filter;
the live cap was hit at ~115 IDs × 15 chars and surfaced as a
generic Next 500 ("Something went wrong while processing your
request.") that hid the real cause. 7 chunks for the sprint
page's max of 500 items.
src/app/layout.tsx
- Add metadataBase derived from NEXT_PUBLIC_SITE_URL /
NEXT_PUBLIC_POCKETBASE_URL. Silences the per-page
"metadataBase not set" warning during dev renders and gives
OG / Twitter cards a stable canonical base.
src/app/api/v1/{quotes,order-items,purchase-orders,stock,
stock-purchases,users,sprint,driver,ai-insights}/route.ts (NEW)
- Add the missing top-level collection routes the agent API
needs. seven are straight passthroughs to their PB collection;
/sprint proxies to quote_items with optional claimStatus and
category query params; /driver lists PB users (no role field
in PB so callers narrow with ?filter=); /ai-insights returns
a roll-up count + most-recent regenerate timestamp across the
four collections that carry ai_* JSON fields.
src/app/api/v1/collection-pool/{route.ts,route.test.ts},
src/app/(app)/collection-pool/{page.tsx,page.test.tsx,
view-model.ts,view-model.test.ts,components.tsx} (DELETED)
- Drop the collection_pool feature entirely: PB has no
collection_pool collection on the live instance
(pb:migrate:orders-v2 was never run), the codebase comment
in purchase-orders-service.ts says "now-removed", and the
sidebar entrypoint has been migrated to /sprint + /deliveries.
The replacement operational status is the `item_status =
"collection_pool"` enum value on order_items, which stays.
src/lib/server/orders-service.ts (+ test), src/lib/pb-helpers.ts,
src/components/sidebar.tsx, src/components/ui/assign-driver-dialog.tsx,
src/app/(app)/collection-map/sidebar.tsx,
src/app/api/mcp/route.ts (+ test) (MODIFIED)
- Remove listCollectionPool + getCollectionPool +
updateCollectionPoolStatus, the /collection-pool sidebar entry,
the deep-link in assign-driver-dialog and collection-map
sidebar, and the collection_pool.list MCP tool (the tool,
handler, and test mock).
scripts/migrations/2026-07-26-fix-quote-counter-and-reassign-numbers.ts (NEW)
- One-shot migration script that:
1. Reads every quote, sorts by created ASC.
2. Reassigns clean Q202607001 … Q202607{N} over the whole
table (including the e+22 garbage and the legacy
Qnnnnnn rows).
3. Sets quote_counters.(year=2026, month=7).last_seq = N
so the next /api/v1/quotes/next-number returns a clean,
atomic, unique number.
Default mode = dry-run; pass --apply to write.
Already executed against the live cloud PB during this session
(verified: 200/200 rows updated, counter at 200, next-number
returns Q202607201, Q202607202, Q202607203 on consecutive calls).
AGENTS.md (MODIFIED)
- Add data-layer-discipline block matching the style of the
three existing blocks. Documents which layer is legacy (pb-helpers)
vs current (lib/server/*-service.ts via /api/v1/*), gives the
migration order (customers → orders → quotes → suppliers →
companies → rest), and includes the grep one-liner future AI
agents should run before extending pb-helpers.
Not in this commit (your WIP, untouched):
src/app/(app)/{dashboard,delivery-notes,layout,orders/[id],
quotes/[id],quotes/approvals,quotes,sprint}/page.tsx,
src/app/(auth)/login/page.tsx, src/components/charts/*,
src/components/{company-form,customer-form,quote-email-card,
sprint-item-pricing-modal,supplier-form,theme-bootstrap,
theme-picker}*.{tsx,test.tsx}, src/app/api/invoicing/delivery-note/*
src/app/api/v1/quotes/[id]/pdf/route.ts, src/app/globals.css,
src/lib/{item-status-ui,pdf/quote-document,quote-status*,themes,
types}.ts, src/lib/server/{order-item-status*,quotes-service}.ts,
src/styles/themes.css, APP_CAPABILITIES.md, README.md,
docs/SHIPPED.md, scripts/smoke/screenshot-pages.mjs,
and the new untracked files under docs/, scripts/migrations/
2026-07-19…25, src/app/{(app)/deliveries,(app)/delivery-notes,
api/v1/ai-insights/rerun,api/v1/deliveries,api/v1/delivery-notes,
api/v1/quotes/[id]/partial-approve,api/v1/quotes/[id]/split},
src/components/{ai-insights-panel,ai-insights-renderer,
ai-item-history-trigger,brand-stripe,delivery-notes,ui/stock-item-search,
ui/duplicate-warning,ui/duplicate-warning.test}.tsx, supplier-form.test.tsx,
src/lib/{pb-helpers-duplicate.test,server/deliveries-service*,
server/delivery-notes-service*}, public/wfdash-logo*.png,
opencode.jsonc.
2026-07-26 20:18:49 +00:00
<!-- END:pb - attachments - pattern -->
2026-09-01 23:17:05 +00:00
<!-- BEGIN:file - url - paths -->
# 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.
<!-- END:file - url - paths -->
fix(api,layout): sprint 500 chunk + add 9 missing top-level agent routes + metadataBase; drop unused collection_pool feature; quote counter fix migration
src/app/api/v1/sprint/attachment-counts/route.ts
- Chunk itemIds into batches of 80 before building the PocketBase filter;
the live cap was hit at ~115 IDs × 15 chars and surfaced as a
generic Next 500 ("Something went wrong while processing your
request.") that hid the real cause. 7 chunks for the sprint
page's max of 500 items.
src/app/layout.tsx
- Add metadataBase derived from NEXT_PUBLIC_SITE_URL /
NEXT_PUBLIC_POCKETBASE_URL. Silences the per-page
"metadataBase not set" warning during dev renders and gives
OG / Twitter cards a stable canonical base.
src/app/api/v1/{quotes,order-items,purchase-orders,stock,
stock-purchases,users,sprint,driver,ai-insights}/route.ts (NEW)
- Add the missing top-level collection routes the agent API
needs. seven are straight passthroughs to their PB collection;
/sprint proxies to quote_items with optional claimStatus and
category query params; /driver lists PB users (no role field
in PB so callers narrow with ?filter=); /ai-insights returns
a roll-up count + most-recent regenerate timestamp across the
four collections that carry ai_* JSON fields.
src/app/api/v1/collection-pool/{route.ts,route.test.ts},
src/app/(app)/collection-pool/{page.tsx,page.test.tsx,
view-model.ts,view-model.test.ts,components.tsx} (DELETED)
- Drop the collection_pool feature entirely: PB has no
collection_pool collection on the live instance
(pb:migrate:orders-v2 was never run), the codebase comment
in purchase-orders-service.ts says "now-removed", and the
sidebar entrypoint has been migrated to /sprint + /deliveries.
The replacement operational status is the `item_status =
"collection_pool"` enum value on order_items, which stays.
src/lib/server/orders-service.ts (+ test), src/lib/pb-helpers.ts,
src/components/sidebar.tsx, src/components/ui/assign-driver-dialog.tsx,
src/app/(app)/collection-map/sidebar.tsx,
src/app/api/mcp/route.ts (+ test) (MODIFIED)
- Remove listCollectionPool + getCollectionPool +
updateCollectionPoolStatus, the /collection-pool sidebar entry,
the deep-link in assign-driver-dialog and collection-map
sidebar, and the collection_pool.list MCP tool (the tool,
handler, and test mock).
scripts/migrations/2026-07-26-fix-quote-counter-and-reassign-numbers.ts (NEW)
- One-shot migration script that:
1. Reads every quote, sorts by created ASC.
2. Reassigns clean Q202607001 … Q202607{N} over the whole
table (including the e+22 garbage and the legacy
Qnnnnnn rows).
3. Sets quote_counters.(year=2026, month=7).last_seq = N
so the next /api/v1/quotes/next-number returns a clean,
atomic, unique number.
Default mode = dry-run; pass --apply to write.
Already executed against the live cloud PB during this session
(verified: 200/200 rows updated, counter at 200, next-number
returns Q202607201, Q202607202, Q202607203 on consecutive calls).
AGENTS.md (MODIFIED)
- Add data-layer-discipline block matching the style of the
three existing blocks. Documents which layer is legacy (pb-helpers)
vs current (lib/server/*-service.ts via /api/v1/*), gives the
migration order (customers → orders → quotes → suppliers →
companies → rest), and includes the grep one-liner future AI
agents should run before extending pb-helpers.
Not in this commit (your WIP, untouched):
src/app/(app)/{dashboard,delivery-notes,layout,orders/[id],
quotes/[id],quotes/approvals,quotes,sprint}/page.tsx,
src/app/(auth)/login/page.tsx, src/components/charts/*,
src/components/{company-form,customer-form,quote-email-card,
sprint-item-pricing-modal,supplier-form,theme-bootstrap,
theme-picker}*.{tsx,test.tsx}, src/app/api/invoicing/delivery-note/*
src/app/api/v1/quotes/[id]/pdf/route.ts, src/app/globals.css,
src/lib/{item-status-ui,pdf/quote-document,quote-status*,themes,
types}.ts, src/lib/server/{order-item-status*,quotes-service}.ts,
src/styles/themes.css, APP_CAPABILITIES.md, README.md,
docs/SHIPPED.md, scripts/smoke/screenshot-pages.mjs,
and the new untracked files under docs/, scripts/migrations/
2026-07-19…25, src/app/{(app)/deliveries,(app)/delivery-notes,
api/v1/ai-insights/rerun,api/v1/deliveries,api/v1/delivery-notes,
api/v1/quotes/[id]/partial-approve,api/v1/quotes/[id]/split},
src/components/{ai-insights-panel,ai-insights-renderer,
ai-item-history-trigger,brand-stripe,delivery-notes,ui/stock-item-search,
ui/duplicate-warning,ui/duplicate-warning.test}.tsx, supplier-form.test.tsx,
src/lib/{pb-helpers-duplicate.test,server/deliveries-service*,
server/delivery-notes-service*}, public/wfdash-logo*.png,
opencode.jsonc.
2026-07-26 20:18:49 +00:00
<!-- BEGIN:data - layer - discipline -->
# 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/<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:
```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.
<!-- END:data - layer - discipline -->
2026-09-29 23:48:44 +00:00
# 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.
2026-10-17 13:35:54 +00:00
Hooks deploy from git: commit changes in `pocketbase/pb_hooks/` , then
`npm run pb:hooks:publish` (pushes that folder to Forgejo `David/WFDASH-PBHOOKS` );
a VPS cron syncs the files listed in `DEPLOY` into the live PB hooks volume
2026-10-17 13:41:20 +00:00
(see `scripts/pb-hooks/README.md` ). The audit hooks went live on 2026-10-17;
`audit_log` has no history from before that date.
2026-09-30 22:36:55 +00:00
2026-09-30 22:19:59 +00:00
# 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` .
2026-09-30 22:36:55 +00:00
- 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.
2026-09-30 22:19:59 +00:00
- The `giellie_chats` collection is created by `npm run pb:migrate:giellie-chats`
2026-09-30 22:36:55 +00:00
(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` ).
2026-10-16 18:09:38 +00:00
# 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.
2026-10-24 21:09:53 +00:00
The POS matcher is **shared** and **size-aware** : `src/lib/server/pos-match.ts`
(the candidate pool, the size-aware ranking and `pickPosItems` ) and
`src/lib/item-size.ts` (the size parser) are used by the quote assistant,
the order fill and the blue price check. `quote-assistant-shortlist.ts`
delegates to it.
2026-10-16 18:09:38 +00:00
- A line is **pending** (purple) only when `ai_fill.status === "filled" && !ai_fill.reviewed_at && pricing_source === "ai_pos"` (`isAiFillPending`).
2026-10-24 21:09:53 +00:00
- A price is written **only for an exact match** . A closest match writes no price and records `ai_fill.closest` (the nearest item + what differs); the badge then reads "Closest: …". The AI answers **exact** , **closest** or **none** per line.
2026-10-16 18:09:38 +00:00
- 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.
2026-10-17 07:55:44 +00:00
2026-10-24 11:06:51 +00:00
# AI order assistant — matches orders to the quote they came from
Runbook: `docs/order-assistant.md` . Files: `src/lib/server/order-assistant-{core,ai,prompt,service}.ts`
(eligibility + auto-link rule, AI call, default prompt, orchestration) and
`src/lib/server/order-quote-link-service.ts` (the deterministic shortlist). Routes:
`POST /api/v1/orders/assistant/tick` (n8n, `assertAgentAuth` ),
`POST /api/v1/orders/{id}/assistant/run` (browser), MCP `orders.assistant_match` . Settings live in
Settings → AI → Order assistant.
2026-10-24 18:49:26 +00:00
- Quote matching is **AI-only** . The rule-based scorer (`order-quote-match.ts`, `computeQuoteSuggestion` ) survives only as the assistant's shortlist; nothing shows or writes its suggestions. New orders are matched by `matchOrder` right after `createOrder` (when the assistant is on), the tick, the sweep route and the order page's "Match with AI".
2026-10-24 11:06:51 +00:00
- `orders.quote` is written **only** through `setOrderQuote` , inside `runWithAuditActor({ name: "AI order assistant" }, …)` . A 409 from it is not an error — the assistant falls back to a suggestion.
- Auto-link needs **all** of: the setting on, confidence `high` , every real order line paired `same_item "sure"` , every pair on a line of the one chosen quote, and the quote's company = the order's company. Otherwise it only writes `potential_quote` (confidence 0.9 / 0.7 / 0.5) and `quote_match_method "ai_suggest"` .
- The AI never changes a line's status, creates a PO or sends anything. An AI failure is recorded on `orders.ai_quote_match` with status `error` and is never thrown out of `matchOrder` .
2026-10-24 18:49:26 +00:00
- Manual live steps (like select values and pb_hooks): run `npm run pb:migrate:order-assistant` against the live PB, publish the `ai_quote_match` / `ai_fill` `IGNORED_FIELDS` hook change, deploy, dry-run then `--apply` `scripts/2026-10-24-clear-rule-quote-suggestions.ts` , activate the n8n workflow "WFDASH Order assistant tick" (every 5 min), then switch the order assistant on in Settings → AI. See the runbook.
2026-10-24 11:06:51 +00:00
2026-10-17 07:55:44 +00:00
# AI Collection Confirm checks — reads driver photos, ticks lines, never confirms
Runbook: `docs/collection-ai-checks.md` . Files: `src/lib/server/collection-ai-{read,pair,findings,service,prompt}.ts`
(vision read, pairing call, document-level findings, orchestration, default prompt) and `src/lib/collection-ai-ticks.ts`
(pure, client-safe tick logic). Routes: `POST /api/v1/collection-confirm/ai/tick` (n8n, agent auth),
`.../ai/check` and `.../add-to-pos` (browser session), MCP `collection_confirm.ai_check` . Settings live in Settings → AI → Collection checks.
- **Manual first:** the Add to POS button and the Check with AI button are manual; the tick only reads and checks, nothing else.
2026-10-19 11:18:21 +00:00
- The AI writes **only** `ai_check` , `collection_doc_reads` rows, an **empty** SKU when it is sure, and — only when a "sure" tax-invoice line shows a discount — the cost set to the price after discount (via `updateOrderItemCost` , with a note). It never overwrites a person's SKU, confirms a collection, or changes status.
2026-10-28 09:36:23 +00:00
- **Auto-push** (`collection-auto-push-service.ts`, runbook section "Auto-push"): a purple card is confirmed by the tick itself and logged to `ai_settings.collection_auto_push_log` ; off by default, needs `npm run pb:migrate:collection-auto-push` . Change what counts as pushable in `planAutoPush` , never by calling confirm elsewhere.
2026-10-17 07:55:44 +00:00
- The service refuses to run until `ai_check` exists (PB drops unknown fields silently); `ai_check` is in `IGNORED_FIELDS` of the audit hook.
- Manual live steps (like select values and pb_hooks): run `npm run pb:migrate:collection-ai` against the live PB, redeploy `pocketbase/pb_hooks/audit_log_utils.js` , deploy, activate the n8n workflow "WFDASH Collection check tick" (every 5 min), then switch Collection checks on in Settings → AI. See the runbook.
2026-10-19 13:08:12 +00:00
# PO numbers: create customer POs only through `createPurchaseOrderWithNumber`
`src/lib/server/po-number.ts` numbers a PO by reading the month's highest
number and adding one. Called in parallel (one order, several suppliers)
that gave 34 duplicate PO numbers by 2026-10-19. `createPurchaseOrderWithNumber`
numbers and creates one PO at a time and retries when the unique index on
`po_number` rejects a clash. Never call `generatePONumber` and then
`purchase_orders.create` yourself; the browser goes through
`POST /api/v1/orders/[id]/purchase-orders` . The partial unique index is a
manual live step: `npm run pb:migrate:po-number-unique` (after deploying).
2026-10-19 15:03:33 +00:00
# Stock orders on Collection Confirm
Collected stock lines show on Collection Confirm until their stock order is
`received` (`STOCK_ON_PAGE_FILTER` in `collection-confirm-service.ts` , shared
with the AI check). Confirm stock calls `markStockOrdersReceived` , which flips
the order to `received` once every line is collected/booked — the
accountant's booking queue lists `received` . Stock photos live on
`stock_purchases.additional_attachments` (multi-file after the manual live
step `npm run pb:migrate:stock-photos` ; run it before deploying the driver
app). Always add with `additional_attachments+` .
2026-10-22 02:04:50 +00:00
# Short collections split on Collection Confirm
When a collected customer line has `qty_collected < quantity` , Confirm
(`confirmOrderItemsWithSplits`) cuts it to what was collected — that part
goes to invoicing and closes in full — and creates the rest as a new
`pending` line on the same order (`src/lib/short-collection.ts`, marker
`order_item_details.shortCollection` ). The process page offers "Back order"
(relink to the original PO: `po_generated` , or `collection_pool` when ready)
via `POST /api/v1/order-items/{id}/back-order` ; otherwise the buyer orders it
elsewhere or cancels it like any new line. Never put a part-invoiced line
back to `ready_invoice` expecting a later top-up: invoicing subtracts
`qty_invoiced` , and the split is what re-orders the shortfall.
Supplier-side lines (`exclude_from_invoice`) close to `invoiced` at Confirm
when every billable line on their order is already billed; stragglers get a
"Close supplier-side lines" button on Invoicing
(`POST /api/v1/invoicing/close-supplier-side`).
feat(manager): a mobile orders board for the managers at /manager
A hidden, installable PWA: one link, opened on a phone, showing where
every order is. New / Processing / Completed, with a tap-to-preview
popup and the invoice/line detail Gary needs. Read-only, and NOT in
the sidebar — the route lives outside `(app)`, so `<Sidebar />` never
renders there.
The preview popup is reused, not rebuilt: the service emits
`UniversalHit`s and the page hands them to the existing
`SearchPreviewDialog`, so the manager gets the desk's quick-overview
verbatim.
Also fixes the /orders Completed tab, which was matching ZERO orders.
`delivered_at` is stamped on only 2 of 12,957 delivered lines and the
business stopped writing `delivered` on 2026-08-14 (the live pipeline
now ends at `invoiced` / `checked_in`), so the old filter never
matched. "Settled" is now defined once — `SETTLED_STATUSES` +
`SETTLED_DATE_FIELDS` — and dated per status: `invoiced_date` for
invoiced (the only properly stamped field, 2,199 lines), `delivered_at`
for delivered, `updated` only where no dedicated column exists.
`delivery_note` and `checked_in` move into `completed` as instructed,
and the standalone `post_processing` tab is retired (a DN is not a
stage of its own). `invoiced` stays in `collected` as well, so
invoiced orders don't vanish from the workflow tabs.
Completed tab: 0 -> 341 orders over 30 days, 112 over 7.
2026-10-26 21:09:01 +00:00
# Manager phone view (`/manager`) — the order pipeline on a phone
A hidden, installable PWA for the managers: one link, opened on a
phone, showing where every order is. It is deliberately NOT in the
sidebar — the route lives in the `(mobile)` route group, outside
`(app)` , so `<Sidebar />` never renders there.
Runbook: `docs/manager-orders.md` .
- Route group `src/app/(mobile)/manager/` (layout + auth-gate + view),
PWA assets `public/manifest-manager.json` + `public/sw-manager.js` ,
registered in `src/components/service-worker-registration.tsx` .
- Read model `src/lib/server/manager-orders-service.ts` , exposed at
`GET /api/v1/manager-orders` (browser-session auth, same shape as
`/api/v1/boss-dashboard` ).
- Three buckets — **New / Processing / Completed** — from
`PIPELINE_GROUP_ITEM_STATUSES` in `src/lib/item-status-ui.ts` . That
map is a COARSE view of the desk's eight workflow tabs; both read
the same status lists so they can never disagree.
- **Completed is time-windowed** (7 days on the phone, 30 on
`/orders` ). "Settled" is defined ONCE in `item-status-ui.ts`
(`SETTLED_STATUSES` + `SETTLED_DATE_FIELDS` ) and the filter is
shared (`settledWindowFilter` in `orders-service.ts` ).
## The date field is per-status — read this before changing the window
There is no single "finished at" column, and the obvious one is a
trap. Measured on live PB (2026-10-26, 12,957 delivered lines):
- `delivered_at` is populated on **2** lines. The rest are legacy rows
whose status was set without a stamp, and their `updated` is the
MIGRATION timestamp. So `delivered` counts only when `delivered_at`
is set — an un-dated delivered row is ARCHIVED, not recent.
- `invoiced_date` is populated on **2,199** lines and is the only
properly stamped settlement date in the schema.
- `completed` , `checked_in` and `delivery_note` have no dedicated
column, so `updated` (bumped on every write) is the only signal.
`SETTLED_DATE_FIELDS` encodes that: `primary` = "use this stamp, and
skip the row if it is empty"; `fallback` = "no stamp exists for this
status, use `updated` ". They are mutually exclusive — giving a status
BOTH would resurface archived rows every time an old row was touched.
Before the fix the Completed tab matched **0** orders, because the
filter only looked at `delivered` + `completed` dated on
`delivered_at` /`updated`, and the business stopped writing `delivered`
on 2026-08-14 (the live pipeline now ends at `invoiced` /
`checked_in` ). It now matches 341 orders over 30 days, 112 over 7.
The `post_processing` tab was retired in the same change (a DN is not
a stage of its own) and `delivery_note` + `checked_in` moved into
`completed` . `invoiced` is listed in BOTH `collected` and `completed`
on purpose: `itemStatusToTabGroup` resolves it to `collected` (the
office's workflow tab keeps its rows) while the completed tab picks
invoiced rows up through its own windowed filter. Dropping it from
`collected` would make invoiced orders vanish from `/orders` .
The preview popup is REUSED, not rebuilt: the service emits
`UniversalHit` s and the page hands them to the existing
`SearchPreviewDialog` (`src/app/(app)/search/search-preview.tsx`), so
the manager sees the same quick-overview the desk sees on /search.