_private/qwestly-hire-docs/features/hire-onboarding.md

Hire onboarding (first-job wizard)

This document describes how the hire onboarding feature works today in qwestly-hire: routes, gates, steps, APIs, UI shell, and debugging. For original initiative intent and historical decisions, see docs/plan/simplify-hm-onboarding.md.


Purpose

Guide a hiring manager from company assignment through first job draft, optional HM notes for matching context, then review and publish, then mark hire onboarding complete so the user can use the main app (/). The wizard lives under /onboarding (not /hire/onboarding).


Completion gate

Hire onboarding is complete when the MongoDB user has onboarding_completed_at set (non-null). That is the primary server-side signal.

  • JWT / site token exposes hire_onboarding_complete, derived from that date (and related auth refresh logic in src/services/auth0.ts).
  • has_completed_signup is not used for this gate anymore (removed from the hire completion path; legacy DB cleanup may still use the migration script scripts/migrate-has-completed-signup.ts where applicable).

Mid-wizard state may include User.onboarding_progress: current_step is one of company | roles | job | hm-notes | review (see HIRE_ONBOARDING_STEPS in src/lib/hire-onboarding/state.ts), optional completed_steps, and optional draft_job_id (Mongo job id used to build resume URLs with ?jobId= for hm-notes and review).


Wizard shell (layout & step indicator)

  • src/app/onboarding/layout.tsx wraps all steps in a Container (max-w-2xl, centered, min-w-0 overflow-x-hidden, vertical padding py-6 sm:py-8 lg:py-12, aligned with candidate onboarding step shell rhythm) and renders HireOnboardingProgress above page content (src/app/onboarding/_components/hire-onboarding-progress.tsx).
  • The progress UI shows three labeled pillars (Company โ†’ Job โ†’ Review). The pathname still includes /onboarding/roles and /onboarding/hm-notes; those URLs highlight the Job pillar (same visual โ€œtrackโ€ as the job stepโ€”connector fill uses the same rules as roles for hm-notes). Connectors use theme colors (bg-primary / bg-border). On narrow viewports, step circles and connector track are slightly smaller and pillar labels truncate with tighter padding; from sm upward they match the larger, non-truncating layout.
  • HireOnboardingStepShell (src/app/onboarding/_components/hire-onboarding-step-shell.tsx) is the shared page title + optional lead + inverted light Card wrapper used by onboarding step clients (and review-step-markup.tsx). Default CardContent padding is p-6 md:p-8 (aligned with candidate form cards). Props cardContentClassName, cardClassName, headerClassName, and descriptionClassName extend defaults per step (e.g. company lg:px-10 xl:px-14, review min-w-0 on header/description, overflow-x-hidden on dense steps).
  • Icon column (candidate-style grid): Company, roles, job, hm-notes, and complete lay out card body as grid grid-cols-[auto_1fr] gap-3 md:gap-4 items-start with a left decorative icon (aria-hidden, typically 32ร—32; job scales ParseRoleIcon to match). Icons live in src/components/icons/onboarding.tsx (TargetCompaniesIcon, MultipleUserIcon, BlueDocumentIcon, LaptopCheckIcon) except job, which uses ParseRoleIcon from src/components/icons/app-icons.tsx. Review uses HireOnboardingStepShell but no left icon columnโ€”the summary and actions stay full width inside a single space-y-6 column.
  • Step content does not nest another outer Containerโ€”width comes from the layout Container only.

Routes

Path Role
/onboarding Redirects to the canonical step for the user (see resolveOnboardingRedirectPath in src/lib/hire-onboarding/state.ts).
/onboarding/company Assign a company via CompanyAssignmentForm.
/onboarding/roles Choose desired roles when required by wizard state (User.desired_roles).
/onboarding/job Job title, optional JD URL / pasted text via JobPostingDetailsForm; creates draft via POST /api/jobs, then advances to hm-notes (not straight to review).
/onboarding/hm-notes?jobId=โ€ฆ Static route hm-notes/page.tsx (not [step]): optional free-text notes merged into hm_additional_info via PATCH /api/jobs/:id, then first matching llm_role_summary generation when eligible (blocks on fetchGeneratedRoleSummary before navigating to review).
/onboarding/review?jobId=โ€ฆ Review title + role summary. Optional roleSummaryFailed=1 if summary generation failed on the hm-notes stepโ€”review shows an error state and then strips the flag from the URL (see below).
/onboarding/complete?jobId=โ€ฆ Post-publish confirmation (complete-onboarding-client.tsx): Continue to dashboard after onboarding_completed_at is set.

Server layout: src/app/onboarding/ (page.tsx, [step]/page.tsx, hm-notes/page.tsx, complete/page.tsx, _components/*, layout.tsx).


Canonical redirect (resolveOnboardingRedirectPath)

After login, users without completed onboarding are sent to a single โ€œnextโ€ URL:

  • No user / resetHireProgress debug / no companies assigned โ†’ /onboarding/company.
  • onboarding_completed_at set (and not forced incomplete by debug) โ†’ /.
  • Wizard pinned to roles โ†’ /onboarding/roles.
  • desired_roles empty โ†’ /onboarding/roles.
  • Has companies, but onboarding_progress.current_step is missing or not a known step โ†’ /onboarding/job (resume default).
  • Otherwise โ†’ /onboarding/${current_step}, and when current_step is hm-notes or review and draft_job_id is set โ†’ append ?jobId=<draft_job_id> so refresh/deep links reopen the correct draft.

Redirects and access

  • Dashboard (/) (src/app/dashboard/page.tsx): If hire onboarding is not complete, redirects into the wizard using hasCompletedHireOnboarding, resolveOnboardingRedirectPath, and optional debug query params (see below). This matches the candidate app pattern (onboarding redirect at the main entry / dashboard, not in the request proxy).
  • Proxy (src/proxy.ts): Handles Auth0, session, token refresh, and authorized / hire_onboarding_complete in the site token; it does not redirect incomplete users into onboarding (avoids allowlist drift and duplicate routing vs resolveOnboardingRedirectPath).

Step behavior (as implemented)

1. Company (/onboarding/company)

  • UI: HireOnboardingStepShell + TargetCompaniesIcon column + CompanyAssignmentForm (src/components/company-assignment-form/) with showPageHeader={false} (company-step-client.tsx). Heading scale steps up from mobile (text-2xl โ†’ sm:text-3xl โ†’ md:text-4xl); card padding p-6 md:p-8 with optional horizontal lg:px-10 xl:px-14 on this step only.
  • Flows: Three flat views โ€” name (default) typeahead by company name against the hire DB, external lookup by domain or LinkedIn URL (browser โ†’ api-python GET /api/public/linkedin-company), and manual fallback (SimpleCompanyForm: required name with inline Add company; optional website / description in a collapsible, collapsed by default โ€” same idea as optional job details on /onboarding/job). Manual submit uses POST /api/companies with buildPublicCompanyPostBody (only public-route fields). Only one body renders at a time; the two non-active views are surfaced as ghost buttons under the active body so the user can pivot without losing an in-flight selection. The name view delegates the typeahead to ConnectedCompanySearchInput from @/packages/ui (companySearchProfile="candidate" so the package's segmented toggle does not render โ€” the host owns the external/manual UX). When merged suggestions are sparse, the list can include a โ€œtry another wayโ€ row that switches the user to the external view (see docs/features/company-search-hire-app.md, Name view).
  • After an external lookup hit: A preview card shows logo on the left; name, optional tagline, and a row with website hostname (from domain) and LinkedIn (with external-link affordance) on the right; a single centered primary action โ€œSelect this companyโ€ (not full-width). Vendor-only rows (no hire Company._id yet) are persisted via the public POST /api/companies (browser, same-origin) which OR-dedupes on linkedin_company_id || domain || linkedin_url. Hire-DB rows short-circuit the POST.
  • Persist: POST /api/onboarding/company with company_ids. The route also accepts optional desired_roles for other callers; the hire wizard usually sets desired roles on /onboarding/roles when that step is shown, not from the company form.
  • Client navigates to /onboarding/roles, preserving hire-debug query params; resetHireProgress is dropped after this navigation so later steps are not pinned back to company (appendHireDebugAfterCompanyStep in src/lib/hire-onboarding/state.ts). Server-side canonical routing may still send users who already have desired_roles straight to job / later steps when they hit /onboarding.

2. Roles (/onboarding/roles)

  • UI: HireOnboardingStepShell + MultipleUserIcon column (roles-step-client.tsx). Helper copy is a native <label htmlFor="roles"> (normal weight, gray) above RoleSelectionForm; user picks the closest function match (โ€œContinueโ€).
  • Persist: POST /api/onboarding/company with company_ids (from props) and desired_roles. On success, client navigates to /onboarding/job and router.refresh().

3. Job (/onboarding/job)

  • UI: HireOnboardingStepShell + ParseRoleIcon column + form (job-step-client.tsx). JobPostingDetailsForm (src/components/jobs/job-posting-details-form.tsx) โ€” job title (required), optional posting URL and pasted text in a collapsible โ€œoptional job detailsโ€ section (collapsed by default). Shared with other job flows for consistent behavior.
  • No URL: POST /api/jobs creates a draft with title (+ optional hm_additional_info from pasted text), then advances to /onboarding/hm-notes?jobId=โ€ฆ (not directly to review). No matching-summary generation on this step.
  • With URL: Analyze runs before creating the job row (POST /api/jobs/analyze-url). Errors show next to the URL field. On success, scrape result is cached client-side.
  • Title conflict: If scraped title differs from the user-entered title, a choice UI appears. While the choice is showing, the primary action is a disabled โ€œContinueโ€ button. After the user picks โ€œYour titleโ€ or โ€œFrom postingโ€, the primary action becomes an enabled โ€œContinueโ€ that only creates the job (no second full โ€œSubmitโ€ pass through analyze). If there is no conflict, analyze and draft creation still happen on that same first submit when appropriate.
  • After create: Progress is updated via POST /api/onboarding/progress toward hm-notes and draft_job_id is stored on the user for resume URLs (src/app/api/onboarding/[[...slug]]/route.ts).

4. HM notes (/onboarding/hm-notes?jobId=โ€ฆ)

  • Static page src/app/onboarding/hm-notes/page.tsx (avoids collision with [step]). Server validates jobId, ownership, and that onboarding_progress.current_step is hm-notes (otherwise redirects to canonical wizard path).
  • UI: HireOnboardingStepShell + BlueDocumentIcon column (hm-notes-step-client.tsx) โ€” optional free-text notes for hiring-manager context. Notes are merged into existing hm_additional_info via mergeHiringManagerNotes (src/lib/hire-onboarding/merge-hm-notes.ts). If the merged text differs from what is already on the job, the client **PATCH**es /api/jobs/:id so the draft reflects merged content before matching-summary generation.
  • Matching summary: After any PATCH (or if merge matches server state), if the draft is eligible (jobHasSubstanceForMatchingSummary in src/lib/hire-onboarding/matching-summary-eligibility.ts), the client calls fetchGeneratedRoleSummary. On success, summary text is stored client-side for review. On failure, navigation to review still happens with roleSummaryFailed=1 so the review step can show failure + retry UX.
  • Title-only / no substance: If after merge the draft is still ineligible for matching summary, the client calls POST /api/onboarding/publish (same as review Submit), then /onboarding/complete?jobId=โ€ฆ โ€” review is skipped because there is nothing meaningful to confirm beyond what was already captured.
  • Otherwise, progress is updated via POST /api/onboarding/progress when moving to review.

5. Review (/onboarding/review?jobId=โ€ฆ)

  • UI: HireOnboardingStepShell with dynamic title / description from view-model state (review-step-markup.tsx); no left icon column (dense summary + edit flows). Same shell supports edit summary from dashboard (flow === 'edit-summary') with optional back link.
  • Loaded only for a valid jobId owned by the user (src/app/onboarding/[step]/page.tsx). Invalid/missing jobId redirects to /onboarding/job.
  • roleSummaryFailed=1: If present, initial state shows a summary error; a useEffect removes the query param after load so refreshes donโ€™t keep the flag. This flag is set when generation fails on the hm-notes step (not the job step).
  • Role summary: May load from the job, client storage, or fetchGeneratedRoleSummary when substantive JD text exists and generation did not already fail; loading/error/retry paths are in review-step-client.tsx.
  • Edit: When a summary exists, user can Edit (ProseMirror) and save via PUT /api/jobs/{id}/matching-summary. In edit mode, job title can also be edited and saved with PATCH /api/jobs/{id}.
  • Submit: POST /api/onboarding/publish with { job_id } sets the job active, sets User.onboarding_completed_at, finalizes onboarding_progress, and the client redirects to /onboarding/complete?jobId=โ€ฆ (then Continue to dashboard).

6. Complete (/onboarding/complete?jobId=โ€ฆ)

  • UI: HireOnboardingStepShell + LaptopCheckIcon column (complete-onboarding-client.tsx): confirmation copy and Continue to dashboard after publish. Server page: complete/page.tsx.

Primary APIs

Endpoint Purpose
POST /api/onboarding/company Persist company (company_ids; optional desired_roles) and set progress toward job step.
POST /api/onboarding/progress Persist User.onboarding_progress between steps. Server validates current_step / completed_steps against allowed wizard steps, requires a valid Mongo draft_job_id owned by the user when current_step is hm-notes or review, and strips unknown fields before save.
POST /api/onboarding/publish Activate job + set onboarding_completed_at (review Submit, or hm-notes when the draft remains ineligible for matching summary).
POST /api/jobs Create draft job (with or without URL analysis payload).
POST /api/jobs/analyze-url URL scrape / structured analysis (job step, before create when URL present).
PUT /api/jobs/{id}/matching-summary Save edited matching summary on review.
PATCH /api/jobs/{id} Update job fields (e.g. merged HM notes on hm-notes, title on review).
Matching summary generation First run on hm-notes when eligible after optional PATCH; review may refetch/retry (see hm-notes-step-client / review-step-client / job APIs).

Onboarding POSTs above are implemented in src/app/api/onboarding/[[...slug]]/route.ts with ApiRouter (same URL paths as before).


Debug / QA (non-production-friendly)

Restricted to primary @qwestly.com accounts (no plus addressing), via canUseHireOnboardingDebug in src/lib/auth-utils.ts: the ?debug=1 Jotai toggle, floating panel, and hire-debug query keys have no effect for other users (server redirects ignore simulation flags; client navigations do not preserve them).

With the floating debug UI enabled (Jotai hireDebug / debug panel), eligible testers can use hire-debug query keys (debug, forceHireOnboarding, resetHireProgress) โ€” see HIRE_DEBUG_QUERY_KEYS in src/lib/hire-onboarding/state.ts.

  • forceHireOnboarding=1: Treat hire onboarding as incomplete for redirects even if onboarding_completed_at is set.
  • resetHireProgress=1: Pin canonical wizard entry to company until cleared (e.g. after company step navigation strips it).
  • src/components/hire-onboarding-debug-panel.tsx: โ€œTest onboarding from startโ€ navigates to /onboarding/company with those flags; โ€œLeave test modeโ€ strips simulation flags.

These flags are URL-only simulation for routing; they do not replace DB migrations for legacy users.


Key source files

Area Location
Gate + redirect helpers src/lib/hire-onboarding/state.ts
Matching-summary eligibility src/lib/hire-onboarding/matching-summary-eligibility.ts
Wizard pages src/app/onboarding/page.tsx, src/app/onboarding/[step]/page.tsx, src/app/onboarding/hm-notes/page.tsx, src/app/onboarding/complete/page.tsx
Layout + step indicator src/app/onboarding/layout.tsx, _components/hire-onboarding-progress.tsx
Shared step shell (title + card) _components/hire-onboarding-step-shell.tsx
Step UIs _components/company-step-client.tsx, roles-step-client.tsx, job-step-client.tsx, hm-notes-step-client.tsx, review-step-markup.tsx (via review-step-client.tsx), complete/complete-onboarding-client.tsx
Onboarding step icons src/components/icons/onboarding.tsx (and ParseRoleIcon in src/components/icons/app-icons.tsx for job)
HM notes merge helper src/lib/hire-onboarding/merge-hm-notes.ts
Job posting form (shared) src/components/jobs/job-posting-details-form.tsx
Company search / preview / select src/components/company-assignment-form/
User types src/types/user.d.ts (onboarding_completed_at, onboarding_progress)
Dashboard gate src/app/dashboard/page.tsx
Onboarding POST APIs (company / progress / publish) src/app/api/onboarding/[[...slug]]/route.ts
Proxy (Auth0 + session + token; no onboarding redirect) src/proxy.ts


Changelog (maintenance)

Date Notes
2026-04 Documented wizard shell (layout, HireOnboardingProgress), company preview card UX, canonical redirect when company exists but progress is missing, hire wizard payload vs optional desired_roles, job optional collapsible + roleSummaryFailed, review query cleanup.
2026-05 hm-notes step, static hm-notes/page.tsx, draft_job_id + ?jobId= resume URLs, JobPostingDetailsForm on job step, matching-summary generation moved to hm-notes (failure โ†’ roleSummaryFailed on review); when still ineligible after merge, publish from hm-notes โ†’ /onboarding/complete (skip review). Progress/API table and file index updates.
2026-05-11 Hire wizard responsive shell: layout Container max-w-2xl, overflow-x-hidden, tighter default padding; HireOnboardingProgress scales down on small screens (circles, labels, connectors). Company step drops nested Container; typography and card padding tuned for mobile.
2026-05-12 HireOnboardingStepShell extracts shared title + inverted card; step icon columns (company / roles / job / hm-notes / complete) + review without icon; layout py-6 sm:py-8 lg:py-12; packages/ui bump for explicitParentClearOnly on company search (see company-search hire doc).

Update this file when you change routes, gates, step UX, or onboarding APIs so it stays the as-built reference for the wizard.