admin-data-foundationrun.md00_intake/stub.mdThe web admin "Platform setup" dashboard is a "use client" page bound entirely to
apps/web/lib/mock/admin.ts. Before any section can show real data, the page needs to load
per-tenant data on the server. Two sections already have backing models and only need wiring: the
header (tenant name/plan/admin) and the setup-checklist readiness (which is DB-backed but the
dashboard ignores it). This feature establishes the real-data architecture the rest of the epic
plugs into.
apps/web/app/(app)/admin/dashboard/page.tsx to a server component that resolves the
current tenant + admin from auth context and loads real data (children stay client where needed).Tenant (name, plan, managing admin) instead of the mock.SetupChecklist model + getSetupChecklistSteps), computing completion % and
done/total from real per-tenant rows./admin renders with the header populated from the real current tenant (no mock tenant name).SetupChecklist data and updates when a step is toggled.touches: apps/web/app/(app)/admin/dashboard/page.tsx, apps/web/lib/setup-checklist-steps.ts,
packages/services (tenant + setup-checklist read).01_define/output/spec.mdThe web admin "Platform setup" dashboard (apps/web/app/(app)/admin/dashboard/page.tsx) is a
"use client" page bound entirely to apps/web/lib/mock/admin.ts — every tenant sees the same
fictional "Vertex Solutions" data. This is the first feature of the admin-dashboard-data epic
(sequence 1 of 7), advancing Build the Bridge → Q2 2026 Objective 3 (Validate Technical
Infrastructure & Payout Flow): before any later section (SLA, commercial, escalation, integrations,
people, derived readiness) can show real data, the page must first load per-tenant data on the
server. Two sections already have real backing and only need wiring — the header (tenant name +
managing admin) and the setup-checklist readiness, which is DB-backed via the existing
SetupChecklist model + getSetupCompletionStats but the dashboard ignores it. This feature
establishes the per-tenant data architecture the rest of the epic plugs into.
"use client" from
app/(app)/admin/dashboard/page.tsx and resolve the current tenant + admin server-side, reusing the
established resolveActionContext({ allowedRoles: ["admin"] }) pattern (already used by the
/setup-checklist server page) — which returns { tenant, tenantId, currentUser } in one call.
Non-admins / unauthenticated users get the same graceful "no access" fallback that page uses.apps/web/lib/admin-dashboard-data.ts exporting
loadAdminDashboardData() that returns a typed AdminDashboardData shape. This is the architectural
seam: it owns the real sections now and is the obvious place each later feature adds its datapoint.
It composes existing services/helpers (resolveActionContext, getSetupCompletionStats,
loadSetupChecklistRows) — no new model, no new service method.tenant.name (real), the plan from Clerk org metadata
(tenant.publicMetadata.plan, falling back to tenant.settings.plan), and "managed by" the
signed-in admin's currentUser.fullName. When no plan value is present, hide the · <plan> plan
segment rather than showing a mock/placeholder plan.setupCompletion (%), setupDone/setupTotal,
and the checklist items from the tenant's real SetupChecklist rows via getSetupCompletionStats /
loadSetupChecklistRows, feeding the ReadinessTiles setupCompletion/setupDone/setupTotal
props and the PlatformReadinessCard. Because the toggle action (setSetupChecklistStepCompleted)
already revalidatePath("/admin"), the server-rendered readiness updates after a step is toggled —
no client state needed.PlatformReadinessCard checklist input so its items come from the real
loadSetupChecklistRows shape ({ stepIndex, title, description, isCompleted }) instead of the
mock { step, done } shape. It stays a display component ("use client" only if it still needs to).readiness.readyChecks tiles keep reading
lib/mock/admin.ts for now; each is replaced by its own epic feature. The server page passes mock
values into those children unchanged so the page renders end-to-end./admin (and /admin/dashboard) render with the header tenant name populated from the real
current tenant — never the mock "Vertex Solutions".fullName; the plan segment shows
the Clerk org metadata plan when set and is hidden when no plan value exists.PlatformReadinessCard show completion %, done/total, and checklist
items computed from the authenticated tenant's real SetupChecklist rows (not the mock list)./setup-checklist is reflected in the dashboard readiness after
revalidation (no stale mock value).loadAdminDashboardData() returns a typed shape with a clear extension point for later epic
features to add real per-section datapoints.admin-people-data).admin-readiness-gaps).plan or tenant-owner field to the Tenant model — plan is read from existing Clerk org
metadata only; "managed by" is the signed-in admin, not a designated owner./setup-checklist.02_build/output/notes.mdapps/web/lib/admin-dashboard-data.ts (new): the per-tenant data seam.
loadAdminDashboardData() resolves the admin context via
resolveActionContext({ allowedRoles: ["admin"] }), loads real setup-checklist
rows via loadSetupChecklistRows(tenantId), and returns a typed
AdminDashboardData (header + readiness). Returns null for
non-admin/unauthenticated visitors so the page renders the access fallback.
Plan is read from Clerk org metadata (tenant.publicMetadata.plan) with a
tenant.settings.plan fallback, normalised to null when absent. This is the
documented extension point each later epic feature adds its section to.apps/web/app/(app)/admin/dashboard/page.tsx: dropped "use client", made the
component async. Header now renders the real tenant name, conditional plan
segment (hidden when no plan), and the signed-in admin's fullName. Readiness
tiles + PlatformReadinessCard are fed real completion/done/total/checklist.
All not-yet-built sections (SLA, service model, commercial, escalation,
integrations, people, config gaps) still pass mock d.* values unchanged.
Non-admins get the graceful "no access" fallback.apps/web/components/admin/dashboard/platform-readiness-card.tsx: checklist
prop adapted from the mock { step, done } shape to the real
SetupChecklistRow shape ({ stepIndex, title, description, isCompleted }),
imported as a type-only import so no server code leaks into the client bundle./admin and /admin/dashboard render with the real current tenant name —
header.tenantName = tenant.name; /admin/page.tsx re-exports the dashboard.fullName; plan shows the Clerk org
metadata value when set and the · <plan> plan segment is hidden when none.PlatformReadinessCard show completion %, done/total, and
items computed from the tenant's real SetupChecklist rows./setup-checklist is reflected after revalidation — the
action already revalidatePath("/admin") and both routes are now
server-rendered against the DB (no stale mock value).d.* unchanged).loadAdminDashboardData() returns a typed AdminDashboardData shape with a
documented extension point for later epic features.gh pr checks. No check expected
to fail.getSetupCompletionStats separately — that helper internally re-runs
loadSetupChecklistRows, so deriving from the single load avoids a redundant DB
query while producing the same Math.round((done/total)*100) value.SetupChecklistRow is imported into the client PlatformReadinessCard via
import type, so the server-only setup-checklist-state module is erased from
the client bundle.03_ship/output/pr.mdmanagedBy used currentUser.fullName, but resolveActionContext returns the user via
userService.findByClerkUserId which is .lean<IUser>() — a plain object with no Mongoose
virtuals, so fullName was undefined at runtime and the header would render "managed by " with
no name. Resolved on branch (commit fix: … derive managing admin name from lean user fields):
build the name from firstname/lastname (falling back to email), matching the established
pattern used across apps/web (e.g. integrations/actions.ts).SetupChecklistRow into the client PlatformReadinessCard is erased at
compile time — no server-only module leaks into the client bundle. No action.PlatformReadinessCard is the changed page, which passes the new SetupChecklistRow
shape; no stale { step, done } call sites remain. No action.resolvePlan reads tenant.publicMetadata / tenant.settings. These can be absent on tenant
documents loaded from Mongo (schema defaults are not materialised on lean reads of pre-existing
docs), which threw Cannot read properties of undefined (reading 'plan') on the local admin login.
Resolved on branch (commit fix: … guard against tenants with no metadata/settings): read the
key via optional chaining. setupCompletion zero-guard avoids NaN on an empty checklist. Done.header.tenantName = tenant.name; /admin re-exports the dashboard.SetupChecklist rows.revalidatePath("/admin") — both routes server-rendered against the DB.loadAdminDashboardData() returns a typed shape with a documented extension point.04_release/output/changelog.mdYour platform setup dashboard now shows your own organisation's live data instead of sample content. The header displays your real organisation name and who is managing the account, and the readiness section reflects your actual setup-checklist progress — completion percentage, how many items are done, and which steps are still outstanding.
When you complete a step on your setup checklist, the dashboard readiness updates to match, so what you see is always your current state rather than a fixed example.
04_release/output/investor-update.mdThe admin "platform setup" dashboard now reflects each organisation's own live data instead of a shared placeholder. An administrator signing in sees their real organisation name, who is managing the account, and their actual setup-checklist progress — so the screen they use to judge "are we ready to operate?" finally tells the truth. This also lays the per-tenant data foundation the rest of the admin control panel will plug into as we wire up the remaining sections.
04_release/output/release.md