Three offline and toolbar fixes, and no service worker in development
The drawing rail's option bar covered the Transform button. ol-ext positions the flyout with `left: 100%`, which is meant to clear the whole rail; the rule that makes that work sets the rail's controls to `position: static`, and it only tied with ol-ext's own `.ol-control.ol-bar .ol-control` on specificity, so stylesheet order decided it and ol-ext's injected sheet came last. The flyout was therefore measured against the Select button and landed on the second column. The selector now carries enough classes to win outright. This is the same failure as v16, in a second place. The local database did not open when the device was offline. The fetch handler skipped every request whose destination was 'worker', to stay clear of the Vite dev server's module plumbing — but in a build SQLocal's worker is a hashed file under /assets/, so it was never cached either. With the network down the request failed, initSchema() never resolved, and everything waiting on the database stopped without an error. The shell, the map and the tiles all worked, so the application looked like it was surviving offline while nothing could be saved or read. The skip now covers only the dev server's own paths; a built worker is cached like any other asset. Verified both ways against a production build with the preview server stopped: before, the schema hangs and the table list is empty; after, the schema completes and all twelve tables are listed. The service worker is no longer registered on the development server. It cannot do its job there anyway — the modules it would need come from node_modules, which it deliberately keeps clear of — while it does cache main.js and src/*.js cache-first, so it can serve a stale module and fight hot reloading. In Safari it also costs the local database outright: a worker script that reaches the browser through a controlling service worker loses its cross-origin-isolation headers, and SQLocal's worker is refused. Because switching registration off does nothing for a worker already installed, initPWA now unregisters whatever it finds and clears the app caches when registration is off. That clearing runs on every load rather than only on the one that unregistered: an unregistered worker goes on controlling its page until the next navigation and refills the caches behind us. The tile caches are left alone — they hold what an officer downloaded for field work. CACHE_VERSION goes to v27 for the first two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4132a12b38
commit
4809ef324a
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
dist/assets/index-C6kuA4o8.js.map
vendored
Normal file
1
dist/assets/index-C6kuA4o8.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/index-Ddi_Zl4K.js.map
vendored
1
dist/assets/index-Ddi_Zl4K.js.map
vendored
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
||||
import{d as f,z as d}from"./index-Ddi_Zl4K.js";import"./vite-helpers-BlTxHScW.js";import"./bootstrap-D1-uvFxm.js";import"./openlayers-D5-VSKzq.js";import"./ol-ext-DV70ljNY.js";function p(e){return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g,"")}function h(e){let l="",o=e;for(;o>=0;)l=String.fromCharCode(o%26+65)+l,o=Math.floor(o/26)-1;return l}function x(e){return e&&!Array.isArray(e)&&Array.isArray(e.cells)?{cells:e.cells,bold:!!e.bold}:{cells:Array.isArray(e)?e:[e],bold:!1}}function u(e){const l=[];return e.forEach((o,n)=>{const{cells:t,bold:a}=x(o),r=a?' s="1"':"",i=t.map((s,c)=>{const m=`${h(c)}${n+1}`;return s==null||s===""?`<c r="${m}"${r}/>`:typeof s=="number"&&Number.isFinite(s)?`<c r="${m}"${r}><v>${s}</v></c>`:`<c r="${m}"${r} t="inlineStr"><is><t xml:space="preserve">${p(s)}</t></is></c>`}).join("");l.push(`<row r="${n+1}">${i}</row>`)}),`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
import{d as f,z as d}from"./index-C6kuA4o8.js";import"./vite-helpers-BlTxHScW.js";import"./bootstrap-D1-uvFxm.js";import"./openlayers-D5-VSKzq.js";import"./ol-ext-DV70ljNY.js";function p(e){return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g,"")}function h(e){let l="",o=e;for(;o>=0;)l=String.fromCharCode(o%26+65)+l,o=Math.floor(o/26)-1;return l}function x(e){return e&&!Array.isArray(e)&&Array.isArray(e.cells)?{cells:e.cells,bold:!!e.bold}:{cells:Array.isArray(e)?e:[e],bold:!1}}function u(e){const l=[];return e.forEach((o,n)=>{const{cells:t,bold:a}=x(o),r=a?' s="1"':"",i=t.map((s,c)=>{const m=`${h(c)}${n+1}`;return s==null||s===""?`<c r="${m}"${r}/>`:typeof s=="number"&&Number.isFinite(s)?`<c r="${m}"${r}><v>${s}</v></c>`:`<c r="${m}"${r} t="inlineStr"><is><t xml:space="preserve">${p(s)}</t></is></c>`}).join("");l.push(`<row r="${n+1}">${i}</row>`)}),`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>${l.join("")}</sheetData></worksheet>`}const y=`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
||||
<fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts>
|
||||
@ -27,4 +27,4 @@ ${n.map(t=>`<Relationship Id="rId${t.id}" Type="http://schemas.openxmlformats.or
|
||||
`)}
|
||||
<Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
||||
</Relationships>`,o["xl/styles.xml"]=y,d(o,"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}function S(e,l){f(g(e),l)}export{g as buildXlsxBlob,S as exportXlsx};
|
||||
//# sourceMappingURL=xlsx-CR1gFYlV.js.map
|
||||
//# sourceMappingURL=xlsx-CYAxSxVe.js.map
|
||||
File diff suppressed because one or more lines are too long
11
dist/index.html
vendored
11
dist/index.html
vendored
@ -1815,8 +1815,15 @@
|
||||
block to the rail itself, so `left: 100%` clears the whole panel. The
|
||||
flyout then always appears at the same place (rail's right edge, centred)
|
||||
instead of jumping to whichever button was pressed.
|
||||
|
||||
The selector needs three classes plus `.ol-control` to beat ol-ext's own
|
||||
`.ol-control.ol-bar .ol-control { position: relative }`. With one class
|
||||
fewer the two rules tie on specificity, source order decides, and
|
||||
ol-ext's stylesheet is injected after this block — so the controls stayed
|
||||
`relative`, the flyout was positioned against the Select button instead
|
||||
of the rail, and it covered the Transform button in the second column.
|
||||
---------------------------------------------------------------- */
|
||||
.ol-editbar.ol-editbar-rail > .ol-control {
|
||||
.ol-control.ol-bar.ol-editbar.ol-editbar-rail > .ol-control {
|
||||
position: static;
|
||||
}
|
||||
.ol-editbar-rail .ol-option-bar {
|
||||
@ -1997,7 +2004,7 @@
|
||||
}
|
||||
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index-Ddi_Zl4K.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-C6kuA4o8.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/vite-helpers-BlTxHScW.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/bootstrap-D1-uvFxm.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/openlayers-D5-VSKzq.js">
|
||||
|
||||
36
dist/sw.js
vendored
36
dist/sw.js
vendored
@ -157,7 +157,25 @@
|
||||
// chunk, and JSZip inside the shpjs one, so the entry needed both on
|
||||
// every visit — about 428 kB that is now fetched only when a COG
|
||||
// layer or a shapefile import actually asks for it. New hashed bundle.
|
||||
const CACHE_VERSION = 'v26';
|
||||
// v27: The drawing rail's option bar covered the Transform button. ol-ext
|
||||
// positions the flyout with `left: 100%`, which is meant to clear the
|
||||
// whole rail — the rule that makes that work sets the rail's controls to
|
||||
// `position: static`, and it only tied with ol-ext's own rule on
|
||||
// specificity, so stylesheet order decided the winner and ol-ext's
|
||||
// injected sheet came last. The flyout was therefore positioned against
|
||||
// the Select button and landed on the second column. The selector now
|
||||
// carries one class more. This is the same failure as v16, in a second
|
||||
// place.
|
||||
// Also: the local database now opens when the device is offline. The
|
||||
// fetch handler skipped every request whose destination was 'worker', to
|
||||
// stay clear of the Vite dev server's module plumbing. In a build,
|
||||
// SQLocal's worker is a hashed file under /assets/, so it was never
|
||||
// cached either — with the network down the request failed, initSchema()
|
||||
// never resolved, and everything waiting on the database stopped without
|
||||
// an error. The map, the tiles and the shell all worked, so the app
|
||||
// looked like it was working offline while nothing could be saved or
|
||||
// read. The skip now covers only the dev server's own paths.
|
||||
const CACHE_VERSION = 'v27';
|
||||
const SHELL_CACHE = `shell-${CACHE_VERSION}`;
|
||||
const MODULES_CACHE = `modules-${CACHE_VERSION}`;
|
||||
const API_CACHE = `api-${CACHE_VERSION}`;
|
||||
@ -358,11 +376,17 @@ self.addEventListener('fetch', (event) => {
|
||||
// Skip chrome-extension and other non-http(s) requests
|
||||
if (!url.protocol.startsWith('http')) return;
|
||||
|
||||
// Skip worker files and Vite dev-server node_modules requests —
|
||||
// intercepting these breaks module workers (e.g. SQLocal/SQLite).
|
||||
if (url.pathname.includes('node_modules') ||
|
||||
url.search.includes('worker_file') ||
|
||||
request.destination === 'worker') return;
|
||||
// Skip the Vite dev server's own module plumbing — it serves worker scripts
|
||||
// straight out of node_modules with a ?worker_file query, and intercepting
|
||||
// those breaks module workers (e.g. SQLocal/SQLite).
|
||||
if (url.pathname.includes('node_modules') || url.search.includes('worker_file')) return;
|
||||
|
||||
// A worker script in a BUILT app is a hashed file under /assets/, as
|
||||
// immutable as any other asset — and it HAS to be cached, or SQLocal cannot
|
||||
// start its worker with the network down and the local database never opens.
|
||||
// Skipping every 'worker' destination was what made that happen: see v27.
|
||||
// Anything else with destination 'worker' is still left alone.
|
||||
if (request.destination === 'worker' && !isAppAsset(url)) return;
|
||||
|
||||
// ----- TILE REQUESTS — passive cache-then-network (per-host bucket) -----
|
||||
const tileCache = getTileCacheName(url);
|
||||
|
||||
@ -1815,8 +1815,15 @@
|
||||
block to the rail itself, so `left: 100%` clears the whole panel. The
|
||||
flyout then always appears at the same place (rail's right edge, centred)
|
||||
instead of jumping to whichever button was pressed.
|
||||
|
||||
The selector needs three classes plus `.ol-control` to beat ol-ext's own
|
||||
`.ol-control.ol-bar .ol-control { position: relative }`. With one class
|
||||
fewer the two rules tie on specificity, source order decides, and
|
||||
ol-ext's stylesheet is injected after this block — so the controls stayed
|
||||
`relative`, the flyout was positioned against the Select button instead
|
||||
of the rail, and it covered the Transform button in the second column.
|
||||
---------------------------------------------------------------- */
|
||||
.ol-editbar.ol-editbar-rail > .ol-control {
|
||||
.ol-control.ol-bar.ol-editbar.ol-editbar-rail > .ol-control {
|
||||
position: static;
|
||||
}
|
||||
.ol-editbar-rail .ol-option-bar {
|
||||
|
||||
11
main.js
11
main.js
@ -218,10 +218,19 @@ async function initApp() {
|
||||
if (showNoDistrictBlockerIfNeeded()) return;
|
||||
|
||||
// 1. Initialize PWA features (Service Worker, install prompt, offline detection)
|
||||
//
|
||||
// The service worker is registered in built applications only. On the Vite
|
||||
// dev server it cannot do its job anyway — the modules it would need are
|
||||
// served out of node_modules, which it deliberately keeps clear of — while
|
||||
// it does cache main.js and src/*.js cache-first, so it can serve a stale
|
||||
// module and fight hot reloading. Offline behaviour is tested against a
|
||||
// build (npm run build && npm run preview), never against the dev server.
|
||||
// When registration is off, initPWA also removes any worker an earlier
|
||||
// session installed on this origin.
|
||||
await initPWA({
|
||||
installButton: '#install-btn',
|
||||
offlineIndicator: '#offline-indicator',
|
||||
autoRegisterSW: true
|
||||
autoRegisterSW: import.meta.env.PROD,
|
||||
});
|
||||
|
||||
// 2. Initialize the map
|
||||
|
||||
36
public/sw.js
36
public/sw.js
@ -157,7 +157,25 @@
|
||||
// chunk, and JSZip inside the shpjs one, so the entry needed both on
|
||||
// every visit — about 428 kB that is now fetched only when a COG
|
||||
// layer or a shapefile import actually asks for it. New hashed bundle.
|
||||
const CACHE_VERSION = 'v26';
|
||||
// v27: The drawing rail's option bar covered the Transform button. ol-ext
|
||||
// positions the flyout with `left: 100%`, which is meant to clear the
|
||||
// whole rail — the rule that makes that work sets the rail's controls to
|
||||
// `position: static`, and it only tied with ol-ext's own rule on
|
||||
// specificity, so stylesheet order decided the winner and ol-ext's
|
||||
// injected sheet came last. The flyout was therefore positioned against
|
||||
// the Select button and landed on the second column. The selector now
|
||||
// carries one class more. This is the same failure as v16, in a second
|
||||
// place.
|
||||
// Also: the local database now opens when the device is offline. The
|
||||
// fetch handler skipped every request whose destination was 'worker', to
|
||||
// stay clear of the Vite dev server's module plumbing. In a build,
|
||||
// SQLocal's worker is a hashed file under /assets/, so it was never
|
||||
// cached either — with the network down the request failed, initSchema()
|
||||
// never resolved, and everything waiting on the database stopped without
|
||||
// an error. The map, the tiles and the shell all worked, so the app
|
||||
// looked like it was working offline while nothing could be saved or
|
||||
// read. The skip now covers only the dev server's own paths.
|
||||
const CACHE_VERSION = 'v27';
|
||||
const SHELL_CACHE = `shell-${CACHE_VERSION}`;
|
||||
const MODULES_CACHE = `modules-${CACHE_VERSION}`;
|
||||
const API_CACHE = `api-${CACHE_VERSION}`;
|
||||
@ -358,11 +376,17 @@ self.addEventListener('fetch', (event) => {
|
||||
// Skip chrome-extension and other non-http(s) requests
|
||||
if (!url.protocol.startsWith('http')) return;
|
||||
|
||||
// Skip worker files and Vite dev-server node_modules requests —
|
||||
// intercepting these breaks module workers (e.g. SQLocal/SQLite).
|
||||
if (url.pathname.includes('node_modules') ||
|
||||
url.search.includes('worker_file') ||
|
||||
request.destination === 'worker') return;
|
||||
// Skip the Vite dev server's own module plumbing — it serves worker scripts
|
||||
// straight out of node_modules with a ?worker_file query, and intercepting
|
||||
// those breaks module workers (e.g. SQLocal/SQLite).
|
||||
if (url.pathname.includes('node_modules') || url.search.includes('worker_file')) return;
|
||||
|
||||
// A worker script in a BUILT app is a hashed file under /assets/, as
|
||||
// immutable as any other asset — and it HAS to be cached, or SQLocal cannot
|
||||
// start its worker with the network down and the local database never opens.
|
||||
// Skipping every 'worker' destination was what made that happen: see v27.
|
||||
// Anything else with destination 'worker' is still left alone.
|
||||
if (request.destination === 'worker' && !isAppAsset(url)) return;
|
||||
|
||||
// ----- TILE REQUESTS — passive cache-then-network (per-host bucket) -----
|
||||
const tileCache = getTileCacheName(url);
|
||||
|
||||
51
src/pwa.js
51
src/pwa.js
@ -52,6 +52,53 @@ export async function registerServiceWorker() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any service worker registered against this origin, and the caches it
|
||||
* filled. Used on the development server, where the service worker is not
|
||||
* registered at all (see initPWA) — without this, a worker installed by an
|
||||
* earlier session would go on controlling the page for as long as it exists.
|
||||
*
|
||||
* It is not only a tidiness problem. On the dev server the worker caches
|
||||
* `/main.js` and `/src/*.js` cache-first, so it can hand back a stale module
|
||||
* and fight hot reloading; and in Safari a worker script that reaches the
|
||||
* browser through a controlling service worker loses its cross-origin-isolation
|
||||
* headers, so SQLocal's worker is refused and the local database never opens.
|
||||
*
|
||||
* @returns {Promise<boolean>} true if anything was removed
|
||||
*/
|
||||
export async function unregisterServiceWorkers() {
|
||||
if (!('serviceWorker' in navigator)) return false;
|
||||
let removed = false;
|
||||
try {
|
||||
for (const registration of await navigator.serviceWorker.getRegistrations()) {
|
||||
if (await registration.unregister()) removed = true;
|
||||
}
|
||||
|
||||
// Clear the app caches on every load, not only on the load that did the
|
||||
// unregistering. An unregistered worker goes on controlling the page it is
|
||||
// already attached to until the next navigation, so it refills those caches
|
||||
// behind us during that one load; deleting them unconditionally is what
|
||||
// makes this converge. Nothing in a development build reads them anyway.
|
||||
//
|
||||
// The tile caches are left alone: they hold what an officer downloaded for
|
||||
// field work, they are expensive to refill, and they are equally valid
|
||||
// with no service worker in sight.
|
||||
if ('caches' in window) {
|
||||
const names = await caches.keys();
|
||||
await Promise.all(
|
||||
names.filter((name) => !name.startsWith('tiles-')).map((name) => caches.delete(name)),
|
||||
);
|
||||
}
|
||||
|
||||
if (removed) {
|
||||
console.log('[PWA] Service worker unregistered (development build) — reload once more to drop it completely');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[PWA] Could not unregister the service worker:', error);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Install Prompt
|
||||
// ============================================================================
|
||||
@ -467,6 +514,10 @@ export async function initPWA(options = {}) {
|
||||
|
||||
if (autoRegisterSW) {
|
||||
await registerServiceWorker();
|
||||
} else {
|
||||
// Nothing should be controlling the page when registration is off; clear
|
||||
// out anything an earlier session left behind.
|
||||
await unregisterServiceWorkers();
|
||||
}
|
||||
|
||||
initInstallPrompt(installButton);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user