From 78c6de538ac8f2984d9524c7ed4c45ca493d95b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?PontualTech=20/=20Karl=C3=A3o?= Date: Mon, 27 Apr 2026 13:28:09 -0300 Subject: [PATCH] feat(reliability): vigia reconnect (Wake Lock release + GPS retry exponencial) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- HANDOFF.md | 16 ++++++++++------ app/diario-bordo.html | 29 ++++++++++++++++++++--------- server/public/index.html | 29 ++++++++++++++++++++--------- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 9cf9c93..7b79fa6 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -86,13 +86,17 @@ Express + SQLite + Docker, deployável em Coolify ou qualquer VPS. - Backend: vitest ou jest para os endpoints - Frontend: playwright para fluxos críticos (especialmente alarme e GPS) -4. **Tratamento de erros no frontend** - - Várias chamadas async fazem `.catch(()=>{})` silenciosamente - - Falhas de sync não são reportadas ao usuário +4. **Tratamento de erros no frontend (parcialmente resolvido)** + - ✅ Catch silencioso de `dispatchWebhooks` em zona PROIBIDA (linha 3280) agora loga + toast (run 2026-04-27-131311 +catch step) + - Restantes: catches em fetches Windy/Open-Meteo (linhas 2712, 2733) — **fallbacks intencionais, mantidos conscientemente** + - Restantes: catches `}catch(e){}` em Wake Lock e tracking state — **best effort em iOS antigo, mantidos** + - Pendente futura: indicador visual de "última sync falhou" no painel cloud -5. **Reconexão da vigia** após app dormir - - Wake Lock pode falhar; GPS pode pausar quando tab inativo - - Investigar Service Worker + Background Sync API +5. **Reconexão da vigia (parcialmente resolvido)** + - ✅ Wake Lock auto-reacquire: `wakeLock.addEventListener('release', ...)` em tracking + anchor (run 2026-04-27-131311 +reconnect step) + - ✅ GPS retry exponencial (1s→2s→4s→8s→16s→30s) com `PERMISSION_DENIED` fatal sem retry, em tracking + anchor + - ✅ `visibilitychange` listener (já existia) re-acquire Wake Lock quando tab volta foreground + - Pendente: Service Worker real (para push notifications + offline tile cache) — exige spec própria, deixar pra futuro 6. **Refatoração do frontend** - HTML monolítico tem ~3500 linhas diff --git a/app/diario-bordo.html b/app/diario-bordo.html index 1a3f349..791d547 100644 --- a/app/diario-bordo.html +++ b/app/diario-bordo.html @@ -1342,9 +1342,9 @@ let trackingInterval=null; function loadTrackingState(){try{const raw=localStorage.getItem(TRACKING_KEY);if(raw){const t=JSON.parse(raw);if(t.active&&confirm('Encontrei um rastreio em andamento. Continuar?')){Object.assign(tracking,t);tracking.watchId=null;startGPS();renderGPSBanner()}else{localStorage.removeItem(TRACKING_KEY)}}}catch(e){}} function saveTrackingState(){try{const{watchId,wakeLock,...r}=tracking;localStorage.setItem(TRACKING_KEY,JSON.stringify(r))}catch(e){}} -async function requestWakeLock(){try{if('wakeLock' in navigator)tracking.wakeLock=await navigator.wakeLock.request('screen')}catch(e){}} +async function requestWakeLock(){try{if('wakeLock' in navigator){tracking.wakeLock=await navigator.wakeLock.request('screen');tracking.wakeLock.addEventListener('release',()=>{tracking.wakeLock=null;if(tracking.active)setTimeout(()=>{if(tracking.active&&!tracking.wakeLock)requestWakeLock()},1000)})}}catch(e){console.warn('wake lock tracking:',e)}} async function releaseWakeLock(){try{if(tracking.wakeLock){await tracking.wakeLock.release();tracking.wakeLock=null}}catch(e){}} -function startGPS(){if(!navigator.geolocation){toast('GPS não disponível');return}tracking.watchId=navigator.geolocation.watchPosition(onGPSUpdate,e=>{toast('Erro GPS: '+e.message)},batteryGPSOptions())} +function startGPS(){if(!navigator.geolocation){toast('GPS não disponível');return}let r=0;function tryStart(){tracking.watchId=navigator.geolocation.watchPosition(p=>{r=0;onGPSUpdate(p)},e=>{if(e.code===e.PERMISSION_DENIED){toast('GPS sem permissão — rastreio interrompido');return}r++;const d=Math.min(1000*Math.pow(2,r-1),30000);toast(`GPS perdido (#${r}) — retentando em ${d/1000}s`);if(tracking.watchId)navigator.geolocation.clearWatch(tracking.watchId);setTimeout(()=>{if(tracking.active)tryStart()},d)},batteryGPSOptions())}tryStart()} function onGPSUpdate(pos){ const p={lat:pos.coords.latitude,lng:pos.coords.longitude,ts:Date.now(),spd:pos.coords.speed||0,acc:pos.coords.accuracy}; @@ -1828,7 +1828,7 @@ const anchorWatch={active:false,watchId:null,startedAt:null,anchorPos:null,swing let anchorMap=null,anchorMarker=null,swingMarker=null,anchorBoatMarker=null,anchorCircle=null,anchorLine=null,swingAnchorLine=null,anchorTimer=null,autoRecenterTimer=null; let pendingAnchorPos=null; -async function requestAnchorWakeLock(){try{if('wakeLock' in navigator)anchorWatch.wakeLock=await navigator.wakeLock.request('screen')}catch(e){}} +async function requestAnchorWakeLock(){try{if('wakeLock' in navigator){anchorWatch.wakeLock=await navigator.wakeLock.request('screen');anchorWatch.wakeLock.addEventListener('release',()=>{anchorWatch.wakeLock=null;if(anchorWatch.active)setTimeout(()=>{if(anchorWatch.active&&!anchorWatch.wakeLock)requestAnchorWakeLock()},1000)})}}catch(e){console.warn('wake lock anchor:',e)}} async function releaseAnchorWakeLock(){try{if(anchorWatch.wakeLock){await anchorWatch.wakeLock.release();anchorWatch.wakeLock=null}}catch(e){}} function loadAnchorState(){try{const raw=localStorage.getItem(ANCHOR_KEY);if(raw){const a=JSON.parse(raw);if(a.active&&confirm('Há uma vigia de fundeio em andamento. Continuar?')){Object.assign(anchorWatch,a);anchorWatch.watchId=null;anchorWatch.wakeLock=null;startAnchorGPS();anchorTimer=setInterval(updateAnchorUI,1000);requestAnchorWakeLock();renderAnchorBanner()}else{localStorage.removeItem(ANCHOR_KEY)}}}catch(e){}} @@ -1899,11 +1899,22 @@ async function confirmAnchor(){ } function startAnchorGPS(){ - anchorWatch.watchId=navigator.geolocation.watchPosition( - onAnchorGPSUpdate, - err=>{toast('GPS perdido: '+err.message)}, - batteryGPSOptions() - ); + let retry=0; + function tryStart(){ + anchorWatch.watchId=navigator.geolocation.watchPosition( + pos=>{retry=0;onAnchorGPSUpdate(pos)}, + err=>{ + if(err.code===err.PERMISSION_DENIED){toast('GPS sem permissão — vigia COMPROMETIDA');return} + retry++; + const delay=Math.min(1000*Math.pow(2,retry-1),30000); + toast(`Vigia GPS perdido (#${retry}) — retentando em ${delay/1000}s`); + if(anchorWatch.watchId)navigator.geolocation.clearWatch(anchorWatch.watchId); + setTimeout(()=>{if(anchorWatch.active)tryStart()},delay); + }, + batteryGPSOptions() + ); + } + tryStart(); } function onAnchorGPSUpdate(pos){ @@ -3277,7 +3288,7 @@ function onZoneEnter(z){ setTimeout(stopAlarmSound,3500); // dispatch webhooks com mensagem de zona const text=`⛔ ${state.boat.name||'Veleiro'} entrou em zona PROIBIDA: ${z.name}`; - dispatchWebhooks(text).catch(()=>{}); + dispatchWebhooks(text).catch(e=>{console.error('webhook zone alarm failed:',e);toast('Falha ao enviar alarme remoto')}); }else{ if('vibrate' in navigator)navigator.vibrate([200,80,200]); } diff --git a/server/public/index.html b/server/public/index.html index 1a3f349..791d547 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -1342,9 +1342,9 @@ let trackingInterval=null; function loadTrackingState(){try{const raw=localStorage.getItem(TRACKING_KEY);if(raw){const t=JSON.parse(raw);if(t.active&&confirm('Encontrei um rastreio em andamento. Continuar?')){Object.assign(tracking,t);tracking.watchId=null;startGPS();renderGPSBanner()}else{localStorage.removeItem(TRACKING_KEY)}}}catch(e){}} function saveTrackingState(){try{const{watchId,wakeLock,...r}=tracking;localStorage.setItem(TRACKING_KEY,JSON.stringify(r))}catch(e){}} -async function requestWakeLock(){try{if('wakeLock' in navigator)tracking.wakeLock=await navigator.wakeLock.request('screen')}catch(e){}} +async function requestWakeLock(){try{if('wakeLock' in navigator){tracking.wakeLock=await navigator.wakeLock.request('screen');tracking.wakeLock.addEventListener('release',()=>{tracking.wakeLock=null;if(tracking.active)setTimeout(()=>{if(tracking.active&&!tracking.wakeLock)requestWakeLock()},1000)})}}catch(e){console.warn('wake lock tracking:',e)}} async function releaseWakeLock(){try{if(tracking.wakeLock){await tracking.wakeLock.release();tracking.wakeLock=null}}catch(e){}} -function startGPS(){if(!navigator.geolocation){toast('GPS não disponível');return}tracking.watchId=navigator.geolocation.watchPosition(onGPSUpdate,e=>{toast('Erro GPS: '+e.message)},batteryGPSOptions())} +function startGPS(){if(!navigator.geolocation){toast('GPS não disponível');return}let r=0;function tryStart(){tracking.watchId=navigator.geolocation.watchPosition(p=>{r=0;onGPSUpdate(p)},e=>{if(e.code===e.PERMISSION_DENIED){toast('GPS sem permissão — rastreio interrompido');return}r++;const d=Math.min(1000*Math.pow(2,r-1),30000);toast(`GPS perdido (#${r}) — retentando em ${d/1000}s`);if(tracking.watchId)navigator.geolocation.clearWatch(tracking.watchId);setTimeout(()=>{if(tracking.active)tryStart()},d)},batteryGPSOptions())}tryStart()} function onGPSUpdate(pos){ const p={lat:pos.coords.latitude,lng:pos.coords.longitude,ts:Date.now(),spd:pos.coords.speed||0,acc:pos.coords.accuracy}; @@ -1828,7 +1828,7 @@ const anchorWatch={active:false,watchId:null,startedAt:null,anchorPos:null,swing let anchorMap=null,anchorMarker=null,swingMarker=null,anchorBoatMarker=null,anchorCircle=null,anchorLine=null,swingAnchorLine=null,anchorTimer=null,autoRecenterTimer=null; let pendingAnchorPos=null; -async function requestAnchorWakeLock(){try{if('wakeLock' in navigator)anchorWatch.wakeLock=await navigator.wakeLock.request('screen')}catch(e){}} +async function requestAnchorWakeLock(){try{if('wakeLock' in navigator){anchorWatch.wakeLock=await navigator.wakeLock.request('screen');anchorWatch.wakeLock.addEventListener('release',()=>{anchorWatch.wakeLock=null;if(anchorWatch.active)setTimeout(()=>{if(anchorWatch.active&&!anchorWatch.wakeLock)requestAnchorWakeLock()},1000)})}}catch(e){console.warn('wake lock anchor:',e)}} async function releaseAnchorWakeLock(){try{if(anchorWatch.wakeLock){await anchorWatch.wakeLock.release();anchorWatch.wakeLock=null}}catch(e){}} function loadAnchorState(){try{const raw=localStorage.getItem(ANCHOR_KEY);if(raw){const a=JSON.parse(raw);if(a.active&&confirm('Há uma vigia de fundeio em andamento. Continuar?')){Object.assign(anchorWatch,a);anchorWatch.watchId=null;anchorWatch.wakeLock=null;startAnchorGPS();anchorTimer=setInterval(updateAnchorUI,1000);requestAnchorWakeLock();renderAnchorBanner()}else{localStorage.removeItem(ANCHOR_KEY)}}}catch(e){}} @@ -1899,11 +1899,22 @@ async function confirmAnchor(){ } function startAnchorGPS(){ - anchorWatch.watchId=navigator.geolocation.watchPosition( - onAnchorGPSUpdate, - err=>{toast('GPS perdido: '+err.message)}, - batteryGPSOptions() - ); + let retry=0; + function tryStart(){ + anchorWatch.watchId=navigator.geolocation.watchPosition( + pos=>{retry=0;onAnchorGPSUpdate(pos)}, + err=>{ + if(err.code===err.PERMISSION_DENIED){toast('GPS sem permissão — vigia COMPROMETIDA');return} + retry++; + const delay=Math.min(1000*Math.pow(2,retry-1),30000); + toast(`Vigia GPS perdido (#${retry}) — retentando em ${delay/1000}s`); + if(anchorWatch.watchId)navigator.geolocation.clearWatch(anchorWatch.watchId); + setTimeout(()=>{if(anchorWatch.active)tryStart()},delay); + }, + batteryGPSOptions() + ); + } + tryStart(); } function onAnchorGPSUpdate(pos){ @@ -3277,7 +3288,7 @@ function onZoneEnter(z){ setTimeout(stopAlarmSound,3500); // dispatch webhooks com mensagem de zona const text=`⛔ ${state.boat.name||'Veleiro'} entrou em zona PROIBIDA: ${z.name}`; - dispatchWebhooks(text).catch(()=>{}); + dispatchWebhooks(text).catch(e=>{console.error('webhook zone alarm failed:',e);toast('Falha ao enviar alarme remoto')}); }else{ if('vibrate' in navigator)navigator.vibrate([200,80,200]); }