Onboarding Wizard (FC-7 / F1) — Design
Date: 2026-07-23
Backlog item: docs/superpowers/specs/2026-06-16-sprint-backlog.md → Section C, FC-7 (F1), Score 1.0, Thesis: Medium, Effort: M
Scope: Frontend only. No backend, schema, or endpoint changes.
Problem
A first-time user who has connected no data sources lands on an empty dashboard with no
guidance. Getting started currently requires discovering the Sources settings page on their
own. The onboarding wizard closes this gap with a guided, first-run setup flow that walks the
user through connecting sources and kicking off the first metric calculation.
Goals
- Detect a zero-data-source user on dashboard load and present a guided setup wizard.
- Reuse the existing data-source creation form with no duplicated form/validation logic.
- Let the user skip individual steps or dismiss the whole wizard; a dismissal is remembered so
the wizard does not reappear.
- On completion, trigger a metrics calculation for the last 30 days so the dashboard is
populated immediately.
Non-goals
- No backend endpoints, DTOs, or migrations.
- No new React Router route (the wizard is a modal, so no
SpaFallbackController change).
- No welcome/intro screen — the flow is three source steps plus a finish/confirm screen.
- No changes to how data sources are collected, synced, or validated.
Decisions (from brainstorming)
- Form reuse: extract the inline source-creation form out of
DataSourcesPage into a
shared DataSourceForm component consumed by both the settings page and the wizard. This is
a declared refactor (per the root CLAUDE.md "no silent refactors" rule) and is what
satisfies the "no duplicate logic" acceptance criterion.
- Presentation: a centered modal overlay on the dashboard, matching the existing modal
patterns in the codebase (Goal modal in DashboardPage, Link-repo modal in
DataSourcesPage). No new route.
- Step model: 3 fixed source steps + 1 finish step. Steps 1–3 use the shared form with
the source type locked (GitHub, Jira, Local Git); step 4 confirms and triggers the
calculation. Each source step is independently skippable.
Architecture
Reused, unchanged
datasourcesApi.list() — zero-source detection.
datasourcesApi.create(req) — creating a source in each step.
metricsApi.calculate(from, to) → POST /metrics/calculate?from=&to= — first calculation.
New files
frontend/src/components/datasources/DataSourceForm.tsx — the form extracted from
DataSourcesPage. Props:
lockedType?: DataSourceType — when set, hides the type-picker and fixes the type (wizard
passes this); when omitted, renders the full type-picker (settings page behavior).
teamId?: string — optional team assignment (settings page manager/admin path).
onCreated: (created: DataSourceConfig) => void — success callback.
onError?: (message: string) => void — surfaces the create error message.
- Owns its own field state and submit handler (moved verbatim from
DataSourcesPage), calling
datasourcesApi.create.
frontend/src/components/onboarding/OnboardingWizard.tsx — the modal wizard.
frontend/src/hooks/useOnboarding.ts — localStorage-backed dismissal flag.
Refactored (declared scope)
frontend/src/pages/DataSourcesPage.tsx — the inline "New source form" block
(DataSourcesPage.tsx:906–1068) is replaced by a <DataSourceForm> usage. The type-picker,
field rendering, token visibility toggle, team selector, and submit/validation logic move into
DataSourceForm. No behavior change for the settings page.
frontend/src/pages/DashboardPage.tsx — add a datasourcesApi.list() query and conditionally
render <OnboardingWizard>.
Trigger logic
In DashboardPage:
const { data: sources } = useQuery({ queryKey: ['datasources'], queryFn: () => datasourcesApi.list()... });
const { dismissed, dismiss } = useOnboarding();
const showWizard = !dismissed && (sources?.length ?? 0) === 0;
The wizard renders only when sources.length === 0 AND not dismissed. Using the shared
['datasources'] query key means the list is already warm/invalidated by the settings page and
by source creation inside the wizard.
Steps
| Step |
Content |
"Skip" |
Primary action |
| 1/4 |
DataSourceForm locked to GITHUB |
advance to step 2 (no source created) |
on onCreated: mark "added ✓", enable Next |
| 2/4 |
DataSourceForm locked to JIRA |
advance to step 3 |
same |
| 3/4 |
DataSourceForm locked to GIT_LOCAL |
advance to step 4 |
same |
| 4/4 |
Summary of sources added this session + "Finish & calculate" |
n/a |
trigger calculate(from, to), then dismiss + close |
- A progress indicator (
1/4 … 4/4) is shown at the top of the modal.
- A persistent "Skip all" control dismisses the entire wizard from any step.
- After a successful
create() in a source step, the step does not auto-close; it shows an
"added ✓" confirmation and enables Next, so the user stays oriented.
Persistence
useOnboarding reads/writes localStorage key da:onboarding-dismissed (value "1").
- Set on Finish (step 4 completes) or Skip all.
- The trigger guard is
zero-sources AND not-dismissed, so a user who later deletes all their
sources is not nagged again.
useOnboarding exposes { dismissed: boolean, dismiss: () => void } and reads the flag on
mount.
Finish → calculation
On "Finish & calculate":
- Compute
to = today and from = today − 30 days as YYYY-MM-DD (using the existing date
helpers in lib/dates).
- Call
metricsApi.calculate(from, to) directly with the 30-day range; show a spinner on
the button while pending. (Do not dispatch the dashboard's da:recalculate event — that
recalculates the dashboard's currently selected range, not the 30-day window this step
requires.)
- On settle (success or error):
dismiss(), invalidate the metric queries so the dashboard
refreshes (queryClient.invalidateQueries), and close the modal.
- A failed calculation still dismisses and closes — the user can recalculate from the dashboard
later; the wizard's job (source setup) is done.
Error & edge states
- Create error:
DataSourceForm surfaces the API message inline using the existing coral
error block; the step stays open for retry.
- Calculate error: button returns to idle with a brief inline error; dismiss + close still
proceed (see above).
datasources list still loading: wizard is not shown until the query resolves (guard
treats undefined as "not zero").
- User adds a source, then reopens: once any source exists the guard fails, so the wizard
will not show on subsequent loads even before the dismissal flag is set.
Testing
Per frontend/CLAUDE.md (component / hook / API-client layers; loading-error-empty where
applicable):
DataSourceForm.test.tsx
- renders the type-picker when
lockedType is omitted; hides it and fixes the type when set;
- renders the correct fields per type (base URL / path / token / repo full name / project key);
- submits the correct
CreateDataSourceRequest payload via datasourcesApi.create;
- surfaces a create error via
onError / inline block.
OnboardingWizard.test.tsx
- appears for a zero-source user; hidden when a source exists;
- "Skip" advances to the next step without creating a source;
- "Skip all" dismisses and writes
da:onboarding-dismissed;
- creating a source enables Next and shows the "added ✓" state;
- Finish triggers
calculate with a 30-day range (to = today, from = today − 30d) and
dismisses;
- does not reappear when
da:onboarding-dismissed is set.
useOnboarding.test.ts — reads absent flag as not-dismissed; dismiss() persists the flag;
reads a previously persisted flag on mount.
DashboardPage.test.tsx (update) — add a @/api/datasources mock. Existing tests mock it
to return a non-empty list so the wizard stays hidden; add one case where an empty list
surfaces the wizard.
Acceptance criteria (from backlog FC-7)
Files touched (summary)
| File |
Change |
components/datasources/DataSourceForm.tsx |
new — extracted shared form |
components/onboarding/OnboardingWizard.tsx |
new — modal wizard |
hooks/useOnboarding.ts |
new — localStorage dismissal flag |
pages/DataSourcesPage.tsx |
refactor — consume DataSourceForm (declared) |
pages/DashboardPage.tsx |
add datasources query + render wizard |
*.test.tsx / *.test.ts |
new tests + update DashboardPage.test.tsx |
Onboarding Wizard (FC-7 / F1) — Design
Date: 2026-07-23
Backlog item:
docs/superpowers/specs/2026-06-16-sprint-backlog.md→ Section C, FC-7 (F1), Score 1.0, Thesis: Medium, Effort: MScope: Frontend only. No backend, schema, or endpoint changes.
Problem
A first-time user who has connected no data sources lands on an empty dashboard with no
guidance. Getting started currently requires discovering the Sources settings page on their
own. The onboarding wizard closes this gap with a guided, first-run setup flow that walks the
user through connecting sources and kicking off the first metric calculation.
Goals
the wizard does not reappear.
populated immediately.
Non-goals
SpaFallbackControllerchange).Decisions (from brainstorming)
DataSourcesPageinto ashared
DataSourceFormcomponent consumed by both the settings page and the wizard. This isa declared refactor (per the root
CLAUDE.md"no silent refactors" rule) and is whatsatisfies the "no duplicate logic" acceptance criterion.
patterns in the codebase (Goal modal in
DashboardPage, Link-repo modal inDataSourcesPage). No new route.the source
typelocked (GitHub, Jira, Local Git); step 4 confirms and triggers thecalculation. Each source step is independently skippable.
Architecture
Reused, unchanged
datasourcesApi.list()— zero-source detection.datasourcesApi.create(req)— creating a source in each step.metricsApi.calculate(from, to)→POST /metrics/calculate?from=&to=— first calculation.New files
frontend/src/components/datasources/DataSourceForm.tsx— the form extracted fromDataSourcesPage. Props:lockedType?: DataSourceType— when set, hides the type-picker and fixes the type (wizardpasses this); when omitted, renders the full type-picker (settings page behavior).
teamId?: string— optional team assignment (settings page manager/admin path).onCreated: (created: DataSourceConfig) => void— success callback.onError?: (message: string) => void— surfaces the create error message.DataSourcesPage), callingdatasourcesApi.create.frontend/src/components/onboarding/OnboardingWizard.tsx— the modal wizard.frontend/src/hooks/useOnboarding.ts— localStorage-backed dismissal flag.Refactored (declared scope)
frontend/src/pages/DataSourcesPage.tsx— the inline "New source form" block(
DataSourcesPage.tsx:906–1068) is replaced by a<DataSourceForm>usage. The type-picker,field rendering, token visibility toggle, team selector, and submit/validation logic move into
DataSourceForm. No behavior change for the settings page.frontend/src/pages/DashboardPage.tsx— add adatasourcesApi.list()query and conditionallyrender
<OnboardingWizard>.Trigger logic
In
DashboardPage:The wizard renders only when
sources.length === 0AND not dismissed. Using the shared['datasources']query key means the list is already warm/invalidated by the settings page andby source creation inside the wizard.
Steps
DataSourceFormlocked toGITHUBonCreated: mark "added ✓", enable NextDataSourceFormlocked toJIRADataSourceFormlocked toGIT_LOCALcalculate(from, to), then dismiss + close1/4 … 4/4) is shown at the top of the modal.create()in a source step, the step does not auto-close; it shows an"added ✓" confirmation and enables Next, so the user stays oriented.
Persistence
useOnboardingreads/writes localStorage keyda:onboarding-dismissed(value"1").zero-sources AND not-dismissed, so a user who later deletes all theirsources is not nagged again.
useOnboardingexposes{ dismissed: boolean, dismiss: () => void }and reads the flag onmount.
Finish → calculation
On "Finish & calculate":
to = todayandfrom = today − 30 daysasYYYY-MM-DD(using the existing datehelpers in
lib/dates).metricsApi.calculate(from, to)directly with the 30-day range; show a spinner onthe button while pending. (Do not dispatch the dashboard's
da:recalculateevent — thatrecalculates the dashboard's currently selected range, not the 30-day window this step
requires.)
dismiss(), invalidate the metric queries so the dashboardrefreshes (
queryClient.invalidateQueries), and close the modal.later; the wizard's job (source setup) is done.
Error & edge states
DataSourceFormsurfaces the API message inline using the existing coralerror block; the step stays open for retry.
proceed (see above).
datasourceslist still loading: wizard is not shown until the query resolves (guardtreats
undefinedas "not zero").will not show on subsequent loads even before the dismissal flag is set.
Testing
Per
frontend/CLAUDE.md(component / hook / API-client layers; loading-error-empty whereapplicable):
DataSourceForm.test.tsxlockedTypeis omitted; hides it and fixes the type when set;CreateDataSourceRequestpayload viadatasourcesApi.create;onError/ inline block.OnboardingWizard.test.tsxda:onboarding-dismissed;calculatewith a 30-day range (to = today,from = today − 30d) anddismisses;
da:onboarding-dismissedis set.useOnboarding.test.ts— reads absent flag as not-dismissed;dismiss()persists the flag;reads a previously persisted flag on mount.
DashboardPage.test.tsx(update) — add a@/api/datasourcesmock. Existing tests mock itto return a non-empty list so the wizard stays hidden; add one case where an empty list
surfaces the wizard.
Acceptance criteria (from backlog FC-7)
shared
DataSourceForm.localStorage(da:onboarding-dismissed) — wizard does notreappear.
Files touched (summary)
components/datasources/DataSourceForm.tsxcomponents/onboarding/OnboardingWizard.tsxhooks/useOnboarding.tspages/DataSourcesPage.tsxDataSourceForm(declared)pages/DashboardPage.tsx*.test.tsx/*.test.tsDashboardPage.test.tsx