Karlão reportou: APK v1.10.15 crasha no boot mesmo após desinstalar
e reinstalar. Significa bug no código, não state corrompido.
Fix preventivo:
- Boot IIFE wrapped em try/catch master
- Cada init (loadState, tracking, anchor, battery, sw, sensors, rt,
gcal) agora em try individual — falha de um não derruba o resto
- Migration defensiva de state.btDevices: filtra entries inválidas
(null, sem id, etc)
- Se loadState crashar (state corrupto), reseta localStorage
- Crash master no try-catch chama alert() nativo + localStorage.clear()
pra recovery automático na próxima abertura
Quando Karlão atualizar pra v1.10.16, vai aparecer mensagem específica
do erro (se ainda houver) — daí descubro causa exata em vez de
adivinhar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BMS chinês declara ff02 com property [read] apenas, MAS aceita writes
em background. Plugin Capacitor força através (Android stack permite).
Web Bluetooth do Chrome respeita spec — recusa writeValue se char
não tem write/wnr declarado.
Workaround: se nenhuma char tem write declarada, força usar primeira
não-notify (geralmente ff02). Tenta writeValue mesmo assim.
Browser pode lançar SecurityError, mas se BMS aceitar = funciona.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Web Bluetooth log mostrou ff02 com property [read] sozinha, mas no
APK v1.10.7 o plugin reportava [wnr,read]. Discrepância revelou typo:
eu usei p.writeWithoutResponses (com S no final) mas o nome correto
da property em BluetoothCharacteristicProperties é singular —
p.writeWithoutResponse. Sempre undefined → ff02 não detectado como
writeChar → probe abortava com 'Sem chars notify+write'.
Fix: 4 ocorrências em bmsProbeWebBluetooth corrigidas. Plugin
Capacitor por sorte usa o nome diferente (writeWithoutResponse no
nested object) então não foi afetado.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Karlão sugeriu testar no notebook (Chrome) onde Web Bluetooth API
é mais madura que plugin Capacitor v6. Implementado bmsProbeWebBluetooth
que usa navigator.bluetooth direto:
- getPrimaryService(ff00)
- getCharacteristics() lista chars
- writeValueWithoutResponse / writeValue conforme properties
- characteristicvaluechanged event listener
- Wake sequence + 3 protocolos JBD/JK/Daly
Quando Web descobrir o protocolo certo, copio a lógica pro path
Capacitor (APK Android também vai funcionar).
requestDevice pra browser web agora inclui ff00/fff0/ffe0 nos
optionalServices pra Web Bluetooth permitir acesso.
Sem APK rebuild (web only) — só deploy backend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Confirmado: BMS responde ao app oficial Xiaoxiang. Problema é técnica
de inicialização não implementada na nossa abordagem.
Adicionado wake-up sequence ANTES do probe de protocolos:
1. ble.read na notify char (acorda stack BLE)
2. delay 300ms
3. write 0x5A x4 (handshake hello observado em alguns Xiaoxiang)
4. delay 1500ms (BMS processa wake)
5. probe normal de protocolos
Cada step protegido por try/catch + timeout 2s — não trava loop.
Logs detalhados pra ver onde falha se acontecer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Log v1.10.9 mostrou que probe parou em '→ TX JBD-0x03' sem testar
próximos 3 protocolos (JK, Daly, JBD-wnr). Causa provável: write
trava mesmo com WNR no plugin v6.
Fix:
- bmsWriteCmd com Promise.race + timeout 3s — write nunca trava
loop indefinidamente
- bmsTryProtocols agora loga '✔ write retornou' e '✗ sem RX em 2.5s'
pra distinguir write OK + BMS mudo de write travado
- Try/catch interno no write — se falha, continua pro próximo
protocolo em vez de abortar loop
Próximo log vai mostrar TODOS os 4 protocolos testados — daí saberemos
se é hardware mudo OU plugin travando.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Karlão reportou que app reinicia ao tentar copiar log (crash em
v1.10.8 também — modal grande + textarea com Unicode pode crashar
WebView Android skinned).
Solução: novo endpoint POST /api/bms/diag-log que aceita texto
do log + auth user, salva em /data/diag-logs/{userId}-{ts}.txt
no servidor. Frontend tem botão '📤 Enviar pro servidor' (verde)
que faz fetch POST. Resposta confirma recebimento.
Eu (Claude) leio o arquivo direto do servidor — sem dependência
de clipboard, share, modal HTML, ou copy manual.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug v1.10.6/7: clicar 'Copiar log' fechava o app (crash WebView).
Causa: navigator.clipboard.writeText em alguns Android skinned
(Samsung One UI, Xiaomi MIUI) requer permissão extra que sem ela
gera SecurityException nativa não capturada.
Fix: substitui por modal full-screen com textarea readonly +
selectable + botão Compartilhar (navigator.share, mais robusto).
Removido document.execCommand (deprecado, também crashava).
Karlão pode agora: (1) selecionar texto manualmente segurando na
textarea, (2) tocar Compartilhar pra mandar via WhatsApp/email
escolhendo o app no share sheet do Android.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DESCOBERTA crítica do log do Karlão: ff02 (write char) tem properties
[wnr,read] — SÓ writeWithoutResponse, sem write. Meu probe primeiro
tentava ble.write() (com response) que trava silenciosamente esperando
ACK que o BMS nunca envia. Por isso log para em '→ TX JBD-0x03' sem
testar próximos protocolos.
Fix:
- Probe detecta properties da writeChar e seta dev.bmsForceWnr=true
quando char tem wnr mas não write
- Log mostra '(force-wnr)' ao lado do Write= no diagnóstico
- bmsWriteCmd respeita bmsForceWnr e usa writeWithoutResponse mesmo
quando o protocol não pediu
Esperado v1.10.7: TX JBD-0x03 com writeWithoutResponse → BMS responde
com pacote 0xDD ... 0x77 → dashboard mostra V/A/SoC/células reais.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Karlão mandou log antigo achando que era novo (não dava pra distinguir
log v1.10.4 de v1.10.5 sem ler timestamps). Adicionado botão 📋 Copiar
log que copia texto puro pro clipboard com header 'Shivao vX.Y.Z · log
diagnóstico' — fica óbvio qual versão está rodando.
Mudanças:
- <details> agora abre por padrão (open attribute)
- Botão 📋 Copiar log (navigator.clipboard + fallback textarea)
- Botão 🗑 Limpar pra zerar histórico antes de novo teste
- Painel max-height 200→300px + font-family mono
- Toast confirma cópia OK
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug v1.10.4: clicar 🔄 Re-ler gerava 'getServices erro: Bluetooth LE
not initialized' porque Android desconecta GATT em background pra
economizar bateria, mas state.btDevices ainda mostra 'conectado'.
Fix: bmsManualRead agora faz 3 passos sequenciais com diagnóstico:
1. ensureBleNativeReady() — garante plugin inicializado
2. ble.connect({deviceId, timeout:15000}) — reconecta GATT (silent
se 'already connected')
3. bmsProbeAndAttach() — probe completo
Cada passo emite log próprio: "Plugin init OK", "GATT reconectado"
ou "GATT já conectado", "🔍 Enumerando characteristics..."
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug crítico v1.10.3: app crashava ao parear ou clicar Re-ler.
Causa: chamadas a ble.requestConnectionPriority() e ble.requestMtu()
não existem no @capacitor-community/bluetooth-le v6.1.0 (foram
adicionadas em v7+). Sem o método, o plugin lança exception nativa
não-tratada que escapa do try/catch JS e derruba o WebView Capacitor.
Fix:
- Remove requestMtu + requestConnectionPriority
- getServices() chamado UMA vez (não no loop por vendor)
- Filtra services por prefixo vendor (ff00, fff0, ffe0, 0203)
- Lista todos chars descobertos com properties no diagnóstico
- Loga "getServices retornou N services" pra confirmar que enumeração rodou
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Karlão reportou log v1.10.1 quando deveria ser v1.10.2 — usuário não
sabia se atualização chegou. Adicionado:
- "Shivao v1.10.X" mostrado no card BMS (status line)
- Primeira linha do log: "📦 Shivao v1.10.3 · Probe iniciado"
- requestConnectionPriority=HIGH antes do probe (alguns BMS exigem)
- requestMtu(247) — Xiaoxiang BMS oficiais às vezes ignoram comandos
enviados em MTU baixo (23 default), exigem 247 pra responder
Próximo passo: usuário atualiza pra v1.10.3, primeira linha do log
confirma versão. Se ainda zero RX, partiremos pra CCCD descriptor manual.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 problemas atacados após teste de Karlão:
1) BMS conectou mas não respondeu o comando 0x03:
- Log mostra "← RX X bytes: hex" pra cada notification recebida
- Listener registrado ANTES de startNotifications (fix de race condition)
- Wait 500ms entre subscribe e primeiro write (alguns BMS precisam wake)
- Após 5s sem resposta, tenta writeWithoutResponse automaticamente
- Botão 🔄 Re-ler manual no card pra forçar query
2) Karlão pediu "monitor visual humano":
- Modal full-screen "⚡ Monitor da Bateria" com:
* Círculo SoC grande SVG (ring chart 160x160) com cor por nível
* Status flow grande: ⚡ CARREGANDO / ↓ DESCARGA / — REPOUSO
* Tempo restante calculado (descarga = remainCap/current)
* Tempo até cheia (carga = (totalCap-remainCap)/current)
* 4 cards: Tensão · Corrente · Potência · Capacidade
* Linha info: ciclos · temperaturas · firmware version
* Grid de células coloridas por health (vermelho <3.0V, verde >3.6V)
* Auto-refresh 10s enquanto modal aberto
- Botão 📊 Monitor no card BMS abre dashboard
3) Estado de erro mais claro:
- Dashboard mostra "Aguardando dados..." se b.voltage ainda não chegou
- Diagnóstico log destaca chunks RX em azul
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Identificado pelo diagnóstico: BMS do Karlão (bat2) usa protocolo JBD
(Jiabaida) — service ff00, notify ff01, write ff02. Padrão de mercado
para BMS chineses (Overkill Solar, Hankzor, JBD oficial, LLT Power,
Xiaoxiang) — cobre ~80% dos BMS BLE de lítio.
Implementação:
- Auto-detect: ao parear, se device tem service ff00 → ativa parser JBD
- bmsAttachJBD() subscribe na char ff01 (notify) + envia comando 0x03
- Comando: DD A5 03 00 FF FD 77 (Read Basic Info)
- Reassembly de chunks BLE (max 20 bytes/chunk) até receber 0x77 (end)
- Parser decodifica: voltage (uint16/100), current (int16/100, signed),
remaining/total capacity (Ah), cycle count, protection bitfield,
SoC (%), FET status, cell count, temperatures (kelvin*10 → °C)
- Re-poll a cada 30s pra atualizar dados em tempo real
- Auto-sync lastBattery com BMS soc pra card resumido
UI expandida:
- Card BMS com 3 stats grandes: TENSÃO (V) · CORRENTE (A) · POTÊNCIA (W)
- Cor dinâmica: verde se carregando (current>0), amarelo se descarregando
- Linha extra: status flow + capacidade (remain/total Ah) + ciclos + temps
- Block de células individuais (4S/8S/16S detectado automaticamente)
- Border-left do card colorido conforme estado de fluxo
Protocolo de referência: gitlab.com/bms-tools/bms-tools
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Karlão reportou: "localiza mas não pareia" (picker abre, seleciona
device, mas conexão falha silenciosa). Sem ver onde trava, impossível fix.
Adicionado:
- setBleDiag() exibe cada step com timestamp + cor (info/ok/warn/err)
- Painel <details> expansível "📋 Diagnóstico" no card BLE
- Logs em cada operação: backend, init, picker, connect, getServices,
battery read, notifications, device info
- Timeout do connect aumentado: 15s → 30s (BMS podem demorar)
- getServices() lista UUIDs descobertos no device — descobre se BMS
expõe Battery Service padrão ou só protocolo proprietário
- Mensagens explícitas de erro em cada catch (e.message ou errorMessage)
Próximo passo: Karlão testa, abre painel diagnóstico, me passa screenshot
ou copia o log. Daí descubro exatamente onde trava (timeout, sem service,
permissão negada, etc).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug v1.9.0: APK Android mostrava "Web Bluetooth não suportado" porque
Android System WebView desabilita Web Bluetooth API por padrão (segurança).
Fix: instala plugin @capacitor-community/bluetooth-le@^6.1.0 (compatível
com Capacitor 6) que expõe API nativa Android/iOS. JS detecta backend:
- Capacitor (APK): usa window.Capacitor.Plugins.BluetoothLe
- Browser web: usa navigator.bluetooth (Chrome PC continua funcionando)
Mudanças:
- mobile/package.json: nova dep @capacitor-community/bluetooth-le ^6.1.0
- AndroidManifest.xml: BLUETOOTH_SCAN (neverForLocation), BLUETOOTH_CONNECT,
BLUETOOTH/BLUETOOTH_ADMIN (Android ≤30), uses-feature bluetooth_le
- bleBackend() detecta runtime, ensureBleNativeReady() inicializa plugin
- pairBluetoothDevice + connectAndRead + reconnect + remove abstraem backend
- UUIDs em formato 128-bit (compatível com ambos)
- parseDataView helper: plugin envia value como base64, web envia DataView
iOS: plugin suporta nativamente — quando build iOS for feito, funciona.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bluetooth & Acessórios (aba Mais):
- Pareamento Web Bluetooth API (acceptAllDevices)
- Lê Battery Service padrão (UUID 0x180F) + characteristic 0x2A19
- Subscribe pra notificações em tempo real (battery_level changes)
- Lê Device Info Service (manufacturer + model)
- Lista persistente de devices pareados (state.btDevices)
- Reconexão via navigator.bluetooth.getDevices() (Chrome ≥85)
- Status visual: 🪫/🔋 + cor por nível (verde >50, amarelo 20-50, vermelho <20)
- Cleanup ao remover device (disconnect GATT + remove do state)
Raymarine Gateway (slot, parser em v1.10):
- Card config com IP + porta TCP/UDP do gateway NMEA 2000→WiFi
- Sugere Yacht Devices YDWG-02 / Actisense W2K-1
- Salva em state.nmeaGateway pra parser futuro
- Sem gateway físico ainda, só persiste config
Limitações documentadas no UI:
- iOS Safari não suporta Web Bluetooth (precisa @capacitor/community/bluetooth-le em v1.10)
- Reconexão automática varia por device (Web Bluetooth não persiste connections)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cartas náuticas nos mapas (rastreio, fundeio, zonas, viagens):
- OpenSeaMap como overlay padrão grátis (sondas, faróis, bóias, marcas)
- Slot Navionics ativável via chave (após aprovação Garmin)
- Dynamic load do JNC.Leaflet.NavionicsOverlay quando chave preenchida
- Layer switcher no canto direito do mapa: OSM Padrão / Satélite Esri,
overlay OpenSeaMap / Navionics
- Helper addMapLayers() centraliza configuração — substituiu 5 usages
manuais de L.tileLayer espalhados (tracking, trip view, anchor,
anchor history, zone editor)
Settings (Mais → Cartas Náuticas):
- Dropdown provedor: OpenSeaMap/Navionics/só OSM
- Campo chave Navionics (password) com link pro form Garmin
- Status visual do provedor ativo
Integração OpenCPN (Mais → Exportar para OpenCPN):
- Botão gera GPX consolidado de todo o diário:
- Tracks: cada viagem com pontos GPS sequenciais
- Waypoints: cada fundeio histórico com símbolo Anchor
- Routes: cada zona (forbidden/attention) como polígono fechado
- Aproximação círculo→polígono 16 pontos pra zonas circulares
- Compatível com OpenCPN, Garmin, Raymarine, B&G, qualquer plotter
GPX-compliant
- Download direto via Blob URL, sem servidor
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug crítico: após login Google ou email, o app pedia pra logar
DE NOVO toda vez que abria/fechava. E sync nunca iniciava.
Causa: cloudConfigured() exigia state.cloud.token, mas no login
Google/email a auth fica em state.auth.accessToken (JWT), não em
state.cloud.token (que é o BOAT_TOKEN avançado).
Resultado: cloudConfigured() retornava false → welcome screen
sempre aparecia, rtConnect() nunca rodava, sync zero.
Fix:
- cloudConfigured() agora retorna true se tem state.auth.accessToken
OU state.cloud.token (qualquer um dos dois)
- maybeShowWelcome() reescrito pra checar autenticação real
- Botão "Usar sem login (modo offline)" mais visível na welcome
screen pra dar saída clara
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reskin completo baseado em pesquisa de Navionics/Windy/PredictWind/Garmin
ActiveCaptain. Mata o feel "magazine editorial vintage" e adota padrões
mobile-app modernos.
Mudanças visuais (CSS overlay v3 sem alterar HTML/JS de business):
- Paleta dark navy (#0d2538) + cyan accent (#06b6d4) + reservado red pra alarme
- Inter (sans-serif) substitui Fraunces (italic editorial)
- Tabular nums em todas as métricas (lat/lon/depth/speed)
- Cards modernos: border-radius 14px + shadows sutis + bg dark
- Header 50% mais compacto (sem compass mark, avatar maior + accent cyan)
- FAB reposicionado acima da bottom nav, gradient cyan
- Modais: bottom sheet no mobile com top corners rounded
- Form fields dark com focus glow cyan
- Buttons com border-radius modernos, primary = cyan filled
Novos componentes:
- Bottom navigation: 5 tabs com line icons (Início/Travessias/Pendências/
Zonas/Mais), backdrop-filter blur, badge vermelho em pendências overdue
- Safety status bar (sticky abaixo do header): GPS dot + Anchor watch +
Bateria. Pulsa amarelo se warn, vermelho se danger
- switchPanel() unifica top tabs (legacy) + bottom nav
Service worker bumped pra invalidar cache antigo automaticamente.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug: ao clicar "Entrar com Google" no APK Capacitor, app abria Chrome,
user logava OK ("Logado, volte pro app"), mas ao voltar pro app o login
não completava — ficava em loop pedindo pra logar de novo.
Causa: Android matava o WebView do app quando ele ia pra background
(usuario indo pro Chrome). Ao reabrir o app, _googleAuthPolling interval
estava perdido e o session_id (em variável JS) também.
Fix: persiste session_id em localStorage com timestamp. Adiciona
resumePollingIfPending() chamado em:
- Bootstrap (sempre, 500ms após init)
- visibilitychange visible (volta do background)
Também faz uma chamada imediata de poll antes de iniciar interval —
caso os tokens já estejam prontos quando o app reabre.
TTL de 10min no localStorage (mesmo TTL do Map no servidor) — após
isso considera expirado e limpa.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Problema: Google Sign-In popup (GSI) não funciona em WebView nativo do
Capacitor. FedCM bloqueia, popup não abre, ou retorna erro silenciosamente.
Solução: detectar Capacitor (window.Capacitor) ou WebView (UA com 'wv') e
usar OAuth redirect tradicional + polling em vez do popup GSI.
Backend (server/src/index.js):
- GET /api/auth/google/start — gera URL OAuth com state contendo
session_id + flow:'login'. App chama isso e abre URL no browser externo.
- /api/google/callback adaptado — quando state.flow=='login', cria/loga
user por email do Google, gera JWT, armazena em pendingGoogleSessions
(Map em memória, TTL 10min) por session_id, mostra HTML "logado, volte
pro app".
- GET /api/auth/google/poll?session=xxx — app faz polling 2s. Retorna
204 se ainda esperando, 200 com tokens (one-shot, deleta após).
Frontend (app/diario-bordo.html):
- Detecta Capacitor/WebView, força fluxo redirect+polling
- Browser web: tenta GSI popup primeiro, fallback redirect se prompt
for bloqueado (FedCM/popup blocker)
- window.open abre Custom Tabs no Android (ou nova aba no PC)
- Timeout de 4min (120 tries × 2s) pro polling
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UX simplificada drasticamente — usuário não precisa mais saber URL/token:
- Tela de boas-vindas full-screen quando não logado
- 3 botões grandes: Google, Email, Servidor próprio (avançado)
- URL hardcoded https://shivao.pontualtech.work como padrão
- Auto-conecta WebSocket + Google Calendar status após login
- Pull inicial automático pra puxar dados existentes da conta
Backend (server/src/index.js):
- Endpoint POST /api/auth/google: recebe credential (Google ID token),
valida via tokeninfo do Google, confere aud == GOOGLE_CLIENT_ID,
cria user automático com email do Google se não existe,
retorna JWT access+refresh tokens
- Reusa GOOGLE_CLIENT_ID/SECRET já configurados no Coolify
Frontend (app/diario-bordo.html):
- Modal welcome com Google Sign-In via @google/gsi/client (script async)
- Tabs Login/Signup pro fluxo email
- Form avançado pra power users self-hosters
- Skip pra modo offline
- Once dismissed, fica oculto (localStorage flag)
Service Worker bumped pra v1.6.0 (invalida cache antigo).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend (server/src/google-calendar.js + endpoints):
- OAuth 2.0 authorization-code flow completo
- Endpoints: /api/google/{status,auth-url,callback,disconnect,sync-pending,pull}
- Auto-refresh de access_token quando expira (com retry 401→refresh→retry)
- Token storage em google_connections (better-sqlite3)
- syncToken pra delta sync eficiente do Google
- Pendência↔evento: ⚓/✅ no summary, dueDate→start.date all-day,
shivaoPendingId/shivaoCompleted em extendedProperties.private
- Graceful disable: 503 + flag isEnabled() se env vars não setadas
Frontend (Arquivo › Google Agenda):
- Card só aparece quando feature ativa no servidor
- Connect: abre OAuth em nova aba + polling 3s pra detectar sucesso
- Auto-sync na criação/edição/deleção de pendência (se conectada + tem dueDate)
- Botão "Sincronizar todas pendências" + "Buscar mudanças do Google"
- Pull automático ao abrir aba Pendências (se passou >2min do último)
- Pendências criadas direto no Google viram pendências locais
Pra ativar em produção, adicionar no Coolify shivao-cloud:
GOOGLE_CLIENT_ID=...apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=...
GOOGLE_REDIRECT_URI=https://shivao.pontualtech.work/api/google/callback
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend (server/src/realtime.js):
- WebSocket server em /ws via lib `ws`
- Auth por JWT ou BOAT_TOKEN (mesmo middleware do REST)
- Broadcast de notificações state:changed por user (skip device origem)
- Heartbeat ping/pong + cleanup de conexões mortas
- Presença: avisa todos os devices do user quantos estão online
- POST /api/data agora dispara broadcast pra outros devices em tempo real
Frontend (app/diario-bordo.html):
- Cliente WS com reconnect exponencial (1s→2s→5s→15s→30s→60s)
- deviceId persistente em localStorage (gerado no primeiro boot)
- Heartbeat 25s pra manter NAT/proxy abertos
- Auto-push debounced 2.5s no saveState (acumula edições rápidas)
- Auto-pull debounced 300ms no recebimento de state:changed
- Reconnect ao voltar pro foreground + ao recuperar conexão
- Indicador visual no header: 🟢 online · 🟡 syncing · 🔴 offline · ⚫ disabled · ⚠️ erro
Echo prevention em 3 camadas:
1) Server skip por originDeviceId (header X-Device-Id)
2) Cliente ignora notif do próprio device
3) Guard temporal: pull rejeita se updated_at < lastPushAt
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Novos campos por embarcação:
- Foto (capture câmera ou galeria, resize automático max 1280px JPEG q=0.85)
- Horímetro inicial do motor
- Data de cadastro (defaulta hoje em novas)
- Matrícula / TIE (Capitania)
- Notas livres
UI:
- Preview circular no editor com botões câmera/galeria/remover
- Avatar circular no header (foto se houver, ícone do tipo senão)
- Avatar 44x44 na lista da frota
- Foto guardada no IndexedDB (mesma store das mídias de viagem/manutenção)
- Lifecycle pareado: remover barco apaga foto, trocar foto apaga antiga
- /apk redirect aponta pra v1.4.1
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
— Frota: gerencia múltiplas embarcações (veleiro/motor/cata/RIB/outro)
— Selector de barco ativo no header, modal de gerência completo
— Campos por barco: nome, tipo, modelo, comprimento, boca, calado, amarra, ano
— Toggle global de unidades (metros/pés) com conversão em todos displays
— Calculadora de fundeio: scope ratio, raio de giro, raio sugerido p/ alarme
— Dicas adaptativas por vento (auto-fetch Windy/OpenMeteo) + tipo de embarcação
— Migration automática state.boat → state.boats[] (compat retroativa)
— Storage interno sempre em metros (ISO), conversão só no boundary
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BACKEND
- Nova tabela payments (user_id, asaas_payment_id, plan, cycle, value, billing_type, status, ...)
- Coluna users.asaas_customer_id (cache pra reaproveitar customer entre payments)
- server/src/billing.js: cliente Asaas v3 com getOrCreateCustomer, createPayment, getPixQrCode, status mapping
- Endpoint POST /api/billing/checkout — cria cobrança + retorna URL/QR PIX
- Endpoint GET /api/billing/payment/:id — verifica status, faz reconciliação se webhook falhou
- Endpoint POST /api/billing/asaas-webhook — ativa licença em RECEIVED/CONFIRMED, revoga em REFUNDED
- Endpoint GET /api/billing/payments — histórico do user
- 503 se ASAAS_API_KEY não configurado (graceful degradation)
- Webhook valida ASAAS_WEBHOOK_TOKEN (shared secret) se setado
FRONTEND (sincronizado app/ + server/public/)
- openUpgradeModal() — modal dinâmico com seleção plano (Pro/Captain) + ciclo (mensal/anual) + tipo (PIX/Cartão/Boleto)
- _doCheckout() — chama backend, exibe QR Code PIX OU link invoice
- checkPaymentStatus() — verifica e ativa licença quando pago
ENV VARS NECESSÁRIAS NO COOLIFY (próximo passo manual):
- ASAAS_API_KEY=$aact_prod_... (chave Asaas que Karlão já usa em outros projetos)
- ASAAS_API_URL=https://api.asaas.com/v3 (default)
- ASAAS_WEBHOOK_TOKEN=whsec_... (gere um valor aleatório, configure no painel Asaas → Integrações)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implementa requisitos pra uso em áreas remotas:
OFFLINE REAL (Service Worker em server/public/sw.js)
- Pré-cache de shell (HTML, manifest, icon, Leaflet, fontes Google)
- Cache-first pra map tiles OSM (offline em alto-mar com tiles já visitados)
- Network-first pra Windy/Open-Meteo (com fallback ao cache)
- /api/* passa direto (não interferir em sync, heartbeat, auth)
- Skip-waiting + claim pra ativar imediatamente após install
SENSORES (sensor widget flutuante canto superior direito)
- Bússola via DeviceOrientationEvent (suporta iOS webkitCompassHeading + Android alpha)
- iOS: pede permission via gesture do usuário (botão 'Ativar bússola')
- Barômetro via Generic Sensor API (Android com sensor real, fallback gracioso)
- Tendência de pressão (subindo/caindo/estável) baseada em janela móvel
- Indicador de online/offline sempre visível
PRÉ-CACHE DE MAPA
- Botão 'Pré-cachear mapa' baixa tiles ~50km de raio (zooms 8-13, ~200 tiles)
- Comunicação page→SW via MessageChannel
- Limit 6 conexões paralelas (respeitando OSM tile policy)
DOCUMENTAÇÃO TERMÔMETRO: API web não tem termômetro de ambiente.
Solução: usar dado da Windy (já implementado) + cache offline via SW.
Sincronizado em app/ e server/public/ — single-file HTML preservado.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Endpoint Express /manifest.json com name/short_name/icons/theme
- /icon.svg vetorial (veleiro estilizado em paleta marítima)
- Links manifest + apple-touch-icon nos 2 HTMLs (app/ e server/public/)
Habilita:
- Android Chrome: 'Add to Home Screen' com ícone bonito
- iOS Safari: ícone na tela inicial
- PWABuilder: pode gerar APK Android sideload-ready
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run 6 da squad shivao-melhoria — HANDOFF item 5 (Reconexão da vigia)
parcialmente resolvido sem Service Worker (decisão conservadora,
SW mal feito quebraria offline-first).
4 fixes em app/diario-bordo.html + server/public/index.html (sincronizados):
1. requestWakeLock (tracking, linha 1345) + requestAnchorWakeLock (anchor, 1831)
- Adicionado wakeLock.addEventListener('release', ...) com auto-reacquire em 1s
- Garante que sistema soltando o Wake Lock (background tab, low battery)
não deixa a tela apagar permanentemente enquanto vigia/rastreio ativos
2. startGPS (tracking, linha 1347) + startAnchorGPS (anchor, 1901)
- Retry exponencial: 1s, 2s, 4s, 8s, 16s, 30s (max)
- PERMISSION_DENIED é fatal sem retry (com mensagem clara ao dono)
- Outros erros (POSITION_UNAVAILABLE, TIMEOUT) → retry com backoff
- Counter resetado a cada GPS update bem-sucedido (recuperação completa)
Combinado com visibilitychange listener existente (linha 1824) que re-acquire
Wake Lock quando tab volta foreground, cobre o cenário completo de:
- Tab em background no Android Chrome (GPS pausa, sistema solta Wake Lock)
- Tela apagada (Wake Lock soltada, GPS continua se permitido)
- Erro transitório de GPS (perda de sinal, recupera sozinho)
- App reaberto com vigia em andamento (loadAnchorState chama startAnchorGPS
que agora tem retry built-in)
Service Worker real (push notifications + cache de tiles offline) fica
pra iteração futura com spec própria.
HANDOFF.md item 5 marcado como "parcialmente resolvido".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Importação inicial do projeto Shivão (Diário de Bordo do veleiro) em estado
pronto pra produção, já incluindo as 5 mudanças de hardening implementadas
pela squad shivao-melhoria em 2026-04-27.
Mudanças de hardening (HANDOFF.md seção "✅ Pronto"):
1. **Rate limiting nos 3 endpoints públicos de share**
- express-rate-limit ^8.4.1, 60 req/min/IP
- server/src/index.js linhas 38, 262, 271, 279
2. **Tamanho de upload reduzido pra 50MB** (era 200MB)
- multer linha 84
3. **Validação Zod nos endpoints autenticados** (POST /api/data e /zones)
- novo arquivo server/src/schemas/index.js
- middleware validate() retorna 400 com top 5 issues
4. **Audit log de ações sensíveis**
- nova tabela audit_log + funções db.audit() e db.recentAudit()
- 6 endpoints instrumentados (state_set, media_insert/delete, share_create/revoke/zones_update)
- novo endpoint GET /api/audit (autenticado)
5. **Catch silencioso de webhook em zona PROIBIDA tratado**
- app/diario-bordo.html + server/public/index.html linha 3280
- agora loga erro + exibe toast ao usuário
Status final do HANDOFF:
- 🔴 Críticos restantes: 1 (CORS — decisão consciente single-tenant, não-acionável)
- 🟡 Importantes: 4 itens (testes, vigia reconnect, refator frontend, demais catches)
- 🟢 Bom-ter: 5 itens
Stack: Node 20 ESM + Express + better-sqlite3 + Docker (Coolify)
Single-tenant pessoal · single-file frontend HTML · offline-first
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>