diff --git a/.env.example b/.env.example index e2e5280..bc38aa0 100644 --- a/.env.example +++ b/.env.example @@ -52,6 +52,11 @@ RESUME_PARSE_MODE="queue" # after(); no worker needed. Default on Vercel. JOB_RUNNER="" +# Dev-login escape hatch for local production-build smoke tests ONLY. +# The dev provider grants ADMIN to any email -- NEVER set this on a deployed +# environment. Dev login is otherwise enabled only under `next dev`. +ALLOW_DEV_LOGIN="" + # ── File Storage (Google Cloud Storage) ────────────────────────────────────── # Create a service account at https://console.cloud.google.com/iam-admin/serviceaccounts # and grant the Storage Object Admin role on the bucket. diff --git a/docs/SETUP.md b/docs/SETUP.md index e8c9efe..08e56be 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -254,9 +254,10 @@ After deploying or rotating keys, **restart both workers** so the new envs take ### 4.3 What happens without the workers -- With `RESUME_PARSE_MODE=queue-and-inline` (default), parsing happens inline during submission — the resume worker is optional for redundancy only. -- With `RESUME_PARSE_MODE=queue`, `enqueueResumeParseJob` returns immediately; the row stays in `PENDING` until the resume worker picks it up. -- `enqueueEmailJob` returns immediately; admin notification and applicant confirmation emails are never sent without the email worker — the applicant submission still succeeds. +- With `JOB_RUNNER=inline` (default on Vercel), no workers are needed: resume parsing and email delivery run after the response in the same serverless invocation. +- With `RESUME_PARSE_MODE=queue` (default), parsing is deferred to the job runner: in `worker` mode the row stays in `PENDING` until the resume worker picks it up; in `inline` mode it completes post-response. +- `RESUME_PARSE_MODE=queue-and-inline` additionally parses during submission — redundant unless you want a synchronous result alongside the queue. +- In `worker` mode, `enqueueEmailJob` returns immediately; admin notification and applicant confirmation emails are never sent without the email worker — the applicant submission still succeeds. Both are visible in `/admin/notifications` (parsing failures + email skips). 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 new file mode 100644 index 0000000..cf1b537 --- /dev/null +++ b/docs/session-reports/2026-06-11-resume-pipeline-oauth-emails.md @@ -0,0 +1,38 @@ +# 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/src/app/(admin)/_components/AdminPageSkeleton.tsx b/src/app/(admin)/_components/AdminPageSkeleton.tsx new file mode 100644 index 0000000..849f36d --- /dev/null +++ b/src/app/(admin)/_components/AdminPageSkeleton.tsx @@ -0,0 +1,22 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +/** + * Generic route-segment fallback for admin pages: a header row plus a few + * card-shaped blocks. Shown by Next.js while the server component streams. + */ +export function AdminPageSkeleton({ cards = 3 }: { cards?: number }) { + return ( +
Loading position…
+Loading…
++ Sign-in is not configured on this deployment. An administrator + must set the Google OAuth environment variables. +
+ )} + + {googleEnabled && ( + )} {showDevLogin && ( <> diff --git a/src/app/signin/page.tsx b/src/app/signin/page.tsx index 79d7240..425d067 100644 --- a/src/app/signin/page.tsx +++ b/src/app/signin/page.tsx @@ -1,13 +1,14 @@ import { Suspense } from "react"; -import { isDevLoginAllowed } from "@/lib/auth/auth.config"; +import { isDevLoginAllowed, isGoogleAuthConfigured } from "@/lib/auth/auth.config"; import { SignInForm } from "./_components/SignInForm"; export default function SignInPage() { const showDevLogin = isDevLoginAllowed(); + const googleEnabled = isGoogleAuthConfigured(); return (