Tenant Admin Console · Surface walkthrough

Members and seats

A per-surface walkthrough of the Tenant Admin Console tenant-admin surface: layout, states, interactions, data, and cross-references.

drafted
8sections3 minread

On this page

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 item data-workspace-id="members"
  • Notifications digest deep link target /members (see notifications.md)
  • Direct URL /members (bookmarkable; middleware-gated)

Layout regions#

Inside TenantShell:

  • Header: TenantHeader shows tenant · <tenantId>"Members and seats"
  • Main: <section data-testid="tenant-members-panel" data-member-count={counts.total}>
    • <h1>Members and seats</h1>
    • subtitle: "N member(s) · active A · invited I · suspended S · deactivated D"
    • .table (data-testid="tenant-members-table") — columns Member, Roles, Groups, Licenses, Status, Last seen; each <tr> has data-member-id and data-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)
  • GatedgetTenantServerSession() null → redirect to /unauthorized; sidebar renders Locked when canEnterTenantWorkspace is 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; userId rendered as code, status as a .statusBadge keyed by status_<status> class; empty groups → "—", empty licenses → "unallocated", null last-seen → "—"

Data & contracts#

  • Reads: no BFF/network read. seedMembers(tenantId) returns a deterministic in-page readonly TenantMember[] fixture; counts derived by filtering on member.status. lastSeenUnixSeconds formatted via new 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#

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".