primeira versão do e-li-nps construido com IA
This commit is contained in:
commit
06950d6e2c
34 changed files with 2524 additions and 0 deletions
298
README.md
Normal file
298
README.md
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
# 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`:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
> 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.
|
||||
|
||||
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**.
|
||||
|
||||
## Recomendações (para prompts / manutenção)
|
||||
|
||||
Alguns cuidados:
|
||||
|
||||
- Nomes de variáveis ou arquivos preferencialmente em português
|
||||
- Sempre adicionar comentários em português que ajudem humanos e IAs na manutenção
|
||||
- Se a mudança for importante, atualizar `README.md`
|
||||
|
||||
---
|
||||
|
||||
## 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**.
|
||||
Loading…
Add table
Add a link
Reference in a new issue