Skip to content

feat(frontend): add FTUE welcome wizard for first-time users#1473

Merged
mergify[bot] merged 3 commits intomainfrom
bgregor/ftue-welcome-wizard
Apr 27, 2026
Merged

feat(frontend): add FTUE welcome wizard for first-time users#1473
mergify[bot] merged 3 commits intomainfrom
bgregor/ftue-welcome-wizard

Conversation

@bobbravo2
Copy link
Copy Markdown
Member

@bobbravo2 bobbravo2 commented Apr 27, 2026

Summary

  • Adds a 4-step onboarding wizard (Welcome → Create Workspace → Connect Integrations → Completion) for first-time users with zero workspaces
  • Introduces INTEGRATION_REGISTRY with compile-time completeness guard so new integrations are automatically surfaced in the wizard
  • Extracts WorkspaceForm with callback interface, shared between CreateWorkspaceDialog and the wizard (no behavior change for existing users)
  • Responsive layout tested across viewports: 1-col (640px) → 2-col (768px) → 3-col (1024px+)

Step 1 - new users with no workspaces get a intro

Screenshot 2026-04-27 at 4 51 27 PM

Step 2 - create workspace

Screenshot 2026-04-27 at 4 51 36 PM

Step 3 - Connect integrations

Screenshot 2026-04-27 at 5 18 18 PM

What's new

File Purpose
components/onboarding/welcome-wizard.tsx Thin state machine shell (107 lines)
components/onboarding/integration-registry.ts Dynamic registry for all 6 integrations
components/onboarding/use-should-show-onboarding.ts Trigger hook (zero projects + localStorage)
components/onboarding/use-app-config.ts Reads server config from meta tags
components/onboarding/steps/*.tsx Welcome, Create Workspace, Integrations, Completion
components/workspace-form.tsx Extracted shared form with callback interface
3 test files (19 tests) Hook, registry, wizard transitions

What changed

  • app/layout.tsx: Added github-app-slug and github-callback-url meta tags (2 lines)
  • create-workspace-dialog.tsx: Refactored to use shared WorkspaceForm (285 → 79 lines)
  • app/projects/page.tsx: Wires wizard + onboarding hook
  • COMPONENT_PATTERNS.md / DEVELOPMENT.md: Extension guide for humans and agents

Testing performed

  • Unit tests: 19 new tests passing (hook trigger logic, registry isConnected derivations, wizard step transitions)
  • Full test suite: 668/668 tests pass, 0 regressions
  • TypeScript: tsc --noEmit clean
  • Local Kind cluster: Validated full flow — workspace creation against live backend, wizard advancing through steps 1→2→3→4
  • Responsive viewports: Manually validated at 640px, 768px, 1024px, 1440px, 1920px
  • Pre-commit hooks: All passed (eslint, trailing-whitespace, etc.)

Testing NOT performed (reviewer note)

  • Integration connection flows within the wizard (GitHub App OAuth redirect, GitLab PAT, etc.) — the cards render and the registry wires them correctly, but we did not complete an actual integration connection during the wizard flow
  • sessionStorage resume after GitHub OAuth redirect (code path exists but untested end-to-end)

How to add a new integration

  1. Add status field to IntegrationsStatus in services/api/integrations.ts
  2. Add one IntegrationEntry to INTEGRATION_REGISTRY in integration-registry.ts
  3. No other onboarding files need to change

The compile-time guard errors if a status key is missing from the registry.

Made with Cursor / Spec that drove these code change: https://gist.github.com/bobbravo2/1f5c26092780d8a0dbdb63f557604921

Summary by CodeRabbit

Release Notes

  • New Features

    • Onboarding wizard guides new users through setup steps: welcome, workspace creation, integration connections, and completion
    • Integration cards for GitHub, GitLab, Google Drive, Jira, CodeRabbit, and Gerrit available in onboarding flow
    • Users can dismiss or skip onboarding at any time
    • Workspace creation form now integrated into onboarding experience
  • Documentation

    • Added component patterns and development guides for onboarding implementation

Adds a multi-step onboarding wizard that guides new users through
workspace creation, integration setup, and starting their first session.

- 4-step wizard: Welcome → Create Workspace → Connect Integrations → Completion
- Dynamic integration registry (INTEGRATION_REGISTRY) with compile-time
  completeness guard -- new integrations only need one registry entry
- Extracted WorkspaceForm with callback interface (shared between wizard
  and CreateWorkspaceDialog, no behavior change for existing users)
- useShouldShowOnboarding hook: triggers on zero workspaces + localStorage flag
- Responsive layout: 1-col (mobile) → 2-col (tablet) → 3-col (desktop)
- Server config via meta tags (github-app-slug, github-callback-url)
- 19 new tests (hook, registry, wizard transitions)
- Documentation: COMPONENT_PATTERNS.md and DEVELOPMENT.md updated with
  extension guide for humans and agents

Made-with: Cursor
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 27, 2026

Deploy Preview for cheerful-kitten-f556a0 canceled.

Name Link
🔨 Latest commit a075cfe
🔍 Latest deploy log https://app.netlify.com/projects/cheerful-kitten-f556a0/deploys/69efd4a355f6210008bc4c19

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

This PR implements a comprehensive onboarding wizard system for new workspaces, introducing a guided 4-step setup flow (welcome → create workspace → integrate services → completion) with documentation patterns, a registry-driven integration card system enforcing compile-time completeness, and reusable WorkspaceForm component.

Changes

Cohort / File(s) Summary
Documentation & Configuration
components/frontend/COMPONENT_PATTERNS.md, components/frontend/DEVELOPMENT.md, components/frontend/src/app/layout.tsx
Added component architecture documentation for onboarding patterns; layout now includes github-app-slug and github-callback-url meta tags for OAuth flows.
Onboarding Wizard Core
components/frontend/src/components/onboarding/welcome-wizard.tsx
4-step wizard component managing state across steps, OAuth sessionStorage resumption, progress tracking, and skip-setup dismissal. Exports WizardState and WizardStepProps types.
Wizard Step Components
components/frontend/src/components/onboarding/steps/*
Four step components: WelcomeStep (intro), CreateWorkspaceStep (project creation with form submission), IntegrationsStep (scrollable service card grid with persistence), CompletionStep (navigation to session/settings).
Integration Registry System
components/frontend/src/components/onboarding/integration-registry.ts
Registry-driven integration management with typed entries for GitHub, GitLab, Google, Jira, CodeRabbit, and Gerrit. Compile-time AssertComplete enforces coverage of all IntegrationsStatus keys.
Onboarding Hooks & Config
components/frontend/src/components/onboarding/use-should-show-onboarding.ts, components/frontend/src/components/onboarding/use-app-config.ts
useShouldShowOnboarding determines visibility based on project count and dismissal flag; useAppConfig reads GitHub credentials from layout meta tags.
Form Components
components/frontend/src/components/workspace-form.tsx, components/frontend/src/components/create-workspace-dialog.tsx
New controlled WorkspaceForm with cluster-aware field rendering (OpenShift/Kubernetes) and validation; CreateWorkspaceDialog refactored to delegate form logic to WorkspaceForm.
Projects Page Integration
components/frontend/src/app/projects/page.tsx
Added onboarding wizard UI to projects page, conditionally displayed based on useShouldShowOnboarding() hook with dismissal persistence.
Test Coverage
components/frontend/src/components/onboarding/__tests__/*
Test suites for integration registry (connection state derivation), useShouldShowOnboarding (visibility logic and dismissal), and WelcomeWizard (step transitions and skip behavior).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ProjectsPage
    participant WelcomeWizard
    participant CreateWorkspaceStep
    participant IntegrationsStep
    participant CompletionStep
    participant API as Backend API
    participant Storage as SessionStorage

    User->>ProjectsPage: Navigate to /projects
    ProjectsPage->>ProjectsPage: useShouldShowOnboarding()<br/>(check project count)
    ProjectsPage->>WelcomeWizard: open=true (if no projects)
    
    User->>WelcomeWizard: Click "Get Started"
    WelcomeWizard->>WelcomeWizard: stepIndex: 0→1
    
    User->>CreateWorkspaceStep: Wizard renders step
    CreateWorkspaceStep->>WorkspaceForm: Display form
    User->>WorkspaceForm: Enter name/displayName
    User->>WorkspaceForm: Click "Create & Continue"
    WorkspaceForm->>API: POST /projects (CreateProjectRequest)
    API-->>WorkspaceForm: Created workspace
    WorkspaceForm->>WelcomeWizard: onNext(workspaceName)
    WelcomeWizard->>WelcomeWizard: stepIndex: 1→2
    
    User->>IntegrationsStep: Wizard renders step
    IntegrationsStep->>API: Fetch IntegrationsStatus
    API-->>IntegrationsStep: Connection status
    IntegrationsStep->>IntegrationsStep: Render IntegrationRegistry cards
    
    User->>IntegrationsStep: Click GitHub App install
    IntegrationsStep->>Storage: Persist wizard state (stepIndex, workspaceName)
    Storage-->>IntegrationsStep: Saved to sessionStorage
    IntegrationsStep->>User: Redirect to GitHub OAuth
    
    User->>IntegrationsStep: Complete OAuth, redirect back
    IntegrationsStep->>Storage: Restore wizard state from sessionStorage
    IntegrationsStep->>WelcomeWizard: Resume at step 2
    User->>IntegrationsStep: Click "Next" (or manual navigation)
    WelcomeWizard->>WelcomeWizard: stepIndex: 2→3
    
    User->>CompletionStep: Wizard renders completion
    User->>CompletionStep: Click "Start a session"
    CompletionStep->>WelcomeWizard: onNext()
    WelcomeWizard->>WelcomeWizard: stepIndex: 3 (last) → onDismiss()
    WelcomeWizard->>ProjectsPage: onDismiss()
    ProjectsPage->>ProjectsPage: setWizardOpen(false)
    ProjectsPage->>ProjectsPage: dismissOnboarding()
    ProjectsPage->>User: Show projects list
Loading
🚥 Pre-merge checks | ✅ 7 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (7 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commits format with type(scope): description and accurately describes the main change—adding a 4-step FTUE welcome wizard for first-time users with zero workspaces.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Performance And Algorithmic Complexity ✅ Passed PR exhibits no blocking performance regressions or algorithmic complexity issues with proper resource cleanup and bounded operations.
Security And Secret Handling ✅ Passed Pull request contains no hardcoded secrets, plaintext credential logging, or injection vulnerabilities. Public GitHub App configuration intentionally exposed; sensitive backend credentials remain server-side only.
Kubernetes Resource Safety ✅ Passed PR modifies only frontend TypeScript/React code in components/frontend/; zero Kubernetes manifests or infrastructure code affected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bgregor/ftue-welcome-wizard
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch bgregor/ftue-welcome-wizard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

<head>
<meta name="backend-ws-base" content={wsBase} />
<meta name="github-app-slug" content={env.GITHUB_APP_SLUG ?? ''} />
<meta name="github-callback-url" content={process.env.GITHUB_CALLBACK_URL ?? ''} />
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this being made more global may have throw a problem for me that I wanted to share:

Screen.Recording.2026-04-27.at.4.50.04.PM.mov

Copy link
Copy Markdown
Member Author

@bobbravo2 bobbravo2 Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sessionStorage resume after GitHub OAuth redirect (code path exists but untested end-to-end)

^ we may have infra callback URIS or secrets that need to rotate

- Add displayName to test wrapper components (react/display-name)
- Remove unused ONBOARDING_FLAG import from wizard test
- Remove unused wizardState destructure in CreateWorkspaceStep

Made-with: Cursor
@bobbravo2 bobbravo2 marked this pull request as ready for review April 27, 2026 21:26
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
components/frontend/src/components/onboarding/__tests__/use-should-show-onboarding.test.ts (1)

43-89: Add a rejected-query test to lock the intended error behavior.

Current tests only assert resolved responses. Please add one case where listProjectsPaginated rejects and assert shouldShow === false after loading settles, so this onboarding gate doesn’t regress.

As per coding guidelines, components/frontend/src/**/*.{ts,tsx}: - Verify loading/error states and error handling in React Query hooks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/frontend/src/components/onboarding/__tests__/use-should-show-onboarding.test.ts`
around lines 43 - 89, Add a new test in use-should-show-onboarding.test.ts that
simulates listProjectsPaginated rejecting: set
mockListPaginated.mockRejectedValue(new Error('fail')), ensure ONBOARDING_FLAG
is not set in localStorage, render the hook via createWrapper and wait for
result.current.isLoading to become false, then assert result.current.shouldShow
=== false; place this alongside the other cases so the test verifies
useShouldShowOnboarding handles rejected queries by hiding the wizard.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/frontend/src/components/onboarding/steps/integrations-step.tsx`:
- Around line 17-18: The integrations query failure path isn't handled: when
useIntegrationsStatus() returns integrations === undefined and isLoading ===
false the component renders empty content; update the IntegrationsStep (or the
component using useIntegrationsStatus) to detect the error case (integrations
=== undefined && !isLoading), render an error state/UI with a clear message, and
wire a retry action that calls refetch from useIntegrationsStatus(); keep
existing loading handling (isLoading) and normal success rendering for
integrations present.

In `@components/frontend/src/components/onboarding/use-should-show-onboarding.ts`:
- Around line 24-33: The hook treats a failed projects query as no projects and
reads localStorage unsafely; update logic to require a successful query before
deciding there are no projects by using the query success flag from
useProjectsPaginated (e.g., check isSuccess alongside isLoading when computing
shouldShow) so errors don't open onboarding, and wrap the ONBOARDING_FLAG read
in the useState initializer in a try/catch with feature checks for window and
localStorage access (fall back to false on any exception) to avoid throws in
restricted browser modes.

In `@components/frontend/src/components/onboarding/welcome-wizard.tsx`:
- Around line 58-67: The wizard never clears the persisted SESSION_KEY on normal
dismiss/completion, causing stale state to resume; update the close/completion
paths (the else branch in handleNext and any explicit dismiss/close handlers
referenced around lines 76-103) to call the same persistence-clearing logic used
in integrations-step.tsx (removeItem(SESSION_KEY) or its helper) before calling
onDismiss() or setStepIndex, and ensure any other exit paths (cancel/close
buttons) also remove SESSION_KEY so the wizard state is always cleared on every
close.
- Around line 42-56: The resume-from-sessionStorage code should validate and
clamp the parsed payload before restoring: when reading SESSION_KEY and
JSON.parse result, ensure parsed.step is a finite integer within
0..(STEPS.length-1) before calling setStepIndex, and only assign
createdWorkspaceName to setWizardState after validating it's a string (or null);
always call sessionStorage.removeItem(SESSION_KEY) in a finally block so
corrupted/invalid JSON doesn't leave stale data, and wrap the parse/validate
logic to handle non-number, out-of-range, or NaN step values safely (use
Number.isInteger and bounds checks) rather than trusting parsed.step directly.

In `@components/frontend/src/components/workspace-form.tsx`:
- Around line 22-29: The generated workspace name can end with a hyphen after
truncation and the OpenShift branch never sets the visible validation state,
causing submit to be silently disabled; update generateWorkspaceName to apply
the truncation then remove leading/trailing hyphens (i.e., run the /^\-+|\-+$/g
trim step after slice) so no trailing hyphen remains, and ensure the OpenShift
path invokes the same name validation flow that sets nameError (or explicitly
sets nameError based on validateWorkspaceName/generateWorkspaceName) so the UI
shows the error and submit enabling logic (which checks nameError) behaves
consistently.
- Around line 50-51: The code derives platform mode from
useClusterInfo.isOpenShift and allows submitting while cluster detection is
unresolved, risking wrong payload; update the component to wait for
clusterLoading to be false (and handle a cluster error state) before deriving
mode from isOpenShift, disable the submit action (and the submit button) when
clusterLoading is true or when useClusterInfo reports an error, and ensure the
submit handler consults this gated state (e.g., check clusterLoading ||
clusterError and return/throw) so setFormData/formData cannot produce the wrong
payload shape until platform type is known.

---

Nitpick comments:
In
`@components/frontend/src/components/onboarding/__tests__/use-should-show-onboarding.test.ts`:
- Around line 43-89: Add a new test in use-should-show-onboarding.test.ts that
simulates listProjectsPaginated rejecting: set
mockListPaginated.mockRejectedValue(new Error('fail')), ensure ONBOARDING_FLAG
is not set in localStorage, render the hook via createWrapper and wait for
result.current.isLoading to become false, then assert result.current.shouldShow
=== false; place this alongside the other cases so the test verifies
useShouldShowOnboarding handles rejected queries by hiding the wizard.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: fd4597ed-d29a-492f-831c-9a3f06316545

📥 Commits

Reviewing files that changed from the base of the PR and between 146a289 and a075cfe.

📒 Files selected for processing (17)
  • components/frontend/COMPONENT_PATTERNS.md
  • components/frontend/DEVELOPMENT.md
  • components/frontend/src/app/layout.tsx
  • components/frontend/src/app/projects/page.tsx
  • components/frontend/src/components/create-workspace-dialog.tsx
  • components/frontend/src/components/onboarding/__tests__/integration-registry.test.ts
  • components/frontend/src/components/onboarding/__tests__/use-should-show-onboarding.test.ts
  • components/frontend/src/components/onboarding/__tests__/welcome-wizard.test.tsx
  • components/frontend/src/components/onboarding/integration-registry.ts
  • components/frontend/src/components/onboarding/steps/completion-step.tsx
  • components/frontend/src/components/onboarding/steps/create-workspace-step.tsx
  • components/frontend/src/components/onboarding/steps/integrations-step.tsx
  • components/frontend/src/components/onboarding/steps/welcome-step.tsx
  • components/frontend/src/components/onboarding/use-app-config.ts
  • components/frontend/src/components/onboarding/use-should-show-onboarding.ts
  • components/frontend/src/components/onboarding/welcome-wizard.tsx
  • components/frontend/src/components/workspace-form.tsx

Comment on lines +17 to +18
const { data: integrations, isLoading, refetch } = useIntegrationsStatus();
const appConfig = useAppConfig();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle the integrations query failure path.

If useIntegrationsStatus() fails, isLoading is false and integrations is undefined, so the main body falls through to null and the step renders as a blank panel with only the footer buttons. Surface an error state with a retry action instead of silently hiding the content.

Suggested fix
-  const { data: integrations, isLoading, refetch } = useIntegrationsStatus();
+  const {
+    data: integrations,
+    isLoading,
+    isError,
+    refetch,
+  } = useIntegrationsStatus();
...
-      {isLoading ? (
+      {isLoading ? (
         <div className="flex items-center justify-center py-8">
           <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
         </div>
+      ) : isError ? (
+        <div className="rounded-md border border-destructive/30 bg-destructive/5 p-4">
+          <p className="text-sm text-destructive">
+            Failed to load integrations. Please try again.
+          </p>
+          <Button
+            variant="outline"
+            className="mt-3"
+            onClick={() => {
+              persistStateBeforeRedirect();
+              refetch();
+            }}
+          >
+            Retry
+          </Button>
+        </div>
       ) : integrations ? (

As per coding guidelines, components/frontend/src/**/*.{ts,tsx}: Verify loading/error states and error handling in React Query hooks.

Also applies to: 63-87

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/onboarding/steps/integrations-step.tsx`
around lines 17 - 18, The integrations query failure path isn't handled: when
useIntegrationsStatus() returns integrations === undefined and isLoading ===
false the component renders empty content; update the IntegrationsStep (or the
component using useIntegrationsStatus) to detect the error case (integrations
=== undefined && !isLoading), render an error state/UI with a clear message, and
wire a retry action that calls refetch from useIntegrationsStatus(); keep
existing loading handling (isLoading) and normal success rendering for
integrations present.

Comment on lines +24 to +33
const { data, isLoading } = useProjectsPaginated({ limit: 1 });

const [dismissed, setDismissed] = useState(() => {
if (typeof window === "undefined") return false;
return localStorage.getItem(ONBOARDING_FLAG) === "true";
});

const hasProjects = (data?.totalCount ?? 0) > 0;
const shouldShow = !isLoading && !hasProjects && !dismissed;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Gate onboarding on successful query and harden localStorage reads.

On Line 32, a failed projects query is currently treated as “zero projects,” which can incorrectly open onboarding for existing users. Also, Line 28 can throw when storage access is blocked (privacy/restricted browser modes), causing hook initialization to fail.

Proposed fix
-  const { data, isLoading } = useProjectsPaginated({ limit: 1 });
+  const { data, isLoading, isError } = useProjectsPaginated({ limit: 1 });

   const [dismissed, setDismissed] = useState(() => {
     if (typeof window === "undefined") return false;
-    return localStorage.getItem(ONBOARDING_FLAG) === "true";
+    try {
+      return localStorage.getItem(ONBOARDING_FLAG) === "true";
+    } catch {
+      return false;
+    }
   });

   const hasProjects = (data?.totalCount ?? 0) > 0;
-  const shouldShow = !isLoading && !hasProjects && !dismissed;
+  const shouldShow = !isLoading && !isError && !hasProjects && !dismissed;

As per coding guidelines, components/frontend/src/**/*.{ts,tsx}: - Verify loading/error states and error handling in React Query hooks.

Also applies to: 26-29

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/onboarding/use-should-show-onboarding.ts`
around lines 24 - 33, The hook treats a failed projects query as no projects and
reads localStorage unsafely; update logic to require a successful query before
deciding there are no projects by using the query success flag from
useProjectsPaginated (e.g., check isSuccess alongside isLoading when computing
shouldShow) so errors don't open onboarding, and wrap the ONBOARDING_FLAG read
in the useState initializer in a try/catch with feature checks for window and
localStorage access (fall back to false on any exception) to avoid throws in
restricted browser modes.

Comment on lines +42 to +56
// Resume from sessionStorage after OAuth redirect (GitHub App install)
useEffect(() => {
try {
const saved = sessionStorage.getItem(SESSION_KEY);
if (saved) {
const parsed = JSON.parse(saved) as { step?: number; createdWorkspaceName?: string };
if (parsed.step !== undefined) setStepIndex(parsed.step);
if (parsed.createdWorkspaceName)
setWizardState((s) => ({ ...s, createdWorkspaceName: parsed.createdWorkspaceName ?? null }));
sessionStorage.removeItem(SESSION_KEY);
}
} catch {
// sessionStorage may be unavailable or contain invalid JSON
}
}, []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Harden the resume payload before restoring it.

Line 48 trusts parsed.step directly. A stale/corrupted sessionStorage value outside 0..STEPS.length - 1 makes STEPS[stepIndex] undefined on the next render, and invalid JSON is never removed because cleanup only happens on the happy path. Clamp the step and clear the key in finally.

Suggested fix
   useEffect(() => {
     try {
       const saved = sessionStorage.getItem(SESSION_KEY);
-      if (saved) {
-        const parsed = JSON.parse(saved) as { step?: number; createdWorkspaceName?: string };
-        if (parsed.step !== undefined) setStepIndex(parsed.step);
-        if (parsed.createdWorkspaceName)
-          setWizardState((s) => ({ ...s, createdWorkspaceName: parsed.createdWorkspaceName ?? null }));
-        sessionStorage.removeItem(SESSION_KEY);
-      }
+      if (!saved) return;
+
+      const parsed = JSON.parse(saved) as {
+        step?: number;
+        createdWorkspaceName?: string;
+      };
+
+      if (
+        typeof parsed.step === "number" &&
+        parsed.step >= 0 &&
+        parsed.step < STEPS.length
+      ) {
+        setStepIndex(parsed.step);
+      }
+
+      if (typeof parsed.createdWorkspaceName === "string" && parsed.createdWorkspaceName) {
+        setWizardState((s) => ({
+          ...s,
+          createdWorkspaceName: parsed.createdWorkspaceName,
+        }));
+      }
     } catch {
       // sessionStorage may be unavailable or contain invalid JSON
+    } finally {
+      try {
+        sessionStorage.removeItem(SESSION_KEY);
+      } catch {
+        // ignore storage failures
+      }
     }
   }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/onboarding/welcome-wizard.tsx` around
lines 42 - 56, The resume-from-sessionStorage code should validate and clamp the
parsed payload before restoring: when reading SESSION_KEY and JSON.parse result,
ensure parsed.step is a finite integer within 0..(STEPS.length-1) before calling
setStepIndex, and only assign createdWorkspaceName to setWizardState after
validating it's a string (or null); always call
sessionStorage.removeItem(SESSION_KEY) in a finally block so corrupted/invalid
JSON doesn't leave stale data, and wrap the parse/validate logic to handle
non-number, out-of-range, or NaN step values safely (use Number.isInteger and
bounds checks) rather than trusting parsed.step directly.

Comment on lines +58 to +67
const handleNext = useCallback(
(update?: Partial<WizardState>) => {
if (update) setWizardState((prev) => ({ ...prev, ...update }));
if (stepIndex < STEPS.length - 1) {
setStepIndex((i) => i + 1);
} else {
onDismiss();
}
},
[stepIndex, onDismiss]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clear persisted wizard state on every close path.

components/frontend/src/components/onboarding/steps/integrations-step.tsx writes SESSION_KEY before refresh, but this component never clears it on normal dismiss/completion. If the user refreshes integration status and then finishes or skips the wizard without a remount, the next open/reload can resume with stale step/workspace data.

Suggested fix
 export function WelcomeWizard({ open, onDismiss }: WelcomeWizardProps) {
@@
+  const dismissWizard = useCallback(() => {
+    try {
+      sessionStorage.removeItem(SESSION_KEY);
+    } catch {
+      // sessionStorage may be unavailable
+    }
+    onDismiss();
+  }, [onDismiss]);
+
   const handleNext = useCallback(
     (update?: Partial<WizardState>) => {
       if (update) setWizardState((prev) => ({ ...prev, ...update }));
       if (stepIndex < STEPS.length - 1) {
         setStepIndex((i) => i + 1);
       } else {
-        onDismiss();
+        dismissWizard();
       }
     },
-    [stepIndex, onDismiss]
+    [stepIndex, dismissWizard]
   );
@@
-    <Dialog open={open} onOpenChange={(o) => { if (!o) onDismiss(); }}>
+    <Dialog open={open} onOpenChange={(o) => { if (!o) dismissWizard(); }}>
@@
         <StepComponent
           onNext={handleNext}
-          onSkip={onDismiss}
+          onSkip={dismissWizard}
           wizardState={wizardState}
         />
@@
           <button
             type="button"
-            onClick={onDismiss}
+            onClick={dismissWizard}
             className="text-xs text-muted-foreground hover:text-foreground transition-colors self-center mt-2"
           >

Also applies to: 76-103

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/onboarding/welcome-wizard.tsx` around
lines 58 - 67, The wizard never clears the persisted SESSION_KEY on normal
dismiss/completion, causing stale state to resume; update the close/completion
paths (the else branch in handleNext and any explicit dismiss/close handlers
referenced around lines 76-103) to call the same persistence-clearing logic used
in integrations-step.tsx (removeItem(SESSION_KEY) or its helper) before calling
onDismiss() or setStepIndex, and ensure any other exit paths (cancel/close
buttons) also remove SESSION_KEY so the wizard state is always cleared on every
close.

Comment on lines +22 to +29
function generateWorkspaceName(displayName: string): string {
return displayName
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "")
.replace(/-+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 63);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

OpenShift path can silently disable submit without an actionable error

Line 192 disables submit on nameError, but in the OpenShift branch (Line 120-132) that error is never shown. Also, Line 22-29 can produce a post-truncation trailing hyphen, triggering this hidden invalid state.

Proposed fix
 function generateWorkspaceName(displayName: string): string {
-  return displayName
+  return displayName
     .toLowerCase()
     .replace(/\s+/g, "-")
     .replace(/[^a-z0-9-]/g, "")
     .replace(/-+/g, "-")
     .replace(/^-+|-+$/g, "")
-    .slice(0, 63);
+    .slice(0, 63)
+    .replace(/^-+|-+$/g, "");
 }
...
         {isOpenShift && (
           <div className="space-y-2">
             <Label htmlFor="displayName">Workspace Name *</Label>
             <Input
               id="displayName"
               data-testid="workspace-display-name-input"
               value={formData.displayName}
               onChange={(e) => handleDisplayNameChange(e.target.value)}
               placeholder="e.g. My Research Workspace"
               maxLength={100}
             />
+            {nameError && (
+              <p className="text-sm text-red-600 dark:text-red-400">{nameError}</p>
+            )}
           </div>
         )}

As per coding guidelines, **/*: "Flag bugs, security vulnerabilities, logic errors, data loss risks, and meaningful refactoring opportunities."

Also applies to: 57-71, 120-132, 192-192

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/workspace-form.tsx` around lines 22 - 29,
The generated workspace name can end with a hyphen after truncation and the
OpenShift branch never sets the visible validation state, causing submit to be
silently disabled; update generateWorkspaceName to apply the truncation then
remove leading/trailing hyphens (i.e., run the /^\-+|\-+$/g trim step after
slice) so no trailing hyphen remains, and ensure the OpenShift path invokes the
same name validation flow that sets nameError (or explicitly sets nameError
based on validateWorkspaceName/generateWorkspaceName) so the UI shows the error
and submit enabling logic (which checks nameError) behaves consistently.

Comment on lines +50 to +51
const { isOpenShift, isLoading: clusterLoading } = useClusterInfo();
const [formData, setFormData] = useState<CreateProjectRequest>({ name: "", displayName: "", description: "" });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Gate form mode and submit on cluster-info resolution

Line 50/Line 120-159 currently derive mode from isOpenShift even while cluster detection is unresolved (or errored), and Line 192 still allows submit. This can send the wrong payload shape before the platform type is known.

Proposed fix
-  const { isOpenShift, isLoading: clusterLoading } = useClusterInfo();
+  const { isOpenShift, isLoading: clusterLoading, isError: clusterError } = useClusterInfo();

+  if (clusterLoading) {
+    return (
+      <Alert>
+        <Loader2 className="h-4 w-4 animate-spin" />
+        <AlertDescription>Detecting cluster capabilities…</AlertDescription>
+      </Alert>
+    );
+  }
+
+  if (clusterError) {
+    return (
+      <Alert variant="destructive">
+        <AlertDescription>
+          Could not determine cluster type. Please retry before creating a workspace.
+        </AlertDescription>
+      </Alert>
+    );
+  }
...
-        <Button data-testid="create-workspace-submit" type="submit" disabled={isSubmitting || !!nameError}>
+        <Button
+          data-testid="create-workspace-submit"
+          type="submit"
+          disabled={isSubmitting || clusterLoading || clusterError || !!nameError}
+        >

As per coding guidelines, components/frontend/src/**/*.{ts,tsx}: "Verify loading/error states and error handling in React Query hooks."

Also applies to: 120-159, 192-192

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/workspace-form.tsx` around lines 50 - 51,
The code derives platform mode from useClusterInfo.isOpenShift and allows
submitting while cluster detection is unresolved, risking wrong payload; update
the component to wait for clusterLoading to be false (and handle a cluster error
state) before deriving mode from isOpenShift, disable the submit action (and the
submit button) when clusterLoading is true or when useClusterInfo reports an
error, and ensure the submit handler consults this gated state (e.g., check
clusterLoading || clusterError and return/throw) so setFormData/formData cannot
produce the wrong payload shape until platform type is known.

@mergify mergify Bot added the queued label Apr 27, 2026
@mergify
Copy link
Copy Markdown
Contributor

mergify Bot commented Apr 27, 2026

Merge Queue Status

  • Entered queue2026-04-27 21:40 UTC · Rule: default
  • Checks skipped · PR is already up-to-date
  • Merged2026-04-27 21:41 UTC · at a075cfefa3559b827ed24643339c6787d2448b9a · squash

This pull request spent 18 seconds in the queue, including 4 seconds running CI.

Required conditions to merge

@mergify mergify Bot merged commit e6f1828 into main Apr 27, 2026
69 checks passed
@mergify mergify Bot deleted the bgregor/ftue-welcome-wizard branch April 27, 2026 21:41
@mergify mergify Bot removed the queued label Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant