versao com plano de fundo nao testado

This commit is contained in:
2026-10-20 23:08:47 -03:00
parent fd86667c4e
commit 2df0be7b6b
53 changed files with 2795 additions and 1270 deletions

View File

@@ -1,9 +1,13 @@
# Convite Safari 🦁 — Manual de Instruções
# Convites Digitais — Manual de Instruções
Este documento reúne **tudo o que é preciso para instalar, publicar e usar** o Convite Safari.
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:** para quem vai instalar e manter a aplicação no servidor.
- **Parte 2 — Uso:** para o organizador da festa e para os convidados.
- **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).
---
@@ -23,12 +27,11 @@ Este documento reúne **tudo o que é preciso para instalar, publicar e usar** 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 — Uso**
**Parte 2 — Histórico de alterações**
12. [Conceitos](#12-conceitos)
13. [Área do organizador](#13-área-do-organizador)
14. [Experiência do convidado](#14-experiência-do-convidado)
15. [Perguntas frequentes](#15-perguntas-frequentes)
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)
---
@@ -36,24 +39,31 @@ Este documento reúne **tudo o que é preciso para instalar, publicar e usar** o
## 1. Visão geral da arquitetura
A aplicação roda em **dois containers** gerenciados pelo Docker Compose:
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
│ └─ 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 **não fica exposto** para fora do servidor; só o container `app` o acessa.
- 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.
@@ -102,6 +112,7 @@ postcss.config.mjs
drizzle.config.ts
drizzle/ (migrations SQL)
scripts/ (script de migration)
pdf-service/ (serviço Python de PDFs)
src/
public/
```
@@ -127,12 +138,17 @@ nano .env
| `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 de 32+ caracteres para assinar a sessão |
| `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.
\* 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`
@@ -158,12 +174,17 @@ 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`.
---
@@ -176,8 +197,8 @@ docker compose up -d --build
O que acontece, em ordem:
1. A imagem do `app` é construída (pode levar alguns minutos na primeira vez).
2. O Postgres sobe; o `app` aguarda o banco ficar saudável.
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`.
@@ -185,7 +206,7 @@ O que acontece, em ordem:
## 6. Verificar se está tudo funcionando
**Status dos containers** — ambos devem estar `running` (e o `db` como `healthy`):
**Status dos containers** — os três devem estar `running` (e `db` e `pdf-service` como `healthy`):
```bash
docker compose ps
@@ -226,6 +247,7 @@ Quando o Cloudflare for apontar para a aplicação (mesmo padrão das outras apl
```dotenv
COOKIE_SECURE=true
APP_URL=https://<seu-domínio>
```
3. Recrie o container do app:
@@ -253,6 +275,7 @@ Os links de convite são gerados a partir do endereço em que o organizador est
```
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.
@@ -276,6 +299,22 @@ Gera um arquivo como `backup-2026-11-10.sql` na pasta atual. Guarde uma cópia *
(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:
@@ -288,7 +327,7 @@ docker compose exec -T db sh -c 'psql -U "$POSTGRES_USER" "$POSTGRES_DB"' < back
docker compose up -d app
```
> ⚠️ **Nunca** use `docker compose down -v` sem querer apagar o banco — o `-v` remove o volume `pgdata`.
> ⚠️ **Nunca** use `docker compose down -v` sem querer apagar o banco — o `-v` remove os volumes `pgdata` e `app-storage` (PDFs).
---
@@ -299,12 +338,14 @@ docker compose up -d app
| 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 | Troque o `SESSION_SECRET` 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.
@@ -320,186 +361,402 @@ Os containers estão com `restart: unless-stopped`: voltam sozinhos se o servido
| 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
# Parte 2 — Uso
## 12. Conceitos
| Termo | Significado |
| Onde olhar | Comando |
| --- | --- |
| **Evento** | A festa: nome, data, horário, local e endereço. Ex.: "1º Aniversário do Arthur" |
| **Convite** | Um link enviado para uma família/grupo. Tem um **nome exibido** (ex.: "Família Silva") e uma lista de convidados |
| **Convidado** | Cada pessoa do convite, marcada como **Adulto** ou **Criança** |
| **Token** | Código único no final do link (`/r/Ab3xY9kL`). Funciona como a "chave" do convite |
| **RSVP / Status** | Resposta de cada convidado: ✓ **Presente**, ✗ **Ausente** ou ⏳ **Pendente** |
| 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` |
Um evento pode ter vários convites; cada convite pode ter vários convidados.
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).
---
## 13. Área do organizador
# Parte 2 — Histórico de alterações
### 13.1 Entrar
## 12. Versão de 05/10/2026
1. Acesse `https://<seu-domínio>/admin` (ou `http://192.168.18.97:3000/admin` antes da publicação). Também há o link discreto **"Área do organizador"** no rodapé da página inicial.
2. Digite a senha do organizador e clique em **Entrar**.
### 12.1 Resumo
A sessão dura **7 dias** no mesmo navegador. Para sair, clique em **Sair** no topo da página.
| # | 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 |
### 13.2 Painel de eventos
A tela inicial do organizador lista todos os eventos com:
- data, horário e local;
- quantidade de convites;
- resumo de presença: **Presentes** (com divisão adultos/crianças), **Ausentes** e **Pendentes**.
Clique em um evento para abrir os detalhes.
### 13.3 Criar um evento
1. No painel, clique em **+ Novo evento**.
2. Preencha:
- **Nome da festa** — ex.: "1º Aniversário do Arthur"
- **Data** e **Horário**
- **Local** — ex.: "Espaço Festa"
- **Endereço** — ex.: "Rua das Flores, 100 - Belo Horizonte/MG"
3. Clique em **Criar evento**. Você será levado à página do evento.
### 13.4 Criar um convite
1. Na página do evento, clique em **+ Novo convite**.
2. Preencha o **Nome exibido no convite** — é o título grande que o convidado vê. Ex.: "Família Silva", "Tio João e Tia Ana".
3. Adicione os convidados:
- digite o nome na primeira linha e escolha **Adulto** ou **Criança**;
- use **+ Adulto** / **+ Criança** para adicionar mais linhas;
- use **✕** para remover uma linha.
Linhas com o nome em branco são ignoradas.
4. Clique em **Criar convite**.
O link do convite é gerado automaticamente.
> 💡 Dica: crie **um convite por família/núcleo**. Assim cada família responde por todos os seus membros de uma vez.
### 13.5 Enviar o convite
Em cada convite, na página do evento:
- **Copiar link** — copia o endereço completo (ex.: `https://seu-dominio/r/Ab3xY9kL`) para colar onde quiser.
- **WhatsApp** — abre o WhatsApp com uma mensagem pronta contendo o nome da família, o nome da festa e o link. Basta escolher o contato e enviar.
- **Abrir convite** — abre o convite em outra aba, exatamente como o convidado verá.
> 🔒 Cada link dá acesso àquele convite e permite responder por ele. **Envie apenas para a família correspondente** e não publique em grupos abertos ou redes sociais.
### 13.6 Acompanhar as respostas
Na página do evento:
- O **Resumo de presença** mostra o total de presentes (adultos e crianças), ausentes e pendentes de todo o evento — útil para fechar números com buffet e decoração.
- Cada convite mostra seus convidados com o status colorido:
- 🟢 **✓ Presente**
- 🔴 **✗ Ausente**
- 🟡 **⏳ Pendente**
- Quando a família responde, aparece **"respondeu em dd/mm/aaaa hh:mm"** abaixo do nome.
Recarregue a página para ver respostas novas.
### 13.7 Editar um convite
1. No convite, clique em **Editar**.
2. Altere o nome exibido, renomeie convidados, troque o tipo, adicione ou remova pessoas.
3. Clique em **Salvar alterações**.
O link **não muda** ao editar. O status de cada convidado aparece ao lado do nome durante a edição.
> ⚠️ Remover um convidado apaga também a resposta dele. Convidados mantidos ou apenas renomeados **preservam** a resposta.
### 13.8 Zerar as respostas de um convite
Se uma família respondeu por engano e quer começar de novo:
1. No convite, clique em **Zerar respostas** (só aparece se já houve resposta).
2. Confirme.
Todos os convidados daquele convite voltam para **Pendente**. O link continua o mesmo.
### 13.9 Excluir um convite
1. No convite, clique em **Excluir** e confirme.
O link deixa de funcionar imediatamente (o convidado verá "Convite não encontrado"). **Não é possível desfazer.**
### 13.10 Editar ou excluir um evento
1. Na página do evento, clique em **Editar evento**.
2. Altere os dados e clique em **Salvar alterações** — as mudanças aparecem na hora em todos os convites do evento.
3. Para excluir, use **Excluir evento** no final da página e confirme.
> ⚠️ Excluir um evento apaga **todos os convites e respostas** dele. Não é possível desfazer.
Os passos para atualizar o servidor estão na [seção 12.9](#129-roteiro-de-atualização-desta-versão).
---
## 14. Experiência do convidado
### 12.2 Visual neutro na área do organizador
O convidado **não precisa de cadastro nem senha** — só do link recebido.
**Antes:** login, painel e página inicial usavam as cores e o 🦁 do tema Safári, mesmo em eventos com outro tema.
### 14.1 Abrir o convite
**Agora:**
Ao abrir o link, o convidado vê:
- 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.
1. O **nome do convite** em destaque (ex.: "Família Silva — esperamos vocês para celebrar!").
2. Os **detalhes da festa**: data, horário, local e endereço.
3. A **lista de convidados** daquele convite, cada um com seu status atual.
4. O quadro de **Confirmação de Presença**, com a contagem de presentes, ausentes e pendentes.
**O que continua igual, de propósito:**
### 14.2 Responder
- 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.
1. Para cada pessoa da lista, toque em:
- **✓** para confirmar presença, ou
- **✗** para informar ausência.
Para mudar de ideia antes de enviar, basta tocar no outro botão.
2. Toque em **Confirmar Presença**.
3. Aparece a tela **"Presença Confirmada!"** com o resumo de quem vai e quem não vai. Toque em **Fechar**.
**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.
Não é obrigatório responder por todos de uma vez: quem ficar como pendente pode ser respondido depois.
---
### 14.3 Mudar a resposta
### 12.3 Limite de tentativas de login
O convidado pode abrir o **mesmo link** a qualquer momento. As respostas já enviadas aparecem marcadas; basta trocar (ex.: de ✓ para ✗) e tocar em **Confirmar Presença** novamente.
**Antes:** a senha do `/admin` podia ser testada sem limite.
### 14.4 Mensagens possíveis
**Agora:**
| Mensagem | Significado |
- 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` |
| --- | --- |
| **Convite não encontrado** | Link digitado errado, incompleto ou convite excluído pelo organizador |
| **Não foi possível enviar sua resposta. Tente novamente.** | Falha de conexão ou o convite foi alterado no meio do caminho. Recarregue a página e responda de novo |
| 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`.
---
## 15. Perguntas frequentes
### 12.6 Card de pré-visualização no WhatsApp
**O convidado precisa instalar algum aplicativo?**
Não. Funciona no navegador do celular ou do computador.
**Antes:** o link do convite colado no WhatsApp aparecia sem imagem e com um título genérico.
**Posso usar o mesmo sistema para várias festas?**
Sim. Crie um evento para cada festa; cada um tem seus próprios convites e totais.
**Agora:** aparece um card com:
**Duas pessoas da mesma família podem responder pelo mesmo link?**
Sim. Vale a última resposta enviada para cada convidado.
- **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.
**Um convidado novo apareceu depois que a família já respondeu. O que faço?**
Edite o convite e adicione a pessoa. Ela aparece como **Pendente** e as respostas anteriores dos outros são mantidas. Peça para a família abrir o link de novo e responder por ela.
**Detalhes:**
**Enviei o link errado para uma família. Como invalido?**
Exclua o convite e crie outro — o novo convite terá um link diferente.
- 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 🦁).
**Esqueci a senha do organizador.**
Quem administra o servidor consulta/altera `ORGANIZER_PASSWORD` no `.env` e recria o app (`docker compose up -d app`).
**Arquivos:** `src/app/r/[token]/opengraph-image.tsx` (novo), `src/app/r/[token]/page.tsx` e `src/components/admin/ShareInvitation.tsx`.
**Os dados ficam salvos se o servidor reiniciar?**
Sim. Os dados ficam no banco Postgres, em um volume persistente. Mesmo assim, mantenha backups (seção 9).
---
### 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.