WFDASH-WEBAPP/AGENTS.md

174 lines
8.5 KiB
Markdown
Raw Normal View History

fix(api,layout): sprint 500 chunk + add 9 missing top-level agent routes + metadataBase; drop unused collection_pool feature; quote counter fix migration 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.
2026-07-26 20:18:49 +00:00
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
<!-- BEGIN:pb-select-field-gotcha -->
# PocketBase select fields are not self-service
The `order_items.item_status` field is a `select` with a fixed list of
values. If you want to set a new value (e.g. `delivery_note` for the
new delivery-note flow), you must add it to the schema in the PB admin
**first** — the API will 500 with "Failed to update record." otherwise.
The local `pb-exports/pb_schema-2.json` is a snapshot for reference
only; the source of truth is the running PB instance.
If a new code path sets a select value that doesn't exist yet, ask the
user to add it via the PB admin before debugging the API. They do this
manually (it's a UI-only change in the PB admin).
<!-- END:pb-select-field-gotcha -->
<!-- BEGIN:pb-attachments-pattern -->
# Reuse existing multi-file fields for new attachment types
The `order_items` collection has a multi-file field `order_item_attachements`
(`maxSelect: 10`, accepts any MIME). When adding a new document type
to attach to an order item (e.g. delivery note PDF), upload the file
to this field rather than creating a new attachment collection. The
operator queries by some key field on the item (e.g.
`delivery_note_number`) to find the items, then reads the attachments
off each item.
Pattern in code:
1. POST endpoint: generate the PDF, upload via multipart/form-data
using the `order_item_attachements` field on each item in the group
2. GET endpoint: query `order_items` by the key field, find a file
whose name matches the expected pattern (e.g. `DN26071401.pdf`),
stream the bytes back via PB's `/api/files/...` URL
3. Stable filename per document type so the retrieval side can
disambiguate from other attachments on the same item
<!-- END:pb-attachments-pattern -->
2026-09-01 23:17:05 +00:00
<!-- BEGIN:file-url-paths -->
# File URLs go through `/api/v1/files/`, never `/api/files/`
The WFDASH file-serving proxy lives at
`/api/v1/files/{collection}/{recordId}/{filename}` (route file:
`src/app/api/v1/files/[collection]/[recordId]/[filename]/route.ts`).
Every `<a href={...}>` and every fetch from a client component that
opens a repair/quote/order attachment must use the `/api/v1/` prefix.
`/api/files/{collection}/{recordId}/{filename}` (no `v1/`) 404s —
that's the *PocketBase* convention, not a route in this app. The
PB instance serves files at that path, and the WFDASH proxy
forwards to it server-side, but a client that points directly at
`/api/files/...` gets the Next.js 404 page.
**Gotcha:** PocketBase record responses (e.g. `record.delivery_note`
as a `string[]` of filenames) sometimes come back as fully-qualified
PB URLs like `http://pb-host/api/files/...` — those should be
re-written to `/api/v1/files/...` on the client before rendering.
The existing detail page builds the path from scratch from the
record id + filename, which sidesteps the issue. Don't blindly
trust the URL PB hands back.
The `src/app/api/invoicing/delivery-note-pdf/route.test.ts` test
file intentionally matches `/api/files/...` in an expect — that's
asserting the *raw PB response* shape, not the URL the client
should use. Different concern.
<!-- END:file-url-paths -->
fix(api,layout): sprint 500 chunk + add 9 missing top-level agent routes + metadataBase; drop unused collection_pool feature; quote counter fix migration 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.
2026-07-26 20:18:49 +00:00
<!-- BEGIN:data-layer-discipline -->
# Data access layer discipline
The app has two coexisting data layers:
- **Legacy**: `src/lib/pb-helpers.ts` — used directly by Server Components
under `src/app/(app)/**/page.tsx` and form components.
- **Current**: `src/lib/server/*-service.ts` — exposed via `/api/v1/*` route
handlers and `/api/mcp`, used by the agent API and external integrations.
Both layers read the same PocketBase collections. Pick the right one
depending on what you're doing.
## Rules
**For new endpoints / features**: put business logic in
`src/lib/server/<feature>-service.ts` and expose it via a thin
`src/app/api/v1/<feature>/<op>/route.ts`. Do NOT extend `pb-helpers.ts`.
**For new pages / form components**: call `/api/v1/*` (or have the
Server Component call the corresponding `lib/server/*-service.ts`
function directly). Do NOT import from `pb-helpers.ts`.
**When touching a page that already uses `pb-helpers.ts`**: migrate its
data calls to the service layer in the same commit. One resource at a
time, no big-bang rewrites. Lowest-risk order: customers → orders →
quotes → suppliers → companies → everything else.
## How to spot you're reaching for the legacy layer
`pb-helpers.ts` is the file in question. Grep for it:
```bash
grep -rn 'from "@/lib/pb-helpers"' src/ --include="*.ts" --include="*.tsx"
```
Each hit is one caller to migrate when you're in the area.
## Migration cost indicator
A single page rewrite is typically: replace `getX(...)` import + call
with `await fetch('/api/v1/x?...')` (or a direct service-layer call),
then update the page to await. Half-day budget for 5 pages if no
weird cross-resource joins are involved.
<!-- END:data-layer-discipline -->
# Audit trail (`audit_log`) is written by a PB hook, not by app code
Every create / update / delete on `quotes`, `quote_items`,
`quote_attachments`, `orders`, `order_items` and `delivery_notes` is
recorded by `pocketbase/pb_hooks/audit_log.pb.js` (+ `audit_log_utils.js`)
into the `audit_log` collection. Don't add hand-written "audit" notes for
field changes — the hook already captures from → to per field.
Attribution: dashboard users writing straight to PB are credited
automatically. Server-side writes go through `getAdminPocketBase()`, which
attaches `X-Audit-*` headers from `src/lib/server/audit-context.ts` (the
browser forwards its PB token as `x-wfdash-auth`; PB verifies it). If a
server path knows the actor better than the request does (MCP `actor`
args), wrap it in `runWithAuditActor(...)`. n8n writes appear as
"Automation" unless the workflow sends `X-Audit-Actor-Name`.
Read side: `src/lib/server/audit-log-service.ts` →
`/api/v1/audit/{quote|order|delivery_note}/{id}` → `<AuditTimeline>`.
Noisy fields (search/AI/timers) are skipped via `IGNORED_FIELDS` in the
hook utils — add new auto-updated fields there.
The hook files are deployed by hand into the PB server's `pb_hooks/`
(like select values, it's a manual step on the live instance).
# Giellie agent (`/giellie`) — tools come from the MCP, policy lives in one file
The sidebar "Giellie Agent" is a MiniMax chat agent (`src/lib/server/giellie-*.ts`,
routes under `/api/v1/giellie/*`, chats in the `giellie_chats` PB collection).
It calls the **same MCP handler** as Hermes/n8n (`src/app/api/mcp/route.ts`),
in-process — a new MCP tool is automatically available to Giellie.
- Safety lives in `src/lib/server/giellie-tool-policy.ts`: `*.delete` and secret/base64
tools are denied; only tools in `READ_TOOLS` run without approval; everything else
(including brand-new tools) pauses for an Approve/Reject card; email/PO sends always
ask. **When you add a read-only MCP tool, add it to `READ_TOOLS`** or Giellie will
ask approval for every call.
- Every `/api/v1/giellie/*` route authenticates with `getBrowserSessionUser(req)`
(PB token in `Authorization`). These routes hold `AGENT_API_KEY`; never expose one
without that check.
- Config (key, model, base URL, on/off) is the existing `/settings/ai` row; Giellie
uses its own system prompt in `giellie-system-prompt.ts`.
- Classification is **args-aware**: use `classifyToolCall(name, input)` (not bare
`classifyTool`) for routing. A plain write whose input carries a contact-routing key
(`email*`, `bcc`, `cc`, `sender`, `recipient`, any depth ≤ 4) is escalated to "confirm".
`quotes.re_run_ai` is denied (it wipes every quote item); `orders.cancel`,
`delivery_notes.cancel` and `delivery_notes.combine` always ask.
- The "Trust this chat" toggle is **per chat** and only auto-approves ordinary writes.
It never covers confirm-class actions: email/PO sends, cancels/merges, and changes to
email/contact fields.
- Env: `AGENT_API_KEY` (server-side MCP auth, required) and, in containers,
`AGENT_API_BASE_URL` (a full `https://` origin) so the MCP's loopback calls reach the
app; scheme-less values like `localhost:4001` are ignored.
- The `giellie_chats` collection is created by `npm run pb:migrate:giellie-chats`
(manual step on the live PB, like select values and pb_hooks). Its API rules are
stricter than the original spec: users may list/view their own chats only;
create/update/delete are null (all writes go through the admin client in
`giellie-chat-store.ts`).