Skip to content

Develop#26

Merged
Benevanio merged 7 commits into
masterfrom
develop
Mar 24, 2026
Merged

Develop#26
Benevanio merged 7 commits into
masterfrom
develop

Conversation

@Benevanio

Copy link
Copy Markdown
Collaborator

No description provided.

Copilot AI review requested due to automatic review settings March 24, 2026 22:59

Copilot AI left a comment

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.

Pull request overview

This PR expands the job-scraping pipeline to support multiple sources (via adapters), adds an in-memory cache + request de-duplication layer, and exposes a new API endpoint to search jobs with caching; it also updates exports/UI to include the job “source”.

Changes:

  • Introduces a multi-source scraping pipeline (sources/, pipeline/) with caching (cache/) and request de-duplication.
  • Adds /api/jobs/search endpoint returning cached search results and updates CLI run flow to use the new pipeline.
  • Updates exports/UI/types to include a source field and adds extensive unit test coverage for the new components.

Reviewed changes

Copilot reviewed 37 out of 39 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
frontend/src/types/jobs.ts Adds source field to the frontend Job type.
frontend/src/components/JobsTableCard.tsx Displays the new “Fonte” column in the jobs table.
frontend/package.json Adds rolldown dependency and reorders some devDependencies.
docker-compose.yml Changes build contexts/dockerfiles and backend start command.
cspell.json Adds a workspace cspell config file.
backend/tests/unit/utils/config.test.js Updates expected default output filenames.
backend/tests/unit/services/pipeline/searchJobsWithCache.test.js Adds unit tests for caching behavior and cache keys.
backend/tests/unit/services/pipeline/scrapeAllSources.test.js Adds unit tests for normalization, dedupe, logging, and error handling across adapters.
backend/tests/unit/services/pipeline/requestDedup.test.js Adds unit tests for request de-duplication behavior.
backend/tests/unit/services/linkedinScraper.test.js Removes legacy LinkedIn scraper tests (old module).
backend/tests/unit/services/cache/cache.test.js Adds unit tests for MemoryCache TTL/CRUD behavior.
backend/tests/unit/services/app.test.js Updates app test to mock the new pipeline + sources.
backend/tests/unit/services/adapters/theMuse.test.js Adds unit tests for The Muse adapter behavior and URL construction.
backend/tests/unit/services/adapters/linkedin.test.js Adds unit tests for LinkedIn adapter parsing and error handling.
backend/tests/unit/services/adapters/lever.test.js Adds unit tests for Lever adapter filtering/normalization.
backend/tests/unit/services/adapters/greenhouse.test.js Adds unit tests for Greenhouse adapter filtering/normalization.
backend/tests/unit/services/adapters/adzuna.test.js Adds unit tests for Adzuna adapter behavior and URL construction.
backend/src/sources/index.js Defines the enabled sources list (LinkedIn, Adzuna, The Muse, ATS builders).
backend/src/sources/green_lever_builder.js Builds Greenhouse/Lever adapters from company lists.
backend/src/pipeline/searchJobsWithCache.js Implements cache-key building + caching around scrapeAllSources.
backend/src/pipeline/scrapeAllSources.js Scrapes all adapters/keywords, normalizes jobs, and deduplicates results.
backend/src/pipeline/requestDedup.js Adds in-process in-flight promise sharing by cache key.
backend/src/jobsApiApp.js Adds /api/jobs/search endpoint using the new pipeline + sources.
backend/src/interface/lever.interface.js Adds curated Lever company slugs list.
backend/src/interface/index.js Exposes greenhouse/lever company lists.
backend/src/interface/greenhouse.interface.js Adds curated Greenhouse company board tokens list.
backend/src/exporter.js Updates PDF export header and adds source column to output.
backend/src/config.js Changes default output filenames to vagas_remoto.*.
backend/src/cache/cache.js Adds MemoryCache implementation and exports a singleton cache.
backend/src/app.js Updates CLI run flow to use searchJobsWithCache(sources, config).
backend/src/adapters/theMuse.js Adds The Muse adapter.
backend/src/adapters/linkedin.js Refactors LinkedIn scraping into an adapter with search().
backend/src/adapters/lever.js Adds Lever adapter factory for company slugs.
backend/src/adapters/greenhouse.js Adds Greenhouse adapter factory for board tokens.
backend/src/adapters/adzuna.js Adds Adzuna adapter factory and URL builder.
backend/.env.example Updates example output filenames to vagas_remoto.*.
.cspell/custom-dictionary-workspace.txt Adds/updates custom dictionary words for spellchecking.
Comments suppressed due to low confidence (1)

backend/src/adapters/linkedin.js:129

  • logWarn only accepts a single message argument (see src/logger.js), so passing a second argument here drops the error details. If you want to log the underlying error, interpolate it into the message string (or extend the logger API).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docker-compose.yml
Comment on lines 19 to 22
build:
context: ./frontend
dockerfile: Dockerfile
context: .
dockerfile: frontend/Dockerfile
container_name: vagas-frontend

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

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

Same issue for the frontend service: build.context: . combined with frontend/Dockerfile (COPY package*.json ./) will pull the root package*.json instead of frontend/package*.json, likely breaking npm ci / caching. Consider reverting context to ./frontend or updating the Dockerfile to copy/install from the frontend/ workspace.

Copilot uses AI. Check for mistakes.
setTimeout(resolve, config.waitBetweenSearchesMs || 1000),
);
} catch (error) {
console.error("Adzuna URL:", url);

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

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

The catch block logs the full Adzuna URL, which includes app_id and app_key query params. This can leak credentials into logs. Please remove the full URL from logs or redact sensitive query parameters before logging (and consider using the shared logger instead of console.error).

Suggested change
console.error("Adzuna URL:", url);
const redactedUrl = new URL(url);
redactedUrl.searchParams.delete("app_id");
redactedUrl.searchParams.delete("app_key");
console.error("Adzuna URL:", redactedUrl.toString());

Copilot uses AI. Check for mistakes.
Comment thread backend/src/exporter.js
{ key: "local", label: "Local", width: 100 },
{ key: "link", label: "Link", width: 280 },
];

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

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

The PDF column widths now sum to ~860pt, which exceeds A4 landscape usable width (~842-2*30 ≈ 782pt). This will overflow/clamp content off-page. Please rebalance/reduce widths (likely link/titulo) or compute widths dynamically based on page width.

Suggested change
// Ensure total column width does not exceed the usable page width.
const usableWidth =
doc.page.width - doc.page.margins.left - doc.page.margins.right;
const totalWidth = cols.reduce((sum, col) => sum + col.width, 0);
if (totalWidth > usableWidth && totalWidth > 0) {
const scale = usableWidth / totalWidth;
cols.forEach((col) => {
col.width = Math.floor(col.width * scale);
});
}

Copilot uses AI. Check for mistakes.
Comment thread backend/src/jobsApiApp.js
Comment on lines +74 to +80
const config = {
...getConfig(),
keywords: req.query.keywords
? String(req.query.keywords)
.split(",")
.map((k) => k.trim())
: getConfig().keywords,

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

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

This endpoint calls getConfig() twice and also allows keywords= to produce [''] (no .filter(Boolean)), which can trigger searches with an empty keyword. Reuse the first config object and normalize/sanitize the query keywords (trim + filter empty + optional de-dupe).

Suggested change
const config = {
...getConfig(),
keywords: req.query.keywords
? String(req.query.keywords)
.split(",")
.map((k) => k.trim())
: getConfig().keywords,
const baseConfig = getConfig();
let keywords;
if (req.query.keywords !== undefined) {
const rawKeywords = String(req.query.keywords)
.split(",")
.map((k) => k.trim())
.filter(Boolean);
keywords =
rawKeywords.length > 0 ? Array.from(new Set(rawKeywords)) : baseConfig.keywords;
} else {
keywords = baseConfig.keywords;
}
const config = {
...baseConfig,
keywords,

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +73
for (const adapter of adapters) {
logInfo(`Iniciando fonte: ${adapter.sourceName}`);

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

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

logInfo uses adapter.sourceName directly; adapters without sourceName will log "Iniciando fonte: undefined" (and this file itself allows sourceName to be missing in tests). Consider using a fallback like adapter.sourceName ?? 'unknown' to keep logs consistent.

Copilot uses AI. Check for mistakes.
Comment thread docker-compose.yml
Comment on lines 3 to 8
build:
context: ./backend
dockerfile: Dockerfile
context: .
dockerfile: backend/Dockerfile
container_name: vagas-backend
command: ["node", "src/server.js"]
command: ["node", "backend/index.js"]
env_file:

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

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

Backend container command now runs node backend/index.js, which executes the one-off scraper (src/app.js) and does not start the API server (src/server.js). This will break the frontend’s VITE_API_PROXY_TARGET=http://backend:3001 and the exposed port. Also, using build.context: . with backend/Dockerfile (COPY package*.json ./) will copy the root package files, likely breaking npm ci / workspace installs. Consider reverting to context: ./backend + command: ["node", "src/server.js"], or update the Dockerfile/command consistently to run the server from the root context (e.g. backend/src/server.js).

Copilot uses AI. Check for mistakes.
Comment thread backend/src/exporter.js
Comment on lines +122 to +131
drawRow(
{
palavra: "Palavra-chave",
titulo: "Título",
empresa: "Empresa",
local: "Local",
link: "Link",
},
true,
);

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

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

The first-page header row passed to drawRow does not include the new source column label, so the "Fonte" header cell will render blank on the first page (it’s only present on subsequent pages). Include source: "Fonte" in the initial header object as well.

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +10
return [...keywords]
.map((k) => String(k).trim().toLowerCase())
.filter(Boolean)
.sort();

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

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

normalizeKeywords() sorts but does not deduplicate. This means logically equivalent keyword sets (e.g. duplicated entries) generate different cache keys, reducing cache hit rate and potentially triggering redundant scraping. Consider de-duplicating keywords (e.g. via a Set) when building the cache key.

Suggested change
return [...keywords]
.map((k) => String(k).trim().toLowerCase())
.filter(Boolean)
.sort();
return [...new Set(
[...keywords]
.map((k) => String(k).trim().toLowerCase())
.filter(Boolean),
)].sort();

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +13
export const sources = [
linkedinAdapter,

createAdzunaAdapter({
appId: process.env.ADZUNA_APP_ID || "",
appKey: process.env.ADZUNA_APP_KEY || "",
country: process.env.ADZUNA_COUNTRY || "br",
}),

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

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

createAdzunaAdapter is always included even when ADZUNA_APP_ID / ADZUNA_APP_KEY are empty strings. That will cause repeated failing HTTP calls and noisy warnings for every run. Consider only adding the Adzuna source when both credentials are provided (or make the adapter return [] early when not configured).

Suggested change
export const sources = [
linkedinAdapter,
createAdzunaAdapter({
appId: process.env.ADZUNA_APP_ID || "",
appKey: process.env.ADZUNA_APP_KEY || "",
country: process.env.ADZUNA_COUNTRY || "br",
}),
const adzunaAppId = process.env.ADZUNA_APP_ID;
const adzunaAppKey = process.env.ADZUNA_APP_KEY;
export const sources = [
linkedinAdapter,
adzunaAppId &&
adzunaAppKey &&
createAdzunaAdapter({
appId: adzunaAppId,
appKey: adzunaAppKey,
country: process.env.ADZUNA_COUNTRY || "br",
}),

Copilot uses AI. Check for mistakes.
Comment thread frontend/package.json
Comment on lines 19 to 22
"react": "^19.2.4",
"react-dom": "^19.2.4",
"rolldown": "^1.0.0-rc.10",
"tailwind-merge": "^3.5.0"

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

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

rolldown is added as a production dependency but there are no references to it in the frontend source/scripts. If it isn’t used, it should be removed to avoid unnecessary install size/optional native bindings; if it is a build-time tool, it likely belongs in devDependencies instead.

Copilot uses AI. Check for mistakes.
@Benevanio
Benevanio merged commit f6fdee6 into master Mar 24, 2026
6 checks passed
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.

3 participants