Command Palette

Search for a command to run...

douglas_kurotaki_97/review-hub
Public
wasmer run douglas_kurotaki_97/review-hub

Review Hub

Visualizador auto-hospedado de code reviews compartilháveis, com controle de acesso. Você (admin) sobe reviews — em Markdown/JSON estruturado (renderizado num tema consistente) ou em HTML self-contained — e libera para viewers específicos; cada viewer entra e vê só o que você atribuiu.

  • Servidor enxuto — Node + stdlib, três libs de apoio (marked, sanitize-html, gray-matter). Sem build.
  • Reviews estruturados — um contrato (findings, severidade, sugestões de código) validado na entrada e renderizado no tema padrão, então todos ficam com o mesmo visual.
  • Acompanhamento por finding — resumo agregado, status Aberto, Resolvido, Ignorado e Em discussão, histórico simples, comentários internos e filtros por severidade, status, categoria, arquivo e busca.
  • Entrega enxuta no PR — gera um comentário curto em Markdown com resumo, principais achados e link para o review completo.
  • Magic link para viewer — admin pode gerar link manual ou enviar convite por email quando um provedor HTTP estiver configurado.
  • Formatação por IA (opcional) — cola um review cru e a IA reorganiza no contrato (endpoint compatível com a API OpenAI; a chave fica só no servidor).
  • Store em arquivo JSON (data/db.json, escrita atômica).
  • Sessão em cookie assinado (HMAC), senhas com scrypt, CSRF double-submit.
  • Isolamento — reviews rodam num <iframe sandbox>; o markdown passa por sanitização.

Rodar

Requer Node 18+ (recomendado 20.12+ para o carregamento automático do .env).

cd review-hub
npm install                 # marked, sanitize-html, gray-matter
cp .env.example .env         # opcional — dá pra rodar sem
ADMIN_EMAIL=voce@empresa.com ADMIN_PASSWORD=troque-isso PORT=4000 npm start

npm start roda node --env-file-if-exists=.env src/server.js. Se seu Node não tiver o flag --env-file, exporte as variáveis inline (como no exemplo acima) ou use node src/server.js.

No primeiro boot ele cria o admin. Se você não definir ADMIN_PASSWORD, uma senha aleatória é gerada e impressa uma vez no console — anote. O admin só é semeado no primeiro boot; para trocar email/senha depois, apague data/db.json e suba de novo.

Acesse http://localhost:4000, entre como admin, cadastre viewers e crie reviews.

Testar

npm test

Os testes usam node:test sem dependências extras. Há testes unitários para parser, comentário PR e use cases de aplicação, incluindo reviews, assignments, status, comentários por finding, magic links, settings e jobs de IA. Testes de integração sobem o servidor em porta livre com DATA_FILE temporário para validar acesso, CSRF, API JSON, status, comentários por finding e magic links sem tocar data/db.json.

Variáveis de ambiente

VariávelPadrãoDescrição
PORT4000Porta HTTP.
DATA_FILEdata/db.jsonCaminho do store JSON. Em hosts com volume persistente, aponte para o mount do volume.
SESSION_SECRET(gerado)Segredo do HMAC de sessão. Defina um valor estável para as sessões sobreviverem a restarts; se vazio, é gerado e persistido em data/db.json.
ADMIN_EMAILadmin@localEmail do admin semeado no 1º boot.
ADMIN_PASSWORD(gerado)Senha do admin. Se vazia, uma aleatória é impressa uma vez.
MAX_BODY_BYTES8388608Limite do corpo da requisição (reviews em HTML podem ser grandes).
MAGIC_LINK_TTL_MS604800000Validade dos magic links de viewer em milissegundos (padrão: 7 dias).
APP_LOCALEpt-BRIdioma padrão para textos de sistema em convites por email e comentário PR. Suporta pt-BR e en.
LLM_API_KEY(vazio)Chave da IA de formatação. Sem ela, o recurso fica desativado; o resto funciona igual.
LLM_BASE_URLhttps://integrate.api.nvidia.com/v1Endpoint compatível com a API OpenAI (NVIDIA NIM, OpenAI, etc.).
LLM_MODELminimaxai/minimax-m3Modelo usado na formatação.
LLM_TEMPERATURE0.2Temperatura padrão (ajustável na tela).
LLM_MAX_TOKENS8192Tamanho máx. da resposta da IA (ajustável na tela).
EMAIL_PROVIDER_URL(vazio)Endpoint HTTP que recebe convites por email via POST JSON. Para Resend, use https://api.resend.com/emails. Sem ele, envio por email fica desativado.
EMAIL_PROVIDER_TOKEN(vazio)Token opcional enviado como Authorization: Bearer ... ao provedor de email.
EMAIL_FROM(vazio)Remetente incluído no payload do provedor. Obrigatório para Resend, ex.: Apontiq <reviews@apontiq.com>.
EMAIL_SUBJECT(localizado)Assunto dos convites por email. Se vazio, usa o padrão de APP_LOCALE; se preenchido, sobrescreve o texto localizado.

Para comprar domínio e configurar Resend do zero, siga docs/infra/domain-email-resend-setup.md. A decisão de marca/domínio proposta está registrada em docs/adr/2026-07-06-public-domain-and-resend.md.

Formatos de review

Cada review tem um campo format:

structured (recomendado)

Enviado como Markdown com frontmatter ou como JSON — mesma estrutura, duas codificações. Você pode colar o conteúdo no textarea ou carregar um arquivo .md, .markdown ou .json; o arquivo é lido no browser e o conteúdo fica disponível para revisão antes de salvar. É validado na entrada e renderizado no tema padrão.

Obrigatório: title e ao menos 1 finding com severity, title e body.

Cada finding renderizado ganha uma âncora determinística e um botão Copiar link, útil para apontar colegas diretamente para um achado específico.

O status de cada finding fica separado do conteúdo do review: novos achados começam como Aberto, admins podem alterar para Resolvido, Ignorado ou Em discussão, e viewers apenas visualizam. Cada mudança real registra uma atividade recente com status anterior, novo status, admin e horário.

Cada finding também pode receber comentários internos. Admins e viewers atribuídos podem comentar pela página do review; a rota /raw isolada mostra a discussão em modo leitura. Os comentários ficam em findingComments, separados do arquivo estruturado original.

Antes da toolbar, o review mostra um resumo de acompanhamento com contagens por status, severidade, categoria e arquivo. Esse resumo usa apenas findings[], findingStates, findings[].category e findings[].file; não cria uma nova persistência.

A toolbar do review combina busca, severidade, status, categoria e arquivo. Categorias aparecem como chips com contagem; arquivos aparecem em um seletor para manter reviews grandes escaneáveis. Esses filtros usam findings[].category e findings[].file, sem mudar o contrato ou persistir preferências.

CampoNívelObrigatórioDescrição
titletoposimTítulo do review.
subtitle, repo, pr, authortoponãoMetadados.
verdicttoponãoapprove | comment | request_changes.
summarytoponãoMarkdown com a visão geral.
findings[]toposimLista com ≥ 1 item.
findings[].severityfindingsimcritical | high | medium | low | info.
findings[].titlefindingsimTítulo curto.
findings[].bodyfindingsimMarkdown com a explicação.
findings[].categoryfindingnãoEx.: correctness, security, performance, style.
findings[].filefindingnãoCaminho do arquivo.
findings[].linesfindingnão[42] ou [42, 48].
findings[].suggestionfindingnão{ before, after } (diff) ou { code } (substituição), com language opcional.

Se algo faltar ou for inválido, o app recusa e mostra o erro apontando o campo.

Sugestões com language reconhecida recebem syntax highlight simples no relatório. Se a linguagem for desconhecida, o código continua sendo exibido como texto escapado, sem realce.

Exemplo — Markdown (examples/sample-review.md):

---
title: "PR #482 — Cadastro de assinaturas"   # aspas: título com # quebra o YAML
repo: kiskadi/billing
pr: 482
verdict: request_changes
---

Resumo do review em **markdown** (opcional).

## high · correctness · N+1 ao listar assinaturas
file: app/controllers/subscriptions_controller.rb:18-24

Explicação do problema em markdown...

```suggestion ruby
- @subscriptions = current_account.subscriptions
+ @subscriptions = current_account.subscriptions.includes(:plan)
```

No Markdown: o frontmatter carrega os metadados; o texto antes do primeiro ## é o summary; cada ## severidade · categoria · título abre um finding; uma linha file: caminho:linhas é opcional; e um bloco ```suggestion <linguagem> com linhas - /+ vira um diff (ou, sem -/+, uma substituição).

Exemplo — JSON: ver examples/sample-review.json.

html (legado)

Cole um HTML self-contained (documento completo ou fragmento com <style>). Ele é servido cru dentro do <iframe sandbox>, mantendo o visual próprio. Útil para importar relatórios prontos, mas sem consistência visual nem busca/filtro.

Formatação por IA (opcional)

Se LLM_API_KEY estiver definida, ao tentar salvar um review estruturado fora do formato, o app abre um modal com os erros e oferece ✨ Formatar com IA — que reorganiza o texto no contrato (não inventa achados).

  • Assíncrono: a formatação roda como um job em background (POST /admin/ai/formatjobId; o cliente faz polling em GET /admin/ai/format/:jobId) com spinner e tempo decorrido — não trava a conexão, então modelos lentos de raciocínio não dão timeout.
  • Configurável: temperatura e tamanho máx. de resposta, globais (painel → Configurações da IA, persistidos em data/db.json) e por formatação (no modal).
  • A chave vive só no servidor — nunca vai para o browser nem para o git (.env é gitignored).

Sem API key, dá para usar uma IA externa (ChatGPT, Claude): o formulário tem 📋 Copiar prompt para IA (o mesmo prompt está em examples/prompt-ia.md) — cole numa IA com o diff e traga o .md pronto.

Entrega no PR

Depois de criar o review, o app abre uma tela de sucesso com o link do review, viewers atribuídos e botões para Copiar link, Comentário PR e Abrir review. O botão Comentário PR copia um comentário curto em Markdown com veredito, contagem por severidade, até 5 achados principais e o link para o review completo. Cole esse texto no PR em vez de publicar o review inteiro como comentário longo. Os rótulos de sistema desse comentário seguem APP_LOCALE; títulos, resumo e achados continuam no idioma do review.

O link respeita o mesmo controle de acesso do review: o colega precisa estar logado e atribuído ao review para abrir.

Para reduzir fricção, o admin pode gerar um magic link para um viewer existente no painel. O link é de uso único, expira, pode ser revogado e apenas autentica aquele viewer: ele não torna nenhum review público nem concede acesso fora das atribuições.

Se EMAIL_PROVIDER_URL estiver configurada, o painel também mostra Enviar convite: o app cria um magic link e envia um POST JSON ao provedor externo com destinatário, assunto, texto, HTML e metadados. Se o envio falhar, a tela ainda mostra o link para envio manual. O texto do convite segue APP_LOCALE, exceto quando EMAIL_SUBJECT sobrescreve o assunto.

Dentro do review completo, cada finding também tem Copiar link. Use esse permalink quando a conversa precisar apontar para um achado específico.

Admins também podem alterar o status de cada finding dentro do review completo. Viewers enxergam o status e a atividade recente, mas não recebem o seletor de edição. A rota /raw isolada é leitura; a edição depende da página /reviews/:id, que faz o POST autenticado pelo shell do app.

Admins e viewers atribuídos também podem comentar em cada finding dentro do review completo. Esses comentários ficam internos ao Review Hub nesta fase; não sincronizam com GitHub/GitLab.

Documentação embutida

Logado como admin, /admin/docs documenta o contrato (com os enums puxados do próprio validador) e a API HTTP, incluindo os contratos JSON em /api/* e um fluxo curl para criar reviews por script.

Documentação no repositório

AGENTS.md define as regras para contribuições e manutenção de documentação. Toda mudança deve atualizar este README.md; se afetar a experiência dentro do app, atualize também a documentação embutida em src/views.js (docsPage).

Para documentação mais profunda, use a árvore docs/:

  • docs/product/ — roadmap, próximas ações e direção de produto.
  • docs/adr/ — decisões arquiteturais importantes.
  • docs/features/ — especificações, fluxos e decisões de produto.
  • docs/infra/ — contratos de API, plano de monorepo, deploy, segredos, backups e operação.

ADRs iniciais já registram as principais decisões: JSON file store na fase manual, evolução para SQLite antes de Postgres, política de acesso por login/atribuição e persistência de status/histórico por finding separada do conteúdo do review. A decisão proposta para nome público, domínio e Resend está em docs/adr/2026-07-06-public-domain-and-resend.md.

O roadmap de débitos técnicos está em docs/product/technical-debt-roadmap.md. A direção arquitetural registrada é evoluir incrementalmente para monorepo com apps/api em NestJS, apps/web em React/Vite e pacotes internos de domínio/aplicação antes de trocar o runtime atual. O plano operacional para essa transição está em docs/infra/monorepo-incremental-plan.md; a decisão atual é não criar workspace vazio e mover apenas pacotes compartilhados reais.

Para email transacional, docs/infra/domain-email-resend-setup.md descreve a compra de domínio, configuração de DNS, verificação do Resend e preenchimento das variáveis EMAIL_PROVIDER_URL, EMAIL_PROVIDER_TOKEN, EMAIL_FROM, EMAIL_SUBJECT e APP_LOCALE.

Para hospedagem pública inicial, docs/infra/wasmer-hosting-spike.md registra o sandbox gratuito na Wasmer. A ADR em docs/adr/2026-07-06-wasmer-hosting-spike.md aceita Wasmer para testes efêmeros, mas rejeita esse ambiente para reviews definitivos enquanto não houver volume persistente novo ou banco suportado.

O roadmap de produto também registra internacionalização como frente futura: pt-BR permanece o padrão e en já existe na camada inicial para convites por email e comentário PR. Conteúdo do review não é traduzido automaticamente.

Segurança

  • Senhas via scrypt + salt; comparação em tempo constante.
  • Sessão: cookie sid httpOnly, assinado com HMAC-SHA256 (SameSite=Lax).
  • CSRF: token double-submit em todas as mutações (POST, PUT, DELETE).
  • Acesso: viewer só abre um review se estiver atribuído; /reviews/:id/raw tem o mesmo gate.
  • Magic links: token aleatório de uso único; o store guarda apenas hash, expiração, uso e revogação. O link autentica o viewer, mas não concede reviews fora de assignments.
  • Reviews rodam em <iframe sandbox="allow-scripts allow-popups allow-downloads"> (sem allow-same-origin), então o conteúdo não acessa a sessão nem o DOM do app.
  • O markdown dos reviews estruturados passa por marked + sanitize-html (allowlist de tags).

Estrutura

src/
  application/       use cases, validações de aplicação e jobs de IA
  config.js          env + defaults
  domain/            regras puras compartilhadas por app/renderização
  store.js           persistência JSON (users, reviews, assignments, magicLinks, settings)
  infrastructure/    adapters/ports para infraestrutura atual
  api-json.js        handlers JSON em /api/* paralelos à UI server-rendered
  auth.js            scrypt (senha), HMAC (sessão), CSRF
  http.js            helpers de request/response (inclui sendJson)
  review-format.js   parse (JSON/Markdown) + validação do contrato
  render-review.js   estrutura → HTML no tema (acompanhamento, badges, sugestões, filtros)
  syntax-highlight.js realce seguro de sugestões de código
  pr-comment.js      comentário curto em Markdown para colar no PR
  llm.js             formatação por IA (endpoint compatível com OpenAI)
  views.js           tema + templates HTML (páginas, formulário, modal, docs)
  server.js          servidor + rotas HTTP
examples/
  sample-review.md   modelo estruturado (Markdown)
  sample-review.json modelo estruturado (JSON)
  prompt-ia.md       prompt para gerar reviews com uma IA externa
  pr-13256-review.html exemplo de review em HTML
docs/
  product/           roadmap e próximas ações
  features/          specs e template de features
  adr/               decisões arquiteturais e template de ADR
  infra/             contratos HTTP JSON, monorepo, operação, schema SQLite e migração futura
test/
  *.test.js          testes node:test unitários e de integração
app.yaml             config candidata para spike na Wasmer
wasmer.toml          pacote candidato usando edgejs-quickjs no spike Wasmer
.wasmerignore        exclusões de deploy (secrets, data, backups, logs)
data/db.json         criado em runtime (gitignored)

Modelo de dados

  • usersadmin (você) e viewer (quem você cadastra).
  • reviewsformat (structured | html), title, subtitle, e data (estruturado) ou bodyHtml (legado). Em reviews estruturados, findingStates guarda o status por finding, findingStatusHistory guarda as mudanças e findingComments guarda comentários internos por achado, todos separados de data.
  • assignments — quais viewers veem quais reviews (o admin vê todos).
  • magicLinks — tokens de uso único para autenticar viewers; o JSON guarda só o hash, expiração, uso e revogação.
  • meta — segredo de sessão e configurações da IA.

Preparação SQLite

O runtime atual continua usando data/db.json. A migração futura para SQLite já tem um contrato inicial documentado em docs/infra/sqlite-schema.sql e docs/infra/sqlite-migration.md: preservar IDs, migrar com upserts idempotentes, manter assignments como gate de acesso e guardar apenas token_hash para magic links.

API HTTP

Há duas superfícies HTTP:

  • UI server-rendered: rotas HTML atuais, com corpo application/x-www-form-urlencoded.
  • API JSON: rotas paralelas em /api/*, com corpo application/json, documentadas em docs/infra/api-contracts.md.

Corpo application/x-www-form-urlencoded. Toda mutação (POST) exige _csrf igual ao cookie csrf.

MétodoRotaAcessoO quê
GET/healthzpúblicaHealthcheck operacional para hospedagem; retorna JSON sem sessão nem dados sensíveis.
POST/loginpúblicaemail, password, _csrf → cookie sid.
GET/magic/:tokenpúblicaConsome magic link válido, cria sessão do viewer e redireciona para /.
POST/logoutloginEncerra a sessão.
GET/reviews/:idlogin + acessoPágina do review (iframe).
GET/reviews/:id/pr-commentlogin + acessoComentário curto em Markdown para colar no PR.
GET/reviews/:id/rawlogin + acessoHTML do review (servido no iframe).
POST/reviews/:id/findings/:findingId/statusadminAtualiza status (open, resolved, ignored, discussing) e registra histórico se houve mudança real.
POST/reviews/:id/findings/:findingId/commentslogin + acessoAdiciona comentário interno ao finding. Campo: body.
GET/adminadminPainel (reviews, viewers, configurações).
GET/admin/docsadminDocumentação do contrato + API.
GET/admin/reviews/:id/successadminTela pós-criação com link, comentário PR e viewers atribuídos.
GET/POST/admin/reviews/new, /admin/reviewsadminFormulário / criar review.
GET/POST/admin/reviews/:id/edit, /admin/reviews/:idadminFormulário / editar.
POST/admin/reviews/:id/deleteadminExcluir review.
POST/admin/ai/validateadminValida source (sem IA) → JSON { ok, errors }.
POST/admin/ai/formatadminInicia formatação por IA → JSON { jobId }.
GET/admin/ai/format/:jobIdadminStatus do job → JSON { status, result, errors }.
POST/admin/settingsadminSalva temperatura / tokens da IA.
POST/admin/users, /admin/users/:id/deleteadminCria / remove viewer.
POST/admin/users/:id/magic-linksadminGera magic link de uso único para um viewer.
POST/admin/users/:id/invitationsadminGera magic link e envia convite por email se configurado.
POST/admin/magic-links/:id/revokeadminRevoga magic link ainda ativo.

Principais rotas JSON:

MétodoRotaAcessoO quê
GET/POST/DELETE/api/sessionpública/loginCSRF, login e logout por JSON.
GET/POST/PUT/DELETE/api/reviews, /api/reviews/:idlogin/adminLista, cria, lê, atualiza e exclui reviews.
GET/PUT/api/reviews/:id/assignmentsadminLê e atualiza viewers atribuídos.
PUT/api/reviews/:id/findings/:findingId/statusadminAtualiza status por finding.
GET/api/reviews/:id/commentslogin + acessoLista comentários internos do review.
POST/api/reviews/:id/findings/:findingId/commentslogin + acessoCria comentário interno por finding.
GET/POST/DELETE/api/viewers, /api/viewers/:idadminLista, cria e remove viewers.
GET/POST/DELETE/api/viewers/:id/magic-links, /api/viewers/:id/invitations, /api/magic-links/:id, /api/magic-links/consumeadmin/públicaLista, gera, envia convite, consome e revoga magic links.
GET/PUT/api/settingsadminLê e salva settings de IA.
POST/GET/api/ai/validate, /api/ai/format, /api/ai/format/:jobIdadminValida review e controla jobs de formatação.

Expor para outras pessoas

Estado atual: Wasmer está funcionando como sandbox gratuito de teste, com dados efêmeros. A URL pública atual é https://apontiq-review-hub-douglas-kurotaki-97.wasmer.app.

O spike foi executado com a CLI autenticada em 2026-07-06. O runtime antigo wasmer/edgejs falhou por import N-API ausente; o runtime atual wasmer/edgejs-quickjs roda o servidor Node. Localmente, use wasmer run --net . para permitir que server.listen abra socket. Novos apps com volume foram recusados nas regiões testadas (us-socal1 e be-mons1), então app.yaml usa DATA_FILE=/tmp/review-hub/apontiq-sandbox-db.json, sem volume persistente.

Fallback rápido para demonstração local:

zrok share public http://localhost:4000

Defina um SESSION_SECRET estável em qualquer ambiente público para que as sessões sobrevivam a restarts.

Sandbox Wasmer:

Arquivos preparados:

  • app.yaml — app apontiq-review-hub, owner douglas_kurotaki_97 e DATA_FILE=/tmp/review-hub/apontiq-sandbox-db.json efêmero.
  • wasmer.toml — pacote usando wasmer/edgejs-quickjs para executar src/server.js.
  • .wasmerignore — evita enviar .env, .git, data/, backups e logs.

Fluxo para o próximo spike:

npm ci --omit=dev
npm test
npm start
curl http://localhost:4000/healthz

wasmer login
OWNER=<seu-usuario-ou-namespace>
wasmer package build --check
wasmer run --net .
curl http://localhost:4000/healthz
wasmer deploy --publish-package --bump --owner "$OWNER"
# cadastre os secrets conforme docs/infra/wasmer-hosting-spike.md
wasmer app info

O runbook completo está em docs/infra/wasmer-hosting-spike.md.

Próximos passos possíveis

  • Configurar secrets no dashboard da Wasmer: ADMIN_EMAIL, ADMIN_PASSWORD, SESSION_SECRET, EMAIL_PROVIDER_* e LLM_*.
  • Testar login admin, criação de review descartável, convite Resend e acesso viewer na URL Wasmer.
  • Apontar app.apontiq.com para a Wasmer depois que os secrets e /healthz estiverem validados.
  • Escolher persistência real depois: volume, banco gerenciado, VPS pequena ou migração do store.
  • Expandir internacionalização para UI completa e documentação embutida.
  • Extrair packages/domain apenas quando houver consumidor real fora de src/.
  • Trocar o runtime JSON para SQLite quando houver necessidade operacional clara; deploy num host real (Fly/Render/VPS).

Apontiq review sharing app