admin-people-datarun.md00_intake/stub.mdThe 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.
missingRoles (roles the platform expects but the tenant has not yet filled).pending rows if that state is available; otherwise documented as
out of scope.missingRoles is derived from a defined "required roles" set, not hard-coded mock strings.User model already records.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.mdThe 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.
userService read, scoped to the authenticated admin's tenant. Each row shows:lastLogin, falling back to platformUsage.lastAccess, formatted relatively;
— when never active),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".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.admin-readiness-gaps):vendor, expert, customer, admin, csm, sdm).
Defined once as a single reusable constant (single source of truth), not hard-coded mock strings.isActive !== false, and has signed in (i.e.
not pending).clerkUserId and no lastLogin).rolesAssigned = requiredRoles.length − missingRoles.length.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.clerkUserId and no lastLogin) render as pending
("Pending invite") rows; all other users render as active.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.admin-readiness-gaps can consume for its rolesAssigned readiness check.totalUsers: 0, rolesAssigned: 0, and all six roles as missing — without errors.pending is derived from never-signed-in, not stored.lastLogin / platformUsage.lastAccess
already record (e.g. session history, online presence).userActivityTrend, recentActivity) and any chart of them.apps/dashboards — that app stays the dummy source-of-truth.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.mdfeat: admin-people-data — wire People row to real per-tenant user dataapps/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.listTenantDirectory
scopes by tenantId; deriveAdminPeople maps each row; page wired to people.users.clerkUserId and no lastLogin) render pending;
all others active — isPending derives status.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.apps/web/lib/admin-people.ts) that admin-readiness-gaps can consume.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.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.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.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.mdAdmins now see their organisation's actual people on the platform setup dashboard, instead of sample content:
This continues making your platform setup dashboard reflect your organisation's real state.
03_ship/output/investor-update.mdThe 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.
03_ship/output/pr.mduserService methods are not enumerated in apps/docs/technical.business/roles); no change to what the docs describe.03_ship/output/investor-update.md) + changelog entry published to apps/help/app/changelog/page.mdx./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.userService.listTenantDirectory + deriveAdminPeople, page wired to data.people.users.)clerkUserId and no lastLogin) render pending; all others active. (isPending in admin-people.ts.)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.)admin-readiness-gaps can consume. (apps/web/lib/admin-people.ts exports REQUIRED_ROLES + deriveRolesReadiness.)totalUsers: 0, rolesAssigned: 0, all six missing, table empty state, no errors. (Derivation over [] + table's existing empty row.)811f8918) on 2026-06-10, after explicit human approval (gate:merge-approved).dpl_AazEvvQ9, live at https://platform.sustentus.com.04_release/output/release.mddpl_AazEvvQ9 (commit 811f8918), READY and aliased to https://platform.sustentus.com / https://web.sustentus.comapps/help); investor update sent this stage