Skip to Content

← All archived runs

Run: admin-data-foundation

run.md

Run: admin-data-foundation

  • request: Admin dashboard data foundation — load per-tenant data server-side; wire header + setup-checklist readiness to real data (epic admin-dashboard-data, 1 of 7)
  • issue: #438
  • branch: feat/438-admin-data-foundation
  • pr: #439 — https://github.com/sustentus/sustentus/pull/439
  • stage: release
  • gates:
    • define approved: yes
    • merge approved: yes
  • updated: 2026-06-09

Log

  • 2026-06-09 define: spec drafted from intake stub; resolved plan source (Clerk org metadata) + managed-by (signed-in admin)
  • 2026-06-09 build: spec approved at gate; added loadAdminDashboardData() loader; wired real header + setup-checklist readiness; dashboard now a server component; other sections left on mock
  • 2026-06-09 ship: PR #439 readied; review fixed lean-user fullName + missing-metadata guard; CI green (format/lint/typecheck/issue-link); merge approved by human

00_intake/stub.md

Stub: Admin dashboard data foundation

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

Problem

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

Proposed change

  • Convert 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).
  • Wire the header to the real Tenant (name, plan, managing admin) instead of the mock.
  • Wire the PlatformReadinessCard + setup-completion readiness tile to the existing DB-backed setup checklist (SetupChecklist model + getSetupChecklistSteps), computing completion % and done/total from real per-tenant rows.
  • Introduce a single per-tenant admin-dashboard data loader/shape that later features extend, so each section has a clear place to add its real datapoint.
  • Leave the not-yet-built sections (SLA, commercial, escalation, integrations, derived readiness) reading mock for now — they are replaced by their own features.

Acceptance criteria (rough)

  • /admin renders with the header populated from the real current tenant (no mock tenant name).
  • Setup-checklist readiness (completion %, done/total, checklist items) reflects the tenant's real SetupChecklist data and updates when a step is toggled.
  • The page loads data server-side and is scoped to the authenticated admin's tenant.
  • Sections not covered by this feature still render (from mock) without errors.

Out of scope (this feature)

  • People / roles wiring (admin-people-data).
  • Any new data model (SLA, commercial, escalation, integrations).
  • Derived readiness checks, service-model status, config gaps (admin-readiness-gaps).

Notes for Define

  • This is the architectural seam: decide the per-tenant loader shape and how client cards receive server data. Honour the "tenant-settings for simple config" decision when designing the shape.
  • 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.md

Spec: Admin dashboard data foundation

  • slug: admin-data-foundation
  • issue: #438
  • personas: Admin
  • touches: apps/web/app/(app)/admin/dashboard/page.tsx, apps/web/lib/admin-dashboard-data.ts (new), apps/web/lib/setup-checklist-state.ts, apps/web/lib/mock/admin.ts, apps/web/components/admin/dashboard/platform-readiness-card.tsx
  • complexity: standard

Problem

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

Proposed change

  • Convert the dashboard to a server component. Drop "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.
  • Introduce a single per-tenant loader at 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.
  • Wire the header to real data. Render 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.
  • Wire setup-checklist readiness to the DB. Compute 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.
  • Adapt 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).
  • Leave the not-yet-built sections on mock. SLA, service-model, commercial, escalation, integrations, roles/people, config-gaps, and the derived 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.

Acceptance criteria

  • /admin (and /admin/dashboard) render with the header tenant name populated from the real current tenant — never the mock "Vertex Solutions".
  • The header's "managed by" name is the signed-in admin's real fullName; the plan segment shows the Clerk org metadata plan when set and is hidden when no plan value exists.
  • The Readiness tiles and PlatformReadinessCard show completion %, done/total, and checklist items computed from the authenticated tenant's real SetupChecklist rows (not the mock list).
  • Toggling a setup step on /setup-checklist is reflected in the dashboard readiness after revalidation (no stale mock value).
  • The page loads its data server-side, scoped to the authenticated admin's tenant; a non-admin/unauthenticated visitor gets the graceful access fallback rather than mock data.
  • All sections not covered by this feature (SLA, service model, commercial, escalation, integrations, people, config gaps) still render from mock without errors.
  • loadAdminDashboardData() returns a typed shape with a clear extension point for later epic features to add real per-section datapoints.

Out of scope

  • People / roles wiring (admin-people-data).
  • Any new data model — SLA, commercial, escalation, integrations (their own features).
  • Derived readiness checks, service-model status, config-gap computation (admin-readiness-gaps).
  • Adding a 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.
  • Editing setup steps from the dashboard — toggling stays on /setup-checklist.

Open questions

  • none

02_build/output/notes.md

Build notes: admin-data-foundation

  • branch: feat/438-admin-data-foundation
  • commits:
    • feat: admin-data-foundation — per-tenant dashboard loader + real header/readiness wiring

What changed

  • apps/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.

Acceptance criteria status

  • /admin and /admin/dashboard render with the real current tenant name — header.tenantName = tenant.name; /admin/page.tsx re-exports the dashboard.
  • "managed by" is the admin's real fullName; plan shows the Clerk org metadata value when set and the · <plan> plan segment is hidden when none.
  • Readiness tiles + PlatformReadinessCard show completion %, done/total, and items computed from the tenant's real SetupChecklist rows.
  • Toggling a step on /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).
  • Data loads server-side scoped to the authenticated admin's tenant; a non-admin/unauthenticated visitor gets the access fallback, not mock data.
  • SLA, service model, commercial, escalation, integrations, people, and config gaps still render from mock without errors (passed d.* unchanged).
  • loadAdminDashboardData() returns a typed AdminDashboardData shape with a documented extension point for later epic features.

Verify result

  • mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. Ship reads them back from gh pr checks. No check expected to fail.

Notes for review

  • Completion % is derived locally from the loaded rows rather than calling 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.md

Ship: admin-data-foundation

  • PR: #439 — https://github.com/sustentus/sustentus/pull/439
  • branch: feat/438-admin-data-foundation
  • CI: format pass · lint pass · typecheck pass · pr-issue-link pass · vercel preview pass
  • docs: no docs impact (internal change to an existing route — new lib helper + client→server conversion; no app/package/route/env-var/architecture documented in apps/docs changed)

Review summary

  • managedBy 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).
  • Type-only import of SetupChecklistRow into the client PlatformReadinessCard is erased at compile time — no server-only module leaks into the client bundle. No action.
  • Only caller of 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.

Acceptance check (vs spec)

  • Real tenant name in header — header.tenantName = tenant.name; /admin re-exports the dashboard.
  • "Managed by" real admin name (post-fix); plan shown from Clerk org metadata when set, hidden when none.
  • Readiness tiles + card computed from real SetupChecklist rows.
  • Toggling a step reflected after revalidatePath("/admin") — both routes server-rendered against the DB.
  • Server-side, tenant-scoped; non-admin/unauthenticated gets the access fallback.
  • Not-yet-built sections still render from mock without errors.
  • loadAdminDashboardData() returns a typed shape with a documented extension point.

Merge & deploy

  • merged: no — awaiting explicit human approval at the merge gate.
  • deploy: pending merge.

04_release/output/changelog.md

What's new: Real data on your platform setup dashboard

Admin

Your 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.md

Investor update: Admin dashboard now runs on real per-tenant data

  • shipped: 2026-06-09 · advances: Build the Bridge · objective: Objective 3 — Validate Technical Infrastructure & Payout Flow

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

  • Administrators now track genuine go-live readiness: setup completion, items done versus outstanding, and per-step status are computed from their own tenant's records, refreshing as steps are completed.
  • Each organisation sees only its own data, loaded securely on the server and scoped to the signed-in admin — a building block for the operational trust the live-transaction milestone depends on.
  • First of seven steps converting the admin dashboard from demo data to real data; the data seam shipped here is what makes the next six fast to deliver.

04_release/output/release.md

Release: admin-data-foundation

  • live: yes — production deploy READY for merge commit 6fb1bacc (web, target production) · https://vercel.com/sustentus/web/GvkW7mtGhFFvF3igAS4CaYamaGzG
  • audience: both (user-facing behaviour changed — admins now see real per-tenant data)
  • drafts: investor-update.md · changelog.md
  • docs updated: none — the change is internal data-wiring; the business docs describe the admin dashboard at a capability level ("Admin sees everything / full dashboard") that did not change. No feature-role-matrix / service-journey / platform-overview edit needed.
  • suggested channels: apps/help changelog page (apps/help/app/changelog/page.mdx) · investor email (pnpm --filter @sustentus/services release:send-investor-update admin-data-foundation)
  • sent: none yet — awaiting explicit, channel-named human approval
  • run pruned: not yet — awaiting go-ahead (no docs PR needed, so this would be a standalone chore PR with Closes #438)