e-li-nps/README.md

10 KiB

e-li.nps (Go + HTMX)

Widget NPS embutível via 1 arquivo JS + API em Go.

Requisitos

  • Go 1.22+
  • PostgreSQL 14+

Variáveis de ambiente

  • DATABASE_URL (obrigatória)
    • Ex: postgres://postgres:postgres@localhost:5432/gonps?sslmode=disable
  • ADDR (opcional, default :8080)
  • SENHA_PAINEL (opcional)
    • Se definida, habilita o painel em /painel.
    • Se vazia, o painel fica desabilitado.

Cache do widget (e-li.nps.js)

O servidor controla o cache de /static/e-li.nps.js via ETag.

  • A versão (ETag) é gerada automaticamente a cada inicialização do servidor.
  • O browser é instruído a revalidar (Cache-Control: no-cache, must-revalidate), então:
    • se o ETag não mudou: o servidor responde 304 (rápido)
    • se o ETag mudou: o browser baixa o JS novo automaticamente

Isso evita problemas de clientes com JS antigo em cache após mudanças.

Arquivo .env

O servidor carrega automaticamente um arquivo .env na raiz do projeto (se existir) usando godotenv. Isso facilita rodar localmente sem exportar variáveis manualmente.

Exemplo de .env:

DATABASE_URL='postgres://postgres:postgres@localhost:5432/gonps?sslmode=disable'
ADDR=':8080'

Como rodar

  1. Suba um Postgres (exemplo via Docker):
docker run --rm -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=gonps -p 5432:5432 postgres:16
  1. Rode o server:
go run ./cmd/server

Rodar com Docker

Este repositório inclui:

  • Dockerfile (build multi-stage do binário Go)
  • docker-compose.yml (apenas o app; Postgres é externo)

Para subir tudo:

docker compose up --build

Dica: esse comando roda em foreground. Ao pressionar Ctrl+C, o Docker encerra os containers.

Para manter rodando em background (recomendado em servidor):

docker compose up -d --build

Para forçar rebuild da imagem (mesmo sem mudanças detectadas):

docker compose build --no-cache && docker compose up

Para parar a aplicação:

docker compose down

Para apenas parar sem remover (mantém o container):

docker compose stop

Para iniciar novamente após stop:

docker compose start

Importante:

  • O Postgres é externo.
  • O arquivo .env é obrigatório e deve ser passado como volume para /app/.env.
  • O servidor carrega esse arquivo automaticamente via godotenv ao iniciar.

Exemplo (compose): ./.env:/app/.env:ro

Postgres no host (host.docker.internal)

Se o seu Postgres estiver rodando no host (fora do container) e você quiser que o container acesse via host.docker.internal, use no .env:

DATABASE_URL='postgres://usuario:senha@host.docker.internal:5432/seu_banco?sslmode=disable'

No Linux, o docker-compose.yml já inclui extra_hosts com host-gateway para esse hostname funcionar.

Publicar com Caddy (reverse proxy)

Este repositório inclui um exemplo de Caddyfile para publicar o serviço em:

  • https://nps.idz.one{ip-app}:8080

Pré-requisitos

  • O DNS de nps.idz.one deve apontar para o IP público do servidor onde o Caddy roda.
  • Portas 80/443 liberadas para o Caddy (TLS automático).

IP real do usuário

O Caddy repassa o IP do cliente via X-Forwarded-For e X-Real-IP. O servidor Go já usa middleware.RealIP (chi), então o IP real chega corretamente e é gravado em ip_real.

Check do IP real (direto / Docker / Caddy)

O painel tem um endpoint de debug que mostra o IP que a aplicação está enxergando e os headers recebidos:

  • GET /painel/debug/ip

Passo a passo:

  1. Faça login no painel em /painel.
  2. Acesse /painel/debug/ip.

O JSON retornado inclui:

  • remote_addr (já após o middleware.RealIP)
  • x_forwarded_for
  • x_real_ip

Interpretação esperada:

  • Rodando direto (sem proxy): remote_addr deve ser o IP do cliente (ou do seu balanceador).
  • Rodando via Docker: se você acessar diretamente a porta publicada, o remote_addr tende a ser o IP do host/bridge; atrás de proxy (Caddy), o remote_addr deve refletir o IP real.
  • Rodando via Caddy: x_forwarded_for deve conter o IP real do cliente e o remote_addr deve refletir esse IP após RealIP.

Depois acesse:

  • Home/README: http://localhost:8080/
  • Teste do widget: http://localhost:8080/teste.html
  • Painel: http://localhost:8080/painel (senha em SENHA_PAINEL)

Painel:

  • Acesse http://localhost:8080/painel
  • Você será redirecionado para /painel/login

Healthcheck:

curl -i http://localhost:8080/healthz

Incluir o widget em outra aplicação

Tipagem TypeScript (opcional)

Se você quiser ter autocomplete e validação de tipos no seu projeto (TS), pode declarar a interface abaixo:

declare global {
  interface Window {
    ELiNPS: {
      init: (opts: ELiNPSInitOptions) => Promise<void> | void;
    };
  }
}

export type ELiNPSInitOptions = {
  // apiBase (opcional)
  // Base da API do e-li.nps.
  // Se o widget estiver sendo servido pelo mesmo host, pode deixar vazio.
  apiBase?: string;

  // cooldownHours (opcional)
  // Tempo (em horas) de cooldown visual no navegador.
  cooldownHours?: number;

  // data_minima_abertura (opcional)
  // Bloqueia a abertura do modal antes de uma data.
  // Formato ISO (data): YYYY-MM-DD (ex.: "2026-01-01").
  data_minima_abertura?: string;

  // produto_nome (obrigatório)
  produto_nome: string;

  // inquilino_codigo (obrigatório)
  inquilino_codigo: string;

  // inquilino_nome (obrigatório)
  inquilino_nome: string;

  // usuario_codigo (obrigatório)
  usuario_codigo: string;

  // usuario_nome (obrigatório)
  usuario_nome: string;

  // usuario_telefone (opcional)
  usuario_telefone?: string;

  // usuario_email (opcional)
  usuario_email?: string;
};
<!-- Carrega o widget (arquivo único) -->
<script src="http://localhost:8080/static/e-li.nps.js"></script>

<script>
  window.ELiNPS.init({
    // apiBase (opcional)
    // Base da API do e-li.nps.
    // - Se o widget estiver sendo servido pelo mesmo host, pode deixar vazio.
    // - Se a API estiver em outro host, informe a URL completa.
    // Ex.: "https://sua-api.exemplo.com".
    apiBase: 'http://localhost:8080',

    // cooldownHours (opcional)
    // Tempo (em horas) de cooldown visual no navegador para evitar o modal
    // reaparecer em sequência.
    // Default: 24.
    cooldownHours: 24,

    // data_minima_abertura (opcional)
    // Bloqueia a abertura do modal antes de uma data.
    // Formato ISO (data): YYYY-MM-DD (ex.: "2026-01-01").
    // Ex.: data_minima_abertura: '2026-01-01',
    data_minima_abertura: '',

    // produto_nome (obrigatório)
    // Nome livre do produto (é exibido ao usuário exatamente como informado).
    // Exemplos: "e-licencie.gov", "Cachaça & Churras".
    // Importante: o backend normaliza apenas para montar nome de tabela/rotas.
    produto_nome: 'e-licencie.gov',

    // inquilino_codigo (obrigatório)
    // Código do cliente/tenant (usado nas regras de exibição e no banco).
    inquilino_codigo: 'acme',

    // inquilino_nome (obrigatório)
    // Nome do cliente/tenant (exibição / auditoria).
    inquilino_nome: 'ACME LTDA',

    // usuario_codigo (obrigatório)
    // Identificador do usuário.
    // Importante: é a chave principal para as regras de exibição.
    usuario_codigo: 'u-123',

    // usuario_nome (obrigatório)
    // Nome do usuário (exibição / auditoria).
    usuario_nome: 'Maria',

    // usuario_telefone (opcional)
    // Telefone do usuário (auditoria). Pode ser vazio.
    usuario_telefone: '+55 11 99999-9999',

    // usuario_email (opcional)
    // Email do usuário. É opcional: o controle de exibição é por
    // (produto + inquilino_codigo + usuario_codigo).
    usuario_email: 'maria@acme.com',
  });
</script>

Endpoints

POST /api/e-li.nps/pedido

curl -sS -X POST http://localhost:8080/api/e-li.nps/pedido \
  -H 'Content-Type: application/json' \
  -d '{
    "produto_nome":"e-licencie.gov",
    "inquilino_codigo":"acme",
    "inquilino_nome":"ACME",
    "usuario_codigo":"u-123",
    "usuario_nome":"Maria",
    "usuario_telefone":"+55...",
    "usuario_email":"maria@acme.com"
  }'

GET /e-li.nps/{produto}/{id}/form

Abre o formulário (HTML) para responder/editar.

PATCH /api/e-li.nps/{produto}/{id}

curl -sS -X PATCH http://localhost:8080/api/e-li.nps/elicencie_gov/<id> \
  -H 'Content-Type: application/json' \
  -d '{"nota":10}'

Finalizar:

curl -sS -X PATCH http://localhost:8080/api/e-li.nps/elicencie_gov/<id> \
  -H 'Content-Type: application/json' \
  -d '{"justificativa":"muito bom", "finalizar":true}'

Observações importantes

  • Fail-closed: se a API falhar, o widget não abre o modal.
  • CORS: liberado com Access-Control-Allow-Origin: *.
  • IP real do usuário: o sistema grava ip_real no banco (IPv4/IPv6).
    • Para funcionar corretamente atrás de proxy/Docker, garanta que o proxy repasse X-Forwarded-For / X-Real-IP.
    • O servidor usa middleware.RealIP (chi) para resolver o IP antes de gravar.
  • Tabelas por produto: nps_{produto} é criada automaticamente ao ver um produto_nome novo.
  • O backend normaliza produto_nome apenas para uso técnico (nome da tabela e rota):
    • minúsculo + trim
    • remove diacríticos
    • converte caracteres fora de [a-z0-9_] para _
    • valida por regex: ^[a-z_][a-z0-9_]*$
  • O nome exibido ao usuário é o original informado e fica salvo em produto_nome na tabela do produto.
  • O controle de exibição (regras 45 dias / 10 dias) é baseado em: produto + inquilino_codigo + usuario_codigo.

Observabilidade (logs)

O servidor registra uma linha por requisição com:

  • metodo, path, status
  • dur_ms (tempo de execução)
  • request_id (quando disponível)
  • ip_real (após middleware.RealIP)

Regra importante (segurança): o projeto não deve logar segredos (senha, tokens, cookies, Authorization, DSN).


Créditos e suporte

Desenvolvido por Azteca Software (e-licencie) para pesquisa de NPS.

Suporte: ti@e-licencie.com.br ou WhatsApp (48) 9 9948 2983.