# 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
```

## Scripts de operação (menu/)

Para facilitar operação com Docker em servidor (onde é comum precisar de `sudo`),
existem dois scripts simples na pasta `menu/`:

### Deploy

Sobe/atualiza a aplicação e abre os logs em seguida:

```bash
sh menu/deploy.sh
```

### Logs

Exibe os logs do `docker compose` (tail 500) e segue (modo "-f"):

```bash
sh menu/log.sh
```

### Parar

Para a aplicação (sem remover containers):

```bash
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):
>
> ```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**.
