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
- Ex:
ADDR(opcional, default:8080)SENHA_PAINEL(opcional)- Se definida, habilita o painel em
/painel. - Se vazia, o painel fica desabilitado.
- Se definida, habilita o painel em
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
- se o ETag não mudou: o servidor responde
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
- Suba um Postgres (exemplo via Docker):
docker run --rm -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=gonps -p 5432:5432 postgres:16
- 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
godotenvao 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.onedeve 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:
- Faça login no painel em
/painel. - Acesse
/painel/debug/ip.
O JSON retornado inclui:
remote_addr(já após omiddleware.RealIP)x_forwarded_forx_real_ip
Interpretação esperada:
- Rodando direto (sem proxy):
remote_addrdeve ser o IP do cliente (ou do seu balanceador). - Rodando via Docker: se você acessar diretamente a porta publicada, o
remote_addrtende a ser o IP do host/bridge; atrás de proxy (Caddy), oremote_addrdeve refletir o IP real. - Rodando via Caddy:
x_forwarded_fordeve conter o IP real do cliente e oremote_addrdeve refletir esse IP apósRealIP.
Depois acesse:
- Home/README:
http://localhost:8080/ - Teste do widget:
http://localhost:8080/teste.html - Painel:
http://localhost:8080/painel(senha emSENHA_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_realno 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.
- Para funcionar corretamente atrás de proxy/Docker, garanta que o proxy repasse
- Tabelas por produto:
nps_{produto}é criada automaticamente ao ver umproduto_nomenovo. - O backend normaliza
produto_nomeapenas 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_nomena 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,statusdur_ms(tempo de execução)request_id(quando disponível)ip_real(apósmiddleware.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.