docs: revisa a documentação com base no que a aplicação faz hoje

- README: o que a aplicação faz, migrations, BP_WEB_PORT e testes com
  go.sum/package-lock.json (sem go mod tidy)
- ARQUITETURA: esquema real (00001 a 00003), rotas que faltavam, Compose
  e imagens atuais, cadeia de proxies, retenção, situação dos testes,
  decisões 30 a 33 e estado da implementação
- DEPLOY: cópia por tar, BP_WEB_PORT em todas as etapas e atualização
  para a versão com exclusão de contas
- MANUAL: papéis do owner e navegação por ano nos feriados
- openapi.yaml: DELETE /accounts/{id} e regras da conta restrita
- Dockerfiles: comentários sobre go.sum e package-lock.json

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 22:20:25 -03:00
parent b35e1a7d69
commit 61fde19133
7 changed files with 266 additions and 195 deletions

View File

@@ -2,24 +2,33 @@
Lembretes de retorno do almoço e de fim de jornada pelo Telegram, a partir dos pontos registrados na PontoMais.
- **Backend:** Go (API REST + worker com bot e jobs)
- **Painel:** Next.js (só administradores)
- **Backend:** Go 1.25 (API REST + worker com bot e jobs, num binário só)
- **Painel:** Next.js 15 (administradores e contas de leitura)
- **Banco:** PostgreSQL 17
- **Execução:** Docker Compose numa máquina Debian, atrás do Nginx Proxy Manager da rede
Documentação:
- [docs/MANUAL_USUARIO.md](docs/MANUAL_USUARIO.md): manual do usuário do Telegram e do administrador do painel;
- [docs/DEPLOY.md](docs/DEPLOY.md): passo a passo de implantação no servidor;
- [docs/ARQUITETURA.md](docs/ARQUITETURA.md): arquitetura, regras de negócio e decisões.
- [docs/DEPLOY.md](docs/DEPLOY.md): passo a passo de implantação e atualização no servidor;
- [docs/ARQUITETURA.md](docs/ARQUITETURA.md): arquitetura, regras de negócio e decisões;
- [secrets/README.md](secrets/README.md): segredos e troca da chave de criptografia;
- [backend/api/openapi.yaml](backend/api/openapi.yaml): contrato da API REST.
> **Estado:** as Sprints 0 a 6 estão implementadas, mas o código **ainda não foi compilado nem testado**. A primeira execução numa máquina com Docker deve revelar ajustes (ver "Primeira validação").
> **Estado:** Sprints 0 a 6 e as evoluções da tabela [Sprints](#sprints) implementadas. O backend compila e passa em `go vet` e `go test`; o painel passa na checagem de tipos (`tsc`). Ainda faltam testes de integração com Postgres real e a medição do atraso da PontoMais no piloto (ARQUITETURA.md, seção 16).
## O que a aplicação faz
- **Telegram:** cadastro (`/cadastrar`), consulta ao vivo do retorno e da saída previstos (`/entrada2`, `/saida2`), situação (`/status`), ligar e pausar lembretes (`/ativarponto`, `/desativarponto`) e comandos de admin (`/usuarios`, `/ativarusuario`, `/desativarusuario`).
- **Lembretes automáticos:** o worker consulta a PontoMais na janela do dia (padrão 11:30–19:00, com frequência adaptativa por usuário) e avisa alguns minutos antes da volta do almoço e da saída. Fins de semana e feriados (feriadosapi.com + cadastro manual) ficam de fora.
- **Painel:** início com alertas, usuários (credencial, jornada, janela personalizada, consulta ao vivo, admin do bot), lembretes (cancelar/reenviar), auditoria, feriados, configurações da janela global com pré-visualização e contas do painel (criar, papel, vínculo com usuário, redefinir senha, desativar, excluir).
- **Operação:** avisos aos admins do bot pelo Telegram, retenção automática de dados, backup diário, CLI administrativa.
## Estrutura
```text
bot-pontos-v2/
├── backend/ # Go: um binário com api, worker, migrate e admin
├── backend/ # Go: um binário com api, worker, migrate, admin e healthcheck
│ ├── cmd/botpontos/ # ponto de entrada e comandos administrativos
│ ├── internal/
│ │ ├── domain/ # entidades e erros (sem dependências)
@@ -31,21 +40,30 @@ bot-pontos-v2/
│ │ ├── bootstrap/ # montagem dos adapters reais
│ │ ├── config/ # variáveis de ambiente e segredos
│ │ ├── httpx/ # utilitários HTTP
│ │ ├── metrics/ # contadores internos (sem endpoint /metrics)
│ │ └── adapters/ # postgres, pontomais, telegram, feriados, crypto
│ ├── db/migrations/ # SQL (goose), embutido no binário
│ └── api/openapi.yaml # contrato da API
├── web/ # Next.js (App Router, CSS próprio)
├── postgres/init/ # cria o usuário da aplicação na 1ª subida do banco
├── scripts/backup-loop.sh # pg_dump diário com retenção de 14 dias
├── deploy/ # firewall da porta 3000 (só o NPM acessa)
├── deploy/ # firewall da porta do painel (só o NPM acessa)
├── secrets/ # Docker secrets (não versionar o conteúdo)
├── docker-compose.yml
└── .env.example
```
Migrations existentes:
| Migration | Conteúdo |
| --- | --- |
| `00001_initial_schema` | Esquema inicial e permissões do usuário `botpontos_app` |
| `00002_holiday_source_feriadosapi` | Libera a origem `feriadosapi` em `holidays.source` |
| `00003_web_account_delete` | Permite excluir contas do painel mantendo a auditoria (`audit_logs.actor_account_name`) |
## Implantação
> O passo a passo completo para o servidor Debian (Docker, firewall, NPM, certificado, primeiro admin, backup e problemas comuns) está em **[docs/DEPLOY.md](docs/DEPLOY.md)**. O resumo abaixo serve de referência rápida.
> O passo a passo completo para o servidor Debian (Docker, firewall, NPM, certificado, primeiro admin, backup, atualização e problemas comuns) está em **[docs/DEPLOY.md](docs/DEPLOY.md)**. O resumo abaixo serve de referência rápida.
Pré-requisito: Debian 12 com Docker Engine e plugin Compose (repositório oficial da Docker), NTP ativo e IP fixo. Go e Node **não** precisam estar instalados; o build acontece dentro dos containers.
@@ -58,8 +76,10 @@ cp .env.example .env
Ajuste no `.env`:
- `BP_HOST_LAN_IP`: IP fixo do servidor (o NPM aponta para ele);
- `BP_WEB_PORT`: porta do painel no servidor (padrão `3000`; troque se ela já estiver em uso);
- `BP_PUBLIC_URL`: endereço do painel, igual ao nome do certificado no NPM (ex.: `https://botpontos.lan`);
- `BP_NPM_IP`: IP do Nginx Proxy Manager.
- `BP_NPM_IP`: IP do Nginx Proxy Manager;
- `BP_HOLIDAY_LOCATION`: código IBGE da cidade dos feriados (padrão `3106200`, Belo Horizonte).
### 2. Segredos
@@ -72,19 +92,19 @@ docker compose up -d --build
```
```bash
docker compose ps
docker compose ps -a
```
`postgres`, `api`, `worker` e `web` devem ficar `healthy`; `migrate` deve terminar com `exited (0)`.
`postgres`, `api`, `worker` e `web` devem ficar `healthy`; `migrate` deve terminar com `exited (0)`; `backup` fica `running`.
### 4. Firewall e proxy
- Instale `deploy/botpontos-firewall.service` (instruções no próprio arquivo) para que só o NPM alcance a porta 3000.
- No NPM, crie o Proxy Host apontando para `http://IP_DO_SERVIDOR:3000` com o certificado interno, Force SSL, HTTP/2 e HSTS (ARQUITETURA.md, 11.4).
- Instale `deploy/botpontos-firewall.service` (instruções no próprio arquivo) para que só o NPM alcance a porta do painel (`BP_WEB_PORT`).
- No NPM, crie o Proxy Host apontando para `http://IP_DO_SERVIDOR:3000` (ou a porta de `BP_WEB_PORT`) com o certificado interno, Force SSL, HTTP/2 e HSTS (ARQUITETURA.md, 11.4).
## Primeiro uso
1. **Criar a conta owner do painel** (a senha é pedida no terminal, sem eco):
1. **Criar a conta owner do painel** (a senha, de no mínimo 12 caracteres, é pedida no terminal, sem eco):
```bash
docker compose run --rm api admin create-owner --username admin
@@ -106,6 +126,8 @@ docker compose ps
4. **Demais usuários:** cada um envia `/cadastrar`; o admin recebe o aviso pelo Telegram, configura a credencial no painel e ativa. O usuário envia `/ativarponto` para ligar os lembretes.
5. **Opcional:** em **Contas**, o owner pode criar uma conta de leitura ligada a um usuário do Telegram, para a pessoa acompanhar só os próprios dados no painel.
## Comandos úteis
Logs de um serviço:
@@ -132,26 +154,25 @@ Recifrar as credenciais após trocar a chave (ver secrets/README.md):
docker compose run --rm api admin rotate-key
```
## Primeira validação
## Desenvolvimento e testes
O código foi escrito sem compilação nem testes. Na primeira máquina onde isso for permitido, nesta ordem:
Os testes não exigem Go nem Node instalados: rodam em containers temporários e usam as versões fixadas em `backend/go.sum` e `web/package-lock.json`.
1. Gerar o `go.sum` e rodar vet e testes do backend:
Backend (`go vet` e testes):
```bash
docker run --rm -v "$PWD/backend:/src" -w /src golang:1.25-alpine sh -c "go mod tidy && go vet ./... && go test ./..."
```
```bash
docker run --rm -v "$PWD/backend:/src" -w /src golang:1.25-alpine sh -c "go vet ./... && go test ./..."
```
2. Gerar o `package-lock.json` e checar os tipos do painel:
Painel (checagem de tipos):
```bash
docker run --rm -v "$PWD/web:/app" -w /app node:22-alpine sh -c "npm install && npx tsc --noEmit"
```
```bash
docker run --rm -v "$PWD/web:/app" -w /app node:22-alpine sh -c "npm ci && npx tsc --noEmit"
```
3. Subir com `docker compose up -d --build` e seguir "Primeiro uso".
4. Piloto com 1 a 3 usuários, medindo o atraso do `last_cached` da PontoMais (log e auditoria mostram quando cada ponto foi detectado).
> Não rode `go mod tidy` com Go 1.25: ele tenta baixar versões que exigem Go 1.26 e quebra o build da imagem. Mantenha o `go.sum` e o `package-lock.json` versionados.
Mantenha o `go.sum` e o `package-lock.json` gerados no projeto, para builds reproduzíveis.
Para rodar localmente pelo Compose, use no `.env` `BP_HOST_LAN_IP=127.0.0.1`, `BP_COOKIE_SECURE=false` e `BP_PUBLIC_URL=http://127.0.0.1:3000`.
## Sprints
@@ -164,3 +185,4 @@ Mantenha o `go.sum` e o `package-lock.json` gerados no projeto, para builds repr
| 4 — API | Login com Argon2id e bloqueio, sessões, CSRF, papéis, todos os endpoints do painel |
| 5 — Painel | Dashboard, usuários (jornada, janela personalizada, credencial, consulta ao vivo), lembretes (cancelar/reenviar), auditoria, feriados, configurações com pré-visualização, contas |
| 6 — Operação | Retenção de dados, manutenção, CLI administrativa, backup, firewall, documentação |
| Evoluções | Feriados pela feriadosapi.com (endpoint por cidade, código IBGE em `BP_HOLIDAY_LOCATION`, migration `00002`); conta de leitura ligada a um usuário, restrita aos próprios dados (ARQUITETURA 9.6, MANUAL e DEPLOY 10.4); exclusão de contas do painel (migration `00003`); porta do painel configurável (`BP_WEB_PORT`); dependências fixadas em `go.sum` e `package-lock.json` |

View File

@@ -9,7 +9,7 @@ RUN if [ -f go.sum ]; then go mod download; fi
COPY . .
ARG VERSION=dev
# Enquanto o go.sum não existir no repositório, ele é gerado aqui (ver README).
# Fallback: sem go.sum, ele é gerado aqui (o go.sum já é versionado; ver README).
RUN if [ ! -f go.sum ]; then go mod tidy; fi \
&& CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" \
-o /out/botpontos ./cmd/botpontos

View File

@@ -10,6 +10,9 @@ info:
`bp_csrf` e um `Origin` igual a BP_PUBLIC_URL.
`x-min-role` indica o papel mínimo: viewer < admin < owner.
Conta restrita (viewer com user_id): só vê e altera o próprio usuário em
/users/{id}*, recebe só os próprios dados em /users, /notifications e
/dashboard, e não acessa /audit, /settings nem /accounts (ARQUITETURA.md, 9.6).
Erros seguem application/problem+json (RFC 9457).
servers:
- url: /api/v1
@@ -558,6 +561,14 @@ paths:
responses:
"200": { description: OK }
"409": { $ref: "#/components/responses/Problem" }
delete:
summary: Exclui a conta e as sessões dela; a auditoria mantém o nome (actor_account_name)
description: Recusa excluir a própria conta (INVALID_STATE) e o último owner ativo (LAST_OWNER).
x-min-role: owner
responses:
"204": { description: Excluída }
"404": { $ref: "#/components/responses/Problem" }
"409": { $ref: "#/components/responses/Problem" }
/accounts/{id}/reset-password:
parameters: [{ $ref: "#/components/parameters/id" }]
post:

View File

@@ -1,8 +1,8 @@
# Bot de Pontos v2 — Documento de Arquitetura
> Status: **Sprints 0 a 6 implementadas, ainda não compiladas nem testadas** (ver `README.md` na raiz do projeto e a seção 16)
> Status: **Sprints 0 a 6 e evoluções implementadas; backend compila e passa nos testes, painel passa na checagem de tipos** (ver `README.md` na raiz do projeto e a seção 16)
> Base: projeto `Modelo` (versão final em `Modelo/Fase2/Sprint4`)
> Stack alvo: **Go (backend) · Next.js (web) · PostgreSQL · Docker Compose**
> Stack: **Go 1.25 (backend) · Next.js 15 (web) · PostgreSQL 17 · Docker Compose**
---
@@ -30,7 +30,7 @@ O Modelo **nunca rodou em produção**, então não há dados a migrar: a v2 com
| Ciclos | Reconciliação 15 min e despacho 5 min, **no mesmo loop** (um bloqueia o outro) | Reconciliação 15 min e despacho **2 min**, em **goroutines independentes** |
| Consultas à PontoMais | O dia inteiro, todo dia útil, todos os usuários a cada 15 min | Só na **janela 11:30–19:00**, com **frequência adaptativa por usuário** (15 min, 60 min ou nenhuma); ~11 consultas/usuário num dia típico (ver 9.12) |
| Datas | `TEXT` | `timestamptz` / `date` |
| Executáveis | `bot`, `admin`, `web` | uma imagem Go com subcomandos `api`, `worker`, `admin`, `migrate` |
| Executáveis | `bot`, `admin`, `web` | uma imagem Go com subcomandos `api`, `worker`, `admin`, `migrate` (e `healthcheck`, `version`) |
| Contas web | Uma conta | N contas com papéis (`owner`, `admin`, `viewer`) |
| Fuso por usuário | Campo existe, mas é ignorado | Aplicado em todos os cálculos |
| Regras de jornada | Por usuário, mas com o texto "8h00" fixo | Por usuário, com mensagens formatadas a partir da configuração |
@@ -64,6 +64,8 @@ flowchart LR
WRK --> PM
WRK --> HOL
API --> PM
API --> TGAPI
API --> HOL
BKP --> DB
```
@@ -71,7 +73,8 @@ flowchart LR
- Só o `web-next` recebe conexões de fora, e **sempre pelo Nginx Proxy Manager (NPM) que já existe na rede**, que cuida do HTTPS. O projeto **não** tem proxy próprio (ver 11.4).
- `api` e `postgres` ficam apenas na rede interna do Compose, sem `ports:`.
- O `worker` não recebe conexões; o bot usa *long polling* no Telegram.
- O `worker` não recebe conexões de fora; o bot usa *long polling* no Telegram. Ele só expõe `/health/live` e `/health/ready` na porta 8081, dentro do container, para o healthcheck.
- A `api` também sai para a internet: valida credenciais na PontoMais, faz o reenvio manual de lembretes pelo Telegram e a sincronização de feriados acionada pelo painel.
## 4. Backend Go
@@ -98,7 +101,7 @@ flowchart LR
```text
backend/
├── cmd/
│ └── botpontos/ # binário único: api | worker | admin | migrate
│ └── botpontos/ # binário único: api | worker | admin | migrate | healthcheck | version
├── internal/
│ ├── domain/ # entidades, value objects, erros de domínio
│ ├── rules/ # cálculos puros: workday, entrada2, saida2, dia útil
@@ -108,7 +111,7 @@ backend/
│ │ ├── postgres/ # repositório (SQL escrito à mão com pgx) e migrations
│ │ ├── pontomais/ # cliente HTTP + cache de token em memória
│ │ ├── telegram/ # cliente próprio da Bot API (portado do Modelo)
│ │ ├── feriados/ # cliente feriadosapi.com + cache anual
│ │ ├── feriados/ # cliente feriadosapi.com (endpoint por cidade)
│ │ └── crypto/ # AES-GCM, Argon2id, tokens
│ ├── httpapi/ # API REST: router, handlers JSON, DTOs
│ ├── httpx/ # utilitários HTTP compartilhados (JSON, problem+json, log, recover)
@@ -117,9 +120,10 @@ backend/
│ ├── metrics/ # contadores internos (sem endpoint /metrics por enquanto)
│ └── config/
├── db/
│ └── migrations/ # goose
│ └── migrations/ # goose: 00001 (esquema), 00002 (feriadosapi), 00003 (excluir conta web)
├── api/
│ └── openapi.yaml # contrato da API (fonte dos tipos do Next)
│ └── openapi.yaml # contrato da API (referência para os tipos do Next)
├── go.mod / go.sum # versões fixadas das dependências
└── Dockerfile
```
@@ -131,8 +135,9 @@ O binário é um só e o subcomando define o papel:
| --- | --- | --- |
| `botpontos api` | `api` | API REST para o Next.js. Pode ter várias réplicas. |
| `botpontos worker` | `worker` | Bot do Telegram (*long polling*), reconciliação e despacho. **Só uma réplica ativa.** |
| `botpontos migrate up` | `migrate` (*one-shot*) | Aplica as migrations antes de `api` e `worker` subirem. |
| `botpontos admin ...` | `docker compose run` | Bootstrap: primeira conta `owner`, rotação de chave etc. |
| `botpontos migrate [up\|status\|down]` | `migrate` (*one-shot*) | Aplica (`up`, padrão) ou consulta as migrations antes de `api` e `worker` subirem. |
| `botpontos admin ...` | `docker compose run` | `create-owner`, `reset-password`, `bot-admin`, `set-credentials`, `rotate-key` (senhas lidas do terminal, sem eco) |
| `botpontos healthcheck <url>` | healthcheck do Compose | Faz GET num endpoint de saúde (a imagem distroless não tem `curl`) |
Mesmo que alguém suba duas réplicas do `worker` por engano, o processo pega um **advisory lock do Postgres** (`pg_try_advisory_lock`) antes de iniciar o bot e os jobs. Quem não conseguir o lock fica em espera.
@@ -147,46 +152,49 @@ Mesmo que alguém suba duas réplicas do `worker` por engano, o processo pega um
| Logs | `log/slog` em JSON |
| Validação | validação manual nos DTOs (sem framework) |
| Telegram | cliente próprio, portado do Modelo (ver decisão 27) |
| Testes de integração | `testcontainers-go` com Postgres real |
| Criptografia e terminal | `golang.org/x/crypto` (Argon2id), `golang.org/x/term` (senha sem eco na CLI) |
| Testes de integração | previstos com `testcontainers-go` e Postgres real; **ainda não existem** (ver 13) |
## 5. Frontend Next.js
### 5.1 Papel
O Next.js é **só a interface administrativa** e atua como **BFF** (*backend for frontend*). O navegador fala apenas com o Next; o Next fala com a API Go pela rede interna.
O Next.js é a **interface do painel** (administradores e contas de leitura, inclusive a conta restrita a um usuário, ver 9.6) e atua como **BFF** (*backend for frontend*). O navegador fala apenas com o Next; o Next fala com a API Go pela rede interna.
### 5.2 Estrutura
```text
web/
├── app/
│ ├── (auth)/login/page.tsx
│ ├── login/page.tsx
│ ├── healthz/route.ts # healthcheck do container (não passa pela API)
│ ├── (painel)/
│ │ ├── layout.tsx # verifica a sessão (server component)
│ │ ├── page.tsx # dashboard
│ │ ├── layout.tsx # verifica a sessão (server component) e monta o menu por papel
│ │ ├── page.tsx # Início (dashboard)
│ │ ├── usuarios/
│ │ │ ├── page.tsx
│ │ │ └── [id]/page.tsx
│ │ ├── notificacoes/page.tsx
│ │ │ └── [id]/page.tsx # + forms.tsx: credencial, jornada, janela, ações
│ │ ├── notificacoes/page.tsx # tela "Lembretes"
│ │ ├── auditoria/page.tsx
│ │ ├── feriados/page.tsx
│ │ ├── configuracoes/page.tsx # janela de consulta (9.13)
│ │ └── contas/page.tsx # contas web (somente owner)
├── components/ # botões de ação, campos da janela, logout
│ │ ├── feriados/page.tsx # + form.tsx
│ │ ├── configuracoes/page.tsx # janela de consulta (9.13), + form.tsx
│ │ └── contas/page.tsx # contas web (somente owner), + forms.tsx
├── components/ # ActionButton, WindowFields, LogoutButton
├── lib/
│ ├── types.ts # tipos da API, escritos à mão a partir do openapi.yaml
│ ├── server-api.ts # GET dos Server Components, repassando o cookie
│ ├── client-api.ts # mutações do navegador, com o token CSRF
│ ├── access.ts # isRestricted: esconde menu e botões da conta restrita
│ └── format.ts # datas, horários e rótulos em pt-BR
├── middleware.ts # redireciona para /login sem cookie de sessão
├── next.config.ts # output: 'standalone' + rewrites /api → api:8080
├── next.config.ts # output: 'standalone' + rewrites /api/v1 → api:8080
└── Dockerfile
```
### 5.3 Comunicação
- **Server Components** buscam dados chamando `http://api:8080/api/v1/...` e repassam o cookie de sessão recebido.
- **Mutações** usam *Server Actions* ou `fetch('/api/v1/...')` no cliente. O `rewrite` do Next encaminha essas chamadas para o Go **na mesma origem**, então não há CORS e o cookie funciona direto.
- **Mutações** usam `fetch('/api/v1/...')` no cliente (`lib/client-api.ts`), sem *Server Actions*. O `rewrite` do Next encaminha essas chamadas para o Go **na mesma origem**, então não há CORS e o cookie funciona direto.
- Os **tipos** ficam em `web/lib/types.ts`, escritos à mão a partir do `openapi.yaml` e de `httpapi/dto.go`. A geração automática (`openapi-typescript`) pode ser adotada depois (decisão 27).
- **UI:** CSS próprio (`app/globals.css`), com tema claro/escuro e layout responsivo; filtros por query string nas tabelas (decisão 27).
@@ -210,7 +218,9 @@ A autenticação continua **no Go**, porque a lógica já existe e fica centrali
- `created_at` e `updated_at`, com trigger para atualizar `updated_at`.
- Migrations só para a frente, numeradas e em SQL.
### 6.2 Esquema proposto
### 6.2 Esquema atual
Resumo do resultado das migrations `00001` a `00003` (`backend/db/migrations/`). Triggers de `updated_at`, índices secundários e permissões ficam nos arquivos SQL.
```sql
CREATE TYPE user_status AS ENUM ('pending', 'active', 'disabled');
@@ -227,6 +237,8 @@ CREATE TABLE users (
status user_status NOT NULL DEFAULT 'pending',
is_bot_admin boolean NOT NULL DEFAULT false,
notifications_enabled boolean NOT NULL DEFAULT false,
-- motivo da pausa, exibido no /status: 'manual' | 'api_failures' | 'admin' | 'user_disabled'
notifications_paused_reason text,
timezone text NOT NULL DEFAULT 'America/Sao_Paulo',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
@@ -267,7 +279,8 @@ CREATE TABLE notifications (
user_id bigint NOT NULL REFERENCES users(id),
reference_date date NOT NULL,
type notification_type NOT NULL,
scheduled_at timestamptz NOT NULL,
scheduled_at timestamptz NOT NULL, -- quando o aviso deve sair
predicted_at timestamptz NOT NULL, -- horário previsto do ponto (vai na mensagem)
status notification_status NOT NULL DEFAULT 'scheduled',
attempts int NOT NULL DEFAULT 0,
locked_at timestamptz,
@@ -279,6 +292,7 @@ CREATE TABLE notifications (
UNIQUE (user_id, reference_date, type)
);
CREATE INDEX notifications_due_idx ON notifications (scheduled_at) WHERE status = 'scheduled';
CREATE INDEX notifications_sending_idx ON notifications (locked_at) WHERE status = 'sending';
CREATE TABLE web_accounts (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
@@ -296,7 +310,7 @@ CREATE TABLE web_accounts (
CREATE TABLE web_sessions (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
account_id bigint NOT NULL REFERENCES web_accounts(id),
account_id bigint NOT NULL REFERENCES web_accounts(id) ON DELETE CASCADE, -- 00003
token_hash bytea NOT NULL UNIQUE,
csrf_token_hash bytea NOT NULL,
expires_at timestamptz NOT NULL,
@@ -310,7 +324,7 @@ CREATE TABLE web_sessions (
CREATE TABLE holidays ( -- cache persistente da API de feriados
day date PRIMARY KEY,
name text NOT NULL,
source text NOT NULL, -- 'feriadosapi' | 'feriados.dev' (legado) | 'manual'
source text NOT NULL CHECK (source IN ('feriadosapi', 'feriados.dev', 'manual')), -- 00002; feriados.dev = legado
created_at timestamptz NOT NULL DEFAULT now()
);
@@ -334,7 +348,7 @@ CREATE INDEX user_days_next_check_idx ON user_days (next_check_at) WHERE next_ch
CREATE TABLE app_settings (
key text PRIMARY KEY, -- ex.: 'query_window_start'
value jsonb NOT NULL, -- ex.: "11:30"
updated_by_account bigint REFERENCES web_accounts(id),
updated_by_account bigint REFERENCES web_accounts(id) ON DELETE SET NULL, -- 00003
updated_at timestamptz NOT NULL DEFAULT now()
);
@@ -350,7 +364,8 @@ CREATE TABLE audit_logs (
occurred_at timestamptz NOT NULL DEFAULT now(),
source audit_source NOT NULL,
actor_user_id bigint REFERENCES users(id),
actor_account bigint REFERENCES web_accounts(id),
actor_account bigint REFERENCES web_accounts(id) ON DELETE SET NULL, -- 00003
actor_account_name text, -- 00003: nome da conta no momento do evento
target_user_id bigint REFERENCES users(id),
event_type text NOT NULL,
success boolean NOT NULL,
@@ -366,6 +381,8 @@ Diferenças importantes em relação ao Modelo:
- A **antecedência** pode ser diferente para cada lembrete (`entrada2_lead_minutes` / `saida2_lead_minutes`).
- A tabela `holidays` guarda feriados confirmados e também **feriados manuais** cadastrados pelo painel. A ordem de consulta está na seção 9.9.
- `audit_logs.details` passa a ser `jsonb`, o que permite filtrar no painel.
- `notifications.predicted_at` guarda o horário previsto do ponto, para a mensagem sempre informar a hora certa (9.3).
- O banco tem dois usuários: `botpontos` (dono, usado pelas migrations e pelo backup) e `botpontos_app` (API e worker, só `SELECT/INSERT/UPDATE/DELETE`), criado por `postgres/init/01-create-app-role.sh`.
### 6.3 Despacho sem duplicidade
@@ -415,19 +432,26 @@ Todas as rotas ficam sob `/api/v1`, usam JSON e exigem sessão, exceto `auth/log
| PUT | `/users/{id}/credentials` | admin | Valida na PontoMais antes de salvar |
| POST | `/users/{id}/credentials/test` | admin | Testa a credencial atual |
| POST | `/users/{id}/notifications/enable` · `/disable` | admin | Liga ou pausa os lembretes |
| PUT | `/users/{id}/bot-admin` | admin | Liga ou desliga o admin do bot no Telegram (`{"enabled": true}`) |
| GET | `/users/{id}/today` | viewer | Pontos do dia, previsões calculadas (consulta ao vivo) e situação do dia (`user_days`) |
| POST | `/users/{id}/today/resume` | admin | Retoma um dia `no_points`/`not_working` (ver 9.12) |
| GET | `/settings` | viewer | Configurações globais (janela de consulta) |
| GET | `/settings` | viewer | Configurações globais (janela de consulta) e parâmetros do servidor (só leitura) |
| PUT | `/settings` | admin | Altera as configurações globais; valida conforme 9.13 |
| POST | `/settings/preview` | admin | Pré-visualiza o efeito de uma janela antes de salvar (9.13) |
| GET | `/notifications` | viewer | Filtros: data, status, usuário, tipo |
| POST | `/notifications/{id}/cancel` | admin | Só quando o status é `scheduled` |
| POST | `/notifications/{id}/resend` | admin | Reenvio manual; só `failed` do dia atual (ver 9.5) |
| GET | `/audit` | viewer | Filtros + paginação por cursor |
| GET/POST/DELETE | `/holidays` | admin | Feriados manuais e sincronização |
| GET/POST/PATCH | `/accounts` | owner | Contas web e papéis |
| GET | `/holidays?year=AAAA` | viewer | Feriados do ano |
| POST | `/holidays` | admin | Cadastra feriado manual |
| POST | `/holidays/sync` | admin | Sincroniza o ano com a feriadosapi.com (9.9) |
| DELETE | `/holidays/{day}` | admin | Remove um feriado manual |
| GET/POST | `/accounts` | owner | Lista e cria contas web |
| PATCH | `/accounts/{id}` | owner | Papel, desativação e usuário ligado (`user_id`) |
| DELETE | `/accounts/{id}` | owner | Exclui a conta (não a própria nem o último owner ativo); auditado em `web_account_deleted` |
| POST | `/accounts/{id}/reset-password` | owner | Resolve a pendência "sem redefinição de senha" |
Erros seguem o formato `application/problem+json` (RFC 9457):
As rotas marcadas como `viewer` têm restrições extras para a conta de leitura ligada a um usuário (tabela em 9.6). Erros seguem o formato `application/problem+json` (RFC 9457):
```json
{ "type": "about:blank", "title": "Credencial ausente", "status": 409, "code": "CREDENTIALS_REQUIRED" }
@@ -454,7 +478,7 @@ flowchart TD
E -->|inválido| J[Audita]
```
Os usuários são processados com concorrência limitada (*worker pool* de 4) e timeout por chamada externa.
Os usuários são processados com concorrência limitada (*worker pool* de 4, `BP_RECONCILIATION_WORKERS`) e timeout por chamada externa.
### 8.2 Ciclos e latência dos lembretes
@@ -462,13 +486,13 @@ Os usuários são processados com concorrência limitada (*worker pool* de 4) e
| --- | --- | --- | --- |
| Reconciliação | tick de 15 min, **só das 11:30 às 19:00** | `BP_RECONCILIATION_INTERVAL` | Consulta na PontoMais só os usuários com `next_check_at` vencido e cria, atualiza ou cancela lembretes (ver 9.12) |
| Despacho | **2 min** | `BP_DISPATCH_INTERVAL` | Envia os lembretes `scheduled` cujo `scheduled_at <= now()` |
| Limpeza | 5 min | — | Marca como `failed` o que está em `sending` há mais de 5 min (`BP_SENDING_TIMEOUT`); 1×/dia aplica a retenção de dados (ver 9.10) |
| Manutenção | 5 min | `BP_MAINTENANCE_INTERVAL` | Marca como `failed` o que está em `sending` há mais de 5 min (`BP_SENDING_TIMEOUT`); 1×/dia, às 03h, aplica a retenção de dados (ver 9.10) |
| Tolerância de atraso | 10 min | `BP_LATE_TOLERANCE` | Limite para ainda enviar um lembrete atrasado (ver 9.3) |
**Regras de implementação:**
- Cada ciclo roda na **sua própria goroutine**, com ticker próprio. No Modelo (`scheduler/scheduler.go`), reconciliação e despacho dividem o mesmo `select`, então uma reconciliação lenta segura todos os envios.
- A reconciliação tem **prazo total** (*deadline*) de 5 min por ciclo e timeout de 10 s por chamada à PontoMais. Usuários que não terminarem dentro do prazo ficam para o próximo ciclo.
- A reconciliação tem **prazo total** (*deadline*) de 5 min por ciclo (`BP_RECONCILIATION_DEADLINE`) e timeout de 10 s por chamada à PontoMais (`BP_PONTOMAIS_TIMEOUT`). Usuários que não terminarem dentro do prazo ficam para o próximo ciclo.
- Se um ciclo ainda estiver rodando quando o próximo tick chegar, o tick é ignorado e não se acumula.
- Os ticks são alinhados ao relógio (ex.: :00, :02, :04...) para o comportamento ser previsível nos logs.
@@ -628,21 +652,24 @@ Um lembrete `failed` pode ser reenviado **manualmente** pelo painel, num botão
### 9.6 Papéis do painel (decidido)
Só administradores acessam o painel. **O usuário comum continua usando só o Telegram.**
O painel é para administradores e contas de leitura. **O usuário comum usa o Telegram**; se precisar ver os próprios dados no painel, o owner cria para ele uma conta de leitura ligada ao seu usuário (abaixo).
| Ação | `viewer` | `admin` | `owner` |
| --- | :-: | :-: | :-: |
| Ver dashboard, usuários, notificações, auditoria e feriados | ✓ | ✓ | ✓ |
| Ver dashboard, usuários, notificações, auditoria, feriados e configurações | ✓ | ✓ | ✓ |
| Ativar/desativar usuário, ligar/pausar lembretes | | ✓ | ✓ |
| Alterar jornada e credenciais, testar API | | ✓ | ✓ |
| Cancelar e reenviar notificação | | ✓ | ✓ |
| Gerenciar feriados manuais | | ✓ | ✓ |
| Alterar configurações globais (janela de consulta) | | ✓ | ✓ |
| Criar/desativar contas web, alterar papéis, redefinir senhas | | | ✓ |
| Definir o admin do bot no Telegram | | ✓ | ✓ |
| Criar/desativar/excluir contas web, alterar papéis e vínculo com usuário, redefinir senhas | | | ✓ |
- **Conta de leitura ligada a um usuário** (`viewer` com `web_accounts.user_id`): fica restrita ao próprio usuário. Vê Início (avisos do sistema e os próprios lembretes de hoje, sem links), Usuários (só a própria ficha), Lembretes (só os próprios, somente leitura) e Feriados (somente leitura). Pode alterar jornada, credencial PontoMais, testar a API e ligar/pausar os próprios lembretes. Não vê Auditoria, Configurações nem Contas e não ativa, desativa nem promove usuários. A regra é aplicada na API (`authUser`, `authUnrestricted`, `isRestricted`), não só no menu. Um usuário só pode estar ligado a uma conta. Leitura **sem** vínculo continua vendo tudo, sem editar.
- A primeira conta `owner` é criada pela CLI (`botpontos admin create-owner`).
- Precisa existir sempre pelo menos um `owner` ativo. A API recusa rebaixar ou desativar o último.
- Precisa existir sempre pelo menos um `owner` ativo. A API recusa rebaixar, desativar ou excluir o último.
- Ninguém pode desativar nem excluir a própria conta (a API responde `409`).
- **Excluir conta** (`DELETE /accounts/{id}`, resposta `204`) apaga a linha de `web_accounts`. A migration `00003` faz as sessões irem junto (`ON DELETE CASCADE`) e deixa `audit_logs.actor_account` e `app_settings.updated_by_account` como `NULL`; o nome da conta fica em `audit_logs.actor_account_name`, gravado em cada evento, então a auditoria continua mostrando quem fez.
- O vínculo entre conta web e usuário do Telegram (`web_accounts.user_id`) é **opcional**; o owner define e troca em Contas. O admin do Telegram (`is_bot_admin`) e as contas do painel são independentes.
#### Conta de leitura restrita (como a API aplica)
@@ -705,18 +732,28 @@ A cada ciclo, o worker decide se o dia é útil nesta ordem:
A sincronização nunca apaga feriados manuais nem sobrescreve uma data que já esteja na tabela.
**API usada (feriadosapi.com):**
- Chamada: `GET {BP_HOLIDAY_API_BASE_URL}/feriados/cidade/{ibge}?ano=AAAA`, com `Authorization: Bearer <chave>`. Padrão da base: `https://feriadosapi.com/api/v1`; chave em `secrets/holiday_api_key`.
- `BP_HOLIDAY_LOCATION` é o **código IBGE** da cidade (Belo Horizonte: `3106200`). Um valor não numérico, como o antigo `MG-belo-horizonte`, faz a sincronização falhar com mensagem explicativa e o fallback de segunda a sexta entra em vigor.
- A resposta traz os feriados **nacionais e municipais** da cidade. O cliente lê `feriados[].data` (formato `dd/mm/aaaa`, convertido para `AAAA-MM-DD`) e `feriados[].nome`; ignora datas de outro ano ou ilegíveis e falha se nenhuma data válida vier.
- Carnaval e outros pontos facultativos que a API não lista devem ser cadastrados como feriado manual.
- Quantas chamadas: uma por ano, na primeira verificação de dia útil sem feriados do ano no banco (`HasSyncedHolidays` considera qualquer origem diferente de `manual`, inclusive a `feriados.dev` legada). Em falha, no máximo uma tentativa por dia por processo. O botão **Sincronizar** do painel chama a API a cada clique.
- Origem gravada em `holidays.source`: `feriadosapi`. Linhas antigas `feriados.dev` continuam válidas (migration `00002`, que também libera o novo valor no `CHECK`).
### 9.10 Retenção de dados
Aplicada pelo job de limpeza, uma vez por dia, de madrugada:
Aplicada pelo job de manutenção, uma vez por dia, no primeiro ciclo depois das 03h (`BP_RETENTION_HOUR`, no fuso padrão). O último dia executado fica em `service_state`, então um restart não repete a limpeza.
| Dado | Mantido por |
| --- | --- |
| `audit_logs` | 1 ano |
| `notifications` | 90 dias (por `reference_date`) |
| `user_days` | 90 dias (mesmo corte das notificações) |
| `web_sessions` expiradas ou revogadas | 7 dias após expirar ou ser revogada |
| `holidays` | Sem limpeza (volume desprezível) |
Os prazos ficam nas variáveis `BP_RETENTION_AUDIT`, `BP_RETENTION_NOTIFICATIONS` e `BP_RETENTION_SESSIONS`. Cada execução registra na auditoria quantas linhas removeu.
Os prazos ficam nas variáveis `BP_RETENTION_AUDIT`, `BP_RETENTION_NOTIFICATIONS` e `BP_RETENTION_SESSIONS`. Cada execução registra na auditoria (`retention_applied`) quantas linhas removeu.
### 9.11 Avisos ao administrador
@@ -838,7 +875,7 @@ Os quatro horários de 9.12 (início, confirmação, fim das reverificações e
**Origem dos valores**
- Na **primeira inicialização**, se a tabela estiver vazia, o worker grava os padrões (`11:30`, `19:00`, `11:45`). As variáveis `BP_QUERY_WINDOW_START`, `BP_QUERY_WINDOW_END` e `BP_DAY_CHECK_DEADLINE` servem só para trocar esses padrões iniciais.
- Na **primeira inicialização**, se a tabela estiver vazia, os padrões são gravados (`11:30`, `11:45`, `15:00`, `19:00`). As variáveis `BP_QUERY_WINDOW_START`, `BP_DAY_CHECK_DEADLINE`, `BP_NO_POINTS_RECHECK_UNTIL` e `BP_QUERY_WINDOW_END` servem só para trocar esses padrões iniciais.
- Depois disso, **vale o que está no banco**. As variáveis de ambiente não sobrescrevem o que foi salvo pelo painel.
**Validação ao salvar** (resposta `422` com o motivo):
@@ -901,88 +938,36 @@ janela efetiva do usuário = janela personalizada (work_schedules), se existir
| Autorização | Papel conferido no Go em toda rota; o Next nunca decide permissão. |
| CSRF | Double submit + checagem de `Origin` + `SameSite=Strict`. |
| Segredos | Senhas PontoMais com AES-256-GCM e `key_version` para rotação. As chaves vêm de *Docker secrets* (`/run/secrets/...`), não de variáveis de ambiente em texto. |
| Banco | Usuário de aplicação sem `SUPERUSER`; usuário separado para migrations. |
| Containers | Imagens `distroless`/`alpine`, usuário não-root, `read_only: true`, `cap_drop: [ALL]`. |
| Banco | Usuário de aplicação (`botpontos_app`) sem `SUPERUSER` e só com DML; usuário dono (`botpontos`) para migrations e backup. |
| Containers | Imagens `distroless`/`alpine`, usuário não-root, `read_only: true`, `cap_drop: [ALL]`, `no-new-privileges`. |
| HTTP | API responde com `X-Content-Type-Options: nosniff`, `Cache-Control: no-store` e `Referrer-Policy: same-origin`. |
| Auditoria | Toda mutação web/Telegram/CLI grava em `audit_logs`, nunca com segredos. |
## 11. Docker
### 11.1 Imagens
- **Go:** build *multi-stage* (`golang:1.23-alpine` → `gcr.io/distroless/static`). Um binário estático com migrations embutidas, imagem de ~15 MB.
- **Next:** `node:22-alpine` com `output: 'standalone'`, imagem de ~150 MB.
- **Go:** build *multi-stage* (`golang:1.25-alpine` → `gcr.io/distroless/static-debian12:nonroot`). Um binário estático com migrations embutidas, imagem de ~15 MB. As dependências vêm do `go.sum` versionado.
- **Next:** `node:22-alpine` com `output: 'standalone'` e `npm ci` a partir do `package-lock.json`, imagem de ~150 MB. O endereço da API (`API_INTERNAL_URL`) entra no build, porque os *rewrites* do Next são resolvidos nessa hora.
### 11.2 `docker-compose.yml` (esqueleto)
### 11.2 `docker-compose.yml`
```yaml
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_DB: botpontos
POSTGRES_USER: botpontos
POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
volumes: [pgdata:/var/lib/postgresql/data]
secrets: [pg_password]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U botpontos"]
interval: 10s
networks: [internal]
O arquivo real fica na raiz do projeto. Resumo:
migrate:
image: botpontos-backend
command: ["migrate", "up"]
depends_on: { postgres: { condition: service_healthy } }
secrets: [pg_password]
networks: [internal]
| Serviço | Imagem | Redes | Segredos | Observações |
| --- | --- | --- | --- | --- |
| `postgres` | `postgres:17-alpine` | `internal` | `pg_password`, `pg_app_password` | Volume `pgdata`; `postgres/init/` cria o usuário `botpontos_app` na 1ª subida |
| `migrate` | `botpontos-backend` | `internal` | `pg_password` | `migrate up` com o usuário dono; termina com `exited (0)` |
| `api` | `botpontos-backend` | `internal`, `egress` | `pg_app_password`, `encryption_key`, `telegram_token`, `holiday_api_key` | Pool de 10 conexões; healthcheck em `:8080/api/v1/health/live` |
| `worker` | `botpontos-backend` | `internal`, `egress` | os mesmos da `api` | Pool de 5 conexões; healthcheck em `:8081/health/live` |
| `web` | `botpontos-web` | `internal`, `ingress` | — | Publica `${BP_HOST_LAN_IP}:${BP_WEB_PORT:-3000}` → `3000`; healthcheck em `/healthz` |
| `backup` | `postgres:17-alpine` | `internal` | `pg_password` | `scripts/backup-loop.sh`: `pg_dump -Fc` diário em `./backups`, 14 dias |
api:
image: botpontos-backend
command: ["api"]
depends_on: { migrate: { condition: service_completed_successfully } }
secrets: [pg_password, encryption_key]
networks: [internal, egress] # egress: testar credenciais na PontoMais
- Todos os serviços usam `restart: unless-stopped` (o `migrate` usa `"no"`). Os do backend e o `web` rodam com `read_only: true`, `cap_drop: [ALL]` e `no-new-privileges`.
- A rede `internal` é `internal: true` (sem internet); `egress` dá saída para PontoMais, Telegram e feriadosapi.com; `ingress` é a rede da porta publicada.
- Variáveis vindas do `.env`: `BP_HOST_LAN_IP` (obrigatória), `BP_WEB_PORT`, `BP_LOG_LEVEL`, `BP_PUBLIC_URL`, `BP_NPM_IP`, `BP_COOKIE_SECURE`, `BP_DEFAULT_TIMEZONE`, `BP_HOLIDAY_LOCATION` e `BP_ENCRYPTION_KEY_VERSION`. As demais variáveis de `internal/config/config.go` têm padrão e só precisam ser definidas para fugir dele.
worker:
image: botpontos-backend
command: ["worker"]
depends_on: { migrate: { condition: service_completed_successfully } }
secrets: [pg_password, encryption_key, telegram_token, holiday_api_key]
networks: [internal, egress]
web:
image: botpontos-web
environment:
API_INTERNAL_URL: http://api:8080
depends_on: [api]
# O NPM está em outra máquina da rede (ver 11.4).
# Publica só no IP de rede local do servidor; o acesso é liberado apenas para o IP do NPM.
ports: ["${BP_HOST_LAN_IP}:3000:3000"]
networks: [internal, ingress]
backup:
image: postgres:17-alpine
entrypoint: ["/scripts/backup-loop.sh"] # pg_dump diário + retenção
volumes: [./backups:/backups, ./scripts:/scripts:ro]
secrets: [pg_password]
networks: [internal]
volumes:
pgdata:
networks:
internal: { internal: true }
egress: {}
ingress: {} # rede com porta publicada, usada só pelo web
secrets:
pg_password: { file: ./secrets/pg_password }
encryption_key: { file: ./secrets/encryption_key }
telegram_token: { file: ./secrets/telegram_token }
holiday_api_key: { file: ./secrets/holiday_api_key }
```
A `api` também tem saída para a internet (rede `egress`), porque o teste e a validação de credenciais na PontoMais (`PUT /users/{id}/credentials`, `POST /users/{id}/credentials/test`) são feitos por ela, na hora, sem passar pelo `worker`.
A `api` tem saída para a internet porque a validação de credenciais na PontoMais (`PUT /users/{id}/credentials`, `POST /users/{id}/credentials/test`), o reenvio manual de lembretes (`POST /notifications/{id}/resend`) e o botão **Sincronizar** de feriados são feitos por ela, na hora, sem passar pelo `worker`.
### 11.3 Backup e restauração
@@ -1000,8 +985,8 @@ A rede onde o projeto vai rodar já tem um **Nginx Proxy Manager (NPM)**. Ele fa
Navegador ──HTTPS──► NPM (outra máquina) ──HTTP :3000──► servidor Docker: web ──► api ──► postgres
```
- O `web` publica a porta `3000` **só no IP de rede local do servidor** (`BP_HOST_LAN_IP`), nunca em `0.0.0.0`.
- A porta 3000 é liberada **apenas para o IP do NPM**. Qualquer outra máquina da rede é recusada, então ninguém consegue pular o NPM (e o HTTPS) acessando o Next direto.
- O `web` publica a porta `3000` **só no IP de rede local do servidor** (`BP_HOST_LAN_IP`), nunca em `0.0.0.0`. Se a 3000 já estiver ocupada no servidor, `BP_WEB_PORT` troca a porta do lado do host (dentro do container continua 3000); a mesma porta vai no firewall e no Forward Port do NPM. Nos exemplos abaixo, troque 3000 por ela.
- A porta é liberada **apenas para o IP do NPM**. Qualquer outra máquina da rede é recusada, então ninguém consegue pular o NPM (e o HTTPS) acessando o Next direto.
- A `api` e o `postgres` não publicam porta; o NPM só enxerga o Next.
**Cuidado com o firewall e o Docker:** portas publicadas pelo Docker **ignoram as regras comuns do `ufw`/`iptables INPUT`** do host, porque o Docker cria as próprias regras de NAT. Como o servidor é uma **máquina física** (fora do Proxmox), a restrição é feita na cadeia **`DOCKER-USER`**, que o Docker consulta antes das próprias regras:
@@ -1013,23 +998,15 @@ iptables -I DOCKER-USER -p tcp -m conntrack --ctorigdstport 3000 ! -s IP_DO_NPM
No Debian 12 o comando `iptables` já é o `iptables-nft`, o mesmo backend que o Docker usa, então a regra funciona sem ajustes.
**Persistência:** não use `iptables-save`/`netfilter-persistent` para isso. O arquivo salvo levaria junto as regras internas do Docker e poderia conflitar com elas no boot. Em vez disso, uma unit systemd aplica só essa regra **depois** que o Docker sobe, sem duplicar:
**Persistência:** não use `iptables-save`/`netfilter-persistent` para isso. O arquivo salvo levaria junto as regras internas do Docker e poderia conflitar com elas no boot. Em vez disso, a unit systemd `deploy/botpontos-firewall.service` aplica só essa regra **depois** que o Docker sobe, sem duplicar. Ela tem duas variáveis para ajustar antes de instalar: `NPM_IP` (IP do NPM) e `WEB_PORT` (a mesma porta de `BP_WEB_PORT`, padrão 3000):
```ini
# /etc/systemd/system/botpontos-firewall.service
[Unit]
Description=Restringe a porta 3000 do Bot de Pontos ao NPM
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
Environment=NPM_IP=IP_DO_NPM
ExecStart=/bin/sh -c 'iptables -C DOCKER-USER -p tcp -m conntrack --ctorigdstport 3000 ! -s $NPM_IP -j DROP 2>/dev/null || iptables -I DOCKER-USER -p tcp -m conntrack --ctorigdstport 3000 ! -s $NPM_IP -j DROP'
[Install]
WantedBy=multi-user.target
Environment=WEB_PORT=3000
ExecStart=/bin/sh -c 'iptables -C DOCKER-USER -p tcp -m conntrack --ctorigdstport $WEB_PORT ! -s $NPM_IP -j DROP 2>/dev/null || iptables -I DOCKER-USER -p tcp -m conntrack --ctorigdstport $WEB_PORT ! -s $NPM_IP -j DROP'
```
Ativação: `systemctl daemon-reload && systemctl enable --now botpontos-firewall`. Se um dia o servidor for movido para um LXC ou VM no Proxmox, a regra pode ser trocada pelo firewall do Proxmox.
@@ -1041,7 +1018,7 @@ Ativação: `systemctl daemon-reload && systemctl enable --now botpontos-firewal
| Campo | Valor |
| --- | --- |
| Domain Names | ex.: `botpontos.lan` ou `painel.seudominio.com.br` |
| Scheme / Forward Hostname / Port | `http` → `IP_DO_SERVIDOR_DOCKER` : `3000` |
| Scheme / Forward Hostname / Port | `http` → `IP_DO_SERVIDOR_DOCKER` : `3000` (ou `BP_WEB_PORT`) |
| Block Common Exploits | ligado |
| Websockets Support | pode ficar desligado (o painel não usa) |
| SSL Certificate | **Custom certificate**: o certificado interno da rede, emitido pela CA interna para o nome do painel (ver abaixo) |
@@ -1061,7 +1038,7 @@ O HTTPS do painel usa um **certificado emitido pela CA interna da rede**, carreg
**Ajustes na aplicação por causa do proxy**
- O NPM envia `X-Forwarded-For` e `X-Forwarded-Proto`. A cadeia de confiança é: o **Next** só aceita esses headers vindos do IP do NPM (`BP_NPM_IP`), e o **Go** só aceita os headers repassados pelo container `web` (`BP_TRUSTED_PROXIES`). Como o firewall só deixa o NPM falar com a porta 3000, ninguém consegue forjar o IP de origem. Esse IP real é o usado no bloqueio de login e na auditoria.
- O NPM envia `X-Forwarded-For`, e o Next o repassa ao Go junto com o próprio endereço. O **Go** percorre o `X-Forwarded-For` da direita para a esquerda e usa o primeiro endereço que **não** é um proxy confiável (`BP_TRUSTED_PROXIES`: `127.0.0.0/8`, `172.16.0.0/12` das redes Docker e o `BP_NPM_IP`, montados no Compose). O valor mais à esquerda, que o cliente poderia forjar, nunca é usado direto, e o firewall só deixa o NPM falar com a porta do painel. Esse IP real é o usado no bloqueio de login, na sessão e na auditoria. O Next não lê `BP_NPM_IP`.
- A checagem de `Origin` do CSRF (5.4) compara com a URL pública configurada em `BP_PUBLIC_URL` (ex.: `https://botpontos.lan`), não com o endereço interno.
- Os cookies continuam `Secure`, porque o navegador só fala HTTPS com o NPM.
@@ -1085,18 +1062,22 @@ O ambiente de produção (até o momento) é uma **máquina física dedicada, se
## 12. Observabilidade
- Logs JSON (`slog`) com `request_id`, `user_id` e `job`.
- `/health/live` e `/health/ready` nos healthchecks do Compose.
- Healthchecks do Compose: `api` em `:8080/api/v1/health/live`, `worker` em `:8081/health/live` (ambos com `botpontos healthcheck`) e `web` em `/healthz`. O `/health/ready` da API e do worker também faz ping no Postgres.
- O worker registra `liderança obtida; worker ativo` e `bot do Telegram iniciado` ao assumir; uma segunda instância registra `outra instância do worker está ativa; aguardando`.
- **Métricas preparadas, não expostas.** O pacote `internal/metrics` define uma interface (`Recorder`) chamada pelos jobs e pelo cliente da PontoMais: lembretes por tipo e status, falhas e latência da PontoMais, duração da reconciliação, consultas evitadas pela janela (9.12). Na primeira versão a implementação é *no-op* (não faz nada) ou só alimenta os contadores do dashboard do painel. **Não existe endpoint `/metrics`.**
- Para ativar o Prometheus no futuro: trocar a implementação do `Recorder` por uma baseada em `prometheus/client_golang` e expor `/metrics` só na rede interna. Nenhum job precisa mudar.
## 13. Testes
| Nível | Ferramenta | Foco |
| --- | --- | --- |
| Unidade | `go test` | `rules/` e `app/` com *fakes* dos ports (relógio, PontoMais, Telegram) |
| Integração | `testcontainers-go` | Repositórios, `SKIP LOCKED`, migrations |
| Contrato | `openapi.yaml` + validação | Handlers respeitam o contrato; os tipos do Next são gerados dele |
| Web | Vitest + Playwright | Login, ativação de usuário, cancelamento de notificação |
| Nível | Ferramenta | Foco | Situação |
| --- | --- | --- | --- |
| Unidade | `go test` | `rules` (jornada, janela, frequência adaptativa), `config`, `crypto`, clientes PontoMais e feriadosapi.com (servidor HTTP falso), comandos do bot, rotas de saúde e segurança da API | **Existe** e passa |
| Tipos do painel | `tsc --noEmit` | Componentes e chamadas à API | **Existe** e passa |
| Integração | `testcontainers-go` | Repositórios, `SKIP LOCKED`, migrations | Pendente |
| Contrato | `openapi.yaml` + validação | Handlers respeitam o contrato | Pendente (o `openapi.yaml` é mantido à mão) |
| Web | Vitest + Playwright | Login, ativação de usuário, cancelamento de notificação | Pendente |
Os comandos para rodar os testes em containers estão no `README.md` ("Desenvolvimento e testes").
## 14. Plano de sprints sugerido
@@ -1109,6 +1090,7 @@ O ambiente de produção (até o momento) é uma **máquina física dedicada, se
| 4 — API + Auth ✓ | Login, sessões, CSRF, papéis, endpoints de usuários/notificações/auditoria, `openapi.yaml` |
| 5 — Painel Next ✓ | Login, dashboard, usuários (incluindo janela personalizada), notificações, auditoria, configurações globais |
| 6 — Operação ✓ | Feriados (API + manual), contas web, backup/restore, Proxy Host no NPM, hardening |
| Evoluções ✓ | Feriados pela feriadosapi.com (migration `00002`); conta de leitura restrita a um usuário (9.6); exclusão de contas web (migration `00003`); porta do painel configurável (`BP_WEB_PORT`); `go.sum` e `package-lock.json` versionados |
## 15. Decisões registradas (ADR resumido)
@@ -1118,7 +1100,7 @@ O ambiente de produção (até o momento) é uma **máquina física dedicada, se
| 2 | Next.js como BFF, auth no Go | Uma fonte de verdade de autorização; sem CORS; API não exposta |
| 3 | Binário Go único com subcomandos | Uma imagem, mesmo código, deploy simples |
| 4 | Hexagonal com ports explícitos | Regras testáveis sem rede; adapters trocáveis (ex.: outro sistema de ponto) |
| 5 | `openapi.yaml` como contrato | Tipos do TypeScript gerados automaticamente; evita divergência entre Go e Next |
| 5 | `openapi.yaml` como contrato | Evita divergência entre Go e Next. Por enquanto os tipos do TypeScript são escritos à mão a partir dele (decisão 27) |
| 6 | Worker único com advisory lock | O Telegram em long polling não aceita dois consumidores |
| 7 | "No máximo uma vez" para lembretes | Preferível perder um lembrete a enviar em duplicidade (mantido do Modelo) |
| 8 | Despacho a cada 2 min, tolerância de 10 min | Aviso útil sem mensagens tardias demais; acima de 10 min vira `expired` e só é auditado |
@@ -1135,7 +1117,7 @@ O ambiente de produção (até o momento) é uma **máquina física dedicada, se
| 19 | HTTPS com certificado da CA interna no NPM (`botpontos.lan`) | Painel só na rede local; sem exposição pública nem Let's Encrypt |
| 20 | Consultas só na janela global 11:30–19:00 | Reduz ~70–80% das chamadas à PontoMais sem atrasar os lembretes comuns |
| 21 | Sem ponto às 11:30 e 11:45 → reverificação de hora em hora até 15:00 → dia não trabalhado | Evita consultas inúteis em férias sem abandonar cedo quem atrasou ou teve ajuste de ponto |
| 22 | Ferramentas: roteador nativo do Go, `goose`, `go-telegram/bot`, `api` com saída para a PontoMais | Menos dependências; validação de credencial na hora |
| 22 | Ferramentas: roteador nativo do Go, `goose`, `go-telegram/bot`, `api` com saída para a PontoMais | Menos dependências; validação de credencial na hora. O `go-telegram/bot` foi substituído por cliente próprio (decisão 27) |
| 23 | Métricas preparadas por interface, sem expor `/metrics` | Não há Prometheus na rede hoje; ativar depois não exige refatoração |
| 24 | Horários da janela editáveis no painel (`app_settings`), com validação e pré-visualização | Ajuste sem redeploy; a validação evita configurações que quebrariam os lembretes |
| 25 | Frequência adaptativa: 15 min até o 3º ponto, depois 60 min + 1 consulta de confirmação antes do lembrete de saída | ~11 consultas/usuário/dia em vez de ~31, mantendo o cancelamento quando a pessoa sai antes |
@@ -1143,12 +1125,18 @@ O ambiente de produção (até o momento) é uma **máquina física dedicada, se
| 27 | Sem geradores de código nesta fase: SQL à mão com `pgx` (no lugar de `sqlc`), cliente Telegram próprio (no lugar de `go-telegram/bot`), tipos TypeScript à mão e CSS próprio (no lugar de Tailwind/shadcn) | O código foi escrito numa máquina onde não é permitido rodar ferramentas; tudo o que exigiria gerar código ou acertar a API de uma biblioteca sem compilar foi evitado. Podem ser adotados depois sem mudar a arquitetura |
| 28 | `app` usa o repositório Postgres concreto; os ports ficam só para integrações externas | Menos código de interface sem compilador; regras puras continuam testadas em `rules` |
| 29 | Em queda da PontoMais já registrada, falhas de ciclos com um único usuário consultado também não contam | Com a frequência adaptativa, muitos ciclos consultam só um usuário; sem isso, alguém poderia ser pausado durante uma queda conhecida |
| 30 | Feriados pela feriadosapi.com, por código IBGE da cidade | Traz feriados nacionais e municipais numa chamada por ano; a feriados.dev deixou de ser usada (linhas antigas continuam válidas) |
| 31 | Conta `viewer` ligada a um usuário fica restrita aos dados dele, com a regra na API | Permite ao usuário acompanhar e ajustar só o que é dele sem abrir o painel inteiro; o menu do Next só esconde o que a API já recusa |
| 32 | Excluir conta web apaga a linha e preserva o nome na auditoria (`actor_account_name`) | Remove contas sem uso sem perder o histórico de quem fez cada ação; desativar continua sendo a opção reversível |
| 33 | Versões das dependências fixadas em `go.sum` e `package-lock.json`; sem `go mod tidy` com Go 1.25 | Builds reproduzíveis; versões novas das bibliotecas exigem Go 1.26 |
## 16. Estado da implementação e pendências
**Implementado (Sprints 0 a 6):** tudo o que as seções 4 a 14 descrevem, com os desvios da decisão 27.
**Implementado:** tudo o que as seções 4 a 14 descrevem (Sprints 0 a 6 e evoluções), com os desvios das decisões 27 e 28.
**Não verificado:** o código **nunca foi compilado nem testado**, por regra do ambiente onde foi escrito. A primeira validação está no `README.md` ("Primeira validação"): `go mod tidy`, `go vet`, `go test`, `npm install`, `tsc` e subida com Docker Compose.
**Verificado:** o backend compila e passa em `go vet` e `go test`; o painel passa em `tsc --noEmit`. As dependências estão fixadas em `backend/go.sum` e `web/package-lock.json`.
**Ainda não verificado automaticamente:** acesso ao Postgres (repositórios, migrations, `SKIP LOCKED`), worker e fluxo do painel no navegador. Isso depende dos testes de integração e web da seção 13 ou da subida com Docker Compose (DEPLOY.md, etapas 6 e 7).
**Pendências externas** (dependem do operador):
@@ -1159,4 +1147,4 @@ O ambiente de produção (até o momento) é uma **máquina física dedicada, se
- cópia dos backups para fora da máquina (11.5);
- medir no piloto o atraso do `last_cached` da PontoMais (8.2).
**Testes automatizados existentes:** regras de jornada, janela e frequência adaptativa (`rules`), configuração, criptografia, cliente PontoMais (com servidor HTTP falso), comandos do bot e rotas de saúde/segurança da API. Ainda faltam testes de integração com Postgres real (13).
**Pendências de código:** testes de integração com Postgres real, testes de contrato e testes web (13); endpoint `/metrics`, se um dia houver Prometheus (12).

View File

@@ -6,7 +6,9 @@ Passo a passo para colocar o Bot de Pontos no ar no servidor de produção:
- **Acesso ao painel:** pelo **Nginx Proxy Manager (NPM)** que já existe em outra máquina da rede, com **certificado interno**.
- **Endereço do painel:** `https://botpontos.lan` (troque pelo nome que você usar).
> O código **ainda não foi compilado nem testado**. A etapa 6 faz essa validação no próprio servidor antes de subir tudo. Se algo falhar, guarde a saída do comando para correção.
> O backend compila e passa nos testes, e o painel passa na checagem de tipos. A etapa 6 repete essa validação no próprio servidor antes de subir tudo. Se algo falhar, guarde a saída do comando para correção.
>
> A porta do painel no servidor é **3000** por padrão. Se ela já estiver em uso, defina `BP_WEB_PORT` no `.env` (etapa 5.1) e use essa porta onde este guia diz 3000 (etapas 7, 8 e 9).
## Visão geral
@@ -28,7 +30,7 @@ Navegador ──HTTPS──► NPM (outra máquina) ──HTTP :3000──► Se
| 5 | Criar `.env` e segredos | servidor |
| 6 | Validar o código (testes) | servidor |
| 7 | Subir os containers | servidor |
| 8 | Firewall da porta 3000 | servidor |
| 8 | Firewall da porta do painel | servidor |
| 9 | DNS, certificado e Proxy Host | rede / NPM |
| 10 | Primeiro acesso e primeiro admin | navegador / Telegram |
| 11 | Backup fora da máquina | servidor |
@@ -171,18 +173,24 @@ No servidor:
mkdir -p /opt/botpontos
```
No **Windows**, no PowerShell, dentro de `C:\Users\BrunoSantos\Documents\Codex\projeto prl3`, envie a pasta (troque usuário e IP):
No **Windows**, no PowerShell, dentro da pasta que contém `bot-pontos-v2` (ex.: `E:\Bruno\GIT`), gere um pacote sem os arquivos locais. O `tar` já vem no Windows 10/11:
```powershell
scp -r .\bot-pontos-v2 usuario@192.168.18.50:/tmp/
tar -czf bot-pontos-v2.tgz --exclude=node_modules --exclude=.next --exclude=.git --exclude=.env --exclude=backups --exclude=tsconfig.tsbuildinfo bot-pontos-v2
```
> A pasta `secrets/` deve ter só o `README.md` neste momento: os segredos são criados direto no servidor (etapa 5). Não é preciso enviar `node_modules` ou `.next`, se existirem.
Envie para o servidor (troque usuário e IP):
De volta ao servidor, mova para o lugar final:
```powershell
scp .\bot-pontos-v2.tgz usuario@192.168.18.50:/tmp/
```
> O pacote não leva `.env`, `backups/`, `node_modules`, `.next` nem o histórico do Git. A pasta `secrets/` deve ter só o `README.md`: os segredos são criados direto no servidor (etapa 5).
De volta ao servidor, extraia no lugar final:
```bash
cp -r /tmp/bot-pontos-v2/. /opt/botpontos/ && rm -rf /tmp/bot-pontos-v2
tar -xzf /tmp/bot-pontos-v2.tgz -C /opt/botpontos --strip-components=1 && rm /tmp/bot-pontos-v2.tgz
```
```bash
@@ -217,12 +225,24 @@ Ajuste:
```env
BP_HOST_LAN_IP=192.168.18.50 # IP fixo do servidor
BP_WEB_PORT=3000 # porta do painel no servidor; troque se a 3000 estiver em uso
BP_PUBLIC_URL=https://botpontos.lan # exatamente o endereço que o navegador usa
BP_NPM_IP=192.168.18.10 # IP do Nginx Proxy Manager
BP_COOKIE_SECURE=true
BP_LOG_LEVEL=info
BP_DEFAULT_TIMEZONE=America/Sao_Paulo
BP_HOLIDAY_LOCATION=3106200 # código IBGE da cidade dos feriados (3106200 = Belo Horizonte)
BP_ENCRYPTION_KEY_VERSION=1
```
Para saber se a 3000 já está ocupada no servidor:
```bash
ss -ltn 'sport = :3000'
```
Se aparecer alguma linha além do cabeçalho, escolha outra porta (ex.: `BP_WEB_PORT=3010`).
### 5.2 Segredos
```bash
@@ -247,7 +267,7 @@ Token do bot (cole o seu entre as aspas simples):
printf '%s' 'COLE_O_TOKEN_DO_BOTFATHER' > secrets/telegram_token
```
Chave da feriadosapi.com, ou arquivo vazio se não tiver:
Chave da feriadosapi.com (enviada como `Authorization: Bearer`), ou arquivo vazio se não tiver. A cidade dos feriados é o código IBGE em `BP_HOLIDAY_LOCATION` no `.env` (padrão `3106200`, Belo Horizonte):
```bash
printf '%s' 'COLE_A_CHAVE' > secrets/holiday_api_key
@@ -275,24 +295,24 @@ cat secrets/encryption_key
## 6. Validar o código (testes)
O código nunca foi compilado. Rode a validação no servidor antes de subir. Esses comandos usam containers temporários e não instalam Go nem Node na máquina.
Rode a validação no servidor antes de subir, para confirmar que o código copiado compila nesta máquina. Esses comandos usam containers temporários e não instalam Go nem Node na máquina.
Backend — gera o `go.sum`, roda `go vet` e os testes:
Backend — roda `go vet` e os testes com as versões fixadas no `go.sum`:
```bash
docker run --rm -v "$PWD/backend:/src" -w /src golang:1.25-alpine sh -c "go mod tidy && go vet ./... && go test ./..."
docker run --rm -v "$PWD/backend:/src" -w /src golang:1.25-alpine sh -c "go vet ./... && go test ./..."
```
Painel — gera o `package-lock.json` e checa os tipos:
Painel — instala as versões do `package-lock.json` e checa os tipos:
```bash
docker run --rm -v "$PWD/web:/app" -w /app node:22-alpine sh -c "npm install && npx tsc --noEmit"
docker run --rm -v "$PWD/web:/app" -w /app node:22-alpine sh -c "npm ci && npx tsc --noEmit"
```
- Tudo passou: siga para a etapa 7.
- Algo falhou: **pare aqui**, salve a saída completa e traga para correção.
> Os arquivos `go.sum` e `package-lock.json` gerados ficam no projeto e tornam os próximos builds reproduzíveis. Copie-os de volta para a pasta do Windows também.
> Os arquivos `go.sum` e `package-lock.json` fazem parte do projeto e fixam as versões das dependências. Não rode `go mod tidy` no servidor: com Go 1.25 ele tenta baixar versões novas que exigem Go 1.26 e o build quebra.
---
@@ -327,7 +347,7 @@ docker compose logs --tail=100 migrate api worker
O worker deve registrar `liderança obtida; worker ativo` e `bot do Telegram iniciado`.
Teste local da porta (no próprio servidor):
Teste local da porta (no próprio servidor; troque 3000 se usou outro `BP_WEB_PORT`):
```bash
curl -s http://192.168.18.50:3000/healthz
@@ -337,16 +357,24 @@ Deve responder `{"status":"ok"}`.
---
## 8. Firewall da porta 3000
## 8. Firewall da porta do painel
Portas publicadas pelo Docker **ignoram** regras comuns de `ufw`/`iptables INPUT`. A regra vai na cadeia `DOCKER-USER`, aplicada por um serviço do systemd.
> **Outra porta:** se definiu `BP_WEB_PORT` (por exemplo `3010`), use essa porta no lugar de 3000 nesta etapa, no `WEB_PORT` do serviço de firewall e no Forward Port do NPM (etapa 9). Dentro do Docker o painel continua na 3000.
Coloque o IP do NPM no arquivo:
```bash
sed -i 's/IP_DO_NPM/192.168.18.10/' deploy/botpontos-firewall.service
```
Só se `BP_WEB_PORT` não for 3000, ajuste também a porta (troque 3010 pela sua):
```bash
sed -i 's/WEB_PORT=3000/WEB_PORT=3010/' deploy/botpontos-firewall.service
```
Instale e ative:
```bash
@@ -363,7 +391,7 @@ Confira a regra:
iptables -S DOCKER-USER
```
Deve aparecer uma linha com `--ctorigdstport 3000` e `! -s 192.168.18.10/32 -j DROP`.
Deve aparecer uma linha com `--ctorigdstport 3000` (ou a sua porta) e `! -s 192.168.18.10/32 -j DROP`.
**Teste de aceite:** de um computador da rede que **não** seja o NPM, o acesso direto precisa falhar (timeout):
@@ -398,7 +426,7 @@ O certificado raiz da CA interna precisa estar instalado nos computadores que ac
| Details | Domain Names | `botpontos.lan` |
| Details | Scheme | `http` |
| Details | Forward Hostname / IP | `192.168.18.50` |
| Details | Forward Port | `3000` |
| Details | Forward Port | `3000` (ou o valor de `BP_WEB_PORT`) |
| Details | Block Common Exploits | ligado |
| Details | Websockets Support | desligado |
| Details | Access List | uma lista que permita só a rede local (ex.: `192.168.18.0/24`) |
@@ -504,7 +532,7 @@ A `encryption_key` usada no restore precisa ser a mesma da época do backup.
- [ ] `docker compose ps -a` com todos saudáveis e `migrate` em `exited (0)`.
- [ ] `timedatectl` mostra o relógio sincronizado.
- [ ] Acesso direto à porta 3000 bloqueado para quem não é o NPM.
- [ ] Acesso direto à porta do painel (3000 ou `BP_WEB_PORT`) bloqueado para quem não é o NPM.
- [ ] `https://botpontos.lan` abre sem alerta de certificado.
- [ ] Login no painel funciona; conta owner criada.
- [ ] Admin do Telegram recebe aviso ao surgir um novo `/cadastrar`.
@@ -523,7 +551,7 @@ A `encryption_key` usada no restore precisa ser a mesma da época do backup.
docker compose exec backup sh -c 'PGPASSWORD=$(cat /run/secrets/pg_password) pg_dump -h postgres -U botpontos -d botpontos -Fc -f /backups/antes-da-atualizacao.dump'
```
2. Copie a nova versão do código para `/opt/botpontos` (como na etapa 4), **sem** sobrescrever `.env`, `secrets/` e `backups/`.
2. Copie a nova versão do código para `/opt/botpontos` como na etapa 4. O pacote não leva `.env` nem `backups/`, e `secrets/` só tem o `README.md`, então os arquivos do servidor são mantidos.
3. Reconstrua e suba (as migrations rodam sozinhas no serviço `migrate`):
```bash
@@ -532,6 +560,25 @@ A `encryption_key` usada no restore precisa ser a mesma da época do backup.
4. Confira com `docker compose ps -a` e os logs.
**Versão com feriadosapi.com e conta de leitura restrita.** Ao atualizar de uma versão anterior (que usava a feriados.dev):
- Troque o conteúdo de `secrets/holiday_api_key` pela chave da feriadosapi.com (a chave antiga não serve).
- No `.env`, troque `BP_HOLIDAY_LOCATION` de `MG-belo-horizonte` para `3106200` (código IBGE). Se esquecer, os feriados não sincronizam e vale segunda a sexta, com aviso aos admins.
- A migration `00002` roda sozinha no serviço `migrate`. Ela só amplia o `CHECK` de `holidays.source`; os feriados já gravados continuam valendo, e o ano atual não é baixado de novo.
- Para corrigir o ano com a nova API, use **Feriados → Sincronizar**: só entram datas que ainda não estão na tabela.
- A conta de leitura restrita (10.4) não exige configuração extra, mas backend e painel precisam subir juntos.
**Versão com exclusão de contas e porta configurável.**
- A migration `00003` roda sozinha. Ela cria `audit_logs.actor_account_name` (preenchida para os eventos antigos) e ajusta as chaves estrangeiras para permitir **Contas → Excluir** sem perder a auditoria.
- `BP_WEB_PORT` é opcional: sem ela, o painel continua na 3000. Se for trocar a porta, ajuste o `.env`, o `WEB_PORT` do firewall e o Forward Port do NPM. A regra antiga continua no `iptables` até o próximo boot; para aplicar a nova na hora:
```bash
cp deploy/botpontos-firewall.service /etc/systemd/system/ && systemctl daemon-reload && systemctl restart botpontos-firewall
```
Lembre de pôr de novo o IP do NPM (e a porta) no arquivo antes de copiar, como na etapa 8, porque a nova versão do arquivo volta com `IP_DO_NPM`.
Atualize também o sistema e as imagens base de tempos em tempos:
```bash
@@ -549,6 +596,7 @@ docker compose pull postgres backup && docker compose up -d --build
| Sintoma | Causa provável | O que fazer |
| --- | --- | --- |
| `docker compose up` reclama de `BP_HOST_LAN_IP` | `.env` ausente ou sem a variável | etapa 5.1 |
| `web` não sobe: `address already in use` / `port is already allocated` | outra aplicação usa a porta do painel | defina outro `BP_WEB_PORT` (etapa 5.1) e ajuste firewall e NPM |
| `secret ... file not found` | algum arquivo de `secrets/` não existe | etapa 5.2 (o `holiday_api_key` pode ser vazio, mas precisa existir) |
| `migrate` termina com erro de senha | senhas do banco trocadas depois da primeira subida | as senhas só valem na 1ª inicialização do volume; veja `secrets/README.md` |
| `api`/`worker` saem com `BP_ENCRYPTION_KEY ... obrigatório` | chave ausente ou com menos de 32 caracteres | refaça `secrets/encryption_key` |

View File

@@ -195,7 +195,8 @@ O painel tem três tipos de conta. Usuários comuns **não** acessam o painel; e
| Alterar jornada, janela personalizada e credencial; testar a PontoMais | | ✓ | ✓ |
| Cancelar e reenviar lembretes | | ✓ | ✓ |
| Cadastrar feriados e alterar a janela global | | ✓ | ✓ |
| Criar contas, mudar papéis, redefinir senhas | | | ✓ |
| Definir o admin do bot | | ✓ | ✓ |
| Criar, desativar e excluir contas, mudar papéis e usuário ligado, redefinir senhas | | | ✓ |
### Conta de leitura ligada a um usuário
@@ -339,10 +340,10 @@ Senhas e tokens nunca aparecem na auditoria.
## Feriados
O bot não consulta nem envia lembretes em fins de semana e feriados.
O bot não consulta nem envia lembretes em fins de semana e feriados. A tela mostra os feriados de um ano por vez; use os links do ano anterior e do seguinte (ex.: **← 2025 · 2027 →**) para navegar. A coluna **Origem** indica `manual` ou a API de onde o feriado veio.
- **Sincronizar com a feriadosapi.com:** baixa os feriados do ano para a cidade configurada. O sistema também faz isso sozinho no primeiro uso de cada ano.
- **Cadastrar feriado manual:** para pontos facultativos, recessos ou feriados que a API não trouxe.
- **Sincronizar (ano) com a feriadosapi.com:** baixa os feriados do ano para a cidade configurada (nacionais e municipais; hoje Belo Horizonte). O sistema também faz isso sozinho no primeiro uso de cada ano. Só entram datas que ainda não estão na lista, então feriados manuais nunca são alterados.
- **Cadastrar feriado manual:** para pontos facultativos, recessos ou feriados que a API não trouxe (por exemplo, o Carnaval).
- **Remover:** só feriados manuais.
Se a API de feriados estiver fora do ar e o ano ainda não tiver sido sincronizado, segunda a sexta contam como dias úteis. Os admins do bot recebem um aviso por dia enquanto isso durar.
@@ -371,8 +372,9 @@ Só leitura: intervalo das consultas, do envio, tolerância de atraso e limite d
- **Usuário do Telegram:** ao criar a conta, ou depois no seletor da linha, escolha o usuário ao qual ela fica ligada (ou "Sem usuário ligado" para remover o vínculo). Com papel de leitura, isso restringe a conta aos dados desse usuário (ver [Papéis](#papéis)). A troca vale na próxima requisição, sem precisar sair e entrar de novo.
- **Redefinir senha:** define uma nova senha e encerra as sessões abertas daquela conta.
- **Desativar / Reativar:** desativar encerra as sessões; ninguém pode desativar a própria conta.
- **Excluir:** apaga a conta de vez, com as sessões abertas. Não dá para desfazer; na auditoria, as ações antigas continuam mostrando o nome da conta. Ninguém pode excluir a própria conta. Para só bloquear o acesso, prefira **Desativar**.
Sempre precisa existir **pelo menos um owner ativo**. O sistema recusa rebaixar ou desativar o último.
Sempre precisa existir **pelo menos um owner ativo**. O sistema recusa rebaixar, desativar ou excluir o último.
## Comandos de admin no Telegram

View File

@@ -4,7 +4,7 @@
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
# Enquanto o package-lock.json não existir no repositório, usa npm install (ver README).
# Fallback: sem package-lock.json, usa npm install (o lock já é versionado; ver README).
RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
# ---- build ----