No description
  • Python 55.8%
  • JavaScript 34.4%
  • Shell 3.7%
  • Bru 3.2%
  • CSS 2.2%
  • Other 0.7%
Find a file
Jônatas Lima bf75729b37
All checks were successful
EEG QA CI / validate-and-build (push) Successful in 2m8s
EEG QA CI / Deploy production (push) Successful in 27s
EEG QA CI / Deploy development (push) Has been skipped
Merge pull request 'develop' (#23) from develop into main
Reviewed-on: #23
2026-09-04 22:35:15 -03:00
.claude refactor: mover perfil EEG do Report pra eeg_reports e aninhar no contrato da API 2026-09-02 23:48:05 -03:00
.forgejo/workflows refactor: Report referencia TestRun em vez de embutir ReportScenario/Step 2026-09-03 00:42:51 -03:00
backend feat: sincronizar Report com card do HubSpot (nota + PDF + estágio opcional) 2026-09-04 14:05:26 -03:00
bruno feat: sincronizar Report com card do HubSpot (nota + PDF + estágio opcional) 2026-09-04 14:05:26 -03:00
docs Merge pull request 'feat: sincronizar Report com card do HubSpot' (#22) from feature/hubspot-card-sync into develop 2026-09-04 15:14:43 -03:00
frontend Merge pull request 'feat: sincronizar Report com card do HubSpot' (#22) from feature/hubspot-card-sync into develop 2026-09-04 15:14:43 -03:00
scripts ci: deploy development from shared VPS directory 2026-08-25 01:03:11 -03:00
.dockerignore chore: unify environments and publish documentation 2026-08-13 21:45:43 -03:00
.env.dev.example fix: improve SQLite and integration resilience 2026-08-16 15:55:24 -03:00
.env.example fix: improve SQLite and integration resilience 2026-08-16 15:55:24 -03:00
.gitignore refactor: separar reporting genérico da especialização EEG via extension point 2026-09-03 00:07:02 -03:00
AGENTS.md ci: deploy development from shared VPS directory 2026-08-25 01:03:11 -03:00
compose.yaml perf: chunk do Recharts vazava pra toda página + compressão/cache ausentes 2026-09-03 11:34:13 -03:00
Dockerfile.docs chore: unify environments and publish documentation 2026-08-13 21:45:43 -03:00
docs-nginx.conf test: cover frontend API behavior and harden static headers 2026-08-16 15:55:36 -03:00
docs.sh docs: add navigable MkDocs site 2026-08-13 21:45:35 -03:00
mkdocs.yml docs: define modular monolith and DDD-lite direction 2026-08-16 16:26:22 -03:00
README.md feat: add Validação Técnica checklist to the EEG module 2026-08-21 14:16:19 -03:00
requirements-docs.txt docs: add navigable MkDocs site 2026-08-13 21:45:35 -03:00
start-local.sh chore: unify environments and publish documentation 2026-08-13 21:45:43 -03:00
start.bat initial commit 2026-08-13 18:50:44 +02:00
start.sh ci: harden production deployment and rollback 2026-08-16 05:19:40 -03:00
stop.sh chore: unify environments and publish documentation 2026-08-13 21:45:43 -03:00

EEG QA — Report de Controle de Qualidade de Exames de EEG

App para registrar, taggear e tirar métricas dos reports de QA dos exames de EEG.

  • Backend: Python + FastAPI + SQLite (tudo, inclusive os anexos, fica em um único arquivo eeg_qa.db), com Alembic pra migrations
  • Frontend: React + Vite

Não é preciso saber Python ou React para rodar — siga o passo a passo abaixo.

📖 Documentação completa (arquitetura, guia de uso tarefa-a-tarefa, ADRs, diagramas): docs/README.md. Localmente, instale requirements-docs.txt e rode ./docs.sh serve para abrir http://127.0.0.1:8001. No Compose, ela é publicada em DOCS_DOMAIN com o mesmo Basic Auth da aplicação.

Estrutura

eeg-qa-report/
├── backend/            # API (FastAPI)
│   ├── main.py           # rotas da API
│   ├── models.py         # tabelas do banco
│   ├── schemas.py        # validação dos dados
│   ├── database.py       # conexão com o SQLite + migrations
│   ├── alembic/           # migrations do banco (histórico de mudanças de schema)
│   ├── modules/           # capacidades verticais (ex.: eeg_reports/Validação Técnica)
│   ├── pdf_export.py      # export em PDF
│   ├── html_export.py     # export em HTML
│   ├── hubspot_client.py  # integração com o HubSpot
│   ├── ai_client.py       # scaffold de geração de cenário via IA
│   ├── migrate_old_data.py # importa dados de um banco em schema antigo
│   ├── tests/              # testes automatizados
│   └── requirements.txt
├── frontend/           # Interface (React)
│   └── src/
│       ├── App.jsx
│       ├── api.js
│       └── components/
├── docs/               # arquitetura, guia de uso, ADRs, diagramas (D2), API (OpenAPI)
└── bruno/              # coleção Bruno pronta pra usar

1. Rodando o backend

Pré-requisito: Python 3.10+ instalado.

cd backend
python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install -r requirements.txt
uvicorn main:app --reload --port 8000

O backend sobe em http://localhost:8000. A documentação interativa (útil para testar a API direto no navegador) fica em http://localhost:8000/docs.

Na primeira vez que rodar, o arquivo backend/eeg_qa.db é criado automaticamente, já aplicando as migrations do Alembic — é o seu banco de dados. Faça backup desse arquivo periodicamente (é só copiá-lo para outro lugar). Detalhes sobre como isso funciona: seção 6 (Migrations).

Atalho: subir os dois juntos

Em vez de repetir os passos 1 e 2 toda vez em dois terminais separados, dá pra usar o script incluído na raiz do projeto:

  • macOS/Linux: ./start.sh — sobe backend e frontend num único terminal; Ctrl+C para os dois de uma vez.
  • Windows: dê dois cliques em start.bat (ou rode start.bat no cmd) — ele abre uma janela para o backend e outra para o frontend; feche as duas para parar.

Na primeira vez que rodar, o script cria o ambiente virtual do Python e instala as dependências do frontend automaticamente (pode demorar um pouco).

2. Rodando o frontend (manual, em dois terminais)

Pré-requisito: Node.js 18+ instalado.

Em outro terminal:

cd frontend
npm install
npm run dev

Acesse http://localhost:5173 no navegador.

Se o backend estiver rodando em outro endereço (por exemplo, num servidor da rede), crie um arquivo frontend/.env com:

VITE_API_URL=http://IP-DO-SERVIDOR:8000

3. Usando o app

Resumo rápido abaixo — guia completo, tarefa por tarefa, em docs/USER_GUIDE.md.

  • Novo report: dê um título livre ao report (ele aparece como cabeçalho do PDF/HTML exportado) e, se quiser, um TL;DR — um resumo de uma linha pra quem só vai bater o olho (ex.: "Reprovado por impedância alta em Fp1/Fp2"); é opcional e só aparece nos exports quando preenchido, então na maioria dos reports (aprovados, sem intercorrência) pode deixar em branco. Preencha também uma ou mais manufaturas e a versão/build testada (ambos funcionam como as tags — multi-select, com sugestões de quem já foi usado e possibilidade de digitar um valor novo), se o exame tem canal Photic/Cardio/vídeo, os dados da revisão de QA (incluindo aprovado por/data), e as informações de rastreamento do teste (link do card no HubSpot, bug ou melhoria, ambiente dev/prod, reports relacionados). Adicione cenários de teste — reaproveitando algum salvo na Biblioteca, importando de um ticket do HubSpot ou de um texto colado (extrai os passos automaticamente se estiverem numerados ou com marcadores), ou criando um avulso na mão — e quantos achados (issues) forem necessários, cada um com categoria, severidade, descrição, passos para reproduzir e comportamento esperado/observado. Cole anexos direto do clipboard (print, log, qualquer arquivo — copie e clique na área "Cole aqui" e aperte Ctrl+V, ou arraste o arquivo). Um rascunho é salvo automaticamente no navegador enquanto você preenche (exceto anexos).
  • Biblioteca: crie suítes e cenários de teste reutilizáveis (nome, descrição curta e passos numerados) pra reaproveitar em sanity checks e regressões, sem digitar tudo de novo a cada report. Cada cenário tem versão — editar não afeta reports/runs que já usaram uma versão anterior, e o histórico de versões fica disponível num clique.
  • Plans e Runs: pra rodar uma suíte inteira de forma organizada (sem precisar de um Report por trás) — agrupe runs num plan (ex.: por release), rode os cenários marcando o status de cada passo, e acompanhe a taxa de aprovação calculada automaticamente.
  • Reports: lista todos os reports já criados (com o TL;DR, quando tiver, aparecendo como subtítulo direto na listagem), com filtros por manufatura, tag, categoria de achado, severidade, status, tipo (bug/melhoria) e ambiente, busca livre, indicador de ação corretiva pendente (com quantos dias em aberto) e botão de exportar para CSV (abre no Excel).
  • Dentro de um report: acompanhe o status de cada passo de cada cenário (Não testado/Passou/Falhou/Bloqueado) com evidências anexadas por passo, relacione reports entre si (regressão de X, mesmo problema de Y), e use o botão Duplicar pra reaproveitar cenários de teste num novo report (reseta status e limpa achados/comentários, mantendo o contexto do exame).
  • Métricas: totais, nota média, tempo médio de ciclo, pendência mais antiga em aberto, achados por categoria/severidade, reports por status/manufatura/versão, cobertura de canais testados, bug vs. melhoria, ambiente de testes, evolução mensal, tags mais usadas — e um botão pra baixar um resumo executivo em PDF (últimos 7/30/90 dias).
  • Validação Técnica: checklist de onboarding de uma clínica/equipamento na plataforma (clínica, destinatária, período, responsáveis, equipamentos, itens do checklist e status geral), separado dos reports comuns. Os itens do checklist já vêm pré-cadastrados como cenários reutilizáveis na Biblioteca (suíte "Validação Técnica EEG"), com opção de marcar cada um como Aprovado ou Ressalva (com observação) e de adicionar itens avulsos. Export em DOCX e PDF seguindo um layout fixo; diferente dos exports de report, a seção de ressalvas sempre aparece — mostra "Nada consta." quando não há nenhuma. Campos para link do deal/ticket no HubSpot já existem no formulário, mas a busca automática de dados ainda não está configurada.
  • Baixar PDF / HTML: na página de cada report, dois formatos de export — PDF e HTML (um arquivo único autocontido, com CSS e imagens embutidos, fácil de abrir em qualquer navegador ou anexar num ticket sem depender de nada externo) — cada um em versão interna completa ou "para cliente" (omite HubSpot, revisor, tags e comentários internos). O TL;DR (quando preenchido) aparece em destaque logo no topo, nos dois formatos. A tabela de cenários e o detalhamento dos passos só aparecem se houver cenários registrados; achados e comentários/ação corretiva também só aparecem se tiverem conteúdo — regra que vale nos dois formatos.

Tags são livres — você digita e cria na hora; sugestões de tags já usadas (e de manufaturas) aparecem de cara, antes mesmo de você digitar qualquer coisa — evita criar uma quase-duplicata por não saber que já existe.

4. Evoluindo para uso em rede / múltiplos usuários

Hoje o SQLite funciona bem para uso local ou por um time pequeno acessando o mesmo backend.

  1. Pra sair do "cada máquina com seu próprio banco" e centralizar num servidor acessível pelas duas máquinas (ou por um time), veja a seção 5 (deploy em VPS com Docker) — já inclui login básico na frente.
  2. Se o volume de uso crescer bastante (muitos usuários gravando ao mesmo tempo), o SQLite pode virar gargalo — nesse ponto, trocar para Postgres é uma migração relativamente simples, já que o código usa SQLAlchemy + Alembic (troca a DATABASE_URL e ajusta o driver, ex. postgresql+psycopg2://...; as migrations existentes aplicam no banco novo do mesmo jeito). Os anexos (guardados como BLOB) migram junto, sem mudança de modelo.
  3. Autenticação por usuário/permissões (além do login único provisório da seção 5) ainda não existe — hoje é tudo ou nada por trás da senha compartilhada.

5. Deploy em VPS com Docker + Traefik

O deploy usa o padrão já existente na VPS: rede externa proxy, entrypoint HTTPS websecure e certificate resolver letsencrypt.

Frontend e API são publicados no mesmo domínio. O frontend ocupa / e o backend atende /api, /docs e /openapi.json. Isso evita problemas de CORS e de HTTP Basic Auth entre dois subdomínios.

Pré-requisitos na VPS

  • Docker e Docker Compose instalados.
  • Traefik conectado à rede externa proxy.
  • Um registro DNS, por exemplo eegqa.jonataslima.xyz, apontando para a VPS.

Confirme a rede:

docker network inspect proxy >/dev/null && echo "Rede proxy encontrada"

Configuração

Na raiz do projeto:

cp .env.example .env
sudo apt update
sudo apt install -y apache2-utils
htpasswd -nB jonatas

Edite o .env e cole a saída do htpasswd entre aspas simples:

APP_DOMAIN=eegqa.jonataslima.xyz
CERT_RESOLVER=letsencrypt
BASIC_AUTH_USERS='jonatas:$2y$05$HASH_REAL_AQUI'

Não remova as aspas simples. Elas preservam os caracteres $ do bcrypt dentro do .env.

Duas integrações opcionais — deixe em branco se não for usar agora, sem problema (o app funciona normalmente sem elas, só os botões correspondentes ficam desabilitados com um erro explicativo):

# HubSpot: buscar nome/pipeline/estágio/passos sugeridos a partir do link do card.
# Gere em HubSpot > Configurações > Integrações > Private Apps (scope crm.objects.tickets.read).
HUBSPOT_ACCESS_TOKEN=

# IA (geração de cenário): scaffold pronto pra apontar pra uma instância Ollama.
AI_BASE_URL=
AI_MODEL=

Validar e subir

mkdir -p data
./start.sh prod
docker compose logs --tail=100 backend frontend

Acesse:

https://eegqa.jonataslima.xyz

A documentação navegável fica em:

https://docs-eegqa.jonataslima.xyz

O navegador pedirá o usuário e a senha do HTTP Basic Auth. A documentação da API ficará em:

https://eegqa.jonataslima.xyz/docs

Teste pelo terminal:

# Sem credenciais, 401 é o resultado esperado.
curl -I https://eegqa.jonataslima.xyz

# Com credenciais, deve retornar {"ok":true}.
curl -u jonatas:'SUA_SENHA' https://eegqa.jonataslima.xyz/api/health

Persistência

O arquivo persistente fica em:

./data/eeg_qa.db

Ele contém os reports e também os anexos armazenados como BLOB. Não apague a pasta data ao atualizar o projeto.

O caminho normal de atualização é um PR aprovado para main: o Forgejo CI publica imagens pelo SHA, sincroniza/valida o Compose, cria backup e promove via SSH. Em uma contingência manual na VPS:

./start.sh prod

O schema do banco agora é controlado por migrations do Alembic (ver seção 6) — o próprio backend aplica as migrations pendentes automaticamente ao iniciar, então não precisa mais apagar o banco a cada atualização de schema. Antes de atualizar pela primeira vez depois de uma mudança grande de schema, faça backup de ./data/eeg_qa.db por garantia.

6. Migrations (Alembic) — atualizando o banco sem perder dados

O schema é controlado por Alembic. O backend, ao subir, roda as migrations pendentes automaticamente (idempotente — se já estiver tudo aplicado, não faz nada).

No dia a dia você não precisa fazer nada — o container já aplica as migrations pendentes ao iniciar. Só entra em cena manualmente quando o schema em si (models.py) for alterado:

cd backend
# depois de editar models.py:
alembic revision --autogenerate -m "descrição da mudança"
# revise o arquivo gerado em alembic/versions/ antes de aplicar
alembic upgrade head

Migrando dados de um banco no schema antigo (pré-Alembic)

Se você tem um eeg_qa.db de antes dessa mudança (schema antigo, sem as tabelas/colunas mais recentes), use backend/migrate_old_data.py pra importar os dados pro banco novo sem perder nada:

# 1. crie um banco novo vazio, no schema atual
DATABASE_URL="sqlite:////caminho/absoluto/eeg_qa_novo.db" alembic upgrade head

# 2. importe os dados do banco antigo pro novo
python migrate_old_data.py /caminho/eeg_qa_antigo.db /caminho/eeg_qa_novo.db

# 3. use --dry-run primeiro se quiser só ver o resumo sem gravar nada
python migrate_old_data.py /caminho/eeg_qa_antigo.db /caminho/eeg_qa_novo.db --dry-run

O script funciona só com a biblioteca padrão do Python (não precisa instalar nada) e pode rodar dentro do container (docker compose run --rm backend python3 migrate_old_data.py ...) direto na VPS, sem precisar tirar o banco do lugar. Ele também resolve um caso específico: manufaturas que antes eram um campo único (ex.: "Nihon Koden; Neurotec: Neurovirtual") são separadas automaticamente em manufaturas individuais, unificando nomes repetidos.

SQLite vs. Postgres

Quem resolve o problema de perder dado a cada mudança de schema é o Alembic, não o banco em si — funciona igual em SQLite e Postgres. Ficar no SQLite por agora e migrar pra Postgres só se sentir gargalo de escrita concorrente na prática é a recomendação (ver seção 4).

7. API — OpenAPI + Bruno

Pra integrar ou automatizar qualquer coisa contra a API, tem duas opções (documentadas em detalhe em docs/API.md):

  • Swagger UI: com o backend rodando, /docs sempre mostra a versão mais atual, sem precisar gerar nada.
  • Coleção Bruno pronta em bruno/ — abra direto no Bruno ("Open Collection"), escolha o ambiente Local ou Producao, e já tem todos os endpoints organizados por área (Reports, Achados, Cenários, Biblioteca, Anexos, Relacionamentos, Picklists, Métricas, HubSpot, Sistema), com bodies de exemplo preenchidos.

O arquivo docs/openapi.json é gerado a partir do app de verdade (python docs/export_openapi.py) — então também pode ser importado em qualquer outra ferramenta que leia OpenAPI (Postman, Insomnia, etc.).

8. Integração com HubSpot

Dá pra buscar nome, pipeline/estágio, prioridade e ticket relacionado de um card do HubSpot direto no app, e usar a descrição do ticket pra gerar os passos de um cenário de teste automaticamente (se a descrição tiver uma lista numerada ou com marcadores).

Isso não é "aproveitar" uma sessão logada no navegador — não é tecnicamente possível de um jeito seguro. O caminho é gerar um Private App Token, uma vez, na sua conta do HubSpot:

  1. HubSpot → Configurações → Integrações → Private Apps → criar um app novo.
  2. Dar o scope crm.objects.tickets.read (e crm.objects.deals.read se também for usar deals).
  3. Copiar o token gerado (algo como pat-na1-...) pro .env, em HUBSPOT_ACCESS_TOKEN (ver seção 5).

Com isso configurado, o botão "Buscar no HubSpot" (na seção de Cenários de teste, tanto ao criar quanto dentro de um report existente) aceita o link do card e devolve os dados pra você revisar antes de usar. Sem link à mão (ou sem querer configurar o token agora), a aba "Colar texto" faz a mesma extração de passos numerados a partir de qualquer texto colado — útil também pra copiar de um ticket do Jira, e-mail, etc.

Limitações que valem saber:

  • A extração de passos por texto é baseada em padrão (1. , 1) , - , no início da linha), não é IA — listas em formatos muito diferentes desses podem não ser reconhecidas. Nesse caso, dá pra editar os passos manualmente depois de importar.
  • Os nomes de propriedade usados (subject, content, hs_pipeline, etc.) são os padrão do HubSpot para tickets/deals. Se sua conta usa propriedades customizadas no lugar dessas, ajuste backend/hubspot_client.py.
  • Essa integração foi implementada e testada com respostas mockadas (backend/tests/test_hubspot_client.py) — não foi validada contra uma conta HubSpot real. Ao configurar o token pela primeira vez, vale testar com um ticket conhecido antes de confiar no fluxo.

9. Geração de cenário via IA (scaffold)

O botão " Gerar com IA" (ao lado da importação do HubSpot) já existe e já chama o backend — só não faz nada de útil ainda porque não tem IA configurada. Pensado pra apontar pra uma instância Ollama rodando na sua VPS (ver seção 5 pra onde configurar AI_BASE_URL/AI_MODEL).

Assim que essas duas variáveis existirem, o botão passa a funcionar de verdade — sem precisar de outra atualização de código. Se um dia trocar de provedor (ex.: API compatível com OpenAI), a única função que precisa mudar é _call_model() em backend/ai_client.py; o resto (prompt, parsing da resposta, endpoint, botão) continua igual.

10. Personalizando os campos

As opções de categoria de achado, severidade, status, tipo (bug/melhoria), ambiente e status de passo de cenário estão centralizadas em frontend/src/api.js (constantes CATEGORIES, SEVERITIES, STATUSES, ISSUE_TYPES, ENVIRONMENTS, STEP_STATUSES, RUN_STATUSES). Para adicionar ou renomear opções, basta editar essas listas ali.

A lista de manufaturas, assim como as tags, não é fixa: ela cresce conforme você digita valores novos ao criar reports, e passa a aparecer como sugestão de autocomplete nos próximos.