Story-1395 pramaan-admin EPIC-026 support-access grants

Support-access oversight that stays correct past the first 200 grants

The monitor used to pull one page of active grants, look up each row's firm from a separate firms-list fetch, and then filter and search in the browser over that partial page. So filtering quietly broke at scale and the page leaned on the enrichment call. The active-grants list contract now serves firmRef on every row and filters firm/staff/text on the server; the page reads the reference directly and pushes filters to the backend.

What changed

A contract refresh against pramaan-functions main c502c5d, then the consumer rebuilt on top of it. No hand-written contract mirrors — the snapshot and generated client move together.

Contract refresh

openapi/admin-api.json synced to functions main; pnpm kubb:generate regenerates src/gen. ActiveGrantListItem now carries firmRef, and the active-grants query accepts firmId, staffUserId, and q.

Server-side filtering

buildActiveSupportGrantQuery forwards firmId/staffUserId/q; the page request routes them to the backend. The browser no longer filters firm/staff/text over a single fetched page.

No enrichment fetch

The listFirms call and the firmId → firmRef map are deleted from support-access/page.tsx. Each row's firm link reads firmRef straight off the grant row.

Operational behavior

Firm, staff, and text filtering plus search now run in the database across every active grant and stay correct past 200, with the existing cursor pagination preserved. Filter, sort, and search selections live in the URL query string, so a view survives refresh and can be shared. The expiring-within quick filter remains a browser-only refinement over the fetched page, because the backend exposes no parameter for it. Every response is parsed against the generated Zod schema via assertContract before the UI trusts a single field.

No "granted by" column is added: under the self-grant model the creator is always the grantee, so grantedByUserId equals the row's staff user and would only duplicate the Staff column. The rebuild also retired two latent defects — hyphenated firm/staff UUID filters no longer get split apart by the table's filter mechanism (the dropdown reflects the active selection after reload), and the Staff column now actually sorts by staff instead of silently falling back to grantedAt.

Verification proof

Gates green: pnpm exec tsc --noEmit, pnpm run lint (0 errors), pnpm run test (544 passed, including tests/unit/grants-contract.test.ts updated for the now-required firmRef), pnpm run test:storybook for the support-access stories (16 passed), and the frontend drift check (pre-existing warnings only).

Contract integrity: openapi/admin-api.json and src/gen/ are committed together; the snapshot is a verbatim sync of functions main c502c5d, so it is reproducible rather than hand-edited.

Remaining live check: verification against a real staging request (acceptance criterion) is owned by the operator and runs on the deployed preview, not on mock or fixture data.