Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions docs/technical/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
7 changes: 7 additions & 0 deletions docs/technical/deployment-vercel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions src/app/requests/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -83,6 +84,11 @@ export default async function RequestPage({
<span>Eingang {dateFormat.format(request.createdAt)}</span>
</div>
</div>
{request.source === "sample" && (
<p className="callout callout-info" data-testid="sample-notice">
<strong>{SAMPLE_LABEL}.</strong> {SAMPLE_EXPLANATION}
</p>
)}
{done && <p role="status">{done}</p>}
{error && <p role="alert">{error}</p>}
{request.status === "ERROR" && request.errorMessage && (
Expand Down
12 changes: 11 additions & 1 deletion src/app/requests/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -125,6 +126,14 @@ export default async function RequestsPage({ searchParams }: { searchParams: Pro
<td className="mono">{dateFormat.format(request.createdAt)}</td>
<td className="subject">
<Link href={`/requests/${request.id}`}>{request.subject ?? "(ohne Betreff)"}</Link>
{request.source === "sample" && (
<>
{" "}
<span className="pill pill-info" data-testid="sample-label">
{SAMPLE_LABEL}
</span>
</>
)}
</td>
<td>
<div className="status-cell">
Expand All @@ -144,7 +153,8 @@ export default async function RequestsPage({ searchParams }: { searchParams: Pro
)}
</td>
<td className="num">
{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") && (
<form action={reprocessAction}>
<input type="hidden" name="requestId" value={request.id} />
<button type="submit" className="btn-small" aria-label={`Erneut verarbeiten: ${request.subject ?? "(ohne Betreff)"}`}>
Expand Down
4 changes: 4 additions & 0 deletions src/app/requests/sample-label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Label of a prepared showcase sample (#71) – the same words in list and detail.
export const SAMPLE_LABEL = "Vorbereitetes Beispiel – aufgezeichnete KI-Antwort";
export const SAMPLE_EXPLANATION =
"Diese synthetische Anfrage ist vorbereitet: Ihre KI-Auswertung wurde einmal aufgezeichnet und wird hier wiederverwendet. Eigene Uploads wertet die KI live aus.";
1 change: 1 addition & 0 deletions src/db/migrations/0019_request_source_check.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "app"."requests" ADD CONSTRAINT "requests_source_check" CHECK (source in ('upload', 'sample'));
Loading
Loading