Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9eb20ff
docs(admin): design daily setup CLI outcomes
WcaleNieWolny Aug 15, 2026
471fa4d
docs(admin): plan daily Setup CLI outcomes
WcaleNieWolny Aug 15, 2026
78959f0
feat(admin): define daily Setup CLI outcomes
WcaleNieWolny Aug 15, 2026
9fafdfa
fix(admin): align daily Setup CLI outcomes model
WcaleNieWolny Aug 15, 2026
8a54c84
feat(admin): attribute daily Setup CLI actions
WcaleNieWolny Aug 15, 2026
d990e47
fix(admin): correct daily Setup action attribution
WcaleNieWolny Aug 15, 2026
ad1b882
feat(admin): query daily Setup CLI events
WcaleNieWolny Aug 15, 2026
f987748
fix(admin): validate daily Setup CLI totals
WcaleNieWolny Aug 15, 2026
fa8d368
fix(admin): reject incomplete Setup CLI events
WcaleNieWolny Aug 15, 2026
67e5951
fix(admin): harden Setup CLI event parsing
WcaleNieWolny Aug 15, 2026
91e5020
feat(admin): expose daily Setup CLI outcomes
WcaleNieWolny Aug 15, 2026
7d8d163
feat(admin): support grouped stacked bar charts
WcaleNieWolny Aug 15, 2026
3295839
fix(admin): color grouped chart legend labels
WcaleNieWolny Aug 15, 2026
cf48d43
feat(admin): map daily Setup CLI chart series
WcaleNieWolny Aug 15, 2026
7cb46dd
feat(admin): chart daily Setup CLI outcomes
WcaleNieWolny Aug 15, 2026
32256fa
test(admin): bound daily CLI chart assertions
WcaleNieWolny Aug 15, 2026
edbe4f1
fix(i18n): add daily CLI chart context
WcaleNieWolny Aug 15, 2026
41e54cb
fix(admin): clarify daily CLI attribution copy
WcaleNieWolny Aug 15, 2026
eac3bf4
docs(admin): avoid typo check on commit hash
WcaleNieWolny Aug 15, 2026
881db58
fix(admin): address daily outcomes review
WcaleNieWolny Aug 15, 2026
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
1,718 changes: 1,718 additions & 0 deletions docs/superpowers/plans/2026-08-15-daily-setup-cli-outcomes.md

Large diffs are not rendered by default.

310 changes: 310 additions & 0 deletions docs/superpowers/specs/2026-08-15-daily-setup-cli-outcomes-design.md

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions messages/en.context.json
Original file line number Diff line number Diff line change
Expand Up @@ -1472,6 +1472,22 @@
"frontend-onboarding-daily-details-to-organization": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart title. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-intent-to-details": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart title. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-organization-to-setup": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart title. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-ai-copy-init": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart category label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-ai-copy-no-cli": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart category label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-ai-copy-other-cli": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart category label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-both-copy-init": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart category label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-both-copy-no-cli": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart category label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-both-copy-other-cli": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart category label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-cli-copy-init": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart category label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-cli-copy-no-cli": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart category label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-cli-copy-other-cli": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart category label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-first-time": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart lifecycle label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-no-action": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart category label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-no-copy-init": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart category label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-no-copy-other-cli": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart category label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-outcomes-description": "Used in Capgo web console areas: pages/admin/dashboard. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-outcomes-v2": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart title. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-daily-setup-cli-returning": "Used in Capgo web console areas: pages/admin/dashboard. Role: chart lifecycle label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-funnel-description": "Used in Capgo web console areas: pages/admin/dashboard. Role: helper or description text. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-funnel-v1-legacy": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
"frontend-onboarding-funnel-v2": "Used in Capgo web console areas: pages/admin/dashboard. Role: UI label. Translate for UI; keep Capgo product names, code, and placeholders unchanged.",
Expand Down
16 changes: 16 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3015,6 +3015,22 @@
"frontend-onboarding-setup-cli-only": "Started CLI",
"frontend-onboarding-setup-cli-and-ai": "Started CLI + AI instructions",
"frontend-onboarding-setup-no-cli": "Didn't start CLI",
"frontend-onboarding-daily-setup-cli-outcomes-v2": "Daily Setup → CLI outcomes (v2)",
"frontend-onboarding-daily-setup-cli-outcomes-description": "Each person is counted once per UTC day. Left: first-time Setup views; right: returning views. Actions are attributed for up to 24 hours.",
"frontend-onboarding-daily-setup-cli-first-time": "First-time",
"frontend-onboarding-daily-setup-cli-returning": "Returning",
"frontend-onboarding-daily-setup-cli-cli-copy-init": "CLI copy + init",
"frontend-onboarding-daily-setup-cli-ai-copy-init": "AI copy + init",
"frontend-onboarding-daily-setup-cli-both-copy-init": "Both copied + init",
"frontend-onboarding-daily-setup-cli-no-copy-init": "No copy + init",
"frontend-onboarding-daily-setup-cli-cli-copy-other-cli": "CLI copy + other CLI",
"frontend-onboarding-daily-setup-cli-ai-copy-other-cli": "AI copy + other CLI",
"frontend-onboarding-daily-setup-cli-both-copy-other-cli": "Both copied + other CLI",
"frontend-onboarding-daily-setup-cli-no-copy-other-cli": "No copy + other CLI",
"frontend-onboarding-daily-setup-cli-cli-copy-no-cli": "CLI copied · no CLI run",
"frontend-onboarding-daily-setup-cli-ai-copy-no-cli": "AI copied · no CLI run",
"frontend-onboarding-daily-setup-cli-both-copy-no-cli": "Both copied · no CLI run",
"frontend-onboarding-daily-setup-cli-no-action": "No action",
"frontend-onboarding-people": "people",
"frontend-onboarding-no-dropoff": "No drop-off",
"frontend-onboarding-transition": "{from} → {to}"
Expand Down
7 changes: 6 additions & 1 deletion src/components/admin/AdminStackedBarChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ interface DataSeries {
label: string
data: Array<{ date: string, value: number }>
color: string
stack?: string
stackLabel?: string
}

const props = defineProps({
Expand Down Expand Up @@ -56,6 +58,8 @@ const chartData = computed(() => {
label: item.label,
data: item.data.map(point => point.value),
color: item.color,
stack: item.stack,
stackLabel: item.stackLabel,
}))

return applyAdminStackedBarAccessibleBorders(
Expand All @@ -65,7 +69,8 @@ const chartData = computed(() => {
)
})

const chartOptions = computed(() => buildAdminStackedBarChartOptions(isDark.value))
const hasGroupedStacks = computed(() => props.series.some(item => item.stack !== undefined))
const chartOptions = computed(() => buildAdminStackedBarChartOptions(isDark.value, hasGroupedStacks.value))
</script>

<template>
Expand Down
114 changes: 104 additions & 10 deletions src/components/admin/adminStackedBarChart.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import type { ChartData, ChartOptions } from 'chart.js'
import type { ChartData, Chart as ChartJs, ChartOptions, LegendItem } from 'chart.js'
import { formatNumberValue } from '~/services/formatLocale'

export interface AdminStackedBarDataset {
label: string
data: number[]
color: string
stack?: string
stackLabel?: string
}

export type AdminStackedBarChartDataset = ChartData<'bar'>['datasets'][number] & { stackLabel?: string }
export type AdminStackedBarChartData = Omit<ChartData<'bar'>, 'datasets'> & { datasets: AdminStackedBarChartDataset[] }
type AdminStackedBarCompatibleDataset = Pick<
ChartData['datasets'][number],
'backgroundColor' | 'borderColor' | 'borderWidth' | 'data' | 'label'
> & { stack?: string, stackLabel?: string }

export function buildAdminStackedBarChartData(
labels: string[],
series: AdminStackedBarDataset[],
): ChartData<'bar'> {
): AdminStackedBarChartData {
return {
labels,
datasets: series.map(item => ({
Expand All @@ -21,15 +30,17 @@ export function buildAdminStackedBarChartData(
borderWidth: 0,
borderRadius: 4,
borderSkipped: false,
stack: item.stack,
stackLabel: item.stackLabel,
})),
}
}

export function applyAdminStackedBarAccessibleBorders(
data: ChartData<'bar'>,
data: AdminStackedBarChartData,
accessibleBorders: boolean,
isDark: boolean,
): ChartData<'bar'> {
): AdminStackedBarChartData {
if (!accessibleBorders)
return data

Expand All @@ -48,15 +59,88 @@ export function formatAdminStackedBarTooltip(label: string, value: number, total
return `${label}: ${formatNumberValue(value)} (${formatNumberValue(percentage, { maximumFractionDigits: 1 })}%)`
}

export function buildAdminStackedBarChartOptions(isDark: boolean): ChartOptions<'bar'> {
interface AdminLegendDataChart {
data: { datasets: AdminStackedBarCompatibleDataset[] }
isDatasetVisible: ChartJs['isDatasetVisible']
}
type AdminLegendToggleChart = AdminLegendDataChart & Pick<ChartJs, 'setDatasetVisibility' | 'update'>

function datasetValue(dataset: AdminStackedBarCompatibleDataset, dataIndex: number): number {
const value = dataset.data[dataIndex]
return typeof value === 'number' ? value : 0
}

function datasetTotal(dataset: AdminStackedBarCompatibleDataset): number {
let total = 0
for (let dataIndex = 0; dataIndex < dataset.data.length; dataIndex++)
total += datasetValue(dataset, dataIndex)
return total
}

export function getAdminStackedBarTooltipTotal(
datasets: readonly AdminStackedBarCompatibleDataset[],
datasetIndex: number,
dataIndex: number,
): number {
const activeStack = datasets[datasetIndex]?.stack
return datasets.reduce((sum, dataset) => {
if (activeStack !== undefined && dataset.stack !== activeStack)
return sum
return sum + datasetValue(dataset, dataIndex)
}, 0)
}

export function buildAdminStackedBarLegendItems(
chart: AdminLegendDataChart,
fontColor: LegendItem['fontColor'] = '#4b5563',
): LegendItem[] {
const seen = new Set<string>()
return chart.data.datasets.flatMap((dataset, datasetIndex) => {
const label = dataset.label ?? ''
if (!label || seen.has(label) || datasetTotal(dataset) === 0)
return []

const matchingDatasets = chart.data.datasets
.map((candidate, index) => ({ candidate, index }))
.filter(({ candidate }) => candidate.label === label)

seen.add(label)
return [{
text: label,
fillStyle: typeof dataset.backgroundColor === 'string' ? dataset.backgroundColor : '#94a3b8',
fontColor,
strokeStyle: typeof dataset.borderColor === 'string' ? dataset.borderColor : '#94a3b8',
lineWidth: typeof dataset.borderWidth === 'number' ? dataset.borderWidth : 0,
hidden: matchingDatasets.every(({ index }) => !chart.isDatasetVisible(index)),
datasetIndex,
pointStyle: 'circle' as const,
}]
})
}

export function toggleAdminStackedBarLegendGroup(chart: AdminLegendToggleChart, label: string): void {
const indexes = chart.data.datasets
.map((dataset, index) => ({ dataset, index }))
.filter(({ dataset }) => dataset.label === label)
.map(({ index }) => index)
const show = indexes.every(index => !chart.isDatasetVisible(index))
for (const index of indexes)
chart.setDatasetVisibility(index, show)
chart.update()
}

export function buildAdminStackedBarChartOptions(
isDark: boolean,
groupedStacks = false,
): ChartOptions<'bar'> {
const textColor = isDark ? '#d1d5db' : '#4b5563'
const mutedTextColor = isDark ? '#9ca3af' : '#6b7280'

return {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
mode: groupedStacks ? 'nearest' : 'index',
intersect: false,
},
layout: {
Expand All @@ -81,7 +165,11 @@ export function buildAdminStackedBarChartOptions(isDark: boolean): ChartOptions<
},
padding: 18,
usePointStyle: true,
...(groupedStacks ? { generateLabels: chart => buildAdminStackedBarLegendItems(chart, textColor) } : {}),
},
...(groupedStacks
? { onClick: (_event, item, legend) => toggleAdminStackedBarLegendGroup(legend.chart, item.text) }
: {}),
},
tooltip: {
backgroundColor: isDark ? 'rgba(31, 41, 55, 0.95)' : 'rgba(255, 255, 255, 0.95)',
Expand All @@ -94,10 +182,16 @@ export function buildAdminStackedBarChartOptions(isDark: boolean): ChartOptions<
callbacks: {
label: (context) => {
const value = Number(context.parsed.y ?? 0)
const total = context.chart.data.datasets.reduce((sum, dataset) => {
return sum + Number(dataset.data[context.dataIndex] ?? 0)
}, 0)
return formatAdminStackedBarTooltip(context.dataset.label || '', value, total)
const total = getAdminStackedBarTooltipTotal(
context.chart.data.datasets,
context.datasetIndex,
context.dataIndex,
)
const dataset: AdminStackedBarCompatibleDataset = context.dataset
const label = dataset.stackLabel
? `${dataset.stackLabel} · ${dataset.label ?? ''}`
: dataset.label ?? ''
return formatAdminStackedBarTooltip(label, value, total)
},
},
},
Expand Down
49 changes: 48 additions & 1 deletion src/pages/admin/dashboard/frontend-onboarding.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ meta:

<script setup lang="ts">
import type { AdminOnboardingJourneyGraphConfig, AdminOnboardingJourneyNode } from '~/components/admin/adminOnboardingJourneyGraph'
import type { FrontendOnboardingAnalytics } from '~/services/adminFrontendOnboarding'
import type {
FrontendOnboardingAnalytics,
FrontendOnboardingDailySetupCliOutcomeKey,
} from '~/services/adminFrontendOnboarding'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
Expand All @@ -20,6 +23,7 @@ import ChartCard from '~/components/dashboard/ChartCard.vue'
import PageLoader from '~/components/PageLoader.vue'
import {
buildFrontendOnboardingDailySeries,
buildFrontendOnboardingDailySetupCliSeries,
buildFrontendOnboardingFunnelStages,
buildFrontendOnboardingFunnelSummaries,
buildFrontendOnboardingGraphMetrics,
Expand Down Expand Up @@ -167,6 +171,27 @@ const setupCliOutcomeValues = computed(() => [
])
const setupCliOutcomeColors = ['#119eff', '#8b5cf6', '#94a3b8']
const hasSetupCliOutcomeData = computed(() => setupCliOutcomes.value.total_users > 0)
const dailySetupCliOutcomeLabels = computed<Record<FrontendOnboardingDailySetupCliOutcomeKey, string>>(() => ({
cli_copy_init: t('frontend-onboarding-daily-setup-cli-cli-copy-init'),
ai_copy_init: t('frontend-onboarding-daily-setup-cli-ai-copy-init'),
both_copy_init: t('frontend-onboarding-daily-setup-cli-both-copy-init'),
no_copy_init: t('frontend-onboarding-daily-setup-cli-no-copy-init'),
cli_copy_other_cli: t('frontend-onboarding-daily-setup-cli-cli-copy-other-cli'),
ai_copy_other_cli: t('frontend-onboarding-daily-setup-cli-ai-copy-other-cli'),
both_copy_other_cli: t('frontend-onboarding-daily-setup-cli-both-copy-other-cli'),
no_copy_other_cli: t('frontend-onboarding-daily-setup-cli-no-copy-other-cli'),
cli_copy_no_cli: t('frontend-onboarding-daily-setup-cli-cli-copy-no-cli'),
ai_copy_no_cli: t('frontend-onboarding-daily-setup-cli-ai-copy-no-cli'),
both_copy_no_cli: t('frontend-onboarding-daily-setup-cli-both-copy-no-cli'),
no_action: t('frontend-onboarding-daily-setup-cli-no-action'),
}))
const dailySetupCliSeries = computed(() => buildFrontendOnboardingDailySetupCliSeries(
visibleAnalytics.value?.daily_setup_cli_outcomes ?? [],
dailySetupCliOutcomeLabels.value,
t('frontend-onboarding-daily-setup-cli-first-time'),
t('frontend-onboarding-daily-setup-cli-returning'),
))
const hasDailySetupCliOutcomeData = computed(() => dailySetupCliSeries.value.length > 0)

const onboardingGraphV3 = computed<AdminOnboardingJourneyGraphConfig>(() => {
const funnel = visibleAnalytics.value?.funnels.v3 ?? []
Expand Down Expand Up @@ -536,6 +561,28 @@ displayStore.defaultBack = '/dashboard'
/>
</ChartCard>

<ChartCard
:title="t('frontend-onboarding-daily-setup-cli-outcomes-v2')"
:is-loading="isLoadingStats"
:has-data="hasDailySetupCliOutcomeData"
>
<template #header>
<div class="min-w-0">
<h2 class="text-xl font-semibold leading-tight text-slate-900 dark:text-white sm:text-2xl">
{{ t('frontend-onboarding-daily-setup-cli-outcomes-v2') }}
</h2>
<p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
{{ t('frontend-onboarding-daily-setup-cli-outcomes-description') }}
</p>
</div>
</template>
<AdminStackedBarChart
:series="dailySetupCliSeries"
:is-loading="isLoadingStats"
accessible-borders
/>
</ChartCard>

<section class="p-6 bg-white border rounded-lg shadow-lg border-slate-300 dark:bg-gray-800 dark:border-slate-900">
<h2 class="text-lg font-semibold text-slate-900 dark:text-white">
{{ t('frontend-onboarding-funnel-v1-legacy') }}
Expand Down
Loading
Loading