763 lines
36 KiB
Markdown
763 lines
36 KiB
Markdown
# Convites Digitais — Manual de Instruções
|
||
|
||
Este documento reúne **tudo o que é preciso para instalar, publicar e manter** os Convites Digitais no servidor. É para o **administrador do sistema**.
|
||
|
||
- **Parte 1 — Deploy:** instalação, configuração, operação, backup e solução de problemas.
|
||
- **Parte 2 — Histórico de alterações:** o que mudou em cada versão e como atualizar o servidor.
|
||
|
||
> Para **usar o sistema** (criar eventos e convites, enviar, acompanhar respostas, PDFs), veja o [MANUAL-USUARIO.md](MANUAL-USUARIO.md). Ele é escrito para o organizador da festa, sem termos técnicos, e pode ser entregue a quem vai gerenciar os eventos.
|
||
>
|
||
> Para **desenvolver ou alterar o código** (arquitetura, banco de dados, desenvolvimento local, serviço Python), veja o [README.md](README.md).
|
||
|
||
---
|
||
|
||
## Sumário
|
||
|
||
**Parte 1 — Deploy**
|
||
|
||
1. [Visão geral da arquitetura](#1-visão-geral-da-arquitetura)
|
||
2. [Pré-requisitos](#2-pré-requisitos)
|
||
3. [Copiar o projeto para o servidor](#3-copiar-o-projeto-para-o-servidor)
|
||
4. [Configurar as variáveis de ambiente](#4-configurar-as-variáveis-de-ambiente)
|
||
5. [Subir a aplicação](#5-subir-a-aplicação)
|
||
6. [Verificar se está tudo funcionando](#6-verificar-se-está-tudo-funcionando)
|
||
7. [Publicar via Cloudflare](#7-publicar-via-cloudflare)
|
||
8. [Atualizar para uma nova versão](#8-atualizar-para-uma-nova-versão)
|
||
9. [Backup e restauração](#9-backup-e-restauração)
|
||
10. [Operação do dia a dia](#10-operação-do-dia-a-dia)
|
||
11. [Solução de problemas](#11-solução-de-problemas)
|
||
|
||
**Parte 2 — Histórico de alterações**
|
||
|
||
12. [Versão de 05/10/2026](#12-versão-de-05102026)
|
||
13. [Versão de 06/10/2026 — PDFs dos convites](#13-versão-de-06102026--pdfs-dos-convites)
|
||
14. [Versão de 07/10/2026 — Mensagem do WhatsApp personalizada](#14-versão-de-07102026--mensagem-do-whatsapp-personalizada)
|
||
|
||
---
|
||
|
||
# Parte 1 — Deploy
|
||
|
||
## 1. Visão geral da arquitetura
|
||
|
||
A aplicação roda em **três containers** gerenciados pelo Docker Compose:
|
||
|
||
| Serviço | Imagem | Função |
|
||
| --- | --- | --- |
|
||
| `app` | construída pelo `Dockerfile` do projeto | Servidor Next.js (páginas, área do organizador, RSVP) na porta `3000` |
|
||
| `db` | `postgres:17-alpine` | Banco de dados. Os dados ficam no volume `pgdata` |
|
||
| `pdf-service` | construída por `pdf-service/Dockerfile` (Python) | Gera os PDFs personalizados dos convites. Só o `app` acessa |
|
||
|
||
```
|
||
Convidado / Organizador
|
||
│
|
||
▼
|
||
Cloudflare (HTTPS) ──► Servidor :3000 ──► container app ──► container db (Postgres)
|
||
│ └─ volume pgdata
|
||
│ HTTP interno (http://pdf-service:8000)
|
||
▼
|
||
container pdf-service
|
||
│
|
||
volume app-storage ◄──────┴── (app e pdf-service: PDFs modelo e gerados)
|
||
```
|
||
|
||
Pontos importantes:
|
||
|
||
- O banco e o `pdf-service` **não ficam expostos** para fora do servidor; só o container `app` os acessa.
|
||
- Os PDFs (modelos e gerados) ficam no volume Docker `app-storage`, **fora do banco**. O banco guarda só as informações sobre eles.
|
||
- As **migrations** (criação/atualização das tabelas) são aplicadas **automaticamente** toda vez que o container `app` sobe.
|
||
- Os dados ficam no volume Docker `pgdata` e **sobrevivem** a reinícios e rebuilds dos containers.
|
||
|
||
---
|
||
|
||
## 2. Pré-requisitos
|
||
|
||
No servidor (ex.: `192.168.18.97`):
|
||
|
||
- **Docker** com o plugin **Compose**. Para conferir:
|
||
|
||
```bash
|
||
docker --version
|
||
docker compose version
|
||
```
|
||
|
||
- **Acesso à internet durante o build** — o build baixa as dependências npm e as fontes do Google.
|
||
- **Porta livre** para a aplicação (padrão `3000`; pode ser alterada em `APP_PORT`).
|
||
|
||
---
|
||
|
||
## 3. Copiar o projeto para o servidor
|
||
|
||
Copie a pasta do projeto para o servidor **sem** as pastas `node_modules`, `.next` e `dist` (elas são recriadas no build).
|
||
|
||
Exemplo com `rsync`, a partir da máquina onde está o código:
|
||
|
||
```bash
|
||
rsync -av --exclude node_modules --exclude .next --exclude dist ./ usuario@192.168.18.97:/opt/convite/
|
||
```
|
||
|
||
> Troque `usuario` e `/opt/convite/` pelo usuário e pela pasta usados nas outras aplicações do servidor.
|
||
|
||
Arquivos essenciais que precisam estar no servidor:
|
||
|
||
```
|
||
Dockerfile
|
||
compose.yml
|
||
.dockerignore
|
||
.env.example
|
||
package.json
|
||
package-lock.json
|
||
next.config.ts
|
||
tsconfig.json
|
||
postcss.config.mjs
|
||
drizzle.config.ts
|
||
drizzle/ (migrations SQL)
|
||
scripts/ (script de migration)
|
||
pdf-service/ (serviço Python de PDFs)
|
||
src/
|
||
public/
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Configurar as variáveis de ambiente
|
||
|
||
No servidor, dentro da pasta do projeto:
|
||
|
||
```bash
|
||
cd /opt/convite
|
||
cp .env.example .env
|
||
nano .env
|
||
```
|
||
|
||
### Variáveis
|
||
|
||
| Variável | Obrigatória | Exemplo | Descrição |
|
||
| --- | --- | --- | --- |
|
||
| `POSTGRES_DB` | não | `convite` | Nome do banco |
|
||
| `POSTGRES_USER` | não | `convite` | Usuário do banco |
|
||
| `POSTGRES_PASSWORD` | **sim** | `Xk29fLq81` | Senha do banco. **Só letras e números** |
|
||
| `DATABASE_URL` | não* | — | Usada apenas fora do Docker. No compose é montada automaticamente |
|
||
| `APP_PORT` | não | `3000` | Porta publicada no servidor |
|
||
| `APP_URL` | recomendada | `https://convite.seudominio.com` | Endereço público do app, sem barra no final. Usado no card de pré-visualização do WhatsApp |
|
||
| `ORGANIZER_PASSWORD` | **sim** | — | Senha de acesso à área do organizador |
|
||
| `SESSION_SECRET` | **sim** | — | Chave secreta de 32+ caracteres usada na verificação da senha |
|
||
| `COOKIE_SECURE` | **sim** | `false` | `false` para acesso por `http://IP`; `true` atrás do Cloudflare (HTTPS) |
|
||
| `RUN_MIGRATIONS` | não | `true` | `false` desliga a migration automática na subida |
|
||
| `INVITE_LINK_BASE_URL` | não | `https://convite.n1b3.com.br` | Endereço usado no link gravado em cada PDF: `<valor>/r/<token>`, sem barra no final. **Em branco, usa o `APP_URL`.** Só defina se o link do PDF tiver de ser diferente do endereço do app |
|
||
| `INVITE_LINK_PATTERN` | não | `com.br/r/` | Trecho que identifica, no PDF modelo, o link a ser trocado pelo link de cada convite |
|
||
| `PDF_SERVICE_URL` | não* | `http://pdf-service:8000` | Endereço do serviço de PDFs. No compose é definido automaticamente |
|
||
| `STORAGE_DIR` | não* | `/storage` | Pasta dos PDFs. No compose é definida automaticamente (volume `app-storage`) |
|
||
|
||
\* Não precisa alterar para o deploy com Docker. Essas variáveis só importam ao rodar fora do Docker (ver [README.md](README.md#variáveis-de-ambiente)).
|
||
|
||
### Gerar o `SESSION_SECRET`
|
||
|
||
Em qualquer máquina com Node.js:
|
||
|
||
```bash
|
||
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
|
||
```
|
||
|
||
Ou no próprio servidor, sem Node:
|
||
|
||
```bash
|
||
openssl rand -base64 32 | tr -d '/+='
|
||
```
|
||
|
||
Cole o resultado em `SESSION_SECRET=`.
|
||
|
||
### Exemplo de `.env` preenchido
|
||
|
||
```dotenv
|
||
POSTGRES_DB=convite
|
||
POSTGRES_USER=convite
|
||
POSTGRES_PASSWORD=Xk29fLq81mPz
|
||
|
||
APP_PORT=3000
|
||
APP_URL=http://192.168.18.97:3000
|
||
|
||
ORGANIZER_PASSWORD=uma-senha-forte-para-o-admin
|
||
SESSION_SECRET=3qD0x9bN1pY7sKcVb2T8wM4rH6jLz5aEoUfGiQ
|
||
COOKIE_SECURE=false
|
||
|
||
INVITE_LINK_PATTERN=com.br/r/
|
||
```
|
||
|
||
> Os PDFs usam o `APP_URL` como endereço do link. Para um endereço diferente, acrescente `INVITE_LINK_BASE_URL=https://...`.
|
||
|
||
> 🔒 O `.env` contém senhas. Não envie esse arquivo por e-mail/chat e restrinja o acesso: `chmod 600 .env`.
|
||
|
||
---
|
||
|
||
## 5. Subir a aplicação
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
```
|
||
|
||
O que acontece, em ordem:
|
||
|
||
1. As imagens do `app` e do `pdf-service` são construídas (pode levar alguns minutos na primeira vez).
|
||
2. O Postgres e o `pdf-service` sobem; o `app` aguarda o banco ficar saudável.
|
||
3. O `app` aplica as migrations pendentes.
|
||
4. O servidor Next.js inicia na porta `APP_PORT`.
|
||
|
||
---
|
||
|
||
## 6. Verificar se está tudo funcionando
|
||
|
||
**Status dos containers** — os três devem estar `running` (e `db` e `pdf-service` como `healthy`):
|
||
|
||
```bash
|
||
docker compose ps
|
||
```
|
||
|
||
**Logs do app:**
|
||
|
||
```bash
|
||
docker compose logs -f app
|
||
```
|
||
|
||
Saída esperada (resumida):
|
||
|
||
```
|
||
Migrations aplicadas.
|
||
▲ Next.js 16.x
|
||
- Local: http://0.0.0.0:3000
|
||
✓ Ready
|
||
```
|
||
|
||
**No navegador:**
|
||
|
||
| Endereço | Deve mostrar |
|
||
| --- | --- |
|
||
| `http://192.168.18.97:3000/` | Página inicial "Convites Digitais de Aniversário" |
|
||
| `http://192.168.18.97:3000/admin` | Tela de login do organizador |
|
||
|
||
Entre com a `ORGANIZER_PASSWORD`. Se cair na lista de eventos (vazia), o deploy está concluído.
|
||
|
||
---
|
||
|
||
## 7. Publicar via Cloudflare
|
||
|
||
Quando o Cloudflare for apontar para a aplicação (mesmo padrão das outras aplicações do servidor):
|
||
|
||
1. Configure o domínio/túnel no Cloudflare apontando para `http://<servidor>:<APP_PORT>`.
|
||
2. No `.env`, altere:
|
||
|
||
```dotenv
|
||
COOKIE_SECURE=true
|
||
APP_URL=https://<seu-domínio>
|
||
```
|
||
|
||
3. Recrie o container do app:
|
||
|
||
```bash
|
||
docker compose up -d app
|
||
```
|
||
|
||
4. Acesse `https://<seu-domínio>/admin` e faça login para confirmar.
|
||
|
||
> ⚠️ Depois de `COOKIE_SECURE=true`, o login **pelo IP em HTTP deixa de funcionar** — isso é esperado. Use sempre o domínio HTTPS.
|
||
|
||
Os links de convite são gerados a partir do endereço em que o organizador está navegando. Por isso, **depois da publicação, gere/copie os links acessando o `/admin` pelo domínio**, e não pelo IP.
|
||
|
||
---
|
||
|
||
## 8. Atualizar para uma nova versão
|
||
|
||
1. Copie o código novo para o servidor (mesmo comando `rsync` do passo 3). O `.env` do servidor **não** é sobrescrito, pois não está na pasta de origem — confira antes.
|
||
2. Faça um backup do banco (ver [seção 9](#9-backup-e-restauração)) — recomendado sempre que houver mudança no banco.
|
||
3. Reconstrua e suba o app:
|
||
|
||
```bash
|
||
docker compose up -d --build app
|
||
```
|
||
|
||
4. Verifique os logs (`docker compose logs -f app`). Novas migrations são aplicadas automaticamente.
|
||
5. Consulte o [Histórico de alterações](#parte-2--histórico-de-alterações) para saber se a versão pede algum passo extra (ex.: variável nova no `.env`).
|
||
|
||
Os dados **não são perdidos** em atualizações.
|
||
|
||
---
|
||
|
||
## 9. Backup e restauração
|
||
|
||
### Backup
|
||
|
||
```bash
|
||
docker compose exec -T db sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > backup-$(date +%F).sql
|
||
```
|
||
|
||
Gera um arquivo como `backup-2026-11-10.sql` na pasta atual. Guarde uma cópia **fora do servidor**.
|
||
|
||
**Backup automático diário (opcional)** — adicione ao `crontab -e` do servidor:
|
||
|
||
```cron
|
||
0 3 * * * cd /opt/convite && docker compose exec -T db sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > /opt/convite/backups/backup-$(date +\%F).sql
|
||
```
|
||
|
||
(crie a pasta antes: `mkdir -p /opt/convite/backups`)
|
||
|
||
### Backup dos PDFs
|
||
|
||
Os PDFs modelo e gerados ficam no volume `app-storage`, **não** entram no `pg_dump`. Para copiá-los:
|
||
|
||
```bash
|
||
docker run --rm -v convite_app-storage:/storage -v "$PWD":/backup alpine tar czf /backup/pdfs-$(date +%F).tar.gz -C /storage .
|
||
```
|
||
|
||
Para restaurar (com os containers parados):
|
||
|
||
```bash
|
||
docker run --rm -v convite_app-storage:/storage -v "$PWD":/backup alpine sh -c 'tar xzf /backup/pdfs-AAAA-MM-DD.tar.gz -C /storage && chown -R 1001:1001 /storage'
|
||
```
|
||
|
||
> Os PDFs gerados podem ser refeitos a qualquer momento (botão **Gerar PDFs de todos os convites**). O que vale guardar é o **PDF modelo** de cada evento.
|
||
|
||
### Restauração
|
||
|
||
A restauração deve ser feita em um **banco vazio**. Para recriar o banco do zero:
|
||
|
||
```bash
|
||
docker compose down
|
||
docker volume rm convite_pgdata # ⚠️ APAGA todos os dados atuais
|
||
docker compose up -d db
|
||
docker compose exec -T db sh -c 'psql -U "$POSTGRES_USER" "$POSTGRES_DB"' < backup-AAAA-MM-DD.sql
|
||
docker compose up -d app
|
||
```
|
||
|
||
> ⚠️ **Nunca** use `docker compose down -v` sem querer apagar o banco — o `-v` remove os volumes `pgdata` e `app-storage` (PDFs).
|
||
|
||
---
|
||
|
||
## 10. Operação do dia a dia
|
||
|
||
| Tarefa | Comando |
|
||
| --- | --- |
|
||
| Ver status | `docker compose ps` |
|
||
| Ver logs do app | `docker compose logs -f app` |
|
||
| Ver logs do banco | `docker compose logs -f db` |
|
||
| Ver logs do serviço de PDFs | `docker compose logs -f pdf-service` |
|
||
| Reiniciar o serviço de PDFs | `docker compose restart pdf-service` |
|
||
| Reiniciar o app | `docker compose restart app` |
|
||
| Parar tudo (mantém dados) | `docker compose down` |
|
||
| Subir novamente | `docker compose up -d` |
|
||
| Console SQL | `docker compose exec db sh -c 'psql -U "$POSTGRES_USER" "$POSTGRES_DB"'` |
|
||
| Trocar a senha do organizador | Edite `ORGANIZER_PASSWORD` no `.env` e rode `docker compose up -d app` |
|
||
| Derrubar todas as sessões abertas | No `/admin`, clique em **Sair de todos** (no topo da página) |
|
||
|
||
Os containers estão com `restart: unless-stopped`: voltam sozinhos se o servidor reiniciar.
|
||
|
||
---
|
||
|
||
## 11. Solução de problemas
|
||
|
||
| Sintoma | Causa provável | Solução |
|
||
| --- | --- | --- |
|
||
| `defina POSTGRES_PASSWORD no .env` ao subir | Variável ausente | Preencha `POSTGRES_PASSWORD` no `.env` |
|
||
| App reiniciando em loop; log com `Falha ao aplicar migrations` | Banco inacessível ou senha errada | Confira `docker compose logs db` e as variáveis `POSTGRES_*` |
|
||
| Erro de senha no Postgres após trocar `POSTGRES_PASSWORD` | A senha só é definida na **primeira** criação do volume | Volte a senha antiga no `.env` ou altere no banco: `ALTER USER convite PASSWORD 'nova';` |
|
||
| Login do `/admin` sempre volta para a tela de login | `COOKIE_SECURE=true` com acesso via HTTP | Use `false` no acesso por IP, ou acesse pelo domínio HTTPS |
|
||
| Erro `SESSION_SECRET não definida` no log | Variável vazia ou curta | Gere um valor de 32+ caracteres (seção 4) |
|
||
| "Senha incorreta." no login | `ORGANIZER_PASSWORD` diferente da digitada | Confira o `.env` e recrie o app após alterar |
|
||
| "Muitas tentativas incorretas" no login | 5 senhas erradas em 15 minutos no mesmo IP | Aguarde o tempo indicado, ou reinicie o app (`docker compose restart app`) para zerar o bloqueio |
|
||
| Link colado no WhatsApp aparece sem imagem | `APP_URL` ausente ou errada, ou o WhatsApp guardou uma versão antiga | Ajuste `APP_URL` no `.env`, rode `docker compose up -d app` e teste com um convite que ainda não foi compartilhado |
|
||
| Build falha baixando fontes/pacotes | Servidor sem internet no build | Libere o acesso à internet durante o `docker compose build` |
|
||
| Porta 3000 em uso | Outra aplicação na mesma porta | Altere `APP_PORT` no `.env` |
|
||
| Link do convite abre com o IP em vez do domínio | Link copiado acessando o admin pelo IP | Acesse o `/admin` pelo domínio e copie de novo |
|
||
| "Endereço do link não configurado" ao gerar PDF | Nem `APP_URL` nem `INVITE_LINK_BASE_URL` no `.env` | Defina `APP_URL` (seção 4) e rode `docker compose up -d pdf-service` |
|
||
| "Serviço de PDF indisponível" ao enviar modelo ou gerar PDF | Container `pdf-service` parado ou com erro | `docker compose ps` e `docker compose logs pdf-service`; suba com `docker compose up -d pdf-service` |
|
||
| "Nenhum link de convite (…/r/…) foi encontrado neste PDF" | O modelo não tem um link clicável com `INVITE_LINK_PATTERN` | Confira o link no PDF (precisa ser um link clicável, não só texto) ou ajuste `INVITE_LINK_PATTERN` |
|
||
| Convite com "Erro ao gerar" | Falha ao gerar aquele PDF | A mensagem aparece no card. Veja `docker compose logs app pdf-service` e clique em **Gerar novamente** |
|
||
| "Geração interrompida pela reinicialização do servidor" | O app reiniciou durante a geração | Clique em **Gerar só pendentes e com erro** |
|
||
| Erro de permissão (`Permission denied`) em `/storage` nos logs | Arquivos do volume com outro dono | `docker compose run --rm --user root --entrypoint chown pdf-service -R 1001:1001 /storage` |
|
||
| PDF baixado com link errado | `APP_URL` (ou `INVITE_LINK_BASE_URL`, se definida) errada | Corrija no `.env`, rode `docker compose up -d pdf-service` e gere os PDFs de novo |
|
||
|
||
### 11.1 Diagnóstico dos PDFs
|
||
|
||
| Onde olhar | Comando |
|
||
| --- | --- |
|
||
| Saúde do serviço de PDFs | `docker compose ps pdf-service` (deve estar `healthy`) |
|
||
| Logs da geração (lado Python) | `docker compose logs -f pdf-service` — linhas `PDF gerado convite=...` ou o erro completo |
|
||
| Logs da geração (lado app) | `docker compose logs -f app` — linhas `Falha ao gerar o PDF do convite ...` |
|
||
| Arquivos no volume | `docker compose exec app ls -R /storage/events` |
|
||
| Lotes recentes | `docker compose exec db sh -c 'psql -U "$POSTGRES_USER" "$POSTGRES_DB" -c "select status, processed, failed, total, created_at from pdf_jobs order by created_at desc limit 5"'` |
|
||
| Testar o serviço por dentro | `docker compose exec app wget -qO- http://pdf-service:8000/health` |
|
||
|
||
Para erros no código do serviço Python (traceback, links de um PDF, nível de log), veja "Manutenção do `pdf-service`" no [README.md](README.md#manutenção-do-pdf-service-código-python).
|
||
|
||
---
|
||
|
||
# Parte 2 — Histórico de alterações
|
||
|
||
## 12. Versão de 05/10/2026
|
||
|
||
### 12.1 Resumo
|
||
|
||
| # | Alteração | Para quem muda algo | Exige ação no deploy? |
|
||
| --- | --- | --- | --- |
|
||
| 1 | Visual neutro (Slate) na área do organizador e na página inicial | Organizador | Não |
|
||
| 2 | Limite de tentativas de login | Organizador | Não |
|
||
| 3 | Sessões guardadas no banco + botão **Sair de todos** | Organizador | Sim: migration automática; todos precisam entrar de novo |
|
||
| 4 | Nova variável `APP_URL` | Quem faz o deploy | Sim: adicionar ao `.env` |
|
||
| 5 | Card de pré-visualização do convite no WhatsApp | Convidado | Depende do item 4 |
|
||
| 6 | Exportação da lista de convidados em CSV | Organizador | Não |
|
||
| 7 | Projeto renomeado para **Convites Digitais** e documentação atualizada | Todos | Não |
|
||
|
||
Os passos para atualizar o servidor estão na [seção 12.9](#129-roteiro-de-atualização-desta-versão).
|
||
|
||
---
|
||
|
||
### 12.2 Visual neutro na área do organizador
|
||
|
||
**Antes:** login, painel e página inicial usavam as cores e o 🦁 do tema Safári, mesmo em eventos com outro tema.
|
||
|
||
**Agora:**
|
||
|
||
- Paleta **Slate / Grafite**: fundo cinza-claro liso, cards brancos, textos em cinza-azulado e botões principais em índigo.
|
||
- O 🦁 foi trocado por um ícone de calendário no login, no cabeçalho do painel e na página inicial.
|
||
- A página inicial agora diz "Convites Digitais" em vez de "Convites Safari".
|
||
- Contorno de foco, seleção de texto e checkboxes da área do organizador também usam índigo.
|
||
- A página **"Convite não encontrado"** ficou neutra (ícone de lupa, fundo cinza-claro).
|
||
- A cor da barra do navegador no celular agora é cinza-claro no app e **a cor do tema** na página de cada convite.
|
||
|
||
**O que continua igual, de propósito:**
|
||
|
||
- As cores de status: verde = presente, vermelho = ausente, âmbar = pendente.
|
||
- A prévia do tema no formulário de evento (mostra as cores do tema escolhido).
|
||
- O botão **WhatsApp** (verde).
|
||
- As páginas dos convites, que continuam com o tema escolhido para cada evento.
|
||
|
||
**Arquivos:** `src/app/admin/**`, `src/components/admin/**`, `src/app/page.tsx`, `src/app/r/[token]/not-found.tsx`, `src/app/globals.css` (classe `.admin-ui`) e `src/components/ui/index.ts`. `Button` e `Input` ganharam a opção `tone="neutral"`; o padrão continua sendo as cores do tema, usadas no convite.
|
||
|
||
---
|
||
|
||
### 12.3 Limite de tentativas de login
|
||
|
||
**Antes:** a senha do `/admin` podia ser testada sem limite.
|
||
|
||
**Agora:**
|
||
|
||
- Depois de **5 senhas erradas em 15 minutos**, o login fica **bloqueado por 15 minutos** para aquele IP.
|
||
- Durante o bloqueio a senha nem é conferida, e a tela mostra quantos minutos faltam.
|
||
- Um login certo zera o contador.
|
||
|
||
**Detalhes técnicos:**
|
||
|
||
- Os contadores ficam **na memória do app**. Reiniciar o container (`docker compose restart app`) zera todos os bloqueios.
|
||
- O IP vem do cabeçalho `cf-connecting-ip`, enviado pelo Cloudflare. Sem ele, o app usa `x-forwarded-for` ou `x-real-ip`.
|
||
- ⚠️ Quem acessa o servidor **direto pelo IP**, sem passar pelo Cloudflare, consegue forjar esses cabeçalhos e escapar do limite. A proteção completa é o servidor aceitar só tráfego vindo do Cloudflare (Cloudflare Tunnel ou regra de firewall).
|
||
|
||
**Arquivos:** `src/lib/login-rate-limit.ts` (novo) e `src/app/admin/auth-actions.ts`.
|
||
|
||
---
|
||
|
||
### 12.4 Sessões no banco e "Sair de todos"
|
||
|
||
**Antes:** o cookie de sessão se validava sozinho, por uma assinatura. **Sair** só apagava o cookie do navegador: uma cópia dele continuava valendo por até 7 dias. Para derrubar todas as sessões era preciso trocar o `SESSION_SECRET` e recriar o app.
|
||
|
||
**Agora:**
|
||
|
||
- Cada login cria uma linha na tabela nova **`organizer_sessions`**. O cookie guarda um token aleatório, e o banco guarda só o **hash** dele.
|
||
- Toda página e ação do organizador confere se a sessão ainda existe e não venceu (continua durando 7 dias).
|
||
- **Sair** apaga a sessão no banco, então uma cópia do cookie para de funcionar.
|
||
- **Sair de todos**, um botão novo no topo do painel com confirmação, apaga as sessões de todos os dispositivos.
|
||
- Sessões vencidas são apagadas a cada novo login.
|
||
- O `SESSION_SECRET` continua **obrigatório**: agora ele é usado na verificação da senha.
|
||
|
||
**Impacto no deploy:**
|
||
|
||
- Migration nova `drizzle/0004_organizer_sessions.sql`, aplicada automaticamente quando o container sobe.
|
||
- Os cookies antigos deixam de valer: **depois da atualização, todos precisam entrar de novo**.
|
||
|
||
**Arquivos:** `src/db/schema.ts`, `src/lib/auth.ts`, `src/app/admin/auth-actions.ts`, `src/app/admin/(painel)/layout.tsx` e `drizzle/0004_organizer_sessions.sql`.
|
||
|
||
---
|
||
|
||
### 12.5 Variável `APP_URL`
|
||
|
||
**Antes:** o endereço base usado nos links das pré-visualizações estava fixo no código como `http://localhost:3000`.
|
||
|
||
**Agora:** esse endereço vem da variável **`APP_URL`** do `.env`.
|
||
|
||
| Situação | Valor de `APP_URL` |
|
||
| --- | --- |
|
||
| Acesso pelo IP (antes do Cloudflare) | `http://192.168.18.97:3000` |
|
||
| Depois da publicação no Cloudflare | `https://<seu-domínio>` |
|
||
|
||
- Escreva **sem barra no final**.
|
||
- O valor é lido com o app rodando: depois de alterar, basta `docker compose up -d app` (sem rebuild).
|
||
- Se a variável faltar ou for inválida, o app usa `http://localhost:3000` e registra um aviso no log. Nesse caso o card do WhatsApp (12.6) fica sem imagem.
|
||
|
||
**Arquivos:** `src/lib/app-url.ts` (novo), `src/app/layout.tsx` e `.env.example`.
|
||
|
||
---
|
||
|
||
### 12.6 Card de pré-visualização no WhatsApp
|
||
|
||
**Antes:** o link do convite colado no WhatsApp aparecia sem imagem e com um título genérico.
|
||
|
||
**Agora:** aparece um card com:
|
||
|
||
- **imagem** (1200×630) nas cores do tema do evento, com o selo e o ícone do tema, "*Família*, você está convidado para", o nome da festa, data · horário · local e "Toque para confirmar sua presença";
|
||
- **título** "*Família*, você está convidado!";
|
||
- **descrição** com o nome da festa, a data, o horário e o local.
|
||
|
||
**Detalhes:**
|
||
|
||
- A imagem mostra só o que a página do convite já mostra para quem tem o link. **A lista de convidados não aparece.**
|
||
- A imagem é gerada na hora, então edições no evento aparecem nos próximos compartilhamentos.
|
||
- O WhatsApp **guarda o card de cada link por um tempo**: links compartilhados antes da atualização podem continuar sem imagem.
|
||
- Os emojis da imagem são baixados da internet ao gerar o card: o servidor precisa de acesso à internet.
|
||
- A mensagem pronta do botão **WhatsApp** agora usa o **ícone do tema do evento** (antes era sempre 🦁).
|
||
|
||
**Arquivos:** `src/app/r/[token]/opengraph-image.tsx` (novo), `src/app/r/[token]/page.tsx` e `src/components/admin/ShareInvitation.tsx`.
|
||
|
||
---
|
||
|
||
### 12.7 Exportar a lista de convidados (CSV)
|
||
|
||
**Agora:** na página do evento, abaixo do **Resumo de presença**, em **Exportar lista (CSV)**, há os botões:
|
||
|
||
| Botão | Conteúdo do arquivo |
|
||
| --- | --- |
|
||
| **Todos** | Todos os convidados do evento |
|
||
| **Presentes** | Só quem confirmou |
|
||
| **Ausentes** | Só quem recusou (e, depois do prazo, quem não respondeu) |
|
||
| **Pendentes** | Só quem ainda não respondeu |
|
||
|
||
- **Colunas:** Convite, Convidado, Tipo (Adulto/Adolescente/Criança), Status e Respondido em (horário de Brasília).
|
||
- **Abre direto no Excel em português** e no Google Planilhas, com colunas e acentos corretos (separador `;`, UTF-8).
|
||
- **Nome do arquivo:** parte do nome da festa e do filtro, ex.: `1-aniversario-do-arthur-presentes.csv`.
|
||
- Só funciona com o organizador logado. Sem login, o endereço redireciona para a tela de login.
|
||
- Nomes que começam com `=`, `+`, `-` ou `@` recebem um apóstrofo na frente, para o Excel não tratá-los como fórmula.
|
||
|
||
**Arquivos:** `src/app/admin/(painel)/eventos/[id]/exportar/route.ts` (novo) e `src/app/admin/(painel)/eventos/[id]/page.tsx`.
|
||
|
||
---
|
||
|
||
### 12.8 Projeto renomeado e documentação
|
||
|
||
- **Novo nome:** "Convite Safari" passou a se chamar **Convites Digitais** no `README.md`, no `instrucoes.md` e na página inicial.
|
||
- **Documentação atualizada:** o README e o `instrucoes.md`, que na época também trazia o manual de uso (hoje no `MANUAL-USUARIO.md`, ver 14.2), passaram a descrever o que já existia e não estava documentado (temas, prazo de confirmação, convidados adolescentes, textos de selo e rodapé) e as novidades desta versão.
|
||
- **Mantido:** o tema padrão no banco continua `safari`, só para eventos antigos criados antes da escolha de tema. O tema **Safári** continua disponível normalmente.
|
||
|
||
---
|
||
|
||
### 12.9 Roteiro de atualização desta versão
|
||
|
||
1. **Backup do banco** (esta versão cria uma tabela nova):
|
||
|
||
```bash
|
||
docker compose exec -T db sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > backup-$(date +%F).sql
|
||
```
|
||
|
||
2. **Copie o código novo** para o servidor (mesmo `rsync` da seção 3).
|
||
3. **Adicione `APP_URL` ao `.env`** do servidor (ver 12.5):
|
||
|
||
```dotenv
|
||
APP_URL=http://192.168.18.97:3000
|
||
```
|
||
|
||
4. **Reconstrua e suba o app:**
|
||
|
||
```bash
|
||
docker compose up -d --build app
|
||
```
|
||
|
||
5. **Confira os logs** (`docker compose logs -f app`): deve aparecer `Migrations aplicadas.` e nenhum aviso de `APP_URL inválida`.
|
||
6. **Valide:**
|
||
- abra `/admin`: a tela de login deve estar no visual neutro; entre com a senha (as sessões antigas foram encerradas);
|
||
- abra um evento e baixe um CSV em **Exportar lista (CSV)**;
|
||
- cole o link de um convite **ainda não compartilhado** no WhatsApp e confira o card com imagem.
|
||
7. **Quando publicar no Cloudflare**, além de `COOKIE_SECURE=true`, troque `APP_URL` para `https://<seu-domínio>` e rode `docker compose up -d app`.
|
||
|
||
---
|
||
|
||
## 13. Versão de 06/10/2026 — PDFs dos convites
|
||
|
||
### 13.1 Resumo
|
||
|
||
| # | Alteração | Para quem muda algo | Exige ação no deploy? |
|
||
| --- | --- | --- | --- |
|
||
| 1 | PDF modelo por evento (upload, substituição, download) | Organizador | Não |
|
||
| 2 | Geração do PDF personalizado por convite e em lote, com progresso | Organizador | Não |
|
||
| 3 | Novo container `pdf-service` (Python) e volume `app-storage` | Quem faz o deploy | Sim: build do novo serviço |
|
||
| 4 | Novas variáveis opcionais `INVITE_LINK_BASE_URL` (padrão: `APP_URL`) e `INVITE_LINK_PATTERN` | Quem faz o deploy | Só conferir se `APP_URL` está correto |
|
||
| 5 | Migration `0005_pdf_generation` (colunas de PDF e tabela `pdf_jobs`) | — | Automática na subida |
|
||
|
||
Os passos para atualizar o servidor estão na [seção 13.3](#133-roteiro-de-atualização-desta-versão).
|
||
|
||
---
|
||
|
||
### 13.2 O que mudou
|
||
|
||
- **Para o organizador:**
|
||
- seção **PDF do convite** na página do evento;
|
||
- status e botões de PDF em cada convite;
|
||
- geração em lote com progresso.
|
||
|
||
Uso detalhado na [seção 2.11 do MANUAL-USUARIO.md](MANUAL-USUARIO.md#211-pdfs-dos-convites).
|
||
- **No servidor:**
|
||
- container novo `pdf-service` (Python), sem porta publicada;
|
||
- volume novo `app-storage` com os PDFs, montado em `/storage` no `app` e no `pdf-service`;
|
||
- migration `0005_pdf_generation`, aplicada na subida.
|
||
- **Variáveis:** `INVITE_LINK_BASE_URL` (opcional; em branco usa o `APP_URL`) e `INVITE_LINK_PATTERN` (padrão `com.br/r/`). Ver [seção 4](#4-configurar-as-variáveis-de-ambiente).
|
||
- **Segurança:**
|
||
- downloads e ações exigem o organizador logado;
|
||
- o convite precisa pertencer ao evento da URL;
|
||
- o navegador nunca envia caminhos de arquivo;
|
||
- o upload aceita só PDF de até 10 MB.
|
||
|
||
Arquitetura, banco, endpoints e manutenção do código estão no [README.md](README.md#pdfs-dos-convites).
|
||
|
||
**Arquivos:**
|
||
|
||
- **Novos:**
|
||
- `pdf-service/` (Dockerfile, `requirements.txt`, `app/main.py`);
|
||
- `src/lib/pdf/` (`storage.ts`, `service.ts`, `jobs.ts`, `state.ts`, `types.ts`);
|
||
- `src/app/admin/pdf-actions.ts`;
|
||
- `src/components/admin/EventPdf.tsx`;
|
||
- `src/instrumentation.ts`;
|
||
- `src/lib/slugify.ts`;
|
||
- rotas em `src/app/admin/(painel)/eventos/[id]/pdf/` e `.../convites/[conviteId]/pdf/`;
|
||
- `drizzle/0005_pdf_generation.sql`.
|
||
- **Alterados:**
|
||
- `src/db/schema.ts`;
|
||
- `src/app/admin/actions.ts` (apaga os PDFs ao excluir evento ou convite);
|
||
- página do evento;
|
||
- `next.config.ts` (limite de upload de 11 MB nas Server Actions);
|
||
- `Dockerfile`, `compose.yml`, `.env.example`, `.dockerignore` e `.gitignore`.
|
||
|
||
---
|
||
|
||
### 13.3 Roteiro de atualização desta versão
|
||
|
||
1. **Backup do banco** (esta versão altera tabelas):
|
||
|
||
```bash
|
||
docker compose exec -T db sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > backup-$(date +%F).sql
|
||
```
|
||
|
||
2. **Copie o código novo** para o servidor (mesmo `rsync` da seção 3). Confira que a pasta `pdf-service/` foi copiada.
|
||
3. **Confira o `.env`** do servidor: o `APP_URL` deve ser o endereço público (`https://convite.n1b3.com.br`), porque os PDFs usam esse endereço no link. As variáveis novas são opcionais:
|
||
|
||
```dotenv
|
||
# INVITE_LINK_BASE_URL=https://convite.n1b3.com.br # só se o link do PDF tiver de ser diferente do APP_URL
|
||
INVITE_LINK_PATTERN=com.br/r/ # padrão; pode omitir
|
||
```
|
||
|
||
4. **Construa e suba tudo** (o `pdf-service` é novo):
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
```
|
||
|
||
5. **Confira:** `docker compose ps` deve mostrar o `pdf-service` como `healthy`, e os logs do app devem mostrar `Migrations aplicadas.`.
|
||
6. **Valide:**
|
||
- abra um evento e envie o PDF modelo em **PDF do convite**;
|
||
- gere o PDF de um convite e baixe o arquivo;
|
||
- abra o PDF e clique no link: deve abrir `<APP_URL>/r/<token do convite>`;
|
||
- clique em **Gerar PDFs de todos os convites** e acompanhe o progresso;
|
||
- rode `docker compose restart` e confira que os PDFs continuam disponíveis para download.
|
||
|
||
---
|
||
|
||
## 14. Versão de 07/10/2026 — Mensagem do WhatsApp personalizada
|
||
|
||
### 14.1 Resumo
|
||
|
||
| # | Alteração | Para quem muda algo | Exige ação no deploy? |
|
||
| --- | --- | --- | --- |
|
||
| 1 | Campo **Mensagem do WhatsApp** no evento, com variáveis e formatação do WhatsApp | Organizador | Não |
|
||
| 2 | Pré-visualização da mensagem no formulário do evento | Organizador | Não |
|
||
| 3 | Migration `0006_event_whatsapp_message` (coluna `whatsapp_message` em `events`) | — | Automática na subida |
|
||
| 4 | Manual de uso separado em `MANUAL-USUARIO.md` | Organizador e administrador | Não |
|
||
|
||
Eventos já existentes ficam com o campo vazio e passam a usar a **nova mensagem padrão** (ver 14.2) a partir dos próximos cliques no botão. Mensagens já enviadas não mudam.
|
||
|
||
---
|
||
|
||
### 14.2 O que mudou
|
||
|
||
**Antes:** a mensagem do botão **WhatsApp** era fixa no código: "Olá, *Família*! Vocês estão convidados para o *festa* *ícone*", seguida do link.
|
||
|
||
**Agora:**
|
||
|
||
- O formulário do evento tem o campo **Mensagem do WhatsApp**, com:
|
||
- botões para inserir as variáveis `{nome}`, `{evento}`, `{data}`, `{hora}`, `{local}`, `{endereco}`, `{icone}` e `{link}`;
|
||
- uma linha de ajuda com a formatação do WhatsApp;
|
||
- três contadores: **Modelo**, **Texto final (exemplo)** e **Link do WhatsApp (exemplo)**;
|
||
- uma **pré-visualização** no estilo do WhatsApp, que aplica negrito, itálico, riscado, monoespaçado e código. Em navegadores sem suporte (Safari < 16.4), a prévia aparece sem formatação, sem quebrar o formulário.
|
||
- O botão **WhatsApp** de cada convite monta o texto a partir do modelo do evento.
|
||
- Botão novo **Copiar mensagem** em cada convite: copia o mesmo texto do botão WhatsApp, para enviar por outro aplicativo.
|
||
- **Nova mensagem padrão**, usada com o campo vazio:
|
||
|
||
```
|
||
Olá, {nome}! Chegou o seu convite para: *{evento}* {icone}
|
||
Confirme a presença: {link}
|
||
```
|
||
|
||
A anterior ("…convidados para **o** *festa*") só combinava com nomes de festa masculinos no singular e com convites de família: "para o Festa da Maria" e "Vocês" num convite de uma pessoa só. A nova não depende do nome da festa nem de quantas pessoas há no convite.
|
||
- **Outros textos com a mesma correção:**
|
||
|
||
| Onde | Antes | Agora |
|
||
| --- | --- | --- |
|
||
| Título do card do link no WhatsApp | "*Família*, você está convidado!" | "*Família*, chegou o seu convite!" |
|
||
| Imagem do card | "*Família*, você está convidado para" | "*Família*, chegou o seu convite para" |
|
||
| Topo da página do convite (e prévia do tema no formulário do evento) | "esperamos vocês para celebrar!" | "estamos esperando por você(s)!" |
|
||
|
||
Links já compartilhados podem continuar mostrando o card antigo por um tempo, porque o WhatsApp guarda o card (ver 12.6).
|
||
- Sem `{link}` na mensagem, o link é adicionado no final.
|
||
- **Limite de 950 caracteres**, com margem abaixo de 1000 para o link do `wa.me`. Ele vale para o modelo e também para o texto final, já com as variáveis trocadas.
|
||
- O texto final é conferido no servidor ao criar ou editar o evento, com os nomes de todos os convites, e ao criar ou editar um convite.
|
||
- Nessa conferência, o link é estimado com o `APP_URL` e um código de 8 caracteres.
|
||
- A mensagem padrão (campo vazio) não é limitada.
|
||
- **Limite de 2300 caracteres no link codificado** (`https://wa.me/?text=...`), conferido nos mesmos momentos. Ele existe porque, no link, acentos ocupam 6 caracteres e emojis até 12. O WhatsApp não publica um limite oficial; os dois valores são margens de segurança.
|
||
- **Mensagem por evento:** não há mensagem diferente por convite. Para personalizar o texto de uma família, o organizador edita a mensagem no próprio WhatsApp antes de enviar.
|
||
|
||
Uso detalhado na [seção 2.5.1 do MANUAL-USUARIO.md](MANUAL-USUARIO.md#251-personalizar-a-mensagem-do-whatsapp).
|
||
|
||
**Arquivos:**
|
||
|
||
- **Novos:**
|
||
- `src/lib/whatsapp-message.ts` (mensagem padrão, variáveis, limite e montagem do texto);
|
||
- `src/components/admin/WhatsAppMessageField.tsx` (campo, botões de variáveis e pré-visualização);
|
||
- `drizzle/0006_event_whatsapp_message.sql`.
|
||
- **Alterados:**
|
||
- `src/db/schema.ts` (coluna `whatsappMessage`);
|
||
- `src/app/admin/actions.ts` (leitura e validação do campo; conferência do texto final ao salvar evento e convite);
|
||
- `src/components/admin/EventForm.tsx` (campo novo; data, horário, local e endereço alimentam a pré-visualização);
|
||
- `src/components/admin/ShareInvitation.tsx` (usa o modelo do evento; botão Copiar mensagem);
|
||
- `src/app/r/[token]/page.tsx`, `src/app/r/[token]/opengraph-image.tsx` e `src/components/invitation/InvitationHero.tsx` (frases neutras);
|
||
- página do evento e página de edição do evento.
|
||
|
||
**Documentação:**
|
||
|
||
- O manual de uso, que era a antiga Parte 2 deste arquivo, foi movido para o [MANUAL-USUARIO.md](MANUAL-USUARIO.md). Ele foi reescrito sem termos técnicos e ganhou a seção **"Quando falar com o administrador"**.
|
||
- Este arquivo ficou só com o deploy (Parte 1) e o histórico, que passou a ser a Parte 2. As seções do histórico foram renumeradas de 16–18 para 12–14.
|
||
|
||
---
|
||
|
||
### 14.3 Roteiro de atualização desta versão
|
||
|
||
1. **Backup do banco** (esta versão adiciona uma coluna):
|
||
|
||
```bash
|
||
docker compose exec -T db sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' > backup-$(date +%F).sql
|
||
```
|
||
|
||
2. **Copie o código novo** para o servidor (mesmo `rsync` da seção 3).
|
||
3. **Reconstrua e suba o app:**
|
||
|
||
```bash
|
||
docker compose up -d --build app
|
||
```
|
||
|
||
4. **Confira os logs** (`docker compose logs -f app`): deve aparecer `Migrations aplicadas.`.
|
||
5. **Valide:**
|
||
- em um evento existente com o campo vazio, clique em **WhatsApp** num convite: deve aparecer a nova mensagem padrão ("Chegou o seu convite para: …");
|
||
- em **Editar evento**, escreva uma mensagem com `*negrito*` e `{nome}` e confira a pré-visualização;
|
||
- salve, clique em **WhatsApp** e confira no WhatsApp o nome da família, a formatação e o link.
|