Commit graph

154 commits

Author SHA1 Message Date
DEls
6824d2c62e Add Workforce attention alerts and 24-hour correction times 2026-10-04 23:38:43 +02:00
DEls
ed69b517e8 Integrate agent work and add dashboard attention indicators
Remove repeated Plant, Financial and Workforce shortcut cards. Add site-scoped Plant and Financial sidebar attention status with automatic refresh.

Include accumulated delivery processing, purchase order recovery, supplier reconciliation, plant data and user management changes and audit records.
2026-10-04 21:53:32 +02:00
DEls
32d4a82cdf feat: add employee search and save pending fleet updates 2026-09-30 00:42:38 +02:00
DEls
4ad262b508 Let site managers create clock users for assigned sites 2026-09-30 00:11:24 +02:00
DEls
71499f5222 Allow assigned-site users to perform operational actions 2026-09-29 23:58:40 +02:00
DEls
7f8561ff6b Use file token when posting protected delivery note page 2026-09-29 14:45:35 +02:00
DEls
11a227bb4e Allow clock users to upload purchase orders for assigned sites 2026-09-29 13:33:46 +02:00
DEls
223da69884 Fix document upload permissions and clock correction origin 2026-09-29 13:26:47 +02:00
DEls
f5259d852f feat(auth): add PocketBase logins and site access controls 2026-09-28 23:51:47 +02:00
DEls
3ba004ffd3 fix(dn): harden delivery-note matching, posting and PO reconciliation
Fresh duplicate check before posting, dismiss route sets stage, free accessory
lines no longer crash promotion, readiness treats only posted notes as terminal,
document-reference/usable-PO helpers, PO delivery reconciliation API + docs and
audit register.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-28 21:16:24 +02:00
DEls
1899ed8e2c fix(dn): unstick long OCR runs, preserve OCR data through manual saves
Tick path
- Bump STALE_CLAIM_MS from 10 to 30 minutes. A 29-source OCR batch
  hit ~29 min and was being reclaimed mid-run by the 10-minute cap,
  leaving the batch in inbox with last_error='reclaimed after a
  stalled run' even though nothing was stalled. MAX_ATTEMPTS still
  bounds a truly stuck batch.
- After each dn_batch_pages create, the staged runner bumps
  dn_batches.page_count in the same lease-fenced batch. Final
  assembly write at run-staged.ts:223 still sets the authoritative
  value, so any drift from a reclaim-then-restart cycle is corrected.
  Inbox tiles now show live progress instead of 0 pages for the
  entire reading window.

PocketBase batch transport
- sendRecordBatch chunks across pb.createBatch() calls at the
  server 50-request cap. A 71-candidate assembly sent as one
  request was returning Invalid batch request data. and 500ing —
  the assembly route caught it and surfaced as the generic Grouping
  could not be saved. 73 mutations now split into 50 + 23 and land.

Assembly save UX
- The dismiss dialog now renders directly under the open-pages alert
  instead of at the bottom of the section (which was hidden behind 71
  document tiles on big batches). Clicking Dismiss page opens the
  reason input where the user is looking.
- saveGrouping surfaces the server-side error code
  (stale_revision / posted_candidate / dismissed_candidate /
  assembly_in_progress / invalid_page_coverage) instead of a generic
  Try again. On success, the page now redirects to /inbox so the
  documents land in their columns.
- The Technical details disclosure is gone from the grouping screen.
  Open pages already surface their reason in the alert above; the
  duplicate disclosure just competed with the decision UI.

Assembly route correctness
- After a successful manual save the route now writes
  stage=complete, status=needs_attention, notes_detected,
  processing_completed_at and clears claimed_at/last_error. The
  runner advances stage on its own runs but a manual save was
  never followed by a runner pass — without this the batch stayed
  in Check page grouping forever even though assembly was done.
- For existing candidates the route only updates grouping metadata
  (pages, page_from, page_to, assembly_revision, status, stage,
  checks). OCR-derived fields (doc_number, supplier_name_raw,
  items, raw_text) are preserved on the row — the runner only
  re-fills those for batches in status=inbox, and post-save the
  batch moves to needs_attention and never reaches the runner
  again. New candidates still get a createdCandidateFields row
  with the OCR fields blank, ready for the runner or operator
  entry.
- A Re-run batch button was added to the document detail header
  so the operator can refill the wiped OCR data (or re-resolve
  checks after a manual override) without first navigating to the
  batch detail page.

Customer identity check
- A Skip this check toggle on the document detail page records
  manual_overrides.skip_customer_identity. run-staged reads the
  override and turns the customer_identity check into passed=null
  (skip) instead of passed=false (fail), so OCR false-positives
  on the customer VAT no longer block posting while still
  requiring an explicit operator decision.
- The PATCH route accepts skip_customer_identity as an editable
  field so the toggle saves through the standard note route.

Tests
- 180 files / 1767 tests pass; typecheck clean.
2026-09-27 13:40:27 +02:00
DEls
39d85fa8a9 fix(ask): supplier balances, payable per supplier per period, PDF fallback
- Ask's supplier loader didn't pass date_received, so supplier_balances
  and the supplier_balance dataset crashed
- supplier_balances takes a period (thisMonth, thisWeek, month1-3) and
  returns what was received from each supplier in it (payable)
- PocketBase's space-separated dates dropped deliveries on the 1st from
  period filters, including the Suppliers page's Payable KPI
- The PDF route falls back to the answer text when a saved report has
  no sections (older format), instead of a 500

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:56:40 +02:00
DEls
58b625701e feat(ask): serve a real PDF from a stored assistant message
GET /api/ask/messages/[id]/pdf builds a PDF for one stored assistant
row: its structured make_pdf_report document when `parsed.pdf` is set,
otherwise its own markdown (headings/paragraphs/lists/GFM tables, with
[[chart:id]]/[[image:id]] tokens resolved against that message's own
stored charts and any referenced generated-image rows). 404s for a
missing message, a non-assistant row, or a thread owned by someone
else — same "not found, not forbidden" reasoning as the existing
thread routes, since there are no logins yet. Returns
application/pdf with Content-Disposition: attachment; filename="ask-<slug>-<date>.pdf".

The chat stream's "done" event now carries the persisted assistant
row's own id (messageId), which the browser needs to ask this route
for a PDF — see the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:16:38 +02:00
DEls
e6772bfb1f fix(ask): migrate created/updated to autodate, sort ties by id
ask_threads/ask_messages/ask_log shipped with created/updated typed as
plain `date` fields, which PocketBase never populates — every row's
created has been empty since. pb-add-ask-collections.py now emits
autodate fields (onCreate, plus onUpdate for updated) and migrates the
three live collections in place under --apply, re-adding
idx_ask_messages_thread_created if PocketBase drops it with the field.

--backfill (apply-only) just reports affected row counts: PocketBase
can only set an autodate field to *now*, never a historical value, so
there's nothing safe to backfill.

Since old rows keep an empty created either way, the read side sorts
ask_messages by `created,id` instead of `created` alone, so tied
(all-empty) rows land in a stable, deterministic order rather than
whatever order PocketBase happens to return them in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 03:05:23 +02:00
DEls
aeba1a2225 test(ask): give the actions route tests a matching Host header
Follow-up to 7773ac8: those tests build a bare new Request(), which
(unlike a real HTTP request) has no Host header unless one is set
explicitly. isSameOrigin now checks Host, so all three routes' tests
started failing every non-cross-site case. Default a Host header
derived from the test's own ORIGIN, and set it explicitly (mismatched
against Origin) in the cross-site case so that test still exercises a
real mismatch rather than a merely-missing header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:28:47 +02:00
DEls
ba75c65795 feat(ask): download Ask-made PDF reports
Added mid-plan (docs/superpowers/plans/2026-09-26-ask-agent-actions.md,
new Task 9b — image generation from Task 9 is untouched): the user can
ask for any custom report and get a finished PDF, e.g. "give me a PDF
on fuel at The Hills for September", not just a manual export of
whatever is on screen (Task 8).

make_pdf_report({ title, subtitle?, filename? }) is the model's own
"this answer is a finished report" signal, called last once it has
already built the report (headings, narrative, KPI tables quoting tool
display strings, charts via make_chart). extractPdfMeta splits the
meta out the same way extractCharts/extractActionCard already do: the
model gets an ack, the browser gets the full meta via a new `event:
pdf`. The assistant's ask_messages row stores it in the existing
`parsed` json field, so reopening the thread still shows the report
cover and Download button — no schema change.

chat-view.tsx renders a report cover (wordmark, title, subtitle, date,
"Prepared by Ask for {name}") and a prominent "Download PDF · {title}"
button reusing Task 8's answerToPdf (same page-slicing, so a chart is
never cut across pages), and auto-downloads once per turn via
nextPdfTurnToDownload — a pure decision function, unit-tested directly
since this codebase's component tests use renderToStaticMarkup rather
than interactive DOM testing.

Live check skipped: needs a thread, which needs ask_threads (not yet
applied). Covered by unit tests (tool meta, the route's pdf event and
persistence, and the once-per-turn auto-download decision) instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:25:00 +02:00
DEls
bdb2b2361b feat(ask): log app change requests for the developer
file_change_request (tools-actions.ts) logs an ask_log row
(type "request", status "open") for a change to the app itself — a new
page, column, button or report layout — distinct from propose_changes,
which changes data; the prompt now routes between the two.
list_change_requests (tools.ts, read-only) lists them, which the
existing MCP server picks up automatically since it iterates the whole
tools array. /dashboard/change-requests lists and re-statuses them
(open/planned/done/declined) via new GET/PATCH routes under
/api/ask/change-requests, linked from the sidebar under Ask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:07:27 +02:00
DEls
ed49d7e198 feat(ask): generate illustrations
src/lib/agent/images.ts's generateImage posts MiniMax image-01's
documented request shape (verified against the installed mmx-cli SDK:
POST {baseUrl}/v1/image_generation with
{model, prompt, aspect_ratio, n, response_format}) and reads
data.image_base64[0]. A base_resp.status_code of 1028/1030/2061 (the
same codes the CLI itself maps to a "needs Plus plan" hint for this
endpoint) surfaces as a clear, non-retried error rather than a generic
failure. generate_image (tools-actions.ts) stores the result as its own
append-only ask_messages row (kind: "image") and returns its url.

extractGeneratedImages splits a tool result the way extractCharts
already splits chart numbers — the browser gets the full image, the
model keeps only imagesAvailable: [{id}] so it can place
[[image:<id>]] without ever seeing pixels or a URL. answer-parts.ts's
splitAnswer now takes an imageIds set alongside chartIds (existing
chart-only callers keep working via a default empty set), and
AnswerMarkdown renders the image inline.

The pb-files proxy (src/app/api/pb-files/.../route.ts) now also serves
ask_messages (previously only delivery_notes/purchase_orders): since
that collection's rules are all null by design (Task 1), this one
collection mints a short-lived admin file token server-side rather
than exposing the underlying PB rule.

Live check skipped (both the chat flow and MiniMax call): reaching
generate_image needs a thread, which needs ask_threads (not yet
applied). Covered by unit tests against the documented request/response
shape and error codes instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:59:52 +02:00
DEls
5aac22ba8f feat(ask): propose, confirm, apply and undo changes
The gateway (src/lib/agent/gateway.ts) is now the one seam every write
goes through: authorize() (NEVER_WRITABLE + dn_batches lock_*/stage),
planOps (schema validation via schema-cache.ts's new validateOp,
before-snapshots, $ref cross-checks, relation-id existence, the
sites/employees duplicate rule, capped at 500 ops), createPendingAction,
applyAction and undoAction. propose_changes is a registered tool;
apply/reject/undo are confirm-card routes under
/api/agent/actions/[id]/, same-origin only.

/api/chat now streams a full action card (`event: action`) while the
model itself only ever sees {actionId, status, problems} — the same
split extractCharts already does for chart numbers (extractActionCard).
The next user turn gets the outcome of the prior turn's action(s)
prepended to what the model reads (ChatEntry has no system role, so
this is the pragmatic equivalent of a system note). chat-view.tsx
renders action-card.tsx per assistant turn, hydrating from
GET /api/agent/actions/[id] when a thread reopens.

Deviations, both noted for the smallest-sensible-adjustment reason:
- Batches run as sequential individual PocketBase calls, not
  pb.createBatch() — that's what makes $ref resolution exact rather
  than dependent on the batch API's own (version-sensitive) request-
  chaining syntax.
- Re-applying a "failed" action isn't specially deduped against ops
  that already partially succeeded; only the single-attempt path is
  implemented in this session.

Live check skipped: ask_threads/ask_messages/ask_log still don't exist
in the target PocketBase (Task 1's --apply is the user's own step).
Covered by mocked unit tests instead — 17 gateway tests plus the
confirm-card routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:54:06 +02:00
DEls
5cf828f4e3 feat(ask): read Excel, CSV, PDF, photos and text attachments
Adds src/lib/agent/attachments.ts (parseAttachment/previewForModel/
summarise for xlsx, xls, csv, pdf — text-layer or scanned, via
pdftotext — image and plain text) with fixtures under
src/lib/agent/__fixtures__/, POST /api/chat/attachments (multipart
upload into an append-only ask_messages row, rasterising a scanned
PDF's first pages so they ride along as vision images the same way a
photographed slip does), and the read_attachment / send_to_po_pipeline
tools in tools-actions.ts. send_to_po_pipeline mirrors
/api/purchase-orders/upload's own hash-dedupe-then-create logic
directly against PocketBase rather than a same-process HTTP call (no
reliable base URL to call itself with from a server-side tool handler).

chat-view.tsx gained a second attach control (FileUp icon) alongside
the existing PO-upload paperclip, wired to the new route; pending
attachment chips ride along on the next message the same way pending
photos already do.

Deviation: send_to_po_pipeline takes { id, site } (the plan's { id }
alone can't resolve which site's purchase_orders row to create,
since /api/purchase-orders/upload always required one).

Live browser check skipped: ask_messages doesn't exist yet in the
target PocketBase (pending the user's --apply of Task 1's schema).
Covered by mocked unit tests and real fixture files instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:25:45 +02:00
DEls
dc25fb13cd feat(ask): long-term memory
Adds src/lib/agent/memory.ts (remember/forget/listMemories, dedupe by
normalised text, formatMemoriesForPrompt) and wires it in: a
"## What you remember" system-prompt section, the remember/forget/
list_memories tools (src/lib/agent/tools-actions.ts, a per-request
factory since these need the actor unlike the static read-only tool
set), a REST view for the UI (/api/ask/memories), and a Memory drawer
in chat-view.tsx (Mine / Shared with everyone, add, delete).

Adjusted src/lib/agent/read-only.test.ts: its blanket "no mutation
calls under src/lib/agent" contract now allowlists the specific write
seams the plan designates (memory.ts here; tools-actions.ts, gateway.ts
and images.ts to follow) instead of forbidding every file, since
memories/requests are specified to bypass the gateway and write
directly.

Live browser check skipped: ask_log doesn't exist yet in the target
PocketBase (pending the user's --apply of Task 1's schema). Covered by
mocked unit tests instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:13:39 +02:00
DEls
7c5df11b84 feat(ask): per-user persistent threads
Adds the "Who's using Ask?" picker (GET /api/ask/users), actor
resolution via x-ask-user (src/lib/agent/context.ts), and thread
persistence (src/lib/chat/threads.ts, /api/chat/threads routes).
/api/chat now accepts { threadId?, message, display?, images? } and
persists each turn as its own append-only ask_messages row; the old
{ history } body still works for one release. chat-view.tsx gained the
picker, a thread sidebar (new/select/rename/archive), and ?thread=<id>
in the URL.

Live browser check skipped: ask_threads/ask_messages/ask_log don't
exist yet in the target PocketBase (Task 1's schema is dry-run only,
pending the user's --apply). Covered by mocked unit tests instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:05:01 +02:00
DEls
1dbe719e2c test(chat): import the route statically so a loaded run cannot time out
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:55:18 +02:00
DEls
278d258024 feat(ask): stream chart specs to the browser, not the model
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:14:01 +02:00
DEls
3551e09170 fix(chat): compute today's date per request, not at boot
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:11:25 +02:00
DEls
e83a0d302c fix: overview counts, wrong-unit fuel limits, meter guard, en-ZA numbers, vehicle merge, docs
- Overview/Plant tiles and pages share one vehicle bucket rule (blank unit_type = fleet)
- Plant landing equipment figures were always 0; hour diffs now reject 0 baselines and >24h/day jumps
- km/L limits on plant (and L/h on vehicles) report unit-mismatch instead of false alerts
- formatPercent/formatDecimal replace display toFixed (en-ZA)
- POST /api/vehicles/[id]/merge + Merge duplicate dialog (dry run first)
- Attention links go to equipment vs vehicles by unit type; PO upload view link fixed
- Conditional hook fixed in DN document view; ESLint flat config (next lint removed in 16)
- Docs: current modules/routes/env vars; login + Ask reports plans

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 22:04:08 +02:00
DEls
2d998370b8 feat(receipts): edit posted received quantities with audit 2026-09-25 07:39:46 +02:00
DEls
a59a5a86d2 Merge branch 'codex/clock-daily-shifts' 2026-09-25 07:32:46 +02:00
DEls
5cdfaa3128 feat(clock): review missed scan-outs without changing phone flow 2026-09-25 07:27:12 +02:00
DEls
8d728333f9 fix(clock): start new local day with an in scan 2026-09-25 07:04:45 +02:00
DEls
29edf2e5aa Revert "feat(clock): add named kiosk and supervisor sessions"
This reverts commit b163ce9f94.
2026-09-25 03:22:43 +02:00
DEls
b163ce9f94 feat(clock): add named kiosk and supervisor sessions 2026-09-25 03:19:25 +02:00
DEls
df201b2a1f feat(receipt-correction): add support for receipt correction form and enhance item editing
Introduced a new secret key for receipt correction in the environment configuration. Enhanced the API for handling item edits in delivery notes, allowing for more granular updates and preserving existing manual overrides. Updated the document detail view to display additional supplier and customer information, and integrated a toggle for skipping customer identity checks. Improved validation for item entries and added tests to ensure robust handling of various edge cases in item editing.
2026-09-25 02:59:16 +02:00
DEls
47ec4f1d1b feat(purchase-orders): enhance PO handling with improved validation and item management
Updated the purchase order processing to include enhanced validation for item edits, ensuring that all required fields are checked and properly formatted. Introduced new functions for computing item amounts, taking into account discounts and VAT. Refactored the PO detail view to streamline item management, including a new ItemsCard component for better organization of line items. Additionally, improved the handling of PO number sanitization to strip leading and trailing characters, enhancing matching accuracy. This update aims to improve the overall robustness and user experience of the purchase order system.
2026-09-23 23:37:37 +02:00
DEls
97fe75931d feat(clock): implement two-step QR scanning flow for employee clocking
Enhanced the PersonScanner component to support a two-step flow for scanning employee badges. The new process includes a lookup phase to identify the employee and their last scan status, followed by a confirmation phase where the operator can verify the scan before recording the clock event. This change addresses issues with accidental double scans and improves user feedback with warnings for recent scans. Updated state management and error handling to reflect the new flow, ensuring a smoother user experience.
2026-09-22 23:50:59 +02:00
DEls
f88d5c519e feat(fuel-logs): enhance vehicle auto-creation and error handling
Introduced a new function to infer vehicle unit type from descriptions, improving the auto-creation process for vehicles in the fuel logs. Enhanced error handling for missing identifiers during vehicle creation, providing clearer feedback on the reasons for failures. Updated the SheetJS parsing to prevent date misinterpretation and refined the PlantPage to display all plant vehicles, regardless of cost per hour, ensuring better visibility of available resources.
2026-09-22 23:47:28 +02:00
DEIs
87f75dd98f feat(plant): fleet map page with GPS-denormalised vehicles, clustering, and tile switcher
Adds /dashboard/plant/map: a Leaflet view of every vehicle scanned from
the MEGA Clock kiosk, plotted at its last known position.

Schema
- vehicles.last_latitude / last_longitude / last_scanned_at /
  last_location_accuracy_m denormalised off fuel_logs so the map page
  renders every vehicle in a single query. Same best-effort pattern as
  attention_dismissed_at; updated by /api/clock/vehicle-scan after every
  successful scan. PB init defaults new number cols to 0 (not null) —
  filter uses 'last_latitude > 0 || last_latitude < 0' to mean 'real fix'.
- Indexed via idx_vehicles_last_scanned.

Map UI
- Custom SVG markers per unit_type: car silhouette for fuel, tractor
  silhouette for plant, 24x24, drop-shadowed.
- Clustering via react-leaflet-cluster over Leaflet.markercluster.
  Cluster icons colour-ramp with size (green->sky->amber->rose) and
  scale 32->54px.
- Three-button base-layer switcher: Street (OSM), Dark (ESRI Dark
  Gray Canvas — CartoDB watermarks tiles without a key), Satellite
  (ESRI World Imagery). Last choice persists in localStorage.
- Site pins: small purple map-marker divIcon, click flies the map to
  zoom level 15.
- Empty-state is a floating chip on the map rather than a full-page
  card so the map is always visible.

APIs
- /api/clock/vehicle-scan: stores the GPS fix on the fuel_logs row
  AND denormalises onto the vehicle when present; missing-coord
  scan leaves last_* untouched.

Sidebar: new Map entry under Plant group.
2026-09-22 21:39:25 +02:00
DEls
a589b14ac9 feat(clock): enhance vehicle scan API with GPS support
Updated the vehicle scan API to include optional GPS coordinates (latitude, longitude) and location accuracy. The API now validates these fields, ensuring both latitude and longitude are provided together and within valid ranges. Enhanced error handling for invalid inputs and updated the API reference documentation accordingly. Additionally, modified the FleetScanner component to capture GPS data and reflect its status in the UI.
2026-09-22 19:59:44 +02:00
DEls
3077223d38 feat(clock): add MEGA Clock API endpoints and update documentation
Introduced three new server-only API endpoints for the MEGA Clock PWA:
- `POST /api/clock/scan` for clocking workers in and out via badge scanning.
- `POST /api/clock/vehicle-lookup` for looking up vehicle details using QR codes.
- `POST /api/clock/vehicle-scan` for logging odometer readings from scanned vehicles.

Updated the README and API reference documentation to include these new endpoints and their functionalities. Replaced the placeholder for fleet scanning in the ClockView component with the new FleetScanner component.
2026-09-22 12:42:39 +02:00
DEls
f1dbc96435 feat(dn): grab and move the order the same way as the slip
The order was a PDF in a frame, which meant the browser's viewer, not
ours: no drag, no shared zoom, controls in a different place. Two halves
of one comparison behaving differently.

Its pages are now rendered to PNGs through the same poppler path the
delivery note's pages use, so both sides go through one viewer and get
the same stepping, zoom, drag-to-pan and remembered width. Switching
sides resets to the first page at 100%, since the position held on one
document means nothing on the other.

The PDF route stays for "Full size". Its file lookup moved to lib and is
now shared with the page renderer, so the two cannot drift on which field
holds the file. Page count comes from pdfinfo rather than rendering, as
the viewer needs the count before it draws anything.

Verified against MEGCPO9239: a two-page order, both pages 200 image/png,
page three 404, no token 401.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 02:05:31 +02:00
DEls
8027b5a263 feat(dn): flip between the slip and the order it is checked against
Verifying a delivery means comparing two documents, and the order was not
one of them: reaching it meant leaving the note, finding the purchase
order and coming back having lost the comparison.

The viewer gains a toggle. The order is a PDF, so the browser's own
viewer handles it — it already paginates and zooms better than wrapping
it in ours would. The toggle only appears when an order is resolved and
carries a file; an unmatched note has nothing on the other side of it.

The document is proxied behind a signed token rather than linked from
PocketBase, which would put the admin JWT in the browser. The token is
namespaced per document, so one minted for a delivery-note page cannot be
replayed against an order, nor one order's against another's — verified
against a live file: valid token 200, absent 401, replayed 401.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 01:44:02 +02:00
DEls
b8dcf20146 fix(dn): settle one product line at a time, and keep the link
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>
2026-09-21 00:15:13 +02:00
DEls
14be7e7e21 fix(dn): keep an operator's correction through the next run
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>
2026-09-20 23:55:56 +02:00
DEls
7a02ab9bb7 feat(dn): work a delivery note on its own page, not its whole batch
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>
2026-09-20 23:50:24 +02:00
DEls
bbd7a7bb23 fix(dn): harden staged assembly and page identity 2026-09-19 19:17:46 +02:00
DEls
eed128ebb3 fix: bind posting audit to delivery note 2026-09-19 18:59:52 +02:00
DEls
11bc1a4b6b fix: verify audit collisions and purchase orders 2026-09-19 18:58:07 +02:00
DEls
2914eefacf fix: harden delivery note posting review gates 2026-09-19 18:53:25 +02:00
DEls
5d094183e6 feat: add delivery note posting and pipeline comparison 2026-09-19 18:45:30 +02:00
DEls
81a4427fea test: cover correction transaction fencing 2026-09-19 18:38:02 +02:00