Traced the console against the workflow it renders. Five gaps, all of which made a working chain look like a broken one. THE AUDIT TRAIL WAS READING THE WRONG KEYS. An audit row's data is keyed by the ACTIVITY's field ids, which carry a per-form suffix — the call notes arrive as contact_notes_2, the document request as documents_notes_3 — and the timeline matched the unsuffixed global ids. Almost nothing ever matched, so the trail was a list of activity names with the agents' reasoning invisible behind it. The one narrative line that did appear was an accident. It now reads the platform's own typed `fields[]` (label, data_type, value) and matches on the base id. That is also what lets it answer the question that was asked: an upload now renders as "3 documents received" with each one named and openable, instead of a bare "Collect Documents". DATA_UPDATE ROWS WERE CLASSED AS AGENT WORK. A bookkeeping row inherits the roles of whoever caused it, and the AI check ran first — so an AI's field write appeared in the trail as an entry titled "Data updated", while the toggle underneath still offered to reveal the others. The activity id says a row is bookkeeping; the roles say who triggered it. DOCUMENT PENDING IS TWO SITUATIONS. Before the upload a person has to act; after it the lead stays in the same state while three AI steps run. Both rendered as "waiting on the partner agent", so a lead that had just been served showed an Upload documents button under a panel saying the documents had been received, and counted against the Action-required queue. phaseOf() derives the difference once, from documents_status, and the header, the queue count, the row status and the action list all read it. The upload demotes to recovery — "Replace or add a document", folded away with the other levers. A STOPPED CHAIN LOOKED IDENTICAL TO A RUNNING ONE. The rating engine refuses to price without an IDV and writes so into quoted_breakup; Engage then declines to raise a quote it would have to fabricate. Both are right, and nobody was told: the refusal sat in a field on a tab and the lead never moved again. blockedOn() surfaces it as an amber strip that names the missing value and opens the form that carries it. THE STALL MEASURE WAS DATED FROM THE WRONG COLUMN. progress() fell back to created_at when updated_at was absent — which it always was, because neither view returned it — so every lead older than half an hour would have reported stalled. It reads updated_at only; the view supplies it as of 76. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| public/brand | ||
| src | ||
| .env.example | ||
| .gitignore | ||
| CLAUDE.md | ||
| eslint.config.js | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| vite.config.js | ||
Zurich Kotak — Lead Desk
Operator console for the Zurich Kotak "Lead to Policy" demo. Leads arrive through three channels — direct, bancassurance and agency — and run one state machine to policy issuance. The agents do the work; a person appears at one queue.
Workflow config is seeded from sm2/custom-apps/zurich-kotak/ (org 84,
app 536, workflow zk_wf_lead). Design doc:
sm2/app-designs/zurich-kotak/design.html. Workflow spec PDF:
sm2/custom-apps/zurich-kotak/zurich-kotak-workflow.pdf.
Run it
npm install
npm run dev # http://localhost:5175
npm run build # → dist/
.env carries the API host for local dev only:
VITE_ZINO_API_URL=https://dev.getzino.in
Logins (all password ZurichKotak@2026, org 84):
| Role | Can do | |
|---|---|---|
sanjay.uw@zurichkotak.example |
SME Underwriter | Clear / Decline Referral — the only human decision |
meera.rm@zurichkotak.example |
Bancassurance RM | Submit a Partner Bank Lead |
arjun.posp@zurichkotak.example |
POSP / Broker | Submit a Partner Agent Lead |
kavya.csr@zurichkotak.example |
Direct / Call centre | Submit a Self-Serve Lead |
deepa.ops@zurichkotak.example |
Operations | Cross-channel visibility |
Sign in as Sanjay to land in Referred to Underwriting, which is where the demo happens.
How it talks to the platform
Everything goes through src/api/client.js:
POST /usr/login— auth.org_idmust be a string; a number returnscannot unmarshal number into Go struct field LoginRequest.org_id.POST /app/536/view/recordview— every queue is one record view (zk-rv-leads) filtered server-side oncurrent_state_name.POST /app/536/view/form-screensthenPOST /app/536/activity— forms are read from the live activity schema and submitted straight back.
Forms are not defined in this repo
Whatever /view/form-screens returns is what renders — labels, types, select
options, which fields are mandatory. Add a field to an activity in Studio,
redeploy, and it appears here with no frontend change. That is deliberate: the
workflow is the source of truth, and a hardcoded form would quietly diverge
from it.
Permissions are never enforced here
The console does not hide a button to stop someone using it. Every permission
decision is the workflow's, made server-side on each submission — the platform
refuses and this app reports what it said. Two refusals worth knowing:
/activity answers 403, /start answers 400, and both carry
permission denied.
The runtime-config contract
VITE_ZINO_API_URL is read at runtime from a config.js the server writes
when it places the build — never compiled in. One artifact is promoted between
environments unchanged, so a build-time URL would point every environment at
whichever backend happened to build it. requireConfigValue throws if it is
missing, so a production build with no config.js fails loudly rather than
calling the wrong backend.
vite.config.js uses base: './' and the router takes its basename from the
<base href> the server writes, so one build serves any mount path. Do not
reintroduce a build-time base.
This is also why the fonts and logo live under src/assets/ and not
public/. Vite rewrites bundled asset URLs to be relative; a public/ file
referenced as /fonts/… stays absolute and 404s under the /zurich-kotak/
mount path. The favicon is the one exception — it stays in public/brand/ and
is referenced relatively from index.html.
Deployment
Registered with frontgen by sm2/custom-apps/zurich-kotak/05_frontgen_project.sql
(slug zurich-kotak, repo_name zurich_kotak, org 84). A push to main builds
and serves at https://preview-dev.getzino.in/zurich-kotak/.
The webhook only fires on a new push — a push made before the frontgen project row existed matched no project and did nothing.
State of the build
| Piece | Status |
|---|---|
| Brand — Zurich Sans, palette, logo | done (kept from the original scaffold) |
| Sign-in against the real gateway | done |
| Runtime config, relative base, router | done |
Pipeline sidebar (mirrors tbl_wf_states) |
done |
| Queue lists | waiting on the zk-rv-leads record view |
| Lead file, activity forms, attribution panel | waiting on zk-dv-lead + form screens |