Skip to content
אצלי
Go back

ClimaBR.app - Painel ambiental por cidade com qualidade do ar, água, clima e alertas

ClimaBR.app - Painel ambiental por cidade com qualidade do ar, água, clima e alertas

Estava buscando projetos sobre a qualidade do ar, encontrei 3 interessantes: O painel do https://waqi.info/ que além de dados de orgãos públicos, também oferece dados de qualidade do ar de diversas fontes como o https://sensor.community e o https://www.habitatmap.org/

O https://aqicn.org/ (com sensores GAIA https://aqicn.org/gaia/list/pt/ partindo de R$271) e em https://www.aqi.in/ onde enviam um sensor com uma contribuição acima de $110.

E nisto há dois pontos diferentes: enquanto no habitatmap e aircasting.org usam dados do AirBeam (precisa adquirir o device, já pronto por $99),
Com o sensor.community é necessário comprar/instalar o hardware e configurar o firmware, DIY e opensource - via https://sensor.community/pt/sensors/airrohr/ - sinceramente, este será meu proximo projeto de fim de semana!

Adquiri 2 sensores de qualidade do ar para minha rede local (wifi, compativel com Tuya). Não possuem o mesmo objetivo que os acima.

Outro ponto: uso um app de clima, mesmo com adblock na rede (DNS com NextDNS) ele força a exibição de ads e o uso de trackers, além disto, os sites existentes estão em níveis insuportáveis.

Qual a dificuldade em ter um site de clima e outros dados ambientais sem gerar custo, ou qual o custo minimo para ter algo superior?

O que é o ClimaBR.app

O ClimaBR.app é um painel ambiental por município brasileiro. Para qualquer uma das 5.571 cidades do país ele reúne, a partir de dados públicos, a previsão do tempo, a qualidade do ar, o índice UV, o vento, o nível de reservatórios, focos de queimada e o alerta de dengue, zika e chikungunya.

A premissa de projeto é simples e rígida: consolidar dado aberto, ser acessível por máquina (AI-first) e rodar com custo zero, inteiramente dentro do free tier dos serviços envolvidos. Tudo o que descrevo abaixo cabe nos limites gratuitos, e este texto mostra exatamente quanto de cada limite o projeto consome hoje.

Um detalhe que define a personalidade do projeto: ele responde no terminal.

https://climabr.app/curl/

climabr - curl

Também é possível conectar via MCP https://climabr.app/.well-known/mcp.json

Além de uma pagina para paineis e outra mobile. O objetivo é seguir com extensão no navegador e tornar o ClimaBR.app uma ferramenta completa para monitoramento ambiental até onde for possível num nível gratuito e aberto.

Arquitetura em uma imagem

GitHub (repositório + Actions)
  ├── Actions (cron) rodam scrapers em Python  → JSON por município (commit no repo)
  └── Actions fazem o build do Astro           → deploy no Cloudflare Pages

Cloudflare (tudo no free tier)
  ├── Pages    → site estático (HTML/CSS/JS gerado pelo Astro)
  ├── Workers  → detecção de curl, formatos SVG/PNG/Prometheus, geolocalização por IP
  └── DNS/SSL/WAF/Analytics → camada de borda

Navegador do usuário
  └── hidrata o tempo atual e a previsão direto do Open-Meteo (sem passar por mim)

A ideia central: o conteúdo é gerado estaticamente e o que é volátil (tempo agora, previsão) é atualizado no cliente, direto na fonte.

A stack de coding

Frontend: Astro em modo estático

O site é um Astro 6.4 em SSG (Static Site Generation). No build ele gera 11.172 páginas HTML (uma por município, mais o “Modo Painel” de cada cidade, mais as páginas de estado, a home e o “Modo Mobile”) e 5.598 endpoints JSON em /api/{uf}/{cidade}.json. No total são cerca de 16.792 arquivos, ~218 MB. Esse número de arquivos importa: como vou contar adiante, ele esbarra num limite concreto do Cloudflare Pages.

A camada visual usa:

O build inteiro das 11 mil páginas leva cerca de 30 segundos.

Worker

Na frente do site roda um Cloudflare Worker em TypeScript, na rota climabr.app/*. Ele inspeciona cada requisição e decide o formato da resposta:

O PNG é gerado dentro do próprio Worker com resvg compilado para WebAssembly (@resvg/resvg-wasm), que rasteriza o SVG do card. Isso evita qualquer serviço externo de imagem.

Coleta de dados: Python + GitHub Actions

Os dados vêm de scrapers em Python puro (sem dependências externas, só a biblioteca padrão), um por fonte: scrape-openmeteo.py, scrape-sabesp.py, scrape-copasa.py, scrape-dengue.py, scrape-ana.py, scrape-inpe.py, scrape-inmet.py e scrape-ondas.py. Cada um escreve em data/cidades/{uf}/{slug}.json.

Eles são disparados por cron no GitHub Actions:

Os dados ficam versionados no próprio repositório (cerca de 22 MB de JSON), e o deploy reconstrói o site com o snapshot mais recente.

As fontes de dados (todas públicas e gratuitas)

Hidratação: estático no servidor, ao vivo no cliente

A página de cada cidade chega com um snapshot dos dados gerado no build. No navegador, um pequeno script busca a previsão e o tempo atual direto do Open-Meteo, usando o lat/lon embutido na página, e sobrescreve só os blocos voláteis. Em caso de falha, o snapshot do build permanece.

Esse desenho tem um efeito interessante de custo: a chamada ao Open-Meteo na navegação sai do navegador do usuário direto para a API, sem passar pela minha infraestrutura. Cada visitante usa a própria “cota” de rede. O Worker só consulta o Open-Meteo quando precisa montar os cards SVG/PNG, e ainda assim com cache de borda.

Duas telas de relance: Modo Painel e Modo Mobile

Além da página completa, cada cidade tem duas visões pensadas para olhar de relance, sem rolagem, cabendo numa única tela:

O Modo Mobile rendeu uma boa lição de arquitetura. A intenção inicial era pré-renderizar uma página por cidade, como o Modo Painel. Só que isso somaria mais 5.571 arquivos ao build e estouraria um limite do Cloudflare Pages (detalhado adiante). A solução foi inverter o desenho: o Modo Mobile é uma única página que lê a cidade da URL (?c=uf/slug), busca o JSON em /api e monta os tiles no próprio navegador, mesclando os dados ao vivo do Open-Meteo. Resultado: a feature inteira custou um arquivo em vez de 5.571.

Free tier: o que cada serviço oferece e quanto eu uso

Para cada serviço, o limite gratuito e o consumo atual do projeto.

Cloudflare Pages

Cloudflare Workers

Cloudflare R2

Outros recursos Cloudflare (todos no free)

GitHub

APIs de dados

Armadilhas que aprendi no caminho

Alguns problemas reais que apareceram e que valem como aprendizado:

1. Content Security Policy bloqueando silenciosamente. A CSP estrita do site bloqueou, em dois momentos diferentes, o fetch da hidratação ao Open-Meteo e depois o beacon do Web Analytics. O sintoma é traiçoeiro: nenhum erro visível, a feature simplesmente não funciona. A lição: toda vez que se adiciona algo que carrega script externo ou faz fetch externo, é preciso atualizar a allowlist da CSP (script-src/connect-src), e validar em um navegador real.

2. resvg sem fonte gera PNG em branco. O resvg em WebAssembly não tem fontes de sistema. Sem carregar um buffer de fonte, ele renderiza só as formas vetoriais e nenhum texto, ou seja, o card PNG saía com o layout e sem dado nenhum. A correção foi embutir um subset mínimo de fonte (cerca de 28 KB gzip, respeitando o limite de 1 MiB do Worker) e, como o resvg também não renderiza emoji colorido, trocar os ícones por formas vetoriais no PNG.

3. Injeção automática de analytics não funciona atrás de um Worker. O beacon que a Cloudflare injeta sozinho no HTML é instável quando a resposta é servida por um Worker. A solução foi injetar o beacon manualmente no layout do Astro.

4. Card com dado estático enquanto a página estava ao vivo. Os cards SVG/PNG eram montados só a partir do JSON estático, então ficavam atrasados em relação à página, que hidrata ao vivo. Passei a buscar o Open-Meteo também no Worker ao montar o card, com cache curto.

5. Um marcador de “já processado” que congelou os dados. Para respeitar o rate limit, a coleta do Open-Meteo pulava cidades já processadas, checando a presença de um campo. Só que o campo era permanente: depois da primeira coleta, todas as cidades passavam a ser puladas para sempre, e a previsão congelou numa data. A correção foi trocar o critério por data de coleta (recolhe se passou de N horas), e não pela mera existência do campo. De quebra, ao tentar acelerar com lotes de 100 coordenadas, estourei o limite de 600 chamadas/minuto e tomei uma enxurrada de erros 429: o ritmo certo é tão importante quanto a lógica.

6. CSS escopado do Astro versus HTML gerado por JavaScript. O Astro, por padrão, escopa o CSS de cada componente (adiciona um atributo aos elementos e restringe os seletores a ele). O Modo Mobile monta os tiles via innerHTML no cliente, em runtime, e esses elementos não recebem o atributo de escopo, então o CSS simplesmente não se aplicava: a tela vinha com as informações sobrepostas, sem as caixas. A correção foi marcar o estilo como global (<style is:global>). A lição mais ampla: o que é renderizado no cliente vive fora do sistema de escopo do framework.

Custo total

Zero de operação. O único gasto recorrente é a renovação anual do domínio .app. Todo o resto, hospedagem, edge computing, coleta de dados, analytics e segurança, cabe nos planos gratuitos descritos acima.

Encerramento

O ClimaBR.app mostra que dá para entregar um serviço público útil, com cobertura nacional e várias fontes de dado, sem custo de infraestrutura, desde que o desenho respeite os limites gratuitos desde o início: estático onde der, edge para o que diferencia, coleta agendada e dado aberto. O código é open source sob AGPL-3.0.


Share this post on:

Next Post
Pedágio numa estrada que virou de graça: o lock-in das IDEs de IA