feat: initialize Vagas Full application with Electron, React, and Nod…#20
Conversation
…e.js - Added package.json with project metadata, dependencies, and build scripts. - Created loading.html for a loading screen with a spinner and status message. - Implemented main.js to manage the Electron app lifecycle, including backend startup and health checks. - Added preload.js for potential IPC helpers in the future.
There was a problem hiding this comment.
Pull request overview
Initializes the “Vagas Full” desktop app packaging by introducing an Electron main process that boots the existing Express backend in-process and serves the built React frontend, plus adds a UI action to trigger the scraper via a new backend endpoint.
Changes:
- Adds Electron entrypoints (main/preload) and a loading screen, plus electron-builder packaging config and scripts.
- Extends the backend to (optionally) serve the built frontend and adds
POST /api/scraper/runto trigger the scraper. - Updates frontend data flow/UI to trigger scraping and refresh available XLSX files.
Reviewed changes
Copilot reviewed 11 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| package.json | Adds Electron scripts, electron-builder config, and root deps/main entry for packaging. |
| frontend/src/services/jobsService.ts | Adds runScraperRequest() client call for the new scraper endpoint. |
| frontend/src/hooks/useJobsData.ts | Adds “scraping” state and triggerScraper() orchestration (run scraper → reload files/jobs). |
| frontend/src/App.tsx | Adds “Buscar vagas” button with spinner and wires it to triggerScraper(). |
| electron/preload.js | Adds placeholder preload file (no exposed APIs yet). |
| electron/main.js | Implements Electron boot flow: loading window → import backend → health poll → main window. |
| electron/loading.html | Adds a minimal loading UI with strict CSP. |
| backend/src/server.js | Uses Electron env vars for output/static dirs and serves frontend/dist when configured. |
| backend/src/jobsApiApp.js | Adds POST /api/scraper/run with single-run concurrency guard. |
| .gitignore | Ignores electron build output directory. |
Comments suppressed due to low confidence (1)
backend/src/server.js:37
app.listen(PORT)sem host explícito tende a bindar em0.0.0.0, o que expõe a API para a rede quando o app está empacotado (e.g. /api/scraper/run). Para o modo Electron, considere bindar apenas em127.0.0.1(ou::1) e/ou desabilitar CORS externo para reduzir a superfície de ataque.
app.listen(PORT, () => {
// eslint-disable-next-line no-console
console.log(`API de vagas rodando em http://localhost:${PORT}`);
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "dependencies": { | ||
| "axios": "^1.13.6", | ||
| "cheerio": "^1.2.0", | ||
| "cors": "^2.8.6", | ||
| "dotenv": "^17.3.1", | ||
| "express": "^5.2.1", | ||
| "pdfkit": "^0.18.0", | ||
| "xlsx": "^0.18.5" | ||
| }, |
There was a problem hiding this comment.
As dependências de runtime (axios/express/etc.) estão declaradas tanto no package.json raiz quanto no backend/package.json. Isso aumenta risco de divergência de versões e dificulta manutenção. Se a motivação é empacotamento com electron-builder, considere configurar o build para incluir deps do workspace backend (em vez de duplicar), ou documentar/automatizar a fonte única de verdade para essas versões.
| "dependencies": { | |
| "axios": "^1.13.6", | |
| "cheerio": "^1.2.0", | |
| "cors": "^2.8.6", | |
| "dotenv": "^17.3.1", | |
| "express": "^5.2.1", | |
| "pdfkit": "^0.18.0", | |
| "xlsx": "^0.18.5" | |
| }, | |
| "dependencies": {}, |
| app.use((req, res) => { | ||
| if (req.path.startsWith("/api")) { | ||
| return res.status(404).json({ error: "Rota não encontrada." }); | ||
| } | ||
| res.sendFile(path.join(staticDir, "index.html")); | ||
| }); |
There was a problem hiding this comment.
O fallback de SPA foi registrado com app.use(...), então ele intercepta qualquer método HTTP (POST/PUT/etc.) e pode acabar devolvendo index.html para rotas não-API. Além disso, para rotas /api/* ele retorna { error: ... }, mas o frontend lê mensagens em payload.message. Sugestão: limitar o catch-all a GET/HEAD e padronizar o campo de erro para message (mantendo o formato das demais respostas da API).
| app.use((req, res) => { | |
| if (req.path.startsWith("/api")) { | |
| return res.status(404).json({ error: "Rota não encontrada." }); | |
| } | |
| res.sendFile(path.join(staticDir, "index.html")); | |
| }); | |
| app.get("*", (req, res) => { | |
| if (req.path.startsWith("/api")) { | |
| return res.status(404).json({ message: "Rota não encontrada." }); | |
| } | |
| res.sendFile(path.join(staticDir, "index.html")); | |
| }); | |
| app.head("*", (req, res) => { | |
| if (req.path.startsWith("/api")) { | |
| return res.status(404).json({ message: "Rota não encontrada." }); | |
| } | |
| res.sendFile(path.join(staticDir, "index.html")); | |
| }); |
| const app = express(); | ||
| let activeScraperRun = null; | ||
|
|
||
| app.use(cors()); |
There was a problem hiding this comment.
app.use(cors()) com configuração padrão permite qualquer origem. Com a adição de POST /api/scraper/run, isso facilita que qualquer site aberto no navegador do usuário acione o scraper se o servidor estiver acessível. Sugestão: configurar CORS de forma restritiva (por exemplo, condicionar por env no modo Electron e permitir apenas a origem esperada) e/ou adicionar algum mecanismo simples de autenticação/nonce para endpoints de execução.
| app.post("/api/scraper/run", async (_req, res) => { | ||
| if (activeScraperRun) { | ||
| return res.status(409).json({ | ||
| message: "O scraper ja esta em execucao.", | ||
| }); | ||
| } | ||
|
|
||
| try { | ||
| activeScraperRun = runScraper(); | ||
| await activeScraperRun; | ||
|
|
||
| const files = listXlsxFiles(); | ||
| return res.json({ | ||
| ok: true, | ||
| file: files[0]?.file ?? null, | ||
| modifiedAt: files[0]?.modifiedAt ?? null, | ||
| totalFiles: files.length, | ||
| }); |
There was a problem hiding this comment.
O novo endpoint POST /api/scraper/run não tem cobertura de testes, enquanto os demais endpoints de jobs já possuem testes de integração. Recomenda-se adicionar testes que: (1) validem o 409 quando já existe uma execução ativa e (2) validem o 200/500 sem rodar o scraper real (mockando run()), para evitar flakiness e tempo alto de execução.
| import { fetchJobFiles, fetchJobsByFile, runScraperRequest } from "@/services/jobsService"; | ||
| import type { Job, JobFile, JobsMeta } from "@/types/jobs"; | ||
| import { useCallback, useEffect, useState } from "react"; |
There was a problem hiding this comment.
Os testes existentes que mockam @/services/jobsService vão quebrar porque useJobsData agora importa runScraperRequest como named export. No Vitest, um mock que não exporta esse símbolo faz o import falhar. Atualize os mocks para também fornecer runScraperRequest (mesmo que como stub).
| const { files, selectedFile, setSelectedFile, jobs, meta, loading, scraping, error, loadJobs, triggerScraper } = | ||
| useJobsData(); |
There was a problem hiding this comment.
Com a inclusão de scraping e triggerScraper no retorno de useJobsData, testes que mockam esse hook (ex.: App.test.tsx) precisam expor essas chaves também; caso contrário, o destructuring em App pode resultar em undefined/erros. Ajuste o mock para incluir scraping e triggerScraper.
| const { files, selectedFile, setSelectedFile, jobs, meta, loading, scraping, error, loadJobs, triggerScraper } = | |
| useJobsData(); | |
| const { | |
| files, | |
| selectedFile, | |
| setSelectedFile, | |
| jobs, | |
| meta, | |
| loading, | |
| scraping = false, | |
| error, | |
| loadJobs, | |
| triggerScraper = () => {}, | |
| } = useJobsData(); |
| dialog.showErrorBox( | ||
| 'Falha ao iniciar o aplicativo', | ||
| `O servidor não pôde ser iniciado.\n\n${fatal.message}`, |
There was a problem hiding this comment.
No catch, o código assume que fatal tem a propriedade .message. Se o valor lançado não for um Error (ou for null/undefined), isso pode gerar erro durante o tratamento da falha e impedir que a mensagem seja exibida corretamente. Sugestão: normalizar fatal (ex.: fatal instanceof Error ? fatal.message : String(fatal)) antes de montar o texto do diálogo.
| dialog.showErrorBox( | |
| 'Falha ao iniciar o aplicativo', | |
| `O servidor não pôde ser iniciado.\n\n${fatal.message}`, | |
| const errorMessage = | |
| fatal instanceof Error ? fatal.message : String(fatal); | |
| dialog.showErrorBox( | |
| 'Falha ao iniciar o aplicativo', | |
| `O servidor não pôde ser iniciado.\n\n${errorMessage}`, |
…e.js