772e23ded8
Deploy to route.crispygoat.com / deploy (push) Successful in 4m7s
The Apple HIG polish pass (liquid-glass chrome, iOS SegmentedControl, ThresholdMeter) was already on origin/main and live, but anyone with the /water PWA installed on their home screen kept seeing the OLD UI because the SW cache name never bumped. Old SW had field-shell-v1 baked into its bytes for every prior deploy. The activate handler only deletes caches that aren't SHELL_CACHE or DATA_CACHE and start with 'field-'. Same name = no eviction = stale HTML + chunks forever. Bumping SHELL_CACHE to 'field-shell-v2' makes the SW bytes change, which triggers the normal SW upgrade flow: install → skipWaiting → activate → claim clients The activate handler then evicts field-shell-v1 (it's now a foreign field-* cache). On next navigation the new SHELL_CACHE is repopulated with the fresh /water HTML + static assets, and the user gets the new UI.
135 lines
4.1 KiB
JavaScript
135 lines
4.1 KiB
JavaScript
// public/sw-field.js — Tuxedo field-worker service worker.
|
|
//
|
|
// Cycle 6 sub-PWA, scoped to /water. Tighter cache strategy than
|
|
// the main shell SW because:
|
|
// - The field app is a thin surface (PIN, headgate list, log form,
|
|
// time tab). Few async chunks; mostly static + the latest data
|
|
// snapshot.
|
|
// - "Offline" here means "still see the cached PIN screen + headgate
|
|
// list" — water entry without connectivity is a degraded but
|
|
// non-blocking experience (the form queue handles drafts).
|
|
//
|
|
// Cache names are deliberately prefixed with `field-` so they never
|
|
// collide with the main `rc-shell-v*` / `rc-data-v*` caches.
|
|
|
|
// Bumped to v2 after the Apple HIG polish pass (liquid-glass chrome,
|
|
// iOS SegmentedControl, ThresholdMeter). The activate handler keeps
|
|
// only this name + DATA_CACHE — anything else starting with `field-`
|
|
// (including the old v1) is evicted on the next SW activation, which
|
|
// happens automatically because the bytes of this file changed.
|
|
const SHELL_CACHE = "field-shell-v2";
|
|
const DATA_CACHE = "field-data-v1";
|
|
const OFFLINE_URL = "/offline.html";
|
|
|
|
const SHELL_ASSETS = [
|
|
"/water",
|
|
"/manifest-field.json",
|
|
"/favicon.svg",
|
|
"/og-default.svg",
|
|
"/offline.html",
|
|
"/icons/icon-192x192.png",
|
|
"/icons/icon-512x512.png",
|
|
];
|
|
|
|
self.addEventListener("install", (event) => {
|
|
event.waitUntil(
|
|
caches.open(SHELL_CACHE).then((cache) => cache.addAll(SHELL_ASSETS))
|
|
);
|
|
self.skipWaiting();
|
|
});
|
|
|
|
self.addEventListener("activate", (event) => {
|
|
// Only manage field-* caches — never touch the main app's caches.
|
|
event.waitUntil(
|
|
Promise.all([
|
|
caches.keys().then((cacheNames) => {
|
|
const toDelete = [];
|
|
for (const name of cacheNames) {
|
|
if (
|
|
name !== SHELL_CACHE &&
|
|
name !== DATA_CACHE &&
|
|
name.startsWith("field-")
|
|
) {
|
|
toDelete.push(caches.delete(name));
|
|
}
|
|
}
|
|
return Promise.all(toDelete);
|
|
}),
|
|
self.clients.claim(),
|
|
])
|
|
);
|
|
});
|
|
|
|
self.addEventListener("fetch", (event) => {
|
|
const req = event.request;
|
|
if (req.method !== "GET") return;
|
|
const url = new URL(req.url);
|
|
if (url.origin !== self.location.origin) return;
|
|
|
|
// Out-of-scope requests: skip (let the main app SW handle them
|
|
// if it's installed for that scope; fall through to network).
|
|
if (!url.pathname.startsWith("/water")) return;
|
|
|
|
// Navigations → network-first, fall back to cache, fall back to offline page.
|
|
if (req.mode === "navigate") {
|
|
event.respondWith(
|
|
fetch(req)
|
|
.then((res) => {
|
|
const clone = res.clone();
|
|
caches.open(SHELL_CACHE).then((cache) => cache.put(req, clone));
|
|
return res;
|
|
})
|
|
.catch(() =>
|
|
caches.match(req).then(
|
|
(cached) => cached ?? caches.match(OFFLINE_URL),
|
|
),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Static assets → cache-first
|
|
if (
|
|
url.pathname.startsWith("/_next/static/") ||
|
|
url.pathname.startsWith("/icons/") ||
|
|
url.pathname.endsWith(".svg") ||
|
|
url.pathname.endsWith(".woff2")
|
|
) {
|
|
event.respondWith(
|
|
caches.match(req).then((cached) => {
|
|
if (cached) return cached;
|
|
return fetch(req).then((res) => {
|
|
const clone = res.clone();
|
|
if (res.status === 200) {
|
|
caches.open(SHELL_CACHE).then((cache) => cache.put(req, clone));
|
|
}
|
|
return res;
|
|
});
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// /api/water* GETs → stale-while-revalidate (so the headgate list
|
|
// and worker availability can hydrate from cache while we re-fetch).
|
|
if (url.pathname.startsWith("/api/water")) {
|
|
event.respondWith(
|
|
caches.open(DATA_CACHE).then((cache) =>
|
|
cache.match(req).then((cached) => {
|
|
const network = fetch(req)
|
|
.then((res) => {
|
|
if (res.status === 200) cache.put(req, res.clone());
|
|
return res;
|
|
})
|
|
.catch(() => cached);
|
|
return cached ?? network;
|
|
}),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Default: try network, fall back to cache.
|
|
event.respondWith(fetch(req).catch(() => caches.match(req)));
|
|
});
|