/ocr-extract answers { extracted: {...}, raw: "..." }. The client checked
`fields` first and `extracted` second, which was right by luck — but an
extraction that returned nothing, or threw, did so silently: the button
went back to idle and the operator had no way to tell a read from a
no-read.
Now an empty or failed extraction says so under the field, and either
way the UPLOAD survives. The file is already stored and referenced; a
failed read only means the fields are not pre-filled, which someone can
recover by typing. Losing the upload because the read failed would not
be recoverable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/upload requires it and it is fixed for this app, but the client only
appended it when ctx happened to carry it — and FileField passes only
activity, field and instance. Every upload came back 400 with
"Missing required params: workflow_uuid".
Taken from config now, so it cannot be forgotten by a caller.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collect Documents shipped with every document field rendering as a plain
text input — ActivityForm had no case for `file` or `ocr`, so they fell
through to the default. There was a form asking you to type an RC book.
Uploads happen ON PICK rather than on submit, because /ocr-extract takes
a REFERENCE to a stored file: the document crosses the wire once, it
survives a reload (a draft cannot carry a File), and re-extracting costs
no second upload.
For an ocr field the extracted values are shown inline — "read from the
document" — and filled into the fields they map to. Only into fields that
are still blank: a value a person typed is never overwritten by one a
model read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The activity was added to the workflow on both Contacted and Proposal
Accepted, but not to the console's action map — so the platform allowed
it, the console never offered it, and nothing errored anywhere. There was
simply no way to upload a document.
Notes the hazard on the map itself: it is hand-maintained against
tbl_wf_state_allowed_activities, and anything missing from it is
invisible rather than broken.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The timeline carried timestamps; the two screens people actually act on
carried none. A queue with no dates cannot tell you which lead is going
stale, and on a renewal book the countdown to expiry is the number that
decides whether anyone should act today — it was buried in a field group.
Queue gains "Renewal due" and "Added", both absolute plus relative: the
absolute answers "when exactly", the relative answers "is this stale".
The countdown is toned — lapsed, urgent, soon, later — on the same
thresholds the workflow itself uses to decide whether to call, so the
screen and the agent are reading the same bands.
Lead header gains the countdown, the current insurer, the registration,
and when the lead arrived and was last touched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The lead file listed the current field values and nothing about how they
got there. On an app whose whole claim is that agents do the work, that
is the wrong thing to show: you could see the outcome and not whether a
machine or a person produced it.
Adds a timeline from the audit trail — every activity in order, badged by
actor, carrying the reasoning the agent actually wrote (attribution,
eligibility, call notes, recommendation rationale, the premium breakup)
rather than the raw field dump that is already below it.
Colour encodes something real: machine work is blue, people amber, the
platform grey, so a file reads at a glance for how much of it was done by
hand.
Also fixes the audit path — the bare /view/audit is not an API route and
falls through to the SPA, returning HTML with a 200 that parses as a JSON
error rather than an HTTP one. It has to be app-scoped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This demo is about the individual agent who sources a lead and gets paid
when it onboards, so the console offers that one entry point and goes
straight to its form rather than showing a chooser with one option.
The direct and bancassurance INIT activities are hidden, not removed —
both still work over the API with their own permissions, and the
bancassurance door is what makes the cross-channel duplicate story
possible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lead_ref is now an id_gen, issued server-side and stripped from the
submission — so the form shows it as auto rather than as an empty box
somebody is expected to type a reference code into, and never sends it.
Also seeds the inputs from the prefill the form response carries. The
door's channel and the signed-in agent are stamped by a prefill pipeline,
so the form should show them already filled rather than ask.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pipeline could be looked at but not driven. Adds the three pieces
that make the machine walkable end to end from the UI.
- ActivityForm renders whatever /view/form-screens returns — labels,
types, select options, mandatory flags — and submits it back. No form
is defined in this repo, so a field added in Studio appears here with
no code change.
- AddLead presents the three doors. Each is a separate INIT activity with
its own permissions; the bank one lands further along because the bank
already did CKYC.
- Lead shows the file grouped in the order it was worked, ending with
what the sourcing agent earns, plus the activities this state allows
and who normally performs each.
The "who normally performs this" label is presentational only. Nothing
here enforces anything — the workflow refuses server-side and the form
reports what it said, including a note when a 403 is the platform
declining rather than the console misbehaving.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The POST body is not the GET query params with a different verb. The view
is named by rv_template_uid (rv_id is the GET spelling) and paging, sort
and filters all live inside search_query. Sending them at the top level
returns 400 "Missing param: rv_template_uid (body)".
This would have failed the moment the record view was seeded, and the
400 would have read as "the view is missing" rather than "the request
was wrong" — the queue screen renders exactly that notice on any error.
Verified against dev: the old shape returns the missing-param 400, the
new one returns "record view not found", which is the view genuinely not
existing yet.
Also adds rows(), which normalises the response key — workflow views
return `data`, rdbms views have been seen returning `records`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The build asserts on <!--BASE_HREF--> and placement fails without it, so
nothing was ever placed and nginx 404'd. The ./config.js tag is the other
half: it carries this environment's VITE_ZINO_API_URL, and without it
requireConfigValue throws and the app renders nothing even once served.
Order matters — the placeholder precedes the favicon href, or that
relative ref resolves against the wrong root once mounted.
Verified by simulating placement locally: filled the placeholder, wrote a
config.js beside the artifact, served it under /zurich-kotak/ and loaded
it. index.html, the JS bundle, config.js and the favicon all 200, the
login renders, and with config.js removed the root stays empty rather
than falling back to some other backend.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RBAC is what this demo argues, so which persona you are signed in as
should never be a guess while presenting.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scaffold had the brand right and nothing behind it: the login was a
600ms setTimeout with a TODO, and three pieces of the frontgen deploy
contract were missing.
- Sign in against POST /usr/login (org_id as a STRING — a number is
rejected by the gateway) with the session in a provider; surface the
gateway's own message rather than a generic "invalid credentials",
because a wrong password and a user without access to this app look
identical from here and are not.
- Runtime config: the API URL is read from the config.js the server
writes at placement, never compiled in, and requireConfigValue throws
so a build with no config.js fails loudly instead of calling whichever
backend built it.
- base: './' plus a router basename taken from <base href>, so one build
serves any mount path.
- Move the fonts and logo from public/ into src/assets/ — Vite rewrites
bundled asset URLs to be relative, while a public/ file referenced as
"/fonts/..." stays absolute and 404s under the /zurich-kotak/ mount.
- API client for recordview / detailview / form-screens / start /
activity, and the pipeline shell whose sidebar is the state machine in
the order a lead moves.
Queue and lead-file data wait on the record and detail views.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>