Four things, and the first is the one that mattered.
ROLE-BASED ACCESS. The console showed every action to everyone on
purpose — "the refusal is the demo" — and the workflow refused
server-side. That is a defensible engineering position and a poor
product: an underwriter saw a row of six buttons, five of which 403.
src/api/permissions.js now mirrors tbl_wf_activity_permissions, and the
session already carries user.roles, so nothing new had to be fetched.
Action buttons, sidebar queues and the entry doors are all filtered.
It remains presentation only — the platform still refuses, and a role
added there but missing here hides a button that would have worked,
which is the failure mode to watch.
Each role's app now looks like their job. Ops sees the whole board and
still cannot clear a referral. An underwriter sees one queue and two
buttons. Agents see the three queues they work in. Reporting is NOT
gated: the overview counts the whole portfolio for everyone, because an
agent tracking leads they filed is reasonable and acting on them is not.
One deliberate divergence from the workflow, commented where it lives:
the two scheduled activities carry no roles at all, because that is the
only configuration under which the scheduler can perform them. Open to
the SYSTEM is not open to everyone signing in, so the UI narrows them —
otherwise an underwriter is offered "Send Document Reminder".
ANALYTICS. The overview was three lists. It is now a measured page:
open leads, pipeline value, written premium, conversion, commission —
then renewal exposure in three buckets, the queues needing a person with
the age of the oldest item in each, and stage distribution as bars.
Checked against the live book rather than assumed, which found a real
bug before it shipped: Policy Issued is written business AND not yet
terminal, so its premium was counted in pipeline and production both.
₹42,912 double counted on 37 leads. Pipeline now excludes anything
already on risk.
The lead page gained the same treatment: renewal countdown, lead age,
time in current stage, premium, commission, AI confidence. Age and
dwell are computed — neither is on the record, and they are the two
figures that answer "is this moving?", which no field could.
TERMINOLOGY. The previous pass over-corrected: fixing builder jargon
("the workflow decides, server-side") produced chat ("What you can do",
"The agents have it", "Nothing here right now"). Neither is how an
operations console reads. Now: Actions · Record · Audit trail · Action
required · Customer response · Automated · Scheduled · Document
collection · Underwriting referral · Premium confirmation.
LAYOUT. Prose subtitles cut to one line or removed. Measures render as
tabular figures in a bordered strip instead of label/value pairs. The
overview splits into work on the left and distribution on the right.
Stage bars replace a wall of equal-weight cards.
Pre-existing lint errors unchanged at 11; none in the new files.
|
||
|---|---|---|
| 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 |