From a17886e58be0131323e80eee426ef0ae1c1bc235 Mon Sep 17 00:00:00 2001 From: Matthew Jackson Date: Fri, 6 Mar 2026 20:47:15 -0800 Subject: [PATCH] Fix Ashby resume upload and add validation error logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ashby wraps file input inside #_systemfield_resume container — search for the actual input[type="file"] element. Also capture and log validation errors from the page when submit returns incomplete. Co-Authored-By: Claude Opus 4.6 --- job_applier.mjs | 3 +++ lib/apply/ashby.mjs | 12 ++++++++---- lib/apply/generic.mjs | 14 ++++++++++++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/job_applier.mjs b/job_applier.mjs index e8f09c5..448dcf4 100644 --- a/job_applier.mjs +++ b/job_applier.mjs @@ -399,6 +399,9 @@ async function handleResult(job, result, results, settings, profile, apiKey) { case 'stuck': case 'incomplete': { + if (result.validation_errors?.length) { + console.log(` ⚠️ Validation errors: ${result.validation_errors.join(' | ')}`); + } const retries = (job.retry_count || 0) + 1; const maxRetry = settings.max_retries ?? DEFAULT_MAX_RETRIES; if (retries <= maxRetry) { diff --git a/lib/apply/ashby.mjs b/lib/apply/ashby.mjs index 5149453..c7570ef 100644 --- a/lib/apply/ashby.mjs +++ b/lib/apply/ashby.mjs @@ -13,10 +13,14 @@ export async function apply(page, job, formFiller) { submitSelector: 'button:has-text("Submit Application")', verifySelector: '#_systemfield_name', beforeSubmit: async (page, formFiller) => { - const resumeInput = await page.$('#_systemfield_resume'); - if (resumeInput && formFiller.profile.resume_path) { - await resumeInput.setInputFiles(formFiller.profile.resume_path).catch(() => {}); - await page.waitForTimeout(1000); + if (!formFiller.profile.resume_path) return; + // Ashby wraps resume upload in a custom component — find the actual file input + const fileInput = await page.$('#_systemfield_resume input[type="file"]') || + await page.$('input[type="file"][name*="resume"]') || + await page.$('input[type="file"]'); + if (fileInput) { + await fileInput.setInputFiles(formFiller.profile.resume_path).catch(() => {}); + await page.waitForTimeout(1500); } }, }); diff --git a/lib/apply/generic.mjs b/lib/apply/generic.mjs index 29e9232..cbe4187 100644 --- a/lib/apply/generic.mjs +++ b/lib/apply/generic.mjs @@ -154,19 +154,29 @@ async function fillAndSubmit(page, job, formFiller, meta, opts) { const verifySelector = opts.verifySelector || 'form button[type="submit"]:not([disabled])'; const postSubmit = await page.evaluate((vs) => { const text = (document.body.innerText || '').toLowerCase(); + // Check for validation errors (red text, error messages) + const errorEls = document.querySelectorAll('[class*="error"], [class*="invalid"], [role="alert"]'); + const validationErrors = Array.from(errorEls) + .map(el => el.innerText?.trim()) + .filter(t => t && t.length < 200) + .slice(0, 3); return { hasSuccess: text.includes('application submitted') || text.includes('successfully applied') || text.includes('thank you') || text.includes('application received') || text.includes('application has been') || text.includes('we received your'), hasForm: !!document.querySelector(vs), + validationErrors, }; - }, verifySelector).catch(() => ({ hasSuccess: false, hasForm: false })); + }, verifySelector).catch(() => ({ hasSuccess: false, hasForm: false, validationErrors: [] })); if (postSubmit.hasSuccess || !postSubmit.hasForm) { return { status: 'submitted', meta }; } - return { status: 'incomplete', meta }; + return { + status: 'incomplete', meta, + ...(postSubmit.validationErrors?.length && { validation_errors: postSubmit.validationErrors }), + }; } // Multi-step: Next/Continue