Versão 2.1 · 24 de agosto de 2026
Sistema que monitora 24 picos de surf em 8 países, identifica janelas de swell que valem uma viagem e avisa por WhatsApp quando elas aparecem — e quando somem.
É da Nivana, agência brasileira de viagens de surf fundada em 1987. Uso familiar e entre amigos, não comercial.
Este documento é a fonte da verdade sobre como o sistema pensa. O
CHANGELOG.md diz o que mudou e quando; o HANDOVER.md diz como trabalhar.
Todo dia, para cada pico, o sistema busca a previsão de 16 dias do Surfline e responde três perguntas que nenhum site de previsão responde:
- Vale a viagem? — a janela tem qualidade suficiente para justificar o voo
- Quando eu surfo? — os melhores dias corridos dentro do teto da viagem
- Até quando eu embarco? — descontando o tempo de deslocamento até o pico
O diferencial é o item 3. Um swell que pica hoje é inútil se você leva três dias para chegar. O sistema só conta os dias que você realmente alcança.
Dois produtos saem disso:
- Dashboard — o estado atual: quem vale viagem hoje, com nota, janela, data de embarque e botão de cotação. Feito para celular.
- Boletim — o que mudou: um texto diário às 9h, escrito por IA a partir dos dados, entregue por WhatsApp e Telegram. Narrativa é papel do boletim, não do dashboard. Quatro sessões, nesta ordem: BOLETIM DO DIA (o mar hoje e amanhã, mundo afora), VAI DAR ONDA (as janelas acionáveis, até dois destinos de países diferentes), SÉRIE AO FUNDO (só o que está no radar — a segunda semana, sem call) e A BOA (o destaque do dia: o maior evento da quinzena, curto ou longo, com ressalva quando ainda é radar).
<pasta local>/nivana/ ← ver HANDOVER §1 para o caminho
├── nivana-dashboard/ ← val HTTP
│ ├── README.md ← este arquivo
│ ├── CHANGELOG.md ← versões e pendências
│ ├── HANDOVER.md ← como trabalhar no projeto
│ ├── DashBoard.ts ← ponto de entrada e rotas (B maiúsculo)
│ ├── config.ts ← TODOS os números ajustáveis
│ ├── spots.ts ← lista de picos e leads
│ ├── surfline.ts ← busca de dados e cache
│ ├── score.ts ← a régua: nível do dia, janela, nota, categoria
│ ├── viagem.ts ← datas de embarque e comentário
│ ├── formato.ts ← unidades, datas, resumos
│ ├── mapa.ts ← mapa de swell em PNG (para o boletim)
│ ├── render.ts ← HTML e CSS (layout v2)
│ └── pagina.ts ← o JavaScript que roda no navegador
├── nivana-boletim/ ← val Cron
│ └── Boletim.ts
├── nivana-aquecedor/ ← val Cron
│ └── Aquecedor.ts
├── nivana-foto/ ← val HTTP, só a imagem do hero
│ └── main.ts
└── nivana-pulso/ ← val Cron, EXPERIMENTO DESCARTÁVEL
└── Pulso.ts
Cada pasta é um clone feito com o vt, a ferramenta de linha de comando do
Val Town. A pasta é o val: editar e dar vt push publica.
Os .md moram dentro de nivana-dashboard/ de propósito: sobem no vt push
junto com o código e ganham histórico de versões.
Onde mexer no dia a dia: config.ts para afinar critérios, spots.ts
para picos e leads. Os outros raramente precisam de edição.
Val Town, conta @NivanaStrikeMission. Cinco vals independentes.
cd <pasta>/nivana/nivana-dashboard
vt pull antes de começar
(edita)
vt status o que está diferente
vt push publica
O vt age sobre a pasta onde você está — confira o caminho antes. O push é
forçado e não pede confirmação; vt push --dry-run mostra o que aconteceria.
Se subir errado, o site do Val Town permite reverter para uma versão anterior;
depois é só vt pull.
URL: https://nivanastrikemission.val.run
Apenas DashBoard.ts tem o gatilho HTTP; os outros são importados por ele
por caminho relativo, e quebram se o nome mudar.
| Variável | Para quê |
|---|---|
SURFLINE_EMAIL / SURFLINE_PASSWORD | Login Premium — sem ele a previsão cai de 16 para 6 dias |
| Rota | O que devolve |
|---|---|
/ | a página |
/?json=1 | o contrato com o boletim: por pico, estado, nota, categoria, janela, dias (só a janela) e serie (os 16 dias completos) |
/?mapa=1 | o mapa de swell em PNG, que o boletim anexa à mensagem |
/?debug=1 | diagnóstico de login e amostra crua do payload do Surfline |
O cache do Surfline (CACHE_TTL_MS em surfline.ts) dura 1 hora.
| Variável | Para quê |
|---|---|
NIVANA_DASH_URL | URL do dashboard, sem barra no final |
ANTHROPIC_API_KEY | texto escrito pelo Claude (claude-opus-5); sem ela sai em lista |
TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID | Telegram, um ou mais destinos separados por vírgula |
ZAPI_INSTANCE / ZAPI_TOKEN / ZAPI_CLIENT_TOKEN / ZAPI_DESTINOS | WhatsApp direto, mensagem individual para cada número da lista |
CALLMEBOT_PHONE / CALLMEBOT_APIKEY | canal legado, opcional |
EMAIL_ON | cópia por e-mail, opcional |
O que uma rodada faz, nesta ordem:
- Busca
/?json=1e compara com o snapshot de ontem (nivana_snapshot_v1). - Grava o histórico (
nivana_hist_AAAA-MM-DD, uma chave por dia, nunca sobrescrita, com aseriecompleta de cada pico). Grava antes de escrever ou enviar: falha na IA ou no envio não custa o dia. ~25 KB/dia. - Monta o resumo para a IA: o que mudou (novo Strike, subiu, encolheu,
sumiu), o que está acontecendo agora (mesmo inalcançável — o leitor
quer saber que o Clássico está bombando hoje), a conferência: o que
o boletim de
CONFERE_DIAS(3) dias atrás previa para hoje contra o que o Surfline mostra hoje (admitir erro em público é o motor de credibilidade), os eventos regionais e os candidatos ao destaque.- Evento regional: os picos são agrupados por bacia (
BACIAnoBoletim.ts: Pacífico americano, Maldivas, Mentawai, África do Sul) e, por dia, conta-se quantos estão GOOD ou melhor.REGIONAL_MIN_PICOS(3) picos porREGIONAL_MIN_DIAS(2) dias seguidos é um evento. É o mesmo olhar do mapa de swell, que a IA não tinha: em 24/08 o Pacífico inteiro estava vermelho e o boletim destacava um pico só na Indonésia. - Candidatos ao destaque: eventos regionais e picos com janela, ranqueados por pico-dias EPIC (soma dos dias EPIC de cada pico dentro do evento ou da janela, mais o segundo swell quando houver). O primeiro é A BOA; a IA recebe a ordem pronta e não compara de cabeça.
- O evento traz o estado lido dos próprios picos: acionável (tem pico em Strike), no radar, ou alcançável sem janela — a bacia acende em dias que nenhum pico tem call porque a régua achou dias melhores à frente. Esse último é contexto para o BOLETIM DO DIA, nunca destaque.
- Evento regional: os picos são agrupados por bacia (
- A IA escreve o texto. O código emenda o que nunca pode variar: o convite ao privado com telefone do Vava, a chamada para o link e a assinatura Vá de Nivana.
- Envia para os canais configurados, com o mapa PNG onde o canal aceita:
por link (preview grande) no Telegram e embutido em base64 na Z-API,
que não conseguia buscar a URL sem
.pngpor conta própria.
Busca o /?json=1 para deixar o cache quente. Quatro vezes e não de hora em
hora porque cada aquecimento são 96 requisições ao Surfline. Sem ping às 9h
porque o boletim já aquece. Confere que vieram picos de verdade; 200 com
zero picos vira erro no log. Primeiro val a cortar se atrapalhar — desligar
não quebra nada.
Serve a foto do hero em base64 e nada mais. Existe porque o val do dashboard
tem limite de 80 mil caracteres. URL na constante HERO do DashBoard.ts.
Mede a cada hora quantas células pico×dia mudaram, para descobrir a que
horas o Surfline roda o modelo e assim cravar o horário ideal do boletim.
Deixar rodar 4–5 dias, ler o log, escolher o horário, e apagar o val e os
blobs nivana_pulso_log e nivana_pulso_prev.
Esta é a seção mais importante. Os números vivem em config.ts; a lógica em
score.ts, cujo cabeçalho repete a régua em português.
Reconstruída em 05/08/2026 com um critério de aceite: caber em três frases que se contam a um amigo. A fórmula anterior (pesos de qualidade, duração e proximidade) cresceu por acúmulo e ficou impossível de explicar.
Nada de nota por pico, calibre ou dado inventado. Por pico e por dia entram: rating LOTUS, faixa de tamanho, período, horas de onda boa, vento.
O modelo de longo prazo do Surfline nunca passa de FAIR TO GOOD — GOOD e EPIC só saem de forecaster humano, para o presente. Então os degraus de cima são nossos, calculados com os dados crus:
| Nível | Regra |
|---|---|
| POOR | FLAT até POOR-FAIR |
| FAIR | FAIR do Surfline — dá para molhar, não é dia de viagem |
| SOLID | FAIR TO GOOD ou qualquer rating humano acima |
| GOOD | SOLID + período ≥ 14 s + média da faixa de surf > 4 ft |
| EPIC | GOOD + 6 h ou mais de onda boa no dia |
Média da faixa, não mínimo: 4–10 ft é mar de 7 ft com séries.
EPIC 25 · GOOD 18,75 · SOLID 12,5 · FAIR 0 · POOR −12,5. Dia perdido anula um dia bom; mediano amortece.
Cada pico tem um lead: dias entre decidir e estar na água (spots.ts).
Dias antes do lead não existem: não entram na janela nem na nota. Um
swell épico que acontece enquanto você está no avião é irrelevante.
A nota é a soma da melhor janela de 4 dias corridos (JANELA_DIAS) a
partir do primeiro dia alcançável. Vai de −50 a 100. Empate resolve pela mais
cedo: previsão perto é previsão confiável. Se o horizonte não comporta 4 dias,
usa o que couber.
| Categoria | Nota mínima | Equivale a |
|---|---|---|
| Trip da Vida | 93,75 | 4 EPIC, ou 3 EPIC + 1 GOOD |
| Classic | 81,25 | swell excepcional |
| Onda boa | 62,5 | vários dias de mar bom |
| Honesto | 50 | surfa todo dia, sem brilho |
Abaixo de 50 o pico não recebe nota nem categoria e sai da tela. O sistema só fala quando é verdade.
- Janela começando em até 7 dias (
HORIZONTE_ACAO) → STRIKE: hora de agir, com data-limite de embarque e botão de cotação. - Mais longe → NO RADAR: nota e destaque, mas sem call de compra — previsão distante ainda muda muito.
Se o mar segue SOLID ou melhor por 2+ dias depois da janela
(MIN_DIAS_EXTENSAO), a tela e o boletim avisam até quando — "o swell segue
forte" quando a maioria é GOOD/EPIC, "o mar segue surfável" caso contrário.
Não muda a janela nem o call.
embarque = primeiro dia da janela − lead
último surf = fim da janela
Viagem longa precisa de mais para se justificar. Decisão tomada em 05/08:
spots.tsganha dois campos opcionais: janela (default 4; Indonésia 7) e piso de call em nota (default 50; Indonésia 64).- A nota passa a ser normalizada: soma × 100 ÷ (25 × janela do pico).
- Abaixo do piso do pico → invisível (opção 1, escolhida contra "visível sem call").
- Piso por pontos, não por composição: GGGGGGG entra, EEEEPPP não.
Trava: só entra em código junto com os leads reais, numa rodada só, com teste no papel antes.
Só falar quando é verdade. Silêncio deliberado. Produto que grita todo dia vira ruído; que só acende quando vale, vira hábito.
Avisar quando piora. O boletim reporta janela que encolhe ou some, e a conferência admite quando o call de três dias atrás não se confirmou. Confiança se constrói na queda.
Laranja significa excepcional. Reservado a Trip da Vida, Classic, Strike e dia EPIC. Onda boa é verde, o resto cinza.
Boletim narra, dashboard mostra. O comentário gerado por dados saiu do dashboard na v2; quem lê o boletim e clica no link não encontra outro boletim.
No radar não tem botão. Sem cotação e sem data de embarque para janela além de 7 dias.
A cotação não tem teto nem piso de dias. A janela recomendada vem pré-selecionada como ponto de partida; o cliente marca o que quiser.
Nós damos as datas, o agente resolve o voo. Sem API de aviação; o Vava tem consolidador e GDS. Telefone e assinatura vêm do código, nunca da IA.
Sem dependência externa evitável. Mapa de swell desenhado com os próprios dados. Sobrou o flagcdn para bandeirinhas, com fallback para o código do país. Emoji de bandeira não é usado (não renderiza no Windows).
Descartados conscientemente: escolha automática de voo; orçamento no formulário (ancora o agente para baixo); passaporte na cotação (é para emitir, não para cotar); API oficial do WhatsApp (exige template pré-aprovado para um texto que muda todo dia).
Três camadas, de cima para baixo:
Status — manchete ("2 Strikes valendo viagem" / "Nada em Strike — 3 no radar" / "Nenhuma janela hoje"), mais Destaque e Alternativa clicáveis, no mesmo padrão do boletim, mas em dados.
Feed — um card por pico com nota, ranqueado por estado e depois por nota. Substitui de uma vez o antigo quadro de Destaques e o acordeão por país. O card traz categoria, janela, embarque (só em Strike), energia, extensão do swell e a faixa de 16 dias com a régua nova; tocar num dia abre o detalhe (tamanho, período, direção com seta, energia em kJ, vento, horas boas). Filtros: Strike e No radar. Picos sem nota aparecem só em "Todos", como linha compacta — a vigilância dos 24 fica visível sem ocupar a tela.
Contexto — mapa de swell (aberto) e manual (fechado).
CTA fixo de cotação — bottom sheet com seleção de início e fim em duas fases sobre a faixa colorida; datas de embarque e volta recalculadas. Envia por WhatsApp ao Vava.
Os leads são chutes. Todos com 2, Indonésia com 3 — e esse é
comprovadamente otimista (GRU–PDG são ~2 noites e ainda tem barco, com dia de
saída). Ver CHANGELOG.md → Pendências.
Sem trava de temporada. Maldivas em janeiro e Indonésia em dezembro aparecem sem explicação.
?json=1 carrega dois campos redundantes. dias (só a janela) e
serie (tudo). dias fica por compatibilidade com blobs antigos.
Coordenadas em spots.ts sem uso. Sobraram do mapa do Windy.
Limite de 80 mil caracteres por arquivo. render.ts está em ~37 mil
depois da v2.
Comece pelo config.ts: todos os números estão lá, comentados. Depois leia o
cabeçalho do score.ts: a régua inteira em português. viagem.ts é o mais
curto dos que importam.
O JavaScript da página vive em pagina.ts, dentro de um String.raw. Duas
coisas quebram o bloco inteiro — e com ele todos os cliques da página: uma
crase e a sequência ${. Aspas simples e concatenação com +,
sempre. Os dados chegam por um <script type="application/json">, não por
interpolação.
O contrato dashboard↔boletim é o /?json=1. Mudou um campo lá, confira
Boletim.ts. Uma chamada órfã já quebrou esse endpoint sem afetar a página.
Não minifique. Os comentários são parte da documentação.