feature: Job-agent email delivery with Resend test sender, rate limits, and UI cooldowns#327
Conversation
📝 WalkthroughWalkthroughImplements "Email me these jobs" feature for the job-agent chat. Students can click a button below job cards to email matched jobs to their inbox. Client collects job IDs and preceding chat context, calls a new ChangesEmail Jobs Feature
Sequence DiagramsequenceDiagram
participant Student
participant Client as EmailJobsButton
participant APIEndpoint as POST /email-jobs
participant Service as JobAgentService
participant Mailer as sendEmail
participant Database as jobAgentEmailLog
Student->>Client: click "Email me these jobs"
Client->>Client: build { jobIds, context }
Client->>APIEndpoint: POST with payload
APIEndpoint->>Service: emailJobs(userId, input)
Service->>Service: check cooldown & daily limit
Service->>Service: fetch & filter eligible jobs
Service->>Service: build email payload (HTML+text)
Service->>Mailer: sendEmail(to, subject, html, text)
Mailer-->>Service: true (success) or throw
Service->>Database: persist jobAgentEmailLog row
Service-->>APIEndpoint: { sent: true, count }
APIEndpoint-->>Client: response
Client->>Client: set 60s cooldown, show "Sent"
Client-->>Student: feedback + countdown
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
client/src/module/student/job-agent/EmailJobsButton.tsx (1)
114-114: ⚡ Quick winReplace arbitrary text size class with a scale token.
Line 114 uses
text-[10px], which violates the Tailwind sizing rule in this repo.♻️ Proposed fix
- <span className="text-[10px] font-mono uppercase tracking-widest text-red-500 dark:text-red-400"> + <span className="text-xs font-mono uppercase tracking-widest text-red-500 dark:text-red-400">As per coding guidelines, "Do not use arbitrary bracket sizes like
text-[17px], use standard scale classes instead".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@client/src/module/student/job-agent/EmailJobsButton.tsx` at line 114, In EmailJobsButton.tsx replace the arbitrary Tailwind class text-[10px] on the <span> (the span in the EmailJobsButton component) with the repo's standard scale token—e.g., use text-xs (or the closest approved token) instead of text-[10px]; if 10px is required, update the design token/scale instead of using an arbitrary bracket size.server/src/utils/email-templates.ts (1)
1337-1348: 💤 Low valueRedundant date parsing in plain text template.
formatJobDeadline(job.deadline)is called twice for the same value—once to check truthiness and again to interpolate. The HTML version correctly stores the result in a variable first (line 1271).♻️ Suggested fix
const jobLines = args.jobs.map((job, index) => { + const deadline = formatJobDeadline(job.deadline); const parts = [ `${index + 1}. ${job.title} - ${job.company}`, job.location ? `Location: ${job.location}` : null, job.salary ? `Salary: ${job.salary}` : null, - formatJobDeadline(job.deadline) ? `Apply by: ${formatJobDeadline(job.deadline)}` : null, + deadline ? `Apply by: ${deadline}` : null, snippet(job.description, 200) || null, `View job: ${job.url}`, ].filter(Boolean);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@server/src/utils/email-templates.ts` around lines 1337 - 1348, In the jobLines plain-text template mapping, avoid calling formatJobDeadline(job.deadline) twice; compute const formattedDeadline = formatJobDeadline(job.deadline) inside the args.jobs.map callback and then use formattedDeadline for the truthiness check and the interpolated "Apply by: ..." string (similar to the HTML version), so only one parse/format occurs per job in the jobLines construction.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@server/src/module/job-agent/job-agent.service.ts`:
- Around line 478-483: The job eligibility query in prisma.jobIndex.findMany
(the block filtering by uniqueJobIds, isActive, and deadline) is missing the
publish-state predicate used by the job feed search; update the where clause to
include the same publish predicate used in your feed search logic (the predicate
that enforces published visibility) so only published jobs are returned—locate
the publish predicate implementation used by the feed search query and add it
into the where object alongside isActive and deadline in the
JobAgentService/job-agent.service.ts eligibility query.
- Around line 440-453: The current sequence reads rate-limit state
(prisma.jobAgentEmailLog.findFirst, .count and startOfToday()) then sends and
logs later, allowing concurrent requests to bypass the 60s/5-per-day limits;
change this to an atomic reservation inside a transaction or per-user lock:
within a single DB transaction use prisma.jobAgentEmailLog.create (or an
upsert/pending marker) plus a count/query to check last sent timestamp and
today's count and fail the operation if limits would be exceeded, then commit so
only one request can reserve/send at a time; update the code paths that call
prisma.jobAgentEmailLog.findFirst, prisma.jobAgentEmailLog.count, and the later
logging/creation to use this transactional reservation pattern (or SELECT ...
FOR UPDATE equivalent) around userId/startOfToday() checks to enforce the limits
atomically.
---
Nitpick comments:
In `@client/src/module/student/job-agent/EmailJobsButton.tsx`:
- Line 114: In EmailJobsButton.tsx replace the arbitrary Tailwind class
text-[10px] on the <span> (the span in the EmailJobsButton component) with the
repo's standard scale token—e.g., use text-xs (or the closest approved token)
instead of text-[10px]; if 10px is required, update the design token/scale
instead of using an arbitrary bracket size.
In `@server/src/utils/email-templates.ts`:
- Around line 1337-1348: In the jobLines plain-text template mapping, avoid
calling formatJobDeadline(job.deadline) twice; compute const formattedDeadline =
formatJobDeadline(job.deadline) inside the args.jobs.map callback and then use
formattedDeadline for the truthiness check and the interpolated "Apply by: ..."
string (similar to the HTML version), so only one parse/format occurs per job in
the jobLines construction.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0233540a-b109-4d9f-9289-9bbefe27f882
📒 Files selected for processing (11)
client/src/module/student/job-agent/AgentMessage.tsxclient/src/module/student/job-agent/EmailJobsButton.tsxclient/src/module/student/job-agent/JobAgentPage.tsxserver/src/database/prisma/migrations/20260519000000_add_job_agent_email_log/migration.sqlserver/src/database/prisma/schema/base.prismaserver/src/module/job-agent/job-agent.controller.tsserver/src/module/job-agent/job-agent.routes.tsserver/src/module/job-agent/job-agent.service.tsserver/src/module/job-agent/job-agent.validation.tsserver/src/utils/email-templates.tsserver/src/utils/email.utils.ts
| const [user, lastEmail, sentToday] = await Promise.all([ | ||
| prisma.user.findUnique({ | ||
| where: { id: userId }, | ||
| select: { name: true, email: true }, | ||
| }), | ||
| prisma.jobAgentEmailLog.findFirst({ | ||
| where: { userId }, | ||
| orderBy: { createdAt: "desc" }, | ||
| select: { createdAt: true }, | ||
| }), | ||
| prisma.jobAgentEmailLog.count({ | ||
| where: { userId, createdAt: { gte: startOfToday() } }, | ||
| }), | ||
| ]); |
There was a problem hiding this comment.
Make rate-limit enforcement atomic to avoid concurrent bypass.
Lines 440-475 read cooldown/daily state before send, while Line 552 logs afterward. Concurrent requests can both pass checks and both send, violating the 60s and 5/day guarantees.
Use an atomic reservation strategy (e.g., transactional per-user lock + pending log row/state) so only one request can pass the gate at a time.
Also applies to: 459-475, 552-559
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/src/module/job-agent/job-agent.service.ts` around lines 440 - 453, The
current sequence reads rate-limit state (prisma.jobAgentEmailLog.findFirst,
.count and startOfToday()) then sends and logs later, allowing concurrent
requests to bypass the 60s/5-per-day limits; change this to an atomic
reservation inside a transaction or per-user lock: within a single DB
transaction use prisma.jobAgentEmailLog.create (or an upsert/pending marker)
plus a count/query to check last sent timestamp and today's count and fail the
operation if limits would be exceeded, then commit so only one request can
reserve/send at a time; update the code paths that call
prisma.jobAgentEmailLog.findFirst, prisma.jobAgentEmailLog.count, and the later
logging/creation to use this transactional reservation pattern (or SELECT ...
FOR UPDATE equivalent) around userId/startOfToday() checks to enforce the limits
atomically.
| const jobs = await prisma.jobIndex.findMany({ | ||
| where: { | ||
| id: { in: uniqueJobIds }, | ||
| isActive: true, | ||
| OR: [{ deadline: null }, { deadline: { gte: now } }], | ||
| }, |
There was a problem hiding this comment.
Add publish-state filtering in job eligibility query.
Line 478 currently filters only by isActive and deadline; it does not enforce the “published” constraint from the endpoint requirements. This can email jobs that shouldn’t be user-visible yet.
Use the same publish predicate used by your job feed search query in this where clause.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/src/module/job-agent/job-agent.service.ts` around lines 478 - 483, The
job eligibility query in prisma.jobIndex.findMany (the block filtering by
uniqueJobIds, isActive, and deadline) is missing the publish-state predicate
used by the job feed search; update the where clause to include the same publish
predicate used in your feed search logic (the predicate that enforces published
visibility) so only published jobs are returned—locate the publish predicate
implementation used by the feed search query and add it into the where object
alongside isActive and deadline in the JobAgentService/job-agent.service.ts
eligibility query.
Summary
Implements “Email me these jobs” for the AI job-agent so students can receive matched jobs by email, with server-side throttling and client cooldown UX.
What & why
Adds a student-only email flow for job-agent results so users can receive their matches in their inbox and revisit them later.
Implements server-side rate limiting (60s cooldown + 5/day) with durable logging to prevent abuse and avoid repeated sends.
Backend changes
[POST /api/job-agent/email-jobs] added behind auth and student role.
Fetches the student’s email from DB (never from request body).
Re-queries [jobIndex] for only active, non-expired job IDs, preserves requested order, and drops invalid jobs.
Generates HTML + text email payloads with job details and internal job links.
Logs sends after successful delivery for accurate cooldown enforcement.
Frontend changes
Adds an “Email me these jobs” action under assistant messages that include jobs.
Shows a success state briefly, then starts a 60s local cooldown.
Handles 429 with server-provided [retryAfter] countdown; shows inline error for other failures.
Passes the preceding user prompt as optional context for the email header.
Email delivery behavior
Currently uses Resend’s test sender (onboarding@resend.dev), so emails land in the Resend test inbox.
For real student inbox delivery, verify a domain in Resend and set [EMAIL_FROM] to a verified sender on that domain.
Files touched
Server: [job-agent.service.ts], [email.utils.ts], [email-templates.ts]
Client: client/src/module/student/ai-agent/AgentMessage.tsx and related UI component(s)
Testing
Manual: trigger “Email me these jobs” in the job agent UI and verify 200 response.
Confirm cooldown behavior on repeat sends.
(Note: Actual inbox delivery requires verified domain in Resend.)
Closes #41.
Screenshots
Summary by CodeRabbit
Release Notes