Calendário Mineral
Um calendário público com os prazos regulatórios e os eventos do setor mineral no Brasil. Dá para navegar por mês, abrir um evento, salvar no Google Agenda ou assinar um resumo mensal por e-mail.
Escrevi 240 dos 241 commits do repositório, entre back-end, fluxo de e-mail e front-end.

O problema
Empresas de mineração no Brasil convivem com prazos regulatórios recorrentes: entregas para a ANM como RAL, DIPEM e DIEF-CFEM, relatórios ambientais para o IBAMA como o RAPP, além dos congressos e feiras do setor. Cada prazo vem de um órgão diferente. A ÍGNEA queria reunir essas datas em um calendário público, em que alguém da equipe publicasse os eventos e os assinantes recebessem um resumo mensal por e-mail.
Decisões
Um resumo por mês, garantido pelo banco
A tabela de resumos tem a coluna do mês como única, e o job procura a linha daquele mês antes de mandar qualquer coisa. O reenvio trava a linha com SELECT FOR UPDATE e recusa resumos já marcados como enviados, então uma segunda execução no mesmo mês ou dois cliques seguidos em reenviar não disparam o e-mail duas vezes.
Validar antes de enviar e avisar quando falha
O HTML do e-mail e cada URL de imagem são verificados antes do primeiro lote sair. Se algo falha, o rascunho é salvo com os erros e o admin recebe um alerta por e-mail. Em fevereiro de 2026 essa checagem barrou o resumo do mês por regras do validador que não fazem sentido em e-mail, e a falha nem pôde ser registrada porque a coluna de status não aceitava esse valor. Desliguei essas regras com um comentário explicando cada uma, corrigi a restrição de status em uma migração e mantive a checagem.
Enviar antes do mês começar
No começo o resumo saía na primeira segunda-feira de cada mês com os eventos daquele mês. Em abril de 2026 mudei para a última segunda com os eventos do mês seguinte, para que os prazos dos primeiros dias do mês cheguem ao leitor antes de o mês começar.
O que eu fiz
- O calendário público em React: liguei a interface à API, criei deep links por evento com React Router, uma visão mensal para celular com react-swipeable, rotas com lazy loading e troquei o Tailwind CSS carregado por CDN por um build no Vite.
- O back-end em Express e PostgreSQL: CRUD de eventos e categorias, login com JWT, senhas com bcrypt e checagem de papel, upload de imagens com Multer e migrações SQL para cada mudança de esquema (campo de órgão responsável, link para o blog, status do resumo).
- O fluxo do resumo por e-mail: agendamento com node-cron, template HTML, validação com html-validate ajustada para e-mail, envio pela API da Brevo em lotes de 99 com os destinatários em cópia oculta e backoff exponencial com até três tentativas por lote em caso de limite de taxa, erro de servidor ou falha de rede.
- Uma área administrativa atrás de uma rota /admin escondida, com edição de eventos, prévia do resumo de qualquer mês, envio de teste para um endereço, histórico de envios e um botão para reenviar resumos que falharam ou saíram pela metade.
- Links compartilháveis de eventos: um middleware no Express que detecta robôs com isbot e injeta as tags Open Graph e Twitter Card, com imagens de prévia de 1200x630 geradas pelo Sharp.
- Um tratamento de imagens com Sharp que reduz cada upload para no máximo 1920 px de largura, sem ampliar, converte para WebP e apaga o arquivo original.
Como funciona
O front-end é uma aplicação React 19 de página única, feita com Vite. O visitante navega pelos eventos por mês, filtra por grupo, faz busca e abre um evento em um modal ou em uma página própria em /events/:id. No celular, o mês muda com um deslize do dedo. A interface está em português e inglês, e cada evento guarda título e descrição nos dois idiomas.
Uma API em Express com TypeScript entrega os dados públicos dos eventos e uma área administrativa protegida. Eventos, categorias, usuários admin e o registro dos resumos ficam no PostgreSQL, e cada mudança de esquema vira uma migração SQL numerada. Quando alguém compartilha o link de um evento, o Express olha o user agent: robôs de redes sociais recebem um HTML com as tags Open Graph e uma imagem de prévia, e o resto dos visitantes recebe o app normal. O formulário de inscrição cadastra o e-mail como contato na Brevo, onde fica a lista de assinantes.
Um job com node-cron roda toda segunda-feira às 11h30, no horário de São Paulo. Na última segunda do mês, ele reúne os eventos publicados do mês seguinte, cancela o envio se não houver nenhum, monta o e-mail do resumo, valida o HTML e as URLs das imagens, busca a lista de assinantes na Brevo e envia o e-mail pela API da Brevo. Cada execução fica registrada em uma tabela de resumos com o status (enviado, parcial ou com falha), e a área admin usa essa tabela para mostrar a prévia, mandar teste, reenviar e consultar os envios anteriores.



Tecnologias
- Front-end
- React 19, TypeScript, Vite, React Router 7, Tailwind CSS 4, react-swipeable, react-hot-toast
- Back-end
- Node.js, Express, TypeScript (tsx), JWT, bcrypt, Multer, Sharp, isbot
- Dados
- PostgreSQL (pg), migrações SQL
- E-mail e agendamento
- API da Brevo, node-cron, html-validate, exponential-backoff
- Infraestrutura
- AWS Lightsail (Ubuntu), PM2