ou
Menção honrosa: este projeto existe por causa da Mariana B S. Foi o conteúdo dela — prolífico e, acima de tudo, profícuo — que me fez criar isso. Mas o indexador é agnóstico: funciona com qualquer perfil do LinkedIn.
Certo dia eu quase surtei.
Tava procurando um post antigo da Mariana e não achava. Fui scrollando, scrollando, scrollando… e percebi duas coisas: (1) tinha muito conteúdo bom que eu ainda não tinha lido, e (2) eu nunca ia dar conta de tudo aquilo.
Minha pressão — normalmente 11 por 7 — foi pra 12 por 8. Deu vontade de beber. Peguei uma Corona Cero (recomendo, é perfeita), relaxei, e decidi: vou indexar isso.
Uma pessoa prolífica já é rara. Prolífica e profícua, mais ainda. O conteúdo da Mariana merecia ser estruturado como um sistema neural. E se funcionasse pra ela, funcionaria pra qualquer um.
Além da história da cerveja, tem o motivo de sempre: eu uso qualquer ideia como desculpa pra testar IA.
Minha meta — como já falei pro meu amigo Cesar Brod — é escravizar a IA. Toda ideia que couber no vibe coding, eu faço. Quero ver se consigo entregar algo de qualidade.
A qualidade vocês avaliam. Pra mim, o experimento é o que conta.
| O que | Quanto |
|---|---|
| MVP inicial | < 5 minutos |
| Backend + frontend conectados | ~15 minutos |
| Ajustes finos | várias horas (dias) |
| Total | ~2 dias de trabalho (a IA codando enquanto eu fazia outras coisas — tenho contas pra pagar) |
| Debug do nginx-proxy | várias horas (o DeepSeek V4 Pro demorou pra perceber que expor a API num subdomínio próprio resolvia o roteamento) |
- Modelo: DeepSeek V4 Pro — era esse que eu queria testar
- TUI: DeepSeek TUI — descobri neste post do Akita
- Frontend inicial: Bolt (depois refeito via vibe coding)
- Backend: FastAPI + SQLAlchemy + PostgreSQL + pgvector
- Browser: Playwright (extração do LinkedIn)
- LLM local: Ollama (classificação de conteúdo)
- Modelo local: qwen3:8b-32k
- Infra: Docker Compose + nginx-proxy + LetsEncrypt
ubb/
├── frontend/ # Next.js 13 + Tailwind
├── backend/ # FastAPI
│ ├── app/
│ │ ├── routers/ # API endpoints
│ │ ├── services/ # LinkedIn agent, Ollama, embeddings
│ │ ├── models.py # SQLAlchemy models
│ │ └── main.py # FastAPI app
│ ├── sync.py # Script de sync (roda no host)
│ └── requirements.*.txt
├── docker-compose.yml # Produção
├── docker-compose.override.yml # Dev
└── .env
Antes de mais nada você precisa ter o override, pois eu não mando pro repo pois às vezes preciso colocar dados sensíveis ou expor portas que em prod não precisa expor:
services:
frontend:
user: root
ports:
- "3001:3000"
volumes:
- ./frontend:/app
- /app/node_modules
- /app/.next
environment:
- NODE_ENV=development
- NEXT_TELEMETRY_DISABLED=1
- WATCHPACK_POLLING=true
command: npm run dev
api:
ports:
- "8000:8000"
volumes:
- ./backend:/app
- /app/__pycache__
environment:
- LOG_LEVEL=debug
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
db:
ports:
- "5432:5432"
volumes:
- pgdata_dev:/var/lib/postgresql/data
volumes:
pgdata_dev:# Subir containers
docker compose up -d
# Criar venv e instalar deps do sync
python3 -m venv .sync-venv
.sync-venv/bin/pip install -r backend/requirements.sync.txt
.sync-venv/bin/playwright install chromium
# Para Firefox stealth (opcional):
.sync-venv/bin/playwright install firefoxO script backend/sync.py extrai posts do LinkedIn, classifica com LLM e faz push para a VM. Roda no host (não no container), acessando o banco local e o Ollama/Gemini CLI.
PYTHONPATH=backend .sync-venv/bin/python backend/sync.py [flags]| Flag | Padrão | Descrição |
|---|---|---|
--headless |
— | Força modo headless (navegador invisível) |
--no-headless |
— | Força modo visível (útil pra debug/OAuth) |
--firefox |
false |
Usa Firefox stealth (invisible_playwright) em vez de Chromium |
--classifier {ollama,gemini} |
ollama |
LLM para classificação de posts |
--max-posts N |
20 |
Máximo de posts a extrair por execução |
--update-all |
false |
Reprocessa posts já existentes no banco |
--push-all |
false |
Envia TODOS os raw_posts e dados classificados pra VM (modo bulk) |
--no-push |
false |
Pula push para VM remota (auto em produção) |
--gemini-setup |
false |
Roda OAuth interativo do Gemini CLI e sai |
| Modo | Descrição | Usa navegador? |
|---|---|---|
monitor ★ |
Extrai só posts novos, para no primeiro duplicado | Sim |
capture |
Scrolla o feed inteiro, varredura completa | Sim |
process |
Só classifica posts pendentes no banco | Não |
★ monitor é o padrão. Ideal pra cron diário.
# ── Modo monitor (diário) ──────────────────────────────
# Chromium + Ollama (padrão)
PYTHONPATH=backend .sync-venv/bin/python backend/sync.py --no-headless
# Firefox + Ollama (stealth, evita detecção)
PYTHONPATH=backend .sync-venv/bin/python backend/sync.py --no-headless --firefox
# Chromium + Gemini CLI (classificação mais rápida)
PYTHONPATH=backend .sync-venv/bin/python backend/sync.py --no-headless --classifier gemini
# Firefox + Gemini (stealth + classificação rápida)
PYTHONPATH=backend .sync-venv/bin/python backend/sync.py --no-headless --firefox --classifier gemini
# Headless (pra cron)
PYTHONPATH=backend .sync-venv/bin/python backend/sync.py --headless
# ── Modo capture (varredura completa) ───────────────────
SYNC_MODE=capture MAX_SCROLLS=80 PYTHONPATH=backend .sync-venv/bin/python backend/sync.py --no-headless
# ── Modo process-only (sem navegador) ───────────────────
# Classificar 50 posts pendentes com Ollama
SYNC_MODE=process PROCESS_COUNT=50 PYTHONPATH=backend .sync-venv/bin/python backend/sync.py
# Classificar com Gemini
SYNC_MODE=process PROCESS_COUNT=50 PYTHONPATH=backend .sync-venv/bin/python backend/sync.py --classifier gemini
# ── Push-all (bulk para VM) ─────────────────────────────
PYTHONPATH=backend .sync-venv/bin/python backend/sync.py --push-all
# ── Reprocessar existentes ──────────────────────────────
PYTHONPATH=backend .sync-venv/bin/python backend/sync.py --update-all --classifier geminiTodas as 4 combinações browser × classificador funcionam:
| Browser | Classificador | Comando |
|---|---|---|
| Chromium | Ollama | --no-headless |
| Chromium | Gemini | --no-headless --classifier gemini |
| Firefox | Ollama | --no-headless --firefox |
| Firefox | Gemini | --no-headless --firefox --classifier gemini |
O Gemini CLI suporta dois métodos de autenticação, controlados por GEMINI_LOGIN:
# .env
GEMINI_LOGIN=key
GEMINI_API_KEY=AIza... # https://aistudio.google.com/apikeyPronto, funciona direto. Sem interação.
# .env
GEMINI_LOGIN=oauthPrimeiro uso — rode o setup interativo:
# Host (roda o Gemini CLI direto no terminal):
gemini
# Siga o link, autorize no navegador, cole o código.
# O token fica em ~/.gemini/settings.json
# Container (Docker):
docker compose run --rm -it sync --gemini-setup
# ┌─────────────────────────────────────────────┐
# │ 1. Abre um link no terminal │
# │ 2. Copie o link e cole no navegador │
# │ 3. Autorize o app e copie o código │
# │ 4. Cole o código de volta no terminal │
# │ 5. Token salvo no volume gemini-config │
# └─────────────────────────────────────────────┘Depois do setup, o sync roda normalmente (o token OAuth fica persistido no volume gemini-config).
| Var | Padrão | Descrição |
|---|---|---|
SYNC_MODE |
monitor |
Modo: capture, monitor, process |
URL_TARGET |
— | URL do perfil/recent-activity no LinkedIn |
LINKEDIN_EMAIL |
— | Email da conta LinkedIn |
LINKEDIN_PASSWORD |
— | Senha da conta LinkedIn |
PLAYWRIGHT_HEADLESS |
false |
Headless mode (quando não usa flag explícita) |
CLASSIFIER |
ollama |
Classificador padrão: ollama ou gemini |
GEMINI_LOGIN |
key |
Auth Gemini: key (API key) ou oauth |
GEMINI_API_KEY |
— | API key do Google AI Studio (quando GEMINI_LOGIN=key) |
GEMINI_MODEL |
— | Modelo Gemini (opcional, ex: gemini-2.5-flash) |
MAX_SCROLLS |
40 |
Scrolls máximos no modo capture |
CONSECUTIVE_DUPES_TO_STOP |
5 |
Dups consecutivas antes de parar |
SCROLL_DELAY_MIN |
3 |
Delay mínimo entre scrolls (segundos) |
SCROLL_DELAY_MAX |
8 |
Delay máximo entre scrolls (segundos) |
PROCESS_COUNT |
10 |
Posts a classificar por execução |
OLLAMA_HOST |
http://localhost:11434 |
Endereço do Ollama |
OLLAMA_MODEL |
— | Modelo Ollama (ex: qwen3:8b-32k) |
TZ |
America/Recife |
Fuso horário dos containers |
SYNC_SCHEDULE |
08:00,14:00,20:00 |
Horários do scheduler (HH:MM separados por vírgula) |
SYNC_ARGS |
--headless |
Argumentos extras para sync.py no scheduler |
SYNC_PUSH_URL |
— | URL da VM para push |
SYNC_PUSH_TOKEN |
— | Token de autenticação do push |
O sync roda automaticamente 3x ao dia (8h, 14h, 20h) via scheduler interno. O container sync fica sempre ativo, dormindo entre execuções.
# Horários customizados (opcional):
SYNC_SCHEDULE=08:00,14:00,20:00 # padrão
SYNC_ARGS="--headless --classifier gemini" # argumentos extras (opcional)Para rodar manualmente (fora do scheduler):
docker compose run --rm sync python sync.py --headless
# ou com flags:
docker compose run --rm sync python sync.py --headless --firefoxTodos os containers usam TZ=America/Recife (configurável no .env).
# .env
TZ=America/Recife# Na VM, junto com o nginx-proxy:
# 1. Copiar docker-compose.yml e .env
# 2. Ajustar .env:
# USE_EXTERNAL_NET=true
# EXTERNAL_NET=external-name
# SYNC_PUSH_TOKEN=<token-seguro>
# NEXT_PUBLIC_API_URL=https://seu-dominio.com
#
# 3. Subir:
docker compose up -dO sync no host empurra novos raw_posts para a VM via HTTPS com token:
# No .env do host:
SYNC_PUSH_URL=https://seu-dominio.com
SYNC_PUSH_TOKEN=<mesmo-token-da-vm>O endpoint POST /api/sync/raw-posts na VM recebe e insere no banco.
| Método | Rota | Descrição |
|---|---|---|
| GET | /api/posts | Posts (filtro: ?discipline_id=X) |
| GET | /api/disciplines | Disciplinas com contagem |
| GET | /api/graph | Grafo de conhecimento |
| GET | /api/search?q= | Busca textual |
| GET | /api/raw-posts | Posts brutos |
| GET | /api/stats | Estatísticas |
| GET | /api/about | Info do perfil alvo |
| POST | /api/sync/raw-posts | Recebe posts do host (token) |
| Var | Descrição |
|---|---|
| SYNC_MODE | capture, monitor, process |
| FRONTEND_PORT | Porta do frontend (default: 3000) |
| URL_TARGET | URL do perfil LinkedIn |
| URL_ABOUT | URL da página "Sobre" do alvo |
| SYNC_PUSH_URL | URL da VM para push |
| SYNC_PUSH_TOKEN | Token de autenticação |
| USE_EXTERNAL_NET | Usar rede Docker externa |
| EXTERNAL_NET | Nome da rede externa |