zurich_kotak/CLAUDE.md
Yashas e90e4204ec console: redesign the UI — one card language, icons, filters, live trail
Visual and information-architecture pass across every screen. No API call,
payload, poll, permission rule or workflow mirror changes; forms still render
from the live schema and the timeline's merge/grouping is unchanged (only the
display order is reversible).

Design language
- Ground + white cards with one hairline and one radius; sentence-case
  headings; shared primitives in index.css (.card .pill .btn .avatar .live).
- One inline icon set (components/Icon.jsx); no more filled navy/red tiles.
- Read-side labels in api/display.js: roles and enum tokens humanized,
  customer initials/tones, money and date formatters. Nothing sent changes.

Shell
- 56px header with brand, global lead search (portfolio rows, "/" to focus)
  and identity chip with a human role label.
- Sidebar rows carry icons and a clear active bar; AI roster at the foot,
  each opening its remit card.

Login
- Operator copy instead of retail copy; centred sign-in card with show/hide
  password; demo persona picker that fills the EMAIL only.

Overview
- "Who holds the work" as four quiet cards that warm only when owed.
- Pipeline flow bar with every stage named beneath it; KPI strip in a card.
- Action rows with chevron / view-only eye; expiry bands filter the table.

Queues
- Filter bar (search, product, channel, expiry band, stalled, hide closed)
  and sortable columns, all client-side on the fetched page.
- Avatars, product/channel chips, stage chips for closed leads, tinted
  stalled rows, count inline with the title.

Lead
- Avatar + chips in the header; metrics card with the stage rail inside,
  and a branch node for Referred / Parked / Lost / Declined.
- Timeline newest-first by default (toggle), the worker's face as the node,
  one footer row per entry, AI markdown rendered as bold.
- "AI team on this lead" panel with per-agent step counts; PDF tags on docs.

Forms & dialogs
- Required marked with an asterisk + legend; "Select…" placeholder; dd Mon
  yyyy hint under date inputs; open activity form in a titled panel;
  one-line dialog footnotes.

Fixes: horizontal overflow at 1024px, boxed "in N days" figure, raw tokens
(sme_underwriter, verbal_call, Rsa), findings cut at "1.".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 11:37:42 +05:30

200 lines
12 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
npm install
npm run dev # http://localhost:5175
npm run build # → dist/
npm run lint # eslint (flat config, ignores dist/)
npm run preview
```
There are no tests and no test runner in this project.
`npm run lint` currently reports 14 pre-existing errors — 8 `react-hooks/refs`, 3
`react-hooks/set-state-in-effect`, 3 `react-refresh/only-export-components` (the provider
and portfolio files export both a component and hooks). A clean run is not the baseline;
compare against this count rather than against zero.
Local dev reads `VITE_ZINO_API_URL` from `.env` (`https://dev.getzino.in`). A deployed
build does **not** — see "Runtime config" below.
## What this is
A React + Vite operator console (no state library, no UI framework — plain CSS files
next to their components) for the Zurich Kotak "Lead to Policy" demo on the Zino
platform. Leads arrive through three channels — direct, bancassurance, agency — and run
one workflow state machine to policy issuance. AI employees carry most of it; a person is
needed at three queues (`Contacted` → upload documents, `Referred to Underwriting`
clear or decline, `Payment Pending` → confirm premium) and the customer holds one
(`Quoted`).
The frontend owns **no business logic**. The workflow (org `84`, app `536`, workflow
`zk_wf_lead`) is the source of truth; this repo is a thin renderer over its API.
Workflow config is seeded outside this repo, from `sm2/custom-apps/zurich-kotak/`
(SQL migrations `02_workflow.sql`, `04_views.sql`, …). Design doc:
`sm2/app-designs/zurich-kotak/design.html`.
## Architecture
- `src/api/client.js` — every gateway call lives here (`ZinoClient`). JWT + user in
`localStorage`, 401 clears the session via an auth-error handler.
- `src/api/config.js` — the environment identifiers and the hand-maintained mirrors of
workflow tables (`STAGES`, `ACTIONS`, `ENTRY`).
- `src/api/provider.jsx``ZinoProvider` / `useZino()`: one client instance plus session
state. Restores an **identity**, never a permission set.
- `src/api/errors.js``describeError()` turns the gateway's five error shapes into a
sentence plus a `kind` the caller branches on (`stale` re-fetches, `gone` returns to the
list).
- `src/layout/Shell.jsx` — 56px header (brand, `GlobalSearch`, `UserMenu`) + the sidebar,
grouped by `NAV_GROUPS` (who is holding the lead) rather than by pipeline position, with
queue counts from one tallied list call and the AI roster at its foot.
- `src/layout/GlobalSearch.jsx` — finds a lead from anywhere. Filters the rows
`PortfolioProvider` already holds (name, business, ref, mobile, registration) — no request
of its own. `/` focuses it.
- `src/api/display.js` — read-side labels only: `roleLabel`, `humanize` (snake_case tokens →
English; names, refs, emails pass through untouched), `initials`/`toneFor` for customer
avatars, money and date formatters, `findingOf`/`leadIn`/`mdRuns` for AI prose. Nothing in
here changes a value that is SENT anywhere.
- `src/components/Icon.jsx` — the one monoline icon set. Use the NAME, never a path.
- `src/index.css` — the tokens plus the shared primitives every screen builds from:
`.card`, `.pill`, `.btn`, `.avatar`, `.live`, `.h-sec`/`.h-sub`. Screen stylesheets style
content; these style containers. Page ground is `--zk-ground`, bounded regions are white
cards.
- `src/screens/Pipeline.jsx` — a queue: one record view filtered server-side on
`current_state_name`. The filter bar (search, product, channel, expiry band, stalled,
hide closed) and the column sort narrow the PAGE that came back, client-side; they never
change the request.
- `src/screens/Lead.jsx` — one lead: header (avatar, chips, Your move / whose move), a
metrics card with the `StageRail` inside it, the status strip, Next step, the timeline
(newest first by default, toggle to read as a story), and a sticky reference rail: the AI
team with per-agent step counts, then At a glance. The full record is `LeadFileDialog`
(`GROUPS`, in the order the lead is worked).
- `src/pages/Login.jsx` — sign-in plus a demo persona picker (the README accounts for org
84). Picking one fills the EMAIL only and focuses the password box — no credential lives
in the bundle. What the persona may do is still the workflow's decision.
- `src/screens/AddLead.jsx` — the INIT doors from `ENTRY`; skips the chooser when only
one is surfaced.
- `src/components/ActivityForm.jsx` — renders the **live** activity schema and submits it
back. `src/components/FileField.jsx` handles `file`/`ocr` fields;
`src/components/Timeline.jsx` renders the audit trail as a story.
Routing: `/stage/:stageUid`, `/lead/:instanceId`, `/add`; unauthenticated renders
`Login` for every path.
## The invariants — break these and it fails in ways tests would not catch
**Forms are never defined in this repo.** `POST /view/form-screens` returns labels,
types, select options and which fields are mandatory; `ActivityForm` renders whatever
comes back. Adding a field in Studio and redeploying surfaces it here with no frontend
change. Do not hardcode a form — it would silently drift from the workflow.
**Permissions are never enforced here.** Nothing hides a button to stop someone. Every
decision is the workflow's, server-side, on each submission; the console reports the
refusal. `/activity` refuses with **403**, `/start` with **400**, both carrying
`permission denied`. Showing an action the signed-in role cannot perform is deliberate —
the refusal is the demo.
**Runtime config, not build-time.** `VITE_ZINO_API_URL` is read at runtime off
`window.__RUNTIME_CONFIG__`, from a `config.js` the server writes next to the artifact at
placement. One artifact is promoted between environments unchanged, so a compiled-in URL
would point every environment at whichever backend built it. `requireConfigValue` throws
rather than falling back — a production build with no `config.js` must fail loudly. The
dev-only `.env` fallback is gated on `import.meta.env.DEV`.
**One build serves any mount path.** `vite.config.js` sets `base: './'`; the router
basename comes from `basePath()`, which reads the `<base href>` the server writes into the
`<!--BASE_HREF-->` placeholder in `index.html`. Do not reintroduce a build-time base, and
do not remove that placeholder or the `config.js` script tag. `import.meta.env.BASE_URL`
is unusable here (it resolves to `./`).
**Bundled assets must live in `src/assets/`, 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. The favicon is the one exception — it stays in
`public/brand/` and is referenced relatively.
**Activity fields are filtered by product line.** `Collect Documents` serves motor
and SME from one form — all eleven upload slots plus both lines' risk fields, to every
lead — and the activity carries no `field_rules` (`[]`), so nothing server-side decides
visibility. A motor renewal was being asked for a Udyam certificate, a stock statement
and a GSTIN. `fieldApplies()` in `api/config.js` decides: `motor_*` is motor and `sme_*`
is SME **by prefix**, so a field added to the workflow tomorrow classifies itself;
`LINE_FIELDS` names the exceptions that carry no prefix (`gstin`, `udyam_no`), and
`DOC_SLOTS` maps the upload slots plus the two genuine conditions (`doc_address_proof`
only when `kyc_outcome === 'refer'`, `doc_financials` only when `sme_sections` includes
`business_interruption`). `ActivityForm` filters on it for both render and submit.
Two things keep this safe. Every slot is `mandatory: false`, so hiding one cannot fail
schema validation. And when the filter would hide EVERY field, `ActivityForm` renders the
form whole instead — `Capture Motor Risk` is twelve motor fields and is runnable from
Contacted whatever the product is, so on an SME lead the filter would otherwise remove
the activity rather than its noise. `entity_name` is deliberately **not** SME-only: motor
leads carry a business name too.
The prefix rule is inference, not configuration — the durable home is `field_rules` on
the activity. When those are seeded, read them and delete this.
**Nothing is pushed — the console polls.** No socket, no SSE, and most steps are carried
by an AI employee that takes one to three minutes, so a lead moves while it is on screen.
The lead detail polls every 12s, queues and the sidebar counts every 30s, all gated on
`document.visibilityState` and all *quiet* — a refresh must never drop a queue back to a
skeleton under whoever is reading it.
**`STAGES` carries `kind`** — `needs` / `customer` / `auto` / `end` — answering "who is
holding this lead", plus `need` (what a queue wants done, which is what the sidebar shows)
and `doing`/`by` (what to say while an AI carries it, instead of an idle screen).
**`STAGES` and `ACTIONS` are hand-mirrored** from `workflow.tbl_wf_states` and
`workflow.tbl_wf_state_allowed_activities`, because the API returns neither the order nor
the labels the sidebar needs. An activity added to the workflow but not added to `ACTIONS`
is simply invisible — the platform allows it, the console never offers it, nothing errors.
This bit once already: `zk-act-drop` (Mark Lost) was missing entirely, so no lead could be
dropped from the console. It is now appended to every state in `DROPPABLE` rather than
written into ten entries. Keep all of it in step with the seed SQL.
No endpoint exposes state → allowed activities (eight plausible paths probed, all 404) and
the detail view carries `current_state_id` but no activity list — which is *why* the map is
hand-maintained. If the platform ever serves it, delete the map.
## API shapes that have already bitten
- `org_id` on `POST /usr/login` must be a **string**; a number returns
`cannot unmarshal number into Go struct field LoginRequest.org_id`.
- `POST /view/recordview` names the view `rv_template_uid` (the GET spelling is `rv_id`),
and paging/sort/filters live **inside** `search_query`. Top-level gives
`400 Missing param: rv_template_uid (body)`.
- Row key varies by source type: workflow views return `data`, rdbms views have returned
`records` — hence the `data ?? records ?? rows` fallbacks.
- Audit must use the **app-scoped** path `/app/536/view/audit`. Bare `/view/audit` is not
an API route; it falls through to the SPA and returns HTML with a 200.
- `POST /upload` **requires** `workflow_uuid` (taken from config, not from the caller's
ctx). The field context (`activity_id`, `field_id`, `instance_id`) is how the backend
resolves allowed types, size limit and `ocr_config` — it ignores anything the client
claims about them.
- `/ocr-extract` takes a **reference** to an already-uploaded file, not bytes, and answers
`{ extracted, raw }`. Files therefore upload on pick, not on submit.
- `POST /view/recordview` takes the view uid in the **body**. The path form the frontend
spec documents (`/view/recordview/zk-rv-leads`) returns **404**.
- The detail view returns `{ config, data }``config.fields[].output_label` names every
one of the 117 fields, so labels come from the server rather than from prettified ids.
- The `message` on a `/start` or `/activity` response is written per activity ("Documents
received — capturing the risk") and is surfaced verbatim, not replaced with a toast.
- On submit, `ActivityForm` sends only fields the activity defines, drops empties (an
unknown field is fatal to schema validation) and skips `id_gen` — that reference is
issued server-side.
- OCR results fill only fields still blank; never overwrite what a person typed with what
a model read.
## 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 fires only on a **new** push.
Demo logins (all password `ZurichKotak@2026`, org `84`) are listed in `README.md`; sign in
as `sanjay.uw@zurichkotak.example` to land in the one human queue.