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 @@
-
+
+