HDFC-Loan-Desk/README.md
Yashas 2e8cc580ac feat: HDFC Loan Desk operator console
Custom frontend for the HDFC loan-origination demo (dev, org 83 / app 524,
workflow hdfc_wf_loan). MSME and personal loan files: agents assemble the
evidence, rules compute capacity, credit decides.

Stack and platform contract follow the Flight-Disruption-Management console,
which runs against this same cluster:

  * API client carries its predecessor's hard-won notes — instance_id goes
    over the wire as a NUMBER (a quoted id fails the int64 decode), org_id as
    a STRING, record views answer under `data` OR `records`, and audit rows
    need the TRIGGER_ prefix filtered or every activity appears three times.
  * VITE_ZINO_API_URL is read at RUNTIME from the config.js the server writes
    at placement, never compiled in, so one artifact is promoted between
    environments unchanged. Missing config fails loudly.
  * base: './' plus a router basename from <base href>, so one build serves
    any mount path.
  * Forms are read from the LIVE activity schema — no field definitions in
    this repo. Add a field in Studio, redeploy, it appears.

Written for this app:

  * EvidencePanel, the centrepiece. Three independent income sources side by
    side with the widest pair marked; every computed ratio shown against the
    threshold it was tested on; and a visible line between what a rule
    computed and what a model wrote (rules are never violet).
  * Queues are the one record view filtered server-side on
    current_state_name — the sidebar is the pipeline. Income variance is
    surfaced in the list, not only on the file.
  * Application 360 with the evidence panel above the offer and the sanction,
    so the screen reads in the order the decision was made.
  * HDFC palette where red is never decoration: the logo block and declines
    only. The referred queue's amber is the only amber in the pipeline.

Known gaps, documented in README rather than hidden: OCR uploads render as a
visible pending row instead of a control that pretends to work, and Credit
Assessment is still performed by a human pending the agent wiring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:42:55 +05:30

123 lines
5.5 KiB
Markdown

# 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
`<base href>` 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 `<html>` — 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`.