diff --git a/html/sw.js b/html/sw.js index fc74eb8..0a2074d 100644 --- a/html/sw.js +++ b/html/sw.js @@ -5,15 +5,16 @@ worker only caches the static app shell so the pages open without a network. Strategy: - • /api/* and non-GET → never touched (pass straight to the network; offline + • /api/* and non-GET → never touched (pass straight to the network; offline reads fall back to the app's localStorage cache, writes queue in the outbox). - • same-origin GET → stale-while-revalidate (instant from cache, refreshed - in the background when online). + • HTML / CSS / JS → network-first, cache as fallback. These reference each + other, so a page must never run against a stale sibling. + • images / icons / manifest → stale-while-revalidate (instant from cache). */ 'use strict'; // Bumped when the shell file list changes, so clients fetch the new assets // instead of serving a half-old shell from the previous cache. -const CACHE = 'wp-suite-shell-v3'; +const CACHE = 'wp-suite-shell-v4'; const SHELL = [ '/', '/index.html', '/work-package-suite.html', '/wp-creation-index.html', '/field.html', '/login.html', '/admin.html', @@ -43,6 +44,17 @@ self.addEventListener('activate', (e) => { ); }); +// Code (HTML / CSS / JS) is fetched NETWORK-FIRST, falling back to the cache when +// offline. Everything else (images, icons, the manifest) stays cache-first, which is +// where offline speed actually comes from. +// +// Why not cache-first for code: these files reference each other, and the cache +// stores them as independent entries. Cache-first served whichever copy of each file +// happened to be stored, so a browser could run new HTML against old CSS — which is +// exactly how the embedded creator once collapsed to a 300x150 iframe. A page must +// only ever run against the stylesheet and scripts it shipped with. +const CODE_RE = /\.(html|css|js)$|\/$/i; + self.addEventListener('fetch', (e) => { const req = e.request; if (req.method !== 'GET') return; // outbox owns writes @@ -50,6 +62,23 @@ self.addEventListener('fetch', (e) => { if (url.origin !== self.location.origin) return; // third-party: default if (url.pathname.startsWith('/api/')) return; // never cache the API + const isCode = CODE_RE.test(url.pathname); + + if (isCode) { + e.respondWith( + fetch(req) + .then((res) => { + if (res && res.ok) { + const copy = res.clone(); + caches.open(CACHE).then((c) => c.put(req, copy)); + } + return res; + }) + .catch(() => caches.match(req)) // offline → last good copy + ); + return; + } + e.respondWith( caches.match(req).then((cached) => { const network = fetch(req) diff --git a/html/work-package-suite-app.js b/html/work-package-suite-app.js index 3812046..e11ecce 100644 --- a/html/work-package-suite-app.js +++ b/html/work-package-suite-app.js @@ -494,17 +494,38 @@ function applyEmbedLayout(on){ if(area) area.classList.toggle('embed-full', !!on); if(frame) frame.classList.toggle('fill', !!on); document.body.classList.toggle('embed-full', !!on); - if(on) measureChrome(); + sizeWPFrame(on); } -// Height of the app bar + tab strip, so the iframe can be exactly the rest. -function measureChrome(){ +// Size the frame with INLINE styles, not only CSS classes. Inline wins over any +// stylesheet — including a stale cached one — so the creator can't collapse to the +// 300x150 default iframe box if the CSS and the HTML are ever out of step. +function sizeWPFrame(on){ + const frame = document.getElementById('wp-frame'); + if(!frame) return; + frame.style.width = '100%'; + frame.style.border = '0'; + if(on){ + const h = viewportMinusChrome(); + document.documentElement.style.setProperty('--wp-chrome-h', (window.innerHeight - h) + 'px'); + frame.style.height = h + 'px'; + frame.style.minHeight = '0'; + } else { + frame.style.height = ''; + frame.style.minHeight = 'calc(100vh - 200px)'; + } +} + +// Whatever is left of the window below the app bar + tab strip. +function viewportMinusChrome(){ const hdr = document.querySelector('.header'); const nav = document.querySelector('.main-nav'); - const h = (hdr ? hdr.offsetHeight : 48) + (nav ? nav.offsetHeight : 48); - document.documentElement.style.setProperty('--wp-chrome-h', h + 'px'); + const chrome = (hdr ? hdr.offsetHeight : 48) + (nav ? nav.offsetHeight : 48); + return Math.max(320, window.innerHeight - chrome); } -window.addEventListener('resize', () => { if(document.body.classList.contains('embed-full')) measureChrome(); }, {passive:true}); +window.addEventListener('resize', () => { + if(document.body.classList.contains('embed-full')) sizeWPFrame(true); +}, {passive:true}); // Show the gate or the embedded Work Package Creator depending on SOP status. // wantDash=true opens the creator straight to the dashboard view. diff --git a/html/work-package-suite.html b/html/work-package-suite.html index 5ec603d..fb46063 100644 --- a/html/work-package-suite.html +++ b/html/work-package-suite.html @@ -372,7 +372,13 @@ - + +