The console fell back to lib/demo/* whenever a request failed, which reads
well until you notice the demo portfolio has no owner on it. The ids are
generated from a formula and ops.ts does not mention institutions at all, so
the fallback listed all 142 fabricated borrowers to whoever was signed in — a
Madhya Pradesh Gramin officer was shown a Vindhya Small Finance Bank
borrower. Clicking it produced a 404 from the API, which was the only correct
part of the exchange. Risk alerts and Field visits never called the API at
all.
Scoping the demo data was attempted and cannot be done honestly: the session
carries blocks, but blocks do not map to banks. Sehore holds both MPGB and
Sehore DCCB, Raisen holds both Sehore DCCB and VSFB, and MPGB's own block list
includes Bhopal, which is entirely VSFB.
So the rule is now the one the central claim requires: borrower rows come
from the tenant-scoped API or the screen says it cannot reach the server. A
console that invents borrowers while the server is down is worse than one
that admits it cannot reach the server — particularly this console, whose
whole argument is that a bank's book is unreachable from another bank's
screen.
useTenantData.ts is the only sanctioned source. Each hook returns
`unavailable` rather than substituting data. scopedEnterprises() now takes the
book as a required argument with no default, so reintroducing the leak is a
compile error rather than an oversight.
Feeds, data sources, connectors and the rule catalogue keep their demo
fallbacks. Nothing in them is a borrower.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
A fourth tab on /officer/integration reading the loan book as a training
set. The verdict banner is the point of the screen: DPD history is the
model's label, and three thousand generated snapshots look identical on
a chart to three thousand real ones, so the split is stated in words at
the top rather than left to be inferred from a percentage.
Four tiles, and "unknown provenance" is one of them — rows written
before stamping existed are shown as their own bucket rather than
counted as real. Generation and purge sit beside a backfill-from-core
control, with the honest note that a backfill from a simulated adapter
produces simulated rows and only a bank's own core counts toward the
real total.
The demo fixture deliberately reports a book that is NOT trainable. A
fixture claiming a healthy training set would defeat the entire purpose
of the panel.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three tabs. Connections lists each bank's configured link with its last
status, and lets an operations user test or run a sync. Field mapping is
the screen that carries the argument: one row per loan-book field, one
column per lender's name for it, which is what "every bank's core is
different" actually reduces to. Sync history reads as an audit — a
failed run is a row, because a feed that stopped three weeks ago must be
visible as something other than silence.
Simulated adapters are badged everywhere they appear, including in the
run history, because the failure mode that matters here is a demo
mistaken for a live feed. Unmapped vendor columns are shown being
dropped at the boundary, with identifying ones marked never stored.
The client-side identifying-column check mirrors the backend's, matching
on the whole name and on each part with separators and camelCase split
alike — so CUST_NAME and borrowerMobile are refused next to the field
rather than after a round trip.
Follows the house pattern: useQuery against the real endpoint, in-file
demo fallback, "Demo data" pill when the backend is unreachable. The
demo fixtures deliberately include one connection that has never run and
one that last failed. Build and oxlint clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>