Answering the second product question returned batch_locked. Resolving a
line fired a full re-run, which holds the batch lease for some twenty
seconds, so the next answer arrived while the lease was still taken. The
re-run achieved nothing either: resolve-item already links the line,
re-evaluates the document and writes its stage.
It was also quietly destructive. A run rebuilds `items` from the page
reads, and only a choice saved as an alias can be re-derived from the
descriptions — so a link made without ticking "remember this alias" was
discarded by the next run, including the tick a minute later. Manual
links now persist in manual_overrides alongside the order number, and are
honoured only while the linked item still belongs to the resolved order,
so correcting the order cannot leave lines pointing at the previous one.
Each line now has its own Save rather than committing the moment the
dropdown changes, and an answered line leaves the list instead of
reloading onto the same screen with the same questions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Picking the right order did nothing. The route stored the corrected
number, the re-run rebuilt the candidate from its page reads, and
po_number_raw went back to the original misread — so the operator
answered the same question again, having been told it was saved.
Nothing in dn_batch_notes distinguished a value a person chose from one
the model read, so the runner could not tell a correction from stale OCR
and overwrote both. A manual_overrides field now records the decision,
and the runner applies it before resolving rather than after, so the
corrected number is what the PO is matched on.
Only fields a run rebuilds are recorded this way. Quantity, unit and the
masses survive on their own; page ranges are the assembler's answer and
are corrected through the assembly route, which keeps the whole batch
consistent. An override must carry a value — clearing a box is not a
decision that a document has no number.
Verified end to end on a slip whose order number was read with a zero for
the letter O: the correction survived the run, resolved the order, moved
the match from fuzzy to exact, and left the document ready to post.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A batch is the unit only while its files are being prepared, read and
grouped. Past that point holding its documents together means one
unresolved order keeps every settled sibling waiting, and clicking a
document to fix one order presented a page strip and five unrelated
notes.
The inbox board now changes unit where the work does: Queued, Reading
photos and Check page grouping hold batches; Needs matching, Ready to
post and Complete hold individual documents. Both stage ladders already
existed in PocketBase, so nothing needed migrating. Failed batches move
to their own strip rather than spending a column.
/document/[id] is the new home for one note — its pages, values, lines,
failures, order picker and, at last, a button that posts it. The post
endpoint has been there and gated since it was written with nothing in
the app calling it. The batch screen keeps page grouping and lists its
documents as links.
The re-run button ran runBatch regardless of DN_STAGED_INGESTION_ENABLED.
On a staged batch that was destructive rather than merely inconsistent:
runBatch reads the single dn_batches.file, so a five-photograph batch
would have been rebuilt from its first photograph alone. It now selects
the runner exactly as the tick does.
Save grouping only set a message. A save bumps the assembly revision and
deliberately blanks each candidate's commercial data for recomputation,
so the screen appeared to do nothing while quietly emptying six
documents, and the next save was rejected as stale against a revision the
operator never saw. It reloads now, as its neighbouring buttons already
did. Choosing an order re-runs the checks in the same action.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A delivery note is a record of material that physically arrived. Refusing
to record it because its quantity cannot be reconciled with the order
leaves the yard's paperwork outside the system, so quantity findings are
now reported on the purchase order rather than gating ingestion.
qty_plausible and delivered_totals_available become informational: still
computed, still stored, never blocking. Only assembly and identity —
po_resolved, product_matched, units_convertible, not_duplicate — hold a
document back.
qty_plausible also stops guessing. Where no common unit exists it fell
back to the raw slip figure, comparing 30,260 kg against an order of 30
and reporting a 1,009x overage with "check the slip for a misread
decimal". The slip was right; the order had no unit. 61 of one site's 148
PO lines carry no unit, and units_convertible deliberately tolerates
that, so the two checks disagreed about the same gap. It now declines to
compare rather than produce a number that blames the wrong document.
The delivered-totals query OR-ed every relevant PO item id into one
filter sent as a GET parameter. Measured against production, 80 clauses
(3,436 chars) succeed and 85 (3,651) return a bare 400; that site's 148
lines put it permanently over, so no candidate could ever reach
ready_to_post. Chunked at 50.
Batch stage no longer counts candidates needing review. Grouping is a
batch question and matching is a document question, and folding them
together parked batches under "Check page grouping" with no grouping left
to check — holding settled notes behind one unresolved order.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three faults stopped the staged pipeline from ingesting anything once it
met a live PocketBase rather than a test double.
PocketBase treats 0 as blank for a required number field, and two staged
fields legitimately hold zero: dn_batch_sources.ordinal is 0 for the
first source in a batch, and dn_batch_pages.region is 0 for a whole-page
read. Both creates were rejected with validation_required, so the first
file of every upload failed and no page ever persisted. The migration now
declares them optional, and pb-relax-dn-zero-fields.py relaxes instances
that already have them required.
The staged runner verifies its batch lease and commits the protected
write in one transaction, which needs PocketBase's batch API. That is off
by default, so the runner failed closed with
dn_pipeline_transactions_required and no deployment note explained why.
pb-enable-batch-api.py turns it on and raises the 3s transaction timeout,
which is tight for a commit carrying page images.
compare-dn-pipelines used top-level await while resolving as CJS, so the
command in the rollout runbook always failed to transform. Renamed .mts.
The tests missed all of this because they mock PocketBase, and the mock
accepts what the real server rejects.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replace the off-screen CSS grid + window.print() in the worker QR
batch dialog with two on-click exports:
- Download PDF — A4 portrait, 2x5 grid of CR80 (85x54mm) cards per
page, brand orange header, generated with pdf-lib (dynamically
imported, same pattern as src/lib/photo-pdf.ts).
- Download CSV — UTF-8 BOM, columns employee_no, name, position,
worker_type, project_code, qr_payload via Papa.unparse.
The single-card print on the worker detail page still uses
window.print() — one card has no pagination problem, only the batch
view did.
Spec §4.4 updated to match the new output mechanism and explain the
mobile failure mode that prompted the change.
New tests (8) cover the CSV builder (header order, qr_payload ==
employee_no per spec §4.1, RFC 4180 quoting, empty input) and the PDF
builder (empty input throws, valid %PDF output, 11 workers paginate
onto 2 pages, fake PNG injection so layout is testable without qrcode).
Staged, photo-first delivery note ingestion: bulk JPEG/PDF intake, one
persisted vision read per page, deterministic assembly into candidate
documents, site-scoped supplier/PO resolution, split review, a
supplier-grouped work queue with manual line linking, and fenced
idempotent posting.
The new pipeline stays dormant behind DN_STAGED_INGESTION_ENABLED; the
tick route keeps running the legacy runner until the flag is set to
exactly "true". The PocketBase schema is already applied in production
(verified by a read-only dry run: every collection, field and index
present, no financial rows touched).
The local draft of the multi-file upload route is superseded by the
branch version, which parameterises its PocketBase filters, checks
duplicates before creating a batch, and rolls back a partially written
batch. That draft is kept in stash@{0}.
Verified on the merged tree: tsc --noEmit clean, 1200 tests across 106
files passed, next build passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>