End-to-end functional flow: a stranger lands at /start, fills the form, gets a tenant + an IT_ADMIN invite, lands in their portal, sees the trial banner counting down, opens the catalog, picks a product, gets an entitlement.
M11.1 — /[slug]/catalog renders the live catalog from tenant-registry, gates owned products (Active chip), exposes two server actions per remaining card: Request → POST /v1/catalog/request (audit event today; ERPNext lead when M8.x lands); Start 14-day trial → POST /v1/catalog/trial-request (immediate entitlement, 14-day expiry per M4.2). Flash banner on success/error via ?ok= / ?err= query params.
M12.1 — Public /start route. Server action calls createTenant({slug,name,plan,admin_email}) → tenant-registry's KC-aware POST /v1/tenants → user lands at /<slug>/dashboard. Dashboard now renders a trial-days-left banner when status=trial (urgent styling when ≤3 days remain, links to /billing for the upgrade flow that M8.3 will provide).
Why
Closes the loop: portal can now create a tenant, sign in, browse the catalog, and request/trial a product without anyone touching curl. Every interaction emits the right audit event so M10.2's compliance view will have content.
Linked milestones: M11.1, M12.1
How
src/lib/tenant-registry.ts widened from one-call client to full read+mutate surface. Returns {ok: true, ...} | {ok: false, error: '...'} so server actions branch cleanly without try/catch noise.
Trial banner days computation moved to a top-level pure helper (computeTrialDaysLeft) — react-hooks/purity lint rejects Date.now() inside the component body, and that rule is worth keeping.
Catalog page uses revalidatePath after a successful trial start so the next render sees the new entitlement without a stale-cache window.
Test plan
pnpm lint / typecheck / test (22 vitest cases, 100% src/lib line/branch/function coverage) / build — all green
Manual: signed-in IT_ADMIN session against the live dev stack — Request + Trial buttons hit tenant-registry, audit log records the events, dashboard banner updates after a trial flip
Risk
Server actions don't redirect the user to a sign-in page if their session is missing — they'll hit the 403 NotAuthorized component or get a 404 if the tenant is missing. M10.1 adds a shared sign-in redirect.
/start is unauthenticated and rate-unlimited. Fine for dev; M14.x-style rate limiting before this goes near a real internet.
Checklist
Unit tests (22, all green)
Docs updated (CHANGELOG)
Secrets via Infisical — none new
Migration — n/a
Tenant scoping — canSee(catalog) gates the catalog page; tenant-slug match guard in parent layout still enforced
CHANGELOG entry
## What
End-to-end functional flow: a stranger lands at `/start`, fills the form, gets a tenant + an IT_ADMIN invite, lands in their portal, sees the trial banner counting down, opens the catalog, picks a product, gets an entitlement.
**M11.1** — `/[slug]/catalog` renders the live catalog from tenant-registry, gates owned products (`Active` chip), exposes two server actions per remaining card: **Request** → `POST /v1/catalog/request` (audit event today; ERPNext lead when M8.x lands); **Start 14-day trial** → `POST /v1/catalog/trial-request` (immediate entitlement, 14-day expiry per M4.2). Flash banner on success/error via `?ok=` / `?err=` query params.
**M12.1** — Public `/start` route. Server action calls `createTenant({slug,name,plan,admin_email})` → tenant-registry's KC-aware `POST /v1/tenants` → user lands at `/<slug>/dashboard`. Dashboard now renders a trial-days-left banner when `status=trial` (urgent styling when ≤3 days remain, links to `/billing` for the upgrade flow that M8.3 will provide).
## Why
Closes the loop: portal can now create a tenant, sign in, browse the catalog, and request/trial a product without anyone touching curl. Every interaction emits the right audit event so M10.2's compliance view will have content.
Linked milestones: **M11.1**, **M12.1**
## How
- `src/lib/tenant-registry.ts` widened from one-call client to full read+mutate surface. Returns `{ok: true, ...} | {ok: false, error: '...'}` so server actions branch cleanly without try/catch noise.
- Trial banner days computation moved to a top-level pure helper (`computeTrialDaysLeft`) — `react-hooks/purity` lint rejects `Date.now()` inside the component body, and that rule is worth keeping.
- Catalog page uses `revalidatePath` after a successful trial start so the next render sees the new entitlement without a stale-cache window.
## Test plan
- [x] `pnpm lint` / `typecheck` / `test` (22 vitest cases, 100% `src/lib` line/branch/function coverage) / `build` — all green
- [x] Manual: signed-in IT_ADMIN session against the live dev stack — Request + Trial buttons hit tenant-registry, audit log records the events, dashboard banner updates after a trial flip
## Risk
- Server actions don't redirect the user to a sign-in page if their session is missing — they'll hit the 403 NotAuthorized component or get a 404 if the tenant is missing. M10.1 adds a shared sign-in redirect.
- `/start` is unauthenticated and rate-unlimited. Fine for dev; M14.x-style rate limiting before this goes near a real internet.
## Checklist
- [x] Unit tests (22, all green)
- [x] Docs updated (CHANGELOG)
- [x] Secrets via Infisical — none new
- [ ] Migration — n/a
- [x] Tenant scoping — `canSee(catalog)` gates the catalog page; tenant-slug match guard in parent layout still enforced
- [x] CHANGELOG entry
M11.1 — /[slug]/catalog page renders the live catalog from tenant-registry,
gates already-owned products with an 'Active' chip, and exposes two
server actions per remaining card:
- Request → POST /v1/catalog/request (emits an audit event; sales
follow-up flow will pick this up when M8.x lands ERPNext + the
Lead webhook)
- Start 14-day trial → POST /v1/catalog/trial-request (provisions
the entitlement immediately; 14-day expiry per M4.2)
Flash banner on success/error (?ok= / ?err= query params).
M12.1 — Public /start route. Server action calls
createTenant({slug, name, plan, admin_email}) → tenant-registry's
KC-aware POST /v1/tenants → user lands at /<slug>/dashboard. The
dashboard now renders a trial-days-left banner when status=trial
and trial_ends_at is set (urgent styling when ≤3 days remain).
Library:
src/lib/tenant-registry.ts widened from one-call client to the
full read+mutate surface (fetchCatalog, fetchEntitlements,
requestProduct, startTrial, createTenant). Returns typed
{ok: true, ...} | {ok: false, error: '...'} so server actions
branch cleanly. 22 vitest cases, 100% line+branch+function
coverage of src/lib/.
Catalog tests rely on the mock-fetch pattern; the user-visible
flow is exercised by Playwright when the dev stack is up.
Refs: M11.1 + M12.1
CODEOWNERS rules
requested review from Benjamin_Boenisch 2026-05-19 16:26:14 +00:00
sharang
merged commit ecbe6ae74b into main2026-05-19 16:27:11 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What
End-to-end functional flow: a stranger lands at
/start, fills the form, gets a tenant + an IT_ADMIN invite, lands in their portal, sees the trial banner counting down, opens the catalog, picks a product, gets an entitlement.M11.1 —
/[slug]/catalogrenders the live catalog from tenant-registry, gates owned products (Activechip), exposes two server actions per remaining card: Request →POST /v1/catalog/request(audit event today; ERPNext lead when M8.x lands); Start 14-day trial →POST /v1/catalog/trial-request(immediate entitlement, 14-day expiry per M4.2). Flash banner on success/error via?ok=/?err=query params.M12.1 — Public
/startroute. Server action callscreateTenant({slug,name,plan,admin_email})→ tenant-registry's KC-awarePOST /v1/tenants→ user lands at/<slug>/dashboard. Dashboard now renders a trial-days-left banner whenstatus=trial(urgent styling when ≤3 days remain, links to/billingfor the upgrade flow that M8.3 will provide).Why
Closes the loop: portal can now create a tenant, sign in, browse the catalog, and request/trial a product without anyone touching curl. Every interaction emits the right audit event so M10.2's compliance view will have content.
Linked milestones: M11.1, M12.1
How
src/lib/tenant-registry.tswidened from one-call client to full read+mutate surface. Returns{ok: true, ...} | {ok: false, error: '...'}so server actions branch cleanly without try/catch noise.computeTrialDaysLeft) —react-hooks/puritylint rejectsDate.now()inside the component body, and that rule is worth keeping.revalidatePathafter a successful trial start so the next render sees the new entitlement without a stale-cache window.Test plan
pnpm lint/typecheck/test(22 vitest cases, 100%src/libline/branch/function coverage) /build— all greenRisk
/startis unauthenticated and rate-unlimited. Fine for dev; M14.x-style rate limiting before this goes near a real internet.Checklist
canSee(catalog)gates the catalog page; tenant-slug match guard in parent layout still enforcedM11.1 — /[slug]/catalog page renders the live catalog from tenant-registry, gates already-owned products with an 'Active' chip, and exposes two server actions per remaining card: - Request → POST /v1/catalog/request (emits an audit event; sales follow-up flow will pick this up when M8.x lands ERPNext + the Lead webhook) - Start 14-day trial → POST /v1/catalog/trial-request (provisions the entitlement immediately; 14-day expiry per M4.2) Flash banner on success/error (?ok= / ?err= query params). M12.1 — Public /start route. Server action calls createTenant({slug, name, plan, admin_email}) → tenant-registry's KC-aware POST /v1/tenants → user lands at /<slug>/dashboard. The dashboard now renders a trial-days-left banner when status=trial and trial_ends_at is set (urgent styling when ≤3 days remain). Library: src/lib/tenant-registry.ts widened from one-call client to the full read+mutate surface (fetchCatalog, fetchEntitlements, requestProduct, startTrial, createTenant). Returns typed {ok: true, ...} | {ok: false, error: '...'} so server actions branch cleanly. 22 vitest cases, 100% line+branch+function coverage of src/lib/. Catalog tests rely on the mock-fetch pattern; the user-visible flow is exercised by Playwright when the dev stack is up. Refs: M11.1 + M12.1