| src | ||
| .env.example | ||
| .gitignore | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.app.json | ||
| tsconfig.json | ||
| tsconfig.node.json | ||
| vite.config.ts | ||
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
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):
| 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_idmust be a string; a number returnscannot 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 oncurrent_state_name.POST /app/524/view/form-screensthenPOST /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:
- 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.
- 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.
- 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.
Brand
Everything below was read off HDFC Bank's own production assets rather than recalled or approximated. Check the source before changing any of it.
| What | Value | Where it came from |
|---|---|---|
| Logo | src/components/HdfcLogo.tsx |
Coordinates lifted verbatim from s7ap1.scene7.com/is/content/hdfcbankPWS/hdfc-bank-logo — the SVG the bank serves in its own site header |
| Blue | #004C8F |
The lockup's bar fill |
| Red | #ED232A |
The mark's ground |
| Typeface | Open Sans | hdfcbank.com preloads OpenSans Regular / SemiBold / Bold / ExtraBold and sets its whole site in it. Self-hosted through @fontsource-variable/open-sans, never a CDN |
| Page tint | #F0F6FB |
The tint behind content on hdfcbank.com |
| Success green | hue of #00B947 |
The bank's green, darkened to #00713D — #00B947 itself is 2.6:1 on white and unreadable as a label |
| Tagline | "We understand your world" | Shown once, on the sign-in card |
The mark is a RED square with a BLUE centre, split by a white channel
cross — not a blue square with a red inset. It was drawn inside out here
originally, in the component and in the favicon in index.html. Those two
carry the same coordinates; change one and you must change the other.
The lockup carries the bank's own 3px white keyline, which is what lets it sit on a coloured ground — so there is no reversed variant, and none is needed. It is also why the header is a light surface: HDFC puts the full lockup on a light ground on every one of its own digital surfaces, and the navy header this app used to have would have dissolved the lockup's own blue bar into itself. The bank's blue does its work as the rule under the header, the active queue and the primary button.
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. HdfcLogo.tsx
is the single exception, and it earns it: a logo's colours are not design
decisions, so the mark stays #ED232A and #004C8F in every theme.
Red is never decoration — it marks the logo 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.
Every text colour clears WCAG AA on each surface it actually lands on, in both
themes. There is one knowing exception: white on --color-signal is 4.31:1 —
the Decline button. Darkening HDFC red to clear AA would have made the one red
button on the screen a different red from the logo beside it.
Known gaps
- OCR document uploads are not wired. The seven
ocrfields 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 fromoffered_*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_managerandcredit_approver.