Points at the backend README for project documentation and covers what is specific to this app: the three routes and their tiers, why login routing is a convenience the backend does not trust, and the useQuery + demo fallback + Demo data pill pattern every screen follows. Records why the demo fallbacks deliberately contain failure states. A demo where every upload is clean and every cell is publishable teaches the wrong thing — a reviewer who only sees green rows will not know the rejection panel or the suppression behaviour exist. Carries the DataMeet CC BY 4.0 attribution. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
UdyamLinc — UI
Three surfaces in one Vite/React app, matching the three institutional tiers. Full project documentation, including the demo path and what is real versus seeded, lives in ../UdyamLinc-Backend/README.md.
npm install
npm run dev # http://localhost:5182
The backend is expected at http://localhost:8082/api/v1/udyam. Override with VITE_UDYAM_API_BASE_URL in .env.local.
The three surfaces
| Route | Tier | Shell |
|---|---|---|
/nabard |
NABARD — aggregates only | features/nabard |
/officer |
Bank console — one institution's book | features/officer |
/app |
Enterprise PWA — offline, Hindi-first | features/enterprise |
Both staff tiers share one sign-in form and land in different portals; homeFor() picks the destination from the role. That routing is a convenience only — the backend re-derives the tier from the signed JWT and refuses the other surface with a 403, so a tampered session gains nothing.
The NABARD portal is deliberately a different chrome from the bank console. An apex user and a branch officer are not two ranks of one job, and a portal that looked identical would invite the assumption that the apex user is a bank user with more rights.
The house pattern
Every screen follows the same shape, and new ones should:
const { data, isError } = useQuery({ queryKey: [...], queryFn: () => get<T>('/path') })
const demo = isError
const rows = data ?? demoRows() // in-file fallback from lib/demo/
…
{demo && <StatusPill status="Demo data" />}
A screen must render with the backend down — field devices are on patchy 2G — but it must say so. The "Demo data" pill means that screen's call failed, not that the whole app is a mock.
Demo fallbacks deliberately include failure states: the data-entry fallback contains rejected rows, and the NABARD fallback contains a bank below the suppression threshold. A demo where everything is clean teaches the wrong thing — a reviewer who only ever sees green rows will not know the rejection panel exists.
Screens that carry the argument
/officer/rules— every trip-wire with its outcome, including the ones that were skipped and why. This is the screen that shows the system refusing to fire on stale data./officer/feeds— provenance. Mandi rows carry a source:Agmarknet, or an amber Hand chip for a price somebody typed. By the time a price reaches a feature the two are indistinguishable, so the distinction has to be visible here./officer/data-entry— upload, preview, commit. Accepted, rejected and overwriting rows stay in three separate groups./nabard— the India choropleth and the zone → state → district drill-down. States with no portfolio are drawn in a neutral grey that is nowhere on the stress ramp, so absence is never read as health.
Checks
npx tsc -b # types
npx oxlint src # lint
npm run build # production build
All three are expected to pass before a commit.
Attribution
State boundaries by the DataMeet India community (datameet/maps), used under CC BY 4.0. Simplified for display and not an authoritative depiction of any boundary. The credit renders on the map itself, as the licence requires.