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/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>
- GET /politica — HTML com todas as 12 seções LGPD/GDPR + DPO
- mobile/play-store-listing.md: nome, descrições curta+completa PT-BR (4000 chars), categoria, tags, justificativas de permissions
- mobile/play-store-assets/icon-512.png — gerado via Playwright (renderiza icon.svg em 512x512)
- mobile/play-store-assets/feature-graphic.png — banner 1024x500 com logo + slogan + features
- mobile/play-store-assets/screenshots/ — telas reais do app (home, arquivo, upgrade modal)