A PocketBase file PATCH under the bare field name replaces the whole
list. On 2026-10-19 a buyer's "Add supplier invoice" on PO202610008LIF
deleted the driver's delivery-note photo (uploaded 14:38, read by the AI
14:40, replaced 14:41). Generating or regenerating a delivery-note PDF
did the same to every line on the DN.
- New src/lib/server/order-item-files.ts: addAttachmentForm() adds with
`order_item_attachements+` and removes (`-`) only older stored copies
of the same document; pickStoredCopy() finds a file by its uploaded
name (PB stores "DN26101913.pdf" as "dn26101913_<10>.pdf").
- Used by the Collection Confirm invoice upload, DN create, DN regenerate
(replaces only its own previous PDF) and /api/invoicing/delivery-note.
- /api/invoicing/delivery-note-pdf finds the DN PDF by name instead of
taking the line's first file (falls back to the old behaviour for
legacy names).
- AGENTS.md: always add, never replace.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Delete photo next to "Add supplier invoice" removes the photo open in
the previewer (wrong upload, or one of several for the same PO) after an
inline "Delete …? Yes, delete" confirm; the confirm resets when you
switch photos. Not offered for the PO PDF.
- POST /api/v1/collection-confirm/delete-photo (browser session) →
deleteCollectionPhoto: takes the file off every order line carrying it
(or the stock purchase's photo slot) and deletes the AI's reads of it
plus the checks built on them (shared forgetReads with Move photo).
- Removed the header's blue "Run agent scan" button and its results panel
(not in use); the /api/v1/agent/scan route stays for the agent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drivers photograph invoices per item and can't see which order a photo
lands on. B&R invoice 00282979 (stock order PO26100702STK) ended up on
the thinners line of PO202610017BLY; the AI read "PO26100702STK" off it
but the card only said "not found".
- The card list now carries each read's po_refs and source file, and sets
ai_doc.other_po when the document names a PO that exists and isn't the
row's PO (none named, or this card's PO among them: no hint).
- PO card: "Photo may be for <PO>" in the header; when open, "Invoice <no>
is for <PO>, not this PO" with a Move photo button. Nothing moves on its
own.
- POST /api/v1/collection-confirm/move-photo (browser session) →
movePhotoToPo: copies the photo to the target (a collected line of a
customer PO, or the stock purchase's single photo slot — refused when
taken), removes it from the source, deletes the AI's read of it and the
checks built on it so it is re-read where it now belongs.
- normalizePoReference moves to the client-safe src/lib/po-reference.ts
(re-exported from the matching service).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plumblink hands out delivery notes with prices on them. The Price tick
was always grey on a delivery note, so a card could go purple without
its prices ever being checked, and the AI's reading notes ("delivery
note, not a tax invoice", "year not printed") were never shown.
- Price tick: compare whenever the document line has a price, invoice
or delivery note; grey only when no price is printed. The AI still
only corrects a cost from a tax invoice.
- ai_doc now carries the read's notes; cardNotes() unions them per card
and the AI findings box lists them under "Reading notes" (the box now
shows when there are notes but no findings).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The collapsed PO card showed white instead of purple: it always carried
`bg-card`, which beat `bg-ai/10` in the CSS (but lost to the green).
Now the card gets exactly one background.
- Purple now means "the AI checked every line and no tick is red or
amber" (grey "couldn't check" ticks are fine). A card the AI checked
with a line off, or with unchecked lines, goes amber and says
"AI checked N of M lines · K need a look". Green still means "photo,
not checked yet".
- cardAiState() in collection-ai-ticks.ts works this out from the same
computeLineTicks the table uses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HYCO (I009753) prints full prices on the lines and "Discount 25.00%" in
the totals; Swagelok (622831) has a "Disc %" column on each line. The AI
compared our cost (already the discounted price) with the printed price,
so the Price tick went red on both.
- The read now captures a line's discount_percent and a whole-document
doc_discount_percent (prompt rule 5b) and stores net_unit_price on each
line (both discounts stacked).
- The Price tick compares the cost with the price after discount; the
tooltip and the document-lines panel spell it out
("R15.10 less 25% invoice discount = R11.33").
- Owner's call: when a "sure" tax-invoice line has a discount and our
cost isn't the discounted price, the AI sets the cost to it (customer
lines through updateOrderItemCost, which adds a cost note; stock lines
through updateStockPurchaseItem) and records why in ai_check.cost_changed,
shown in purple under the line. No discount, a delivery note or a
"likely" match: the cost is never touched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The n8n tick was calling in every 5 minutes but WFDASH answered
"disabled" because Collection checks was off in Settings → AI, and
nothing on Collection Confirm said so.
- New "AI checks" switch next to Refresh, the same setting as Settings →
AI → Collection checks. Purple and "On" when running, with the last
run in the tooltip; a gear links to /settings/ai.
- GET/PUT /api/v1/collection-confirm/ai/enabled (browser session). Only
reads/writes collection_ai_enabled and the last run — no key, model or
prompt goes through it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A collapsed PO card turned green as soon as the driver uploaded a photo,
whether or not the AI had read it, so "everything is green" said nothing
about the agent. Now:
- A collapsed card with at least one AI-checked line is purple (the AI
colour) instead of green; green still means "photo, not checked yet".
- The header says "AI checked N of M lines" with the robot icon, open
or closed.
- Each line the AI checked gets a light purple wash in the items table.
Lines whose documents couldn't be paired (pair_failed, grey ticks) don't
count as checked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The AI and POS columns sat at the far right of the items table, hidden
behind the document preview in the split view. Ticks (now labelled SKU /
Qty / Desc / Price) and the POS status render under each description,
and the table is back to its original six columns and width.
- Owner's call: fill an empty SKU on a sure pairing even if the code may
be misread; the rep edits it and a near-miss still helps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Dates: the model also returns the date as printed; code parses SA
day-first formats (16-10-26 -> 2026-10-16) and drops dates more than a
year old or a month ahead (the Bestobell read gave 2016-10-26).
- SKU: a new code_certain flag per line; the AI only fills an empty SKU
when it read every character clearly (it filled 0515315 for 051315).
- Supplier: names sharing a distinctive word (Bestobell, Belting) no
longer raise a 'supplier differs' finding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sideways Bestobell invoice was read as doc 10045064 / a bank-details
line; turned 270 deg the same photo reads correctly (10245084,
PO202610010MRE, rubber insertion @ R365). When no printed PO number
resolves, retry the photo at 270 and 90 deg and keep the first read whose
PO resolves (within the tick's time budget).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
quotes.pb.js is copied byte-for-byte from the live WFDASH PocketBase.
DEPLOY lists the hooks the VPS cron syncs into the hooks volume; the
audit hooks stay undeployed for now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F2: tick table (SKU green via the code match incl. AI fills, grey per tick,
pairing failure -> all grey, different product -> red), stock lines use the
line SKU, Add to POS needs a SKU, SKU fills audited for order lines only.
F3: content_hash is a non-unique index (identical uploads share a hash,
skipped PDFs have an empty hash) - matches the migration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
M2: the confirm dialog pre-fill uses the card's newest document (as the
chip) and only when it is a tax invoice.
M3: the AI ticks column (and its min-width) shows only when a row has
ai_check; the POS column stays whenever in_pos is known.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F1: outstanding = round(ordered - collected, 3) > 0 and the order-line
mismatch compares round(order_qty, 3) with round(quantity, 3), so float
noise never produces "0 are still outstanding" or a false mismatch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
I7: a hash whose read failed this run is recorded as a failed copy with no
second AI call.
M4: the card's own PO lines are always offered for pairing alongside the
POs the document names; the fallback finding still applies when none resolve.
M5: a failed findings write no longer leaves the lines unchecked.
M6: the schema guard requires ai_check on order_items and stock_purchase_items.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
I5: a customer row without a SKU returns needs_sku and creates nothing; the
Add to POS button is disabled for it ("Set a SKU first").
I6: a createStockItem failure is caught per row (status failed + error), the
route still 200s, and the page toasts "N added, M failed, K need a SKU".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
I4: ConfirmRow.line_sku carries stock_purchase_items.sku; the SKU tick,
the AI robot and (for stock rows) the SKU column use line_sku before the
catalogue sku via effectiveSku. The SKU pencil keeps line_sku in step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
I3: ai_check.pair_failed marks lines whose covering document couldn't be
paired; computeLineTicks returns all grey for them. The card findings box
now also lists each row's ai_check.findings after the document findings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
I1: a 'different'/unrated answer never claims a PO line in parsePairs, is
stored as doc_line null (verdict + reason kept), loses to a real pairing,
is not a pairing for the findings, and gives all-red ticks.
I2: document lines are always numbered 1..n.
M1: the SKU tick goes green only through the normalised code match.
Also adds effectiveSku (line_sku before sku) for I4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PocketBase drops unknown fields on update, so before the migration runs
the AI's prices would land with no purple marker and no notes. The
manual button / MCP now return 409 'not set up yet' instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Runbook: pre-migration settings don't persist (not "saves fail"),
eligible statuses, 15 lines per quote, audit attribution, clone/template
behaviour, supplier kept, doubts dot, confidence guidance (Medium is the
default; raise to High if reps often change AI prices).
- quotes.assistant_fill added to docs/api-mcp-layer.md,
docs/audit/api-mcp-coverage.md and the Giellie skill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- M1: fillQuote and the tick refuse templates ("quote is not open
(template)"); both quote pages gate the fill button with
canAiFillQuote (new/follow_up/claimed, not a template).
- M4: run summary keeps <= 20 errors of <= 300 chars (boundRunErrors).
- M5: forced runs offer never-tried lines before tried ones.
- M6: counts route 400s on ids that aren't PB record ids.
- M7: a fill never replaces a supplier already on the line.
- M8: warn dot on the AI marker when a pending fill has doubts.
- M9: MCP hint states the real eligibility rule.
- M10: detail-page price dialog re-reads the line before recording an
AI review.
- M11: exported QUOTE_NOT_OPEN_ERROR_PREFIX / AI_DISABLED_ERROR used by
the run route.
- M14: prompt textarea maxLength 50000.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- With a SKU on the line, shortlistStockItems runs a second narrow query
(sku = raw || sku ~ normalised) and merges by id, so the capped broad
pull can't crowd out the exact SKU row. Quotes and backslashes are
stripped from the raw SKU before any filter is built.
- fillQuote catches a shortlist failure per line and marks only that line
error instead of abandoning the quote.
- Supplier names load once per fillQuote and are passed to each
shortlist call (new optional parameter; default unchanged).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A source line whose AI fill is still pending review (isAiFillPending) is
copied without cost, selling price, markup or supplier, so the purple
draft price can't resurface as an ordinary price on the copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After "Let AI fill this quote" the page re-pulls the quote's lines and
merges them with mergeItemsAfterAiRun: rows unchanged since the last load
take the server copy (so fills show), rows with unsaved changes stay as
typed, with a toast saying how many were kept. Previously onDone ran
fetchData, which replaced every row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- decideWithAi throws on stop_reason max_tokens instead of returning a
partial answer; fillQuote marks a requested line the AI did not answer
as error (retried after 1 h), never a permanent no_match.
- QUOTE_MAX_LINES 30 -> 15; the tick also caps lines per quote so the
60-line budget reaches other quotes.
- runAssistantTick stops starting quotes after a 90 s time budget and
returns reason running while a newer start than the last summary is
under 10 minutes old (lease).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AuditActor gains ignoreRequestUser: buildAuditHeaders then omits the
forwarded PB token (X-Audit-Auth) and request-derived email hint, so the
PB audit hook falls back to X-Audit-Actor-Name. fillQuote sets it for
all its writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Quote assistant card in Settings, AI (on/off, interval, min confidence,
editable prompt with reset, last-run line); parseSettingsForm handles the
qa_* fields. docs/quote-assistant.md has the runbook and the n8n tick
workflow spec; AGENTS.md gets a short section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>