Plan: admin-analytics-dashboard (owner-gated /admin surface + Phase 2 PostHog)¶
URL: https://mkdocs.justinsforge.com/memory/plans/admin-analytics-dashboard-2026-07-07/
Date: 2026-07-07
Approved spec: A new owner-gated /admin surface on app.creatortrack.ai (dev :3062/admin) showing platform-wide and per-user analytics built entirely on CreatorTrack's own Postgres. Platform view: total users/workspaces, DAU/WAU/MAU, signups over time, job-run/error health. Users list -> per-user drill-down: activity & engagement (last seen, sessions/day approximated from activity gaps, pageviews over time), app adoption (which apps they open + frequency), content volume (nodes/tasks/transactions/campaigns/CRM records created). Hard-gated by assertPersonalAppsOwner(me); cross-user aggregates run inside asUser(ownerIdentity) where RLS already grants all-row visibility. Read-only. Phase 2 (documented, NOT built this session): self-hosted PostHog at posthog.creatortrack.ai behind Cloudflare Access, with replay/funnel/dwell panels embedded back into /admin.
Worktree: ~/forge-suite-wt/feat/admin-analytics-dashboard (branch feat/admin-analytics-dashboard, DB clone ct_feat_admin_analytics_dashboard, dev on :3062)
Out of scope this round:
- PostHog deployment, instrumentation, or embedding (Phase 2, documented only).
- True screen-time/dwell tracking (Phase 1 approximates engagement from event timestamps; real dwell comes from PostHog).
- Any write/mutation path; the entire surface is read-only.
- Changing the existing per-page analytics (getPageAnalytics) behavior.
- New DB roles or RLS bypass; owner all-row visibility via existing RLS only.
Task list¶
Task 1: Platform-overview read-model¶
- Files: create
~/forge-suite-wt/feat/admin-analytics-dashboard/lib/admin-analytics.ts - What: Add
getPlatformOverview(owner: Identity)returning{ totalUsers, totalWorkspaces, totalMemberships, dau, wau, mau, signupsByDay[] }. DAU/WAU/MAU = distinctactor_user_idacrossapp.activity+ distinctuser_idacrossapp.page_viewsin the window. Signups fromcore.users.created_at. All queries wrapped inasUser(owner, tx). - Verification:
cd ~/forge-suite-wt/feat/admin-analytics-dashboard && npx tsc --noEmit 2>&1 | head(clean) andPGDATABASE=ct_feat_admin_analytics_dashboard npx tsx -e "import {getPlatformOverview} from './lib/admin-analytics'; import {ownerIdentity} from './lib/admin-analytics'; getPlatformOverview(ownerIdentity()).then(r=>{console.log(JSON.stringify(r,null,2));process.exit(0)})"returns counts without error. - Commit:
feat(admin): platform-overview read-model (getPlatformOverview)
Task 2: Per-user read-models¶
- Files: edit
lib/admin-analytics.ts - What: Add
listUsers(owner)->[{ userId, email, name, createdAt, lastSeen, events30d, pageviews30d }](lastSeen = max activity/pageview ts); andgetUserDetail(owner, userId)->{ profile, activityByDay[], pageviewsByDay[], appAdoption[], contentVolume }.appAdoptionderived fromapp.nodes.type+app.activitygrouped by app;contentVolume= counts fromapp.nodes,finance.transactions,campaigns.*,crm.*for that user's workspaces. Sessions/day approximated by grouping activity timestamps with a 30-min gap threshold. - Verification:
npx tsc --noEmitclean;PGDATABASE=ct_feat_admin_analytics_dashboard npx tsx -e "import {listUsers,getUserDetail,ownerIdentity} from './lib/admin-analytics'; (async()=>{const u=await listUsers(ownerIdentity());console.log(u.length,'users');if(u[0])console.log(JSON.stringify(await getUserDetail(ownerIdentity(),u[0].userId),null,2));process.exit(0)})()"runs clean. - Commit:
feat(admin): per-user read-models (listUsers, getUserDetail)
Task 3: Owner-gated JSON API routes¶
- Files: create
app/api/admin/overview/route.ts,app/api/admin/users/route.ts,app/api/admin/users/[id]/route.ts - What: Each route:
const me = await currentUser(); assertPersonalAppsOwner(me);(404/notFoundsemantics via 403 JSON for non-owner), then return the matching read-model as JSON. Accept?days=window param (default 30). - Verification: With dev owner session active on
:3062:curl -fsS http://127.0.0.1:3062/api/admin/overview | jq '{totalUsers,dau}'returns numbers;curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3062/api/admin/overviewwithout owner cookie returns 401/403. - Commit:
feat(admin): owner-gated /api/admin overview + users routes
Task 4: /admin route scaffold + hard gate + Platform view¶
- Files: create
app/(suite)/admin/layout.tsx(callsassertPersonalAppsOwner(me), elsenotFound()),app/(suite)/admin/page.tsx(Platform overview: stat tiles for users/workspaces/DAU/WAU/MAU + signups-over-time chart),app/(suite)/admin/_components/StatTile.tsx - What: Server components reading
getPlatformOverviewdirectly (not via HTTP). Reuse existing chart primitives from the finance/analytics UI where present; otherwise a minimal inline bar/line. - Verification: Browser (dev owner)
http://100.97.43.104:3062/adminrenders platform tiles + chart; non-owner requestcurl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3062/adminreturns 404. - Commit:
feat(admin): /admin route with owner gate + platform overview
Task 5: Users list + per-user drill-down UI¶
- Files: create
app/(suite)/admin/users/page.tsx(sortable list: email, last seen, 30d events, 30d pageviews),app/(suite)/admin/users/[id]/page.tsx(drill-down: engagement charts, app-adoption breakdown, content-volume tiles, recent activity timeline),app/(suite)/admin/_components/UserRow.tsx,app/(suite)/admin/_components/AdoptionBars.tsx - What: Server components using
listUsers/getUserDetail. Rows link to/admin/users/[id]. - Verification: Browser:
/admin/userslists users; clicking a row loads/admin/users/<id>with populated charts and content-volume tiles; no console errors in the dev logtail /tmp/ct-agent-feat_admin_analytics_dashboard.log. - Commit:
feat(admin): users list + per-user analytics drill-down
Task 6: Owner-only nav entry¶
- Files: edit the suite sidebar/nav component (identify exact file in
app/(suite)/layout.tsxrender tree; likelycomponents/workspace/*Sidebar*or the layout itself) - What: Add an "Admin" nav link visible only when
isPersonalAppsOwner(me), routing to/admin. - Verification: Browser (dev owner): Admin link visible in nav and navigates to
/admin. Confirm gate: link absent in the owner-filtered branch whenisPersonalAppsOwneris false (grep the conditional). - Commit:
feat(admin): owner-only Admin nav entry
Task 7: Resilience pass (per-panel error isolation)¶
- Files: edit
app/(suite)/admin/page.tsx,app/(suite)/admin/users/[id]/page.tsx - What: Wrap each data panel so a failing query renders an inline error card instead of crashing the route (React error boundary or try/catch around each read-model call returning a typed error state). Honors doctrine "fail loud" (surface the error visibly, don't swallow).
- Verification: Temporarily point one read-model at a bad table name, reload
/admin, confirm only that panel shows an error card and the rest render; revert. - Commit:
feat(admin): per-panel error isolation on /admin
Task 8: Phase-2 PostHog deployment spec (documentation only, no infra)¶
- Files: create
~/forge-suite-wt/feat/admin-analytics-dashboard/docs/admin-posthog-phase2.md - What: Write the Phase-2 blueprint: self-hosted PostHog LXC on the fleet, hostname
posthog.creatortrack.aivia Cloudflare tunnel + Cloudflare Access (owner-only), Next.jsposthog-jsApp-Router instrumentation plan, which events to capture, and which panels (session replay, activation funnel, retention) to iframe-embed into/admin. Explicitly a plan, not executed this session. - Verification:
test -f docs/admin-posthog-phase2.md && head -5 docs/admin-posthog-phase2.md - Commit:
docs(admin): Phase-2 PostHog deployment blueprint
Task 9: Register and document¶
- Files: edit
/home/justinwieb/forge/MEMORY.md(index line under CreatorTrack section), create/home/justinwieb/forge/memory/general/reference_creatortrack_admin_analytics.md - What: Topic file documenting the
/adminsurface (route, owner gate, read-model lib, data sources, Phase-2 PostHog pointer) with the mkdocsURL:header; add MEMORY.md index entry. Run the CreatorTrack production build as the end-to-end check. - Verification:
cd ~/forge-suite-wt/feat/admin-analytics-dashboard && npx next build 2>&1 | tail -20succeeds (route/adminin the build output);grep admin_analytics /home/justinwieb/forge/MEMORY.md. - Commit:
docs(admin): register admin-analytics in forge memory