diff --git a/CHANGELOG.md b/CHANGELOG.md index 758fe3f..2b1ae4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ This file records what changes **in the product** – process and session state ## [Unreleased] ### Added +- Prepared samples for the showcase (`pnpm seed:samples`): per demo company one synthetic request to review + (with an uncertain and a missing value) and one already approved and exported through the normal path. Their + extraction replays a recorded AI answer, so they work even while the model provider is overloaded; list and + detail label them "Vorbereitetes Beispiel – aufgezeichnete KI-Antwort" (#71). - ERP export sends the reviewed positions (description, quantity, unit, material, dimensions) with each approved request – ERP contract 1.1.0, additive and optional. A position value the ERP would refuse blocks the approval so the clerk can still correct it. diff --git a/docs/technical/architecture.md b/docs/technical/architecture.md index a97e7bc..c6d2387 100644 --- a/docs/technical/architecture.md +++ b/docs/technical/architecture.md @@ -30,6 +30,7 @@ Every new file belongs to one of these modules – otherwise add the module here | `review` | `src/features/review/` | review UI, corrections, approve/reject | authenticated UI | confidential + personal | session, role check, audit | built: review page (fields + status badges + source view for mail, PDF incl. OCR label, XLSX cells, DOCX paragraphs/tables, attachments; line items as a table with per-field status and audited corrections), corrections with history, approve (→ export job) / reject with reason, duplicate decision (confirm or reject as duplicate) | | `export` | `src/features/export/` | ERP port + REST adapter, idempotency | outbound HTTP | confidential | idempotency key, unique export, timeout | built: REST adapter (timeout, error classes, contract validation), export handler under row lock, `drainExports()`, `request_exports` | | `erp-mock` | `src/features/erp-mock/` | simulated ERP REST API | route behind flag | synthetic | disabled unless `ERP_MOCK_ENABLED` | built: idempotent receiver (replay → same reference, 409 on a different body), fault injection, bounded in-memory store, route `/api/erp-mock/v1/quote-requests` | +| `samples` | `src/features/samples/`, entrypoints `src/seed-samples.ts` (`pnpm seed:samples`) and `src/samples-record.ts` (`pnpm samples:record`) | prepared showcase cases: synthetic mails with a recorded AI answer, seeded through intake → processing → approval | operator scripts only | synthetic | no model call when seeding (recording replayed inline, no queued processing job); requests marked `source = 'sample'` (CHECK, migration 0019) | built: two samples per demo company – one in review (uncertain + missing value), one approved with its export queued (#71) | | `identity` | `src/features/identity/` | Better Auth, users, companies, roles | public login route | personal (staff) | rate limit, invite-only | built: Better Auth (invite-only, organization + admin plugins), `authorize()`, audited invite, user management (`/users`: roles, deactivate/reactivate, last-admin rule), seed | | `tenancy` | `src/features/tenancy/` | `withTenant()`, RLS policies | internal | – | forced RLS, `app_rw` without BYPASSRLS | built: `withTenant()`, forced RLS on `app.*`, guard test (every `app` table: `company_id`, forced RLS, only company policies; allow-list empty) | | `audit` | `src/features/audit/` | append-only audit events | internal | personal (staff) | INSERT/SELECT only | partial: `recordAudit()` (append-only enforced by grants) | diff --git a/docs/technical/deployment-vercel.md b/docs/technical/deployment-vercel.md index fd9414f..e7483ce 100644 --- a/docs/technical/deployment-vercel.md +++ b/docs/technical/deployment-vercel.md @@ -118,6 +118,13 @@ With the same shell variables and a strong `SEED_PASSWORD` (≥ 12 chars, passwo creates the two synthetic companies with their admins (invite-only; see `src/seed.ts`). Hand the accounts out only to people who may see the demo. +Then `pnpm seed:samples` (same variables and `SEED_PASSWORD`) prepares per company one sample to review and one +approved sample that it exports right away through the normal export path to the configured ERP (the ERP mock +route; if it is not reachable, the export is queued and the next drain retries it) (#71). Their extraction replays answers recorded +once with `pnpm samples:record` (`src/features/samples/data/`) – no model call, so the samples work even while the +model provider is overloaded. List and detail label them „Vorbereitetes Beispiel – aufgezeichnete KI-Antwort“. The +step is idempotent: repeat it whenever visitors have decided the samples, and it adds only what is missing. + ## 7. Jobs without a worker - `after()`: with `JOB_DRAIN_INLINE=true`, upload, approval and "Erneut verarbeiten" drain once after the diff --git a/eslint.config.mjs b/eslint.config.mjs index 24e63f4..bb89191 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,9 +12,10 @@ const config = [ }, }, // Logs are a data-leak path (#28): application code logs through `logEvent` (fixed key set, IDs and - // codes only). Exempt: the operator's seed script and deploy step (`setup.ts`) – no tenant data, - // their messages explain role/connection problems with URLs masked. - { files: ["src/**/*.{ts,tsx}"], ignores: ["src/seed.ts", "src/setup.ts"], rules: { "no-console": "error" } }, + // codes only). Exempt: the operator's seed scripts (`seed.ts`, `seed-samples.ts`), the sample recorder + // (`samples-record.ts`, #71) and the deploy step (`setup.ts`) – they print sample keys, statuses and + // field statuses of synthetic mails, never tenant data or secrets; setup masks URLs. + { files: ["src/**/*.{ts,tsx}"], ignores: ["src/seed.ts", "src/seed-samples.ts", "src/samples-record.ts", "src/setup.ts"], rules: { "no-console": "error" } }, ]; export default config; diff --git a/package.json b/package.json index 25eb039..ad0f015 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "verify:full": "pnpm verify && pnpm e2e && pnpm evals", "setup:deploy": "tsx src/setup.ts", "seed:demo": "tsx src/seed.ts", + "seed:samples": "tsx src/seed-samples.ts", + "samples:record": "tsx src/samples-record.ts", "contract:types": "openapi-typescript contracts/ai-service.openapi.yaml -o src/features/extraction/ai-service.contract.ts && openapi-typescript contracts/erp-export.openapi.yaml -o src/features/export/erp-export.contract.ts", "e2e": "playwright test", "evals": "uv run --project services/ai --directory services/ai python -m requestflow_ai.evals --replay" diff --git a/src/app/requests/[id]/page.tsx b/src/app/requests/[id]/page.tsx index 62689aa..1b3f5f4 100644 --- a/src/app/requests/[id]/page.tsx +++ b/src/app/requests/[id]/page.tsx @@ -5,6 +5,7 @@ import { getRuntime, requestActor } from "@/app/_server/runtime"; import { duplicateDecidable, loadReview, REJECTION_REASON_MAX, type ReviewField } from "@/features/review"; import { processingNotice } from "../processing-notice"; import { requestRowView } from "../row-view"; +import { SAMPLE_EXPLANATION, SAMPLE_LABEL } from "../sample-label"; import { StatusPill } from "../status-pill"; import { DONE_MESSAGES, ERROR_MESSAGES, messageFor } from "./messages"; import { approveAction, confirmNotDuplicateAction, correctFieldAction, rejectAction, rejectAsDuplicateAction } from "./actions"; @@ -83,6 +84,11 @@ export default async function RequestPage({ Eingang {dateFormat.format(request.createdAt)} + {request.source === "sample" && ( +

+ {SAMPLE_LABEL}. {SAMPLE_EXPLANATION} +

+ )} {done &&

{done}

} {error &&

{error}

} {request.status === "ERROR" && request.errorMessage && ( diff --git a/src/app/requests/page.tsx b/src/app/requests/page.tsx index cb66dfd..b24acd7 100644 --- a/src/app/requests/page.tsx +++ b/src/app/requests/page.tsx @@ -6,6 +6,7 @@ import { listExportRecords } from "@/features/export"; import { listRequests, parseCursor, type RequestFilter } from "@/features/requests"; import { reprocessAction } from "./actions"; import { requestRowView } from "./row-view"; +import { SAMPLE_LABEL } from "./sample-label"; import { REQUEST_STATUS_LABEL } from "./status-labels"; import { StatusPill } from "./status-pill"; import { UploadForm } from "./upload-form"; @@ -125,6 +126,14 @@ export default async function RequestsPage({ searchParams }: { searchParams: Pro {dateFormat.format(request.createdAt)} {request.subject ?? "(ohne Betreff)"} + {request.source === "sample" && ( + <> + {" "} + + {SAMPLE_LABEL} + + + )}
@@ -144,7 +153,8 @@ export default async function RequestsPage({ searchParams }: { searchParams: Pro )} - {request.status === "ERROR" && ( + {/* A sample in ERROR from processing is not reprocessed: that would call the live model (#71). */} + {request.status === "ERROR" && !(request.source === "sample" && request.errorStage === "processing") && (