Files
convite-rael/instrucoes.md

763 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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