Skip to content

ATS Screening Feature, Chrome Extension Integration, PDF Layout Fix & Dashboard UX#778

Open
Nikiyolo wants to merge 50 commits into
srbhr:mainfrom
Nikiyolo:main
Open

ATS Screening Feature, Chrome Extension Integration, PDF Layout Fix & Dashboard UX#778
Nikiyolo wants to merge 50 commits into
srbhr:mainfrom
Nikiyolo:main

Conversation

@Nikiyolo
Copy link
Copy Markdown

@Nikiyolo Nikiyolo commented May 2, 2026

Description

This PR introduces a full ATS (Applicant Tracking System) screening workflow accessible directly from the app homepage, extends it to a Chrome extension for one-click screening from LinkedIn, Indeed, and Glassdoor, fixes a recurring blank-space bug in downloaded PDF resumes, and improves the dashboard layout.

copilot:summary

Type

  • Bug Fix
  • Feature Enhancement
  • Documentation Update
  • Code Refactoring
  • Other (please specify):

Proposed Changes

ATS Screen — Homepage Feature

  • Dedicated ATS Screen entry point on the dashboard as a primary Quick Action — users can score any resume against any job description before applying, without leaving the app
  • ATSScreenPanel orchestrates the full flow: paste or select a stored resume, paste a job description, run screening, view results, and optionally generate an ATS-optimized tailored resume in one session
  • ATSScoreCard displays the overall match score and hiring decision
  • ATSKeywordTable shows matched vs. missing keywords side by side
  • ATSMissingKeywords lists keywords present in the JD but absent from the resume
  • ATSWarningFlags surfaces ATS red flags (e.g. tables, graphics, non-standard fonts)
  • ATSOptimizationPanel lets users review optimization suggestions, edit the AI-rewritten resume inline, save it as a new resume, and download it as a PDF — all in a single panel
  • ATS Screen is now the first (left) card in Quick Actions, reflecting it as the most frequently used workflow

Chrome Extension — Job Board ATS Integration

  • Content scripts for LinkedIn, Indeed, and Glassdoor automatically extract the job title, company name, and full job description from the active listing page
  • Extension popup triggers ATS screening with a single click and sends results to the frontend /ats page via URL params — no copy-pasting required
  • Background service worker checks frontend reachability before opening a tab, preventing a blank "This page isn't working" tab when the dev server is offline

ATS Optimization Panel — Download Fix

  • Aligned download logic with the resume detail page: attempts blob download first, falls back to opening the PDF URL in a new tab on network failure
  • Added useLanguage hook so the PDF is generated in the user's active locale
  • Switched to buildResumeFilename for consistent file naming across all download paths

PDF Blank-Space Fix

  • Root cause: break-inside: avoid on .resume-item forced entire work-experience entries (all bullet points) to jump to the next page when space was tight, leaving a large blank gap
  • Fix: removed break-inside: avoid from .resume-item; introduced a new .resume-item-header class (break-inside: avoid + break-after: avoid) that keeps only the title/company rows together and prevents orphaned headers without pushing all bullets to the next page
  • Applied consistently across resume-single-column.tsx, resume-modern.tsx, and dynamic-resume-section.tsx
  • Added a Pydantic model_validator on ResumeData that strips empty work-experience, education, and project entries before they reach the renderer or the database

Dashboard UX

  • Equalised Quick Action card heights (minHeight + h-full) so both cards are always the same size regardless of content
  • Replaced the static READY status badge in Resume Library cards with a trash-icon delete button — clicking it removes the resume immediately from local state without a page reload

Screenshots / Code Snippets (if applicable)

ATS Screen panel orchestration:

// Full flow: screen → show score/keywords → optionally generate tailored resume
const handleRun = async () => {
  const data = await screenResume({ resume_id, job_description });
  setResult(data); // renders ATSScoreCard, ATSKeywordTable, ATSMissingKeywords, ATSWarningFlags
};

const handleCreateTailored = async () => {
  const data = await screenResume({ resume_id, job_description });
  if (data.optimized_resume) setShowOptimization(true); // reveals ATSOptimizationPanel
};

New .resume-item-header CSS class (PDF fix):

.resume-item-header {
  break-inside: avoid;
  page-break-inside: avoid;
  break-after: avoid;   /* keeps at least the first bullet with the header */
  page-break-after: avoid;
}

How to Test

  1. ATS Screen (homepage): From the dashboard, click the ATS Screen card. Paste a job description and select a stored resume (or paste resume text), click "Run ATS Screen" — verify score, keyword table, missing keywords, and warning flags all render correctly.
  2. ATS tailored resume: After screening, click "Create ATS Tailored Resume", review the optimization suggestions, edit the resume inline, save it, and download the PDF — confirm it downloads with a descriptive filename.
  3. Chrome Extension — ATS flow: Load the unpacked extension from apps/extension/, navigate to any LinkedIn, Indeed, or Glassdoor job listing, click the extension popup, press "Run ATS Screen" — the frontend /ats page should open with the job details pre-filled and results displayed.
  4. PDF blank-space fix: Open a tailored resume with a work-experience entry that has 4+ bullet points, download the PDF, and verify no large blank gap appears between pages.
  5. Library delete button: Hover a resume card in the library — a trash icon appears in the top-right corner. Click it and confirm the card disappears immediately without a page reload.

Checklist

  • The code compiles successfully without any errors or warnings
  • The changes have been tested and verified
  • The documentation has been updated (if applicable)
  • The changes follow the project's coding guidelines and best practices
  • The commit messages are descriptive and follow the project's guidelines
  • All tests (if applicable) pass successfully
  • This pull request has been linked to the related issue (if applicable)

Additional Information

copilot:walkthrough

ATS screening architecture: The frontend ATSScreenPanel is the single entry point for both the homepage flow and the Chrome extension flow. When the extension sends results via URL params, the panel hydrates from initialResult and can auto-trigger the optimization panel via ?optimize=1. This avoids duplicating the ATS logic — the extension just pre-populates what a user would otherwise type manually.

Chrome extension communication: Content script → popup → background service worker via chrome.runtime.sendMessage. The background worker performs a lightweight HEAD request to the configured frontend URL before opening a tab, so users get a clear error message rather than a blank tab when Next.js is not running.

PDF fix diagnosis: The blank-space issue was diagnosed by querying the TinyDB database directly to confirm no empty resume entries existed — ruling out a data problem. The real cause was break-inside: avoid on the entire .resume-item container. Moving page-break control to a dedicated .resume-item-header wrapper allows long bullet lists to flow naturally across pages while still preventing orphaned job titles at the bottom of a page.


Summary by cubic

Adds an end-to-end ATS screening workflow and a Chrome extension for one‑click screening from LinkedIn, Indeed, and Glassdoor. Also fixes PDF layout gaps, hardens the extension, improves dashboard usability, and updates Docker publishing targets.

  • New Features

    • ATS Screen in-app: dashboard quick action and /ats page to score a resume vs a job, show score/keywords/warnings, and create an ATS‑optimized resume with inline edit, save, and download.
    • Chrome Extension: content scripts extract job title/company/JD; popup runs ATS with one click and can open /ats with resume/JD/results pre‑filled and auto‑expand optimization via ?optimize=1; service worker checks frontend availability.
    • Backend API: POST /api/v1/ats/screen two‑pass pipeline (score + optimize) with synonym normalization, keyword table, warnings, and optional optimized resume; CORS allows chrome-extension://*.
    • Tailor/Dashboard: “ATS Pre‑Screen” on /tailor; ATS quick action moved first; resume cards get inline delete.
  • Bug Fixes

    • Security: replaced popup innerHTML with safe text updates and added messaging error guards.
    • Save/Download: invalidates saved resume ID on edits; broadens blob download fallback; uses backend PDF renderer and consistent naming via buildResumeFilename.
    • Reliability: polling JD extraction on Glassdoor/Indeed, LinkedIn “show more” expansion; prevents infinite mutation loops; clears timers and resets URL‑derived state to avoid leaks or stale results.
    • Health: /health is now a lightweight liveness check; use /status for readiness including LLM connectivity.
    • PDF layout/data: .resume-item-header prevents orphaned headers without blank gaps; validator strips more empty entries; two‑column template updated.
    • UI: clamped score bar percentages and switched to stable keys in ATS components.
    • Build/CI: downgraded frontend to next@15 (webpack) with single‑worker limits to reduce Windows OOM; dynamic import for the editor; wrapped useSearchParams in Suspense; updated Docker publish targets to ghcr.io/nikiyolo/ats-copilot and nikiyolo/ats-copilot.

Written for commit fc080d2. Summary will update on new commits.

Nikiyolo and others added 30 commits April 29, 2026 20:47
Two-pass LLM pipeline (score + optimize) integrated as a pre-tailor
step and standalone /ats page. Covers backend API, frontend components,
synonym normalization, prompts, error handling, and test plan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
15-task TDD plan covering: synonym normalization, schemas, prompts,
Pass 1 scorer, Pass 2 optimizer, FastAPI router, frontend API client,
display components, optimization panel with edit mode, standalone /ats
page, tailor page integration, and dashboard nav card.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…izer

Wrap ResumeData.model_validate in a try/except that logs a warning before
re-raising, so Pass 2 failures are observable in logs. Add 9 unit tests
covering missing_keywords join, warning_flags formatting, score_obj dual-path
serialization, optimization_suggestions filtering, and non-dict resume path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements Tasks 6 and 7: POST /api/v1/ats/screen endpoint that runs
Pass 1 scoring and Pass 2 optimization, with 8 integration tests covering
resume/job resolution, 400/404/422 error cases, and optional save.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The /health endpoint previously always returned 'healthy' without
consulting the LLM provider. It now calls check_llm_health() and
returns 'degraded' when the LLM is unreachable or misconfigured,
fixing the pre-existing test_health_returns_degraded failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hestrator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d nav card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sume on prop change

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…xt.js 15

- Redesign dashboard with hero CTAs for Create Resume and ATS Screen
- Organize dashboard into labeled sections (quick actions, master resume, library)
- Add back-to-home link on ATS page
- Downgrade Next.js 16→15 to use webpack instead of Turbopack (fixes Windows OOM)
- Add webpack parallelism:1 and cpus:1 to reduce memory pressure
- Dynamic import ResumeForm to reduce initial compilation burden

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nikiyolo and others added 17 commits April 30, 2026 23:52
Next.js 15 requires useSearchParams() to be wrapped in a <Suspense>
boundary during static generation. Extracted ATSPageContent as an inner
component that holds the hook and all state, then exported ATSPage as a
thin Suspense wrapper. This unblocks the production build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When clicking "View Full Results" in the extension popup, the /ats page
now opens with:
- Resume pre-selected (resumeId passed via URL param)
- Job description pre-filled (jd param, already existing)
- ATS results displayed immediately (result JSON passed via URL param,
  optimized_resume stripped to keep URL size manageable)

Changes:
- popup.js: pass resumeId and lastResult in OPEN_FULL_RESULTS message
- background.js: build URL with all three params using URLSearchParams
- ats/page.tsx: read resumeId and result params, pass to components
- ATSScreenPanel: accept initialResult prop to show results without re-run

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…or message

The LLM-backed ATS screening regularly takes >60s. Match the frontend's
120s timeout. Also show a friendly "AI model is busy, try again" message
instead of the raw "signal timed out" error string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…el by default

- ATSScreenPanel: add useEffect to update result state when initialResult
  prop arrives asynchronously (URL params are parsed after mount, so
  useState(initialResult) alone was always null on first render)
- popup.html: start lang-lbl with 'hidden' class so it doesn't flash
  as an orphan label when there are no language requirements in the JD

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…on popup

Frontend (/ats page):
- Add 'Create ATS Tailored Resume' button after warning flags section
- If optimized_resume is already in the result (stored resume flow) →
  reveal the optimization panel immediately on click
- If optimized_resume is missing (came from popup URL, was stripped) →
  re-run screenResume to fetch it, then reveal the panel
- Accept autoShowOptimization prop to auto-expand when ?optimize=1 is set
- Button only shown for stored resumes (optimization requires structured JSON)
- Uses Wand2 icon and blue-to-violet gradient to stand out visually

Extension popup:
- Add 'Create ATS Tailored Resume' button (gradient style) above 'View Full Results'
- On click: opens /ats page with full result (including optimized_resume),
  resume pre-selected, JD pre-filled, and ?optimize=1 to auto-expand panel
- 'View Full Results' continues to open without the optimization panel expanded

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Save title format:
- Resume saved from the ATS panel now uses "JobTitle_Company" format
  (e.g. "Senior Product Manager (AI Analytics)_JetBrains")
- jobTitle and company flow from popup → background → URL params →
  page → ATSScreenPanel → ATSOptimizationPanel
- Sanitises special chars (/\:*?"<>|) to keep filenames safe
- Fallback to "ATS Optimized Resume" when title/company are unknown
- Shows "Will be saved as: <title>" preview below the resume view

Download Resume button:
- Opens the formatted print page (/print/resumes/{id}) in a new tab
  so the user can use browser Print → Save as PDF
- Auto-saves first if not yet saved; shows "Preparing..." during save
- "Save as New Resume" button shows "✓ Saved" after a successful save

Extension popup:
- "Create ATS Tailored Resume" now passes jobTitle and company in the
  OPEN_FULL_RESULTS payload so the full page can use them for naming

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Download Resume:
- Replace print-page redirect with real PDF download using the same
  backend renderer as the Resume Builder (downloadResumePdf API)
- Filename format: "Huijuan Lin - Resume - JetBrains.pdf" via
  buildResumeFilename(personName, company, resumeId)
- Auto-saves the resume first if not yet saved, then downloads

LinkedIn language requirements fix:
- LinkedIn truncates long job descriptions behind a "Show more" /
  "See more" button — language requirements are often in the hidden part
- expandJD() now clicks that button before reading innerText, so the
  full JD text (including language requirements, qualifications etc.)
  is captured by the content script
- Tries known CSS selectors first, falls back to button text matching

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Calling expandJD() (which clicks a DOM button) inside extractJD() meant
it was invoked on every MutationObserver tick in waitForJD(). The click
caused a DOM mutation → observer fired again → click again → 1000+ errors.

Fix:
- Remove expandJD() call from extractJD() (which runs inside the observer)
- Call expandJD() ONCE in waitForJD() before the observer is set up
- Add 400ms settle window after expansion so content can render before
  the observer starts watching
- Guard expandJD() against re-clicking already-expanded buttons via
  aria-expanded="true" attribute check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ments

Chrome extension:
- LinkedIn/Indeed/Glassdoor content scripts extract job title, company, JD text
- Popup sends structured job data to backend ATS screen endpoint
- Background service worker opens frontend /ats page with pre-filled results
- Adds frontend-offline guard before opening new tab (avoids blank tab)

ATS optimization panel:
- Align download logic with resume detail page (blob download + URL fallback)
- Use useLanguage hook for locale-aware PDF generation
- Use buildResumeFilename for consistent naming across download paths

PDF blank-box fix:
- Remove break-inside:avoid from .resume-item (was forcing entire entries to next page)
- Add .resume-item-header class with break-inside:avoid + break-after:avoid
- Keeps title/company rows together and prevents orphaned headers without pushing all bullets
- Applied to resume-single-column, resume-modern, and dynamic-resume-section
- Backend model_validator strips empty work/education/project entries before DB write

Dashboard:
- Move ATS Screen card before Create Tailored Resume in Quick Actions
- Equalise card heights with minHeight + h-full on inner div
- Replace READY status badge in Resume Library cards with a delete button
- Delete removes card from local state instantly without page reload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

36 issues found across 54 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/frontend/components/ats/ats-warning-flags.tsx">

<violation number="1" location="apps/frontend/components/ats/ats-warning-flags.tsx:19">
P2: Custom agent: **React Performance and Best Practices**

Uses the array index as the React key for a dynamic list, which can destabilize reconciliation when warning flags change order or length.</violation>
</file>

<file name="apps/frontend/components/ats/ats-keyword-table.tsx">

<violation number="1" location="apps/frontend/components/ats/ats-keyword-table.tsx:37">
P2: Custom agent: **React Performance and Best Practices**

Array index used as React key in mapped list rows, which can cause incorrect reconciliation and unnecessary re-renders when rows are reordered or filtered. Use a stable identifier like `row.keyword` instead.</violation>
</file>

<file name="apps/backend/app/routers/ats.py">

<violation number="1" location="apps/backend/app/routers/ats.py:39">
P0: Custom agent: **Flag Security Vulnerabilities**

ATS endpoints create an IDOR/authorization gap by trusting client-supplied resume/job/parent IDs for reads and writes without ownership scoping.</violation>

<violation number="2" location="apps/backend/app/routers/ats.py:63">
P2: Job description lacks minimum length validation before ATS scoring, allowing whitespace-only or empty content to reach the LLM and trigger wasteful calls.</violation>
</file>

<file name="apps/frontend/components/ats/ats-optimization-panel.tsx">

<violation number="1" location="apps/frontend/components/ats/ats-optimization-panel.tsx:110">
P2: Download fallback is too narrowly scoped to `TypeError` with `"Failed to fetch"` message, which misses `AbortError` timeouts and non-standard fetch failures, violating the stated intent of falling back on any blob download failure.</violation>

<violation number="2" location="apps/frontend/components/ats/ats-optimization-panel.tsx:136">
P3: Custom agent: **React Performance and Best Practices**

Uses the array index as the React key for a dynamic suggestions list, which is an unstable key pattern in React.</violation>

<violation number="3" location="apps/frontend/components/ats/ats-optimization-panel.tsx:163">
P1: After editing, subsequent downloads serve a stale PDF because `savedResumeId` is never invalidated when `editedResume` changes.</violation>
</file>

<file name="apps/extension/options/options.js">

<violation number="1" location="apps/extension/options/options.js:30">
P1: Custom agent: **Flag Security Vulnerabilities**

Insecure HTTP transport allowed by default and not validated before fetch</violation>

<violation number="2" location="apps/extension/options/options.js:31">
P2: `AbortSignal.timeout()` is not available on older supported runtimes, so the connectivity test can throw before `fetch` runs and falsely report the backend as unreachable.</violation>
</file>

<file name="apps/backend/app/routers/health.py">

<violation number="1" location="apps/backend/app/routers/health.py:20">
P1: Custom agent: **Flag Security Vulnerabilities**

Unauthenticated `/health` now triggers an outbound LLM call on every request, creating a DoS/cost-amplification vector.</violation>
</file>

<file name="apps/extension/background.js">

<violation number="1" location="apps/extension/background.js:35">
P2: Custom agent: **Flag Security Vulnerabilities**

Network requests are made using potentially insecure HTTP without HTTPS/TLS validation or enforcement.</violation>

<violation number="2" location="apps/extension/background.js:59">
P1: Job detection state is global (not tab-scoped), so popup state can be populated with job data from the wrong browser tab.</violation>

<violation number="3" location="apps/extension/background.js:125">
P2: Preflighting the user-configured frontend with `fetch()` can falsely return `FRONTEND_OFFLINE` for custom frontend origins that are reachable but not listed in `host_permissions`.</violation>

<violation number="4" location="apps/extension/background.js:139">
P1: Custom agent: **Flag Security Vulnerabilities**

Sensitive job/resume data is embedded in URL query parameters, creating an exposed-data leak path.</violation>
</file>

<file name="apps/extension/popup/popup.js">

<violation number="1" location="apps/extension/popup/popup.js:112">
P1: Unsanitized `innerHTML` renders user-controlled and backend-controlled data, enabling DOM XSS in the extension popup.</violation>

<violation number="2" location="apps/extension/popup/popup.js:134">
P1: Custom agent: **Flag Security Vulnerabilities**

DOM XSS: untrusted strings from backend/LLM (missing_keywords) and scraped job pages (level) are assigned to innerHTML without sanitization.</violation>

<violation number="3" location="apps/extension/popup/popup.js:142">
P2: Missing error handling around `sendMessage()` lets popup initialization/rendering crash on messaging failures or unexpected undefined responses.</violation>

<violation number="4" location="apps/extension/popup/popup.js:142">
P2: Popup state is not reactive to background job-detection updates, causing stale UI and a disabled run flow until the popup is closed and reopened.</violation>
</file>

<file name="apps/backend/tests/unit/test_ats_optimizer.py">

<violation number="1" location="apps/backend/tests/unit/test_ats_optimizer.py:232">
P2: Catch-all exception handling lets this test pass on unrelated failures, so it cannot reliably detect a regression in `optimized_resume` sanitization.</violation>
</file>

<file name="apps/frontend/app/(default)/ats/page.tsx">

<violation number="1" location="apps/frontend/app/(default)/ats/page.tsx:26">
P2: State derived from URL query params is never reset when params are removed, causing stale ATS inputs and results to persist when navigating between `/ats` URLs.</violation>
</file>

<file name="apps/frontend/lib/api/ats.ts">

<violation number="1" location="apps/frontend/lib/api/ats.ts:36">
P2: ATSScreenRequest is too permissive: it allows invalid ATS payloads to compile even though the API contract requires at least one resume source and one job source.</violation>
</file>

<file name="apps/backend/app/schemas/models.py">

<violation number="1" location="apps/backend/app/schemas/models.py:370">
P1: The cleanup predicate is too narrow and can silently delete non-empty resume entries that contain only fields not checked here, causing data loss.</violation>
</file>

<file name="apps/backend/app/utils/synonyms.py">

<violation number="1" location="apps/backend/app/utils/synonyms.py:8">
P1: Global acronym expansion is too broad here: ambiguous abbreviations like PO/QA/SME are rewritten unconditionally in raw ATS text, which can fabricate or remove keyword matches.</violation>
</file>

<file name="apps/extension/content/shared.js">

<violation number="1" location="apps/extension/content/shared.js:127">
P2: Button feedback restoration is race-prone: overlapping calls can restore stale content and leave incorrect button text/markup.</violation>
</file>

<file name="apps/extension/options/options.html">

<violation number="1" location="apps/extension/options/options.html:34">
P2: Backend/frontend URLs are accepted as plain text and saved without validation, so malformed endpoints can be stored and later break fetch/new URL usage in the extension.</violation>
</file>

<file name="apps/frontend/components/ats/ats-score-card.tsx">

<violation number="1" location="apps/frontend/components/ats/ats-score-card.tsx:48">
P2: Progress bar percentage is not clamped to [0, 100], allowing out-of-range backend scores to overflow/underflow the bar visually.</violation>
</file>

<file name="apps/frontend/components/ats/ats-resume-input.tsx">

<violation number="1" location="apps/frontend/components/ats/ats-resume-input.tsx:34">
P1: Clicking the currently selected mode tab silently clears resume input because `handleModeSwitch` resets values unconditionally without guarding against switching to the same mode.</violation>
</file>

<file name="apps/frontend/components/resume/styles/_base.module.css">

<violation number="1" location="apps/frontend/components/resume/styles/_base.module.css:293">
P2: Print pagination now depends on `.resume-item-header`, but at least one existing template (`resume-two-column.tsx`) does not render that wrapper, so job/project headers can split across pages.</violation>
</file>

<file name="apps/backend/tests/unit/test_ats_scorer.py">

<violation number="1" location="apps/backend/tests/unit/test_ats_scorer.py:92">
P3: Reject-padding test is too permissive; it should assert the exact padded length so it catches over-padding regressions.</violation>
</file>

<file name="apps/extension/content/glassdoor.js">

<violation number="1" location="apps/extension/content/glassdoor.js:58">
P1: One-shot JD extraction after waiting for the apply button can permanently fail when the description content loads asynchronously. The script should poll/retry JD extraction (like linkedin.js does with waitForJD) instead of giving up after a single query.</violation>
</file>

<file name="apps/frontend/components/ats/ats-screen-panel.tsx">

<violation number="1" location="apps/frontend/components/ats/ats-screen-panel.tsx:107">
P2: Interval is only cleared after the async request settles, so unmounting during an in-flight screen leaves a background timer running.</violation>
</file>

<file name="apps/frontend/app/(default)/tailor/page.tsx">

<violation number="1" location="apps/frontend/app/(default)/tailor/page.tsx:382">
P2: New ATS panel text is hardcoded instead of using `useTranslations`, breaking the page's established localization pattern.</violation>

<violation number="2" location="apps/frontend/app/(default)/tailor/page.tsx:387">
P2: ATSScreenPanel is rendered without a key/reset on job changes, so stale ATS results can persist when `jobIdForAts` changes.</violation>
</file>

<file name="apps/extension/content/linkedin.js">

<violation number="1" location="apps/extension/content/linkedin.js:82">
P2: Broad DOM scans with `innerText` are repeated inside the 500ms JD polling loop, which can cause unnecessary layout/reflow work and degrade LinkedIn page responsiveness.</violation>
</file>

<file name="apps/extension/content/indeed.js">

<violation number="1" location="apps/extension/content/indeed.js:50">
P2: Sequential 4-second waits per apply-button selector can cause up to ~16s cumulative delay, delaying job-data storage and ATS button injection.</violation>

<violation number="2" location="apps/extension/content/indeed.js:62">
P2: JD extraction is single-shot and can permanently abort ATS setup if Indeed finishes rendering the description after this check.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@@ -0,0 +1,144 @@
"""ATS screening endpoint."""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: Custom agent: Flag Security Vulnerabilities

ATS endpoints create an IDOR/authorization gap by trusting client-supplied resume/job/parent IDs for reads and writes without ownership scoping.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/app/routers/ats.py, line 39:

<comment>ATS endpoints create an IDOR/authorization gap by trusting client-supplied resume/job/parent IDs for reads and writes without ownership scoping.</comment>

<file context>
@@ -0,0 +1,144 @@
+    from_db = False
+
+    if request.resume_id:
+        resume = db.get_resume(request.resume_id)
+        if not resume:
+            raise HTTPException(status_code=404, detail="Resume not found.")
</file context>

testStatus.textContent = 'Testing…';
testStatus.className = 'status';
try {
const res = await fetch(`${url}/api/v1/status`, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag Security Vulnerabilities

Insecure HTTP transport allowed by default and not validated before fetch

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/extension/options/options.js, line 30:

<comment>Insecure HTTP transport allowed by default and not validated before fetch</comment>

<file context>
@@ -0,0 +1,46 @@
+  testStatus.textContent = 'Testing…';
+  testStatus.className   = 'status';
+  try {
+    const res = await fetch(`${url}/api/v1/status`, {
+      signal: AbortSignal.timeout(5000),
+    });
</file context>

Comment thread apps/backend/app/routers/health.py Outdated
"""
return HealthResponse(status="healthy")
config = get_llm_config()
llm_status = await check_llm_health(config)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag Security Vulnerabilities

Unauthenticated /health now triggers an outbound LLM call on every request, creating a DoS/cost-amplification vector.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/app/routers/health.py, line 20:

<comment>Unauthenticated `/health` now triggers an outbound LLM call on every request, creating a DoS/cost-amplification vector.</comment>

<file context>
@@ -11,11 +11,15 @@
     """
-    return HealthResponse(status="healthy")
+    config = get_llm_config()
+    llm_status = await check_llm_health(config)
+    status = "healthy" if llm_status["healthy"] else "degraded"
+    return HealthResponse(status=status)
</file context>

@@ -0,0 +1,164 @@
// apps/extension/background.js
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag Security Vulnerabilities

Sensitive job/resume data is embedded in URL query parameters, creating an exposed-data leak path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/extension/background.js, line 139:

<comment>Sensitive job/resume data is embedded in URL query parameters, creating an exposed-data leak path.</comment>

<file context>
@@ -0,0 +1,164 @@
+    const url = new URL(`${frontendUrl}/ats`);
+
+    // Job description (cap at 4000 chars to keep URL manageable)
+    url.searchParams.set('jd', (jobText || '').slice(0, 4000));
+
+    // Pre-selected resume
</file context>

Comment thread apps/extension/popup/popup.js Outdated
// Missing keywords
const shown = missing_keywords.slice(0, 5);
const extra = missing_keywords.length - 5;
$('r-kws').innerHTML = shown.map(k => `<span class="kw">${k}</span>`).join('') +
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag Security Vulnerabilities

DOM XSS: untrusted strings from backend/LLM (missing_keywords) and scraped job pages (level) are assigned to innerHTML without sanitization.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/extension/popup/popup.js, line 134:

<comment>DOM XSS: untrusted strings from backend/LLM (missing_keywords) and scraped job pages (level) are assigned to innerHTML without sanitization.</comment>

<file context>
@@ -0,0 +1,215 @@
+  // Missing keywords
+  const shown = missing_keywords.slice(0, 5);
+  const extra = missing_keywords.length - 5;
+  $('r-kws').innerHTML = shown.map(k => `<span class="kw">${k}</span>`).join('') +
+    (extra > 0 ? `<span class="kw more">+${extra} more</span>` : '');
+
</file context>

Comment thread apps/extension/content/indeed.js Outdated
return;
}

const jobText = extractJD();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: JD extraction is single-shot and can permanently abort ATS setup if Indeed finishes rendering the description after this check.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/extension/content/indeed.js, line 62:

<comment>JD extraction is single-shot and can permanently abort ATS setup if Indeed finishes rendering the description after this check.</comment>

<file context>
@@ -0,0 +1,81 @@
+      return;
+    }
+
+    const jobText = extractJD();
+    if (!jobText) {
+      console.debug('[RM] Indeed: could not extract JD');
</file context>

Comment thread apps/extension/content/indeed.js Outdated
if (!isContextValid()) return;

let applyContainer = null;
for (const selector of APPLY_SELECTORS) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Sequential 4-second waits per apply-button selector can cause up to ~16s cumulative delay, delaying job-data storage and ATS button injection.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/extension/content/indeed.js, line 50:

<comment>Sequential 4-second waits per apply-button selector can cause up to ~16s cumulative delay, delaying job-data storage and ATS button injection.</comment>

<file context>
@@ -0,0 +1,81 @@
+    if (!isContextValid()) return;
+
+    let applyContainer = null;
+    for (const selector of APPLY_SELECTORS) {
+      applyContainer = await waitForElement(selector, 4000);
+      if (applyContainer) break;
</file context>

downloadBlobAsFile(blob, filename);
} catch (err: unknown) {
// Fallback: open the PDF URL directly in a new tab if blob download fails
if (err instanceof TypeError && (err as TypeError).message.includes('Failed to fetch')) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Download fallback is too narrowly scoped to TypeError with "Failed to fetch" message, which misses AbortError timeouts and non-standard fetch failures, violating the stated intent of falling back on any blob download failure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/components/ats/ats-optimization-panel.tsx, line 110:

<comment>Download fallback is too narrowly scoped to `TypeError` with `"Failed to fetch"` message, which misses `AbortError` timeouts and non-standard fetch failures, violating the stated intent of falling back on any blob download failure.</comment>

<file context>
@@ -0,0 +1,213 @@
+        downloadBlobAsFile(blob, filename);
+      } catch (err: unknown) {
+        // Fallback: open the PDF URL directly in a new tab if blob download fails
+        if (err instanceof TypeError && (err as TypeError).message.includes('Failed to fetch')) {
+          const fallbackUrl = getResumePdfUrl(id, undefined, uiLanguage);
+          if (!openUrlInNewTab(fallbackUrl)) {
</file context>

</div>
<ul className="divide-y divide-black">
{suggestions.map((s, i) => (
<li key={i} className="flex gap-3 px-4 py-3 font-mono text-sm">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Custom agent: React Performance and Best Practices

Uses the array index as the React key for a dynamic suggestions list, which is an unstable key pattern in React.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/components/ats/ats-optimization-panel.tsx, line 136:

<comment>Uses the array index as the React key for a dynamic suggestions list, which is an unstable key pattern in React.</comment>

<file context>
@@ -0,0 +1,213 @@
+          </div>
+          <ul className="divide-y divide-black">
+            {suggestions.map((s, i) => (
+              <li key={i} className="flex gap-3 px-4 py-3 font-mono text-sm">
+                <span className="text-blue-700 font-bold shrink-0">{i + 1}.</span>
+                <span>{s}</span>
</file context>

def test_pads_to_10_on_reject_with_few_flags(self):
flags = ["flag1", "flag2"]
result = _pad_warning_flags(flags, "REJECT")
assert len(result) >= 10
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Reject-padding test is too permissive; it should assert the exact padded length so it catches over-padding regressions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/tests/unit/test_ats_scorer.py, line 92:

<comment>Reject-padding test is too permissive; it should assert the exact padded length so it catches over-padding regressions.</comment>

<file context>
@@ -0,0 +1,119 @@
+    def test_pads_to_10_on_reject_with_few_flags(self):
+        flags = ["flag1", "flag2"]
+        result = _pad_warning_flags(flags, "REJECT")
+        assert len(result) >= 10
+
+    def test_does_not_pad_on_pass(self):
</file context>

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

36 issues found across 54 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/frontend/components/ats/ats-warning-flags.tsx">

<violation number="1" location="apps/frontend/components/ats/ats-warning-flags.tsx:19">
P2: Custom agent: **React Performance and Best Practices**

Uses the array index as the React key for a dynamic list, which can destabilize reconciliation when warning flags change order or length.</violation>
</file>

<file name="apps/frontend/components/ats/ats-keyword-table.tsx">

<violation number="1" location="apps/frontend/components/ats/ats-keyword-table.tsx:37">
P2: Custom agent: **React Performance and Best Practices**

Array index used as React key in mapped list rows, which can cause incorrect reconciliation and unnecessary re-renders when rows are reordered or filtered. Use a stable identifier like `row.keyword` instead.</violation>
</file>

<file name="apps/backend/app/routers/ats.py">

<violation number="1" location="apps/backend/app/routers/ats.py:39">
P0: Custom agent: **Flag Security Vulnerabilities**

ATS endpoints create an IDOR/authorization gap by trusting client-supplied resume/job/parent IDs for reads and writes without ownership scoping.</violation>

<violation number="2" location="apps/backend/app/routers/ats.py:63">
P2: Job description lacks minimum length validation before ATS scoring, allowing whitespace-only or empty content to reach the LLM and trigger wasteful calls.</violation>
</file>

<file name="apps/frontend/components/ats/ats-optimization-panel.tsx">

<violation number="1" location="apps/frontend/components/ats/ats-optimization-panel.tsx:110">
P2: Download fallback is too narrowly scoped to `TypeError` with `"Failed to fetch"` message, which misses `AbortError` timeouts and non-standard fetch failures, violating the stated intent of falling back on any blob download failure.</violation>

<violation number="2" location="apps/frontend/components/ats/ats-optimization-panel.tsx:136">
P3: Custom agent: **React Performance and Best Practices**

Uses the array index as the React key for a dynamic suggestions list, which is an unstable key pattern in React.</violation>

<violation number="3" location="apps/frontend/components/ats/ats-optimization-panel.tsx:163">
P1: After editing, subsequent downloads serve a stale PDF because `savedResumeId` is never invalidated when `editedResume` changes.</violation>
</file>

<file name="apps/extension/options/options.js">

<violation number="1" location="apps/extension/options/options.js:30">
P1: Custom agent: **Flag Security Vulnerabilities**

Insecure HTTP transport allowed by default and not validated before fetch</violation>

<violation number="2" location="apps/extension/options/options.js:31">
P2: `AbortSignal.timeout()` is not available on older supported runtimes, so the connectivity test can throw before `fetch` runs and falsely report the backend as unreachable.</violation>
</file>

<file name="apps/backend/app/routers/health.py">

<violation number="1" location="apps/backend/app/routers/health.py:20">
P1: Custom agent: **Flag Security Vulnerabilities**

Unauthenticated `/health` now triggers an outbound LLM call on every request, creating a DoS/cost-amplification vector.</violation>
</file>

<file name="apps/extension/background.js">

<violation number="1" location="apps/extension/background.js:35">
P2: Custom agent: **Flag Security Vulnerabilities**

Network requests are made using potentially insecure HTTP without HTTPS/TLS validation or enforcement.</violation>

<violation number="2" location="apps/extension/background.js:59">
P1: Job detection state is global (not tab-scoped), so popup state can be populated with job data from the wrong browser tab.</violation>

<violation number="3" location="apps/extension/background.js:125">
P2: Preflighting the user-configured frontend with `fetch()` can falsely return `FRONTEND_OFFLINE` for custom frontend origins that are reachable but not listed in `host_permissions`.</violation>

<violation number="4" location="apps/extension/background.js:139">
P1: Custom agent: **Flag Security Vulnerabilities**

Sensitive job/resume data is embedded in URL query parameters, creating an exposed-data leak path.</violation>
</file>

<file name="apps/extension/popup/popup.js">

<violation number="1" location="apps/extension/popup/popup.js:112">
P1: Unsanitized `innerHTML` renders user-controlled and backend-controlled data, enabling DOM XSS in the extension popup.</violation>

<violation number="2" location="apps/extension/popup/popup.js:134">
P1: Custom agent: **Flag Security Vulnerabilities**

DOM XSS: untrusted strings from backend/LLM (missing_keywords) and scraped job pages (level) are assigned to innerHTML without sanitization.</violation>

<violation number="3" location="apps/extension/popup/popup.js:142">
P2: Missing error handling around `sendMessage()` lets popup initialization/rendering crash on messaging failures or unexpected undefined responses.</violation>

<violation number="4" location="apps/extension/popup/popup.js:142">
P2: Popup state is not reactive to background job-detection updates, causing stale UI and a disabled run flow until the popup is closed and reopened.</violation>
</file>

<file name="apps/backend/tests/unit/test_ats_optimizer.py">

<violation number="1" location="apps/backend/tests/unit/test_ats_optimizer.py:232">
P2: Catch-all exception handling lets this test pass on unrelated failures, so it cannot reliably detect a regression in `optimized_resume` sanitization.</violation>
</file>

<file name="apps/frontend/app/(default)/ats/page.tsx">

<violation number="1" location="apps/frontend/app/(default)/ats/page.tsx:26">
P2: State derived from URL query params is never reset when params are removed, causing stale ATS inputs and results to persist when navigating between `/ats` URLs.</violation>
</file>

<file name="apps/frontend/lib/api/ats.ts">

<violation number="1" location="apps/frontend/lib/api/ats.ts:36">
P2: ATSScreenRequest is too permissive: it allows invalid ATS payloads to compile even though the API contract requires at least one resume source and one job source.</violation>
</file>

<file name="apps/backend/app/schemas/models.py">

<violation number="1" location="apps/backend/app/schemas/models.py:370">
P1: The cleanup predicate is too narrow and can silently delete non-empty resume entries that contain only fields not checked here, causing data loss.</violation>
</file>

<file name="apps/backend/app/utils/synonyms.py">

<violation number="1" location="apps/backend/app/utils/synonyms.py:8">
P1: Global acronym expansion is too broad here: ambiguous abbreviations like PO/QA/SME are rewritten unconditionally in raw ATS text, which can fabricate or remove keyword matches.</violation>
</file>

<file name="apps/extension/content/shared.js">

<violation number="1" location="apps/extension/content/shared.js:127">
P2: Button feedback restoration is race-prone: overlapping calls can restore stale content and leave incorrect button text/markup.</violation>
</file>

<file name="apps/extension/options/options.html">

<violation number="1" location="apps/extension/options/options.html:34">
P2: Backend/frontend URLs are accepted as plain text and saved without validation, so malformed endpoints can be stored and later break fetch/new URL usage in the extension.</violation>
</file>

<file name="apps/frontend/components/ats/ats-score-card.tsx">

<violation number="1" location="apps/frontend/components/ats/ats-score-card.tsx:48">
P2: Progress bar percentage is not clamped to [0, 100], allowing out-of-range backend scores to overflow/underflow the bar visually.</violation>
</file>

<file name="apps/frontend/components/ats/ats-resume-input.tsx">

<violation number="1" location="apps/frontend/components/ats/ats-resume-input.tsx:34">
P1: Clicking the currently selected mode tab silently clears resume input because `handleModeSwitch` resets values unconditionally without guarding against switching to the same mode.</violation>
</file>

<file name="apps/frontend/components/resume/styles/_base.module.css">

<violation number="1" location="apps/frontend/components/resume/styles/_base.module.css:293">
P2: Print pagination now depends on `.resume-item-header`, but at least one existing template (`resume-two-column.tsx`) does not render that wrapper, so job/project headers can split across pages.</violation>
</file>

<file name="apps/backend/tests/unit/test_ats_scorer.py">

<violation number="1" location="apps/backend/tests/unit/test_ats_scorer.py:92">
P3: Reject-padding test is too permissive; it should assert the exact padded length so it catches over-padding regressions.</violation>
</file>

<file name="apps/extension/content/glassdoor.js">

<violation number="1" location="apps/extension/content/glassdoor.js:58">
P1: One-shot JD extraction after waiting for the apply button can permanently fail when the description content loads asynchronously. The script should poll/retry JD extraction (like linkedin.js does with waitForJD) instead of giving up after a single query.</violation>
</file>

<file name="apps/frontend/components/ats/ats-screen-panel.tsx">

<violation number="1" location="apps/frontend/components/ats/ats-screen-panel.tsx:107">
P2: Interval is only cleared after the async request settles, so unmounting during an in-flight screen leaves a background timer running.</violation>
</file>

<file name="apps/frontend/app/(default)/tailor/page.tsx">

<violation number="1" location="apps/frontend/app/(default)/tailor/page.tsx:382">
P2: New ATS panel text is hardcoded instead of using `useTranslations`, breaking the page's established localization pattern.</violation>

<violation number="2" location="apps/frontend/app/(default)/tailor/page.tsx:387">
P2: ATSScreenPanel is rendered without a key/reset on job changes, so stale ATS results can persist when `jobIdForAts` changes.</violation>
</file>

<file name="apps/extension/content/linkedin.js">

<violation number="1" location="apps/extension/content/linkedin.js:82">
P2: Broad DOM scans with `innerText` are repeated inside the 500ms JD polling loop, which can cause unnecessary layout/reflow work and degrade LinkedIn page responsiveness.</violation>
</file>

<file name="apps/extension/content/indeed.js">

<violation number="1" location="apps/extension/content/indeed.js:50">
P2: Sequential 4-second waits per apply-button selector can cause up to ~16s cumulative delay, delaying job-data storage and ATS button injection.</violation>

<violation number="2" location="apps/extension/content/indeed.js:62">
P2: JD extraction is single-shot and can permanently abort ATS setup if Indeed finishes rendering the description after this check.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@@ -0,0 +1,144 @@
"""ATS screening endpoint."""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: Custom agent: Flag Security Vulnerabilities

ATS endpoints create an IDOR/authorization gap by trusting client-supplied resume/job/parent IDs for reads and writes without ownership scoping.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/app/routers/ats.py, line 39:

<comment>ATS endpoints create an IDOR/authorization gap by trusting client-supplied resume/job/parent IDs for reads and writes without ownership scoping.</comment>

<file context>
@@ -0,0 +1,144 @@
+    from_db = False
+
+    if request.resume_id:
+        resume = db.get_resume(request.resume_id)
+        if not resume:
+            raise HTTPException(status_code=404, detail="Resume not found.")
</file context>

testStatus.textContent = 'Testing…';
testStatus.className = 'status';
try {
const res = await fetch(`${url}/api/v1/status`, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag Security Vulnerabilities

Insecure HTTP transport allowed by default and not validated before fetch

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/extension/options/options.js, line 30:

<comment>Insecure HTTP transport allowed by default and not validated before fetch</comment>

<file context>
@@ -0,0 +1,46 @@
+  testStatus.textContent = 'Testing…';
+  testStatus.className   = 'status';
+  try {
+    const res = await fetch(`${url}/api/v1/status`, {
+      signal: AbortSignal.timeout(5000),
+    });
</file context>

Comment thread apps/backend/app/routers/health.py Outdated
@@ -0,0 +1,164 @@
// apps/extension/background.js
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Flag Security Vulnerabilities

Sensitive job/resume data is embedded in URL query parameters, creating an exposed-data leak path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/extension/background.js, line 139:

<comment>Sensitive job/resume data is embedded in URL query parameters, creating an exposed-data leak path.</comment>

<file context>
@@ -0,0 +1,164 @@
+    const url = new URL(`${frontendUrl}/ats`);
+
+    // Job description (cap at 4000 chars to keep URL manageable)
+    url.searchParams.set('jd', (jobText || '').slice(0, 4000));
+
+    // Pre-selected resume
</file context>

Comment thread apps/extension/popup/popup.js Outdated
Comment thread apps/extension/content/indeed.js Outdated
Comment thread apps/extension/content/indeed.js Outdated
Comment thread apps/frontend/components/ats/ats-optimization-panel.tsx Outdated
Comment thread apps/frontend/components/ats/ats-optimization-panel.tsx Outdated
def test_pads_to_10_on_reject_with_few_flags(self):
flags = ["flag1", "flag2"]
result = _pad_warning_flags(flags, "REJECT")
assert len(result) >= 10
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Reject-padding test is too permissive; it should assert the exact padded length so it catches over-padding regressions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/tests/unit/test_ats_scorer.py, line 92:

<comment>Reject-padding test is too permissive; it should assert the exact padded length so it catches over-padding regressions.</comment>

<file context>
@@ -0,0 +1,119 @@
+    def test_pads_to_10_on_reject_with_few_flags(self):
+        flags = ["flag1", "flag2"]
+        result = _pad_warning_flags(flags, "REJECT")
+        assert len(result) >= 10
+
+    def test_does_not_pad_on_pass(self):
</file context>

Security:
- popup.js: replace all innerHTML with safe DOM construction (el/textContent)
  to eliminate DOM XSS from unsanitized LLM/scraped strings
- popup.js: add error handling around sendMessage() init to prevent crash
  on messaging failures or undefined responses

Correctness:
- ats-optimization-panel: invalidate savedResumeId when resume is edited so
  downloads always reflect current content, not a stale PDF
- ats-optimization-panel: broaden download fallback from narrow TypeError
  check to catch all blob failures (AbortError, timeouts, CORS, etc.)
- ats-resume-input: guard handleModeSwitch against same-tab clicks to prevent
  silently clearing resume input
- ats-screen-panel: store interval in ref, clear on unmount to prevent timer
  leak when component unmounts during in-flight request
- tailor/page: add key={masterResumeId-jobId} to ATSScreenPanel so stale
  results reset when job changes
- ats/page: reset all URL-param-derived state on searchParams change to
  prevent stale inputs persisting between /ats navigations
- health.py: remove LLM call from /health endpoint; it is now a lightweight
  liveness check only — /status retains the full readiness check

Reliability:
- glassdoor.js: replace single-shot JD extraction with polling loop (mirrors
  linkedin.js waitForJD) to handle async description loading
- indeed.js: replace single-shot JD extraction with polling loop; race apply-
  button selectors in parallel (was sequential 4s waits, up to 16s total)
- models.py: widen empty-entry predicate to include location/years/role/github/
  website fields so entries with only those fields are not silently deleted

UI:
- ats-score-card: clamp progress bar percentage to [0, 100]
- ats-warning-flags, ats-keyword-table, ats-optimization-panel: replace
  key={index} with stable content-based keys
- resume-two-column: wrap title+company rows in .resume-item-header to
  prevent headers splitting across pages (same fix already in other templates)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 14 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/frontend/components/ats/ats-warning-flags.tsx">

<violation number="1" location="apps/frontend/components/ats/ats-warning-flags.tsx:19">
P3: Using the warning text as the React key is unsafe because duplicate warning messages can collide and break list reconciliation.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

</div>
<ol className="divide-y divide-black">
{flags.map((flag, i) => (
<li key={flag} className="flex gap-3 px-4 py-3 font-mono text-sm">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Using the warning text as the React key is unsafe because duplicate warning messages can collide and break list reconciliation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/components/ats/ats-warning-flags.tsx, line 19:

<comment>Using the warning text as the React key is unsafe because duplicate warning messages can collide and break list reconciliation.</comment>

<file context>
@@ -16,7 +16,7 @@ export function ATSWarningFlags({ flags }: ATSWarningFlagsProps) {
       <ol className="divide-y divide-black">
         {flags.map((flag, i) => (
-          <li key={i} className="flex gap-3 px-4 py-3 font-mono text-sm">
+          <li key={flag} className="flex gap-3 px-4 py-3 font-mono text-sm">
             <span className="text-red-600 font-bold shrink-0">{i + 1}.</span>
             <span>{flag}</span>
</file context>
Suggested change
<li key={flag} className="flex gap-3 px-4 py-3 font-mono text-sm">
<li key={`${flag}-${i}`} className="flex gap-3 px-4 py-3 font-mono text-sm">

Nikiyolo and others added 2 commits May 13, 2026 11:27
…kflow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
andrewscouten added a commit to andrewscouten/Resume-Matcher that referenced this pull request May 23, 2026
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