diff --git a/AGENTS.md b/AGENTS.md index 0af5ca3..1a45183 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ ScoutLane is a Next.js 16 recruitment platform using React 19, Prisma, and TypeScript. Main app routes live in `src/app`, reusable UI in `src/components`, shared utilities in `src/lib`, server-side workflows in `src/server`, validation schemas in `src/schemas`, and shared types in `src/types`. Prisma schema, migrations, and seed data are in `prisma`. Static assets belong in `public`. Unit and integration tests are colocated as `*.test.ts` or `*.test.tsx`; browser tests live in `tests/e2e`. -Read `docs/HANDOFF.md` before larger changes. Add or update `docs/session-reports/*.md` when a task changes architecture, auth, parsing, email, subscriptions, or deployment behavior. +Read `docs/handoff/HANDOFF.md` (the handoff-tree father) before larger changes — or invoke the `handoff-context` skill to load full context via Explore/Plan subagents. Record session work in the handoff tree at `docs/handoff/-/HANDOFF.md` and update the father's `## Current state` + `## Session index` (append, never overwrite). This supersedes the old `docs/HANDOFF.md` + `docs/session-reports/`. ## Build, Test, and Development Commands diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md deleted file mode 100644 index bb49163..0000000 --- a/docs/HANDOFF.md +++ /dev/null @@ -1,127 +0,0 @@ -# ScoutLane Handoff - -Last updated: 2026-05-20 - -## Read This First - -Use this file as the first stop for any new agent session. It is the fast index for current repo state, open issues, session history, and reusable local context. - -Current snapshot: - -| Item | Value | -|---|---| -| Branch | `main` tracking `origin/main` | -| Latest observed commit | `11c4521 fix applicant deletion and resume submission handling` | -| Recently merged PR | `#69 fix: improve resume parsing and handoff docs` | -| Main next task | Fix OpenRouter model fallback parsing and add the AI Engineer seed job/template | -| Working tree note | App fixes for applicant deletion, submission handling, upload guard, custom fields, and parse-now were pushed in `11c4521`; generated `playwright-report/` remains local-only | -| Local app note | Recent local testing used `http://localhost:3009`; use `localhost`, not `127.0.0.1`, for Next dev browser checks | -| Primary test email | `ridi.pillaca@gmail.com` | -| Test resume | `C:\Users\a2021\OneDrive\Escritorio\2026\Toronto\RichardPillaca_RESUME.pdf` | -| Job sample source | `C:\Users\a2021\OneDrive\Escritorio\Vibe projects workspace\PROYECTOS\Documentation\Scoutlane\job post sample.md` | - -## Open Issues - -| Priority | Area | Symptom | Next Diagnostic Step | Source | -|---|---|---|---|---| -| P1 | Resume parsing | Fixed locally on 2026-05-26: retry parsing now loads app-owned `/api/resumes/...` files directly from local/database storage, and OpenRouter falls back through `openrouter/owl-alpha`, `openrouter/free`, and `openrouter/auto`. | Re-run preview smoke after deploy; keep `OPENROUTER_MODEL=openrouter/owl-alpha` or another known-working model. | `2026-05-26-codex-fixes-notifications-hardening` | -| P1 | AI Engineer seed data | The external job sample has not been added to seed data. | Add one `AI Engineer, Global Security` active job plus one reusable AI Engineer template in `prisma/seed.ts`, based on the sample but branded as ScoutLane demo data. | `2026-05-20-parsing-ai-engineer-plan` | -| P2 | Application submission E2E | Browser submit on `localhost:3009` POSTed successfully and showed application success, but parsing warning remains because of OpenRouter model config. | Re-run after model config fix and email env config. | `2026-05-20-parsing-performance-test` | -| P2 | Email confirmation | Resolved 2026-05-26: `EMAIL_FROM` changed from unverified `noreply@scoutlane.local` to `ScoutLane `. Local dev emails now send via Resend's shared test domain. Free tier limitation: emails only deliver to the Resend account owner's email. | Production needs a verified custom domain. | `2026-05-20-parsing-performance-test` | -| P2 | Auth/dev login | Dev credentials login can return a session without a persisted `User`, which can break admin DB operations. | Upsert the dev user in `src/lib/auth/auth.ts` sign-in callback; keep Prisma out of `auth.config.ts`. | Review findings | -| P2 | Job creation | New-job form fields `slug`, `department`, `whatYouWillDo`, `requirements`, and `toolsAndSkills` can be dropped during parsing. | Add those fields to the `safeParse` input in `createJobImpl`. | Review findings | -| P2 | Public application form | Required custom application fields and select options were fixed in `11c4521`. | Retest on the deployed app after Vercel finishes building. | `11c4521` | -| P2 | Auth/email/notifications | Google auth, email OTP verification, confirmed job alerts, and email-only notifications are planned but not implemented. | Implement the auth/email plan from the session index before production testing. | `2026-05-20-auth-email-notifications-plan` | -| P3 | Lint | `pnpm lint` previously passed with an existing custom font warning in `src/app/layout.tsx`. | Fix separately if polishing final quality gates. | `2026-05-20-public-job-readability` | - -## Session Index - -| Date | Session Name | Topic | Status | Commit/Branch | Report | -|---|---|---|---|---|---| -| 2026-05-19 | `2026-05-19-docs-review` | Initial curated docs import and docs branch review | Historical | `docs-update` context | [Report](./session-reports/2026-05-19-docs-review.md) | -| 2026-05-20 | `2026-05-20-public-job-readability` | Public job detail/application readability, form contrast, browser checks | Implemented; parsing issue remains | `9569964` then later `f56df75` observed | [Report](./session-reports/2026-05-20-public-job-readability.md) | -| 2026-05-20 | `2026-05-20-auth-email-notifications-plan` | Plan for Google OAuth, email OTP, job alert subscriptions, email notifications, AI Engineer seed data | Planned, not implemented | `feat/redesign` | [Report](./session-reports/2026-05-20-auth-email-notifications-plan.md) | -| 2026-05-20 | `2026-05-20-handoff-system` | Reworked handoff into fast index plus session reports | Implemented | `feat/redesign` | [Report](./session-reports/2026-05-20-handoff-system.md) | -| 2026-05-20 | `2026-05-20-parsing-performance-test` | Ran parsing-focused tests, fixed PDF extraction for `pdf-parse` v2, collected navigation performance | Implemented; E2E submit needs clean retest | `feat/redesign` | [Report](./session-reports/2026-05-20-parsing-performance-test.md) | -| 2026-05-20 | `2026-05-20-parsing-ai-engineer-plan` | Decision-complete plan for OpenRouter fallback parsing and AI Engineer seed job/template | Planned, not implemented | `main` | [Report](./session-reports/2026-05-20-parsing-ai-engineer-plan.md) | - -## Current Verified Work - -Public job pages were redesigned for readability: - -- `/careers/data-scientist` -- `/careers/devops-engineer` - -Changed files in that work: - -- `src/app/(public)/careers/[slug]/page.tsx` -- `src/components/public/ApplicationForm.tsx` - -Verified commands: - -```bash -pnpm typecheck -pnpm lint -``` - -Results: - -- `pnpm typecheck` passed. -- `pnpm lint` passed with one existing warning in `src/app/layout.tsx` about custom font loading. -- Browser checks confirmed form labels, inputs, upload area, custom fields, and "About ScoutLane" copy are readable. - -## Resume Parsing Diagnostic Checklist - -Expected fixed behavior: - -- Applicant submission stores a valid `resumeUrl`. -- Applicant detail can open or preview the original resume. -- Parsing status moves from `PENDING` to `PARSING` to `COMPLETED`, or to visible `FAILED` with retry. -- `Match to job`, `Education`, and `Work experience` populate after successful parsing. -- Retry parsing works from the admin detail/API route. - -Required diagnostic path: - -1. Submit test applications with PDF, DOC, and DOCX resumes. -2. Inspect created applicants for `resumeUrl`, `parsingStatus`, `parsedData`, `score`, and `data`. -3. Verify the uploaded file URL opens from the server and admin resume viewer. -4. Confirm `enqueueResumeParseJob` runs and creates the expected pg-boss job. -5. Run `pnpm worker:resume` and confirm applicant rows update. -6. Check errors from `extractText`, OpenRouter parsing, match scoring, GCS/local storage, and email separately. -7. Add tests for upload success/failure, enqueue failure, worker parse success/failure, and admin retry. - -## Reusable Context - -Git safe-directory command: - -```bash -git -c safe.directory="C:/Users/a2021/OneDrive/Escritorio/Vibe projects workspace/PROYECTOS/ScoutLane" -``` - -Common verification commands: - -```bash -pnpm typecheck -pnpm lint -pnpm test -pnpm test:e2e -pnpm worker:resume -``` - -Local environment notes: - -- Current untracked local-only path observed on 2026-05-20: `playwright-report/`. -- In sandboxed runs, `pnpm` commands can hit Windows `EPERM` reading `node_modules/.pnpm`; rerun with approved permissions if needed. -- Git may warn about `C:\Users\a2021/.config/git/ignore` permission. The warning is known and not caused by project files. -- A second Next dev server on `3007` previously reported that another server was already running for this directory. -- Worker deployment must run outside Vercel serverless; use a persistent Node runtime for `pnpm worker:resume`. - -## How To Update This Handoff - -For each meaningful work session: - -1. Add one row to the Session Index. -2. Add or update Open Issues. -3. Keep Current Snapshot accurate. -4. Put detailed notes in `docs/session-reports/YYYY-MM-DD-short-topic.md`. -5. Do not bury active blockers only inside old session reports. diff --git a/docs/session-reports/2026-05-19-docs-review.md b/docs/session-reports/2026-05-19-docs-review.md deleted file mode 100644 index 05600ad..0000000 --- a/docs/session-reports/2026-05-19-docs-review.md +++ /dev/null @@ -1,28 +0,0 @@ -# Session Report: Docs Review - -## Goal - -create a branch docs. i will add and modify documenattion, i will adjust the handoff adn claude.md and have everything ready so i cna finish the project. - -## What landed - -- Created the working documentation branch as `docs-update` because `docs` conflicts with existing `docs/api-and-architecture`. -- Merged downloaded Markdown notes and current repo state into `/docs`. -- Added documentation index, architecture, API, stack, integrations, testing, UX protocol, project guide, handoff, and Claude context docs. -- Updated README links and provider wording to point at `/docs` and OpenRouter. -- Updated `.gitignore` so `/docs` documentation can be tracked. - -## What's still open - -- Run full verification from a clean environment: `pnpm lint`, `pnpm typecheck`, `pnpm test -- --run`, `pnpm build`, and `pnpm test:e2e`. -- Commit the documentation cleanup with a `docs:` commit. -- Confirm worker deployment target for `pnpm worker:resume`. - -## Next Recommended Action - -Review the new `/docs` set, then run the verification commands and commit the branch. - -## Risks/Blockers Encountered - -- Git cannot create a literal `docs` branch while `docs/api-and-architecture` exists. -- The repo previously ignored most `/docs/*` files, so `.gitignore` had to change. diff --git a/docs/session-reports/2026-05-20-auth-email-notifications-plan.md b/docs/session-reports/2026-05-20-auth-email-notifications-plan.md deleted file mode 100644 index 1195db6..0000000 --- a/docs/session-reports/2026-05-20-auth-email-notifications-plan.md +++ /dev/null @@ -1,66 +0,0 @@ -# Session Report: 2026-05-20 Auth Email Notifications Plan - -## Goal - -Plan implementation for Google auth, email verification, job-alert subscriptions, email-only notifications, and AI Engineer seed/demo data using `ridi.pillaca@gmail.com` as the test email. - -## What Changed - -- No code was implemented in this planning session. -- The agreed scope is job-alert subscriptions, email-only notifications, and email OTP two-step verification. -- Paid billing subscriptions are out of scope for the first pass. - -## Planned Implementation - -- Google auth: keep Auth.js Google provider, require Google OAuth in production, and upsert users by normalized email in `src/lib/auth/auth.ts`. -- Initial admin: assign `ADMIN` to `INITIAL_ADMIN_EMAIL=ridi.pillaca@gmail.com`. -- Two-step verification: require short-lived email OTP for admin access when the email is listed in `TWO_STEP_EMAILS`. -- Job alerts: change alerts from immediate active state to confirmed subscription before sending new-job notifications. -- Notifications: use Resend and `EmailLog` for verification emails, job alerts, application confirmations, and admin emails. -- Seed data: add an AI Engineer, Global Security job/template based on the external job post sample. - -## Files To Expect - -- `src/lib/auth/auth.ts` -- `src/lib/auth/auth.config.ts` -- `src/lib/email/send.ts` -- `src/server/services/job-alerts.ts` -- `src/server/services/submit-job-application-impl.ts` -- `prisma/schema.prisma` -- `prisma/seed.ts` -- `docs/*` - -## Verification - -Planned commands: - -```bash -pnpm typecheck -pnpm lint -pnpm test -pnpm test:e2e -``` - -Planned manual flow: - -- Sign in with Google as `ridi.pillaca@gmail.com`. -- Complete email OTP and reach `/admin`. -- Subscribe to job alerts, confirm email, publish AI Engineer job, receive notification. -- Apply with `C:\Users\a2021\OneDrive\Escritorio\2026\Toronto\RichardPillaca_RESUME.pdf`. - -## Commit / Push Status - -- Branch: `feat/redesign` -- Commit: Not implemented -- Push: Not pushed - -## Open Issues - -- P2 auth/email/notifications: plan is ready, implementation still pending. -- P2 review findings should be carried into the same implementation pass or fixed first. - -## Next Agent Instructions - -- Implement review findings first if they block core flows. -- Keep Prisma imports out of `auth.config.ts` because middleware uses the edge-safe config. -- Treat email OTP and job alert confirmation as server-enforced flows, not only client UI. diff --git a/docs/session-reports/2026-05-20-handoff-system.md b/docs/session-reports/2026-05-20-handoff-system.md deleted file mode 100644 index d51ba43..0000000 --- a/docs/session-reports/2026-05-20-handoff-system.md +++ /dev/null @@ -1,59 +0,0 @@ -# Session Report: 2026-05-20 Handoff System - -## Goal - -Make project context easy to find and reuse across repeated agent sessions by turning `docs/HANDOFF.md` into a fast index and moving detailed history into session reports. - -## What Changed - -- Reworked `docs/HANDOFF.md` into a current-state dashboard with `Read This First`, `Open Issues`, `Session Index`, verified work, resume parsing checklist, and reusable local context. -- Updated `docs/README.md` with a fast context workflow and session report index. -- Added a reusable session report template. -- Added dedicated reports for public job readability and the auth/email/notifications plan. -- Updated `.gitignore` so curated `docs/session-reports/*.md` files can be tracked. -- Recorded pre-existing uncommitted non-doc paths so future agents do not overwrite them accidentally. - -## Files Changed - -- `.gitignore` -- `docs/HANDOFF.md` -- `docs/README.md` -- `docs/session-reports/TEMPLATE.md` -- `docs/session-reports/2026-05-20-public-job-readability.md` -- `docs/session-reports/2026-05-20-auth-email-notifications-plan.md` -- `docs/session-reports/2026-05-20-handoff-system.md` - -## Verification - -Commands run: - -```bash -git status --short -git diff -- docs/HANDOFF.md docs/README.md -git check-ignore -v docs/session-reports/TEMPLATE.md docs/session-reports/2026-05-20-handoff-system.md -``` - -Results: - -- Confirmed the previous `HANDOFF.md` content and preserved its active context in the new index. -- Confirmed `docs/session-reports/*.md` was ignored before the `.gitignore` update. -- No application code changed. -- Pre-existing non-doc worktree changes were observed but not edited. - -## Commit / Push Status - -- Branch: `feat/redesign` -- Commit: Not committed in this session -- Push: Not pushed in this session - -## Open Issues - -- P1 resume parsing remains the highest-priority product issue. -- P2 auth/email/notifications implementation remains pending. -- Uncommitted non-doc changes exist in `src/lib/storage/upload.ts`, `src/server/services/submit-job-application-impl.ts`, `src/app/api/resumes/`, `src/lib/storage/upload.test.ts`, and `playwright-report/`. - -## Next Agent Instructions - -- Start future sessions from `docs/HANDOFF.md`. -- Add one Session Index row and one session report per meaningful work session. -- Keep active blockers in the Open Issues table, not only inside session reports. diff --git a/docs/session-reports/2026-05-20-parsing-ai-engineer-plan.md b/docs/session-reports/2026-05-20-parsing-ai-engineer-plan.md deleted file mode 100644 index a0ca8d2..0000000 --- a/docs/session-reports/2026-05-20-parsing-ai-engineer-plan.md +++ /dev/null @@ -1,71 +0,0 @@ -# Session Report: 2026-05-20 Parsing AI Engineer Plan - -## Goal - -Prepare the next implementation pass to fix resume parsing and add the AI Engineer seed job/template from the external sample job post. - -## Current State - -- Branch: `main` -- Latest observed commit: `11c4521 fix applicant deletion and resume submission handling` -- PR `#69 fix: improve resume parsing and handoff docs` is merged. -- PDF extraction is fixed, and browser application submission reached OpenRouter. -- Remaining parsing blocker: default model `deepseek/deepseek-chat-v3.1:free` returns `404 No endpoints found`. -- Live OpenRouter model metadata showed `openrouter/owl-alpha` as a free model supporting `response_format`. - -## Implementation Plan - -- Update `src/lib/llm/openrouter.ts` so parsing uses model fallbacks: - - first `OPENROUTER_MODEL`, when configured - - then `openrouter/owl-alpha` - - then one additional current free JSON-capable fallback if verified during implementation -- Retry parsing/scoring only for provider/model availability failures such as 404, no endpoint, or unsupported model. -- Keep Zod validation in `src/lib/llm/resume.ts` and `src/lib/match/scoreApplicant.ts`; do not accept unstructured LLM output. -- Update `.env.example`, `README.md`, and docs to remove the dead DeepSeek default. -- Add one active seeded job and one reusable template in `prisma/seed.ts`: - - title: `AI Engineer, Global Security` - - slug: `ai-engineer-global-security` - - department: `Technology and Operations` - - location: `Toronto, Ontario, Canada` - - type: `Full-time` - - salary: `Competitive` - - custom fields: portfolio/GitHub, LLM/RAG experience, security/compliance background, preferred focus select -- Use the external sample as content guidance but keep the seed as ScoutLane demo data, not exact RBC branding. -- Also fix `src/server/services/jobs/create-impl.ts` so `slug`, `department`, `whatYouWillDo`, `requirements`, and `toolsAndSkills` are included in `jobCreationSchema.safeParse`. - -## Important Working Tree Note - -The following app fixes were pushed in `11c4521`. Do not revert them: - -- required custom application fields -- select custom field options -- production guard for local resume storage -- applicant update/delete work -- admin parse-now for pending/stuck resume parsing - -Generated `playwright-report/` remains untracked and should not be committed. - -## Verification - -Run after implementation: - -```bash -pnpm typecheck -pnpm test -- --run -pnpm test:e2e -pnpm db:seed -``` - -Manual checks: - -- Confirm `/careers/ai-engineer-global-security` renders. -- Submit `RichardPillaca_RESUME.pdf` using `ridi.pillaca@gmail.com`. -- Confirm the applicant has `resumeUrl`, `parsingStatus=COMPLETED`, populated `parsedData`, numeric `score`, and `data.match`. -- Confirm required custom fields block empty submission and select options render. - -## Open Issues - -- P1 parsing: implement OpenRouter fallback model policy. -- P1 seed data: add AI Engineer job/template. -- P2 email: local confirmation still needs `RESEND_API_KEY` and `EMAIL_FROM`. -- P2 job creation: include structured fields in `createJobImpl` parsing. diff --git a/docs/session-reports/2026-05-20-parsing-performance-test.md b/docs/session-reports/2026-05-20-parsing-performance-test.md deleted file mode 100644 index 33145e1..0000000 --- a/docs/session-reports/2026-05-20-parsing-performance-test.md +++ /dev/null @@ -1,76 +0,0 @@ -# Session Report: 2026-05-20 Parsing Performance Test - -## Goal - -Run app tests around job application/resume parsing and measure performance when moving between pages. - -## What Changed - -- Fixed PDF text extraction for the installed `pdf-parse` v2 API by using `PDFParse` with `Uint8Array` input. -- Added `pdf-parse` to `serverExternalPackages` so Next runs the server-side parser as an external package instead of bundling its worker incorrectly. -- Aligned the existing `extractTextFromResumeBuffer` test mock with the `Uint8Array` API shape. -- Intentionally ignored `.data/` because local resume uploads are written under `.data/resumes`. - -## Files Changed - -- `.gitignore` -- `next.config.ts` -- `src/lib/resume/extractText.ts` -- `src/lib/resume/extractText.test.ts` -- `docs/HANDOFF.md` -- `docs/README.md` -- `docs/session-reports/2026-05-20-parsing-performance-test.md` - -## Verification - -Commands run: - -```bash -pnpm typecheck -pnpm test -- --run -PLAYWRIGHT_BASE_URL=http://127.0.0.1:3008 pnpm test:e2e -agent-browser vitals http://localhost:3008/ --json -agent-browser vitals http://localhost:3008/careers --json -agent-browser vitals http://localhost:3008/careers/data-scientist --json -agent-browser vitals http://localhost:3008/signin --json -``` - -Results: - -- `pnpm typecheck` passed. -- `pnpm test -- --run` passed: 12 files, 59 tests. -- `pnpm test:e2e` passed: 6 Playwright smoke tests. -- Direct `PDFParse` check against `RichardPillaca_RESUME.pdf` extracted 3712 chars. -- Browser submit on `http://localhost:3009/careers/senior-frontend-engineer` POSTed successfully and showed `Application submitted successfully.` -- Resume extraction passed the PDF worker stage; the remaining parse failure is OpenRouter model availability: `404 No endpoints found for deepseek/deepseek-chat-v3.1:free`. - -Performance results in local dev mode: - -| Route / Transition | Result | -|---|---:| -| `/` cold load | 1620ms navigation script; vitals TTFB 1022.9ms, LCP 1140ms, CLS 0 | -| `/careers` cold load | vitals TTFB 234ms, LCP 452ms, CLS 0 | -| `/careers/data-scientist` cold load | vitals TTFB 1155.3ms, LCP 1280ms, CLS 0 | -| `/signin` cold load | vitals TTFB 130.8ms, LCP 224ms, CLS 0 | -| `/` to `/careers/devops-engineer` click | 544ms | -| detail to `/` click | 372ms | -| `/` to `/careers/backend-platform-engineer` click | 381ms | -| `/signin` goto | 662ms | - -## Commit / Push Status - -- Branch: `feat/redesign` -- Commit: Not committed in this session -- Push: Not pushed in this session - -## Open Issues - -- P1 OpenRouter model config: `deepseek/deepseek-chat-v3.1:free` returned 404. Set `OPENROUTER_MODEL` to an available model, then re-run the submit. -- P2 email confirmation: local submit logs `RESEND_API_KEY is not configured`; configure Resend env before email-flow validation. -- P2 dev server: use the same host as the running server. `localhost` worked; `127.0.0.1` caused Next dev cross-origin resource warnings. - -## Next Agent Instructions - -- Start the app on `localhost` and use the same host in browser tests to avoid Next dev cross-origin blocking. -- Re-run a clean application submit after configuring `OPENROUTER_MODEL`, `RESEND_API_KEY`, and `EMAIL_FROM`. -- After submit, inspect the applicant row for `parsingStatus`, `parsedData`, `data.match`, and `score`. diff --git a/docs/session-reports/2026-05-20-public-job-readability.md b/docs/session-reports/2026-05-20-public-job-readability.md deleted file mode 100644 index c6398f8..0000000 --- a/docs/session-reports/2026-05-20-public-job-readability.md +++ /dev/null @@ -1,53 +0,0 @@ -# Session Report: 2026-05-20 Public Job Readability - -## Goal - -Improve the public job detail/application view because the dark page and inherited form colors made the job content, "About ScoutLane" section, and application form hard to read. - -## What Changed - -- Reworked public job detail pages such as `/careers/data-scientist` and `/careers/devops-engineer` from a full dark surface to a light, higher-contrast reading layout. -- Kept a compact dark brand header while moving job content and the application form onto readable light surfaces. -- Improved metadata pills, form labels, inputs, placeholders, upload area, and submit button contrast. -- Adjusted narrow mobile nav behavior and decorative background positioning. - -## Files Changed - -- `src/app/(public)/careers/[slug]/page.tsx` -- `src/components/public/ApplicationForm.tsx` -- `docs/HANDOFF.md` -- `docs/README.md` - -## Verification - -Commands run: - -```bash -pnpm typecheck -pnpm lint -``` - -Results: - -- `pnpm typecheck` passed. -- `pnpm lint` passed with one existing warning in `src/app/layout.tsx` about custom font loading. -- Browser checks were performed on `http://localhost:3006/careers/devops-engineer` and `http://localhost:3006/careers/data-scientist`. - -## Commit / Push Status - -- Branch: `feat/redesign` -- Commit: `9569964 fix: improve public job application readability` -- Push: Pushed to `origin/feat/redesign` -- Later observed HEAD: `f56df75 fix: applicant View link 404, resume preview, OpenRouter AI setup` - -## Open Issues - -- P1 resume parsing: a real self-application submitted around 2026-05-20 1:14 AM did not show parsed resume data, score, education, or work experience. -- P3 lint: custom font loading warning remains in `src/app/layout.tsx`. - -## Next Agent Instructions - -- Prioritize the resume upload/parsing investigation before more UI polish. -- Inspect applicant rows for `resumeUrl`, `parsingStatus`, `parsedData`, `score`, and `data`. -- Run `pnpm worker:resume` and confirm the pg-boss worker updates applicant parsing status and score. -- Re-check deployed Vercel URLs after the branch is deployed. diff --git a/docs/session-reports/2026-05-20-resend-google-email-templates.md b/docs/session-reports/2026-05-20-resend-google-email-templates.md deleted file mode 100644 index c47522d..0000000 --- a/docs/session-reports/2026-05-20-resend-google-email-templates.md +++ /dev/null @@ -1,42 +0,0 @@ -# Session: Resend setup, Google auth doc, email composer, template cleanup - -## Goal (verbatim) - -> i need to setup the RESEND_API_KEY is not configured, the google auth, the email sends, the notifications and the email revsiion when sending the application -> -> i also need to delete thsi part in teh jobs template Pipeline stages Applied/Screening/Assessment/Interview/Offer/Hired/Rejected is tottaly unuseful adn unnecesary. also make a relible template complete and use it to make a job use the job post sample in docs/job post sample and create more. also in the job page like /careers/ delete this in all jobs About ScoutLane … but you can say it in teh landing page hero section smmall and short - -## What landed (uncommitted, on `main`) - -- **Templates UI** — removed the `Pipeline stages` textarea from the template editor; stage names are still persisted via hidden default so existing jobs are unaffected. `src/app/(admin)/admin/templates/[id]/_components/TemplateEditor.tsx` -- **Public job page** — removed the `About ScoutLane` marketing card from every `/careers/`. `src/app/(public)/careers/[slug]/page.tsx` -- **Landing hero** — replaced the generic subtitle with a short ScoutLane brand line that doubles as the "about" copy. `src/components/public/CareersJobBoard.tsx` -- **Seed templates** — replaced the two placeholder templates with five complete ones (AI Engineer from the sample doc + Senior Frontend Engineer, Backend Platform Engineer, Product Designer, Data Engineer). Each carries title, department, location, type, salary, whatYouWillDo, requirements, toolsAndSkills, questions. Removed the stale `jobTemplate.deleteMany()` call so upserts survive re-runs. `prisma/seed.ts` -- **Resend** — `getResendClientOrNull`, `getEmailFromOrNull`, `isEmailConfigured` helpers; sends now log to `EmailLog` with `status=0` and reason `SKIPPED: ...` when the key/from is missing instead of crashing. New helpers `sendCustomEmail`, `sendAdminNewApplicationEmail`, `buildApplicationConfirmationEmail`. `src/lib/email/client.ts`, `src/lib/email/send.ts` -- **Admin notification email** — application submit now emails every ADMIN in the organization with the applicant name + a deep link into the dashboard. `src/server/services/submit-job-application-impl.ts` -- **Compose email panel** — new `ApplicantEmailComposer` on the applicant profile page with a template picker (shortlisted, interview, offer, decline, follow-up, blank), subject/body inputs, HTML preview toggle, and sonner toast on success/skip/error. Backed by a new server action `sendApplicantEmail`. Files: `src/server/services/emails/send-applicant.ts`, `…/send-applicant-impl.ts`, `src/app/(admin)/admin/jobs/[id]/applicants/[applicantId]/_components/ApplicantEmailComposer.tsx` -- **Email templates preview page** — `/admin/email-templates` renders the applicant confirmation and admin notification emails so the team can QA copy. Sidebar gets a new `Email templates` entry. `src/app/(admin)/admin/email-templates/page.tsx`, `src/app/(admin)/_components/SidebarNav.tsx` -- **Toasts** — sonner installed, `Toaster` mounted in the root layout, stage-change action wired to it. -- **Procurement guide** — `docs/SETUP.md` with step-by-step Resend signup, domain verification, Google Cloud OAuth client creation, env vars, redirect URIs, and prod hardening. `.env.example` comments updated to point at the guide. - -## Verification - -- `pnpm typecheck` — clean. -- `pnpm lint` — clean (only pre-existing `no-page-custom-font` warning in root layout). -- `pnpm test -- --run` — 72/72 passing. -- `pnpm build` — succeeds; new `/admin/email-templates` route compiled. - -## What's still open - -- Real procurement of `AUTH_GOOGLE_ID` / `AUTH_GOOGLE_SECRET` / `RESEND_API_KEY` / verified `EMAIL_FROM` domain — covered by `docs/SETUP.md`, but the user has to do the manual signups. -- Re-seed (`pnpm db:seed`) and Vercel redeploy needed before the new templates show up in `/admin/templates` and the new `/admin/email-templates` page is reachable in production. -- The admin notification email goes to every ADMIN in the organization; if you want per-user opt-in we still need a `notificationPreferences` field on `User`. -- No real-time in-app inbox built — email + toasts is the chosen scope. - -## Next recommended action - -Run `pnpm db:seed` locally to confirm the five new templates appear, then push a feature branch + PR. - -## Risks / blockers - -- None encountered. Sonner is a fresh dependency (no peer conflicts during install). diff --git a/docs/session-reports/2026-05-26-codex-fixes-notifications-hardening.md b/docs/session-reports/2026-05-26-codex-fixes-notifications-hardening.md deleted file mode 100644 index fe7b533..0000000 --- a/docs/session-reports/2026-05-26-codex-fixes-notifications-hardening.md +++ /dev/null @@ -1,44 +0,0 @@ -# 2026-05-26 — Codex fixes + notifications/AI parsing hardening - -## Goal - -> "i want to have the features: notifications and AI parsing working correctly. I want to know whats working adn what not. I need to fix the issues found from the Codex adversial review. […] My goal is to have the features working correctly so i need to have determinitsic test, functionality, integration, unit test. […] Ones done I need to commit and push the changes, make a pr and merge afeter all the CI checks and vercel checks pass." - -## What landed - -- `94b0221` — feat: normalize Resend email send results and surface provider errors. Discriminated `EmailSendResult` shape closes Codex finding #1 (silent failure on error-union responses). Includes the composer, sidebar, sonner Toaster, email-templates page, .env.example updates, and sonner@2.0.7. -- `049e786` — feat: queue applicant emails via pg-boss to remove sync admin fan-out. Closes Codex finding #2. New `src/server/queues/emails.ts`, `src/server/workers/emails.ts`, `pnpm worker:emails`. -- `3495a64` — feat: harden resume parser failure paths and refresh template + careers UI. Empty-text guard, full error logging on the worker, hidden stages textarea on template editor, About-ScoutLane removed from job pages, brand subtitle on the careers hero, five seeded templates. -- `17b9a7a` — test: cover Resend error-union, email queue fan-out, and admin notification deferral. +12 tests; total 94/94 local. -- `443a611` — docs: add Resend/Google/OpenRouter setup walkthrough and Vercel preview smoke checklist (`docs/SETUP.md` extended, new `docs/SMOKE.md`, gitignore whitelist). -- `4f38c23` — fix(test): skip pdf-parse sample test when fixture is gitignored (pre-existing CI failure unrelated to this work, surfaced by the PR). - -PR: https://github.com/RikepilB/ScoutLane/pull/79 - -## What's still open - -- CI run on the feature branch — currently re-running after the test fix. -- Vercel preview — currently building. -- Manual `docs/SMOKE.md` walkthrough on the preview URL once both go green. Required before merging to `main`. -- Provision `pnpm worker:emails` on the worker host before promoting to `master`/production. -- Optional follow-up: move the resume sample PDF into `src/test/fixtures/` so CI exercises it (privacy review needed first). - -## Next recommended action - -Wait for the re-run CI + Vercel preview to finish on PR #79, then walk `docs/SMOKE.md` against the preview URL. If green, merge to `main`, then open the second PR (`main` → `master`) for the production deploy. - -## Risks / blockers - -- Worker host needs the new `worker:emails` process running with the same `DATABASE_URL` as the web app. Without it, admin emails queue indefinitely. -- Resend domain verification must complete (DNS propagation) before "Sending is live" turns green on `/admin/email-templates`. -- Google OAuth redirect URI for the Vercel preview domain must be added to the OAuth client before step 5 of the smoke checklist will succeed. - -## 2026-05-26 local continuation - -- Local smoke exposed that standalone `tsx` workers did not load `.env`, so `pnpm worker:emails` failed with `DATABASE_URL is required for the email queue.` -- Added `import "dotenv/config";` to `src/server/workers/emails.ts` and `src/server/workers/resume.ts` so local worker scripts load the same env file as Prisma seed while still respecting real process env in deployed worker hosts. -- Browser retry parsing exposed that local `/api/resumes/...` URLs could open in the PDF viewer but fail server-side parsing with `Could not download resume (HTTP 404)` when `NEXT_PUBLIC_APP_URL` pointed at a different dev port. -- Fixed `parseApplicantResumeFromUrl` to load app-owned resume URLs directly from local `.data/resumes` or database-backed `ResumeFile` storage before falling back to HTTP fetches for external object-storage URLs. -- Replaced the dead OpenRouter default path with `openrouter/owl-alpha`, added built-in fallbacks `openrouter/free` and `openrouter/auto`, and added a per-request OpenRouter timeout so provider routing failures do not leave parsing stuck. -- Verified applicant `cmpesy7ey000104l53sh1637r` now parses successfully: structured work, education, skills, and match data populate in the admin applicant detail page. -- Verification: `cmd.exe /c pnpm typecheck` passed; `cmd.exe /c pnpm test -- --run` passed with 105/105 tests. diff --git a/docs/session-reports/2026-05-28-takehome-finish-p0.md b/docs/session-reports/2026-05-28-takehome-finish-p0.md deleted file mode 100644 index 35f605a..0000000 --- a/docs/session-reports/2026-05-28-takehome-finish-p0.md +++ /dev/null @@ -1,41 +0,0 @@ -# Session Report — Take-Home Finish P0 (2026-05-28) - -## Goal - -Verbatim from the user's `/goal`: ship the take-home submission finish plan — restore canonical assessment + security docs (G1), persist dropped job-create fields (G2), resolve the custom file-field type (G3), persist a User row on dev sign-in (G4), production Google OAuth (G5), security audit evidence + rate limit (G6), hosted deploy (G7), Codespaces verify (G8), extend e2e smoke (G9), README + demo + tag (G10). - -## What landed (uncommitted — awaiting explicit commit approval) - -- **G2** — `src/server/services/jobs/create-impl.ts`: added `slug`, `department`, `whatYouWillDo`, `requirements`, `toolsAndSkills` to the `safeParse` input (they were read at lines 99–111 but never extracted). Replaced placeholder `create-impl.test.ts` with a real mocked test asserting passthrough + slug derivation. -- **G3** — Removed the dead custom `file` field type everywhere: form builder (`form/page.tsx`), `ApplicationForm.tsx`, `careers/[slug]/page.tsx`, `schemas/template.ts` enum, `TemplateEditor.tsx`, `TemplatePreview.tsx`. Resume upload remains the only file mechanism. (`.md` description upload + resume upload untouched.) -- **G4** — Extracted the NextAuth `signIn` callback into `src/lib/auth/sign-in.ts` (`handleSignIn`); dev provider now upserts an ADMIN `User` + reuses/creates an org; DB failure never blocks dev sign-in. `auth.ts` delegates. New `sign-in.test.ts` (6 tests). Keeps Prisma out of `auth.config.ts`. -- **G6** — New `src/lib/rate-limit.ts` (in-memory fixed-window limiter + `clientIpFromHeaders`) with `rate-limit.test.ts` (4 tests). Wired ~10/min/IP into both apply paths: `POST /api/public/jobs/[slug]/applications` and the `submitJobApplication` Server Action. Ran `pnpm audit --prod` (1 high `next` middleware-bypass advisory, patch `16.2.6`; 3 moderate transitive) and a secret scan (clean). Appended an "Evidence Collected — 2026-05-28" section to `docs/SECURITY-AUDIT.md`. -- **G1** — `docs/ASSESSMENT-PROGRESS.md` + `docs/SECURITY-AUDIT.md` already existed on disk but untracked; appended dated scoreboard / evidence sections, whitelisted both in `.gitignore`, added both to `docs/README.md` index. - -Gate: `pnpm typecheck` clean · `pnpm lint` clean (1 pre-existing font warning) · `pnpm test -- --run` 124 passing · `pnpm build` succeeds. - -- **next bump (DONE)** — `next` 16.2.5 → 16.2.6, closing the high middleware/proxy-bypass advisory (GHSA-26hh-7cqf-hhc6). `pnpm audit --prod` now 3 moderate (transitive `uuid`) only. Commit `7fbe50d`. -- **G9 (DONE)** — `tests/e2e/smoke.spec.ts`: added public-apply-with-resume + admin-applicants-list/CSV tests, a minimal PDF fixture, and `RESUME_PARSE_MODE=queue` in the playwright webServer env so apply submit returns promptly (default mode parses inline via OpenRouter). Also fixed the pre-existing careers-landing test path (`/` not `/careers`). **Full smoke suite 7/7 green** (chromium, workers=1, against a seeded DB). Commit `c5cc90d`. - -## What's still open (all user-manual) - -- **G5** — GCP OAuth client + Vercel envs (Production + Preview). -- **G7** — Vercel envs + Render workers + Resend; verify end-to-end on hosted URL. -- **G8** — Codespaces cold-clone verify. -- **G10** — README + recorded demo + `v1.0.0-takehome` tag + prod PR. - -## Commits (this session) - -`3b964a1` G3 file-field · `456662d` G4 dev upsert · `a27251b` G6 rate-limit+audit · `6139594` G1 docs · `7fbe50d` next 16.2.6 bump · `c5cc90d` G9 e2e. (G2 field-drop fix landed inside the external `23f16fc`.) - -Final gate: `pnpm typecheck` clean · `pnpm lint` clean (1 pre-existing font warning) · `pnpm test -- --run` 124 passing · `pnpm build` succeeds · `pnpm test:e2e` (chromium) 7/7. - -## Next recommended action - -All agent-side P0 code is committed and green; hand off to the user for G5/G7/G8/G10 (deploy + demo + tag), then open PR #79 → main. - -## Risks/blockers encountered - -- `docs/ASSESSMENT-PROGRESS.md` + `SECURITY-AUDIT.md` existed untracked on disk — appended rather than overwrote. -- Uncommitted UI work in flight from a prior session (`admin/jobs/page.tsx`, `applicants/page.tsx`, new `JobCard.tsx`) — left untouched; will stage only this session's files. -- `next` high advisory open (patch available, not yet applied). diff --git a/docs/session-reports/2026-06-11-resume-pipeline-oauth-emails.md b/docs/session-reports/2026-06-11-resume-pipeline-oauth-emails.md deleted file mode 100644 index cf1b537..0000000 --- a/docs/session-reports/2026-06-11-resume-pipeline-oauth-emails.md +++ /dev/null @@ -1,38 +0,0 @@ -# 2026-06-11 — Resume pipeline, OAuth, emails, MCP cleanup - -## Goal - -"fix mcp, connect to composio … fix scoutlane in the parsing whenever i upload a pdf it does not parse correctly … preferable gemini 2.5 flash … word is not embed, pdf is embeded, word cna be download, but pdf not found. The embeed needs fixing, then the login with my google email … fix the bugs with notifications and setting up the emails directly for people onces they submit their application." - -## What landed (PR #89, branch `fix/resume-pipeline-and-notifications`) - -- `5ae6980` fix(resume): pdf-parse via dynamic import compatible with Next bundling (+ committed PDF fixture, un-skipped test) -- `133fa2d` chore: .gitattributes marking binary assets (prevents fixture corruption) -- `8d8d3a4` feat(llm): default model `google/gemini-2.5-flash` + fallbacks, tightened prompt -- (storage) feat(resumes): shared disk→DB resolution, Content-Disposition, clearer 404 -- (preview) feat(resumes): DOCX→sanitized-HTML inline preview, authenticated route, sandboxed iframe -- (jobs) feat(jobs): `JOB_RUNNER=worker|inline` dispatcher; inline `after()` path = no workers on Vercel -- `ac86c72` fix(auth): conflicting Google env warning, Google users upserted with org, `AUTH_ALLOWED_EMAIL_DOMAIN` -- `b9ddcef` fix(jobs): sequential inline admin fan-out (Resend rate limit) -- MCP: removed broken `vercel` (SSE) + misconfigured `caveman-shrink`; Composio already connected -- Local `.env`: Google creds migrated legacy→`AUTH_GOOGLE_*` (root cause of broken login: empty `AUTH_GOOGLE_*` shadowing real legacy values), `JOB_RUNNER="inline"`, model updated - -E2E smoke verified (prod build, no workers): PDF parse COMPLETED via gemini-2.5-flash; DOCX parse COMPLETED; preview route 401 logged-out; EmailLog rows written for confirmation + admin fan-out. - -CodeRabbit review triage (`afa2270`): fixed SETUP.md §4.3 (RESUME_PARSE_MODE default is `queue`; documented JOB_RUNNER=inline), added domain-allowlist bypass regression test and inline fan-out failure-behavior test. Skipped as invalid: preview-route contentType null guard (type is always `string`), endsWith "subdomain bypass" (the `@` anchor already enforces exact domain), synchronous inline sends (fire-and-forget by design; failures land in EmailLog). - -## Still open - -- Merge PR #89 → main, then main → master to deploy -- Vercel env: set `AUTH_GOOGLE_ID/SECRET`, `AUTH_SECRET`, `OPENROUTER_API_KEY`, `RESEND_API_KEY`; verify GCP redirect URI for prod domain -- Verify a Resend domain for production `EMAIL_FROM` (test sender only delivers to account owner) -- Follow-up: `/api/resumes/*` unauthenticated (pre-existing) - -## Next recommended action - -Merge PR #89, set the Vercel env vars above, then smoke a PDF application on production. - -## Risks/blockers encountered - -- Resend free tier: unverified domain blocks delivery to non-owner addresses (logged in EmailLog, expected) -- Inline LLM call bounded by serverless maxDuration (60s); Retry button is the recovery path diff --git a/docs/session-reports/TEMPLATE.md b/docs/session-reports/TEMPLATE.md deleted file mode 100644 index 2dc2af2..0000000 --- a/docs/session-reports/TEMPLATE.md +++ /dev/null @@ -1,42 +0,0 @@ -# Session Report: - -## Goal - -State the user goal and the intended outcome in one or two sentences. - -## What Changed - -- List the main behavior, documentation, or code changes. -- Include file paths only when they help the next agent find the work quickly. - -## Files Changed - -- `` - -## Verification - -Commands run: - -```bash - -``` - -Results: - -- `` - -## Commit / Push Status - -- Branch: `` -- Commit: `` -- Push: `` - -## Open Issues - -- `` ``: `` - -## Next Agent Instructions - -- Start from ``. -- Preserve ``. -- Verify with ``. diff --git a/src/app/(admin)/admin/jobs/[id]/integrations/_components/IntegrationForm.test.tsx b/src/app/(admin)/admin/jobs/[id]/integrations/_components/IntegrationForm.test.tsx new file mode 100644 index 0000000..e8a8946 --- /dev/null +++ b/src/app/(admin)/admin/jobs/[id]/integrations/_components/IntegrationForm.test.tsx @@ -0,0 +1,26 @@ +import { describe, it, expect, afterEach, vi } from "vitest"; +import { cleanup, render, screen, fireEvent } from "@testing-library/react"; + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ refresh: vi.fn() }), +})); + +import { IntegrationForm } from "./IntegrationForm"; + +afterEach(() => { + cleanup(); +}); + +const stages = [{ id: "s1", name: "New", order: 0 }]; + +describe("IntegrationForm", () => { + it("masks the API key input so it can't be shoulder-surfed or screen-shared", () => { + render(); + + fireEvent.click(screen.getByRole("button", { name: /add integration/i })); + + const apiKeyInput = screen.getByPlaceholderText("sk-..."); + expect(apiKeyInput).toHaveAttribute("type", "password"); + expect(apiKeyInput).toHaveAttribute("autocomplete", "off"); + }); +}); diff --git a/src/app/(admin)/admin/jobs/[id]/integrations/_components/IntegrationForm.tsx b/src/app/(admin)/admin/jobs/[id]/integrations/_components/IntegrationForm.tsx index b1a094b..7f37412 100644 --- a/src/app/(admin)/admin/jobs/[id]/integrations/_components/IntegrationForm.tsx +++ b/src/app/(admin)/admin/jobs/[id]/integrations/_components/IntegrationForm.tsx @@ -95,7 +95,8 @@ export function IntegrationForm({ jobId, stages }: IntegrationFormProps) {
setApiKey(e.target.value)} placeholder="sk-..." diff --git a/src/app/api/public/job-alerts/route.test.ts b/src/app/api/public/job-alerts/route.test.ts new file mode 100644 index 0000000..08054ec --- /dev/null +++ b/src/app/api/public/job-alerts/route.test.ts @@ -0,0 +1,66 @@ +// @vitest-environment node +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { NextRequest } from "next/server"; + +const { subscribeMock } = vi.hoisted(() => ({ + subscribeMock: vi.fn(), +})); + +vi.mock("@/server/services/job-alerts", () => ({ + subscribe: subscribeMock, + unsubscribe: vi.fn(), +})); + +import { POST } from "@/app/api/public/job-alerts/route"; + +beforeEach(() => { + subscribeMock.mockReset(); + subscribeMock.mockResolvedValue({ success: true, message: "Subscribed!" }); +}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +function postFrom(ip: string) { + const request = new Request("http://localhost/api/public/job-alerts", { + method: "POST", + headers: { "Content-Type": "application/json", "x-forwarded-for": ip }, + body: JSON.stringify({ email: "ada@example.com" }), + }) as unknown as NextRequest; + + return POST(request); +} + +describe("POST /api/public/job-alerts", () => { + it("allows requests under the per-IP limit", async () => { + const response = await postFrom("203.0.113.10"); + expect(response.status).toBe(200); + expect(subscribeMock).toHaveBeenCalledWith("ada@example.com"); + }); + + it("returns 429 with Retry-After once an IP exceeds the limit", async () => { + const ip = "203.0.113.20"; + for (let i = 0; i < 10; i++) { + const ok = await postFrom(ip); + expect(ok.status).toBe(200); + } + + const limited = await postFrom(ip); + expect(limited.status).toBe(429); + expect(limited.headers.get("Retry-After")).toBeTruthy(); + const body = (await limited.json()) as { error: string }; + expect(body.error).toMatch(/too many requests/i); + }); + + it("tracks limits independently per IP", async () => { + const ip = "203.0.113.30"; + for (let i = 0; i < 10; i++) { + await postFrom(ip); + } + await postFrom(ip); // exhausts this IP + + const otherIp = await postFrom("203.0.113.31"); + expect(otherIp.status).toBe(200); + }); +}); diff --git a/src/app/api/public/job-alerts/route.ts b/src/app/api/public/job-alerts/route.ts index ea7c704..11657bd 100644 --- a/src/app/api/public/job-alerts/route.ts +++ b/src/app/api/public/job-alerts/route.ts @@ -1,13 +1,28 @@ import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { subscribe, unsubscribe } from "@/server/services/job-alerts"; +import { clientIpFromHeaders, createRateLimiter } from "@/lib/rate-limit"; const schema = z.object({ email: z.string().email().max(254), }); +// ~10 subscribe attempts per minute per IP. Shared across requests in this +// runtime instance; see src/lib/rate-limit.ts for production considerations. +const jobAlertRateLimiter = createRateLimiter({ limit: 10, windowMs: 60_000 }); + export async function POST(request: NextRequest) { try { + const ip = clientIpFromHeaders(request.headers); + const rate = jobAlertRateLimiter.check(ip); + if (!rate.allowed) { + const retryAfter = Math.max(1, Math.ceil((rate.resetAt - Date.now()) / 1000)); + return NextResponse.json( + { error: "Too many requests. Please try again shortly." }, + { status: 429, headers: { "Retry-After": String(retryAfter) } }, + ); + } + const body = schema.safeParse(await request.json()); if (!body.success) { return NextResponse.json({ error: "Invalid email" }, { status: 400 });