Commit graph

658 commits

Author SHA1 Message Date
DEls
4922d649ae feat: add remote purchasing MCP to existing app 2026-10-05 02:46:36 +02:00
DEls
9f0698f1d1 feat: run durable MCP processing jobs in the existing app 2026-10-05 02:04:15 +02:00
DEls
4be0b2fa4e feat: expose matching and fenced receipt posting through MCP 2026-10-05 02:00:37 +02:00
DEls
f9a5ca80a7 feat: add immutable purchasing uploads and candidate edits 2026-10-05 01:55:31 +02:00
DEls
68e1c223a3 feat: share validated purchasing writes with MCP 2026-10-05 01:48:49 +02:00
DEls
cc8e2c8f01 feat: expose purchasing details and protected source documents 2026-10-05 01:42:27 +02:00
DEls
08becc6270 feat: journal MCP operations and fence purchasing writes 2026-10-05 01:38:15 +02:00
DEls
f9d3427767 feat: enforce MCP capabilities and no-delete policy 2026-10-05 01:33:37 +02:00
DEls
96da8ac4e8 feat: add authenticated same-app MCP transport 2026-10-05 01:31:17 +02:00
DEls
9c9a9d1866 docs: plan same-app remote purchasing MCP implementation 2026-10-05 01:22:08 +02:00
DEls
7ffe4eb94d docs: use existing app URL for remote MCP design 2026-10-05 01:16:34 +02:00
DEls
4970e61e70 docs: specify remote purchasing MCP service 2026-10-05 00:51:44 +02:00
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
bcbd3167e4 feat(clock): unify scanners and document capture 2026-09-30 06:44:36 +02:00
DEls
758999ce3b fix(clock): keep fleet dialogs above phone keyboard 2026-09-30 01:07:11 +02:00
DEls
b92449d337 fix(clock): pause QR decoding and fit search above keyboard 2026-09-30 01:03:23 +02:00
DEls
8f0407c49c Improve user settings layout and forms on mobile 2026-09-30 01:00:42 +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
b700252932 fix(auth): verify PocketBase backup key before migration 2026-09-29 07:01:15 +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
da9dca7c6e feat(plant): add "Add vehicle" button to the Fleet & Fuel page
Generalizes the Plant page's add-dialog into a shared AddVehicleButton
component parameterized by unit_type, and wires it into the vehicles
page against the existing POST /api/vehicles endpoint.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 08:33:53 +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
0f022d5ffa fix(ask): keep the Asking-as bar pinned under the navbar
The Ask page sized itself as 100svh-4rem, but the Topbar is 89px on
desktop and 167px on phones, so the page overflowed and the bar scrolled
under the header. The Topbar now publishes its live height as
--topbar-h and the Ask page fills exactly the space below it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:56:40 +02:00
DEls
1d8307453f fix(ask): key tool-provenance pills by index and name
A turn can call the same tool more than once (e.g. two query_records),
which gave duplicate React keys and a console warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:51:39 +02:00
DEls
956ba4e116 Merge branch 'feat/mobile-friendly'
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:45:35 +02:00
DEls
3feddebcaa fix(mobile): audit and fix layout overflow across dashboard pages at 375/768px
Audited /dashboard, /dashboard/sites (+ detail), /dashboard/workforce/workers,
/dashboard/workforce/on-site, /dashboard/plant/vehicles (+ detail),
/dashboard/plant/equipment (+ detail), /dashboard/plant/fuel-logs,
/dashboard/financial/purchase-orders (+ detail),
/dashboard/financial/delivery-notes/inbox, /dashboard/financial/suppliers,
and /dashboard/chat at 375x812 and 768x1024 using a scroll-right test to
catch real horizontal page overflow (as opposed to a table's own,
intentional internal scroll).

Two repeating bug patterns accounted for every overflow found:

1. A page-specific `right={<div className="flex items-center gap-2">...}`
   passed to Topbar can carry two or three labelled buttons (edit/import/
   merge/back etc.). Without `flex-wrap` they don't fit a 375px screen in
   one row and push the whole page sideways. Fixed on the vehicles,
   equipment, and site detail pages, plus their list-page counterparts
   (vehicles fuel-view, equipment plant-view) — all five instances of
   this exact pattern. Same fix applied to a delivery-note card's button
   cluster on the PO detail page, and to Topbar's own right-side
   container as a general backstop.

2. `Intl.NumberFormat("en-ZA", ...)` joins currency/number groups with
   non-breaking spaces, so a wide value like "R 24 988 663" can't wrap.
   In a 3-up grid (site cards' Contract/Spent/Remaining) that overflowed
   the column; fixed by stacking those to 1-up below `sm`. In a 2-up grid
   (PO detail KPI tiles) added `min-w-0` so the tile clips instead of
   growing the grid track past the viewport.

Also: the shared Dialog now caps at `max-h-[90dvh]` with internal
scroll (a dialog taller than the phone viewport had no way to reach
its close button or footer before), and the sidebar hamburger button
was overlapping the Topbar's title on every page below `lg` — reserved
space for it instead.

Tables already scroll horizontally inside their own card via the
shared `<Table>` component or an `overflow-auto` wrapper, and KPI
grids already collapse responsively — both were fine everywhere checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:44:19 +02:00
DEls
64d8fe1be2 fix(reports): externalize pdfmake's font stack, thin dense chart labels
Found live: rendering a report PDF 500'd with "ENOENT ... data.trie".
pdfmake's server PdfPrinter pulls in pdfkit -> @foliojs-fork/fontkit,
which loads font-shaping tables from a path relative to its own
installed location at runtime; webpack-bundled into the route's chunk,
that lookup can't find them. serverExternalPackages leaves the whole
chain on native Node `require`, where the real node_modules layout
resolves them — the same reason Next already ships `sharp` and
`@react-pdf/renderer` externalized by default.

Also found live: a bar chart with ~30 categories (fuel by vehicle)
printed completely overlapping x-axis labels. categoryLabelStride
thins them the way recharts' minTickGap does, keeping only as many
labels as the plot width can fit without collision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:33:14 +02:00
DEls
2f613181ac feat(ask): download real PDFs from the chat UI, drop the screenshot path
The "Download PDF" button (both the report CTA and the per-answer icon)
and the once-per-report auto-download now fetch
GET /api/ask/messages/[id]/pdf instead of screenshotting the DOM with
html-to-image. That fetch/blob/anchor-download logic lives in its own
new download-pdf-button.tsx, kept small and separate from chat-view.tsx
since another change is landing in that file's sidebar at the same time.

A Turn's `id` (the stored ask_messages row id) now rides in from a
thread reload (rowToTurn) or from the stream's "done" event once the
row is persisted; both the button and the auto-download wait for it.

Removes answer-pdf.ts (the html-to-image renderer and its page-slicing
math) and its tests — nothing else used them. slugify moved to
src/lib/reports/pdf/filename.ts in an earlier commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:16:46 +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
51e80bfe1d feat(ask): make_pdf_report takes a structured document
make_pdf_report no longer just tags an answer with a title/subtitle —
it now takes { title, subtitle?, sections: [{ heading?, text?, kpis?,
chartId?, imageId?, table? }] }, matching src/lib/reports/pdf/document.ts's
ReportDocument. A section's table prefers { resultId } (pulled from the
cached query_records result server-side) over { columns, rows }, so the
model still never hand-copies a figure. The document is stored as-is
(no rendering here — that's the next commit's route) and streamed to
the browser via the existing `pdf` SSE event.

Updates the system prompt's PDF rule to describe the structured call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:16:28 +02:00
DEls
3e06a78fcb feat(reports): add a real, server-side PDF builder
Replaces Ask's screenshot-then-wrap PDFs with an actual PDF pipeline
that runs in plain Node (no Chromium/Puppeteer, Alpine-safe): pdfmake
draws real text, real tables (zebra rows, right-aligned numbers, a
repeated header row across a page break) and vector charts (bar,
stacked bar, line, area, donut) built from ChartSpec as hand-rolled
SVG, using pdfkit's standard 14 fonts so no font files ship at all.
Chosen over @react-pdf/renderer for its native repeated-table-header
support, which that library only gets via manual pagination.

- chart-svg.ts: ChartSpec -> drawing plan (bar count, axis max) -> SVG.
- markdown-blocks.ts: GFM markdown -> headings/paragraphs/lists/tables.
- table-content.ts / kpi-content.ts / chart-content.ts / image-content.ts:
  the pdfmake content nodes for each piece.
- chrome.ts: masthead + running header + "Page n of N" footer.
- resolve-section-table.ts: a report section's table, preferring the
  cached query_records result (table: { resultId }) so figures are
  never hand-copied.
- build.ts: assembles either an answer's markdown or a make_pdf_report
  structured document into one PDF.

Verified against real pdftotext/pdftoppm output, including the header
repeating across a page break.

Adds pdfmake, unified, remark-parse (+ @types/pdfmake, @types/pdfkit);
drops html-to-image, whose only caller is removed in a later commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:16:20 +02:00
DEls
7009197716 feat(ask): make the thread list sidebar minimisable
Desktop gets a collapse/expand toggle in the chat header
(ThreadSidebarToggle) that persists to localStorage via
useThreadSidebarCollapsed, a useSyncExternalStore-backed hook — chosen
over a useEffect+setState "read on mount" hook because that pattern
trips react-hooks/set-state-in-effect, and the error budget for this
branch has no room to add one.

Phones already hid the panel behind a slide-over drawer; the missing
piece was a header trigger that shows the current thread's title
(ThreadDrawerTrigger), and Esc-to-close alongside the existing
backdrop-tap/pick-a-thread close paths.

Both new components are intentionally small and separate from the
main file, since the PDF download button in the same file is being
worked on concurrently by another change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:15:17 +02:00
DEls
efc1ed61d0 fix(layout): keep the Topbar pinned with iOS safe-area padding
The Topbar was already `sticky top-0`, which tracks the whole-page
scroll fine on both phones and desktop since neither the dashboard
layout nor any page wraps it in a clipping scroll container. What was
missing was the iOS PWA notch: add `paddingTop: env(safe-area-inset-top)`
so the bar's own background paints under the status bar/notch in
home-screen standalone mode instead of leaving a transparent gap.

z-30 stays below the z-40/z-50 used by drawers, dropdowns and dialogs,
and below the Leaflet map page's isolated stacking context (1760f77),
so the Topbar's dropdowns still float above the map and dialogs still
float above the Topbar.

Also fixes a real overlap bug surfaced while auditing at 375px: the
sidebar's fixed hamburger button sits at the same top-left corner as
the Topbar's title below `lg`, and nothing reserved room for it, so
the page title rendered partially underneath the button. Reserve that
space with padding instead, and truncate the title/subtitle so long
site names don't overflow the header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:00:27 +02:00
DEls
31655a573c fix(ask): call the MiniMax image endpoint once, not /v1/v1
MINIMAX_BASE_URL already ends in /v1, so appending /v1/image_generation
hit a non-existent path and every image request failed. Also accept the
image_urls response shape by downloading the image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:44:19 +02:00
DEls
2f34308840 fix(ask): bake CSS custom properties into PDF chart captures
Ask-made PDF reports were shipping charts with axes but no bars/line:
html-to-image serializes the answer clone to a standalone SVG document
and rasterizes that, and never re-resolves CSS custom properties in
that step. Recharts sets fill/stroke straight to var(--series-1) etc.
as raw SVG attributes (report-chart.tsx's seriesColor), so an
unresolvable fill fell back to SVG's default (black — solid black
bars) and an unresolvable stroke fell back to "none" (an invisible
line), reproduced live with "Make me a PDF report on fuel at The Hills
for September".

bakeCssVariableColors walks the clone once it's attached to the
document with its light theme forced, and replaces every var(...)
fill/stroke attribute with its computed literal color before capture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 03:05:37 +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
b065eb58b8 fix(ask): hide an unparseable filed-on date instead of showing "Invalid Date"
ask_log rows come back with created: "" — the collection's created/
updated fields are declared as plain date fields rather than PocketBase's
autodate type, so nothing populates them on create. That's a schema fix
(a human --apply step per Task 1's own rules, out of scope here). In the
meantime, the Change Requests page rendered new Date("").toLocaleDateString()
verbatim as the string "Invalid Date". Omit the line instead when the
date doesn't parse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:28:56 +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
7b1d0baa5c fix(ask): stop PDF pages from duplicating content at page breaks
Live-testing the PDF report ("fuel at The Hills for September") showed
the same chart block rendered twice: once at the bottom of page 1,
again at the top of page 2. Every page drew the *entire* rendered
answer image, repositioned so its own break offset lined up with the
page's top — relying on the page's fixed height to crop away
everything else. That only works when a page's content run happens to
be exactly CONTENT_HEIGHT_PX tall; slicePages deliberately breaks
*earlier* to avoid cutting a block in half, so the leftover space at
the bottom of the shorter page bled into the next block's content
instead of staying blank.

Now each page gets its own canvas-cropped PNG sized to exactly the
content it should show (breaks[i] to breaks[i+1]), embedded as a
separate image — no reliance on page bounds to "crop".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:21:12 +02:00
DEls
7773ac8f55 fix(ask): compare Origin against the Host header, not request.url
Confirm/Reject/Undo were unusable from any host that didn't happen to
match Next's internally-reported request.url hostname: dev serves on
127.0.0.1 (or a LAN IP) but request.url came back normalised to
"localhost", so every genuine same-origin click failed with "Cross-
site request refused" (403) even though the browser's real Origin
header was correct. Compare Origin's host against the request's own
Host header instead, which always reflects what the client actually
sent.

Found live-testing the fixed-cost Confirm/Undo flow: PocketBase showed
the row was never actually blocked from being created by the guard
itself, but every apply/reject/undo click from the browser 403'd.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:07:07 +02:00
DEls
738fcf9f6d fix(ask): keep bucketed chart rows chronological and reject non-numeric series
Live-testing a free-query chart ("litres per vehicle per week") turned
up two bugs:

- query_records grouped rows in first-seen order, not calendar order.
  Grouping by (vehicle, week) walks one vehicle's weeks at a time, so a
  week bucket could appear more than once and out of order — and a
  chart reads that row order straight onto its x-axis. Bucketed
  results now always sort chronologically by the bucket field first;
  anything the model sorts by still applies as a tie-breaker.

- make_chart accepted a non-numeric column (e.g. the "vehicle" label
  column) as a series, silently producing a blank plot with no y-axis
  since its cell values aren't numbers. It now checks actual cell
  values and returns a clear error naming the numeric columns
  available, so the model can correct itself instead of shipping an
  empty chart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 02:01:09 +02:00
DEls
693d096c0d fix(ask): log tool failures server-side
A failed tool call only ever reached the model as a short "Error: ..."
message so it could recover — but nothing was logged server-side, so
a real bug (as opposed to the model misusing a tool) left no trace to
debug from. Log the full error to the server console before handing
the short message back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 01:52:35 +02:00