Commit graph

393 commits

Author SHA1 Message Date
SJCoetz
d5ed390de4 feat(pb): add stock_purchases.additional_attachments file field (migration)
Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:32:12 +02:00
SJCoetz
bbfe20ec2d docs: register stock_purchases.add_attachment in feature backlog + API reference
Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:29:55 +02:00
SJCoetz
dd57d29de3 feat(stock-orders): attachments card on detail page
Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:29:00 +02:00
SJCoetz
f89bbb3a35 feat(mcp): add stock_purchases.add_attachment tool
Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:24:00 +02:00
SJCoetz
ccf04bc39f feat(api): add stock-purchase attachments delete route
Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:21:25 +02:00
SJCoetz
a72c5ca0a7 feat(api): add stock-purchase attachments upload route
Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:21:00 +02:00
SJCoetz
70c158b374 feat(stock-purchases): add additional_attachments service helpers
Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:20:26 +02:00
SJCoetz
9e118a3b5f feat(stock-purchase): split warehouse confirm from accountant booking
Repositioned booking responsibility: warehouse confirms physical
collection only (records quantity_received + flips header to
"received"); the accountant books into inventory via the new
invoicing flow once the supplier invoice is processed. This way
the payment-cycle reasoning lives at the booking step, not at
receipt.

Changes
- Migration: add booked_at + booked_by to stock_purchases
- Service: bookStockFromInvoice (accountant-side, splits from
  bookStockRows)
- API route: POST /api/v1/invoicing/stock-book
- UI: stock rows surfaced on invoicing page (StockBookingSection)
- UI: stock-purchase detail page
  - Driver picker on header (sentinel "delivery" user auto-collects
    every line, real drivers set assigned_to_driver)
  - "Confirm collection" replaces "Mark received" (warehouse-side
    only — no inventory writes)
  - Cancel button with confirmation dialog
- collection-confirm: confirmStockRows stripped of inventory writes;
  the prior /api/v1/collection-confirm/book-stock route was renamed
  to /confirm-stock and is now strictly the warehouse-side confirm
- Tests for all new flows (5/5 passing on the stock-order detail
  page; 10/10 on StockBookingSection)

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:01:15 +02:00
SJCoetz
df87094eb9 chore(playwright): add 2026-08-06 test artifacts
Add captured Playwright page state snapshots and console logs from the 2026-08-06 test session, stored in the .playwright-mcp directory.
2026-08-06 19:38:39 +02:00
SJCoetz
bf54511db1 fix(buyer-tasks): make card layout mobile-friendly
At <=640px the buyer-tasks card content was crushed by the action
column sitting beside it: the PO number badge broke mid-string
('PO-2026-08-042' wrapping to 4 lines), supplier names wrapped to
6 lines, and the new 'Sent to ...' caption overlapped the PO
badge on the PO Sent tab.

Three responsive tweaks, all using Tailwind breakpoint utilities:

1. Outer card container switches from 'flex items-start' to
   'flex flex-col sm:flex-row sm:items-start'. The action column
   drops below the info column on mobile, giving the info all the
   available width and the buttons full-width tap targets.

2. PO number badge gains 'whitespace-nowrap'. The badge stays
   atomic; the parent's 'flex-wrap' carries the whole badge to a
   new line when it doesn't fit.

3. PO Sent right column: buttons stack vertically on mobile
   ('flex-col sm:flex-row' + 'flex-1 sm:flex-none' on each
   button) and the caption switches from 'text-right' to
   'text-left sm:text-right' so it sits under the buttons.

Desktop layout is unchanged — verified at 1280px that the
side-by-side buttons, right-aligned caption, and inline single
button all still render exactly as before.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-06 18:21:38 +02:00
SJCoetz
d2ffbea6bc feat(buyer-tasks): show recipient + send date on PO Sent card
Adds a small muted caption under the Re-Send / Mark as ready buttons
on the PO Sent tab showing the email address the PO was sent to and
the send timestamp (en-GB short format like '5 Aug 2026, 14:23').

Data sources come from the existing BuyerTaskGroup.emailPayload
exposed in commit f61c173 — no new PB fields, no new types. The
caption only renders when sentAt is present, so legacy rows (or a
fresh draft flipped to po_sent without a successful send) stay clean.

Visible on mail_failed too: the operator can see who we tried to
reach and when, alongside the existing 'Mail failed' banner.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-06 18:16:22 +02:00
SJCoetz
f3052bfd40 Merge branch 'feat/buyer-tasks-resend-task1'
Adds a Re-Send button next to Mark as ready on the PO Sent tab of the buyer-tasks page. The button opens the existing SendPoModal in resend mode, pre-filled from the PO's previous email_payload (to/cc/bcc/subject/body). Also migrates the hardcoded purple Mark-as-ready color to the themed primary token, and fixes the mail-failed 'Resend' label that previously opened the wrong dialog.
2026-08-06 18:11:59 +02:00
SJCoetz
add7ab3302 fix(send-po-modal): seed default body when resend has no prior email_payload
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-06 18:10:08 +02:00
SJCoetz
abdc3a4d9f fix(send-po-modal): preserve previous message body on resend (don't overwrite with default template)
Co-authored-by: Orca <help@stably.ai>
2026-08-06 18:06:22 +02:00
SJCoetz
f61c173a1a fix(buyer-tasks): surface PO email_payload + attachments on group for resend modal prefill
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-06 18:01:58 +02:00
SJCoetz
6008f17c9a feat(buyer-tasks): add Re-Send button on PO Sent tab + theme-token cleanup
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-06 17:43:17 +02:00
SJCoetz
ff94086ec0 docs(plan): add implementation plan for Re-Send button on PO Sent tab
Co-authored-by: Orca <help@stably.ai>
2026-08-06 17:34:28 +02:00
SJCoetz
3a954982d0 docs(spec): add Re-Send button on PO Sent tab of buyer-tasks
Co-authored-by: Orca <help@stably.ai>
2026-08-06 17:33:25 +02:00
SJCoetz
7f477ca949 fix: resolve PO bug, add sku, improve webhooks
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
2026-08-06 13:13:57 +02:00
SJCoetz
4a69b57ec2 docs(pdf): remove stray duplicate 'from quoteSubtotal() in two ways:' line
Co-authored-by: Orca <help@stably.ai>
2026-08-06 12:22:22 +02:00
SJCoetz
68dcdd565d chore(pdf): silence unused-vars on intentional totalAmount destructure
Co-authored-by: Orca <help@stably.ai>
2026-08-06 12:21:00 +02:00
SJCoetz
180288e6e3 test(pdf): add render smoke tests for PO PDF (covers VAT totals)
Co-authored-by: Orca <help@stably.ai>
2026-08-06 12:13:52 +02:00
SJCoetz
07900075a4 docs(pdf): move VAT bullet to Layout block + tidy effective-qty comment
Co-authored-by: Orca <help@stably.ai>
2026-08-06 12:08:14 +02:00
SJCoetz
7a6e32cadb feat(pdf): add 15% VAT line + VAT-inclusive Total to PO PDF
Co-authored-by: Orca <help@stably.ai>
2026-08-06 12:02:55 +02:00
SJCoetz
a1714833a8 docs(pdf): fix poSubtotal JSDoc summary (effective-qty prose) + tidy blank lines
Co-authored-by: Orca <help@stably.ai>
2026-08-06 12:00:48 +02:00
SJCoetz
965f3e3bbe docs(pdf): restore poSubtotal JSDoc + clarify fallback uses || not ??
Co-authored-by: Orca <help@stably.ai>
2026-08-06 11:56:51 +02:00
SJCoetz
b3d9eeb5ce feat(pdf): add poSubtotal helper for PO PDF totals
Co-authored-by: Orca <help@stably.ai>
2026-08-06 11:54:23 +02:00
SJCoetz
40d069b5aa docs(plan): add 15% VAT to supplier PO PDF implementation plan
Co-authored-by: Orca <help@stably.ai>
2026-08-06 11:48:21 +02:00
SJCoetz
a663de2d36 docs(spec): clarify placement of vat computation
Co-authored-by: Orca <help@stably.ai>
2026-08-06 11:45:11 +02:00
SJCoetz
3beedc7ad6 docs(spec): add 15% VAT to supplier-facing PO PDF
Co-authored-by: Orca <help@stably.ai>
2026-08-06 11:44:54 +02:00
SJCoetz
5053ec96ea test: add quote detail page layout regression tests
Add a vitest suite that enforces critical layout invariants for the quote detail page, and include accompanying Playwright trace snapshots and console logs from the test execution.
2026-08-06 02:04:28 +02:00
SJCoetz
e2bcafe4d8 chore(playwright-mcp): add test snapshot and console logs 2026-08-06 01:56:47 +02:00
SJCoetz
2bd8fcf247 fix: resolve PocketBase expand projection bug
This commit fixes a critical PocketBase projection bug where omitting "expand" from fields lists silently drops related data, leading to missing supplier/customer/order info across the app. It also includes usability improvements and test coverage:

1.  Add "expand" to PB fields projections in invoicing page, quotes lookup API, sprint page, and quote search helper
2.  Update PO Sent tab filter to exclude "collected" and "ready_invoice" statuses
3.  Reorder buyer tasks list to sort by supplier first, then order number
4.  Add comprehensive regression tests for all fixed issues

Included Playwright test capture artifacts from local testing.
2026-08-06 01:33:16 +02:00
SJCoetz
d8dea63d16 feat(invoicing): include sibling order items in invoicing rows
- extract reusable constants and helper functions including escapeFilterValue, dedupeRawRows, listAllInvoicingRows, and loadInvoicingRows
- add safe filter value escaping to prevent query injection risks
- rewrite data fetching to pull all related order items for orders with ready-to-invoice rows, then deduplicate results
- replace raw getFullList calls with paginated PocketBase API usage
- update test suite to mock getList instead of getFullList and add coverage for new behavior
- clean up redundant eslint disable comments in modal components
2026-08-05 23:17:06 +02:00
SJCoetz
9f601002af refactor(collection-confirm): make PO cards start collapsed by default
Remove the initial open state for the first PO card, and update all tests to expect the default collapsed state. Users can expand cards manually to view contents, improving scannability for long PO queues.
2026-08-05 23:08:44 +02:00
SJCoetz
12fa65a481 feat: add new order, invoice, and dashboard management tools
- 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
2026-08-05 23:00:54 +02:00
SJCoetz
d79c433704 fix(quotes): import QuoteItem + return real array from dedup branch
Production deploy failed the TypeScript type-check (next build exited 1)
because commit 2acfd2d changed CreateQuoteFromEmailResult.items from
'number' to 'QuoteItem[]' without updating:

  1. The type import — QuoteItem was never added to the import from
     @/lib/types (line 6 only brought in Quote, Customer, Company).

  2. The dedup branch (line 1060-1076) — still passed itemsCount: number
     where the new shape requires QuoteItem[]. Now fetches the real rows
     via getFullList and sets items_count = items.length for back-compat.

The build log shows the error was visible right at 'Running TypeScript ...'
but the deployment platform's log scraper cut off the actual stderr,
making it look like an opaque DeploymentException. Reproduced locally
with 'npm run build' to confirm both errors.

Build now passes (no next build / tsc failures). All 1392 tests pass.

Co-authored-by: Orca <help@stably.ai>
2026-08-05 16:18:31 +02:00
SJCoetz
ba1f916102 feat(orders): show From + Subject in review modal Source of truth header
The Source of truth panel previously only showed the email subject and
body inside a collapsed-by-default toggle. Operators couldn't see who
sent the mail or what the subject was without expanding the body.

This change adds a persistent header above the collapsible body with
two always-visible lines:

  From:     jane@acme.test    (AtSign icon)
  Subject:  RFQ for 10x bolts  (Mail icon)

Sender resolution chain (in getOrderReviewData):

  1. order.email_sender  — n8n ingest snapshot of the actual From:
                           on the inbound RFQ (best signal)
  2. quote.email_sender  — covers pre-snapshot legacy orders
  3. customer.email      — last-resort fallback
  4. ''                  — renders as '(unknown sender)' placeholder

Subject already lived on the same record; the header just promotes it
out of the collapsed body so it's visible without expanding. The
existing '(no subject)' / '(no body)' placeholder pattern is matched
with a new '(unknown sender)' placeholder.

Tests: 7 new (3 modal header, 4 service sender fallback chain). 101
passing total.

Co-authored-by: Orca <help@stably.ai>
2026-08-05 16:09:53 +02:00
SJCoetz
7f1603d9b0 test(mcp): cover order_items.update + quotes.update regenerate_pdf
- 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>
2026-08-05 15:52:30 +02:00
SJCoetz
106af81934 feat(mcp): order_items.update for invoice reconciliation + quotes.update regenerate_pdf
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>
2026-08-05 15:52:30 +02:00
SJCoetz
f76b235e9a test(mcp): cover quotes.get_pdf, list_attachments, and the two new order attachment tools
- 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>
2026-08-05 11:51:23 +02:00
SJCoetz
b3344f9f47 feat(mcp): quote PDF retrieval + order/quote attachment tools
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>
2026-08-05 11:51:23 +02:00
SJCoetz
0ac05b847b test(mcp): cover quotes.update + create_from_email unit field
- 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>
2026-08-05 11:39:16 +02:00
SJCoetz
2acfd2d346 fix(mcp): add quotes.update + preserve unit in create_from_email
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>
2026-08-05 11:39:16 +02:00
SJCoetz
c41908f4b7 feat(quote-items): CRUD via REST + MCP for AI auto-fill agents
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>
2026-08-05 02:40:09 +02:00
SJCoetz
0013ff5267 docs(bugs): add post-mortem for MCP initialize handshake bug
Captures the full timeline of the 2026-08-04 incident where WFDASH
/api/mcp silently failed every spec-compliant MCP client's handshake
because the route didn't handle the mandatory 'initialize' method.

Includes:
  - Symptom (Hermes log lines + WebUI '0 tools' display)
  - Root cause (HTTP 404 on initialize → client terminates session)
  - Fix summary (the three new handlers, with the critical 'don't use
    the content envelope for initialize' gotcha)
  - Verification recipe (curl against the live endpoint)
  - Lessons to apply to other JSON-RPC endpoints we might add later
    (initialize is mandatory; don't reuse the tools/call envelope;
    HTTP 404 is a signal the endpoint isn't MCP at all)

Co-authored-by: Orca <help@stably.ai>
2026-08-05 01:53:02 +02:00
SJCoetz
fd07b514ab fix(mcp): handle initialize/notifications/initialized/ping handshake
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>
2026-08-05 01:53:02 +02:00
SJCoetz
0455ddd346 docs(giellie): correct tool count (70 → 69) + add post-redeploy verification
The earlier change log claimed the catalog went 69 → 70 after wiring
tools.discovery, but tools.discovery was already in the catalog from
the start — only its runtime behavior changed from 'Unknown tool' to
functional. Live post-redeploy smoke test confirms 69 tools.

Fix all six '70' references in the doc to '69', and append a second
change-log entry recording what was verified live after the redeploy:
  - tools.discovery returns the flat 69-tool catalog via JSON-RPC ✅
  - GET /api/mcp still works ✅
  - 5 other bugs reproduce exactly as documented (validates the doc) ✅
  - ISHERWOOD + CNL letterhead PDF regeneration on Q000087 still works ✅

Co-authored-by: Orca <help@stably.ai>
2026-08-04 22:53:46 +02:00
SJCoetz
8fe80fefc8 docs(giellie): refresh skill doc — corrected host, all tokens, smoke-test findings
Bring the Giellie agent's operating manual in line with the live production
state after the 2026-08-04 smoke test against https://wfdash.apphub.co.za.

Bugs / quirks documented (with workarounds):
  - tools.discovery was unwired; now fixed in code (separate commit),
    live server needs redeploy before response changes
  - orders.list silently ignores 'filter'; only 'search' is honoured
  - quotes.lookup returns opaque 'fetch failed' — use quotes.list instead
  - collection_pool.list + sprint.list return 'Missing collection context'
  - orders.get 'not found' for at least one valid id
  - quotes.items.rewrite was incorrectly listed as an MCP tool (it's REST)

Architectural gaps documented:
  - No quote_items.list MCP tool; quotes.create_from_email returns item
    count only, so a freshly-ingested quote's item IDs can't be recovered
    via the agent API
  - is_processed is intentionally NOT in bulk_update_quote_items whitelist;
    end-to-end create→price→render pipeline requires human in the UI

Credentials & endpoints now include every token the agent might need:
AGENT_API_KEY, PB admin email/password, n8n base URL + API key, six
n8n webhook URLs (rewriter, price-list match, PO send, AI insights,
markup approval, customer list, QB invoice creator), TomTom map key.

Other updates:
  - Replaced stale base URL wfdash.davidels.dev with wfdash.apphub.co.za
  - Bumped tool count 69 → 70 (tools.discovery added to catalog)
  - Added three-way tool discovery guide (tools/list, tools.discovery,
    GET /api/mcp)
  - Added 'Quote PDF rendering' guide explaining the 422 gates,
    status transitions, and letterhead codes
  - Added change log at the bottom
  - All worked examples updated to the correct host

Co-authored-by: Orca <help@stably.ai>
2026-08-04 22:47:55 +02:00
SJCoetz
ff1709c2da fix(mcp): implement tools.discovery handler returning flat tool catalog
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>
2026-08-04 22:47:50 +02:00