Skip to content

feat(jobs): polish job creation wizard#207

Merged
JoachimLK merged 13 commits into
mainfrom
ui/job-wizard-polish
Jun 19, 2026
Merged

feat(jobs): polish job creation wizard#207
JoachimLK merged 13 commits into
mainfrom
ui/job-wizard-polish

Conversation

@JoachimLK

@JoachimLK JoachimLK commented Jun 17, 2026

Copy link
Copy Markdown
Contributor
  • Create a new script test-step3.js to automate the testing of the sign-in page.
  • Launch Chromium browser with specific arguments and set viewport size.
  • Navigate to the sign-in page and take a screenshot.
  • Log input types and placeholders for all input fields on the page.
  • Handle errors gracefully by logging them to the console.

Summary

  • What does this PR change?
  • Why is this needed?

PR title must follow Conventional Commits — e.g. feat(jobs): add bulk import or fix: handle null salary. The squash-merged title is what release-please uses to generate the changelog and pick the next version. PRs with non-conventional titles are blocked by CI.

Type of change

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Chore

Validation

  • I tested locally
  • I added/updated relevant documentation
  • I verified multi-tenant scoping and auth behavior for affected API paths

DCO

  • All commits in this PR are signed off (Signed-off-by) via git commit -s

Summary by CodeRabbit

Release Notes

  • New Features

    • Job wizard now recommends scoring templates from the job title, with one-click “Use recommended scoring” or a “Skip scoring” path.
    • Added phone requirement controls to the application form builder, plus a persistent live candidate preview and preview-mode replica.
    • Added a draft applicant preview page and a standardized public application header.
  • UX Improvements

    • “Share & track” now organizes tracked links by category and simplifies the job-creation layout.
  • Testing / DevOps

    • Improved E2E reliability (analytics consent, deterministic redirects) and CI robustness; added typecheck and schema unit tests.

- Create a new script `test-step3.js` to automate the testing of the sign-in page.
- Launch Chromium browser with specific arguments and set viewport size.
- Navigate to the sign-in page and take a screenshot.
- Log input types and placeholders for all input fields on the page.
- Handle errors gracefully by logging them to the console.
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-207 June 17, 2026 19:07 Destroyed
@railway-app

railway-app Bot commented Jun 17, 2026

Copy link
Copy Markdown

🚅 Deployed to the reqcore-pr-207 environment in applirank

Service Status Web Updated (UTC)
applirank ✅ Success (View Logs) Jun 19, 2026 at 10:48 am

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces phone requirement support throughout the stack (database, API, composables), builds a reusable application form component suite (form body, live preview, recruiter editor), integrates them into recruiter dashboard and candidate apply pages, adds a draft preview page, refactors Create Job wizard Steps 1/3/4 and dashboard layout, and hardens E2E tests with analytics consent handling and auth robustness improvements.

Changes

Application Form Builder with Phone Requirement

Layer / File(s) Summary
Phone requirement foundation
server/database/migrations/0029_wakeful_secret_warriors.sql, server/database/migrations/meta/_journal.json, server/database/schema/app.ts, server/api/jobs/[id].get.ts, server/api/jobs/[id].patch.ts, server/api/public/jobs/[slug].get.ts, server/api/public/jobs/[slug]/apply.post.ts, app/composables/useJob.ts, app/composables/useJobs.ts, server/utils/schemas/job.ts
Adds phoneRequirement column ('hidden'|'optional'|'required') to job table with default 'optional', includes it in database migrations and schema, extends API endpoints to select/return the field, applies phone validation/hiding in apply handler based on requirement, and updates composable payloads to accept optional phoneRequirement.
Question schema and form validation
server/utils/schemas/jobQuestion.ts, app/components/QuestionForm.vue
Tightens createQuestionSchema with input trimming, 50-option cap, and .superRefine rules for select-type presence and case-insensitive uniqueness; updates updateQuestionSchema with trimming and 50-option constraint; adds client-side QuestionForm validation enforcing 500-char labels, 1000-char help text, 200-char options, option uniqueness, and UI maxlength/disable controls.
Job creation schema and scoring validation
server/utils/schemas/job.ts, server/utils/schemas/scoring.ts, tests/unit/job-creation-schema.test.ts
Extends createJobSchema with phoneRequirement enum and tightens title/description/location validation; introduces createJobWizardSchema with status, questions, criteria arrays and .superRefine for unique criterion keys and conditional criteria requirement; updates updateJobSchema; adds createCriterionSchema input trimming; creates unit tests validating schema constraints (whitespace rejection, trimming, duplicate option detection, criteria limits).
Form body presentational component
app/components/ApplicationFormBody.vue
New reusable form component supporting live (interactive) and preview (non-interactive click-for-edit) modes. Renders personal information (name, email, phone conditional on phoneRequirement), resume/cover-letter uploads with conditional visibility, custom questions via DynamicField, server error banner, honeypot input, field-group click-to-edit navigation in preview via edit-field event, and inline validation error display.
Job header and preview components
app/components/PublicJobApplicationHeader.vue, app/components/ApplicationBuilderPreview.vue
Adds PublicJobApplicationHeader rendering job title, optional markdown description, conditional organization/location badges, and mapped job-type labels; adds ApplicationBuilderPreview with desktop/mobile mode toggle, job metadata chip display, and embedded ApplicationFormBody in preview mode for live form demonstration.
Recruiter application form builder
app/components/ApplicationBuilder.vue
Full recruiter form editor with contracts defining ApplicationForm (including phoneRequirement) and BuilderOperations (phone/resume/cover-letter requirement setters and question CRUD/reorder); implements optimistic requirement toggles with rollback on failure, question add/edit/delete with 50-question max, move operations, preview-to-editor navigation with scrolling focus, inline error display, and split layout with left controls and right live preview panel.
Draft applicant preview page
app/pages/dashboard/jobs/preview.vue
Route-authenticated page rendering draft applicant preview for form testing. Loads draft JSON from localStorage (reqcore-job-draft key), shows loading skeleton during load, displays error state when no draft found; renders PublicJobApplicationHeader + ApplicationFormBody in preview mode with v-model bindings; blocks submission with fixed error message; supports refresh/close actions.
Recruiter dashboard form management
app/pages/dashboard/jobs/[id]/application-form.vue
Replaces legacy requirement refs and JobQuestions section with ApplicationBuilder component. Introduces builderModel synchronized from fetched job/questions via watchers; wires builderOperations to persist question CRUD/reorder via useJobQuestions and requirement toggles via updateJob. Updates container width to max-w-6xl.
Candidate apply page integration
app/pages/jobs/[slug]/apply.vue
Replaces inline form markup with ApplicationFormBody and PublicJobApplicationHeader. Adds phone validation when phoneRequirement==='required'; conditionally includes/excludes phone in FormData and JSON submission payloads based on requirement. Updates icon imports.
Job creation API with transactional persistence
server/api/jobs/index.post.ts
Updates POST handler to validate against createJobWizardSchema and wrap job/question/criterion creation in database transaction. Inserts job with status and phoneRequirement fields; conditionally inserts related jobQuestion and scoringCriterion rows with displayOrder indices. Returns created job from transaction.
Job creation Step 1 form layout
app/pages/dashboard/jobs/new.vue
Rewrites Step 1 "Job details" form into more compact sectioned layout with updated headings/instructions and reorganized fields. Updates top action button styling/wording for draft saving.
Step 3 recommended-first scoring mode
app/pages/dashboard/jobs/new.vue
Introduces showAdvanced ref for mode switching, replaces async template loading with synchronous local scoringTemplates dataset, adds recommendedTemplate/recommendedCriteria computed from job-title regex, implements useRecommendedScoring/skipScoring helpers, removes Step 3 AI-warning watcher, and adds navigation icons.
Step 3 scoring UI rewrite
app/pages/dashboard/jobs/new.vue
Rewrites Step 3 template: renders recommended-scoring card with preview and "Use recommended" / "Skip scoring" buttons, gates advanced option cards and AI-provider warning behind showAdvanced, reworks criteria editing (weight sliders, removal, custom form, auto-score toggle).
Step 4 distribution UI and header
app/pages/dashboard/jobs/new.vue
Adds distributionGroups constant for grouped channel rendering, updates header from "Distribution hub" to "Share & track", replaces hardcoded job-boards/outreach/social-media sections with v-for over groups; preserves custom job board UI after grouped sections.
Wizard layout and page structure
app/pages/dashboard/jobs/new.vue
Refactors page container width/padding and stepper spacing; removes right-side Tips sidebar; leaves simplified main-form layout.
Dashboard layout full-bleed support
app/layouts/dashboard.vue
Refactors layout class binding to array syntax, toggling between fullbleed (flex-column/overflow-hidden) and standard scrollable modes. Wraps slot content with fullbleed styling (min-h-0, overflow-hidden) or contents (non-fullbleed).
Config and i18n updates
package.json, i18n/i18n.config.ts
Adds typecheck npm script invoking nuxi typecheck with increased Node.js heap size; updates i18n config string literal formatting and call termination.

E2E Testing Infrastructure – Analytics Consent and Auth Hardening

Layer / File(s) Summary
Analytics consent fixture helper and auth hardening
e2e/fixtures.ts
Introduces declineAnalyticsConsent helper that sets reqcore-consent=denied cookie. Integrates into authenticated page fixture. Reworks post-sign-in and post-org-creation navigation: explicit API-response waits, exact "Sign in" button selection, direct /onboarding/create-org navigation, re-auth checkpoints for hard navigations, and networkidle settling.
Critical-flow E2E specs with consent handling
e2e/critical-flows/candidate-application.spec.ts, e2e/critical-flows/resume-upload.spec.ts, e2e/critical-flows/source-tracking.spec.ts
Updates test files to call declineAnalyticsConsent(context) immediately after creating new browser contexts before navigating to candidate pages. Adjusts resume-upload authentication with exact button matching and explicit URL waits.
Job creation E2E test expansion
e2e/critical-flows/job-creation.spec.ts
Adds invalid draft rejection test; expands main test to configure application requirements (resume off, cover letter required), add required single-select question, verify persistent preview updates, edit question label and assert preview sync, open draft preview popup, switch device to mobile, and after publishing navigate to /jobs/${jobSlug}/apply to verify form matches builder configuration.
CI workflow robustness and debug script
.github/workflows/e2e-tests.yml, debug-signin-page.js
Updates workflow: uses npm ci --no-audit with fetch-retry env vars and step timeout; adds timeout-minutes: 2 to MinIO step; removes duplicated setup block. Adds debug-signin-page.js Playwright script that launches Chromium, navigates to sign-in route, captures screenshot, logs input metadata, ensures browser shutdown.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~80 minutes

Possibly related PRs

  • reqcore-inc/reqcore#62: Both PRs touch the Playwright E2E candidate/job-creation tests and shared e2e/fixtures.ts—the main PR builds on those flows (e.g., adding declineAnalyticsConsent handling) while #62 introduces the underlying E2E suite/fixtures/workflow.
  • reqcore-inc/reqcore#113: Both PRs modify the job-creation "scoring criteria" flow (e.g., app/pages/dashboard/jobs/new.vue) and the underlying scoring/criteria management integration (persisting/updating job scoring criteria), so the changes are directly related at the code-feature level.
  • reqcore-inc/reqcore#124: The main PR's "Publish & distribute" flow now creates/copies tracked channel links, which directly depends on the retrieved PR's new Zod schemas for tracking-link creation/query/update validation.

Poem

🐰 A form builder blooms with questions and care,
Recruiter edits left, candidates see fair!
Phone requirements dance (hidden, optional, required),
Steps One and Three refactored, new layouts inspired.
Tests decline consent, auth flows stand tall,
The CI pipeline hops, delivering it all! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is severely incomplete, containing unfinished template placeholders and misaligned content unrelated to the actual code changes. Provide a complete PR description that accurately summarizes the actual changes (job wizard enhancements, schema validation, phone requirement support, etc.) instead of placeholder text about a test script. Ensure all required sections are filled out and validation checkboxes reflect the actual testing performed.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(jobs): polish job creation wizard' is directly supported by substantial changes to the job creation wizard UI, schema validation, and application form components throughout the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ui/job-wizard-polish

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
app/pages/dashboard/jobs/new.vue (1)

115-115: 💤 Low value

aiScoringChosen is declared but never assigned a value.

This ref is persisted (line 310), restored (line 328), reset (line 361), and watched (line 380), but is never set to true anywhere in the component. This is dead state that adds unnecessary storage and watcher overhead.

Either remove it entirely, or wire it up where scoring is actually chosen (e.g., in useRecommendedScoring or when criteria are loaded).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/dashboard/jobs/new.vue` at line 115, The `aiScoringChosen` ref is
declared but never assigned to `true` anywhere in the component, creating dead
state with unnecessary storage and watcher overhead. Either completely remove
the `aiScoringChosen` ref declaration along with all its usages in the persist
call, restore logic, reset call, and watch callback, or wire it up by setting it
to `true` in the appropriate location where scoring is actually chosen, such as
within the `useRecommendedScoring` function or when criteria are loaded into the
component.
test-step3.js (3)

12-12: 💤 Low value

Consider making the base URL configurable.

The hardcoded http://localhost:3000 assumes the dev server is running on port 3000. Using an environment variable would make the script more flexible for different port configurations.

📝 Optional improvement
-  await page.goto('http://localhost:3000/auth/sign-in');
+  const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
+  await page.goto(`${baseUrl}/auth/sign-in`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-step3.js` at line 12, The page.goto() method call contains a hardcoded
base URL of http://localhost:3000 which limits flexibility for different port
configurations. Extract the base URL into an environment variable by replacing
the hardcoded string with a reference to process.env (e.g., process.env.BASE_URL
or similar) and provide a sensible default fallback value like
http://localhost:3000 if the environment variable is not set. This will allow
the script to work with different server configurations without code changes.

26-26: 💤 Low value

Consider more robust error logging.

Logging only e.message will display undefined if the thrown error doesn't have a message property. While uncommon, logging the full error provides better debugging information.

🐛 Suggested improvement
-})().catch(e => { console.error(e.message); process.exit(1); });
+})().catch(e => { console.error(e instanceof Error ? e.message : String(e)); process.exit(1); });

Or log the full error for debugging:

-})().catch(e => { console.error(e.message); process.exit(1); });
+})().catch(e => { console.error('Script failed:', e); process.exit(1); });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-step3.js` at line 26, In the .catch() error handler, replace the
console.error call that logs only e.message with logging the full error object e
instead. The current approach of logging e.message can result in undefined being
logged if the thrown error doesn't have a message property, making debugging
difficult. By logging the complete error object, you ensure all relevant error
details are captured regardless of the error's structure.

14-14: ⚡ Quick win

Misleading script and screenshot naming.

The filename test-step3.js and screenshot path step3-01-signin.png suggest this script tests Step 3 of the job wizard, but it actually debugs the sign-in page. According to the stack context, Step 3 is the candidate scoring flow, not authentication.

📝 Suggested naming

Rename the file and update the screenshot path to reflect the actual purpose:

Option 1 (if this is a general debug script):

  • Rename file: test-step3.jsdebug-signin-page.js
  • Update screenshot path: step3-01-signin.pngdebug-signin-page.png

Option 2 (if this is part of a larger e2e flow):

  • Rename file: test-step3.jsdebug-e2e-flow.js
  • Update screenshot path: step3-01-signin.pnge2e-01-signin.png
-  await page.screenshot({ path: '/tmp/step3-01-signin.png' });
+  await page.screenshot({ path: '/tmp/debug-signin-page.png' });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-step3.js` at line 14, The file test-step3.js and the screenshot path
step3-01-signin.png suggest the script tests Step 3 of a wizard (candidate
scoring), but it actually debugs the sign-in page authentication flow. Rename
the file from test-step3.js to a more accurate name that reflects it debugs
sign-in functionality (such as debug-signin-page.js or debug-e2e-flow.js
depending on its actual purpose), and update the screenshot path from
/tmp/step3-01-signin.png to /tmp/debug-signin-page.png or /tmp/e2e-01-signin.png
to align with the new file name and the script's actual functionality.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/pages/dashboard/jobs/new.vue`:
- Around line 1378-1380: The click handler in the template is setting
scoringMode to 'ai' synchronously before generateAiCriteria() completes, which
causes inconsistent UI state if the function returns early or fails. Since
generateAiCriteria() already sets scoringMode = 'ai' on line 224 after a
successful fetch, remove the redundant scoringMode = 'ai' assignment from the
`@click` handler and keep only the generateAiCriteria() call. This ensures the
state only changes when the operation actually succeeds.

In `@test-step3.js`:
- Around line 4-7: The chromium.launch() call hardcodes the executablePath to a
Linux-specific path which breaks cross-platform compatibility on macOS and
Windows. Remove the hardcoded executablePath property entirely to allow
Playwright to automatically detect the installed Chrome/Chromium on any
platform, or if a specific path is required for containerized environments,
replace the hardcoded string with a reference to an environment variable that
can be configured per deployment environment.
- Around line 4-25: The browser resource acquired from chromium.launch() is not
guaranteed to be closed if an error occurs before reaching browser.close() on
line 25, causing a zombie Chrome process to persist. Refactor this code to use a
try-finally block where the browser initialization and all page interactions are
in the try block, and browser.close() is placed in the finally block to ensure
the browser is always closed regardless of whether an error occurs during
execution.
- Around line 5-6: The args array in the Playwright launch configuration
contains '--no-sandbox' and '--disable-setuid-sandbox' flags that are not
necessary for the automated test suite. Since test-step3.js is a manual debug
script, either remove these flags entirely, or make them conditional by checking
for an environment variable (such as DEBUG_SANDBOX_DISABLED) and only adding
them to the args array when that variable is set. If you choose to keep them
conditional, ensure you add a comment explaining why they might be needed in a
specific local environment.

---

Nitpick comments:
In `@app/pages/dashboard/jobs/new.vue`:
- Line 115: The `aiScoringChosen` ref is declared but never assigned to `true`
anywhere in the component, creating dead state with unnecessary storage and
watcher overhead. Either completely remove the `aiScoringChosen` ref declaration
along with all its usages in the persist call, restore logic, reset call, and
watch callback, or wire it up by setting it to `true` in the appropriate
location where scoring is actually chosen, such as within the
`useRecommendedScoring` function or when criteria are loaded into the component.

In `@test-step3.js`:
- Line 12: The page.goto() method call contains a hardcoded base URL of
http://localhost:3000 which limits flexibility for different port
configurations. Extract the base URL into an environment variable by replacing
the hardcoded string with a reference to process.env (e.g., process.env.BASE_URL
or similar) and provide a sensible default fallback value like
http://localhost:3000 if the environment variable is not set. This will allow
the script to work with different server configurations without code changes.
- Line 26: In the .catch() error handler, replace the console.error call that
logs only e.message with logging the full error object e instead. The current
approach of logging e.message can result in undefined being logged if the thrown
error doesn't have a message property, making debugging difficult. By logging
the complete error object, you ensure all relevant error details are captured
regardless of the error's structure.
- Line 14: The file test-step3.js and the screenshot path step3-01-signin.png
suggest the script tests Step 3 of a wizard (candidate scoring), but it actually
debugs the sign-in page authentication flow. Rename the file from test-step3.js
to a more accurate name that reflects it debugs sign-in functionality (such as
debug-signin-page.js or debug-e2e-flow.js depending on its actual purpose), and
update the screenshot path from /tmp/step3-01-signin.png to
/tmp/debug-signin-page.png or /tmp/e2e-01-signin.png to align with the new file
name and the script's actual functionality.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b16e8b7f-7c2d-408e-958c-f8b862e8cd5f

📥 Commits

Reviewing files that changed from the base of the PR and between f47296e and d0f424e.

📒 Files selected for processing (2)
  • app/pages/dashboard/jobs/new.vue
  • test-step3.js

Comment thread app/pages/dashboard/jobs/new.vue
Comment thread test-step3.js Outdated
Comment thread test-step3.js Outdated
Comment thread test-step3.js Outdated
@JoachimLK JoachimLK changed the title Add Playwright script for sign-in page testing feat(jobs): polish job creation wizard Jun 17, 2026
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-207 June 17, 2026 19:25 Destroyed
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-207 June 18, 2026 06:45 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/e2e-tests.yml (1)

69-77: ⚠️ Potential issue | 🟠 Major

Pin MinIO image to a specific version instead of latest.

Line 77 uses a floating latest tag, which breaks reproducibility and can unexpectedly change behavior or security posture between CI runs. Pin to a specific version or digest.

Suggested fix
-            minio/minio:latest server /data
+            minio/minio:REPLACE_WITH_PINNED_VERSION server /data
+# or (preferred):
+#           minio/minio@sha256:REPLACE_WITH_DIGEST server /data
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/e2e-tests.yml around lines 69 - 77, The MinIO Docker image
reference in the "Start MinIO" step uses the floating `latest` tag, which
compromises reproducibility and consistency across CI runs. Replace the
`minio/minio:latest` image reference with a specific version tag (such as
`minio/minio:RELEASE.2024-01-15T01-26-29Z` or another stable release version) to
ensure predictable and reproducible builds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/e2e-tests.yml:
- Around line 52-57: Replace the mutable version tags in the GitHub Actions with
pinned commit SHAs for both the Checkout action (line 52-53) and the Setup
Node.js action (line 56). For the Checkout action specifically, add
`persist-credentials: false` in the with section to prevent the GITHUB_TOKEN
from being stored in git config, which reduces the risk of credential exposure
through artifact uploads in subsequent workflow steps.

In `@debug-signin-page.js`:
- Line 24: The screenshot path in the page.screenshot() call is hardcoded to
/tmp/debug-signin-page.png, which is not portable to Windows and other systems.
Replace this hardcoded path with a platform-independent solution using the os
module's tmpdir() function combined with the path module's join() function to
construct the path. This will ensure the screenshot is written to the correct
temporary directory regardless of the operating system.

---

Outside diff comments:
In @.github/workflows/e2e-tests.yml:
- Around line 69-77: The MinIO Docker image reference in the "Start MinIO" step
uses the floating `latest` tag, which compromises reproducibility and
consistency across CI runs. Replace the `minio/minio:latest` image reference
with a specific version tag (such as `minio/minio:RELEASE.2024-01-15T01-26-29Z`
or another stable release version) to ensure predictable and reproducible
builds.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6ddd9aa1-0d71-4fd9-9a6e-c8d490b6ac24

📥 Commits

Reviewing files that changed from the base of the PR and between d0f424e and 9b20abd.

📒 Files selected for processing (3)
  • .github/workflows/e2e-tests.yml
  • app/pages/dashboard/jobs/new.vue
  • debug-signin-page.js

Comment on lines +52 to +57
- name: Checkout
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/e2e-tests.yml | head -80

Repository: reqcore-inc/reqcore

Length of output: 2807


🌐 Web query:

GitHub Actions security best practices pin actions by SHA persist-credentials false

💡 Result:

To harden your GitHub Actions workflows, implementing both SHA pinning and the persist-credentials: false setting are standard security best practices [1][2][3]. Pinning Actions by SHA Pinning an action to a full-length commit SHA is considered the most secure way to reference actions [1][2]. Unlike tags or branch names (e.g., v4, main), which are mutable and can be force-updated to point to malicious code by an attacker who compromises the action's repository, a commit SHA is cryptographically immutable [1][4][5]. - Implementation: Reference the action using its 40-character commit hash: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 [4][6]. - Governance: You can enforce this practice across your organization or repository using GitHub Actions policy settings, which allow administrators to mandate SHA pinning [1][7]. Using persist-credentials: false By default, the actions/checkout action stores the GITHUB_TOKEN in the local .git/config file to enable subsequent git commands [8][6]. This makes the token accessible to any step in your job, increasing the risk of credential theft if a dependency or script in your workflow is compromised [3][9][10]. - Implementation: Explicitly set persist-credentials: false in your checkout step if your workflow does not require git push or authenticated git operations [3][10][6]: yaml - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 with: persist-credentials: false - Use Case: If a job needs to perform git operations, evaluate whether you can isolate that task into a separate, restricted job or use a more granular authentication method [10][5]. Always follow the principle of least privilege by setting appropriate permissions at the workflow or job level [11][12][5].

Citations:


🏁 Script executed:

grep -n "upload\|artifact" .github/workflows/e2e-tests.yml

Repository: reqcore-inc/reqcore

Length of output: 265


Pin GitHub Actions by commit SHA and disable credential persistence in checkout.

Lines 53 and 56 use mutable tags (@v6), and line 52 does not set persist-credentials: false. This weakens supply-chain integrity; the GITHUB_TOKEN persisted in git config is accessible to any workflow step and risks exposure through artifact uploads (lines 111, 119).

Suggested hardening diff
       - name: Checkout
-        uses: actions/checkout@v6
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
+        with:
+          persist-credentials: false

       - name: Setup Node.js
-        uses: actions/setup-node@v6
+        uses: actions/setup-node@8f152de45cc393bb48ce5d6f9a02fee1e4b622f4
         with:
           node-version: 22.22
           cache: npm
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@8f152de45cc393bb48ce5d6f9a02fee1e4b622f4
with:
node-version: 22.22
cache: npm
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 52-53: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 53-53: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 56-56: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/e2e-tests.yml around lines 52 - 57, Replace the mutable
version tags in the GitHub Actions with pinned commit SHAs for both the Checkout
action (line 52-53) and the Setup Node.js action (line 56). For the Checkout
action specifically, add `persist-credentials: false` in the with section to
prevent the GITHUB_TOKEN from being stored in git config, which reduces the risk
of credential exposure through artifact uploads in subsequent workflow steps.

Source: Linters/SAST tools

Comment thread debug-signin-page.js Outdated
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-207 June 18, 2026 08:49 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/components/ApplicationBuilder.vue`:
- Around line 177-216: The functions moveQuestion, setRequireResume, and
setRequireCoverLetter dispatch operations without serialization, allowing rapid
clicks to send overlapping requests that can result in stale persisted state.
Implement a serialization mechanism (such as a promise queue or in-flight flag)
to ensure that only one operation executes at a time. For moveQuestion, await
the runOp call before returning. For setRequireResume and setRequireCoverLetter,
ensure the promise chain is properly queued so that subsequent calls wait for
the previous operation to complete before executing their own runOp call.
- Around line 222-237: The handleEditField function currently has no behavior
for the personal field types (name, email, phone), which causes a dead action
when users click on these fields in preview mode even though they appear
clickable. Add a new condition before the final comment to handle when the field
is one of these three personal fields, and scroll to the personal information
section using an anchor ref (similar to how documentsAnchor is used for resume
and coverLetter, and questionsAnchor is used for questions). Create or reference
a personalInfoAnchor to enable smooth scrolling to the personal fields editing
area.

In `@app/components/ApplicationBuilderPreview.vue`:
- Around line 142-144: The v-for loop in ApplicationBuilderPreview.vue that
iterates over metadata is using item.label as the key, which is not guaranteed
to be unique since different metadata items can have the same label (like
"Remote" for both location and remote status). Replace the :key binding from
item.label with a unique identifier that combines the index and label, or use a
truly unique property from the item object if available, to ensure stable list
patching and prevent rendering issues.

In `@app/pages/dashboard/jobs/`[id]/application-form.vue:
- Around line 58-71: The `BuilderQuestion` type definition does not match the
`ApplicationBuilder` component's contract. Update the type definition to change
the `type` field from `string` to `QuestionType` to match the expected enum
type. Additionally, make the optional fields `description` and `options`
required fields that accept null values by removing the optional `?` operator
and keeping the `| null` part, so they become `description: string | null` and
`options: string[] | null` respectively. This ensures the `builderModel` shape
aligns with what the `ApplicationBuilder` component expects when the model is
bound to it.

In `@e2e/critical-flows/job-creation.spec.ts`:
- Around line 23-24: The test function verifies the public candidate application
form using the authenticatedPage context which represents a recruiter session.
This can mask candidate-specific rendering differences and produce false
positives. Replace the authenticatedPage usage in the assertions around lines
126-132 that verify the public /apply form with an unauthenticated page context.
Create a new unauthenticated context (typically available in the test framework
for accessing pages without authentication) and use it specifically for those
public form assertions to accurately test how a candidate would experience the
application form.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b565d911-777e-41a6-ae7b-bc15c6be05f3

📥 Commits

Reviewing files that changed from the base of the PR and between 9b20abd and 5118b6e.

📒 Files selected for processing (13)
  • app/components/ApplicationBuilder.vue
  • app/components/ApplicationBuilderPreview.vue
  • app/components/ApplicationFormBody.vue
  • app/components/JobQuestions.vue
  • app/layouts/dashboard.vue
  • app/pages/dashboard/jobs/[id]/application-form.vue
  • app/pages/dashboard/jobs/new.vue
  • app/pages/jobs/[slug]/apply.vue
  • e2e/critical-flows/candidate-application.spec.ts
  • e2e/critical-flows/job-creation.spec.ts
  • e2e/critical-flows/resume-upload.spec.ts
  • e2e/critical-flows/source-tracking.spec.ts
  • e2e/fixtures.ts
💤 Files with no reviewable changes (1)
  • app/components/JobQuestions.vue

Comment on lines +177 to +216
function moveQuestion(index: number, direction: 'up' | 'down') {
const list = model.value.questions
const target = direction === 'up' ? index - 1 : index + 1
if (target < 0 || target >= list.length) return
if (props.operations) {
// Compute the post-swap order and persist; the parent syncs the model back.
const reordered = [...list]
;[reordered[index], reordered[target]] = [reordered[target]!, reordered[index]!]
const order = reordered.map((q, i) => ({ id: q.id, displayOrder: i }))
runOp(() => props.operations!.reorderQuestions(order))
return
}
;[list[index], list[target]] = [list[target]!, list[index]!]
}

function setRequireResume(value: boolean) {
if (model.value.requireResume === value) return
if (props.operations) {
const prev = model.value.requireResume
model.value.requireResume = value
runOp(() => props.operations!.setRequireResume(value)).then((ok) => {
if (!ok) model.value.requireResume = prev
})
return
}
model.value.requireResume = value
}

function setRequireCoverLetter(value: boolean) {
if (model.value.requireCoverLetter === value) return
if (props.operations) {
const prev = model.value.requireCoverLetter
model.value.requireCoverLetter = value
runOp(() => props.operations!.setRequireCoverLetter(value)).then((ok) => {
if (!ok) model.value.requireCoverLetter = prev
})
return
}
model.value.requireCoverLetter = value
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Serialize mutation ops to prevent out-of-order persisted state.

Line 177 onward dispatches reorder/toggle writes without an in-flight guard, and moveQuestion does not await runOp. Rapid clicks can send overlapping requests and leave persisted order/requirements in a stale state.

Suggested fix
-function moveQuestion(index: number, direction: 'up' | 'down') {
+async function moveQuestion(index: number, direction: 'up' | 'down') {
+  if (busy.value) return
   const list = model.value.questions
   const target = direction === 'up' ? index - 1 : index + 1
   if (target < 0 || target >= list.length) return
   if (props.operations) {
     // Compute the post-swap order and persist; the parent syncs the model back.
     const reordered = [...list]
     ;[reordered[index], reordered[target]] = [reordered[target]!, reordered[index]!]
     const order = reordered.map((q, i) => ({ id: q.id, displayOrder: i }))
-    runOp(() => props.operations!.reorderQuestions(order))
+    await runOp(() => props.operations!.reorderQuestions(order))
     return
   }
   ;[list[index], list[target]] = [list[target]!, list[index]!]
 }
 
-function setRequireResume(value: boolean) {
+async function setRequireResume(value: boolean) {
+  if (busy.value) return
   if (model.value.requireResume === value) return
   if (props.operations) {
     const prev = model.value.requireResume
     model.value.requireResume = value
-    runOp(() => props.operations!.setRequireResume(value)).then((ok) => {
-      if (!ok) model.value.requireResume = prev
-    })
+    const ok = await runOp(() => props.operations!.setRequireResume(value))
+    if (!ok) model.value.requireResume = prev
     return
   }
   model.value.requireResume = value
 }
 
-function setRequireCoverLetter(value: boolean) {
+async function setRequireCoverLetter(value: boolean) {
+  if (busy.value) return
   if (model.value.requireCoverLetter === value) return
   if (props.operations) {
     const prev = model.value.requireCoverLetter
     model.value.requireCoverLetter = value
-    runOp(() => props.operations!.setRequireCoverLetter(value)).then((ok) => {
-      if (!ok) model.value.requireCoverLetter = prev
-    })
+    const ok = await runOp(() => props.operations!.setRequireCoverLetter(value))
+    if (!ok) model.value.requireCoverLetter = prev
     return
   }
   model.value.requireCoverLetter = value
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/ApplicationBuilder.vue` around lines 177 - 216, The functions
moveQuestion, setRequireResume, and setRequireCoverLetter dispatch operations
without serialization, allowing rapid clicks to send overlapping requests that
can result in stale persisted state. Implement a serialization mechanism (such
as a promise queue or in-flight flag) to ensure that only one operation executes
at a time. For moveQuestion, await the runOp call before returning. For
setRequireResume and setRequireCoverLetter, ensure the promise chain is properly
queued so that subsequent calls wait for the previous operation to complete
before executing their own runOp call.

Comment on lines +222 to +237
function handleEditField(field: string) {
if (field === 'resume' || field === 'coverLetter') {
documentsAnchor.value?.scrollIntoView({ behavior: 'smooth', block: 'center' })
return
}
if (field.startsWith('question:')) {
const id = field.slice('question:'.length)
const q = model.value.questions.find((qq) => qq.id === id)
if (q) {
startEdit(q)
nextTick(() => questionsAnchor.value?.scrollIntoView({ behavior: 'smooth', block: 'center' }))
}
return
}
// name / email / phone are mandatory, fixed fields — nothing to edit.
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preview clicks for personal fields currently lead to a dead action.

ApplicationFormBody emits edit-field for name, email, and phone in preview mode, but this handler intentionally no-ops for those values. The preview still shows clickable affordance, so this feels broken to users.

Suggested fix (scroll to personal info section)
 const questionsAnchor = ref<HTMLElement | null>(null)
 const documentsAnchor = ref<HTMLElement | null>(null)
+const personalInfoAnchor = ref<HTMLElement | null>(null)
 
 /** Clicking a field in the preview jumps to (and opens) its editor on the left. */
 function handleEditField(field: string) {
+  if (field === 'name' || field === 'email' || field === 'phone') {
+    personalInfoAnchor.value?.scrollIntoView({ behavior: 'smooth', block: 'center' })
+    return
+  }
   if (field === 'resume' || field === 'coverLetter') {
     documentsAnchor.value?.scrollIntoView({ behavior: 'smooth', block: 'center' })
     return
   }
   if (field.startsWith('question:')) {
@@
-  // name / email / phone are mandatory, fixed fields — nothing to edit.
 }
-      <div>
+      <div ref="personalInfoAnchor">
         <h2 class="text-base font-semibold text-surface-900 dark:text-surface-100 pb-3 border-b border-surface-100 dark:border-surface-800">Personal information</h2>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function handleEditField(field: string) {
if (field === 'resume' || field === 'coverLetter') {
documentsAnchor.value?.scrollIntoView({ behavior: 'smooth', block: 'center' })
return
}
if (field.startsWith('question:')) {
const id = field.slice('question:'.length)
const q = model.value.questions.find((qq) => qq.id === id)
if (q) {
startEdit(q)
nextTick(() => questionsAnchor.value?.scrollIntoView({ behavior: 'smooth', block: 'center' }))
}
return
}
// name / email / phone are mandatory, fixed fields — nothing to edit.
}
function handleEditField(field: string) {
if (field === 'name' || field === 'email' || field === 'phone') {
personalInfoAnchor.value?.scrollIntoView({ behavior: 'smooth', block: 'center' })
return
}
if (field === 'resume' || field === 'coverLetter') {
documentsAnchor.value?.scrollIntoView({ behavior: 'smooth', block: 'center' })
return
}
if (field.startsWith('question:')) {
const id = field.slice('question:'.length)
const q = model.value.questions.find((qq) => qq.id === id)
if (q) {
startEdit(q)
nextTick(() => questionsAnchor.value?.scrollIntoView({ behavior: 'smooth', block: 'center' }))
}
return
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/ApplicationBuilder.vue` around lines 222 - 237, The
handleEditField function currently has no behavior for the personal field types
(name, email, phone), which causes a dead action when users click on these
fields in preview mode even though they appear clickable. Add a new condition
before the final comment to handle when the field is one of these three personal
fields, and scroll to the personal information section using an anchor ref
(similar to how documentsAnchor is used for resume and coverLetter, and
questionsAnchor is used for questions). Create or reference a personalInfoAnchor
to enable smooth scrolling to the personal fields editing area.

Comment on lines +142 to +144
v-for="item in metadata"
:key="item.label"
class="inline-flex items-center gap-1.5 rounded-full border border-surface-200 bg-surface-50 px-2.5 py-1 text-xs font-medium text-surface-600 dark:border-surface-700 dark:bg-surface-800 dark:text-surface-300"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use a unique key for metadata pills.

item.label is not guaranteed unique (for example, location and remote status can both render "Remote"), which can cause unstable list patching.

Suggested fix
-              <span
-                v-for="item in metadata"
-                :key="item.label"
+              <span
+                v-for="(item, index) in metadata"
+                :key="`${index}-${item.label}`"
                 class="inline-flex items-center gap-1.5 rounded-full border border-surface-200 bg-surface-50 px-2.5 py-1 text-xs font-medium text-surface-600 dark:border-surface-700 dark:bg-surface-800 dark:text-surface-300"
               >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
v-for="item in metadata"
:key="item.label"
class="inline-flex items-center gap-1.5 rounded-full border border-surface-200 bg-surface-50 px-2.5 py-1 text-xs font-medium text-surface-600 dark:border-surface-700 dark:bg-surface-800 dark:text-surface-300"
v-for="(item, index) in metadata"
:key="`${index}-${item.label}`"
class="inline-flex items-center gap-1.5 rounded-full border border-surface-200 bg-surface-50 px-2.5 py-1 text-xs font-medium text-surface-600 dark:border-surface-700 dark:bg-surface-800 dark:text-surface-300"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/ApplicationBuilderPreview.vue` around lines 142 - 144, The
v-for loop in ApplicationBuilderPreview.vue that iterates over metadata is using
item.label as the key, which is not guaranteed to be unique since different
metadata items can have the same label (like "Remote" for both location and
remote status). Replace the :key binding from item.label with a unique
identifier that combines the index and label, or use a truly unique property
from the item object if available, to ensure stable list patching and prevent
rendering issues.

Comment on lines +58 to +71
type BuilderQuestion = {
id: string
label: string
type: string
description?: string | null
required: boolean
options?: string[] | null
}

// Sync with fetched job data
const builderModel = ref<{
requireResume: boolean
requireCoverLetter: boolean
questions: BuilderQuestion[]
}>({ requireResume: false, requireCoverLetter: false, questions: [] })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Align builderModel with ApplicationBuilder's ApplicationForm contract.

Type checking fails at Line 259 because the bound model shape is looser than the child component contract. builderModel uses type: string but ApplicationBuilder expects type: QuestionType; optional fields must be required with null fallback.

Suggested fix
 type BuilderQuestion = {
   id: string
   label: string
-  type: string
-  description?: string | null
+  type: QuestionType
+  description: string | null
   required: boolean
-  options?: string[] | null
+  options: string[] | null
 }

-const builderModel = ref<{
-  requireResume: boolean
-  requireCoverLetter: boolean
-  questions: BuilderQuestion[]
-}>({ requireResume: false, requireCoverLetter: false, questions: [] })
+const builderModel = ref<ApplicationForm>({
+  requireResume: false,
+  requireCoverLetter: false,
+  questions: [],
+})

 watch(jobQuestions, (qs) => {
-  builderModel.value.questions = (qs ?? []).map((q: any) => ({
+  builderModel.value.questions = (qs ?? []).map((q) => ({
     id: q.id,
     label: q.label,
-    type: q.type,
+    type: q.type as QuestionType,
     description: q.description ?? null,
     required: q.required,
     options: q.options ?? null,
   }))
 }, { immediate: true })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
type BuilderQuestion = {
id: string
label: string
type: string
description?: string | null
required: boolean
options?: string[] | null
}
// Sync with fetched job data
const builderModel = ref<{
requireResume: boolean
requireCoverLetter: boolean
questions: BuilderQuestion[]
}>({ requireResume: false, requireCoverLetter: false, questions: [] })
type BuilderQuestion = {
id: string
label: string
type: QuestionType
description: string | null
required: boolean
options: string[] | null
}
const builderModel = ref<ApplicationForm>({
requireResume: false,
requireCoverLetter: false,
questions: [],
})
watch(jobQuestions, (qs) => {
builderModel.value.questions = (qs ?? []).map((q) => ({
id: q.id,
label: q.label,
type: q.type as QuestionType,
description: q.description ?? null,
required: q.required,
options: q.options ?? null,
}))
}, { immediate: true })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/dashboard/jobs/`[id]/application-form.vue around lines 58 - 71, The
`BuilderQuestion` type definition does not match the `ApplicationBuilder`
component's contract. Update the type definition to change the `type` field from
`string` to `QuestionType` to match the expected enum type. Additionally, make
the optional fields `description` and `options` required fields that accept null
values by removing the optional `?` operator and keeping the `| null` part, so
they become `description: string | null` and `options: string[] | null`
respectively. This ensures the `builderModel` shape aligns with what the
`ApplicationBuilder` component expects when the model is bound to it.

Comment on lines +23 to 24
test('recruiter can configure the application form and publish a job', async ({ authenticatedPage }) => {
const page = authenticatedPage

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use an unauthenticated context for the published /apply assertions.

Line 126 currently verifies the public candidate form using authenticatedPage (recruiter session). That can hide candidate-only rendering differences and produce a false green.

Suggested adjustment
-test('recruiter can configure the application form and publish a job', async ({ authenticatedPage }) => {
+test('recruiter can configure the application form and publish a job', async ({ authenticatedPage, browser }) => {
   const page = authenticatedPage
   ...
-  await page.goto(`/jobs/${jobSlug}/apply`)
-  await expect(page.getByLabel('Cover Letter')).toBeVisible()
-  await expect(page.getByText('Resume / CV', { exact: false })).toHaveCount(0)
-  const publishedQuestion = page.getByLabel(UPDATED_QUESTION_LABEL)
+  const candidateContext = await browser.newContext()
+  const candidatePage = await candidateContext.newPage()
+  await candidatePage.goto(`/jobs/${jobSlug}/apply`)
+  await expect(candidatePage.getByLabel('Cover Letter')).toBeVisible()
+  await expect(candidatePage.getByText('Resume / CV', { exact: false })).toHaveCount(0)
+  const publishedQuestion = candidatePage.getByLabel(UPDATED_QUESTION_LABEL)
   await expect(publishedQuestion).toBeVisible()
   await expect(publishedQuestion.getByRole('option', { name: 'Playwright' })).toHaveCount(1)
   await expect(publishedQuestion.getByRole('option', { name: 'Cypress' })).toHaveCount(1)
+  await candidateContext.close()

Also applies to: 126-132

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/critical-flows/job-creation.spec.ts` around lines 23 - 24, The test
function verifies the public candidate application form using the
authenticatedPage context which represents a recruiter session. This can mask
candidate-specific rendering differences and produce false positives. Replace
the authenticatedPage usage in the assertions around lines 126-132 that verify
the public /apply form with an unauthenticated page context. Create a new
unauthenticated context (typically available in the test framework for accessing
pages without authentication) and use it specifically for those public form
assertions to accurately test how a candidate would experience the application
form.

@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-207 June 18, 2026 10:45 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
server/database/migrations/0029_wakeful_secret_warriors.sql (1)

1-1: ⚡ Quick win

Enforce phone_requirement domain at the database layer.

Right now this column accepts any text value. Adding a CHECK constraint keeps DB state aligned with the app/API enum contract.

Proposed migration adjustment
-ALTER TABLE "job" ADD COLUMN "phone_requirement" text DEFAULT 'optional' NOT NULL;
+ALTER TABLE "job"
+  ADD COLUMN "phone_requirement" text DEFAULT 'optional' NOT NULL,
+  ADD CONSTRAINT "job_phone_requirement_check"
+    CHECK ("phone_requirement" IN ('hidden', 'optional', 'required'));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/database/migrations/0029_wakeful_secret_warriors.sql` at line 1, The
migration adding the `phone_requirement` column to the `job` table lacks a CHECK
constraint to enforce valid domain values at the database layer. Modify the
ALTER TABLE statement to include a CHECK constraint on the `phone_requirement`
column that restricts values to the valid enum options that match your app/API
contract (for example, valid values like 'required', 'optional', 'preferred',
etc.). This ensures the database enforces data integrity by rejecting invalid
values rather than allowing any arbitrary text.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/pages/dashboard/jobs/preview.vue`:
- Around line 92-94: The closePreview function relies solely on window.close()
which can fail silently when the page is opened directly or blocked by browser
policy, leaving users stuck. Modify the closePreview function to add a fallback
mechanism: after calling window.close(), use router.back() or history.back() as
a fallback to navigate the user back to the previous page if the window close
operation fails to perform any navigation action. This ensures users can always
navigate away from the preview page regardless of how it was opened or browser
restrictions.

In `@server/database/schema/app.ts`:
- Line 67: The phoneRequirement column uses a text column with TypeScript-level
type enforcement via $type, which does not enforce the allowed values at the
database level in PostgreSQL. Replace the text column with a proper PostgreSQL
enum type by first creating an enum definition for the three allowed values
(hidden, optional, required) and then using that enum type for the
phoneRequirement column instead of text. This ensures that invalid values cannot
be inserted into the database, preventing the API/UI contract violations that
can occur when applying validation behavior in routes like
server/api/public/jobs/[slug]/apply.post.ts.

---

Nitpick comments:
In `@server/database/migrations/0029_wakeful_secret_warriors.sql`:
- Line 1: The migration adding the `phone_requirement` column to the `job` table
lacks a CHECK constraint to enforce valid domain values at the database layer.
Modify the ALTER TABLE statement to include a CHECK constraint on the
`phone_requirement` column that restricts values to the valid enum options that
match your app/API contract (for example, valid values like 'required',
'optional', 'preferred', etc.). This ensures the database enforces data
integrity by rejecting invalid values rather than allowing any arbitrary text.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 40682315-0db1-4536-bcbb-c3dde0297c67

📥 Commits

Reviewing files that changed from the base of the PR and between 5118b6e and b269934.

📒 Files selected for processing (23)
  • app/components/ApplicationBuilder.vue
  • app/components/ApplicationBuilderPreview.vue
  • app/components/ApplicationFormBody.vue
  • app/components/PublicJobApplicationHeader.vue
  • app/composables/useJob.ts
  • app/composables/useJobs.ts
  • app/pages/dashboard/jobs/[id]/application-form.vue
  • app/pages/dashboard/jobs/new.vue
  • app/pages/dashboard/jobs/preview.vue
  • app/pages/jobs/[slug]/apply.vue
  • e2e/critical-flows/job-creation.spec.ts
  • i18n/i18n.config.ts
  • package.json
  • server/api/jobs/[id].get.ts
  • server/api/jobs/[id].patch.ts
  • server/api/jobs/index.post.ts
  • server/api/public/jobs/[slug].get.ts
  • server/api/public/jobs/[slug]/apply.post.ts
  • server/database/migrations/0029_wakeful_secret_warriors.sql
  • server/database/migrations/meta/0029_snapshot.json
  • server/database/migrations/meta/_journal.json
  • server/database/schema/app.ts
  • server/utils/schemas/job.ts
✅ Files skipped from review due to trivial changes (2)
  • i18n/i18n.config.ts
  • app/composables/useJob.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/pages/dashboard/jobs/[id]/application-form.vue
  • app/components/ApplicationBuilderPreview.vue
  • app/components/ApplicationFormBody.vue
  • e2e/critical-flows/job-creation.spec.ts

Comment on lines +92 to +94
function closePreview() {
window.close()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Line 93: window.close() can silently fail and leave the user stuck.

When this page is opened directly (or browser policy blocks close), the “Back to editor” action does nothing. Add a history/router fallback.

Proposed fix
 function closePreview() {
-  window.close()
+  if (window.opener && !window.opener.closed) {
+    window.close()
+    return
+  }
+  if (window.history.length > 1) {
+    window.history.back()
+    return
+  }
+  void navigateTo('/dashboard/jobs')
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function closePreview() {
window.close()
}
function closePreview() {
if (window.opener && !window.opener.closed) {
window.close()
return
}
if (window.history.length > 1) {
window.history.back()
return
}
void navigateTo('/dashboard/jobs')
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/dashboard/jobs/preview.vue` around lines 92 - 94, The closePreview
function relies solely on window.close() which can fail silently when the page
is opened directly or blocked by browser policy, leaving users stuck. Modify the
closePreview function to add a fallback mechanism: after calling window.close(),
use router.back() or history.back() as a fallback to navigate the user back to
the previous page if the window close operation fails to perform any navigation
action. This ensures users can always navigate away from the preview page
regardless of how it was opened or browser restrictions.

/** Experience level required for this role */
experienceLevel: experienceLevelEnum('experience_level'),
// ── Application form settings ──
phoneRequirement: text('phone_requirement').$type<'hidden' | 'optional' | 'required'>().notNull().default('optional'),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Enforce phoneRequirement at the database level, not only TypeScript.

Line 67 uses a text column with $type<...>, which is compile-time only and does not enforce the allowed values in PostgreSQL. That can let invalid values persist and break the API/UI contract that assumes only hidden | optional | required (for example, apply validation behavior in server/api/public/jobs/[slug]/apply.post.ts).

Suggested direction
+export const phoneRequirementEnum = pgEnum('phone_requirement', ['hidden', 'optional', 'required'])
...
-  phoneRequirement: text('phone_requirement').$type<'hidden' | 'optional' | 'required'>().notNull().default('optional'),
+  phoneRequirement: phoneRequirementEnum('phone_requirement').notNull().default('optional'),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/database/schema/app.ts` at line 67, The phoneRequirement column uses a
text column with TypeScript-level type enforcement via $type, which does not
enforce the allowed values at the database level in PostgreSQL. Replace the text
column with a proper PostgreSQL enum type by first creating an enum definition
for the three allowed values (hidden, optional, required) and then using that
enum type for the phoneRequirement column instead of text. This ensures that
invalid values cannot be inserted into the database, preventing the API/UI
contract violations that can occur when applying validation behavior in routes
like server/api/public/jobs/[slug]/apply.post.ts.

@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-207 June 19, 2026 05:39 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/critical-flows/job-creation.spec.ts`:
- Around line 62-66: The request listener is registered after the button click
on "Save as draft", causing the first POST request to /api/jobs to be missed and
not counted. Move the page.on('request') listener setup that checks for POST
requests to /api/jobs before the click action on the "Save as draft" button to
ensure all relevant requests are captured and counted.

In `@server/utils/schemas/jobQuestion.ts`:
- Around line 43-49: The updateQuestionSchema allows invalid state where
select-type questions can have null options during PATCH operations. Add
superRefine validation to updateQuestionSchema to enforce the same select-type
invariants that exist in createQuestionSchema, ensuring that when type is set to
a select variant (single_select, multi_select, etc.), the options field must
contain at least one unique option rather than being null or empty.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3505cc37-9820-42aa-bd72-e2d21cbe2f4e

📥 Commits

Reviewing files that changed from the base of the PR and between b269934 and b128764.

📒 Files selected for processing (11)
  • app/components/ApplicationBuilder.vue
  • app/components/ApplicationBuilderPreview.vue
  • app/components/QuestionForm.vue
  • app/composables/useJobs.ts
  • app/pages/dashboard/jobs/new.vue
  • e2e/critical-flows/job-creation.spec.ts
  • server/api/jobs/index.post.ts
  • server/utils/schemas/job.ts
  • server/utils/schemas/jobQuestion.ts
  • server/utils/schemas/scoring.ts
  • tests/unit/job-creation-schema.test.ts
💤 Files with no reviewable changes (1)
  • app/components/ApplicationBuilderPreview.vue
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/composables/useJobs.ts
  • app/components/ApplicationBuilder.vue

Comment thread e2e/critical-flows/job-creation.spec.ts Outdated
Comment thread server/utils/schemas/jobQuestion.ts
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-207 June 19, 2026 06:26 Destroyed
Signed-off-by: Joachim LK <joachim.l.kolle.pers@gmail.com>

# Conflicts:
#	package-lock.json
#	package.json
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-207 June 19, 2026 09:29 Destroyed
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-207 June 19, 2026 09:39 Destroyed
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-207 June 19, 2026 09:45 Destroyed
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-207 June 19, 2026 10:03 Destroyed
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-207 June 19, 2026 10:45 Destroyed
@JoachimLK JoachimLK merged commit 1926a8a into main Jun 19, 2026
11 checks passed
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