Context. surface tenant-admin · domain identity-access · route /members · auth signed-in + tenant:admin (getTenantServerSession) · source apps/oshun/tenant-admin/src/app/members/page.tsx
Last walked. —
Purpose#
Read-only directory of tenant membership and seat usage: lists each member's
user id, roles, groups, licenses, status, and last-seen time, with a summary
line of status counts. Backs the members workspace (core group) in
libs/oshun/navigation/src/tenant-ia.ts.
Entry points#
- Shell sidebar (
TenantSidebar) — "Members" (shortLabel) under the Core group, link to/members(data-workspace-id="members") - Workspace grid on
/(home) — list itemdata-workspace-id="members" - Notifications digest deep link target
/members(see notifications.md) - Direct URL
/members(bookmarkable; middleware-gated)
Layout regions#
Inside TenantShell:
- Header:
TenantHeadershowstenant · <tenantId>› "Members and seats" - Main:
<section data-testid="tenant-members-panel" data-member-count={counts.total}><h1>Members and seats</h1>- subtitle: "
Nmember(s) · activeA· invitedI· suspendedS· deactivatedD" .table(data-testid="tenant-members-table") — columns Member, Roles, Groups, Licenses, Status, Last seen; each<tr>hasdata-member-idanddata-member-status
States#
- Loading — N/A; server component renders the fixture in one pass
- Empty — N/A in practice;
seedMembers()always returns 4 members. (Code would render an empty<tbody>and "0 members" if the list were empty, but that path is unreachable with the fixture.) - Populated (short) — 4 members render:
op-jane(active, tenant-admin),op-luis(active, reviewer),op-sara(invited, analyst, licenses → "unallocated"),op-alex(suspended, reviewer) - Populated (long) — N/A; fixture is fixed at 4 rows (no pagination)
- Error (recoverable) — N/A; no runtime query
- Error (unrecoverable) — no session →
redirect('/unauthorized?reason=missing-session&returnTo=/members') - Offline — N/A for data (SSR-rendered)
- Gated —
getTenantServerSession()null → redirect to/unauthorized; sidebar renders Locked whencanEnterTenantWorkspaceis false - Standalone PWA — verify; no manifest/SW confirmed in source
Interactions#
The <main> content is a static table — no buttons, inputs, sorting, or row
actions. All interactivity is shell chrome:
- Skip to main content (link,
TenantShell) → anchors#tenant-main - Sidebar workspace links (links,
TenantSidebar) → navigate accessible workspaces;aria-current="page"on current; Locked span when gated - Member rows — non-interactive;
userIdrendered ascode, status as a.statusBadgekeyed bystatus_<status>class; empty groups → "—", empty licenses → "unallocated", null last-seen → "—"
Data & contracts#
- Reads: no BFF/network read.
seedMembers(tenantId)returns a deterministic in-pagereadonly TenantMember[]fixture; counts derived by filtering onmember.status.lastSeenUnixSecondsformatted vianew Date(...).toLocaleString(). - Writes: None.
- Realtime: None.
- Caching: SSR (server component); deterministic per
tenantId - Auth/role check:
getTenantServerSession()in page; middleware gate
Contract: TenantMember from @oshun/tenant-console
(libs/oshun/tenant-console). Doc comment in source states the fixture is
shaped exactly like the live TenantMember contract so a future BFF members
route is a single import change.
Cross-references#
- Workspace definition:
libs/oshun/navigation/src/tenant-ia.ts(members— groupcore,requiredScopes: ['tenant:admin:*'],iconToken: 'users') - Shell:
TenantShell.tsx,TenantSidebar.tsx,TenantHeader.tsx - Sibling routes: roles.md (roles assigned to members), integrations.md, notifications.md, policy.md, status.md
- Conventions: ../WALKTHROUGH/00-conventions.md
Open questions / known gaps#
- No BFF members route is wired for tenant scope yet (explicit fixture per source doc comment). Document the live endpoint when added.
- No member detail view, invite, suspend, or seat-reallocation actions exist on this route — verify whether they are planned elsewhere.
-
.toLocaleString()renders against the server locale/timezone; verify intended display timezone for "Last seen".