LincPayandClaude Opus 5 627e11e9a9 feat(nabard): regional office console, and the horticulture sector pack
Adds the fourth supervisory tier to the console — RegionalOfficesPage at
/nabard/regional-offices, a nav entry, and a sidebar that names the tier
(NABARD Head Office vs Regional Office) because NABARD HO and the Maharashtra
RO are both in Mumbai and a drill-down labelled by city reads as circular.

The login page gains a public /auth/regional-offices dropdown and a one-click
persona panel grouped by tier. The dropdown only pre-fills credentials: scope
still comes from the token's office claim, so picking Bhopal and signing in as
Bengaluru shows Bengaluru.

The geography drill gets a By regional office / By zone root toggle rather than
an RO rung inserted into the zone ladder. They do not nest — Chandigarh
supervises Punjab and Haryana across a zone boundary — and they answer
different questions: a zone is weather and cropping pattern, an office is who is
accountable. Both roots narrow through the same zone parameter.

Two bugs worth naming, since both were invisible until they weren't:

  - isApexRole() matched only NABARD_*, so RO_ADMIN routed to the bank console —
    the borrower-facing surface, for the one user who may never see a borrower.
    The backend refused every call, so it read as an empty screen rather than a
    leak, but it was still the wrong door.
  - Adding a sector to the backend without the UI's Record<Sector, ...> maps
    white-screens the whole officer console on SECTORS[e.sector].short of
    undefined. HORTICULTURE needed seven maps. Widen the Sector type first and
    TypeScript finds every one of them.

Bands read from the stored value, never recomputed from score. They differ
because a fired trip-wire escalates the band without rewriting the score —
Madhya Pradesh reads 35 RED by band and 5 by threshold, so recomputing made RED
collapse when drilling HO -> RO on the same state.

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

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.
  • /officer/integration — core banking / LMS. The mapping table is the argument: each lender's core names the same eleven fields differently, and a per-bank mapping is what absorbs that. Simulated adapters are badged as simulated on every screen they appear on, and unmapped vendor columns are shown being dropped at the boundary.
  • /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.

S
Description
No description provided
Readme
499 KiB
Languages
TypeScript 95.8%
CSS 3.5%
JavaScript 0.4%
HTML 0.2%
Dockerfile 0.1%