Widget NPS embutível via 1 arquivo JS + API em Go.
DATABASE_URL (obrigatória)
postgres://postgres:postgres@localhost:5432/gonps?sslmode=disableADDR (opcional, default :8080)SENHA_PAINEL (opcional)
/painel.O painel agora permite:
Regras:
Persistência:
painel_resposta_statuspainel_resposta_comentarioNota: as respostas continuam nas tabelas por produto (
nps_{produto}).
O servidor controla o cache de /static/e-li.nps.js via ETag.
Cache-Control: no-cache, must-revalidate), então:
304 (rápido)Isso evita problemas de clientes com JS antigo em cache após mudanças.
O widget e-li.nps.js carrega um módulo WASM compilado em Go, para concentrar
as regras de negócio do cliente (pré-validações, cooldown e decisão de abertura
com base na resposta do backend).
Arquivos servidos:
/static/e-li.nps.js (arquivo único do widget)/static/e-li.nps.wasm (módulo WASM)/static/wasm_exec.js (runtime do Go para WASM)Regras importantes:
Para (re)gerar os arquivos do WASM localmente:
# gera o módulo WASM
GOOS=js GOARCH=wasm go build -o web/static/e-li.nps.wasm ./cmd/widgetwasm
# copia o runtime JS do Go para WASM
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" web/static/wasm_exec.js
Observação: no build via Docker, esses passos já são executados no
Dockerfile.
.envO 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'
docker run --rm -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=gonps -p 5432:5432 postgres:16
go run ./cmd/server
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
Para facilitar operação com Docker em servidor (onde é comum precisar de sudo),
existem dois scripts simples na pasta menu/:
Sobe/atualiza a aplicação e abre os logs em seguida:
sh menu/deploy.sh
Exibe os logs do docker compose (tail 500) e segue (modo "-f"):
sh menu/log.sh
Para a aplicação (sem remover containers):
sh menu/parar.sh
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
godotenvao iniciar.Exemplo (compose):
./.env:/app/.env:ro
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.
Este repositório inclui um exemplo de Caddyfile para publicar o serviço em:
https://nps.idz.one → {ip-app}:8080nps.idz.one deve apontar para o IP público do servidor onde o Caddy roda.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.
O painel tem um endpoint de debug que mostra o IP que a aplicação está enxergando e os headers recebidos:
GET /painel/debug/ipPasso a passo:
/painel./painel/debug/ip.O JSON retornado inclui:
remote_addr (já após o middleware.RealIP)x_forwarded_forx_real_ipInterpretação esperada:
remote_addr deve ser o IP do cliente (ou do seu balanceador).remote_addr tende a ser o IP do host/bridge; atrás de proxy (Caddy), o remote_addr deve refletir o IP real.x_forwarded_for deve conter o IP real do cliente e o remote_addr deve refletir esse IP após RealIP.Depois acesse:
http://localhost:8080/http://localhost:8080/teste.htmlhttp://localhost:8080/painel (senha em SENHA_PAINEL)Exportação CSV (painel):
GET /painel/export.csvproduto=<produto> (obrigatório)baixas=1 (opcional; exporta apenas notas baixas <=6)http://localhost:8080/painel/export.csv?produto=exemplo&baixas=1Painel:
http://localhost:8080/painel/painel/loginHealthcheck:
curl -i http://localhost:8080/healthz
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>
POST /api/e-li.nps/pedidocurl -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}/formAbre o formulário (HTML) para responder/editar.
PATCH /api/e-li.nps/{produto}/{id}Regra de negócio: a nota (NPS) vai de 0 até 10.
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}'
Access-Control-Allow-Origin: *.ip_real no banco (IPv4/IPv6).
X-Forwarded-For / X-Real-IP.middleware.RealIP (chi) para resolver o IP antes de gravar.nps_{produto} é criada automaticamente ao ver um produto_nome novo.produto_nome apenas para uso técnico (nome da tabela e rota):
[a-z0-9_] para _^[a-z_][a-z0-9_]*$produto_nome na tabela do produto.O servidor registra uma linha por requisição com:
metodo, path, statusdur_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).
Desenvolvido por Azteca Software (e-licencie) para pesquisa de NPS.
Suporte: ti@e-licencie.com.br ou WhatsApp (48) 9 9948 2983.
Página gerada automaticamente a partir de README.md