Agregador de vagas inspirado no Glassdoor Brasil: API em Node.js com Clean Architecture e app Flutter com Clean Architecture e go_router.
| Pasta | O que é |
|---|---|
| glassdoor-api/ | API (Node 24, TypeScript, Express 5, Prisma, PostgreSQL) |
| app_glassdoor/ | App (Flutter 3.47.5 via FVM, flutter_bloc, go_router, dio) |
| globot/ | Coletor de vagas de fontes oficiais (Greenhouse, Lever, Adzuna) que envia à API |
- nvm (o Node vem do
glassdoor-api/.nvmrc) - FVM (o Flutter vem do
app_glassdoor/.fvmrc) - Docker com Compose. Seu usuário precisa estar no grupo
docker:sudo usermod -aG docker $USERe depois sair e entrar na sessão.
# 1. Banco e Redis
docker compose up -d db db-test redis
# 2. API (http://localhost:3333)
cd glassdoor-api
nvm use
npm install # também gera o client do Prisma
cp -n .env.example .env # só na 1ª vez (-n não sobrescreve); preencha JWT_ACCESS_SECRET e INGEST_API_TOKEN
npm run db:deploy # aplica as migrations
npm run db:seed # 200 vagas fictícias (apaga também as vagas ingeridas)
npm run dev
# 3. Worker de ingestão (outro terminal): coleta a Remotive na partida e depois a cada 6 h
cd glassdoor-api
npm run dev:worker
# 4. globot (opcional): vagas de páginas de carreira (Greenhouse, Lever) enviadas à API.
# Precisa da API rodando, com INGEST_API_TOKEN no glassdoor-api/.env.
cd globot
nvm use && npm install
cp -n .env.example .env # só na 1ª vez; copie para cá o INGEST_API_TOKEN do glassdoor-api/.env
npm run collect # uma rodada (npm run dev: a cada 6 h)
# 5. App (outro terminal)
cd app_glassdoor
fvm flutter pub get
fvm dart run build_runner build --delete-conflicting-outputs
fvm flutter run --dart-define-from-file=env/dev.json # emulador Android
fvm flutter run --dart-define-from-file=env/local.json # Linux, macOS ou simulador iOS
# Celular Android pelo cabo USB: o localhost:3333 do celular passa a ser o da máquina
adb reverse tcp:3333 tcp:3333
fvm flutter run -d <id do celular> --dart-define-from-file=env/local.json
# Celular no mesmo Wi-Fi do PC (sem cabo): põe no app_glassdoor/.env o domínio do IP atual do PC
(cd glassdoor-api && npm run lan:app-env) # ex.: http://api.192-168-0-135.sslip.io:3333/api/v1
fvm flutter run -d <id do celular> --dart-define-from-file=.env
app_glassdoor/.env: é a configuração local do app, fora do git, como o.envda API.
- Criar: na primeira vez,
cp -n .env.example .env. O modelo lista oAPI_BASE_URLde cada aparelho.- Uso: o app lê o
.envsozinho ao iniciar, então bastafvm flutter run. Depois de editar o arquivo, faça hot restart (R).- Atalhos: um
--dart-define-from-file(ex.:env/dev.jsonno emulador) tem prioridade sobre o.env. Esse valor fica fixo até o próximoflutter run, mesmo que a flag aponte para o próprio.env(como nas configurações do VS Code). Depois de editar o arquivo, pare (q) e rode de novo, porque oRnão basta.- Gerar o endereço: o
lan:app-envtroca só a linhaAPI_BASE_URL.- Segurança: o
.envvai dentro do APK, então nada de segredo nele.
Um DNS no PC (dnsmasq, no compose) responde api.glassdoor.lan com o IP do PC e repassa os outros nomes à internet.
(cd glassdoor-api && npm run lan:app-env -- --dns) # grava LAN_IP no .env da raiz e o domínio no app_glassdoor/.env
sudo docker compose --profile dns up -d --build # sobe o DNS na porta 53 do IP do PC
dig @192.168.0.135 api.glassdoor.lan +short # teste no PC: deve responder o IP do PCNo celular (Android 10 / MIUI 12):
-
Abra Configurações → Wi-Fi e toque na seta da rede conectada.
-
Em "Configurações de IP", troque para Estático e preencha:
- Endereço IP: o atual do celular (ex.:
192.168.0.86); - Gateway:
192.168.0.1; - Prefixo:
24; - DNS 1: o IP do PC (
192.168.0.135); - DNS 2: em branco. Com um DNS público ali, o Android às vezes pergunta a ele e não acha o nome.
- Endereço IP: o atual do celular (ex.:
-
No navegador do celular, teste
http://api.glassdoor.lan:3333/health. Com o celular no adb, confira também pelo PC:adb shell dumpsys connectivity | grep -o 'DnsAddresses: [^]]*]' # deve listar só o IP do PC adb shell ping -c1 api.glassdoor.lan # deve responder do IP do PC
Se o DnsAddresses ainda listar endereços IPv6 depois do passo 2 (ex.: 2804:14d:1:0:181:213:132:2), eles são os DNS que o roteador da operadora anuncia por IPv6. O modo Estático não os remove. O Android pode perguntar a eles e não achar o nome, como acontece com um DNS 2 preenchido. Para resolver, desligue o IPv6 da rede local no roteador ou use o sslip.io (abaixo).
Enquanto o celular estiver com esse DNS, o PC e o container dns precisam estar ligados, senão os sites deixam de abrir nesse Wi-Fi. Para desfazer, volte a "Configurações de IP" para DHCP. Se o IP do PC mudar, rode o lan:app-env -- --dns de novo, depois sudo docker compose --profile dns up -d --force-recreate, e troque o DNS 1 no celular.
Por que
.lane não.local: iPhone, Mac, Linux e Android 12+ tratam.localcomo mDNS e nem perguntam ao DNS. O Android 10 não resolve.localde jeito nenhum.
Domínio sem configurar nada: o
sslip.ioé um DNS público que devolve o IP escrito no nome (api.192-168-0-135.sslip.io→192.168.0.135). Ele funciona em qualquer celular, ao contrário de nomes.local, que o Android só resolve a partir da versão 12. O IP do PC vem por DHCP e pode mudar: rode olan:app-envde novo, ou reserve o IP no roteador. Se o roteador bloquear o domínio, o comando avisa e usa o IP. Para gerar com o IP, usenpm run lan:app-env -- --ip.
DioException [connection timeout]? O app está tentando um endereço que o aparelho não alcança. Sem--dart-define-from-file, o app usa10.0.2.2, que só existe no emulador Android. Num celular físico, use oadb reverseacima. Confira também se a API responde:curl localhost:3333/health.
DioException [connection error]: Failed host lookup? O celular não achou o nome da API no DNS. O app mostra "Sem conexão. Verifique sua internet.", a mesma mensagem dos timeouts, mesmo com o celular conectado à internet. Comapi.glassdoor.lan, a causa quase sempre é o celular ainda usar o DNS do roteador, e não o do PC. Confira nesta ordem:curl localhost:3333/health # 1. PC: a API responde? dig @192.168.0.135 api.glassdoor.lan +short # 2. PC: o DNS devolve o IP do PC? adb shell curl -s http://192.168.0.135:3333/health # 3. celular: alcança a API pelo IP? adb shell dumpsys connectivity | grep -o 'DnsAddresses: [^]]*]' # 4. celular: qual DNS está usando? adb shell ping -c1 api.glassdoor.lan # 5. celular: acha o nome?
- 1 a 3 passam, mas o 5 dá
unknown host: o problema é o DNS do celular. Faça os passos de "No celular" acima, ou use o sslip.io, que não depende dele:(cd glassdoor-api && npm run lan:app-env).- Falha no 1 ou no 2: suba a API ou o container
dns.- Falha no 3: o celular e o PC não estão na mesma rede, ou um firewall bloqueia a porta 3333.
No VS Code, .vscode/launch.json tem uma configuração por alvo. A do celular USB já roda o adb reverse.
Sem Node na máquina e sem recarga automática: é o jeito "como em produção". Os segredos vêm dos mesmos .env (glassdoor-api/.env e globot/.env), e as migrations rodam sozinhas antes da API.
# Pare antes o npm run dev e o npm run dev:worker: usam a mesma porta e a mesma fila
docker compose --profile api up -d --build # bancos, Redis, migrations, API e worker
docker compose --profile api --profile globot up -d --build # + globot (coleta a cada 6 h)
docker compose --profile api run --rm api-migrate npx prisma db seed # vagas fictícias (apaga as vagas!)
docker compose logs -f api worker globot # acompanhar
docker compose --profile api --profile globot down # parar tudo (os dados ficam)Sem estar no grupo docker, use sudo docker compose …. O app continua rodando na máquina, com fvm flutter run como acima.
# API
cd glassdoor-api && npm run lint && npm run typecheck && npm test # unit + integração (db-test)
# globot (sem rede: fontes em fixtures)
cd globot && npm run lint && npm run typecheck && npm test
# App
cd app_glassdoor && fvm flutter analyze && bash tool/check_layers.sh && fvm flutter test
# Ponta a ponta, com a API rodando:
fvm flutter test integration_test -d linux --dart-define-from-file=env/local.jsonConcluídas:
- Fase 0 (funda��ão);
- Fatia 1.1 (busca de vagas sobre a seed);
- Fatia 1.2 (ingestão de vagas reais da Remotive, com worker BullMQ);
- globot com Greenhouse e Lever (adiantado da Fase 2) e importação autenticada na API.
Próxima: Fatia 1.3, identidade.
Fontes de vagas: só fontes oficiais. O projeto não raspa o Glassdoor nem outros sites. Cada fonte tem os termos de uso aplicados no código. A Remotive aceita no máximo 4 consultas por dia e exige atribuição. Não aumente a frequência no job_sources.config: o worker recusa uma agenda acima do teto.