Skip to content

Onboarding Wizard — Design #93

Description

@julia-shtal

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)

  1. 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.
  2. 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.
  3. 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":

  1. Compute to = today and from = today − 30 days as YYYY-MM-DD (using the existing date
    helpers in lib/dates).
  2. 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.)
  3. On settle (success or error): dismiss(), invalidate the metric queries so the dashboard
    refreshes (queryClient.invalidateQueries), and close the modal.
  4. 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)

  • Wizard appears for users with zero data sources on dashboard load.
  • Each step wraps the existing data-source creation form (no duplicate form logic) via the
    shared DataSourceForm.
  • "Skip" advances to the next step; "Skip all" dismisses.
  • Dismissed state stored in localStorage (da:onboarding-dismissed) — wizard does not
    reappear.
  • Completing the wizard triggers metrics calculation for the last 30 days.
  • Frontend integration test asserts the wizard appears for new users and can be skipped.

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions