Public
A simple starter web endpoint
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.

Nivana Strike Mission

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.


1. O que o sistema faz

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:

  1. Vale a viagem? — a janela tem qualidade suficiente para justificar o voo
  2. Quando eu surfo? — os melhores dias corridos dentro do teto da viagem
  3. 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).

2. Estrutura

<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.


3. Como rodar

Val Town, conta @NivanaStrikeMission. Cinco vals independentes.

3.1 Fluxo de trabalho

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.

3.2 Os vals

Dashboard — HTTP

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ávelPara quê
SURFLINE_EMAIL / SURFLINE_PASSWORDLogin Premium — sem ele a previsão cai de 16 para 6 dias
RotaO que devolve
/a página
/?json=1o contrato com o boletim: por pico, estado, nota, categoria, janela, dias (só a janela) e serie (os 16 dias completos)
/?mapa=1o mapa de swell em PNG, que o boletim anexa à mensagem
/?debug=1diagnóstico de login e amostra crua do payload do Surfline

O cache do Surfline (CACHE_TTL_MS em surfline.ts) dura 1 hora.

Boletim — Cron 0 12 * * * (09h de Brasília, o ano todo)

VariávelPara quê
NIVANA_DASH_URLURL do dashboard, sem barra no final
ANTHROPIC_API_KEYtexto escrito pelo Claude (claude-opus-5); sem ela sai em lista
TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_IDTelegram, um ou mais destinos separados por vírgula
ZAPI_INSTANCE / ZAPI_TOKEN / ZAPI_CLIENT_TOKEN / ZAPI_DESTINOSWhatsApp direto, mensagem individual para cada número da lista
CALLMEBOT_PHONE / CALLMEBOT_APIKEYcanal legado, opcional
EMAIL_ONcópia por e-mail, opcional

O que uma rodada faz, nesta ordem:

  1. Busca /?json=1 e compara com o snapshot de ontem (nivana_snapshot_v1).
  2. Grava o histórico (nivana_hist_AAAA-MM-DD, uma chave por dia, nunca sobrescrita, com a serie completa de cada pico). Grava antes de escrever ou enviar: falha na IA ou no envio não custa o dia. ~25 KB/dia.
  3. 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 (BACIA no Boletim.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 por REGIONAL_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.
  4. 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.
  5. 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 .png por conta própria.

Aquecedor — Cron 0 0,11,15,21 * * * (08h, 12h, 18h, 21h de Brasília)

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.

Foto — HTTP

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.

Pulso — Cron 0 * * * * — experimento, não móvel

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.


4. Regras de negócio — a Régua Nivana

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.

4.1 Só dados do Surfline

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.

4.2 Cada dia ganha um nível

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ívelRegra
POORFLAT até POOR-FAIR
FAIRFAIR do Surfline — dá para molhar, não é dia de viagem
SOLIDFAIR TO GOOD ou qualquer rating humano acima
GOODSOLID + período ≥ 14 s + média da faixa de surf > 4 ft
EPICGOOD + 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.

4.3 Cada dia vale pontos

EPIC 25 · GOOD 18,75 · SOLID 12,5 · FAIR 0 · POOR −12,5. Dia perdido anula um dia bom; mediano amortece.

4.4 Alcance — o conceito central

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.

4.5 Janela e nota

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.

4.6 A nota vira categoria

CategoriaNota mínimaEquivale a
Trip da Vida93,754 EPIC, ou 3 EPIC + 1 GOOD
Classic81,25swell excepcional
Onda boa62,5vários dias de mar bom
Honesto50surfa 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.

4.7 O timing vira estado

  • 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.

4.8 Extensão do swell

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.

4.9 Datas da viagem

embarque  = primeiro dia da janela − lead
último surf = fim da janela

4.10 Decidido, ainda não codificado — janela e piso por pico

Viagem longa precisa de mais para se justificar. Decisão tomada em 05/08:

  • spots.ts ganha 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.


5. Decisões de produto e design

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).


6. O que existe na tela (layout v2, mobile-first)

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.


7. Problemas conhecidos

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.


8. Para quem assume o projeto

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.