feat(ble): breadcrumb persistente sobrevive crash WebView v1.10.18
Some checks are pending
Build Android (APK + AAB) / build-android (push) Waiting to run

Karlão reportou: APK fecha 'em seguida' ao mandar parear. v1.10.17
removeu wake-up do path Capacitor mas crash ainda persiste — agora
é em ble.requestDevice ou ble.connect/getServices.

Sem alert popup = crash nativo lado Java do plugin BLE. Try/catch JS
não captura. Solução: breadcrumb em localStorage ANTES de cada
chamada nativa.

bleCrumb(step) grava 'shivao_ble_last_step' no disco antes de:
- ensureBleNativeReady
- requestDevice
- selected:<name>
- ble.connect
- ble.getServices

Se app crashar, próxima abertura lê o breadcrumb e mostra alert
'⚠ Crash detectado · Última ação: ble.connect @ 2026...' — daí
descobrimos exatamente onde o plugin Java explode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
PontualTech / Karlão 2026-04-29 16:01:23 -03:00
parent 70b123735e
commit 0c0b2d2825
6 changed files with 51 additions and 13 deletions

View file

@ -3749,6 +3749,16 @@ async function updateStorageInfo(){
(async()=>{ (async()=>{
// Wrapper try/catch pra capturar crash no boot (Capacitor WebView pode fechar silenciosamente) // Wrapper try/catch pra capturar crash no boot (Capacitor WebView pode fechar silenciosamente)
try{ try{
// Detecta crash BLE da sessão anterior via breadcrumb
try{
const lastStep=localStorage.getItem('shivao_ble_last_step');
if(lastStep){
setTimeout(()=>{
try{alert('⚠ Crash detectado na sessão anterior!\nÚltima ação BLE: '+lastStep+'\n\nMe envie esta mensagem.')}catch{}
},2000);
localStorage.removeItem('shivao_ble_last_step');
}
}catch{}
await openDB(); await openDB();
try{loadState()}catch(e){console.error('[boot] loadState',e);try{localStorage.removeItem(STORAGE_KEY)}catch{}/* corrupt state — reset */} try{loadState()}catch(e){console.error('[boot] loadState',e);try{localStorage.removeItem(STORAGE_KEY)}catch{}/* corrupt state — reset */}
// Migration defensiva: limpa entries inválidas em state.btDevices // Migration defensiva: limpa entries inválidas em state.btDevices
@ -5885,6 +5895,12 @@ function setBleDiag(msg,type){
console.log('[ble]',msg); console.log('[ble]',msg);
} }
// Breadcrumb persistente: grava ANTES de cada chamada nativa pra sobreviver crash do WebView
function bleCrumb(step){
try{localStorage.setItem('shivao_ble_last_step',step+' @ '+new Date().toISOString())}catch{}
}
function bleCrumbClear(){try{localStorage.removeItem('shivao_ble_last_step')}catch{}}
async function pairBluetoothDevice(){ async function pairBluetoothDevice(){
const backend=bleBackend(); const backend=bleBackend();
setBleDiag('Backend: '+(backend||'NENHUM'),backend?'info':'err'); setBleDiag('Backend: '+(backend||'NENHUM'),backend?'info':'err');
@ -5892,8 +5908,10 @@ async function pairBluetoothDevice(){
try{ try{
let deviceId,deviceName; let deviceId,deviceName;
if(backend==='capacitor'){ if(backend==='capacitor'){
bleCrumb('ensureBleNativeReady');
setBleDiag('Inicializando plugin nativo...'); setBleDiag('Inicializando plugin nativo...');
await ensureBleNativeReady(); await ensureBleNativeReady();
bleCrumb('requestDevice');
setBleDiag('Plugin OK · abrindo picker...'); setBleDiag('Plugin OK · abrindo picker...');
const ble=window.Capacitor.Plugins.BluetoothLe; const ble=window.Capacitor.Plugins.BluetoothLe;
const result=await ble.requestDevice({ const result=await ble.requestDevice({
@ -5901,9 +5919,10 @@ async function pairBluetoothDevice(){
optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO], optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO],
allowDuplicates:false, allowDuplicates:false,
}); });
if(!result?.deviceId){setBleDiag('Picker cancelado','warn');return} if(!result?.deviceId){bleCrumbClear();setBleDiag('Picker cancelado','warn');return}
deviceId=result.deviceId; deviceId=result.deviceId;
deviceName=result.name||'Dispositivo BLE'; deviceName=result.name||'Dispositivo BLE';
bleCrumb('selected:'+deviceName);
setBleDiag('Selecionado: '+deviceName+' ('+deviceId+')','ok'); setBleDiag('Selecionado: '+deviceName+' ('+deviceId+')','ok');
}else{ }else{
setBleDiag('Abrindo picker do navegador...'); setBleDiag('Abrindo picker do navegador...');
@ -5963,6 +5982,7 @@ async function connectAndRead(deviceId,deviceName){
if(backend==='capacitor'){ if(backend==='capacitor'){
const ble=window.Capacitor.Plugins.BluetoothLe; const ble=window.Capacitor.Plugins.BluetoothLe;
try{ try{
bleCrumb('ble.connect');
await ble.connect({deviceId,timeout:30000}); await ble.connect({deviceId,timeout:30000});
setBleDiag('GATT conectado','ok'); setBleDiag('GATT conectado','ok');
}catch(e){ }catch(e){
@ -5972,13 +5992,12 @@ async function connectAndRead(deviceId,deviceName){
const conn=_bleConnections.get(deviceId)||{}; const conn=_bleConnections.get(deviceId)||{};
conn.backend='capacitor';conn.deviceId=deviceId;conn.connected=true; conn.backend='capacitor';conn.deviceId=deviceId;conn.connected=true;
_bleConnections.set(deviceId,conn); _bleConnections.set(deviceId,conn);
// Discover all services pra diagnóstico + auto-detect protocols
try{ try{
bleCrumb('ble.getServices');
const r=await ble.getServices({deviceId}); const r=await ble.getServices({deviceId});
const svcs=(r.services||r||[]).map(s=>s.uuid||s).slice(0,8); const svcs=(r.services||r||[]).map(s=>s.uuid||s).slice(0,8);
setBleDiag('Serviços encontrados: '+svcs.length+' ('+svcs.map(u=>u.slice(4,8)).join(', ')+')','info'); setBleDiag('Serviços encontrados: '+svcs.length+' ('+svcs.map(u=>u.slice(4,8)).join(', ')+')','info');
info.services=svcs; info.services=svcs;
// Auto-detect: service ff00 = JBD/LLT Power BMS
const hasJbd=svcs.some(u=>u.toLowerCase().startsWith('0000ff00')); const hasJbd=svcs.some(u=>u.toLowerCase().startsWith('0000ff00'));
if(hasJbd){ if(hasJbd){
setBleDiag('🔋 JBD BMS protocol detectado!','ok'); setBleDiag('🔋 JBD BMS protocol detectado!','ok');
@ -6461,7 +6480,7 @@ async function removeBluetoothDevice(id){
renderBluetoothCard(); renderBluetoothCard();
} }
const APP_VERSION='1.10.17'; const APP_VERSION='1.10.18';
function renderBluetoothCard(){ function renderBluetoothCard(){
const el=document.getElementById('bt-list'); const el=document.getElementById('bt-list');
const supportEl=document.getElementById('bt-support'); const supportEl=document.getElementById('bt-support');

View file

@ -7,8 +7,8 @@ android {
applicationId "br.com.pontualtech.shivao" applicationId "br.com.pontualtech.shivao"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 30 versionCode 31
versionName "1.10.17" versionName "1.10.18"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

View file

@ -1,6 +1,6 @@
{ {
"name": "shivao-mobile", "name": "shivao-mobile",
"version": "1.10.17", "version": "1.10.18",
"description": "Shivao app nativo (Capacitor wrapper Android/iOS)", "description": "Shivao app nativo (Capacitor wrapper Android/iOS)",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",

View file

@ -3749,6 +3749,16 @@ async function updateStorageInfo(){
(async()=>{ (async()=>{
// Wrapper try/catch pra capturar crash no boot (Capacitor WebView pode fechar silenciosamente) // Wrapper try/catch pra capturar crash no boot (Capacitor WebView pode fechar silenciosamente)
try{ try{
// Detecta crash BLE da sessão anterior via breadcrumb
try{
const lastStep=localStorage.getItem('shivao_ble_last_step');
if(lastStep){
setTimeout(()=>{
try{alert('⚠ Crash detectado na sessão anterior!\nÚltima ação BLE: '+lastStep+'\n\nMe envie esta mensagem.')}catch{}
},2000);
localStorage.removeItem('shivao_ble_last_step');
}
}catch{}
await openDB(); await openDB();
try{loadState()}catch(e){console.error('[boot] loadState',e);try{localStorage.removeItem(STORAGE_KEY)}catch{}/* corrupt state — reset */} try{loadState()}catch(e){console.error('[boot] loadState',e);try{localStorage.removeItem(STORAGE_KEY)}catch{}/* corrupt state — reset */}
// Migration defensiva: limpa entries inválidas em state.btDevices // Migration defensiva: limpa entries inválidas em state.btDevices
@ -5885,6 +5895,12 @@ function setBleDiag(msg,type){
console.log('[ble]',msg); console.log('[ble]',msg);
} }
// Breadcrumb persistente: grava ANTES de cada chamada nativa pra sobreviver crash do WebView
function bleCrumb(step){
try{localStorage.setItem('shivao_ble_last_step',step+' @ '+new Date().toISOString())}catch{}
}
function bleCrumbClear(){try{localStorage.removeItem('shivao_ble_last_step')}catch{}}
async function pairBluetoothDevice(){ async function pairBluetoothDevice(){
const backend=bleBackend(); const backend=bleBackend();
setBleDiag('Backend: '+(backend||'NENHUM'),backend?'info':'err'); setBleDiag('Backend: '+(backend||'NENHUM'),backend?'info':'err');
@ -5892,8 +5908,10 @@ async function pairBluetoothDevice(){
try{ try{
let deviceId,deviceName; let deviceId,deviceName;
if(backend==='capacitor'){ if(backend==='capacitor'){
bleCrumb('ensureBleNativeReady');
setBleDiag('Inicializando plugin nativo...'); setBleDiag('Inicializando plugin nativo...');
await ensureBleNativeReady(); await ensureBleNativeReady();
bleCrumb('requestDevice');
setBleDiag('Plugin OK · abrindo picker...'); setBleDiag('Plugin OK · abrindo picker...');
const ble=window.Capacitor.Plugins.BluetoothLe; const ble=window.Capacitor.Plugins.BluetoothLe;
const result=await ble.requestDevice({ const result=await ble.requestDevice({
@ -5901,9 +5919,10 @@ async function pairBluetoothDevice(){
optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO], optionalServices:[BLE_BATTERY_SERVICE,BLE_DEVICE_INFO],
allowDuplicates:false, allowDuplicates:false,
}); });
if(!result?.deviceId){setBleDiag('Picker cancelado','warn');return} if(!result?.deviceId){bleCrumbClear();setBleDiag('Picker cancelado','warn');return}
deviceId=result.deviceId; deviceId=result.deviceId;
deviceName=result.name||'Dispositivo BLE'; deviceName=result.name||'Dispositivo BLE';
bleCrumb('selected:'+deviceName);
setBleDiag('Selecionado: '+deviceName+' ('+deviceId+')','ok'); setBleDiag('Selecionado: '+deviceName+' ('+deviceId+')','ok');
}else{ }else{
setBleDiag('Abrindo picker do navegador...'); setBleDiag('Abrindo picker do navegador...');
@ -5963,6 +5982,7 @@ async function connectAndRead(deviceId,deviceName){
if(backend==='capacitor'){ if(backend==='capacitor'){
const ble=window.Capacitor.Plugins.BluetoothLe; const ble=window.Capacitor.Plugins.BluetoothLe;
try{ try{
bleCrumb('ble.connect');
await ble.connect({deviceId,timeout:30000}); await ble.connect({deviceId,timeout:30000});
setBleDiag('GATT conectado','ok'); setBleDiag('GATT conectado','ok');
}catch(e){ }catch(e){
@ -5972,13 +5992,12 @@ async function connectAndRead(deviceId,deviceName){
const conn=_bleConnections.get(deviceId)||{}; const conn=_bleConnections.get(deviceId)||{};
conn.backend='capacitor';conn.deviceId=deviceId;conn.connected=true; conn.backend='capacitor';conn.deviceId=deviceId;conn.connected=true;
_bleConnections.set(deviceId,conn); _bleConnections.set(deviceId,conn);
// Discover all services pra diagnóstico + auto-detect protocols
try{ try{
bleCrumb('ble.getServices');
const r=await ble.getServices({deviceId}); const r=await ble.getServices({deviceId});
const svcs=(r.services||r||[]).map(s=>s.uuid||s).slice(0,8); const svcs=(r.services||r||[]).map(s=>s.uuid||s).slice(0,8);
setBleDiag('Serviços encontrados: '+svcs.length+' ('+svcs.map(u=>u.slice(4,8)).join(', ')+')','info'); setBleDiag('Serviços encontrados: '+svcs.length+' ('+svcs.map(u=>u.slice(4,8)).join(', ')+')','info');
info.services=svcs; info.services=svcs;
// Auto-detect: service ff00 = JBD/LLT Power BMS
const hasJbd=svcs.some(u=>u.toLowerCase().startsWith('0000ff00')); const hasJbd=svcs.some(u=>u.toLowerCase().startsWith('0000ff00'));
if(hasJbd){ if(hasJbd){
setBleDiag('🔋 JBD BMS protocol detectado!','ok'); setBleDiag('🔋 JBD BMS protocol detectado!','ok');
@ -6461,7 +6480,7 @@ async function removeBluetoothDevice(id){
renderBluetoothCard(); renderBluetoothCard();
} }
const APP_VERSION='1.10.17'; const APP_VERSION='1.10.18';
function renderBluetoothCard(){ function renderBluetoothCard(){
const el=document.getElementById('bt-list'); const el=document.getElementById('bt-list');
const supportEl=document.getElementById('bt-support'); const supportEl=document.getElementById('bt-support');

View file

@ -1,7 +1,7 @@
// Shivao Service Worker — offline real // Shivao Service Worker — offline real
// Estratégia: shell precachado, tiles cache-first, windy network-first, /api passa direto. // Estratégia: shell precachado, tiles cache-first, windy network-first, /api passa direto.
// Versão usada nos cache names — bumpa essa string pra invalidar caches antigos em deploys. // Versão usada nos cache names — bumpa essa string pra invalidar caches antigos em deploys.
const VERSION = 'shivao-v1.10.17'; const VERSION = 'shivao-v1.10.18';
const SHELL_CACHE = `shivao-shell-${VERSION}`; const SHELL_CACHE = `shivao-shell-${VERSION}`;
const TILES_CACHE = 'shivao-tiles-v1'; // separado pra não invalidar tiles em update do shell const TILES_CACHE = 'shivao-tiles-v1'; // separado pra não invalidar tiles em update do shell
const WINDY_CACHE = `shivao-windy-${VERSION}`; const WINDY_CACHE = `shivao-windy-${VERSION}`;

View file

@ -413,7 +413,7 @@ app.get('/.well-known/assetlinks.json', (req, res) => {
}); });
// Atalho: /apk redireciona pra última APK release no Forgejo // Atalho: /apk redireciona pra última APK release no Forgejo
const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.10.17/Shivao-v1.10.17.apk'; const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.10.18/Shivao-v1.10.18.apk';
app.get('/apk', (req, res) => res.redirect(302, LATEST_APK_URL)); app.get('/apk', (req, res) => res.redirect(302, LATEST_APK_URL));
// Página A4 imprimível com QR Code + instruções (cola no barco/marina) // Página A4 imprimível com QR Code + instruções (cola no barco/marina)