MAI CALL - step 12

Passo 12 completo. Build limpo, AC server-side totalmente verificado.

O que foi implementado:

lib/queue/ — camada de persistência offline:

db.ts — Dexie 4 com tabelas pending e deadLetters
broadcast.ts — BroadcastChannel helper (mai-call-sync) para comunicar entre tabs
sync.ts — loop de sync com retry/backoff: signPhotoUpload → PUT MinIO → create; 409 = sucesso; 4xx = dead-letter; erros de rede = paragem + retry na próxima volta
SyncProvider — React Context que:

Arranca sync ao reconectar (online event + visibilitychange)
Polling de 10s como fallback
Regista Background Sync API quando disponível
Expõe pendingCount / deadLetterCount via useSyncState()
Formulário (/maintenance/new) — refatorado: ao submeter, escreve em IndexedDB e navega imediatamente para /sent sem esperar pelo servidor. O SyncProvider processa a fila em background.

Feedback visual:

SyncChip na home: "Tudo sincronizado" / "N pedidos por enviar" / erro dead-letter
/maintenance/sent: mostra "Em fila" (Clock) ou "Enviado" (CheckCircle2) reactivamente via BroadcastChannel
Workbox (@ducanh2912/next-pwa) — app shell precaching ativo, para que o app carregue mesmo sem rede depois da primeira visita.
This commit is contained in:
pedrogomes committed 2026-05-16 16:55:59 +01:00
1 parent 617c81357f
commit b7e3208eb2
17 files changed
+3965 -75

No files matched your search

+1
View File
@@ -0,0 +1 @@
if(!self.define){let e,s={};const n=(n,t)=>(n=new URL(n+".js",t).href,s[n]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()}).then(()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(t,a)=>{const c=e||("document"in self?document.currentScript.src:"")||location.href;if(s[c])return;let i={};const r=e=>n(e,c),o={module:{uri:c},exports:i,require:r};s[c]=Promise.all(t.map(e=>o[e]||r(e))).then(e=>(a(...e),i))}}define(["./workbox-5194662c"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/dynamic-css-manifest.json",revision:"d751713988987e9331980363e24189ce"},{url:"/_next/static/E24ILEIxTT35zMD-Lqk4Z/_buildManifest.js",revision:"57b739a594f4a81ede20407d3eb2f67f"},{url:"/_next/static/E24ILEIxTT35zMD-Lqk4Z/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/2-1fec31df5cccd978.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/207-5830eb1c236921c4.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/366-9f25a484ec1a0ad1.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/453-4f1bf64721542ff1.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/5e89d14c-29448a56658b5c05.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/69.85d4fd0d1cced6f3.js",revision:"85d4fd0d1cced6f3"},{url:"/_next/static/chunks/699-e0dfe35c99f38031.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/718-c610b317eb50f713.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/864.76d176e27ab6eb2e.js",revision:"76d176e27ab6eb2e"},{url:"/_next/static/chunks/app/_not-found/page-27b9c7c9c84d3c3e.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/app/api/auth/%5B...nextauth%5D/route-eb91e6bcaad07c95.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/app/api/trpc/%5Btrpc%5D/route-ac9de9091f551860.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/app/error-b113a02aedbc7163.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/app/layout-4f117dbcc88e9f9d.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/app/maintenance/new/page-5955e116c9dd3501.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/app/maintenance/sent/page-45ea63d2cf48c8ea.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/app/not-found-62acb79750232d66.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/app/page-81637445222f88bf.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/app/select-operator/page-dab79b3f8b754bfc.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/framework-e9d381d8cb9498da.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/main-app-06906c35d26f4210.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/main-ec3a7b1cd2097aab.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/pages/_app-f69aca65b88454b7.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/pages/_error-df94080b8a9a72e7.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-51c1b9324c7c4d52.js",revision:"E24ILEIxTT35zMD-Lqk4Z"},{url:"/_next/static/css/3295c4d4c839af1a.css",revision:"3295c4d4c839af1a"},{url:"/icon-192.svg",revision:"4b4dfb506ed7eebc6e763bf8f09d75e6"},{url:"/icon-512.svg",revision:"d247a6555ec14dd052372bb297c43ae4"},{url:"/icon-maskable.svg",revision:"2fd823b9a4b1bec7aab84a22921c75c6"},{url:"/manifest.webmanifest",revision:"135f7a5aaec94ede8eb024ac054e5763"},{url:"/swe-worker-5c72df51bb1f6ee0.js",revision:"76fdd3369f623a3edcf74ce2200bfdd0"}],{ignoreURLParametersMatching:[/^utm_/,/^fbclid$/]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:function(e){return _ref.apply(this,arguments)}}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:2592e3})]}),"GET"),e.registerRoute(/\/_next\/static.+\.js$/i,new e.CacheFirst({cacheName:"next-static-js-assets",plugins:[new e.Line truncated
@@ -0,0 +1 @@
(()=>{"use strict";self.onmessage=async e=>{switch(e.data.type){case"__START_URL_CACHE__":{let t=e.data.url,a=await fetch(t);if(!a.redirected)return(await caches.open("start-url")).put(t,a);return Promise.resolve()}case"__FRONTEND_NAV_CACHE__":{let t=e.data.url,a=await caches.open("pages");if(await a.match(t,{ignoreSearch:!0}))return;let s=await fetch(t);if(!s.ok)return;if(a.put(t,s.clone()),e.data.shouldCacheAggressively&&s.headers.get("Content-Type")?.includes("text/html"))try{let e=await s.text(),t=[],a=await caches.open("static-style-assets"),r=await caches.open("next-static-js-assets"),c=await caches.open("static-js-assets");for(let[s,r]of e.matchAll(/<link.*?href=['"](.*?)['"].*?>/g))/rel=['"]stylesheet['"]/.test(s)&&t.push(a.match(r).then(e=>e?Promise.resolve():a.add(r)));for(let[,a]of e.matchAll(/<script.*?src=['"](.*?)['"].*?>/g)){let e=/\/_next\/static.+\.js$/i.test(a)?r:c;t.push(e.match(a).then(t=>t?Promise.resolve():e.add(a)))}return await Promise.all(t)}catch{}return Promise.resolve()}default:return Promise.resolve()}}})();
@@ -0,0 +1 @@
define(["exports"],function(t){"use strict";try{self["workbox:core:7.0.0"]&&_()}catch(t){}const e=(t,...e)=>{let s=t;return e.length>0&&(s+=` :: ${JSON.stringify(e)}`),s};class s extends Error{constructor(t,s){super(e(t,s)),this.name=t,this.details=s}}try{self["workbox:routing:7.0.0"]&&_()}catch(t){}const n=t=>t&&"object"==typeof t?t:{handle:t};class r{constructor(t,e,s="GET"){this.handler=n(e),this.match=t,this.method=s}setCatchHandler(t){this.catchHandler=n(t)}}class i extends r{constructor(t,e,s){super(({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)},e,s)}}class a{constructor(){this.t=new Map,this.i=new Map}get routes(){return this.t}addFetchListener(){self.addEventListener("fetch",t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)})}addCacheListener(){self.addEventListener("message",t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map(e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})}));t.waitUntil(s),t.ports&&t.ports[0]&&s.then(()=>t.ports[0].postMessage(!0))}})}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:r,route:i}=this.findMatchingRoute({event:e,request:t,sameOrigin:n,url:s});let a=i&&i.handler;const o=t.method;if(!a&&this.i.has(o)&&(a=this.i.get(o)),!a)return;let c;try{c=a.handle({url:s,request:t,event:e,params:r})}catch(t){c=Promise.reject(t)}const h=i&&i.catchHandler;return c instanceof Promise&&(this.o||h)&&(c=c.catch(async n=>{if(h)try{return await h.handle({url:s,request:t,event:e,params:r})}catch(t){t instanceof Error&&(n=t)}if(this.o)return this.o.handle({url:s,request:t,event:e});throw n})),c}findMatchingRoute({url:t,sameOrigin:e,request:s,event:n}){const r=this.t.get(s.method)||[];for(const i of r){let r;const a=i.match({url:t,sameOrigin:e,request:s,event:n});if(a)return r=a,(Array.isArray(r)&&0===r.length||a.constructor===Object&&0===Object.keys(a).length||"boolean"==typeof a)&&(r=void 0),{route:i,params:r}}return{}}setDefaultHandler(t,e="GET"){this.i.set(e,n(t))}setCatchHandler(t){this.o=n(t)}registerRoute(t){this.t.has(t.method)||this.t.set(t.method,[]),this.t.get(t.method).push(t)}unregisterRoute(t){if(!this.t.has(t.method))throw new s("unregister-route-but-not-found-with-method",{method:t.method});const e=this.t.get(t.method).indexOf(t);if(!(e>-1))throw new s("unregister-route-route-not-registered");this.t.get(t.method).splice(e,1)}}let o;const c=()=>(o||(o=new a,o.addFetchListener(),o.addCacheListener()),o);function h(t,e,n){let a;if("string"==typeof t){const s=new URL(t,location.href);a=new r(({url:t})=>t.href===s.href,e,n)}else if(t instanceof RegExp)a=new i(t,e,n);else if("function"==typeof t)a=new r(t,e,n);else{if(!(t instanceof r))throw new s("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});a=t}return c().registerRoute(a),a}try{self["workbox:strategies:7.0.0"]&&_()}catch(t){}const u={cacheWillUpdate:async({response:t})=>200===t.status||0===t.status?t:null},l={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},f=t=>[l.prefix,t,l.suffix].filter(t=>t&&t.length>0).join("-"),w=t=>t||f(l.precache),d=t=>t||f(l.runtime);function p(t,e){const s=new URL(t);for(const t of e)s.searchParams.delete(t);return s.href}class y{constructor(){this.promise=new Promise((t,e)=>{this.resolve=t,this.reject=e})}}const g=new Set;function m(t){return"string"==typeof t?new Request(t):t}class v{constructor(t,e){this.h={},Object.assign(this,e),this.event=e.event,this.u=t,this.l=new y,this.p=[],this.m=[...t.plugins],this.v=new Map;for(const t of this.m)this.v.set(t,{});this.event.waitUntil(this.l.promise)}async fetch(t){const{event:e}=this;let n=m(t);if("navigate"===n.mode&&e instanceof FetchEvent&&e.preloadResponse){const t=await e.preloadResponse;if(t)return t}const r=this.hasCallback("fetchDidFail")?n.clone():null;try{for(const t of this.iterateCallbacks("requestWillFetch"))n=await t({request:n.clone(),event:e})}catch(t){if(t instanceof Error)throw new s("plugin-error-request-will-fetch",{thrownErrorMessage:t.message})}const i=n.clone();try{let t;t=await fetch(n,"navigate"===n.mode?void 0:this.u.fetchOptions);for(const s of this.iterateCallbacks("fetchDidSucceed"))t=await s({event:e,request:i,response:t});return t}catch(t){throw r&&await this.runCallbacks("fetchDidFail",{error:t,event:e,originalRequest:r.clone(),request:i.clone()}),t}}async fetchAndCachePut(t){const e=await this.fetch(t),s=e.clone();return this.waitUntil(this.cachePut(t,s)),e}async cacheMatch(t){const e=m(t);let s;const{cacheName:n,matchOptions:r}=this.u,i=await this.getCacheKey(e,"read"),a=Object.assign(Object.assign({},r),{cacheName:n});s=await caches.match(i,a);for(const t of this.iterateCLine truncated