Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions docs/proposals/admin-dashboard-intention-ia.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Admin Dashboard: Intention-Based Information Architecture

**Status:** Implemented (this PR)
**UI primitive:** `src/components/Tabs.vue` primary + `secondaryTabs` (same pattern as app/settings)

## What changed

Admin is no longer a flat feature list. Primary tabs are intentions; secondary tabs are the surfaces inside each intention.

| Primary | Secondary |
| --- | --- |
| Pulse | — |
| Onboarding | Funnel · Sources · Cohorts |
| Product | Updates · Plugins · CLI · Builder · Notifications |
| Retention | Trials · Churn · Inactive |
| Customers | Organizations · Credits |
| Revenue | Overview · Upgrades · Risk |
| Platform | Replication · Capacity · Debug |

Legacy URLs (`/admin/dashboard/users`, `/updates`, …) redirect to the new hubs.

## Layout wiring

`src/layouts/admin.vue` mirrors `src/layouts/app.vue`: primary tab keeps the hub active; secondary tabs switch within the hub.

## Onboarding focus

`users.vue` was split into:

- `/admin/dashboard/onboarding` — funnel + trends
- `/admin/dashboard/onboarding/sources` — registration sources / email / country
- `/admin/dashboard/onboarding/cohorts` — stuck-stage attention from live funnel stats
- `/admin/dashboard/retention/*` — trials / churn / inactive

## Screenshots

Real product captures (local worktree, `admin@capgo.app`):

- `docs/pr-assets/admin-intention-ia/real-onboarding-funnel.webp`
- `docs/pr-assets/admin-intention-ia/real-product-updates.webp`
- `docs/pr-assets/admin-intention-ia/real-pulse.webp`
639 changes: 356 additions & 283 deletions messages/en.context.json

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,70 @@
"admin-credits-select-org": "Select Organization",
"admin-credits-title": "Grant Credits",
"admin-dashboard": "Admin Dashboard",
"admin-pulse": "Pulse",
"admin-pulse-description": "Landing attention surface for registrations, trials, revenue, product health, and stuck onboarding.",
"admin-pulse-new-registrations": "New registrations",
"admin-pulse-activation-rate-subtitle": "Activation rate {rate}%",
"admin-pulse-activation-unavailable": "Activation rate unavailable",
"admin-pulse-trials": "Trials",
"admin-pulse-trials-subtitle": "Organizations currently on trial",
"admin-pulse-paying-orgs": "Paying organizations",
"admin-pulse-mrr-subtitle": "MRR ${mrr}",
"admin-pulse-mrr-unavailable": "MRR unavailable",
"admin-pulse-update-success": "Update success",
"admin-pulse-update-success-subtitle": "Latest update success rate",
"admin-pulse-builders-free": "Available builders",
"admin-pulse-builders-used": "Running builders",
"admin-pulse-builders-used-subtitle": "Busy online runners",
"admin-pulse-builders-waiting": "Waiting jobs",
"admin-pulse-builders-waiting-subtitle": "Queued for a runner",
"admin-pulse-builders-offline": "Offline workers",
"admin-pulse-builders-offline-subtitle": "Registered but offline",
"admin-pulse-builders-online": "{count} online",
"admin-pulse-builder-unreachable": "Builder unreachable",
"admin-pulse-attention": "Needs attention",
"admin-pulse-attention-description": "Stuck onboarding gaps and active trials that may need follow-up.",
"admin-pulse-attention-trials-subtitle": "Open the retention hub for trial details",
"admin-onboarding": "Onboarding",
"admin-onboarding-funnel": "Funnel",
"admin-onboarding-sources": "Sources",
"admin-onboarding-cohorts": "Cohorts",
"admin-onboarding-cohorts-description": "Stuck onboarding stages derived from the live funnel. Use this surface to prioritize who needs attention.",
"admin-onboarding-cohorts-hint": "This is the action surface for stuck onboarding — counts come from the same onboarding funnel stats as the Funnel tab.",
"admin-onboarding-stuck-app-no-channel": "App but no channel",
"admin-onboarding-stuck-app-no-channel-description": "Organizations that created an app but never created a channel",
"admin-onboarding-stuck-channel-no-bundle": "Channel but no bundle",
"admin-onboarding-stuck-channel-no-bundle-description": "Organizations with a channel that never uploaded a bundle",
"admin-onboarding-stuck-bundle-no-device": "Bundle but no production device",
"admin-onboarding-stuck-bundle-no-device-description": "Organizations with a bundle that have no production plugin device yet",
"admin-onboarding-stuck-device-no-download": "Device but no update download",
"admin-onboarding-stuck-device-no-download-description": "Organizations with a production device that have not completed an update download",
"admin-product": "Product",
"admin-retention": "Retention",
"admin-retention-trials": "Trials",
"admin-retention-churn": "Churn",
"admin-retention-inactive": "Inactive",
"admin-retention-inactive-description": "Paid organizations with zero MAU or no uploads in the selected period.",
"admin-retention-inactive-truncated": "Results were truncated after loading the maximum number of pages.",
"admin-customers": "Customers",
"admin-revenue": "Revenue",
"admin-revenue-overview": "Overview",
"admin-revenue-upgrades": "Upgrades",
"admin-revenue-risk": "Risk",
"admin-platform": "Platform",
"admin-platform-capacity": "Capacity",
"admin-capacity-online-workers": "Online workers",
"admin-capacity-registered": "{count} registered",
"admin-capacity-usage-by-hour": "Builder usage by hour",
"admin-capacity-usage-by-hour-description": "Free vs used reconstructed from worker +/− events and build start/end intervals",
"admin-plugins-active-devices-30d": "Active devices (30d)",
"admin-plugins-ios-devices-30d": "iOS devices (30d)",
"admin-plugins-android-devices-30d": "Android devices (30d)",
"admin-plugins-version-breakdown": "Version Breakdown Over Time",
"admin-plugins-version-ladder": "Version Ladder",
"admin-plugins-plugin-versions": "Plugin Versions",
"admin-plugins-major-versions": "Major Versions",
"admin-plugins-device-share": "Device Share",
"admin-organizations": "Organizations",
"admin-users-email-type-breakdown": "Email Type Breakdown",
"admin-users-email-type-breakdown-description": "Registration mix in the selected period, split between professional, personal, and disposable email domains.",
Expand Down Expand Up @@ -364,6 +428,7 @@
"app-id": "App ID",
"org-to-app": "Org → App",
"organizations-created": "Organizations Created",
"organizations-in-trial-period": "Organizations in trial period",
"register-to-org": "Register → Org",
"app-to-channel": "App → Channel",
"app-id-missing": "App ID is missing",
Expand Down Expand Up @@ -653,6 +718,7 @@
"canceled-photo-selection": "You canceled the picture selection",
"cancellation-date": "Cancellation Date",
"cancellation-reason": "Cancellation Reason",
"cancellations": "Cancellations",
"cancelled-organizations-list": "Cancelled Organizations List",
"capgo-updates-disabled": "Capgo updates disabled",
"cannot-change-allow-device-custom-id": "Cannot change allow device custom ID setting, please check the browser console",
Expand Down Expand Up @@ -1536,11 +1602,13 @@
"native-dependencies": "Native Dependencies",
"native-dependencies-description": "Native packages and their versions included in this bundle",
"need-more-contact-us": "Need more ? Contact us for tailored plan",
"need-upgrade": "Orgs Need Upgrade",
"need-upgrade-description": "Organizations with unpaid overages",
"need-upgrade-trend": "Organizations Needing Upgrade",
"never": "Never",
"new-bundle-ready-banner": "You have new bundles that are not deployed to production.",
"new-organizations": "New Organizations",
"new-subscriptions": "New Subscriptions",
"new-name-not-changed": "The new name is the same as the old one",
"new-name-to-long": "The new name is too long. You can only use 32 characters",
"new-name-to-short": "The apikey name is too short. It must be at least 4 characters long",
Expand Down Expand Up @@ -2079,6 +2147,7 @@
"recommended": "Recommended",
"refresh": "Refresh",
"register": "Register",
"paid-at": "Paid at",
"registered-at": "Registered at",
"register-heading": "Registration",
"register-next": "Register",
Expand Down Expand Up @@ -2425,6 +2494,7 @@
"trial-extensions": "Trial Extensions",
"extended-trial-subscriptions": "Extended Trial Subscriptions",
"trial-left": "days left",
"trial-organizations": "Trial Organizations",
"trial-organizations-list": "Trial Organizations List",
"trial-plan-expired": "Trial Expired",
"try-a-different-search-term": "Try a different search term",
Expand Down Expand Up @@ -2751,6 +2821,9 @@
"admin-notifications-delivery-chart": "Daily delivery",
"admin-notifications-last-month-chart": "Trailing 30 days",
"admin-notifications-campaigns-chart": "Campaigns created per day",
"admin-capacity-workers": "Workers",
"admin-capacity-used": "Used",
"admin-capacity-free": "Free",
"frontend-onboarding": "Frontend onboarding",
"frontend-onboarding-version-1": "Onboarding v1",
"frontend-onboarding-attempts": "Onboarding attempts",
Expand Down
70 changes: 70 additions & 0 deletions scripts/capture-admin-screens.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { mkdirSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { chromium } from '@playwright/test'

const rootDir = join(dirname(fileURLToPath(import.meta.url)), '..')
const OUT = join(rootDir, '.context/admin-screens')
const DOCS = join(rootDir, 'docs/pr-assets/admin-intention-ia')
const BASE = 'http://localhost:5173'

mkdirSync(OUT, { recursive: true })
mkdirSync(DOCS, { recursive: true })

async function waitForAdmin(page, path, readyLocator) {
await page.goto(`${BASE}${path}`, { waitUntil: 'domcontentloaded' })
await page.waitForURL((url) => {
try {
return new URL(url).pathname.replace(/\/$/, '') === path.replace(/\/$/, '')
}
catch {
return false
}
}, { timeout: 120000 })
await page.getByRole('button', { name: /onboarding|pulse|product/i }).first().waitFor({ timeout: 120000 })
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
if (readyLocator)
await readyLocator(page).waitFor({ timeout: 120000 })
// give charts a moment
await page.waitForTimeout(3000)
}

async function shot(page, name) {
await page.screenshot({ path: join(OUT, `${name}.png`), fullPage: false })
await page.screenshot({ path: join(DOCS, `${name}.png`), fullPage: false })
console.log('saved', name, page.url())
}

const browser = await chromium.launch({ headless: true })
const page = await browser.newPage({ viewport: { width: 1440, height: 1100 } })
page.setDefaultTimeout(120000)

await page.goto(`${BASE}/login`, { waitUntil: 'domcontentloaded' })
await page.locator('input[type="email"]').first().fill('admin@capgo.app')
await page.getByRole('button', { name: /continue/i }).first().click()
await page.locator('input[type="password"]').first().waitFor({ state: 'visible' })
await page.locator('input[type="password"]').first().fill('adminadmin')
await page.getByRole('button', { name: /log in/i }).first().click()
await page.waitForURL(u => !String(u).includes('/login'), { timeout: 90000 })
console.log('logged in', page.url())

await waitForAdmin(page, '/admin/dashboard/pulse', p => p.getByText(/new registrations|pulse/i).first())
await shot(page, 'real-pulse')

await waitForAdmin(page, '/admin/dashboard/onboarding', p => p.getByRole('button', { name: /funnel|sources|cohorts/i }).first())
await shot(page, 'real-onboarding-funnel')

await waitForAdmin(page, '/admin/dashboard/onboarding/sources', p => p.getByText(/registrations by source|sources/i).first())
await shot(page, 'real-onboarding-sources')

await waitForAdmin(page, '/admin/dashboard/onboarding/cohorts', p => p.getByRole('button', { name: /cohorts/i }).first())
await shot(page, 'real-onboarding-cohorts')

await waitForAdmin(page, '/admin/dashboard/product/updates', p => p.getByRole('button', { name: /updates|plugins|cli/i }).first())
await shot(page, 'real-product-updates')

await waitForAdmin(page, '/admin/dashboard/retention', p => p.getByRole('button', { name: /trials|churn|inactive/i }).first())
await shot(page, 'real-retention')

await waitForAdmin(page, '/admin/dashboard/customers/organizations', p => p.getByRole('button', { name: /organizations|credits/i }).first())
await shot(page, 'real-customers')
await browser.close()
Loading
Loading