feat(reliability): vigia reconnect (Wake Lock release + GPS retry exponencial)
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>
This commit is contained in:
parent
5b02feae50
commit
78c6de538a
3 changed files with 50 additions and 24 deletions
16
HANDOFF.md
16
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
|
||||
|
|
|
|||
|
|
@ -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(){
|
||||
let retry=0;
|
||||
function tryStart(){
|
||||
anchorWatch.watchId=navigator.geolocation.watchPosition(
|
||||
onAnchorGPSUpdate,
|
||||
err=>{toast('GPS perdido: '+err.message)},
|
||||
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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(){
|
||||
let retry=0;
|
||||
function tryStart(){
|
||||
anchorWatch.watchId=navigator.geolocation.watchPosition(
|
||||
onAnchorGPSUpdate,
|
||||
err=>{toast('GPS perdido: '+err.message)},
|
||||
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]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue