Skip to Content

← All archived runs

Run: admin-people-data

run.md

Run: admin-people-data

  • request: Wire the admin dashboard People row (Roles & permissions card + User management table) to real per-tenant User data, with a defined required-roles / missing-roles derivation.
  • issue: #442
  • branch: claude/admin-people-data-dashboard-qitp8i
  • pr: #443 — https://github.com/sustentus/sustentus/pull/443
  • stage: build
  • gates:
    • define approved: yes
    • merge approved: no
  • updated: 2026-06-10

Log

  • 2026-06-09 define: spec drafted from intake stub; 4 scope decisions resolved (required roles = all six; table lists all tenant users; pending = never signed in; rolesAssigned = required roles filled).
  • 2026-06-10 build: spec approved by owner (gate:spec-approved); stage:define → stage:build. Rebased onto main for the merged admin-data-foundation loader. Added apps/web/lib/admin-people.ts (REQUIRED_ROLES + derivation, reusable by admin-readiness-gaps), userService.listTenantDirectory(), wired People datapoint into the loader, and the dashboard People row to real per-tenant data.

00_intake/stub.md

Stub: Admin dashboard — People (users & roles) data

  • feature-slug: admin-people-data
  • epic: admin-dashboard-data
  • personas: Admin
  • initiative: Build the Bridge / objective: Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: admin-data-foundation
  • sequence: 2 of 7

Problem

The People row (Roles & permissions card + User management table) is dummy data. The User model already exists with a role discriminator (vendor | expert | customer | admin | csm | sdm) and per-tenant scoping, so this section can be made real without a new model — it just needs querying and the "missing roles" derivation.

Proposed change

  • Wire the UserManagementTable to the tenant's real users: name, role, email, last active, status (active / pending invite).
  • Wire the RolesPermissionsCard to real counts: total users, how many have roles assigned, and derive missingRoles (roles the platform expects but the tenant has not yet filled).
  • Define what "rolesAssigned" and "missingRoles" mean against the real role enum (which of the six roles are required for go-live vs optional).

Acceptance criteria (rough)

  • User management table lists the tenant's real users with role, email, last-active and status.
  • Pending invites surface as pending rows if that state is available; otherwise documented as out of scope.
  • Roles & permissions card shows real total-users and roles-assigned counts.
  • missingRoles is derived from a defined "required roles" set, not hard-coded mock strings.

Out of scope (this feature)

  • Inviting users or editing roles from the dashboard (read-path only this round).
  • Activity feed / last-active precision beyond what the User model already records.

Notes for Define

  • Decide the canonical "required roles for go-live" list — this also feeds the rolesAssigned readiness check in admin-readiness-gaps, so keep it reusable.
  • touches: packages/services/src/db/models/user.ts (read), apps/web/components/admin/dashboard/{user-management-table,roles-permissions-card}.tsx.

01_define/output/spec.md

Spec: Admin dashboard — People (users & roles) from real data

  • slug: admin-people-data
  • issue: #442
  • personas: Admin
  • touches: apps/web/app/(app)/admin/dashboard (data loader + People section wiring), packages/services/src/db/services/users (read), apps/web/components/admin/dashboard/{user-management-table,roles-permissions-card}.tsx (read — already prop-driven)
  • complexity: standard

Problem

The admin "Platform setup" dashboard's People row (the Roles and permissions card + User management table) is bound to dummy data in apps/web/lib/mock/admin.ts. The User model already exists with a required role discriminator (vendor | expert | customer | admin | csm | sdm), per-tenant scoping, isActive, lastLogin and a sparse clerkUserId, so this section can be made real without a new model — it just needs querying plus a defined "missing roles" derivation.

This advances Build the Bridge / Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow: the admin dashboard is the operational control surface, and a tenant cannot be judged go-live ready while its team/roles view is mock. It is sequence 2 of the admin-dashboard-data epic and depends on admin-data-foundation (sequence 1), which converts /admin to a server-fetched, per-tenant page and introduces the single per-tenant admin-dashboard data loader this feature extends with a People datapoint.

Proposed change

  • User management table — populate from the tenant's real users (all six roles) via the existing per-tenant userService read, scoped to the authenticated admin's tenant. Each row shows:
    • name (full name; falls back to email when name is unset),
    • role (mapped to its display label: Vendor / Customer / Expert / CSM / SDM / Admin),
    • email,
    • last active (from lastLogin, falling back to platformUsage.lastAccess, formatted relatively; when never active),
    • status — pending for never-signed-in users (no clerkUserId and no lastLogin), otherwise active. The table already renders a distinct "Pending invite" row for status: "pending".
  • Roles and permissions card — populate real counts derived from the same tenant user set:
    • totalUsers — count of the tenant's non-deleted users,
    • rolesAssigned — number of required roles that are filled (0–6),
    • missingRoles — the required roles that are not filled, rendered as display labels.
  • Definitions (canonical for this epic — reused by admin-readiness-gaps):
    • Required roles for go-live = all six roles (vendor, expert, customer, admin, csm, sdm). Defined once as a single reusable constant (single source of truth), not hard-coded mock strings.
    • A user is active when it is not soft-deleted, isActive !== false, and has signed in (i.e. not pending).
    • A user is pending when it has never signed in (no clerkUserId and no lastLogin).
    • A required role is filled when the tenant has ≥1 active user holding it; otherwise it is missing. rolesAssigned = requiredRoles.length − missingRoles.length.
  • Wire the People section of apps/web/app/(app)/admin/dashboard/page.tsx to receive these derived values from the server data loader (replacing d.rolesPermissions and d.users). The two card components are already fully prop-driven and need no behavioural change.

Acceptance criteria

  • The User management table lists the tenant's real users (all six roles), each with name, role (display label), email, last-active and status — scoped to the authenticated admin's tenant, with no mock users.
  • Users who have never signed in (no clerkUserId and no lastLogin) render as pending ("Pending invite") rows; all other users render as active.
  • The Roles and permissions card shows the real totalUsers and rolesAssigned counts for the tenant.
  • missingRoles is derived from the defined required-roles set (all six) — required roles with no active user — via a single reusable constant, never hard-coded strings.
  • The required-roles set and the filled/missing derivation live in one reusable place that admin-readiness-gaps can consume for its rolesAssigned readiness check.
  • When the tenant has no users, the table renders its existing empty state and the card shows totalUsers: 0, rolesAssigned: 0, and all six roles as missing — without errors.

Out of scope

  • Inviting users or editing roles from the dashboard — read-path only this round.
  • A dedicated invite-status model/field — pending is derived from never-signed-in, not stored.
  • Last-active precision or an activity feed beyond what lastLogin / platformUsage.lastAccess already record (e.g. session history, online presence).
  • The unrendered mock fields (userActivityTrend, recentActivity) and any chart of them.
  • Replicating the change into apps/dashboards — that app stays the dummy source-of-truth.

Open questions

  • Non-blocking sequencing: this feature extends the per-tenant data loader introduced by admin-data-foundation. Build assumes that loader exists; if foundation has not yet merged when Build runs, Build wires the People datapoint into the same loader shape (and the foundation work, if still mock, is replaced by its own feature). This does not change what gets built here.

02_build/output/notes.md

Build notes: admin-people-data

  • branch: claude/admin-people-data-dashboard-qitp8i (issue #442, PR #443)
  • commits:
    • feat: admin-people-data — wire People row to real per-tenant user data

What changed

  • apps/web/lib/admin-people.ts (new): the single reusable source of truth for the epic's People derivation. Exports REQUIRED_ROLES (all six roles), roleLabel, deriveRolesReadiness (totalUsers / rolesAssigned / missingRoles) and deriveAdminPeople (table rows + readiness). admin-readiness-gaps consumes REQUIRED_ROLES + deriveRolesReadiness for its rolesAssigned check — no hard-coded role strings.
  • packages/services/src/db/services/users/index.ts: added userService.listTenantDirectory(tenantId) — a per-tenant, non-paginated read of non-deleted users selecting the fields the derivation needs (role, isActive, clerkUserId, lastLogin, platformUsage, isDeleted).
  • apps/web/lib/admin-dashboard-data.ts: the foundation loader now returns a people: AdminPeople datapoint built from deriveAdminPeople(listTenantDirectory(tenantId)).
  • apps/web/app/(app)/admin/dashboard/page.tsx: the People row's RolesPermissionsCard and UserManagementTable now read from data.people instead of d.rolesPermissions / d.users (mock). Both components were already prop-driven and needed no behavioural change.

Acceptance criteria status

  • User management table lists the tenant's real users (all six roles) with name, role label, email, last-active and status, tenant-scoped, no mock — listTenantDirectory scopes by tenantId; deriveAdminPeople maps each row; page wired to people.users.
  • Never-signed-in users (no clerkUserId and no lastLogin) render pending; all others activeisPending derives status.
  • Roles and permissions card shows real totalUsers and rolesAssigned — wired to people.
  • missingRoles derived from REQUIRED_ROLES (all six) — required roles with no active user — via a single reusable constant, never hard-coded.
  • Required-roles set + filled/missing derivation live in one reusable place (apps/web/lib/admin-people.ts) that admin-readiness-gaps can consume.
  • Empty tenant → totalUsers: 0, rolesAssigned: 0, all six roles missing, table empty state — users.length === 0 flows through the derivation and the table's existing empty row without errors.

Verify result

  • Mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. typecheck dependsOn: ["^build"], so the freshly built @sustentus/services dist exposes the new listTenantDirectory method to the web app's typecheck. No check is expected to fail.

Notes for review

  • Branch was rebased onto main to pick up the merged admin-data-foundation loader (apps/web/lib/admin-dashboard-data.ts) before building.
  • active is defined as not-soft-deleted and isActive !== false and signed-in; listTenantDirectory already filters soft-deleted, but isActive keeps the soft-delete guard so the derivation is correct on any user list admin-readiness-gaps passes it.
  • Mock adminData.users / .rolesPermissions are now orphaned but left in place — other dashboard sections still read adminData, and pruning mock is out of this feature's scope.

03_ship/output/changelog.md

See your real team on your platform setup dashboard

Admins now see their organisation's actual people on the platform setup dashboard, instead of sample content:

  • The user management table lists your real team members — name, role, email, when they were last active, and whether an invite is still pending.
  • The roles and permissions card shows your real totals and flags which required roles still have no active user, so you can see what is missing before going live.

This continues making your platform setup dashboard reflect your organisation's real state.

03_ship/output/investor-update.md

Admin dashboard now shows real team and role coverage

The admin platform-setup dashboard's People view is now backed by live per-tenant data — every team member, their role, and the go-live roles still unfilled — replacing the last of the sample content in that section. This continues hardening the admin control surface our operators rely on to judge whether a tenant is ready to transact.

  • Advances Build the Bridge: the readiness view is now trustworthy — role coverage is derived from a tenant's real users, so "which roles are missing" reflects reality rather than a fixed example.
  • Supports our Q2 objective to Validate Technical Infrastructure & Payout Flow — a tenant cannot be confidently cleared for live transactions while its team-and-roles view is mock.
  • Built on the shared per-tenant data foundation, so each dashboard section flips from sample to real with no new plumbing.

03_ship/output/pr.md

Ship: admin-people-data

  • PR: #443 — https://github.com/sustentus/sustentus/pull/443
  • branch: claude/admin-people-data-dashboard-qitp8i
  • CI: format pass · lint pass · typecheck pass · issue-link pass · preview build pass
  • technical docs: no technical docs impact — no new app/route/env var/architecture; userService methods are not enumerated in apps/docs/technical.
  • business docs: no business docs impact — read-path wiring of an already-documented capability ("user management" in business/roles); no change to what the docs describe.
  • release notes: both — investor draft (03_ship/output/investor-update.md) + changelog entry published to apps/help/app/changelog/page.mdx.

Review summary

  • /code-review (high) — 2 low-severity findings, both in pre-existing out-of-scope code; neither widens this read-path feature's scope:
    • user-management-table.tsx:108 keys rows by user.email; with real data a tenant can hold the same email across roles (unique index is tenantId+email+role), so duplicate keys are reachable — a console warning only (rows have no local state). Follow-up (spec keeps this component untouched), not blocking.
    • admin-dashboard-data.ts awaits escalation, people, and checklist sequentially though independent — could Promise.all. Left as-is to match the foundation loader's established pattern. Accepted.
  • No correctness findings against the changed code.

Acceptance check (vs spec)

  • User management table lists the tenant's real users (all six roles) with name, role label, email, last-active, status — tenant-scoped, no mock. (userService.listTenantDirectory + deriveAdminPeople, page wired to data.people.users.)
  • Never-signed-in users (no clerkUserId and no lastLogin) render pending; all others active. (isPending in admin-people.ts.)
  • Roles and permissions card shows real totalUsers and rolesAssigned. (Wired to data.people.)
  • missingRoles derived from REQUIRED_ROLES (all six) — required roles with no active user — via a single reusable constant, never hard-coded. (deriveRolesReadiness.)
  • Required-roles set + filled/missing derivation in one reusable place admin-readiness-gaps can consume. (apps/web/lib/admin-people.ts exports REQUIRED_ROLES + deriveRolesReadiness.)
  • Empty tenant → totalUsers: 0, rolesAssigned: 0, all six missing, table empty state, no errors. (Derivation over [] + table's existing empty row.)

Merge & deploy

  • merged: yes — squash-merged to main (811f8918) on 2026-06-10, after explicit human approval (gate:merge-approved).
  • deploy: READY — web production deploy dpl_AazEvvQ9, live at https://platform.sustentus.com.

04_release/output/release.md

Release: admin-people-data