Commit Graph
13 Commits
Author SHA1 Message Date
LincPay 39ca34be68 feat(config): resolve the API base at runtime from config.js 2026-08-15 17:01:45 +05:30
LincPay aa1484c285 build(dev): proxy /api in dev and preview so the production bundle is testable 2026-08-15 16:46:33 +05:30
LincPay 318b83625a build(deploy): relative API base and a same-origin /api proxy 2026-08-15 15:33:21 +05:30
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>
v1.0-nabard-submission
2026-08-15 15:29:36 +05:30
LincPayandClaude Opus 5 e923daf825 feat(integration): DPD history tab — coverage, generation and backfill
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>
2026-08-07 18:58:14 +05:30
LincPayandClaude Opus 5 0a76a53ceb feat(integration): core banking console at /officer/integration
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>
2026-08-07 18:20:15 +05:30
LincPayandClaude Opus 5 8939b1cab9 docs: UI README — the three surfaces and the house pattern
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>
2026-08-06 20:02:38 +05:30
LincPayandClaude Opus 5 587207324f fix(nabard): source the India map from DataMeet under CC BY 4.0
The previous boundary file came from a repository with no LICENSE, whose
own README says the data was 'curated from publicly available sources on
the internet for personal usage' and names no upstream source. No licence
means all rights reserved by default, and committing it to this repo is
redistribution. Not something to put in front of a government institution.

Replaced with datameet/maps, which states CC BY 4.0 explicitly and asks
only for attribution. DataMeet is also the right choice on the merits: it
is an Indian civic-data community and depicts India's boundaries as Indian
sources do, which for a NABARD audience is a larger risk than licensing —
a foreign-sourced outline that draws Jammu and Kashmir or Arunachal Pradesh
differently is noticeable and awkward in a way a missing credit is not.

The attribution renders on the map itself, because CC BY requires the
credit to travel with the work rather than sit in a licences page nobody
opens. It carries 'simplified, not authoritative' alongside, which matters
independently of the licence.

Better geometry and less of it: true state polygons rather than district
polygons concatenated per state, so shared edges are no longer stored
twice — 101 KB against 223 KB, at a finer simplification tolerance. Bundle
drops 22 KB gzipped.

Names are canonicalised at generation time (DataMeet ships 'NCT of Delhi'
and misspells 'Arunanchal Pradesh'), so map keys match the platform's own
district table. Source predates the 2019 reorganisation: Ladakh is drawn
within Jammu and Kashmir, and that is recorded in the file header rather
than left for someone to notice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 18:51:31 +05:30
LincPayandClaude Opus 5 570e6335a0 feat(nabard): India choropleth on the national view
The first question an apex supervisor asks is geographic — which part of
the country is deteriorating — and thirty-six table rows answer that far
more slowly than a shape does. Clicking a state drills into its districts,
so the map is a way into the hierarchy rather than a separate view of it.

Three rules a default chart library would get wrong:

No data is not zero. A state with no refinanced portfolio is drawn in a
neutral grey that sits nowhere on the stress ramp, so it cannot be misread
as healthy. Today that is most of the country, and saying so plainly is
more useful than an empty-looking map that implies national coverage.

Suppressed is its own state, hatched rather than shaded — those cells have
borrowers whose numbers are deliberately not shown, which is a different
fact from a low rate.

Colour is not the only channel: every state is reachable by hover, focus
and keyboard, carries a title element, and its figures appear as text
beside the map. A choropleth alone fails colour-blind readers and prints
badly.

The ramp is single-hue in the brand green and anchored on the worst
observed rate rather than on 100 — with a national spread of 15-30% a fixed
scale renders every state identically, and a red-to-green ramp would encode
a value judgement the number does not carry.

Geometry is a static asset generated once from an open district dataset,
dissolved to state level, projected to a 520x592 viewBox and decimated to
~220 KB. Not a hosted basemap: the portal has to render inside a bank
network with no outbound tile requests. Boundaries are simplified for
display and labelled as not authoritative.

Adds ~73 KB gzipped to the bundle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 18:41:46 +05:30
LincPayandClaude Opus 5 38067c41f8 feat(nabard): supervisory portal — national view, banks, early warning
/nabard, reachable only by a NABARD role. Four screens: national view with
the India→zone→state→district drill-down, partner-bank scorecard that
expands into branches, early-warning digest, and the institution register.

Deliberately a different chrome from the bank console. A NABARD user and a
branch officer are not two ranks of one job; a portal that looked identical
would invite the assumption that the apex user is a bank user with more
rights. The sidebar states on every screen that this tier holds no borrower
identity, so a supervisor can tell a partner bank what it can see without
checking.

Response rate is the column the scorecard is built around. Stress rate is
mostly a fact about a bank's districts and sectors; response rate is a fact
about the bank, it is the one no bank self-reports, and low values are
coloured to draw the eye.

Suppressed cells keep their row and lose their numbers — dropping the row
reads as 'no enterprises here', a different and wrong claim, and the one a
reader would most easily act on.

Login routes by tier: both staff tiers share one sign-in and land in
different portals. RequireApex bounces a bank session to /officer rather
than to /login, because signed in as the wrong tier is not signed out. The
routing is a convenience only — the backend re-derives the tier from the
signed JWT and refuses the other surface with a 403.

The demo fallback includes a partner bank below the suppression threshold,
so the withholding behaviour is visible without a backend.

tsc, oxlint and npm run build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 18:10:51 +05:30
LincPayandClaude Opus 5 98d3793bdf feat(console): portfolio-by-sector screen with real category icons
/officer/portfolio — sectors first, then the enterprises inside one.

The existing enterprises screen is a flat ranked list. It answers 'who is
worst' but not 'where is the problem', and a sector is the unit a shock
actually arrives in: a feed price moves against poultry, a milk rate
against dairy. So the count of high-risk enterprises per sector is the
first thing on the page.

Icons come from lucide-react rather than hand-drawn paths — a supervisor
scans this grid before reading it, so the shape is doing the work. One
literal icon per sector: milk, egg, wheat, scissors, storefront.

Drill-down keeps the same shape as the rest of the console: band filter
chips, a ranked table with a six-month trend line, and a detail panel with
the score donut, the loan position and a link to the full risk file. Only
the trend's endpoint dot is coloured; a fully coloured line would read as
a severity gradient it does not mean.

Reads /officer/enterprises with the house demo fallback and a Demo data
pill. Verified against the live backend: 142 enterprises across five
sectors, bands and six-month trends all present.

tsc, oxlint and npm run build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 15:52:12 +05:30
LincPayandClaude Opus 5 438b97d2d4 feat(console): data-entry screen, and mark hand-entered prices as such
/officer/data-entry — upload, preview, commit, and the batch history, for
the feeds no API serves and for the loan book.

The screen is built around one rule: nothing is written until someone has
seen what will be. Accepted, rejected and overwriting rows are shown as
three separate groups, because merging them into one list of rows hides
the only two questions the person clicking Commit has — what will not
land, and what will land on top of something already there. An overwriting
row shows the value it replaces beside the one replacing it.

The screen states in plain words that the effective date is the date being
reported about rather than today, and that when we learned it is recorded
separately — the distinction is invisible in a spreadsheet and expensive
to get wrong. Selecting the loan book adds the no-borrower-identity rule.

On the feeds page, mandi rows now carry a source: Agmarknet, or a '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 there or
it is visible nowhere.

The demo fallback deliberately contains rejections. A demo where every
upload is clean teaches the wrong thing — a reviewer who only sees green
rows will not know the rejection panel exists.

tsc, oxlint and npm run build all clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 15:17:44 +05:30
LincPayandClaude Opus 5 f430f85202 chore: initial commit — UdyamLinc UI, phases A-E
Vite/React 19 console and enterprise PWA. Officer console screens, offline
ledger and sync, data feeds and trip-wire rules screens with freshness chip.

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