Cambios implementados: 1. Lazy loading de imágenes: - catalog.js: loading="lazy" decoding="async" en part cards y detail panel - inventory.js: lazy loading en imagen de detalle de item 2. Minificación de assets: - scripts/minify-assets.sh: minifica JS (terser) y CSS para POS y Dashboard - 25 archivos .min.js + 5 .min.css generados en pos/static/ - 14 archivos .min.js + 8 .min.css generados en dashboard/ 3. Nginx auto-serve minified: - try_files $1.min.js antes de servir .js original - try_files $1.min.css antes de servir .css original - Transparente para los templates HTML (cero cambios en HTML) 4. Cache warming script: - scripts/warm_vehicle_cache.py: pobla Redis con vehicle info por batches - Mitiga DISTINCT ON + 4 JOINs sobre 2B filas - Corre en background, procesa ~1.5M parts Tests: 73/73 pasando
1 line
4.6 KiB
JavaScript
1 line
4.6 KiB
JavaScript
!function(){"use strict";var n="sync_queue",e="inventory_cache",t="cached_parts",r=null;function o(){return new Promise((function(o,c){if(r)o(r);else{var i=indexedDB.open("nexus_pos_offline",2);i.onupgradeneeded=function(r){var o=r.target.result;if(o.objectStoreNames.contains(n)||o.createObjectStore(n,{keyPath:"id",autoIncrement:!0}),!o.objectStoreNames.contains(e)){var c=o.createObjectStore(e,{keyPath:"item_id"});c.createIndex("sku","sku",{unique:!1}),c.createIndex("name","name",{unique:!1})}if(!o.objectStoreNames.contains(t)){var i=o.createObjectStore(t,{keyPath:"part_number"});i.createIndex("name","name",{unique:!1}),i.createIndex("category","category",{unique:!1})}},i.onsuccess=function(n){r=n.target.result,o(r)},i.onerror=function(){c(i.error)}}}))}function c(){return o().then((function(e){return new Promise((function(t,r){var o=e.transaction(n,"readonly").objectStore(n).getAll();o.onsuccess=function(){t(o.result)},o.onerror=function(){r(o.error)}}))})).then((function(e){if(!e.length)return Promise.resolve({synced:0});var t=Promise.resolve(),r=0,c=0;return e.forEach((function(e){t=t.then((function(){var t={method:e.method,headers:{"Content-Type":"application/json"}};return e.body&&(t.body=JSON.stringify(e.body)),fetch(e.url,t).then((function(t){if(t.ok)return r++,i=e.id,o().then((function(e){return new Promise((function(t,r){var o=e.transaction(n,"readwrite");o.objectStore(n).delete(i),o.oncomplete=function(){t()},o.onerror=function(){r(o.error)}}))}));var i;c++})).catch((function(){c++}))}))})),t.then((function(){return{synced:r,failed:c,total:e.length}}))}))}window.addEventListener("online",(function(){console.log("[SyncEngine] Online — processing queue..."),c().then((function(n){n.synced>0&&console.log("[SyncEngine] Synced "+n.synced+" operations")})).catch((function(n){console.error("[SyncEngine] Queue processing error:",n)}))})),window.addEventListener("offline",(function(){console.log("[SyncEngine] Offline — operations will be queued")})),"serviceWorker"in navigator&&navigator.serviceWorker.addEventListener("message",(function(n){n.data&&"SYNC_REQUESTED"===n.data.type&&c()})),o().catch((function(n){console.error("[SyncEngine] Failed to open IndexedDB:",n)})),window.SyncEngine={queueOperation:function(e,t,r){return o().then((function(o){return new Promise((function(c,i){var a=o.transaction(n,"readwrite");a.objectStore(n).add({url:e,method:t,body:r||null,timestamp:Date.now()}),a.oncomplete=function(){c()},a.onerror=function(){i(a.error)}}))}))},processQueue:c,getQueueCount:function(){return o().then((function(e){return new Promise((function(t,r){var o=e.transaction(n,"readonly").objectStore(n).count();o.onsuccess=function(){t(o.result)},o.onerror=function(){r(o.error)}}))}))},isOnline:function(){return navigator.onLine},cacheInventory:function(){return fetch("/pos/api/sync/inventory").then((function(n){if(!n.ok)throw new Error("Sync inventory failed: "+n.status);return n.json()})).then((function(n){var t=n.items||[];return o().then((function(n){return new Promise((function(r,o){var c=n.transaction(e,"readwrite"),i=c.objectStore(e);i.clear(),t.forEach((function(n){i.put(n)})),c.oncomplete=function(){r(t.length)},c.onerror=function(){o(c.error)}}))}))}))},getCachedInventory:function(n){return o().then((function(t){return new Promise((function(r,o){var c=t.transaction(e,"readonly").objectStore(e).getAll();c.onsuccess=function(){var e=c.result;if(n){var t=n.toLowerCase(),o=e.filter((function(n){return n.sku&&-1!==n.sku.toLowerCase().indexOf(t)||n.name&&-1!==n.name.toLowerCase().indexOf(t)||n.barcode&&-1!==n.barcode.toLowerCase().indexOf(t)}));r(o)}else r(e)},c.onerror=function(){o(c.error)}}))}))},cacheTopParts:function(){return fetch("/pos/api/sync/top-parts").then((function(n){if(!n.ok)throw new Error("Sync top-parts failed: "+n.status);return n.json()})).then((function(n){var e=n.parts||[];return o().then((function(n){return new Promise((function(r,o){var c=n.transaction(t,"readwrite"),i=c.objectStore(t);i.clear(),e.forEach((function(n){i.put(n)})),c.oncomplete=function(){console.log("[SyncEngine] Cached "+e.length+" top parts"),r(e.length)},c.onerror=function(){o(c.error)}}))}))}))},searchCachedParts:function(n){return o().then((function(e){return new Promise((function(r,o){var c=e.transaction(t,"readonly").objectStore(t).getAll();c.onsuccess=function(){var e=c.result;if(n){var t=n.toLowerCase(),o=e.filter((function(n){return n.part_number&&-1!==n.part_number.toLowerCase().indexOf(t)||n.name&&-1!==n.name.toLowerCase().indexOf(t)||n.category&&-1!==n.category.toLowerCase().indexOf(t)||n.brand&&-1!==n.brand.toLowerCase().indexOf(t)}));r(o)}else r(e)},c.onerror=function(){o(c.error)}}))}))}}}(); |