Skip to content

Angel Asensio - Solved#18

Open
asensiodev wants to merge 1 commit intoLIDR-academy:mainfrom
asensiodev:solved-aac
Open

Angel Asensio - Solved#18
asensiodev wants to merge 1 commit intoLIDR-academy:mainfrom
asensiodev:solved-aac

Conversation

@asensiodev
Copy link
Copy Markdown

@asensiodev asensiodev commented Apr 14, 2026

Implement candidate models and migration, POST /api/candidates with multipart validation and PDF/DOCX uploads, and React dashboard plus form. Default API port 3011 to avoid clashes with other local LTI projects on 3010. Root GET / returns HTML clarifying API vs frontend URL.

Add detailed manual test guide and SDD-style tickets under docs/tickets. Stop tracking backend/.env; add .env.example and ignore **/.env.

Made-with: Cursor

Summary by CodeRabbit

Release Notes

New Features

  • Added candidate management system with form supporting name, email, phone, address, education, and work experience
  • Implemented resume/CV file upload with PDF/DOCX validation and file size limits
  • Added duplicate email detection and validation error messaging
  • Introduced success confirmation screen after candidate creation

Tests

  • Added comprehensive test coverage for candidate creation workflow

Documentation

  • Added step-by-step testing guide for the candidate creation flow
  • Updated setup instructions for backend and database configuration

Implement candidate models and migration, POST /api/candidates with
multipart validation and PDF/DOCX uploads, and React dashboard plus form.
Default API port 3011 to avoid clashes with other local LTI projects on 3010.
Root GET / returns HTML clarifying API vs frontend URL.

Add detailed manual test guide and SDD-style tickets under docs/tickets.
Stop tracking backend/.env; add .env.example and ignore **/.env.

Made-with: Cursor
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

📝 Walkthrough

Walkthrough

This pull request implements the "Add Candidate to ATS" feature with full-stack changes: database schema and migrations for candidate records with education/experience/resume relations, backend Express API with file upload handling and validation, environment configuration updates, and frontend React components for candidate creation workflow with multi-step routing.

Changes

Cohort / File(s) Summary
Database & Configuration
.gitignore, backend/.env, backend/.env.example, docker-compose.yml, backend/prisma/schema.prisma, backend/prisma/migrations/...
Added .env ignore pattern, created .env.example with database credentials, removed credentials from .env, updated Docker Compose DB credentials, created Prisma schema for Candidate/CandidateEducation/CandidateWorkExperience/CandidateResume models with cascade deletes and indexes, and added migration with 87 lines of SQL table definitions and constraints.
Backend Core & Error Handling
backend/src/app.ts, backend/src/index.ts, backend/src/prisma.ts, backend/src/errors/ApiError.ts
Refactored Express setup: moved app creation and CORS configuration to app.ts, separated Prisma initialization to prisma.ts, added centralized error-handling middleware responding with ApiError shape, and updated index.ts to start server only outside test environment.
Backend Candidate Feature
backend/src/routes/candidates.ts, backend/src/services/candidateService.ts, backend/src/validators/candidatePayload.ts, backend/src/middleware/candidateUpload.ts
Implemented POST /api/candidates endpoint with Multer file upload (PDF/DOCX filtering, UUID-based storage), Zod payload validation, transactional Prisma service for creating candidate with nested relations, and centralized route error handler mapping Prisma/Multer/validation errors to HTTP status codes (400, 409, 413, 500).
Backend Dependencies & Tests
backend/package.json, backend/src/tests/app.test.ts, backend/src/tests/app.test.js, backend/src/tests/candidates.test.ts
Added cors, multer, zod runtime dependencies and TypeScript type packages; added prisma:migrate script; replaced root route assertion with HTML content verification; added integration tests for candidate creation validating 400/201/409 responses and service invocation.
Frontend Infrastructure & Styling
frontend/jest.config.js, frontend/package.json, frontend/src/App.tsx, frontend/src/App.css, frontend/src/api/candidates.ts, frontend/README.md
Added Jest/Jsdom configuration with stylesheet mocking; added ts-jest and jest-environment-jsdom dev dependencies; refactored App to state-driven view router (Dashboard/Form/Success); replaced .App* styles with .ats-* design system (grid, buttons, forms, alerts); created API client with createCandidate(FormData) function and response types.
Frontend Pages & Components
frontend/src/pages/Dashboard.tsx, frontend/src/pages/AddCandidate.tsx, frontend/src/pages/SuccessScreen.tsx, frontend/src/tests/App.test.tsx, frontend/src/tests/styleMock.js
Added Dashboard page with "Add Candidate" button, AddCandidate form page (497 lines) with client validation, dynamic education/experience rows, resume upload, error mapping, and accessibility attributes; added SuccessScreen showing created candidate details; updated app tests to verify navigation flow.
Documentation
README.md, docs/GUIA_PRUEBA_ADD_CANDIDATE.md, docs/tickets/add-candidate/README.md, docs/tickets/add-candidate/TICKET-*.md, prompts.md
Updated README with backend directory requirement, port 3011 default, Docker cleanup guidance, and DATABASE_URL configuration instructions; added comprehensive Spanish test walkthrough; added ticket documentation for database/backend/frontend scopes with acceptance criteria; added project prompts registry.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser/Frontend
    participant ReactApp as React App
    participant API as Express API
    participant FileSystem as File System
    participant Database as PostgreSQL

    Browser->>ReactApp: User submits form with resume file
    ReactApp->>ReactApp: Validate fields, check file extension
    ReactApp->>API: POST /api/candidates (FormData)
    
    API->>API: Run Multer upload middleware
    API->>FileSystem: Store resume with UUID filename
    API->>API: Parse & validate payload (Zod)
    
    alt Validation Fails
        API-->>ReactApp: 400 VALIDATION_ERROR
        ReactApp-->>Browser: Display field errors
    else Email Duplicate
        API->>Database: Check email uniqueness
        Database-->>API: Email exists
        API-->>ReactApp: 409 DUPLICATE_EMAIL
        ReactApp-->>Browser: Show email error
    else Success
        API->>Database: Start transaction
        Database->>Database: CREATE Candidate
        Database->>Database: CREATE CandidateEducation (if any)
        Database->>Database: CREATE CandidateWorkExperience (if any)
        Database->>Database: CREATE CandidateResume
        Database->>Database: Commit transaction
        API->>Database: Fetch full candidate with relations
        Database-->>API: Return candidate data
        API-->>ReactApp: 201 Created (candidate JSON)
        ReactApp->>ReactApp: Update app state to success view
        ReactApp-->>Browser: Display success screen
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Poem

🐰 A Candidate's Journey

Database tables bloom with care,
Prisma's migrations everywhere,
Files hop upward through the air,
Frontend forms validate with flair,
Success screens greet each new player! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'Angel Asensio - Solved' does not describe the changeset; it appears to be an author name followed by a status label, not a summary of the actual changes made. Replace the title with a clear, concise summary of the main change, such as 'Add candidate management feature with form and backend API' or 'Implement candidate creation with CV upload and dashboard UI'.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

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: 11

🧹 Nitpick comments (5)
backend/src/tests/app.test.ts (1)

9-10: Make the frontend URL assertion less brittle.

Asserting a fixed localhost:3000 can break when local frontend port changes. Prefer checking presence of “frontend” plus a generic localhost URL pattern.

Example resilient assertion tweak
     expect(response.text).toContain('API');
-    expect(response.text).toContain('localhost:3000');
+    expect(response.text.toLowerCase()).toContain('frontend');
+    expect(response.text).toMatch(/localhost:\d{2,5}/);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/tests/app.test.ts` around lines 9 - 10, The assertion is brittle
because it expects a fixed "localhost:3000"; update the test that uses response
(in backend/src/tests/app.test.ts) to instead assert for the presence of
"frontend" and a generic localhost URL pattern; replace the
expect(response.text).toContain('localhost:3000') with a more resilient check
that verifies response.text contains "frontend" and/or uses
expect(...).toMatch(/localhost:\d+/) (or a combined regex) so any local port
passes.
backend/src/tests/candidates.test.ts (1)

18-105: Good test coverage for core scenarios.

The tests cover three important paths:

  • Validation failure (400) with invalid email
  • Successful creation (201) with education data
  • Duplicate email conflict (409) via Prisma P2002

Consider adding tests for:

  • File upload (resume) scenarios
  • Missing required fields validation
  • File size/type rejection (413, 400)

These can be added in a follow-up if not blocking.

,

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

In `@backend/src/tests/candidates.test.ts` around lines 18 - 105, Add tests to
cover resume file upload and missing/invalid file handling: extend
backend/src/tests/candidates.test.ts by adding cases that use
request(app).post('/api/candidates') with .attach('resume', ...) to assert
successful upload (201 and resume present in response) and rejection scenarios
(oversized file -> expect 413, wrong mime type -> expect 400). Also add tests
for missing required fields (e.g., omit firstName or email) expecting 400
VALIDATION_ERROR and ensure mockedCreate (the mock for
candidateService.createCandidateWithRelations) is not called; reuse mockedCreate
and Prisma.PrismaClientKnownRequestError patterns from existing tests to
simulate backend behavior.
backend/prisma/migrations/20260409140000_add_candidate_ats/migration.sql (1)

65-87: Appropriate indexes and constraints.

  • Unique indexes on User.email and Candidate.email enforce uniqueness and enable efficient lookups
  • Unique constraint on CandidateResume.candidateId enforces 1:1 relationship
  • CASCADE delete/update maintains referential integrity

Consider adding indexes on CandidateEducation.candidateId and CandidateWorkExperience.candidateId if you expect frequent queries loading a candidate with their related records. Postgres doesn't auto-index foreign keys. This is optional for small datasets.

,

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

In `@backend/prisma/migrations/20260409140000_add_candidate_ats/migration.sql`
around lines 65 - 87, Add non-unique indexes on the foreign key columns to speed
joins when loading a candidate's related rows: create indexes on
CandidateEducation("candidateId") and CandidateWorkExperience("candidateId") in
the migration (referencing the tables CandidateEducation and
CandidateWorkExperience and the column candidateId) so the DB can efficiently
query by candidateId; add appropriately named indexes (e.g.,
CandidateEducation_candidateId_idx and CandidateWorkExperience_candidateId_idx)
alongside the existing CREATE INDEX statements in migration.sql.
frontend/src/App.tsx (1)

8-26: Clean state-driven view routing with type-safe discriminated union.

The View type with discriminated union ensures view.candidate is only accessible when view.name === 'success', providing compile-time safety. The callback wiring for navigation between views is straightforward.

The conditional rendering pattern works but could be slightly cleaner:

,

♻️ Optional: Cleaner conditional rendering
-      {view.name === 'dashboard' ? (
-        <Dashboard onAddCandidate={() => setView({ name: 'form' })} />
-      ) : null}
-      {view.name === 'form' ? (
-        <AddCandidate
-          onCancel={() => setView({ name: 'dashboard' })}
-          onSuccess={(candidate) => setView({ name: 'success', candidate })}
-        />
-      ) : null}
-      {view.name === 'success' ? (
-        <SuccessScreen candidate={view.candidate} onBack={() => setView({ name: 'dashboard' })} />
-      ) : null}
+      {view.name === 'dashboard' && (
+        <Dashboard onAddCandidate={() => setView({ name: 'form' })} />
+      )}
+      {view.name === 'form' && (
+        <AddCandidate
+          onCancel={() => setView({ name: 'dashboard' })}
+          onSuccess={(candidate) => setView({ name: 'success', candidate })}
+        />
+      )}
+      {view.name === 'success' && (
+        <SuccessScreen candidate={view.candidate} onBack={() => setView({ name: 'dashboard' })} />
+      )}

Using && short-circuit is more idiomatic for conditional rendering in React.

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

In `@frontend/src/App.tsx` around lines 8 - 26, Replace the ternary-with-null
conditional renders in the App component with idiomatic short-circuit rendering
(use &&) to simplify the JSX: for each conditional currently using patterns like
{view.name === 'dashboard' ? (<Dashboard ... />) : null}, change to {view.name
=== 'dashboard' && <Dashboard onAddCandidate={() => setView({ name: 'form' })}
/>}, and similarly for AddCandidate and SuccessScreen while keeping the same
props and the discriminated union View, setView, and candidate handling intact.
frontend/src/pages/AddCandidate.tsx (1)

57-64: Static arrays can be module-level constants.

The useMemo hooks with empty dependency arrays wrap static string arrays that never change. Moving them outside the component as constants eliminates unnecessary hook overhead.

♻️ Proposed refactor
+const SUGGESTED_INSTITUTIONS = ['Universidad Politécnica de Madrid', 'Universidad Complutense', 'Universidad de Barcelona', 'ESADE', 'IE'];
+const SUGGESTED_COMPANIES = ['Acme Corp', 'Globex', 'Initech', 'Umbrella', 'Stark Industries'];
+
 export function AddCandidate({ onCancel, onSuccess }: Props) {
   const formId = useId();
   // ... state declarations ...
-
-  const suggestedInstitutions = useMemo(
-    () => ['Universidad Politécnica de Madrid', 'Universidad Complutense', 'Universidad de Barcelona', 'ESADE', 'IE'],
-    [],
-  );
-  const suggestedCompanies = useMemo(
-    () => ['Acme Corp', 'Globex', 'Initech', 'Umbrella', 'Stark Industries'],
-    [],
-  );

Then use SUGGESTED_INSTITUTIONS and SUGGESTED_COMPANIES directly in the JSX.

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

In `@frontend/src/pages/AddCandidate.tsx` around lines 57 - 64, The arrays wrapped
by useMemo (suggestedInstitutions and suggestedCompanies) are static and should
be moved to module-level constants to remove unnecessary hooks; create top-level
constants (e.g., SUGGESTED_INSTITUTIONS and SUGGESTED_COMPANIES) with the
respective string arrays and replace usages of suggestedInstitutions and
suggestedCompanies in AddCandidate.tsx with those constants, removing the
useMemo declarations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/.env.example`:
- Around line 3-7: Current .env.example defines both
DB_PASSWORD/DB_USER/DB_NAME/DB_PORT and DATABASE_URL which creates two sources
of truth (Prisma uses DATABASE_URL); remove the DB_* entries from .env.example
(or alternatively remove DATABASE_URL and document switching Prisma to use DB_*)
so there is a single canonical connection string—update the example to keep only
DATABASE_URL (and a short comment instructing maintainers to update
DATABASE_URL) and ensure Prisma and any startup scripts reference DATABASE_URL
consistently.

In `@backend/src/middleware/candidateUpload.ts`:
- Around line 47-52: Validate MAX_UPLOAD_MB at startup by parsing
process.env.MAX_UPLOAD_MB into a Number and ensuring it is finite and > 0 before
using it to compute the Multer fileSize limit; update the logic around the maxMb
variable used by candidateUploadMiddleware to fall back to the default (10) when
the parsed value is NaN, non-finite, or <= 0, and log or throw a clear error at
module init if you prefer strict failure instead of silent fallback so negative
or invalid values cannot produce an invalid limits.fileSize for multer/busboy.

In `@backend/src/routes/candidates.ts`:
- Around line 69-79: When parseEducationJson or parseExperienceJson throws and
you return the ApiError 400 path, ensure the uploaded file is removed first to
avoid orphaned PII: in the catch blocks around parseEducationJson and
parseExperienceJson (and the analogous 92-97 block) call a cleanup that removes
req.file.path if req.file exists before calling next(new ApiError(...)) and
returning; reference candidateUploadMiddleware which writes the file, and use
the same file path cleanup logic (delete/unlink) used elsewhere in the codebase
so the file is always deleted on these pre-database validation failures.

In `@backend/src/validators/candidatePayload.ts`:
- Around line 7-8: The startDate and endDate fields in the candidate payload
schema currently use z.string().trim().optional().nullable() and accept
arbitrary text; replace those with string validators that enforce YYYY, YYYY-MM
or YYYY-MM-DD formats using a regex (e.g. /^\d{4}(-\d{2}(-\d{2})?)?$/) while
preserving optional()/nullable() semantics, update both occurrences of startDate
and endDate in the validator (including any nested employment/education entries)
to use z.string().trim().regex(...).refine or .regex(..., { message: 'invalid
date format' }) so invalid values fail safeParse() and tests reflect the new
error message.

In `@docs/tickets/add-candidate/TICKET-02-backend-api.md`:
- Around line 15-17: Actualiza la especificación del endpoint de creación de
candidato para convertir la elección actual (single-request multipart/form-data)
en un contrato claro: documenta que POST /candidates acepta Content-Type:
multipart/form-data con campos JSON planos (e.g., name, email, education[],
experience[]) y un campo de archivo resume (campo: "resume") que es opcional;
valida tipos MIME aceptados (application/pdf,
application/vnd.openxmlformats-officedocument.wordprocessingml.document), tamaño
máximo (p. ej. 5MB), y esquema de campos requeridos/formatos (email regex,
fechas ISO, etc.); especifica códigos de respuesta y cuerpos de error coherentes
(400 con {error, details}, 422 para validación, 201 con {id, ...} al crear), y
referencia las implementaciones actuales middleware candidateUpload
(candidateUpload.ts) y la ruta POST handler en routes/candidates.ts para que
tests y frontend esperen ese formato exacto.
- Around line 95-99: Update the docs to show both distinct 400 response shapes
emitted by the candidate creation route: one for INVALID_JSON when parsing
malformed `education`/`experience` JSON (the parse block that returns
`INVALID_JSON`) and one for VALIDATION_ERROR that returns a `message` plus the
flattened validation errors (the validation path that uses
`parsed.error.flatten()`). Add example payloads for each variant (INVALID_JSON
with an `error` and `field`/`reason` info, and VALIDATION_ERROR with `message`
and a `details` object/array matching the flattened error shape) and clearly
label which runtime condition produces each response.

In `@frontend/package.json`:
- Around line 44-46: The package.json currently lists ts-jest and
jest-environment-jsdom at v29 but keeps `@types/jest`@^27.x in dependencies and
lacks an explicit jest devDependency; update package.json so that "jest" is
added to devDependencies (matching v29.x), upgrade or add "@types/jest" to a
v29-compatible release (or remove it entirely if you rely on Jest's built-in
types), ensure "ts-jest" and "jest-environment-jsdom" remain on v29, and remove
any "@types/jest" entry from dependencies (move to devDependencies if kept) so
types and runtime versions match.

In `@frontend/src/api/candidates.ts`:
- Around line 1-2: getBaseUrl currently falls back to 'http://localhost:3011'
unconditionally; change it so the localhost fallback is only used in development
and production uses a same-origin relative URL. Update the getBaseUrl function
to: if REACT_APP_API_URL is set, return it with trailing slash removed; else if
process.env.NODE_ENV === 'development' return 'http://localhost:3011'; otherwise
return a relative base (e.g., '' or '/') so production builds never hardcode
localhost. Ensure you reference getBaseUrl and REACT_APP_API_URL (and NODE_ENV)
when making this change.

In `@frontend/src/App.css`:
- Around line 76-85: Add a missing disabled style for the secondary button by
creating a .ats-btn-secondary:disabled rule that mirrors the primary button's
disabled appearance—set muted color, transparent background, and a subdued
border-color and cursor: not-allowed so the "Volver al panel" button (which uses
disabled={submitting} in AddCandidate.tsx) looks and behaves consistently;
update the CSS near the existing .ats-btn-secondary and its :hover rule to
include this :disabled selector referencing .ats-btn-secondary:disabled.

In `@frontend/src/pages/AddCandidate.tsx`:
- Around line 315-327: The degree input (and similarly the job title input and
resume error span) is missing an id and aria-describedby so error spans aren't
announced; update the degree input in the education row render (where
value={row.degree} and setEducation(...) is used) to include a unique id like
`edu_deg_${i}` and set aria-describedby to the corresponding error span id
(e.g., `edu_deg_${i}_error`) only when fieldErrors[`edu_deg_${i}`] exists, and
ensure the error span has that id and role="status"; apply the same pattern to
the job title input (matching its fieldErrors key, e.g., `job_title_${i}`) and
to the resume input/error span (matching the resume fieldErrors key) so screen
readers will announce the error messages.

In `@prompts.md`:
- Line 51: Fix typos in the prompt text on line containing "Puedes crear una
carpeta dentro del proyecto ccon el nomvre que mejor consideres y definir 3
tickets de trabajo para cda uno de las tareas tecnicas?" by changing "ccon el
nomvre" to "con el nombre" and "cda" to "cada", and optionally correct
"nomvre"→"nombre" and "tecnicas"→"técnicas" to improve clarity; update the
sentence in prompts.md so it reads correctly in Spanish while preserving the
original intent.

---

Nitpick comments:
In `@backend/prisma/migrations/20260409140000_add_candidate_ats/migration.sql`:
- Around line 65-87: Add non-unique indexes on the foreign key columns to speed
joins when loading a candidate's related rows: create indexes on
CandidateEducation("candidateId") and CandidateWorkExperience("candidateId") in
the migration (referencing the tables CandidateEducation and
CandidateWorkExperience and the column candidateId) so the DB can efficiently
query by candidateId; add appropriately named indexes (e.g.,
CandidateEducation_candidateId_idx and CandidateWorkExperience_candidateId_idx)
alongside the existing CREATE INDEX statements in migration.sql.

In `@backend/src/tests/app.test.ts`:
- Around line 9-10: The assertion is brittle because it expects a fixed
"localhost:3000"; update the test that uses response (in
backend/src/tests/app.test.ts) to instead assert for the presence of "frontend"
and a generic localhost URL pattern; replace the
expect(response.text).toContain('localhost:3000') with a more resilient check
that verifies response.text contains "frontend" and/or uses
expect(...).toMatch(/localhost:\d+/) (or a combined regex) so any local port
passes.

In `@backend/src/tests/candidates.test.ts`:
- Around line 18-105: Add tests to cover resume file upload and missing/invalid
file handling: extend backend/src/tests/candidates.test.ts by adding cases that
use request(app).post('/api/candidates') with .attach('resume', ...) to assert
successful upload (201 and resume present in response) and rejection scenarios
(oversized file -> expect 413, wrong mime type -> expect 400). Also add tests
for missing required fields (e.g., omit firstName or email) expecting 400
VALIDATION_ERROR and ensure mockedCreate (the mock for
candidateService.createCandidateWithRelations) is not called; reuse mockedCreate
and Prisma.PrismaClientKnownRequestError patterns from existing tests to
simulate backend behavior.

In `@frontend/src/App.tsx`:
- Around line 8-26: Replace the ternary-with-null conditional renders in the App
component with idiomatic short-circuit rendering (use &&) to simplify the JSX:
for each conditional currently using patterns like {view.name === 'dashboard' ?
(<Dashboard ... />) : null}, change to {view.name === 'dashboard' && <Dashboard
onAddCandidate={() => setView({ name: 'form' })} />}, and similarly for
AddCandidate and SuccessScreen while keeping the same props and the
discriminated union View, setView, and candidate handling intact.

In `@frontend/src/pages/AddCandidate.tsx`:
- Around line 57-64: The arrays wrapped by useMemo (suggestedInstitutions and
suggestedCompanies) are static and should be moved to module-level constants to
remove unnecessary hooks; create top-level constants (e.g.,
SUGGESTED_INSTITUTIONS and SUGGESTED_COMPANIES) with the respective string
arrays and replace usages of suggestedInstitutions and suggestedCompanies in
AddCandidate.tsx with those constants, removing the useMemo declarations.
🪄 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: fba4e366-eff9-4270-9b72-6f8f809acc75

📥 Commits

Reviewing files that changed from the base of the PR and between 3d3d798 and 2e95f06.

⛔ Files ignored due to path filters (2)
  • backend/package-lock.json is excluded by !**/package-lock.json
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (37)
  • .gitignore
  • README.md
  • backend/.env
  • backend/.env.example
  • backend/package.json
  • backend/prisma/migrations/20260409140000_add_candidate_ats/migration.sql
  • backend/prisma/migrations/migration_lock.toml
  • backend/prisma/schema.prisma
  • backend/src/app.ts
  • backend/src/errors/ApiError.ts
  • backend/src/index.ts
  • backend/src/middleware/candidateUpload.ts
  • backend/src/prisma.ts
  • backend/src/routes/candidates.ts
  • backend/src/services/candidateService.ts
  • backend/src/tests/app.test.js
  • backend/src/tests/app.test.ts
  • backend/src/tests/candidates.test.ts
  • backend/src/validators/candidatePayload.ts
  • docker-compose.yml
  • docs/GUIA_PRUEBA_ADD_CANDIDATE.md
  • docs/tickets/add-candidate/README.md
  • docs/tickets/add-candidate/TICKET-01-base-de-datos.md
  • docs/tickets/add-candidate/TICKET-02-backend-api.md
  • docs/tickets/add-candidate/TICKET-03-frontend-ui.md
  • frontend/README.md
  • frontend/jest.config.js
  • frontend/package.json
  • frontend/src/App.css
  • frontend/src/App.tsx
  • frontend/src/api/candidates.ts
  • frontend/src/pages/AddCandidate.tsx
  • frontend/src/pages/Dashboard.tsx
  • frontend/src/pages/SuccessScreen.tsx
  • frontend/src/tests/App.test.tsx
  • frontend/src/tests/styleMock.js
  • prompts.md
💤 Files with no reviewable changes (2)
  • backend/.env
  • backend/src/tests/app.test.js

Comment thread backend/.env.example
Comment on lines +3 to +7
DB_PASSWORD=password
DB_USER=postgres
DB_NAME=mydatabase
DB_PORT=5432
DATABASE_URL="postgresql://postgres:password@localhost:5432/mydatabase"
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

Avoid config drift between DB_* keys and DATABASE_URL.

Right now there are two sources of truth, but Prisma reads DATABASE_URL. If someone edits DB_* only, the app still points to old credentials.

Suggested simplification
 PORT=3011
-DB_PASSWORD=password
-DB_USER=postgres
-DB_NAME=mydatabase
-DB_PORT=5432
-DATABASE_URL="postgresql://postgres:password@localhost:5432/mydatabase"
+DATABASE_URL=postgresql://postgres:password@localhost:5432/mydatabase
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 3-3: [UnorderedKey] The DB_PASSWORD key should go before the PORT key

(UnorderedKey)


[warning] 4-4: [UnorderedKey] The DB_USER key should go before the PORT key

(UnorderedKey)


[warning] 5-5: [UnorderedKey] The DB_NAME key should go before the DB_PASSWORD key

(UnorderedKey)


[warning] 6-6: [UnorderedKey] The DB_PORT key should go before the DB_USER key

(UnorderedKey)


[warning] 7-7: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)


[warning] 7-7: [UnorderedKey] The DATABASE_URL key should go before the DB_NAME key

(UnorderedKey)

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

In `@backend/.env.example` around lines 3 - 7, Current .env.example defines both
DB_PASSWORD/DB_USER/DB_NAME/DB_PORT and DATABASE_URL which creates two sources
of truth (Prisma uses DATABASE_URL); remove the DB_* entries from .env.example
(or alternatively remove DATABASE_URL and document switching Prisma to use DB_*)
so there is a single canonical connection string—update the example to keep only
DATABASE_URL (and a short comment instructing maintainers to update
DATABASE_URL) and ensure Prisma and any startup scripts reference DATABASE_URL
consistently.

Comment on lines +47 to +52
const maxMb = Number(process.env.MAX_UPLOAD_MB) || 10;

export const candidateUploadMiddleware = multer({
storage,
fileFilter,
limits: { fileSize: maxMb * 1024 * 1024 },
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

🧩 Analysis chain

🏁 Script executed:

find backend -name "candidateUpload.ts" -type f

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 131


🏁 Script executed:

git ls-files backend/src/middleware/ | head -20

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 131


🏁 Script executed:

wc -l backend/src/middleware/candidateUpload.ts

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 134


🏁 Script executed:

cat -n backend/src/middleware/candidateUpload.ts | sed -n '40,60p'

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 512


🏁 Script executed:

grep -r "MAX_UPLOAD_MB" backend --include="*.ts" --include="*.js"

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 186


🏁 Script executed:

find backend -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "candidateUpload\|MAX_UPLOAD" 2>/dev/null

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 74


🏁 Script executed:

grep -r "process.env\." backend/src --include="*.ts" -A 2 | head -40

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 895


🏁 Script executed:

cat -n backend/src/middleware/candidateUpload.ts

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 1907


🏁 Script executed:

grep -r "fileSize\|upload\|resume" backend/src --include="*.ts" -B 2 -A 2 | head -50

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 3087


🏁 Script executed:

grep -r "fileSize\|multer" backend/src --include="*.ts" -B 3 -A 3 | grep -A 10 -B 10 "error\|Error\|catch"

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 1171


🌐 Web query:

Multer fileSize negative limit behavior documentation

💡 Result:

Multer's official documentation does not explicitly document the behavior of negative values for the limits.fileSize option. The limits object is passed directly to the underlying busboy parser without any validation or normalization for negative values in current released versions (up to v2.1.x as of 2026). Busboy, which handles the actual multipart parsing, expects fileSize to be a non-negative integer (default: Infinity). Negative values are likely treated as 0 or invalid: - Setting fileSize: 0 rejects all files immediately with a LIMIT_FILE_SIZE MulterError, as busboy emits the 'limit' event on the first byte (per busboy issue #297, where even fileSize: 1 rejects exactly 1-byte files). - Negative values (e.g., -1) are probably coerced to 0 by JavaScript Number handling or cause busboy to reject files similarly, since limits must be integers and negative limits make no sense for size constraints. A pending PR (#1335, opened 2025) proposes adding validation to throw a MulterError for invalid (negative/non-integer) limits before parsing starts, but it is not merged as of 2026-04-14. To disallow all file uploads, explicitly set limits: { files: 0 } or use multer.none. Avoid negative fileSize values, as they are unsupported and lead to undefined behavior equivalent to rejecting all files. Example error handling: app.use((err, req, res, next) => { if (err.code === 'LIMIT_FILE_SIZE') { res.status(400).send('File too large'); } });

Citations:


Validate MAX_UPLOAD_MB as a positive number at startup.

Line 47 uses loose parsing: Number(process.env.MAX_UPLOAD_MB) || 10. Negative values like -1 pass through (since -1 is truthy) and set an invalid limit. According to Multer/busboy behavior, negative fileSize limits cause all uploads to be rejected. Validate this env var as a finite positive number before creating the middleware.

🔧 Suggested fix
-const maxMb = Number(process.env.MAX_UPLOAD_MB) || 10;
+const configuredMaxMb = process.env.MAX_UPLOAD_MB;
+const maxMb = configuredMaxMb == null ? 10 : Number(configuredMaxMb);
+
+if (!Number.isFinite(maxMb) || maxMb <= 0) {
+  throw new Error('MAX_UPLOAD_MB must be a positive number');
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/middleware/candidateUpload.ts` around lines 47 - 52, Validate
MAX_UPLOAD_MB at startup by parsing process.env.MAX_UPLOAD_MB into a Number and
ensuring it is finite and > 0 before using it to compute the Multer fileSize
limit; update the logic around the maxMb variable used by
candidateUploadMiddleware to fall back to the default (10) when the parsed value
is NaN, non-finite, or <= 0, and log or throw a clear error at module init if
you prefer strict failure instead of silent fallback so negative or invalid
values cannot produce an invalid limits.fileSize for multer/busboy.

Comment on lines +69 to +79
try {
parsedEducation = parseEducationJson(req.body?.education);
} catch {
next(new ApiError(400, 'INVALID_JSON', 'education no es JSON válido'));
return;
}
try {
parsedExperience = parseExperienceJson(req.body?.experience);
} catch {
next(new ApiError(400, 'INVALID_JSON', 'experience no es JSON válido'));
return;
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

Delete rejected resumes on the pre-database 400 paths.

By the time Line 70 runs, candidateUploadMiddleware has already written the file to disk (backend/src/middleware/candidateUpload.ts:27-35). The early 400s at Line 72, Line 78, and Line 94 return without removing req.file.path, so invalid submissions leave orphaned CVs on disk and retain applicant PII.

🧹 Suggested fix
+async function cleanupUploadedFile(file?: Express.Multer.File) {
+  if (!file?.path) return;
+  try {
+    await fs.promises.unlink(file.path);
+  } catch {
+    /* ignore */
+  }
+}
+
 router.post('/', (req: Request, res: Response, next: NextFunction) => {
   void (async () => {
     const file = req.file;
@@
     try {
       parsedEducation = parseEducationJson(req.body?.education);
     } catch {
+      await cleanupUploadedFile(file);
       next(new ApiError(400, 'INVALID_JSON', 'education no es JSON válido'));
       return;
     }
     try {
       parsedExperience = parseExperienceJson(req.body?.experience);
     } catch {
+      await cleanupUploadedFile(file);
       next(new ApiError(400, 'INVALID_JSON', 'experience no es JSON válido'));
       return;
     }
@@
     if (!parsed.success) {
+      await cleanupUploadedFile(file);
       next(
         new ApiError(400, 'VALIDATION_ERROR', 'Datos de entrada no válidos', parsed.error.flatten()),
       );
       return;
     }
@@
     } catch (e) {
-      if (file?.path) {
-        try {
-          fs.unlinkSync(file.path);
-        } catch {
-          /* ignore */
-        }
-      }
+      await cleanupUploadedFile(file);
       if (isPrismaUniqueViolation(e)) {
         next(new ApiError(409, 'DUPLICATE_EMAIL', 'Ya existe un candidato con este correo'));
         return;

Also applies to: 92-97

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

In `@backend/src/routes/candidates.ts` around lines 69 - 79, When
parseEducationJson or parseExperienceJson throws and you return the ApiError 400
path, ensure the uploaded file is removed first to avoid orphaned PII: in the
catch blocks around parseEducationJson and parseExperienceJson (and the
analogous 92-97 block) call a cleanup that removes req.file.path if req.file
exists before calling next(new ApiError(...)) and returning; reference
candidateUploadMiddleware which writes the file, and use the same file path
cleanup logic (delete/unlink) used elsewhere in the codebase so the file is
always deleted on these pre-database validation failures.

Comment on lines +7 to +8
startDate: z.string().trim().optional().nullable(),
endDate: z.string().trim().optional().nullable(),
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

🏁 Script executed:

cat -n backend/src/validators/candidatePayload.ts

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 1877


🏁 Script executed:

cat -n backend/src/services/candidateService.ts | head -60

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 2252


🏁 Script executed:

find . -name "schema.prisma" -type f

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 120


🏁 Script executed:

cat -n backend/prisma/schema.prisma

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 2572


🏁 Script executed:

find backend -name "*.ts" -o -name "*.tsx" | head -20

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 408


🏁 Script executed:

find . -type f \( -name "*.tsx" -o -name "*.ts" -o -name "*.jsx" -o -name "*.js" \) | grep -E "(form|candidate|date)" | head -20

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 366


🏁 Script executed:

cat -n frontend/src/pages/AddCandidate.tsx

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 22351


🏁 Script executed:

cat -n backend/src/tests/candidates.test.ts

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 3990


Validate date fields instead of accepting arbitrary text.

Lines 7-8 and 14-15 currently accept any trimmed string without format validation, so arbitrary values like "later" pass safeParse() and persist unchanged to the database. Add format validation for these date fields.

🔧 Suggested fix
+const optionalDateString = z
+  .string()
+  .trim()
+  .refine((value) => value === '' || /^\d{4}(-\d{2})?(-\d{2})?$/.test(value), 'Fecha no válida')
+  .transform((value) => (value === '' ? null : value))
+  .optional()
+  .nullable();
+
 const educationItemSchema = z.object({
   institution: z.string().trim().min(1, 'institution requerido'),
   degree: z.string().trim().min(1, 'degree requerido'),
   field: z.string().trim().optional().nullable(),
-  startDate: z.string().trim().optional().nullable(),
-  endDate: z.string().trim().optional().nullable(),
+  startDate: optionalDateString,
+  endDate: optionalDateString,
 });
@@
 const experienceItemSchema = z.object({
   company: z.string().trim().min(1, 'company requerido'),
   title: z.string().trim().min(1, 'title requerido'),
-  startDate: z.string().trim().optional().nullable(),
-  endDate: z.string().trim().optional().nullable(),
+  startDate: optionalDateString,
+  endDate: optionalDateString,
   description: z.string().trim().optional().nullable(),
 });

The regex allows YYYY, YYYY-MM, or YYYY-MM-DD formats, matching the frontend placeholder examples ("2018 o 2018-09") and test expectations.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
startDate: z.string().trim().optional().nullable(),
endDate: z.string().trim().optional().nullable(),
const optionalDateString = z
.string()
.trim()
.refine((value) => value === '' || /^\d{4}(-\d{2})?(-\d{2})?$/.test(value), 'Fecha no válida')
.transform((value) => (value === '' ? null : value))
.optional()
.nullable();
const educationItemSchema = z.object({
institution: z.string().trim().min(1, 'institution requerido'),
degree: z.string().trim().min(1, 'degree requerido'),
field: z.string().trim().optional().nullable(),
startDate: optionalDateString,
endDate: optionalDateString,
});
const experienceItemSchema = z.object({
company: z.string().trim().min(1, 'company requerido'),
title: z.string().trim().min(1, 'title requerido'),
startDate: optionalDateString,
endDate: optionalDateString,
description: z.string().trim().optional().nullable(),
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/validators/candidatePayload.ts` around lines 7 - 8, The startDate
and endDate fields in the candidate payload schema currently use
z.string().trim().optional().nullable() and accept arbitrary text; replace those
with string validators that enforce YYYY, YYYY-MM or YYYY-MM-DD formats using a
regex (e.g. /^\d{4}(-\d{2}(-\d{2})?)?$/) while preserving optional()/nullable()
semantics, update both occurrences of startDate and endDate in the validator
(including any nested employment/education entries) to use
z.string().trim().regex(...).refine or .regex(..., { message: 'invalid date
format' }) so invalid values fail safeParse() and tests reflect the new error
message.

Comment on lines +15 to +17
El backend actual (`backend/src/index.ts`) expone una ruta de prueba y manejo genérico de errores. Se necesita una **API REST** (o RPC documentado) que permita al reclutador **crear un candidato** con sus datos, educación, experiencia y **subida opcional u obligatoria de CV** en **PDF o DOCX**, con **validación**, **respuestas coherentes** y **mensajes de error** comprensibles para el cliente.

**Objetivo entregable:** endpoint(s) funcionando contra PostgreSQL vía Prisma, subida de fichero persistente, validación servidor, tests automatizados mínimos y notas de seguridad.
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

Turn the chosen upload flow into a concrete contract.

This ticket still leaves both "multipart vs two-step" and "optional vs required CV" open-ended. The implementation has already chosen single-request multipart/form-data, and resume is optional (backend/src/routes/candidates.ts:53-129, backend/src/middleware/candidateUpload.ts:49-53). Keeping the contract ambiguous here is enough for frontend and tests to drift.

📝 Suggested doc update
-El backend actual (`backend/src/index.ts`) expone una ruta de prueba y manejo genérico de errores. Se necesita una **API REST** (o RPC documentado) que permita al reclutador **crear un candidato** con sus datos, educación, experiencia y **subida opcional u obligatoria de CV** en **PDF o DOCX**, con **validación**, **respuestas coherentes** y **mensajes de error** comprensibles para el cliente.
+El backend actual (`backend/src/index.ts`) expone una ruta de prueba y manejo genérico de errores. Se necesita una **API REST** que permita al reclutador **crear un candidato** con sus datos, educación, experiencia y **subida opcional de CV** en **PDF o DOCX**, con **validación**, **respuestas coherentes** y **mensajes de error** comprensibles para el cliente.
@@
-- Aceptación **multipart/form-data** si el CV se envía en la misma petición (recomendado para UX del ticket 03), **o** flujo en dos pasos (crear candidato + `POST /api/candidates/:id/resume`); **elegir una** y documentarla en §4.
+- Aceptación de **multipart/form-data** en `POST /api/candidates`, adjuntando `resume` en la misma petición cuando exista.
@@
-| `resume` | file | opcional/obligatorio según producto |
+| `resume` | file | opcional |

Also applies to: 25-30, 65-77

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

In `@docs/tickets/add-candidate/TICKET-02-backend-api.md` around lines 15 - 17,
Actualiza la especificación del endpoint de creación de candidato para convertir
la elección actual (single-request multipart/form-data) en un contrato claro:
documenta que POST /candidates acepta Content-Type: multipart/form-data con
campos JSON planos (e.g., name, email, education[], experience[]) y un campo de
archivo resume (campo: "resume") que es opcional; valida tipos MIME aceptados
(application/pdf,
application/vnd.openxmlformats-officedocument.wordprocessingml.document), tamaño
máximo (p. ej. 5MB), y esquema de campos requeridos/formatos (email regex,
fechas ISO, etc.); especifica códigos de respuesta y cuerpos de error coherentes
(400 con {error, details}, 422 para validación, 201 con {id, ...} al crear), y
referencia las implementaciones actuales middleware candidateUpload
(candidateUpload.ts) y la ruta POST handler en routes/candidates.ts para que
tests y frontend esperen ese formato exacto.

Comment thread frontend/package.json
Comment on lines +44 to +46
"devDependencies": {
"jest-environment-jsdom": "^29.7.0",
"ts-jest": "^29.1.2"
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

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Declared frontend test stack:"
jq '{
  test_script: .scripts.test,
  dependencies: ((.dependencies // {}) | with_entries(select(.key|test("(^react-scripts$|^@types/jest$)")))),
  devDependencies: ((.devDependencies // {}) | with_entries(select(.key|test("(^jest$|^ts-jest$|^jest-environment-jsdom$|^@types/jest$)"))))
}' frontend/package.json

if [ -f frontend/package-lock.json ]; then
  echo "Resolved versions from lockfile:"
  jq -r '{
    jest: .packages["node_modules/jest"].version,
    ts_jest: .packages["node_modules/ts-jest"].version,
    jest_environment_jsdom: .packages["node_modules/jest-environment-jsdom"].version
  }' frontend/package-lock.json
fi

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 466


Update Jest and its type definitions to v29 for proper type support.

The package declares ts-jest@^29.1.2 and jest-environment-jsdom@^29.7.0, but carries @types/jest@^27.5.2 in dependencies. Lockfile resolves Jest to 29.7.0, creating a type definition mismatch that may hide TypeScript errors in tests. Additionally, jest should be explicitly declared in devDependencies to avoid accidental removal.

Suggested fix
  "devDependencies": {
+   "jest": "^29.7.0",
+   "@types/jest": "^29.5.12",
    "jest-environment-jsdom": "^29.7.0",
    "ts-jest": "^29.1.2"
  }

Also remove @types/jest from dependencies if present.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"devDependencies": {
"jest-environment-jsdom": "^29.7.0",
"ts-jest": "^29.1.2"
"devDependencies": {
"jest": "^29.7.0",
"@types/jest": "^29.5.12",
"jest-environment-jsdom": "^29.7.0",
"ts-jest": "^29.1.2"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/package.json` around lines 44 - 46, The package.json currently lists
ts-jest and jest-environment-jsdom at v29 but keeps `@types/jest`@^27.x in
dependencies and lacks an explicit jest devDependency; update package.json so
that "jest" is added to devDependencies (matching v29.x), upgrade or add
"@types/jest" to a v29-compatible release (or remove it entirely if you rely on
Jest's built-in types), ensure "ts-jest" and "jest-environment-jsdom" remain on
v29, and remove any "@types/jest" entry from dependencies (move to
devDependencies if kept) so types and runtime versions match.

Comment on lines +1 to +2
const getBaseUrl = (): string =>
process.env.REACT_APP_API_URL?.replace(/\/$/, '') || 'http://localhost:3011';
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

🏁 Script executed:

# First, let's see the actual code in the candidates.ts file
cat -n frontend/src/api/candidates.ts | head -20

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 705


🏁 Script executed:

# Check if there's any deployment/environment configuration
find . -type f \( -name ".env*" -o -name "*.config.*" -o -name "package.json" \) | grep -E "\.(env|config|package)" | head -20

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 163


🏁 Script executed:

# Search for how getBaseUrl is used in the codebase
rg "getBaseUrl" -n

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 251


🏁 Script executed:

# Check for REACT_APP_API_URL references to understand how it's configured
rg "REACT_APP_API_URL" -n

Repository: LIDR-academy/Ai4devs-ticket-LTI-2026-03-senior

Length of output: 1886


Don't default browser API calls to localhost outside development.

Line 2 falls back to http://localhost:3011 unconditionally. If REACT_APP_API_URL is not set during the production build, the bundle will contain undefined and callers will POST to the user's own machine instead of the hosted API. Restrict that fallback to development, or use a same-origin relative URL in production.

🔧 Suggested fix
-const getBaseUrl = (): string =>
-  process.env.REACT_APP_API_URL?.replace(/\/$/, '') || 'http://localhost:3011';
+const getBaseUrl = (): string => {
+  const configured = process.env.REACT_APP_API_URL?.replace(/\/+$/, '');
+  if (configured) return configured;
+  return process.env.NODE_ENV === 'development' ? 'http://localhost:3011' : '';
+};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getBaseUrl = (): string =>
process.env.REACT_APP_API_URL?.replace(/\/$/, '') || 'http://localhost:3011';
const getBaseUrl = (): string => {
const configured = process.env.REACT_APP_API_URL?.replace(/\/+$/, '');
if (configured) return configured;
return process.env.NODE_ENV === 'development' ? 'http://localhost:3011' : '';
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/api/candidates.ts` around lines 1 - 2, getBaseUrl currently
falls back to 'http://localhost:3011' unconditionally; change it so the
localhost fallback is only used in development and production uses a same-origin
relative URL. Update the getBaseUrl function to: if REACT_APP_API_URL is set,
return it with trailing slash removed; else if process.env.NODE_ENV ===
'development' return 'http://localhost:3011'; otherwise return a relative base
(e.g., '' or '/') so production builds never hardcode localhost. Ensure you
reference getBaseUrl and REACT_APP_API_URL (and NODE_ENV) when making this
change.

Comment thread frontend/src/App.css
Comment on lines +76 to +85
.ats-btn-secondary {
background: transparent;
color: #e2e8f0;
border-color: rgba(148, 163, 184, 0.45);
}

.ats-btn-secondary:hover:not(:disabled) {
border-color: #cbd5e1;
color: #fff;
}
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

Missing :disabled style for secondary button.

The primary button has explicit disabled styling (lines 71-74), but .ats-btn-secondary:disabled is missing. In AddCandidate.tsx, the "Volver al panel" button uses disabled={submitting}, so it needs consistent disabled styling.

🎨 Proposed fix
 .ats-btn-secondary:hover:not(:disabled) {
   border-color: `#cbd5e1`;
   color: `#fff`;
 }
+
+.ats-btn-secondary:disabled {
+  opacity: 0.65;
+  cursor: not-allowed;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.ats-btn-secondary {
background: transparent;
color: #e2e8f0;
border-color: rgba(148, 163, 184, 0.45);
}
.ats-btn-secondary:hover:not(:disabled) {
border-color: #cbd5e1;
color: #fff;
}
.ats-btn-secondary {
background: transparent;
color: `#e2e8f0`;
border-color: rgba(148, 163, 184, 0.45);
}
.ats-btn-secondary:hover:not(:disabled) {
border-color: `#cbd5e1`;
color: `#fff`;
}
.ats-btn-secondary:disabled {
opacity: 0.65;
cursor: not-allowed;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/App.css` around lines 76 - 85, Add a missing disabled style for
the secondary button by creating a .ats-btn-secondary:disabled rule that mirrors
the primary button's disabled appearance—set muted color, transparent
background, and a subdued border-color and cursor: not-allowed so the "Volver al
panel" button (which uses disabled={submitting} in AddCandidate.tsx) looks and
behaves consistently; update the CSS near the existing .ats-btn-secondary and
its :hover rule to include this :disabled selector referencing
.ats-btn-secondary:disabled.

Comment on lines +315 to +327
<input
value={row.degree}
onChange={(ev) => {
const v = ev.target.value;
setEducation((rows) => rows.map((r, j) => (j === i ? { ...r, degree: v } : r)));
}}
aria-invalid={!!fieldErrors[`edu_deg_${i}`]}
/>
{fieldErrors[`edu_deg_${i}`] ? (
<span className="ats-field-error" role="status">
{fieldErrors[`edu_deg_${i}`]}
</span>
) : null}
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

Missing id and aria-describedby on degree/title inputs.

The institution and company inputs have proper id attributes for focus targeting and accessibility, but the degree input (and similarly, title input at line 405) lacks an id and aria-describedby linkage. This means the error message won't be announced by screen readers.

🔧 Proposed fix for degree input
               <label className="ats-field">
                   Título / grado
                   <input
+                    id={`${formId}-edu_deg_${i}`}
                     value={row.degree}
                     onChange={(ev) => {
                       const v = ev.target.value;
                       setEducation((rows) => rows.map((r, j) => (j === i ? { ...r, degree: v } : r)));
                     }}
                     aria-invalid={!!fieldErrors[`edu_deg_${i}`]}
+                    aria-describedby={fieldErrors[`edu_deg_${i}`] ? `${formId}-edu_deg_${i}-err` : undefined}
                   />
                   {fieldErrors[`edu_deg_${i}`] ? (
-                    <span className="ats-field-error" role="status">
+                    <span id={`${formId}-edu_deg_${i}-err`} className="ats-field-error" role="status">
                       {fieldErrors[`edu_deg_${i}`]}
                     </span>
                   ) : null}
                 </label>

Apply the same pattern to the title input (lines 405-418) and resume error span (lines 476-479).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<input
value={row.degree}
onChange={(ev) => {
const v = ev.target.value;
setEducation((rows) => rows.map((r, j) => (j === i ? { ...r, degree: v } : r)));
}}
aria-invalid={!!fieldErrors[`edu_deg_${i}`]}
/>
{fieldErrors[`edu_deg_${i}`] ? (
<span className="ats-field-error" role="status">
{fieldErrors[`edu_deg_${i}`]}
</span>
) : null}
<input
id={`${formId}-edu_deg_${i}`}
value={row.degree}
onChange={(ev) => {
const v = ev.target.value;
setEducation((rows) => rows.map((r, j) => (j === i ? { ...r, degree: v } : r)));
}}
aria-invalid={!!fieldErrors[`edu_deg_${i}`]}
aria-describedby={fieldErrors[`edu_deg_${i}`] ? `${formId}-edu_deg_${i}-err` : undefined}
/>
{fieldErrors[`edu_deg_${i}`] ? (
<span id={`${formId}-edu_deg_${i}-err`} className="ats-field-error" role="status">
{fieldErrors[`edu_deg_${i}`]}
</span>
) : null}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/AddCandidate.tsx` around lines 315 - 327, The degree input
(and similarly the job title input and resume error span) is missing an id and
aria-describedby so error spans aren't announced; update the degree input in the
education row render (where value={row.degree} and setEducation(...) is used) to
include a unique id like `edu_deg_${i}` and set aria-describedby to the
corresponding error span id (e.g., `edu_deg_${i}_error`) only when
fieldErrors[`edu_deg_${i}`] exists, and ensure the error span has that id and
role="status"; apply the same pattern to the job title input (matching its
fieldErrors key, e.g., `job_title_${i}`) and to the resume input/error span
(matching the resume fieldErrors key) so screen readers will announce the error
messages.

Comment thread prompts.md

Te recomendamos encarecidamente definir primero los 3 tickets de trabajo a fondo, y usarlos como input para el asistente de código.

Puedes crear una carpeta dentro del proyecto ccon el nomvre que mejor consideres y definir 3 tickets de trabajo para cda uno de las tareas tecnicas? tipo SDD, todo bien detallado
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

Minor typos in prompt documentation.

Line 51 contains typos: "ccon el nomvre" → "con el nombre", "cda" → "cada". Since this is an internal record of prompts used, this is low priority but worth correcting for clarity.

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

In `@prompts.md` at line 51, Fix typos in the prompt text on line containing
"Puedes crear una carpeta dentro del proyecto ccon el nomvre que mejor
consideres y definir 3 tickets de trabajo para cda uno de las tareas tecnicas?"
by changing "ccon el nomvre" to "con el nombre" and "cda" to "cada", and
optionally correct "nomvre"→"nombre" and "tecnicas"→"técnicas" to improve
clarity; update the sentence in prompts.md so it reads correctly in Spanish
while preserving the original intent.

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