# HDFC Bank — Loan Desk Custom operator console for the HDFC loan-origination demo. MSME and personal loan files: **agents assemble the evidence, rules compute capacity, credit decides.** The workflow behind it is seeded from `sm2/custom-apps/hdfc-loan-desk/` (org **83**, app **524**, workflow `hdfc_wf_loan`). Design doc: `sm2/app-designs/loan-origination/design.html`. ## Run it ```bash npm install npm run dev # http://localhost:5174 npm run build # tsc -b && vite build → dist/ npm run typecheck ``` `.env` carries the API host **for local dev only**: ``` VITE_ZINO_API_URL=https://dev.getzino.in ``` Logins (all password `HdfcDemo@2026`, org `83`): | Email | Role | Can do | |---|---|---| | `priya.rm@hdfc.example` | RM, Business Banking | Capture, Upload Documents, **Make Offer**, Decline | | `rahul.credit@hdfc.example` | Credit Manager (maker) | Credit Assessment, **Approve / Decline Referred** | | `anita.approver@hdfc.example` | Credit Approver (checker) | **Sanction** — and nothing else | | `vikram.ops@hdfc.example` | Disbursal Ops | **Release Disbursal** — and nothing else | Sign in as Rahul to land in the Credit Queue, which is where the demo happens. ## How it talks to the platform Everything goes through `src/api/client.ts`. Three routes matter: - `POST /usr/login` — auth. **`org_id` must be a string**; a number returns `cannot unmarshal number into Go struct field LoginRequest.org_id`. - `POST /app/524/view/recordview` — every queue is this one record view (`hdfc-rv-applications`) filtered server-side on `current_state_name`. - `POST /app/524/view/form-screens` then `POST /app/524/activity` — forms are read from the **live** activity schema and submitted straight back. ### Forms are not defined in this repo `ActivityForm` renders whatever `/view/form-screens` returns — 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. ### 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.ts` uses `base: './'` and the router takes its basename from the `` the server writes, so **one build serves any mount path**. Do not reintroduce a build-time base. ## What the screens are for **Queues** (`src/screens/PipelineScreen.tsx`) — the sidebar *is* the pipeline, in the order a file moves. The variance figure is surfaced in the list, not just on the file, so a credit manager scanning the queue can see which referrals are corroboration questions before opening any of them. **The file** (`src/screens/ApplicationScreen.tsx`) — evidence panel first, above the offer and the sanction, because whoever reads the screen top to bottom should read the decision in the order it was made. **The evidence panel** (`src/components/EvidencePanel.tsx`) is the point of the whole app. It is built around three claims: 1. **Three independent income sources, side by side**, with the widest pair marked. A single figure labelled "variance 44.3%" is a number nobody can check; three figures with the spread drawn is an argument a person can accept or reject on sight. 2. **Every ratio shown against the threshold it was tested on.** "1.34" means nothing; "1.34 against a floor of 1.25" is a finding. 3. **A visible line between what a rule computed and what a model wrote.** Rules are never violet; the agent's narrative always is. Nobody should have to be told which is which. ## Theming One attribute — `data-theme` on `` — swaps the whole console. Every colour is a CSS custom property redefined under `:root[data-theme='dark']` in `src/styles.css`. There is no second stylesheet and no `dark:` prefix anywhere. **Never put a literal colour in a component.** A hex or a raw Tailwind shade is invisible to the toggle and will be wrong in one of the two themes. The palette is the bank's two colours, and **red is never decoration** — it marks the logo block and a decline, nothing else. So when red appears in a queue it means something. The referred queue's amber is the only amber in the pipeline, for the same reason: it should read as "needs you" without reading as an error. A referred file is not a bad file. ## Known gaps - **OCR document uploads are not wired.** The seven `ocr` fields on Upload Documents render as a visible pending row rather than a control that pretends to work; the extracted figures are entered directly. The evidence factory that generates the documents is the next piece of work. - **No AI employees yet.** Credit Assessment is currently performed by a human (the Credit Manager holds the role alongside the agent). Wiring the agent changes *who submits the activity*, not the workflow — the state machine, the gate and this console are unchanged by it. - **The advisory offer** only renders once the advisor has run; the `ai_suggested_*` columns are deliberately separate from `offered_*` so a suggestion can never be mistaken for a commitment. - **Four-eyes is role separation**, not same-user detection: it holds because no user is granted both `credit_manager` and `credit_approver`.