Skip to content

fix: Fixes the public application form to show file upload and cover letter, additionally imrpove the multi step form to simplify the process#66

Merged
JoachimLK merged 9 commits into
mainfrom
fix/upload-cv-&-cover-letter
Mar 4, 2026
Merged

fix: Fixes the public application form to show file upload and cover letter, additionally imrpove the multi step form to simplify the process#66
JoachimLK merged 9 commits into
mainfrom
fix/upload-cv-&-cover-letter

Conversation

@JoachimLK
Copy link
Copy Markdown
Contributor

@JoachimLK JoachimLK commented Mar 4, 2026

Summary

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

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

  • New Features

    • Jobs can require a resume/CV and request a cover letter; applicants may upload a resume file and submit cover letter text (validation and size limits applied).
    • Publish & share step adds a copyable final application link and publish flow.
    • Application requirements panel with toggles and Save action.
    • Document preview modal for viewing candidate documents (PDF inline, fallback/download).
  • Tests

    • Expanded end-to-end application scenarios and improved test reliability/timeouts.
  • Chores

    • Removed legacy Snyk guidance from repository instructions.

…letter, additionally imrpove the multi step form to simplify the process
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-66 March 4, 2026 10:15 Destroyed
@railway-app
Copy link
Copy Markdown

railway-app Bot commented Mar 4, 2026

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

Service Status Web Updated (UTC)
applirank ✅ Success (View Logs) Mar 4, 2026 at 3:08 pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 4, 2026

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

Adds resume and cover-letter support across DB, server, and frontend: new job flags and application field, migrations and snapshots, API/schema updates, UI for job settings, applicant resume/cover-letter handling, document preview, E2E test additions, and Playwright/CI adjustments. Removes a Snyk instruction block.

Changes

Cohort / File(s) Summary
Database migrations & schema
server/database/migrations/0008_nervous_stone_men.sql, server/database/migrations/0009_organic_gauntlet.sql, server/database/migrations/meta/0008_snapshot.json, server/database/migrations/meta/0009_snapshot.json, server/database/migrations/meta/_journal.json, server/database/schema/app.ts
Add require_resume/require_cover_letter to job, add cover_letter_text to application, include migration snapshots and journal updates.
Server endpoints (job & public apply)
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
Expose and persist new job flags; public apply endpoint accepts coverLetterText, supports dedicated resume multipart part, validates required fields, uploads resume to S3, creates resume document records, and updates application persistence and file-counting logic.
Server schemas
server/utils/schemas/job.ts, server/utils/schemas/publicApplication.ts
Add requireResume/requireCoverLetter to job schemas (create + explicit update schema); add optional coverLetterText (max 10000) to public application schema.
Frontend: composables
app/composables/useJob.ts, app/composables/useJobs.ts
Extend create/update payload typings to accept and forward requireResume and requireCoverLetter.
Frontend: dashboard — application requirements panel
app/pages/dashboard/jobs/[id]/application-form.vue
Add UI to toggle/save requireResume and requireCoverLetter, sync from job data, and call updateJob with save/error/success states.
Frontend: dashboard — document preview
app/pages/dashboard/jobs/[id]/index.vue
Add document preview modal with PDF iframe + fallback, open/close handlers, Esc key close, download/close controls, and wired document actions.
Frontend: job creation wizard
app/pages/dashboard/jobs/new.vue
Add Step 4 "Publish & share", publish/draft flow, final application link generation and copy-to-clipboard, and related UI/state changes.
Frontend: public apply form
app/pages/jobs/[slug]/apply.vue
Add built-in resume file and coverLetterText UI/state, validations (required flags, file size, cover letter length), FormData vs JSON submission branching, and integration with existing file-question handling.
E2E tests & Playwright / CI
e2e/critical-flows/candidate-application.spec.ts, e2e/critical-flows/job-creation.spec.ts, playwright.config.ts, .github/workflows/e2e-tests.yml
Expand candidate-application spec for full question set and recruiter validations; update job-creation spec for new publish flow; adjust Playwright timeouts and add MinIO service + S3 env vars in CI workflow.
Docs / CI cleanup
.github/instructions/snyk_rules.instructions.md, .github/workflows/e2e-tests.yml
Removed Snyk instruction block; CI e2e workflow updated (timeouts, MinIO service, env vars) and Allure step fixed to create results dir.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Recruiter as Recruiter
    participant UI as Dashboard Requirements UI
    participant Composable as useJob
    participant API as PATCH /api/jobs/[id]
    participant DB as Database

    Recruiter->>UI: Toggle Require Resume/Cover Letter, click Save
    UI->>Composable: updateJob({ requireResume, requireCoverLetter })
    Composable->>API: PATCH /api/jobs/[id] payload
    API->>DB: Update job record (require_resume, require_cover_letter)
    DB-->>API: Updated job record
    API-->>Composable: Response (job)
    Composable-->>UI: Show success or error
Loading
sequenceDiagram
    autonumber
    actor Candidate as Candidate
    participant UI as Public Apply Form
    participant API as POST /api/public/jobs/:slug/apply
    participant Validation as Server Validation
    participant S3 as S3/MinIO
    participant DB as Database

    Candidate->>UI: Fill responses, attach resume file?, provide coverLetterText
    UI->>API: POST (FormData if files else JSON)
    API->>Validation: Validate required resume/cover letter, sizes, lengths
    alt resume present
        Validation->>S3: Upload resume part
        S3-->>API: File URL/Key
        API->>DB: Create document record (type=resume)
    end
    API->>DB: Insert application (include cover_letter_text)
    DB-->>API: Confirmation
    API-->>UI: Success response
    UI-->>Candidate: Show confirmation
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I nudged new fields into schema and view,
Resumes and letters now travel through.
Previews unfurl and publish links gleam,
Files upload, tests run, and recruiters beam.
Hop along — hiring flows like a stream!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is a bare template with no actual content filled in. All sections are empty: the summary lacks details about what changed and why, the type of change is not selected, and validation items are not checked. Fill in the summary with what this PR changes and why it's needed. Select the appropriate type of change(s) and complete the validation checklist where applicable.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title describes the main changes: fixing the public application form to show file upload and cover letter, and improving the multi-step form. However, it's somewhat unfocused, mentioning two distinct areas and containing a typo ('imrpove').
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/upload-cv-&-cover-letter

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.

@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-66 March 4, 2026 10:27 Destroyed
Copy link
Copy Markdown

@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: 3

Caution

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

⚠️ Outside diff range comments (2)
app/pages/dashboard/jobs/[id]/index.vue (1)

356-372: ⚠️ Potential issue | 🟡 Minor

Prevent background candidate navigation while document preview is open.

Arrow key navigation still runs behind the modal. This can change the selected candidate while the user is reading a document preview.

Suggested fix
 function handleKeyNavigation(event: KeyboardEvent) {
-  if (event.key === 'Escape' && showDocPreview.value) {
-    closeDocPreview()
-    return
-  }
+  if (showDocPreview.value) {
+    if (event.key === 'Escape') {
+      closeDocPreview()
+    }
+    return
+  }
 
   if ((event.target as HTMLElement)?.tagName === 'INPUT' || (event.target as HTMLElement)?.tagName === 'TEXTAREA' || (event.target as HTMLElement)?.tagName === 'SELECT') return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/`[id]/index.vue around lines 356 - 372, The keydown
handler allows ArrowUp/ArrowDown to change selected candidate while a document
preview modal is open; modify the handler so when showDocPreview.value is true
it only processes the Escape key (calling closeDocPreview()) and otherwise
returns early to prevent goToPreviousCard() / goToNextCard() from running;
locate the block handling event.key and event.target in the keydown listener and
reorder or add a conditional that checks showDocPreview.value before running the
ArrowUp/ArrowDown logic.
server/api/public/jobs/[slug]/apply.post.ts (1)

327-333: ⚠️ Potential issue | 🔴 Critical

Required resume validation/upload happens after DB writes and can silently fail.

Line 327 creates the application before resume size/MIME checks (Lines 435-458). Also, Lines 477-484 swallow resume upload errors and still return success. This can persist an application missing a required resume and then block user retry via duplicate-application checks.

🔧 Suggested direction
+ // Validate built-in resume (size + MIME) before creating application records.
+ // If job requires resume and upload fails, fail the request.

-  const [newApplication] = await db.insert(application).values({
+  const [newApplication] = await db.insert(application).values({
     organizationId: orgId,
     candidateId,
     jobId,
     status: 'new',
     coverLetterText: coverLetterText || null,
   }).returning({ id: application.id })

...
-    } catch (uploadError) {
+    } catch (uploadError) {
       try {
         await deleteFromS3(storageKey)
       } catch (cleanupError) {
         console.error('[Reqcore] Failed to clean up orphaned S3 object:', storageKey, cleanupError)
       }
-      console.error('[Reqcore] Resume upload failed during application:', uploadError)
+      if (existingJob.requireResume) {
+        throw createError({ statusCode: 503, statusMessage: 'Resume upload failed. Please retry.' })
+      }
+      console.error('[Reqcore] Resume upload failed during application:', uploadError)
     }

Also applies to: 431-485

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

In `@server/api/public/jobs/`[slug]/apply.post.ts around lines 327 - 333, The code
inserts the application (db.insert(application).values(...) returning { id:
application.id } -> newApplication) before performing resume size/MIME
validation and before performing the resume upload, which can silently fail and
leave a persisted application that blocks retries; move the resume validation
and the actual upload (the resume upload routine / function used later) to occur
before creating the DB record, or if you must insert first, perform the upload
inside a transaction and rollback/delete the inserted application on
upload/validation failure and propagate the error; also stop swallowing upload
errors (ensure the upload promise errors are thrown) so apply.post returns
failure rather than success when required resume upload/validation fails.
🧹 Nitpick comments (3)
server/database/migrations/0008_nervous_stone_men.sql (1)

3-3: Optional cleanup: drop redundant index creation from this migration.

member_user_org_unique_idx already exists from migration 0007; this is a safe no-op, but it makes this migration less focused.

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

In `@server/database/migrations/0008_nervous_stone_men.sql` at line 3, Remove the
redundant CREATE UNIQUE INDEX statement for member_user_org_unique_idx from
migration 0008_nervous_stone_men.sql: delete the line CREATE UNIQUE INDEX IF NOT
EXISTS "member_user_org_unique_idx" ON "member" USING btree
("user_id","organization_id"); so the migration no longer attempts to recreate
an index that was already created in migration 0007, keeping 0008 focused and
idempotent.
server/database/migrations/0009_organic_gauntlet.sql (1)

1-1: Consider enforcing cover letter length at the database layer too.

Right now size limits are only enforced in application validation. Adding a DB check keeps data integrity intact even for non-API writers.

Suggested migration hardening
 ALTER TABLE "application" ADD COLUMN "cover_letter_text" text;
+ALTER TABLE "application"
+  ADD CONSTRAINT "application_cover_letter_text_len_chk"
+  CHECK ("cover_letter_text" IS NULL OR char_length("cover_letter_text") <= 10000);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/database/migrations/0009_organic_gauntlet.sql` at line 1, Add a
DB-level CHECK constraint to the "application" table to enforce the same maximum
cover letter length as the application validation (use the same MAX value) so
non-API writers cannot insert oversized text; alter the migration that creates
the cover_letter_text column (or add a subsequent ALTER) to add a constraint
named like application_cover_letter_length that uses
char_length(coalesce(cover_letter_text, '')) <= <MAX> (Postgres char_length) to
validate length while allowing NULL.
app/pages/dashboard/jobs/[id]/application-form.vue (1)

60-69: Surface failures when saving requirements.

If the PATCH fails, users get no explicit feedback. Add an error state/message so save failures are visible.

♻️ Suggested improvement
 const isSavingRequirements = ref(false)
 const requirementsSaved = ref(false)
+const requirementsError = ref<string | null>(null)

 async function saveRequirements() {
   isSavingRequirements.value = true
+  requirementsError.value = null
   try {
     await updateJob({ requireResume: requireResume.value, requireCoverLetter: requireCoverLetter.value })
     requirementsSaved.value = true
     setTimeout(() => { requirementsSaved.value = false }, 2000)
+  } catch (error: any) {
+    requirementsSaved.value = false
+    requirementsError.value = error?.data?.statusMessage ?? 'Failed to save requirements.'
   } finally {
     isSavingRequirements.value = false
   }
 }
         <button
           type="button"
           :disabled="isSavingRequirements"
           class="inline-flex items-center gap-1.5 rounded-lg bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700 disabled:opacity-50 transition-colors"
           `@click`="saveRequirements"
         >
           {{ requirementsSaved ? 'Saved!' : isSavingRequirements ? 'Saving…' : 'Save requirements' }}
         </button>
+        <p v-if="requirementsError" class="mt-2 text-xs text-danger-600 dark:text-danger-400">
+          {{ requirementsError }}
+        </p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/`[id]/application-form.vue around lines 60 - 69, The
saveRequirements function currently swallows failures because it only uses a
finally block and sets requirementsSaved on success; add explicit error
handling: create a reactive error state (e.g., requirementsError.value), wrap
the await updateJob(...) in a try/catch inside saveRequirements, set
requirementsError with the caught error message on failure and ensure
requirementsSaved is only set on success, and still clear isSavingRequirements
in finally; also update the template to display requirementsError when present
so users see save failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/pages/dashboard/jobs/new.vue`:
- Line 450: The form and submit button are causing duplicate submissions because
the button uses `@click`="handleSubmit()" while the form uses `@submit.prevent` with
a wrapper; remove the `@click` from the button (the button should remain
type="submit") and simplify the form handler to call the method directly by
replacing the wrapper `@submit.prevent`="() => handleSubmit()" with
`@submit.prevent`="handleSubmit" so only the form submit triggers the single
handleSubmit() invocation.

In `@app/pages/jobs/`[slug]/apply.vue:
- Around line 85-89: The client-side validation for cover letters must enforce
the 10,000-character limit: in the validation block that checks
job.value?.requireCoverLetter and coverLetterText.value (and the equivalent
validation later in the file), add a check that if
coverLetterText.value.trim().length > 10000 then set errors.value.coverLetter to
a clear message like "Cover letter must be 10,000 characters or fewer" (and
prevent submission). Ensure you update both the initial required-cover-letter
check and the duplicate validation block (the one also referencing
coverLetterText.value and errors.value.coverLetter) so the length rule is
consistently applied before allowing the form to submit.

In `@server/api/public/jobs/`[slug]/apply.post.ts:
- Around line 151-154: The code fetches existingJob with requireCoverLetter but
never enforces it; update the application validation logic (the handler in
apply.post.ts that checks existingJob.requireResume) to also check
existingJob.requireCoverLetter and return a 400 error if a required cover letter
is missing or empty. Locate the section using existingJob.requireResume and
mirror that logic for existingJob.requireCoverLetter (validate
req.body.coverLetter or equivalent field and use the same error response
pattern) so applications cannot be accepted without the cover letter when the
job requires it.

---

Outside diff comments:
In `@app/pages/dashboard/jobs/`[id]/index.vue:
- Around line 356-372: The keydown handler allows ArrowUp/ArrowDown to change
selected candidate while a document preview modal is open; modify the handler so
when showDocPreview.value is true it only processes the Escape key (calling
closeDocPreview()) and otherwise returns early to prevent goToPreviousCard() /
goToNextCard() from running; locate the block handling event.key and
event.target in the keydown listener and reorder or add a conditional that
checks showDocPreview.value before running the ArrowUp/ArrowDown logic.

In `@server/api/public/jobs/`[slug]/apply.post.ts:
- Around line 327-333: The code inserts the application
(db.insert(application).values(...) returning { id: application.id } ->
newApplication) before performing resume size/MIME validation and before
performing the resume upload, which can silently fail and leave a persisted
application that blocks retries; move the resume validation and the actual
upload (the resume upload routine / function used later) to occur before
creating the DB record, or if you must insert first, perform the upload inside a
transaction and rollback/delete the inserted application on upload/validation
failure and propagate the error; also stop swallowing upload errors (ensure the
upload promise errors are thrown) so apply.post returns failure rather than
success when required resume upload/validation fails.

---

Nitpick comments:
In `@app/pages/dashboard/jobs/`[id]/application-form.vue:
- Around line 60-69: The saveRequirements function currently swallows failures
because it only uses a finally block and sets requirementsSaved on success; add
explicit error handling: create a reactive error state (e.g.,
requirementsError.value), wrap the await updateJob(...) in a try/catch inside
saveRequirements, set requirementsError with the caught error message on failure
and ensure requirementsSaved is only set on success, and still clear
isSavingRequirements in finally; also update the template to display
requirementsError when present so users see save failures.

In `@server/database/migrations/0008_nervous_stone_men.sql`:
- Line 3: Remove the redundant CREATE UNIQUE INDEX statement for
member_user_org_unique_idx from migration 0008_nervous_stone_men.sql: delete the
line CREATE UNIQUE INDEX IF NOT EXISTS "member_user_org_unique_idx" ON "member"
USING btree ("user_id","organization_id"); so the migration no longer attempts
to recreate an index that was already created in migration 0007, keeping 0008
focused and idempotent.

In `@server/database/migrations/0009_organic_gauntlet.sql`:
- Line 1: Add a DB-level CHECK constraint to the "application" table to enforce
the same maximum cover letter length as the application validation (use the same
MAX value) so non-API writers cannot insert oversized text; alter the migration
that creates the cover_letter_text column (or add a subsequent ALTER) to add a
constraint named like application_cover_letter_length that uses
char_length(coalesce(cover_letter_text, '')) <= <MAX> (Postgres char_length) to
validate length while allowing NULL.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6dea833f-7e9a-4bf0-9048-0b7266eb1982

📥 Commits

Reviewing files that changed from the base of the PR and between f7f79e8 and 3e02c4a.

📒 Files selected for processing (20)
  • .github/instructions/snyk_rules.instructions.md
  • app/composables/useJob.ts
  • app/composables/useJobs.ts
  • app/pages/dashboard/jobs/[id]/application-form.vue
  • app/pages/dashboard/jobs/[id]/index.vue
  • app/pages/dashboard/jobs/new.vue
  • app/pages/jobs/[slug]/apply.vue
  • 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/0008_nervous_stone_men.sql
  • server/database/migrations/0009_organic_gauntlet.sql
  • server/database/migrations/meta/0008_snapshot.json
  • server/database/migrations/meta/0009_snapshot.json
  • server/database/migrations/meta/_journal.json
  • server/database/schema/app.ts
  • server/utils/schemas/job.ts
  • server/utils/schemas/publicApplication.ts
💤 Files with no reviewable changes (1)
  • .github/instructions/snyk_rules.instructions.md

Comment thread app/pages/dashboard/jobs/new.vue
Comment on lines +85 to +89
// Validate required cover letter
if (job.value?.requireCoverLetter && !coverLetterText.value.trim()) {
errors.value.coverLetter = 'Cover letter is required'
}

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

Enforce the 10,000-character cover letter limit client-side.

The UI communicates a max length, but client validation doesn’t currently enforce it.

🛠️ Proposed fix
-  if (job.value?.requireCoverLetter && !coverLetterText.value.trim()) {
+  const trimmedCoverLetter = coverLetterText.value.trim()
+  if (job.value?.requireCoverLetter && !trimmedCoverLetter) {
     errors.value.coverLetter = 'Cover letter is required'
   }
+  if (trimmedCoverLetter.length > 10000) {
+    errors.value.coverLetter = 'Cover letter is too long. Maximum 10,000 characters.'
+  }
                   <textarea
                     id="coverLetterText"
                     v-model="coverLetterText"
                     rows="6"
+                    maxlength="10000"
                     placeholder="Tell us why you're interested in this role…"

Also applies to: 456-470

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

In `@app/pages/jobs/`[slug]/apply.vue around lines 85 - 89, The client-side
validation for cover letters must enforce the 10,000-character limit: in the
validation block that checks job.value?.requireCoverLetter and
coverLetterText.value (and the equivalent validation later in the file), add a
check that if coverLetterText.value.trim().length > 10000 then set
errors.value.coverLetter to a clear message like "Cover letter must be 10,000
characters or fewer" (and prevent submission). Ensure you update both the
initial required-cover-letter check and the duplicate validation block (the one
also referencing coverLetterText.value and errors.value.coverLetter) so the
length rule is consistently applied before allowing the form to submit.

Comment thread server/api/public/jobs/[slug]/apply.post.ts
Copy link
Copy Markdown

@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 (3)
app/pages/jobs/[slug]/apply.vue (1)

70-70: File-size limit should be centralized to avoid drift.

This file hardcodes 10 MB, while the backend validates against MAX_FILE_SIZE. A shared constant would keep client/server behavior aligned.

Also applies to: 117-122

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

In `@app/pages/jobs/`[slug]/apply.vue at line 70, Replace the hardcoded
client-side file size limit (const maxSize = 10 * 1024 * 1024 in
app/pages/jobs/[slug]/apply.vue and the similar checks around lines 117-122)
with the shared MAX_FILE_SIZE constant used by the backend: add/import the
shared constant (e.g., MAX_FILE_SIZE from your shared/config or constants
module) and use that value for all client-side validations (the maxSize variable
and any file-size checks) so client and server limits stay synchronized.
app/pages/dashboard/jobs/[id]/index.vue (2)

580-585: Non-PDF fallback UI is currently unreachable.

handleDocPreview returns early for non-PDF files, so the non-PDF modal fallback block never renders.

♻️ Suggested adjustment
 function handleDocPreview(doc: SwipeDocument) {
-  if (doc.mimeType !== 'application/pdf') {
-    // Non-PDFs: fall back to download
-    window.open(`/api/documents/${doc.id}/download`, '_blank')
-    return
-  }
   docPreviewDocId.value = doc.id
   docPreviewFilename.value = doc.originalFilename
   docPreviewMimeType.value = doc.mimeType
-  docPreviewUrl.value = getPreviewUrl(doc.id)
+  docPreviewUrl.value = doc.mimeType === 'application/pdf'
+    ? getPreviewUrl(doc.id)
+    : null
   showDocPreview.value = true
 }

Also applies to: 1329-1342

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

In `@app/pages/dashboard/jobs/`[id]/index.vue around lines 580 - 585, The non-PDF
fallback UI in handleDocPreview is never reached because the function returns
early for non-PDF mimeTypes; instead of returning, flip the control flow so PDFs
open the in-browser preview (window.open for 'application/pdf') and non-PDFs
fall through to the existing fallback modal/render logic (e.g., trigger the
non-PDF modal/display path currently below). Specifically, update
handleDocPreview to call the PDF preview only in the PDF branch and remove the
early return so the non-PDF branch can execute the modal/fallback rendering
(refer to handleDocPreview and the non-PDF modal state/handler used later in the
file).

1294-1302: Add dialog semantics to the preview modal.

The modal container should expose dialog semantics (role="dialog" + aria-modal="true") for better screen-reader behavior.

♿ Suggested accessibility tweak
-      <div v-if="showDocPreview" class="fixed inset-0 z-50 flex items-center justify-center px-4 py-6">
+      <div
+        v-if="showDocPreview"
+        class="fixed inset-0 z-50 flex items-center justify-center px-4 py-6"
+        role="dialog"
+        aria-modal="true"
+      >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/`[id]/index.vue around lines 1294 - 1302, The
preview modal needs proper dialog semantics: on the modal container element (the
div with classes "relative flex flex-col ... max-w-4xl" shown when
showDocPreview is true) add role="dialog" and aria-modal="true", and wire an
accessible label by adding an id to the filename span (e.g.,
id="docPreviewTitle") and setting aria-labelledby="docPreviewTitle" on the modal
container; keep existing closeDocPreview click handler and docPreviewFilename
binding intact so screen readers announce the title and the modal is exposed
correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/pages/dashboard/jobs/`[id]/application-form.vue:
- Around line 60-68: The saveRequirements function currently only uses
try/finally so a failing updateJob causes unhandled rejections and never
surfaces an error; change it to try/catch/finally, move requirementsSaved.value
= true and the setTimeout into the try block after await updateJob(...), and in
the catch block handle the failure by setting an error state or calling the
existing UI error handler (e.g., set a new reactive error variable or invoke a
toast) so the user is informed; always restore isSavingRequirements.value =
false in the finally block. Ensure you reference saveRequirements, updateJob,
isSavingRequirements, and requirementsSaved when making the edits.

In `@app/pages/dashboard/jobs/new.vue`:
- Line 450: The form's submit is wired twice causing duplicate runs of
handleSubmit: remove the extra click-based call from the submit button and rely
on the form-level `@submit.prevent` to invoke handleSubmit, or alternatively make
the button a plain type="submit" with no `@click`; update the submit button code
referenced around the button click handler (the element that currently calls
handleSubmit() in lines ~969-976) to remove its `@click` invocation so only the
form-level submit handler (handleSubmit) is executed.

In `@app/pages/jobs/`[slug]/apply.vue:
- Around line 85-89: The client-side validation must enforce the server's
10,000-character cover-letter limit: inside the same validation block that
checks job.value?.requireCoverLetter and coverLetterText.value (and the other
validation location around the submit/validate function), add a check for
coverLetterText.value.length > 10000 and set errors.value.coverLetter to a clear
message like "Cover letter must be 10,000 characters or fewer" (ensure you still
trim when checking emptiness but use the raw length for the max-length check);
update both occurrences (the required-cover-letter check and the separate submit
validation) so the client rejects overlong input before sending to the server.

In `@playwright.config.ts`:
- Around line 28-34: The configured timeouts in playwright.config.ts (timeout,
expect.timeout, use.actionTimeout, use.navigationTimeout) are set too low and
may cause flaky E2E tests; update the values to match the practical budget used
by your current end-to-end scenarios (e.g., restore the test-level timeout from
30_000 to the previous higher value and increase use.actionTimeout and
use.navigationTimeout to more generous numbers) so long chained
setup/apply/publish flows have enough headroom; locate the timeout, expect, and
use.actionTimeout/use.navigationTimeout entries and raise them to the project’s
proven stable values or make them configurable via PLAYWRIGHT_* env vars.

In `@server/api/public/jobs/`[slug]/apply.post.ts:
- Around line 153-164: The code fetches existingJob.requireCoverLetter but never
enforces it; add a validation after the resume check that throws createError({
statusCode: 422, statusMessage: 'Cover letter is required for this position' })
when existingJob.requireCoverLetter is true and there is no coverLetterText and
no coverLetterUpload (i.e., ensure the check uses
existingJob.requireCoverLetter, coverLetterText, coverLetterUpload and
createError to reject missing cover letters).
- Around line 431-484: The resume upload catch currently swallows errors
(uploadError) and only logs them, allowing a 201 success even when a required
resume failed; update the catch in the resumeUpload handling so that after
attempting cleanup via deleteFromS3(storageKey) you rethrow (or throw a
createError with appropriate status) the original uploadError so the request
fails, and if there's a job-level flag like requireResume (or require_resume)
ensure you explicitly check it and return a 4xx/5xx error when persistence via
uploadToS3 or db.insert(document).values fails; in short: after cleanup, do not
continue—propagate the error from uploadToS3 / db.insert so the caller sees
failure.

---

Nitpick comments:
In `@app/pages/dashboard/jobs/`[id]/index.vue:
- Around line 580-585: The non-PDF fallback UI in handleDocPreview is never
reached because the function returns early for non-PDF mimeTypes; instead of
returning, flip the control flow so PDFs open the in-browser preview
(window.open for 'application/pdf') and non-PDFs fall through to the existing
fallback modal/render logic (e.g., trigger the non-PDF modal/display path
currently below). Specifically, update handleDocPreview to call the PDF preview
only in the PDF branch and remove the early return so the non-PDF branch can
execute the modal/fallback rendering (refer to handleDocPreview and the non-PDF
modal state/handler used later in the file).
- Around line 1294-1302: The preview modal needs proper dialog semantics: on the
modal container element (the div with classes "relative flex flex-col ...
max-w-4xl" shown when showDocPreview is true) add role="dialog" and
aria-modal="true", and wire an accessible label by adding an id to the filename
span (e.g., id="docPreviewTitle") and setting aria-labelledby="docPreviewTitle"
on the modal container; keep existing closeDocPreview click handler and
docPreviewFilename binding intact so screen readers announce the title and the
modal is exposed correctly.

In `@app/pages/jobs/`[slug]/apply.vue:
- Line 70: Replace the hardcoded client-side file size limit (const maxSize = 10
* 1024 * 1024 in app/pages/jobs/[slug]/apply.vue and the similar checks around
lines 117-122) with the shared MAX_FILE_SIZE constant used by the backend:
add/import the shared constant (e.g., MAX_FILE_SIZE from your shared/config or
constants module) and use that value for all client-side validations (the
maxSize variable and any file-size checks) so client and server limits stay
synchronized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 25d49ec8-01a0-4134-b6f8-ac320582c1c6

📥 Commits

Reviewing files that changed from the base of the PR and between f7f79e8 and 8880b45.

📒 Files selected for processing (21)
  • .github/instructions/snyk_rules.instructions.md
  • app/composables/useJob.ts
  • app/composables/useJobs.ts
  • app/pages/dashboard/jobs/[id]/application-form.vue
  • app/pages/dashboard/jobs/[id]/index.vue
  • app/pages/dashboard/jobs/new.vue
  • app/pages/jobs/[slug]/apply.vue
  • playwright.config.ts
  • 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/0008_nervous_stone_men.sql
  • server/database/migrations/0009_organic_gauntlet.sql
  • server/database/migrations/meta/0008_snapshot.json
  • server/database/migrations/meta/0009_snapshot.json
  • server/database/migrations/meta/_journal.json
  • server/database/schema/app.ts
  • server/utils/schemas/job.ts
  • server/utils/schemas/publicApplication.ts
💤 Files with no reviewable changes (1)
  • .github/instructions/snyk_rules.instructions.md

Comment thread app/pages/dashboard/jobs/[id]/application-form.vue
Comment thread app/pages/dashboard/jobs/new.vue
Comment thread app/pages/jobs/[slug]/apply.vue
Comment thread playwright.config.ts Outdated
Comment on lines +153 to +164
columns: { id: true, organizationId: true, requireResume: true, requireCoverLetter: true },
})

if (!existingJob) {
throw createError({ statusCode: 404, statusMessage: 'Job not found or not accepting applications' })
}

// Validate required resume
if (existingJob.requireResume && !resumeUpload) {
throw createError({ statusCode: 422, statusMessage: 'Resume/CV is required for this position' })
}

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

requireCoverLetter is retrieved but never enforced.

At Line 153 you fetch requireCoverLetter, but Line 160-163 validates only resume. A job configured to require a cover letter still accepts empty coverLetterText.

💡 Suggested fix
   if (existingJob.requireResume && !resumeUpload) {
     throw createError({ statusCode: 422, statusMessage: 'Resume/CV is required for this position' })
   }
+  if (existingJob.requireCoverLetter && !coverLetterText?.trim()) {
+    throw createError({ statusCode: 422, statusMessage: 'Cover letter is required for this position' })
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/api/public/jobs/`[slug]/apply.post.ts around lines 153 - 164, The code
fetches existingJob.requireCoverLetter but never enforces it; add a validation
after the resume check that throws createError({ statusCode: 422, statusMessage:
'Cover letter is required for this position' }) when
existingJob.requireCoverLetter is true and there is no coverLetterText and no
coverLetterUpload (i.e., ensure the check uses existingJob.requireCoverLetter,
coverLetterText, coverLetterUpload and createError to reject missing cover
letters).

Comment thread server/api/public/jobs/[slug]/apply.post.ts
…n field types and improve job publishing steps
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-66 March 4, 2026 11:08 Destroyed
Copy link
Copy Markdown

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
e2e/critical-flows/candidate-application.spec.ts (1)

26-31: ⚠️ Potential issue | 🟠 Major

Use run-unique applicant identity and deterministic row targeting.

Static applicant data plus name-based selection can pick old records and produce false positives in shared datasets.

🧪 Suggested stabilization diff
-const APPLICANT = {
+const BASE_APPLICANT = {
   firstName: 'Jane',
   lastName: 'Doe',
   email: 'jane.doe@example.com',
   phone: '+49 170 1234567',
 }
@@
-    await candidatePage.getByLabel('First name').fill(APPLICANT.firstName)
-    await candidatePage.getByLabel('Last name').fill(APPLICANT.lastName)
-    await candidatePage.getByLabel('Email').fill(APPLICANT.email)
-    await candidatePage.getByLabel('Phone').fill(APPLICANT.phone)
+    const applicant = {
+      ...BASE_APPLICANT,
+      email: `jane.doe+${Date.now()}@example.com`,
+    }
+    await candidatePage.getByLabel('First name').fill(applicant.firstName)
+    await candidatePage.getByLabel('Last name').fill(applicant.lastName)
+    await candidatePage.getByLabel('Email').fill(applicant.email)
+    await candidatePage.getByLabel('Phone').fill(applicant.phone)
@@
-    const appRow = page.getByText(`${APPLICANT.firstName} ${APPLICANT.lastName}`).first()
+    const appRow = page.getByRole('row').filter({ hasText: applicant.email }).first()
     await expect(appRow).toBeVisible({ timeout: 15_000 })

Also applies to: 221-224, 431-434

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

In `@e2e/critical-flows/candidate-application.spec.ts` around lines 26 - 31, The
test uses a static APPLICANT object which can collide with existing records;
update the APPLICANT definition to generate a run-unique identity (e.g., append
Date.now() or a short UUID to email and phone) so each test creates a distinct
applicant, and change any DOM/table targeting that currently relies on
name-based selection to instead locate the row by that unique email (or by a
test-only data attribute or the created record ID returned from the create flow)
so selection is deterministic; apply this change for the APPLICANT usage and the
other occurrences referenced (the blocks around the current name-based
selectors) so all assertions/querying use the unique email or explicit
id/test-id instead of first/last name.
🧹 Nitpick comments (2)
e2e/critical-flows/job-creation.spec.ts (1)

60-63: Consider stricter application-link parsing before deriving slug.

Current split logic works, but a pathname-shaped assertion makes failures clearer and avoids accidental partial matches.

Proposed refinement
-    const applicationLink = await page.locator('input[readonly]').inputValue()
-    expect(applicationLink).toContain('/jobs/')
-    const jobSlug = applicationLink.split('/jobs/')[1]?.split('/apply')[0] ?? ''
+    const applicationLink = await page.locator('input[readonly]').inputValue()
+    expect(applicationLink).toMatch(/\/jobs\/[^/]+\/apply(?:$|[?#])/)
+    const match = applicationLink.match(/\/jobs\/([^/]+)\/apply(?:$|[?#])/)
+    const jobSlug = match?.[1] ?? ''
     expect(jobSlug.length, 'Job slug must not be empty').toBeGreaterThan(0)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/critical-flows/job-creation.spec.ts` around lines 60 - 63, The current
logic that derives jobSlug from applicationLink is fragile; instead parse and
assert the full pathname shape before extracting the slug: use the
applicationLink value (from page.locator('input[readonly]').inputValue()) to
construct a URL or match a strict regex that ensures pathname starts with
'/jobs/' and ends with '/apply' (e.g. assert pathname matches
/^\/jobs\/[^\/]+\/apply$/), then reliably extract jobSlug from the pathname
segment between '/jobs/' and '/apply' and keep the existing non-empty assertion
on jobSlug.
e2e/critical-flows/candidate-application.spec.ts (1)

258-273: Scope file upload to the intended question instead of a global file-input selector.

Using input[type="file"] globally is brittle once another upload control exists on the form.

♻️ Suggested locator tightening
-    await candidatePage.locator('input[type="file"]').setInputFiles({
+    const coverLetterUploadSection = candidatePage
+      .locator('section,div,fieldset')
+      .filter({ hasText: 'Cover letter document' })
+      .first()
+    await coverLetterUploadSection.locator('input[type="file"]').setInputFiles({
       name: 'cover-letter.pdf',
       mimeType: 'application/pdf',
       buffer: pdfBuffer,
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/critical-flows/candidate-application.spec.ts` around lines 258 - 273, The
global file-input selector is brittle; scope the upload to the specific custom
question element instead of using candidatePage.locator('input[type="file"]')
directly. Locate the question container (e.g., by its question label text,
aria-label, role, or a data-testid unique to that custom question) and call
container.locator('input[type="file"]') to call setInputFiles with the existing
pdfBuffer payload; update the locator usage surrounding pdfBuffer and the
setInputFiles call so only the intended question's input is targeted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/critical-flows/candidate-application.spec.ts`:
- Around line 161-167: Add a positive coverage path that flips the "Require
resume/CV" and the "Require cover letter" toggles ON (use the same resumeToggle
located via page.getByRole('button', { name: /Require resume\/CV/i }) and the
analogous coverLetter toggle located via page.getByRole('button', { name:
/Require cover letter/i })), assert each toggle's aria-pressed becomes 'true',
then attempt to submit the candidate form (use the existing submit control,
e.g., the page.getByRole('button', { name: /Submit/i }) or form submit flow)
without supplying the required resume file or cover-letter text and verify the
UI shows validation errors/prevents submission; this ensures required-field
behavior is enforced rather than bypassed.

---

Outside diff comments:
In `@e2e/critical-flows/candidate-application.spec.ts`:
- Around line 26-31: The test uses a static APPLICANT object which can collide
with existing records; update the APPLICANT definition to generate a run-unique
identity (e.g., append Date.now() or a short UUID to email and phone) so each
test creates a distinct applicant, and change any DOM/table targeting that
currently relies on name-based selection to instead locate the row by that
unique email (or by a test-only data attribute or the created record ID returned
from the create flow) so selection is deterministic; apply this change for the
APPLICANT usage and the other occurrences referenced (the blocks around the
current name-based selectors) so all assertions/querying use the unique email or
explicit id/test-id instead of first/last name.

---

Nitpick comments:
In `@e2e/critical-flows/candidate-application.spec.ts`:
- Around line 258-273: The global file-input selector is brittle; scope the
upload to the specific custom question element instead of using
candidatePage.locator('input[type="file"]') directly. Locate the question
container (e.g., by its question label text, aria-label, role, or a data-testid
unique to that custom question) and call container.locator('input[type="file"]')
to call setInputFiles with the existing pdfBuffer payload; update the locator
usage surrounding pdfBuffer and the setInputFiles call so only the intended
question's input is targeted.

In `@e2e/critical-flows/job-creation.spec.ts`:
- Around line 60-63: The current logic that derives jobSlug from applicationLink
is fragile; instead parse and assert the full pathname shape before extracting
the slug: use the applicationLink value (from
page.locator('input[readonly]').inputValue()) to construct a URL or match a
strict regex that ensures pathname starts with '/jobs/' and ends with '/apply'
(e.g. assert pathname matches /^\/jobs\/[^\/]+\/apply$/), then reliably extract
jobSlug from the pathname segment between '/jobs/' and '/apply' and keep the
existing non-empty assertion on jobSlug.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: baaf5bf9-b72d-4bcc-957a-be224567657c

📥 Commits

Reviewing files that changed from the base of the PR and between 8880b45 and eb0afe5.

📒 Files selected for processing (2)
  • e2e/critical-flows/candidate-application.spec.ts
  • e2e/critical-flows/job-creation.spec.ts

Comment on lines +161 to +167
// "Require resume/CV" defaults to ON — toggle it off so the candidate flow
// does not need a resume upload (the file_upload custom question covers files)
await page.getByRole('button', { name: /Require resume\/CV/i }).waitFor({ state: 'visible', timeout: 10_000 })
const resumeToggle = page.getByRole('button', { name: /Require resume\/CV/i })
if ((await resumeToggle.getAttribute('aria-pressed')) === 'true') {
await resumeToggle.click()
}
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

Critical flow bypasses the resume/cover-letter requirement path.

This path explicitly turns resume requirement off and never enables/verifies cover-letter requirement behavior. The test can pass while the PR’s core requirement toggles regress. Please add a coverage path (here or sibling spec) that enables these toggles and validates required-field submit behavior.

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

In `@e2e/critical-flows/candidate-application.spec.ts` around lines 161 - 167, Add
a positive coverage path that flips the "Require resume/CV" and the "Require
cover letter" toggles ON (use the same resumeToggle located via
page.getByRole('button', { name: /Require resume\/CV/i }) and the analogous
coverLetter toggle located via page.getByRole('button', { name: /Require cover
letter/i })), assert each toggle's aria-pressed becomes 'true', then attempt to
submit the candidate form (use the existing submit control, e.g., the
page.getByRole('button', { name: /Submit/i }) or form submit flow) without
supplying the required resume file or cover-letter text and verify the UI shows
validation errors/prevents submission; this ensures required-field behavior is
enforced rather than bypassed.

@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-66 March 4, 2026 11:19 Destroyed
Copy link
Copy Markdown

@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.

Caution

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

⚠️ Outside diff range comments (1)
server/api/public/jobs/[slug]/apply.post.ts (1)

332-338: ⚠️ Potential issue | 🔴 Critical

Avoid persisting the application before required resume storage succeeds.

Line 332 creates the application before Line 436–490 uploads the built-in resume. If resume storage fails (Line 489), the API returns error but the application row already exists; retries then hit duplicate-application conflicts.

Please either (1) move built-in resume persistence before application creation, or (2) rollback the created application on resume upload failure.

Also applies to: 436-490

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

In `@server/api/public/jobs/`[slug]/apply.post.ts around lines 332 - 338, The code
currently inserts the application via db.insert(...).values(...) (creating
newApplication) before attempting to store the built-in resume, which can leave
a dangling application if resume upload fails; change this by either (preferred)
building and uploading the resume first and only calling
db.insert(application).values(...) once you have the resume storage result
(include the resume id/URL in the application payload), or wrap the insert and
resume upload in a single transaction and roll back/delete the application on
upload failure (catch errors after db.insert and delete the created application
by id/newApplication.id or abort the transaction); update the logic around the
application variable and the resume upload block so the resume storage result is
available at insert time or ensure a reliable rollback on failure.
♻️ Duplicate comments (1)
app/pages/dashboard/jobs/new.vue (1)

261-261: ⚠️ Potential issue | 🔴 Critical

Fix submit handler signature mismatch at form binding.

Line 450 passes the form's SubmitEvent into handleSubmit, but Line 261 expects a 'publish' | 'draft' mode string. This causes a TypeScript TS2345 error and runtime misbehavior where the event object fails mode comparisons, preventing the intended publish/draft logic from executing. Line 382 shows the correct pattern with explicit parameters.

Fix
-          <form `@submit.prevent`="handleSubmit" class="p-6 md:p-8">
+          <form `@submit.prevent`="() => handleSubmit()" class="p-6 md:p-8">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` at line 261, The submit handler signature
is wrong for the form binding: update the async function handleSubmit to accept
the form SubmitEvent as the first parameter and an optional mode as the second
(or a union for the first arg), e.g. handleSubmit(eventOrMode, mode =
publishChoice.value), detect when the first arg is a SubmitEvent (call
preventDefault()) versus a mode string, and then use the resolved mode for the
publish/draft logic so calls that pass the event (from the form) and calls that
pass a mode both work; reference the existing handleSubmit function and
publishChoice.value to implement this normalization.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@server/api/public/jobs/`[slug]/apply.post.ts:
- Around line 332-338: The code currently inserts the application via
db.insert(...).values(...) (creating newApplication) before attempting to store
the built-in resume, which can leave a dangling application if resume upload
fails; change this by either (preferred) building and uploading the resume first
and only calling db.insert(application).values(...) once you have the resume
storage result (include the resume id/URL in the application payload), or wrap
the insert and resume upload in a single transaction and roll back/delete the
application on upload failure (catch errors after db.insert and delete the
created application by id/newApplication.id or abort the transaction); update
the logic around the application variable and the resume upload block so the
resume storage result is available at insert time or ensure a reliable rollback
on failure.

---

Duplicate comments:
In `@app/pages/dashboard/jobs/new.vue`:
- Line 261: The submit handler signature is wrong for the form binding: update
the async function handleSubmit to accept the form SubmitEvent as the first
parameter and an optional mode as the second (or a union for the first arg),
e.g. handleSubmit(eventOrMode, mode = publishChoice.value), detect when the
first arg is a SubmitEvent (call preventDefault()) versus a mode string, and
then use the resolved mode for the publish/draft logic so calls that pass the
event (from the form) and calls that pass a mode both work; reference the
existing handleSubmit function and publishChoice.value to implement this
normalization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 26f03efb-a578-4d08-97c4-f6a786bd91b8

📥 Commits

Reviewing files that changed from the base of the PR and between eb0afe5 and 41ed76f.

📒 Files selected for processing (4)
  • app/pages/dashboard/jobs/[id]/application-form.vue
  • app/pages/dashboard/jobs/new.vue
  • app/pages/jobs/[slug]/apply.vue
  • server/api/public/jobs/[slug]/apply.post.ts

- Fix TypeScript error in new.vue: @submit.prevent handler bound directly to
  handleSubmit(mode) which caused SubmitEvent to be inferred as 'publish'|'draft';
  wrap in arrow function so no args are passed
- Add MinIO (bitnami/minio) as a service container in the E2E CI workflow so
  file uploads (resume + custom file_upload question) have a working S3-compatible
  backend during Playwright runs
- Increase Playwright test timeout to 120s in CI (from 30s) and raise
  actionTimeout/navigationTimeout to match the long multi-step job creation
  and candidate application flows

Signed-off-by: Joachim <joachim.l.kolle.pers@gmail.com>
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-66 March 4, 2026 12:10 Destroyed
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (3)
playwright.config.ts (1)

55-55: Consider making local webServer timeout configurable.

Line 55 is reasonable, but allowing an env override can reduce local startup flakes on slower machines.

💡 Optional tweak
         webServer: {
           command: 'npm run dev',
           url: 'http://localhost:3000',
           reuseExistingServer: true,
-          timeout: 60_000,
+          timeout: Number(process.env.PLAYWRIGHT_WEBSERVER_TIMEOUT ?? 60_000),
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@playwright.config.ts` at line 55, The local Playwright webServer timeout is
hardcoded as timeout: 60_000; make it configurable by reading an env var (e.g.,
process.env.LOCAL_WEBSERVER_TIMEOUT), parse it to an integer with a safe
fallback to 60000, and use that value in the webServer.timeout field; update the
config where the webServer object and its timeout property are defined so the
code uses the parsed env value (keeping the numeric fallback) to reduce startup
flakes on slower machines.
app/pages/dashboard/jobs/new.vue (2)

450-450: Simplify the form submit handler.

The wrapper function () => handleSubmit() is unnecessary since handleSubmit already uses a default parameter. You can pass the function reference directly.

♻️ Simplify handler
-          <form `@submit.prevent`="() => handleSubmit()" class="p-6 md:p-8">
+          <form `@submit.prevent`="handleSubmit" class="p-6 md:p-8">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` at line 450, The form submit currently uses
an unnecessary wrapper `@submit.prevent`="() => handleSubmit()"; replace it with a
direct reference `@submit.prevent`="handleSubmit" to simplify the handler since
the existing handleSubmit function already accepts defaults; update the template
where the <form> uses the wrapper so it directly binds to handleSubmit.

333-342: Consider a gentler fallback for clipboard failures.

Using alert() as a fallback works but can be jarring. Since the link is already visible in a selectable input field above (line 805-810), consider just focusing that input or showing a toast message instead.

♻️ Alternative fallback approach
 async function copyFinalLink() {
   try {
     await navigator.clipboard.writeText(finalApplicationLink.value)
     linkCopiedFinal.value = true
     setTimeout(() => { linkCopiedFinal.value = false }, 3000)
   } catch {
-    // fallback
-    alert(finalApplicationLink.value)
+    // Fallback: select the input text so user can Ctrl+C
+    const input = document.querySelector('input[readonly][value]') as HTMLInputElement | null
+    input?.select()
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/dashboard/jobs/new.vue` around lines 333 - 342, The fallback in
copyFinalLink() currently uses alert(finalApplicationLink.value); change it to a
gentler UI action: in the catch block, focus and select the existing input that
displays the link (add a template ref like finalLinkInput to that input and
reference it in the script), and/or trigger a toast message (e.g.,
showToast("Unable to copy to clipboard — the link is selected for you")). Update
copyFinalLink() to call finalLinkInput?.focus(); finalLinkInput?.select(); and
set linkCopiedFinal.value or call the app's toast helper instead of using
alert().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/pages/dashboard/jobs/new.vue`:
- Line 450: The form submit currently uses an unnecessary wrapper
`@submit.prevent`="() => handleSubmit()"; replace it with a direct reference
`@submit.prevent`="handleSubmit" to simplify the handler since the existing
handleSubmit function already accepts defaults; update the template where the
<form> uses the wrapper so it directly binds to handleSubmit.
- Around line 333-342: The fallback in copyFinalLink() currently uses
alert(finalApplicationLink.value); change it to a gentler UI action: in the
catch block, focus and select the existing input that displays the link (add a
template ref like finalLinkInput to that input and reference it in the script),
and/or trigger a toast message (e.g., showToast("Unable to copy to clipboard —
the link is selected for you")). Update copyFinalLink() to call
finalLinkInput?.focus(); finalLinkInput?.select(); and set linkCopiedFinal.value
or call the app's toast helper instead of using alert().

In `@playwright.config.ts`:
- Line 55: The local Playwright webServer timeout is hardcoded as timeout:
60_000; make it configurable by reading an env var (e.g.,
process.env.LOCAL_WEBSERVER_TIMEOUT), parse it to an integer with a safe
fallback to 60000, and use that value in the webServer.timeout field; update the
config where the webServer object and its timeout property are defined so the
code uses the parsed env value (keeping the numeric fallback) to reduce startup
flakes on slower machines.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 71488ab6-92c9-4e53-8b4a-ca75b9d68f28

📥 Commits

Reviewing files that changed from the base of the PR and between 41ed76f and 693308a.

📒 Files selected for processing (3)
  • .github/workflows/e2e-tests.yml
  • app/pages/dashboard/jobs/new.vue
  • playwright.config.ts

@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-66 March 4, 2026 12:48 Destroyed
Copy link
Copy Markdown

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/e2e-tests.yml:
- Around line 37-45: The workflow uses a mutable image tag
"bitnami/minio:latest" and a health check that calls curl (options: --health-cmd
"curl -f http://localhost:9000/minio/health/live"), which makes CI
non-deterministic and brittle; replace the image with a pinned digest form
"bitnami/minio@sha256:<digest>" (obtain the digest by pulling the image locally
and inspecting RepoDigests) and update the health check to use the MinIO client
(mc) command shipped in the image instead of curl (e.g., an mc admin/health or
mc alias and stat/check invocation) so the healthcheck reliably works without
curl.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dc2416eb-3fbd-4c0b-9199-fac2696cbaeb

📥 Commits

Reviewing files that changed from the base of the PR and between 693308a and ae933a0.

📒 Files selected for processing (1)
  • .github/workflows/e2e-tests.yml

Comment thread .github/workflows/e2e-tests.yml Outdated
Comment on lines +37 to +45
image: bitnami/minio:latest
env:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
MINIO_DEFAULT_BUCKETS: reqcore
ports:
- 9000:9000
options: >-
--health-cmd "curl -f http://localhost:9000/minio/health/live"
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

🌐 Web query:

bitnami/minio Docker image pinning version CI best practices

💡 Result:

CI best practices for pinning bitnami/minio (Bitnami) Docker images

  1. Don’t use :latest in CI for anything you need to reproduce

    • Tags are mutable; the same tag can later point to a different image, causing non-reproducible builds/tests and “it worked yesterday” CI failures. Prefer digests (immutable). [1][2]
  2. Pin by digest (optionally keep a tag for readability)

    • Use:
      • bitnami/minio@sha256:<digest> (or bitnamisecure/...@sha256:<digest>, or your mirror)
    • You can keep the tag alongside digest for humans, but the digest is what makes it reproducible. [1][2]
  3. Plan around Bitnami’s post‑2025 distribution changes (important for bitnami/minio)

    • Since Aug 28, 2025, Bitnami’s free/public availability changed: community access is focused on a limited set of images and “latest”-only, while versioned/older tags moved to bitnamilegacy (no updates/support) or to paid offerings; this has already broken real CI pipelines pulling versioned bitnami/minio:<version>-debian-... tags. [3][4]
    • Practical implication: if you rely on version tags from docker.io/bitnami, expect breakage or staleness; treat bitnamilegacy as a temporary bridge only. [3][4]
  4. Mirror the exact digest you tested into your own registry

    • Best practice for CI reliability: resolve and pull the upstream image once, then push the same digest to an internal registry (GHCR/ECR/GCR/Artifactory) and have CI pull from there. This protects you from upstream tag moves/removals (which Bitnami changes made more likely). [3][4]
  5. Add supply-chain verification + scanning gates

    • Bitnami has historically recommended verifying publisher/integrity via Docker Content Trust (DCT) for images on Docker Hub; in CI, pair digest-pinning with signature verification where applicable and vulnerability scanning before promotion. [5][1]
  6. Automate updates intentionally (don’t “accidentally upgrade”)

    • Use a scheduled job/renovation bot that:
      1. checks for an allowed update,
      2. updates the pinned digest,
      3. runs integration tests,
      4. promotes the new digest to your mirror only if green. (Digest pinning prevents silent drift between those runs.) [2][1]

References

  • [1] Google Kubernetes Engine docs: why tags are mutable; why digests avoid unexpected deploy results
  • [2] Chainguard Academy: reproducibility and why digest pinning is recommended
  • [3] Bitnami announcement (GitHub issue) on catalog changes effective Aug 28, 2025 (legacy migration, latest-only community images)
  • [4] Velero issue showing CI breakage from pulling a versioned bitnami/minio:<...> tag no longer available
  • [5] Bitnami blog: recommendation to enable Docker Content Trust (image signing / integrity verification)

🌐 Web query:

bitnami/minio image curl available healthcheck

💡 Result:

For bitnami/minio, don’t rely on curl being present for a Docker HEALTHCHECK (recent MinIO images have dropped curl, so healthcheck: ["CMD","curl", ...] can break). [2]

Recommended healthcheck for bitnami/minio: use the bundled mc client (Bitnami documents that mc is shipped in the image). [1]

healthcheck:
  test: ["CMD", "mc", "ready", "local"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 10s

If you specifically want an HTTP probe, MinIO exposes unauthenticated health endpoints like /minio/health/live (returns 200 when online), but run the probe using a tool you know exists (e.g., mc as above, or a separate tiny curl container). [3]

Sources: [1] [2] [3]


🌐 Web query:

bitnami/minio latest versions tags digest strategy

💡 Result:

What’s going on with bitnami/minio “latest versions / tags” (as of Mar 4, 2026)

  • On Docker Hub, bitnami/minio is now positioned as a Bitnami Secure Image and the repository currently reports no published tags (i.e., you can’t browse/pin :RELEASE... or :x.y.z tags there). It also explicitly points users to the Bitnami Legacy registry for the older Debian-based images. [1]
  • Bitnami’s catalog change (effective Aug 28, 2025) explains why: community access is generally limited to a smaller set of images and (for community-tier) only the moving :latest tag, while older/versioned tags are moved to a legacy repository. [2]
  • This has already caused real-world breakage where pipelines referenced older pinned tags that are no longer available from docker.io/bitnami/... (example: a workflow pulling bitnami/minio:2021.6.17-debian-10-r7). [3]

Recommended tag/digest strategy

  1. Don’t rely on :latest for production rollouts (it’s a rolling tag that can change underneath you). [4]
  2. Pin by digest for reproducibility:
    • Kubernetes: image: bitnami/minio@sha256:<digest>
    • Docker: docker pull bitnami/minio:latest then read the pulled digest via docker image inspect (look at RepoDigests) and deploy using @sha256:....
  3. If you must use human-readable version tags:
    • You’ll likely need to pull them from the Bitnami Legacy repository (but expect no ongoing security patches there per the policy change), or
    • Switch to another distribution/source that still publishes immutable version tags.

Common confusion: bitnamicharts/minio

bitnamicharts/minio on Docker Hub is a Helm chart artifact published as an OCI image, so its tags (often sha256-...) are not the same thing as the bitnami/minio runtime container tags. [5]

Sources: [1] [2] [3] [4] [5]


Pin MinIO image by digest and use healthcheck that doesn't rely on curl.

Line 37 uses bitnami/minio:latest, which causes non-deterministic CI builds since the tag is mutable and can point to different image digests over time. Pin by digest instead: bitnami/minio@sha256:<digest>.

Line 45 assumes curl is available in the image, but recent MinIO images have dropped curl, breaking the healthcheck command. Use the bundled mc client instead:

Suggested fix
      minio:
-       image: bitnami/minio:latest
+       image: bitnami/minio@sha256:<resolved-digest>
        env:
          MINIO_ROOT_USER: minioadmin
          MINIO_ROOT_PASSWORD: minioadmin
          MINIO_DEFAULT_BUCKETS: reqcore
        ports:
          - 9000:9000
        options: >-
-         --health-cmd "curl -f http://localhost:9000/minio/health/live"
+         --health-cmd "mc ready local"
          --health-interval 5s
          --health-timeout 5s
          --health-retries 10

To find the digest: run docker pull bitnami/minio:latest locally, then docker inspect bitnami/minio | jq -r '.[0].RepoDigests'.

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

In @.github/workflows/e2e-tests.yml around lines 37 - 45, The workflow uses a
mutable image tag "bitnami/minio:latest" and a health check that calls curl
(options: --health-cmd "curl -f http://localhost:9000/minio/health/live"), which
makes CI non-deterministic and brittle; replace the image with a pinned digest
form "bitnami/minio@sha256:<digest>" (obtain the digest by pulling the image
locally and inspecting RepoDigests) and update the health check to use the MinIO
client (mc) command shipped in the image instead of curl (e.g., an mc
admin/health or mc alias and stat/check invocation) so the healthcheck reliably
works without curl.

@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-66 March 4, 2026 13:54 Destroyed
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-66 March 4, 2026 14:36 Destroyed
@railway-app railway-app Bot temporarily deployed to applirank / reqcore-pr-66 March 4, 2026 14:58 Destroyed
@JoachimLK JoachimLK merged commit f3163b0 into main Mar 4, 2026
5 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