395 lines
11 KiB
Markdown
395 lines
11 KiB
Markdown
# 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.
|
|
|
|
### WebAssembly (WASM) — regras do widget em Go
|
|
|
|
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:
|
|
|
|
- **Fail-closed**: se o WASM não carregar, o widget não abre.
|
|
- Cache é controlado por **ETag** e o browser sempre **revalida**.
|
|
- O backend Go continua sendo a **autoridade** das regras e persistência.
|
|
|
|
#### Build local do WASM
|
|
|
|
Para (re)gerar os arquivos do WASM localmente:
|
|
|
|
```bash
|
|
# 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`.
|
|
|
|
### 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`:
|
|
|
|
```env
|
|
DATABASE_URL='postgres://postgres:postgres@localhost:5432/gonps?sslmode=disable'
|
|
ADDR=':8080'
|
|
```
|
|
|
|
## Como rodar
|
|
|
|
1. Suba um Postgres (exemplo via Docker):
|
|
|
|
```bash
|
|
docker run --rm -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=gonps -p 5432:5432 postgres:16
|
|
```
|
|
|
|
2. Rode o server:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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):
|
|
>
|
|
> ```bash
|
|
> docker compose up -d --build
|
|
> ```
|
|
|
|
Para forçar rebuild da imagem (mesmo sem mudanças detectadas):
|
|
|
|
```bash
|
|
docker compose build --no-cache && docker compose up
|
|
```
|
|
|
|
Para parar a aplicação:
|
|
|
|
```bash
|
|
docker compose down
|
|
```
|
|
|
|
Para apenas parar sem remover (mantém o container):
|
|
|
|
```bash
|
|
docker compose stop
|
|
```
|
|
|
|
Para iniciar novamente após stop:
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```ts
|
|
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;
|
|
};
|
|
```
|
|
|
|
```html
|
|
<!-- 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`
|
|
|
|
```bash
|
|
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}`
|
|
|
|
```bash
|
|
curl -sS -X PATCH http://localhost:8080/api/e-li.nps/elicencie_gov/<id> \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"nota":10}'
|
|
```
|
|
|
|
Finalizar:
|
|
|
|
```bash
|
|
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**.
|