# 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: `/r/`, 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://:`. 2. No `.env`, altere: ```dotenv COOKIE_SECURE=true APP_URL=https:// ``` 3. Recrie o container do app: ```bash docker compose up -d app ``` 4. Acesse `https:///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://` | - 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://` 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 `/r/`; - 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.