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>
78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
import { useState } from 'react';
|
|
import { useZino } from '../api/provider';
|
|
import { ORG_ID } from '../api/config';
|
|
import { Button, ErrorNote, Input, Label } from '../components/core';
|
|
import { HdfcWordmark } from '../components/HdfcMark';
|
|
import { ThemeToggle } from '../components/ThemeToggle';
|
|
|
|
/**
|
|
* Sign in.
|
|
*
|
|
* The org is fixed — this build serves one tenant — so it is not a field the
|
|
* user has to know. It still goes over the wire as a STRING: the gateway
|
|
* rejects a number with "cannot unmarshal number into Go struct field
|
|
* LoginRequest.org_id", which is a confusing failure to hit from a login form.
|
|
*/
|
|
export function Login() {
|
|
const { login } = useZino();
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [busy, setBusy] = useState(false);
|
|
const [err, setErr] = useState<string | null>(null);
|
|
|
|
async function submit() {
|
|
if (!email || !password) {
|
|
setErr('Enter your email and password');
|
|
return;
|
|
}
|
|
setBusy(true);
|
|
setErr(null);
|
|
try {
|
|
await login(email, password, ORG_ID);
|
|
} catch (e) {
|
|
setErr((e as { message?: string })?.message ?? 'Could not sign in');
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="flex min-h-full flex-col bg-bg">
|
|
<header className="flex items-center justify-between border-b border-deepnavy bg-navy px-4 py-2.5">
|
|
<HdfcWordmark onNavy />
|
|
<ThemeToggle onNavy />
|
|
</header>
|
|
|
|
<div className="flex flex-1 items-center justify-center p-4">
|
|
<form
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
void submit();
|
|
}}
|
|
className="lift w-full max-w-sm rounded-lg border border-line bg-panel p-6"
|
|
>
|
|
<h1 className="text-lg font-semibold text-ink">Sign in</h1>
|
|
<p className="mt-1 text-sm text-muted">
|
|
MSME and personal loan origination.
|
|
</p>
|
|
|
|
<div className="mt-5 space-y-3">
|
|
{err && <ErrorNote>{err}</ErrorNote>}
|
|
<div>
|
|
<Label required>Email</Label>
|
|
<Input value={email} onChange={setEmail} type="email" placeholder="name@hdfc.example" />
|
|
</div>
|
|
<div>
|
|
<Label required>Password</Label>
|
|
<Input value={password} onChange={setPassword} type="password" placeholder="••••••••" />
|
|
</div>
|
|
<Button kind="primary" type="submit" busy={busy} className="w-full">
|
|
Sign in
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|