Add `sku` to the order_item.patch MCP tool whitelist and update its documentation
Fix stuck stock POs in buyer tasks po_sent tab by filtering out downstream statuses
Rewrite margin approval webhook to use fire-and-forget dispatch to avoid UI blocking, add server-side error logging
Add MCP feature requests and local setup documentation
- Add server-side `/api/v1/dashboard` endpoint to reduce client-side payload and round trips
- Implement invoice upload and order-item SKU update APIs with complete test suites
- Refactor order and order-items list endpoints (including MCP tools) to support proper PocketBase filter and sort params
- Fix pagination validation for order-items route to clamp page ≥1 and perPage 1-500
- Add redirects in next.config.ts to map underscore collection paths to hyphenated API routes
- Improve buyer tasks page to avoid redundant tab count refetches when switching tabs
- Refactor collection confirm components with editable SKUs and updated accessibility labels
- Add cached admin PocketBase auth to minimize repeated authentication calls
- Add performance testing scripts for dashboard endpoint latency
- Update quotes list route to default to excluding archived counts
- Fix photo strip component text and labels for non-invoice documents
- Refactor sprint page to lazy load supplier/customer data for pricing modal
- Clean up unused imports in test files
- Add full test coverage for all new APIs, services, and components
- order_items.update happy path: patches cost_price + qty_invoiced
+ supplier_invoice_number, verifies the whitelist strips nothing
and the row is written
- order_items.update forbidden: system-controlled fields (order,
purchase_order, quote_item, id) are rejected
- quotes.update regenerate_pdf:true: patch is saved first, then
the PDF endpoint is called with the freshly-persisted letterhead,
and the response includes both the updated quote and the new PDF
attachment metadata
- tools/list test count: 77 (was 76)
All 1385 tests pass.
Co-authored-by: Orca <help@stably.ai>
Two new capabilities, both requested by the agent for the
collected-vs-invoice reconciliation workflow:
1. order_items.update — patch fields on an order_item for invoice
reconciliation. Lets the agent compare what the supplier invoiced
(qty_invoiced + supplier_invoice_number + cost_price) against what
was ordered (quantity) and what was collected (qty_collected),
then write the differences. Whitelist mirrors the quote_items.update
pattern:
cost_price, selling_price, quantity, markup, markup_percent,
notes, qty_processed/assigned/collected/verified/stocked/picked/
loaded/delivered/invoiced/cancelled, supplier_invoice_number,
customer_invoice_number, collection_date, collected_date,
verified_date, delivered_date, invoiced_date
System-controlled fields (id, order, purchase_order, quote_item,
product, created, updated) are rejected with forbidden_field so
the agent can never accidentally reassign an item to a different
order or PO.
2. quotes.update with regenerate_pdf:true — the agent can now
switch letterhead (or any other header field) AND re-render the
PDF in a single call. The patch is saved first, then the existing
POST /api/v1/quotes/{id}/pdf route is called using the freshly-
persisted letterhead, so the response always matches the record.
Useful for: 'switch this quote to ISHERWOOD and send me the new
PDF'. Without the flag, the existing 2-step workflow still works
(quotes.update, then quotes.get_pdf).
All 1385 tests pass (was 1382; +3: regenerate_pdf happy path,
order_items.update happy path, order_items.update forbidden field).
Co-authored-by: Orca <help@stably.ai>
- quotes.list_attachments: proxies to GET /api/v1/quotes/{id}/attachments
- quotes.get_pdf: proxies to /api/v1/quotes/{id}/pdf/base64 (with optional
letterhead + regenerate query params)
- orders.add_attachment: posts multipart to /api/v1/orders/{id}/attachments/upload
- orders.list_attachments: proxies to /api/v1/orders/{id}/matched-attachments
All four tests use a stubbed global fetch (restored in finally) so
the MCP route's internal HTTP calls are deterministic and don't hit
the real server. Updated the tools/list test to include the four new
tools (76 total, was 72).
All 1382 tests pass.
Co-authored-by: Orca <help@stably.ai>
Closes the four gaps the agent flagged for the email→quote→PDF→send
workflow:
NEW REST ROUTES
- GET /api/v1/quotes/[id]/pdf/base64 — auto-renders the quote PDF
(idempotent against an existing pdf_generated_at) and returns it as
{ pdf: <base64>, attachment_id, filename, size, content_type,
letterhead }. 422 on placeholder customer / unpriced items. Used by
the new quotes.get_pdf MCP tool to give MCP clients a way to
download the rendered PDF — the streaming /api/v1/quotes/[id]/pdf
endpoint is still available for the WebUI's iframe.
- POST /api/v1/orders/[id]/attachments — uploads a file to the
order's MATCHED QUOTE. PB has no order_attachments collection;
orders share the quote_attachments table via their matched quote.
201 returns the new attachment + matched_quote_id so the agent can
confirm where the file landed.
NEW MCP TOOLS (catalog now 76, was 72)
- quotes.list_attachments — list all attachments on a quote
(generated PDF + original RFQ email attachments + operator uploads)
- quotes.get_pdf — auto-render + return the PDF binary as
base64 so the agent can save/forward/email it
- orders.add_attachment — upload to an order (proxy to the new
order upload route; multipart form)
- orders.list_attachments — list attachments on an order (proxies
the existing /api/v1/orders/[id]/matched-attachments route, which
returns the matched quote's attachments)
The 'upload to a PDF' workflow is now: attachments.upload_for_quote
(or orders.add_attachment) → quotes.list_attachments to see what's
there (including the generated PDF) → quotes.get_pdf to download
the PDF → quotes.send_email with selectedAttachmentIds to ship it all.
WHY the existing 'attachments.upload_for_quote' was not enough:
the agent previously had no way to retrieve the rendered PDF binary
or to attach files to orders. Now it has both. The order upload goes
via the matched quote because that's the single attachment table in PB.
All 1382 tests pass (was 1378; +4 for the new tools).
Co-authored-by: Orca <help@stably.ai>
- quotes.update: happy path with whitelisted fields, forbidden_field
rejection for system-controlled fields (status/customer/company/
quote_number/id), empty-patch rejection, missing-id rejection
- create_from_email: unit field now preserved end-to-end, result.items
returns the actual QuoteItem array (not just a count)
Also updates the existing tools/list test (now 72 tools, was 71) and
the create_from_email mock in route.test.ts to use the new
items/items_count shape so it matches the type.
All 1379 tests pass.
Co-authored-by: Orca <help@stably.ai>
Closes the two real gaps from the agent's gap analysis (2026-08-05):
1. **quotes.update MCP tool** — was missing entirely. The quote header
has fields (company_letterhead, quote_date, quote_reference, etc.)
that couldn't be patched via MCP, so the agent had to pass
letterhead at PDF-render time and couldn't persist it on the record.
Now exposed with a strict field whitelist mirroring the
quote_items whitelist pattern: any field outside the allowed set
(status, customer, company, quote_number, id, created, updated, …)
errors with forbidden_field rather than silently writing to PB.
New catalog entry + dispatch handler in src/app/api/mcp/route.ts.
Whitelist constant QUOTE_PATCH_FIELDS colocated with the existing
QUOTE_ITEM_PATCH_FIELDS for reviewer discoverability.
2. **create_from_email now preserves the unit field** — extracted_items
from the AI's RFQ parsing carries {description, quantity, unit,
notes} but the create loop was silently dropping unit. Also the
service returned items: <count> which made the agent believe no
items were created when in fact stub rows existed. Now returns
the actual QuoteItem array (with a back-compat items_count field)
and the unit value lands on each row.
The agent's third claim (PB admin auth unavailable) is a misdiagnosis
— WFDASH uses its own AGENT_API_KEY and all writes go through the
Next.js admin client. No code change needed for that.
All 1379 tests pass. quote_items.update path unaffected. The
quotes.update path mirrors the same pattern: whitelist + reject empty
patch + call pb.collection.update.
Co-authored-by: Orca <help@stably.ai>
Closes architectural gap #1 from the Giellie skill (the operator's AI
agent could ingest quotes via /api/mcp but had no way to enumerate,
add, or patch the resulting line items - every auto-price flow had to
stop and ask a human for the item IDs).
Adds:
- GET /api/v1/quote_items (list w/ filter, sort, expand)
- POST /api/v1/quote_items (create on existing quote)
- GET /api/v1/quote_items/[id] (fetch one)
- PATCH /api/v1/quote_items/[id] (whitelisted patch)
- quote_items.list MCP tool
- quote_items.get MCP tool
- quote_items.create MCP tool
- quote_items.update MCP tool
Patch whitelist on update is identical to bulk_update_quote_items:
cost_price, selling_price, description, sku, unit, supplier, markup,
pricing_source, pricing_notes, matched_price_list_id, user_edited
Everything else (id, quote, created, updated, quantity, is_processed)
returns 400 / MCP error with the allow-list echoed back so the agent
can self-correct.
quote_items.update does NOT enforce the already_priced skip - that's
the safety-net for bulk_update_quote_items. Single-row patches are
explicit writes.
Newly created rows have is_processed=false forced server-side; Giellie
still cannot flip the flag (architectural gap #2 unchanged - that's a
UI gate by design).
Also: removed "JEI" from quotes.company_letterhead select values on
the live PB - valid values are now ["CNL", "ISHERWOOD"]. Use the
uppercase form when setting this field.
Giellie skill doc refreshed: tool count 69 to 73, new "quote_items
CRUD" subsection, three new REST rows, architectural gap #1 marked
RESOLVED, hard rule #1 updated to cover the new endpoint, new worked
example for "add a missing line item + price it", change-log entry.
Tests: 82 passing across the three touched files (was 63 before).
Type-check: zero new errors (pre-existing 29 in
orders-service-review.test.ts unchanged).
Deployment: pending operator rebuild + redeploy. Until then
POST /api/v1/quote_items returns 404 and the new tools don't appear
in tools/list.
Co-authored-by: Orca <help@stably.ai>
WFDASH /api/mcp previously only handled tools/list and tools/call,
which is fine for our internal curl-based testing but broke every
spec-compliant MCP client (Hermes, Claude Desktop, Cursor, mcp-cli).
The MCP protocol requires a three-step handshake before tools/list:
1. client → server: initialize
2. server → client: result with protocolVersion, capabilities, serverInfo
3. client → server: notifications/initialized (notification, no response)
Before this fix, step 1 got HTTP 404 with error.code -32601, which
MCP clients interpret as 'this isn't an MCP server' and terminate
the session. Symptom: configured servers show 'CONFIGURED' in the
Hermes WebUI but '0 tools', with the runtime logging
'unhandled errors in a TaskGroup ... Session terminated'.
Three new handlers, each with the right response shape:
initialize → 200 + plain result {protocolVersion, capabilities, serverInfo}
notifications/initialized → 204 + empty body (notification, no envelope)
ping → 200 + result {}
Critical: the initialize response uses a PLAIN object for result,
not the content/text envelope that our jsonRpcOk helper wraps
tools/list and tools/call results in. Using jsonRpcOk here would
have left the handshake broken in a subtler way.
Four new Vitest cases:
- initialize returns server info (regression: 2026-08-04)
- initialize echoes client-provided protocolVersion (forward compat)
- notifications/initialized returns 204
- ping returns {}
Bug report with full timeline + lessons: see
docs/bugs/2026-08-04-mcp-initialize-handshake-missing.md
(same commit series; pushed separately as the second commit).
All 46 tests pass.
Co-authored-by: Orca <help@stably.ai>
The tools.discovery tool was declared in the catalog (line 1300) but no
tools/call dispatch branch existed for it — every call fell through to
'Unknown tool: tools.discovery'. Functionality was reachable only via
the parallel GET /api/mcp endpoint, which the JSON-RPC-only MCP clients
in n8n couldn't use.
Add a real handler that returns the same `tools` array as tools/list,
wrapped in the standard JSON-RPC content envelope. Mirrors GET /api/mcp
for JSON-RPC-only clients.
Also add a Vitest case that asserts the catalog shape (array of tools,
each with name+description+inputSchema, and tools.discovery in the list).
Deployment note: this fix is in source; the live apphub.co.za server
runs a built bundle and will keep returning 'Unknown tool' until rebuilt.
Co-authored-by: Orca <help@stably.ai>
Restores the /collection-pool feature in this codebase.
- src/lib/server/orders-service.ts
- Re-adds `listCollectionPool({ status, page, perPage })` which
queries the `collection_pool` PB collection via the admin
client (filter on pool_status, sort -created, expand
order_item/order/supplier/drv_id). MCP route.ts already
imported this — without it, the MCP route failed to
compile.
- src/app/(app)/collection-pool/{page,components,view-model}*.tsx
- Re-creates the page directory (5 files, 2 315 lines) carrying
the supplier-group + per-stop checkbox UI, the
`CollectionPoolRowSource = "customer" | "stock"` union, the
assignment-state badge helpers (buildProximityMarkerMap,
buildSupplierGroups, buildVisibleRows), and the
MarkReadyDialog / AssignDriverDialog wiring.
- src/app/api/mcp/route.test.ts
- Adds `const listCollectionPool = vi.fn()` and exposes it in
the vi.mock factory so the existing collection_pool.list test
cases (and the bulk_update_quote_items ones that call
`.mockReset()` on every fixture) stop crashing with
'ReferenceError: listCollectionPool is not defined'.
Not in this commit (kept narrow per request):
- The /api/v1/collection-pool route handler and its test
- The 'Collection Pool' entry in the sidebar nav
- The migration script that creates the live `collection_pool`
PB collection (it doesn't exist on the cloud PB instance
yet; the page render relies on it being created separately
or via `npm run pb:migrate:orders-v2`)
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.
Add full sprint pricing workflow and supporting infrastructure:
- New sidebar navigation entry for the sprint dashboard
- PocketBase category collection schema and migration script
- Server API routes for sprint item claims, attachments, counts, and file proxying
- Dev identity utilities and UI switcher for multi-user testing
- Reusable FilterTabs component replacing ItemStatusTabs
- Updated TypeScript types for sprint-related entities
Also include:
- Fix PocketBase file URL method casing (getUrl → getURL) across codebase
- Add development utility, smoke test, and helper scripts
- Update package.json with new sprint migration command
This commit overhauls the order management system to use item-level status tracking instead of a single order-level status:
- Remove deprecated `orders.status` field and add `invoiced` and quote match audit fields
- Add `isOrderComplete` utility with tests to validate fully delivered orders
- Add purchase order PDF rendering support and tooling
- Add geo coordinate fields for companies/customers with map link UI
- Add REST management APIs for customers, companies, and suppliers
- Update customer/company services to use relation fields instead of free text
- Add status pill and tab filter components for item status visualization
- Add quote-to-order matching logic and unit tests
- Update all pages, components, and tests to match the new schema
- Include all required PocketBase migrations and schema updates
Fix inconsistent line endings across all project files for cross-environment consistency.
Add complete unit test suite covering core utilities, API routes, and server services.
Add Playwright E2E test setup with smoke, auth, and agent API tests.
Update all project configuration files to match standardized formatting.
Fix MCP API error handling to properly propagate HTTP status codes and use correct JSON-RPC error codes.
Add VSCode workspace configuration, extensions, and example MCP config files.
Update .gitignore to exclude new test and build artifacts.
Introduce PocketBase workflow support, migration tooling, server APIs and documentation. Adds a set of PocketBase collection JSONs (order_items, order_items_v2, collection_pool, delivery_notes, delivery_note_items, repairs) and a large import manifest for workflow collections; includes docs describing MCP/API layer, migration audit, order-item schema and rollout plans. Adds server-side agent/MCP endpoints and services (api/mcp, api/v1/orders, collection-pool, order-items status, health) plus helper modules (pocketbase-admin, orders-service, order-item-status, agent-auth) and a migration script to create order_items_v2/collection_pool. UI: new pages for collection-pool, delivery-notes, repairs and several updates to orders/quotes pages, sidebar, tooltip and pb helpers/types. Also updates package.json (migration script and related changes) and global styles. Required env vars and suggested migration command are documented (e.g. NEXT_PUBLIC_POCKETBASE_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD, ORDER_ITEMS_COLLECTION; run npm run pb:migrate:orders-v2).