pi-web-ui 0.74.0 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web/dist/sw.js CHANGED
@@ -1,126 +1,163 @@
1
- /**
2
- * pi-web-ui — Progressive Web App service worker.
3
- *
4
- * Strategy overview
5
- * -----------------
6
- * pi-web-ui is a WebSocket-first app that needs a live backend, so we do NOT
7
- * try to make it fully offline. Instead the SW focuses on what makes it a
8
- * reliable *installable* PWA on mobile/desktop:
9
- *
10
- * - network-first for navigation requests (falls back to the cached app
11
- * shell when the network flaps), and
12
- * - cache-first for hashed static assets, which Vite fingerprints so a cache
13
- * hit is always the right version until a new deploy publishes new hashes.
14
- *
15
- * Real-time / dynamic / credential-bearing routes (/ws, /api, /themes,
16
- * /plugins) are always fetched from the network and never cached, so we never
17
- * risk serving stale theme/plugin code or caching anything sensitive.
18
- */
19
-
20
- const STATIC_CACHE = "pi-web-ui-static-v1";
21
- const SHELL_CACHE = "pi-web-ui-shell-v1";
22
-
23
- // App root within this origin — "/" for root deployments, "/pi/" behind an
24
- // nginx sub-path reverse proxy. All path checks below are relative to it, so
25
- // the worker behaves identically under either deployment layout.
26
- const SCOPE = new URL("./", self.registration.scope).pathname;
27
-
28
- /** Map a request pathname to an app-relative path ("/pi/ws" "/ws"), or null
29
- * when the request lies outside the registration scope (shouldn't happen). */
30
- function appPath(pathname) {
31
- if (SCOPE === "/") return pathname;
32
- if (pathname.startsWith(SCOPE)) return "/" + pathname.slice(SCOPE.length);
33
- return null;
34
- }
35
-
36
- self.addEventListener("install", (event) => {
37
- // Take control as soon as this version activates so the current page is
38
- // served by the new worker without requiring a second reload.
39
- self.skipWaiting();
40
- event.waitUntil(caches.open(SHELL_CACHE));
41
- });
42
-
43
- self.addEventListener("activate", (event) => {
44
- event.waitUntil(
45
- caches
46
- .keys()
47
- .then((keys) =>
48
- Promise.all(keys.filter((k) => k !== STATIC_CACHE && k !== SHELL_CACHE).map((k) => caches.delete(k))),
49
- )
50
- // Apply to already-open pages immediately.
51
- .then(() => self.clients.claim()),
52
- );
53
- });
54
-
55
- // Only cache simple, safe GET requests. Everything else goes straight through.
56
- function isCachable(request) {
57
- const method = request.method;
58
- if (method !== "GET") return false;
59
-
60
- const url = new URL(request.url);
61
- if (url.origin !== self.location.origin) return false;
62
-
63
- // Never cache real-time, dynamic or credential/data endpoints.
64
- const path = appPath(url.pathname);
65
- if (
66
- path === null ||
67
- path.startsWith("/ws") ||
68
- path.startsWith("/api") ||
69
- path.startsWith("/themes") ||
70
- path.startsWith("/plugins")
71
- ) {
72
- return false;
73
- }
74
- return true;
75
- }
76
-
77
- self.addEventListener("fetch", (event) => {
78
- const { request } = event;
79
- if (!isCachable(request)) {
80
- // Let the browser/backend handle WebSockets, API calls and cross-origin
81
- // requests normally.
82
- return;
83
- }
84
-
85
- const requestUrl = new URL(request.url);
86
-
87
- // Navigation → app shell. Network-first with cached fallback: users get the
88
- // latest build when online but can still reopen the app while flaky.
89
- if (request.mode === "navigate") {
90
- event.respondWith(
91
- fetch(request)
92
- .then((response) => {
93
- const copy = response.clone();
94
- caches.open(SHELL_CACHE).then((cache) => cache.put(request, copy));
95
- return response;
96
- })
97
- .catch(() => caches.match(request).then((cached) => cached || caches.match(SCOPE) || Response.error())),
98
- );
99
- return;
100
- }
101
-
102
- // Static assets (hashed by Vite) cache-first.
103
- const path = appPath(requestUrl.pathname);
104
- const isStatic =
105
- path !== null &&
106
- (path.startsWith("/assets/") ||
107
- path.startsWith("/icons/") ||
108
- path === "/favicon.svg" ||
109
- path === "/icon.ico" ||
110
- path === "/manifest.webmanifest");
111
-
112
- if (isStatic) {
113
- event.respondWith(
114
- caches.match(request).then((cached) => {
115
- if (cached) return cached;
116
- return fetch(request).then((response) => {
117
- if (response && response.ok) {
118
- const copy = response.clone();
119
- caches.open(STATIC_CACHE).then((cache) => cache.put(request, copy));
120
- }
121
- return response;
122
- });
123
- }),
124
- );
125
- }
126
- });
1
+ /**
2
+ * pi-web-ui — Progressive Web App service worker.
3
+ *
4
+ * Strategy overview
5
+ * -----------------
6
+ * pi-web-ui is a WebSocket-first app that needs a live backend, so we do NOT
7
+ * try to make it fully offline. Instead the SW focuses on what makes it a
8
+ * reliable *installable* PWA on mobile/desktop:
9
+ *
10
+ * - network-first for navigation requests (falls back to the cached app
11
+ * shell when the network flaps), and
12
+ * - cache-first for hashed static assets, which Vite fingerprints so a cache
13
+ * hit is always the right version until a new deploy publishes new hashes.
14
+ *
15
+ * It also owns notification clicks (`notificationclick`): the desktop / OS
16
+ * notifications posted from the page carry the page URL in `data.url`, and a
17
+ * click must bring the app back on Windows (and Linux) the click handler is
18
+ * the only thing that can do that, otherwise the toast just disappears.
19
+ *
20
+ * Real-time / dynamic / credential-bearing routes (/ws, /api, /themes,
21
+ * /plugins) are always fetched from the network and never cached, so we never
22
+ * risk serving stale theme/plugin code or caching anything sensitive.
23
+ */
24
+
25
+ const STATIC_CACHE = "pi-web-ui-static-v1";
26
+ const SHELL_CACHE = "pi-web-ui-shell-v1";
27
+
28
+ // App root within this origin "/" for root deployments, "/pi/" behind an
29
+ // nginx sub-path reverse proxy. All path checks below are relative to it, so
30
+ // the worker behaves identically under either deployment layout.
31
+ const SCOPE = new URL("./", self.registration.scope).pathname;
32
+
33
+ /** Map a request pathname to an app-relative path ("/pi/ws" → "/ws"), or null
34
+ * when the request lies outside the registration scope (shouldn't happen). */
35
+ function appPath(pathname) {
36
+ if (SCOPE === "/") return pathname;
37
+ if (pathname.startsWith(SCOPE)) return "/" + pathname.slice(SCOPE.length);
38
+ return null;
39
+ }
40
+
41
+ self.addEventListener("install", (event) => {
42
+ // Take control as soon as this version activates so the current page is
43
+ // served by the new worker without requiring a second reload.
44
+ self.skipWaiting();
45
+ event.waitUntil(caches.open(SHELL_CACHE));
46
+ });
47
+
48
+ self.addEventListener("activate", (event) => {
49
+ event.waitUntil(
50
+ caches
51
+ .keys()
52
+ .then((keys) =>
53
+ Promise.all(keys.filter((k) => k !== STATIC_CACHE && k !== SHELL_CACHE).map((k) => caches.delete(k))),
54
+ )
55
+ // Apply to already-open pages immediately.
56
+ .then(() => self.clients.claim()),
57
+ );
58
+ });
59
+
60
+ // Only cache simple, safe GET requests. Everything else goes straight through.
61
+ function isCachable(request) {
62
+ const method = request.method;
63
+ if (method !== "GET") return false;
64
+
65
+ const url = new URL(request.url);
66
+ if (url.origin !== self.location.origin) return false;
67
+
68
+ // Never cache real-time, dynamic or credential/data endpoints.
69
+ const path = appPath(url.pathname);
70
+ if (
71
+ path === null ||
72
+ path.startsWith("/ws") ||
73
+ path.startsWith("/api") ||
74
+ path.startsWith("/themes") ||
75
+ path.startsWith("/plugins")
76
+ ) {
77
+ return false;
78
+ }
79
+ return true;
80
+ }
81
+
82
+ self.addEventListener("fetch", (event) => {
83
+ const { request } = event;
84
+ if (!isCachable(request)) {
85
+ // Let the browser/backend handle WebSockets, API calls and cross-origin
86
+ // requests normally.
87
+ return;
88
+ }
89
+
90
+ const requestUrl = new URL(request.url);
91
+
92
+ // Navigation → app shell. Network-first with cached fallback: users get the
93
+ // latest build when online but can still reopen the app while flaky.
94
+ if (request.mode === "navigate") {
95
+ event.respondWith(
96
+ fetch(request)
97
+ .then((response) => {
98
+ const copy = response.clone();
99
+ caches.open(SHELL_CACHE).then((cache) => cache.put(request, copy));
100
+ return response;
101
+ })
102
+ .catch(() => caches.match(request).then((cached) => cached || caches.match(SCOPE) || Response.error())),
103
+ );
104
+ return;
105
+ }
106
+
107
+ // Static assets (hashed by Vite) → cache-first.
108
+ const path = appPath(requestUrl.pathname);
109
+ const isStatic =
110
+ path !== null &&
111
+ (path.startsWith("/assets/") ||
112
+ path.startsWith("/icons/") ||
113
+ path === "/favicon.svg" ||
114
+ path === "/icon.ico" ||
115
+ path === "/manifest.webmanifest");
116
+
117
+ if (isStatic) {
118
+ event.respondWith(
119
+ caches.match(request).then((cached) => {
120
+ if (cached) return cached;
121
+ return fetch(request).then((response) => {
122
+ if (response && response.ok) {
123
+ const copy = response.clone();
124
+ caches.open(STATIC_CACHE).then((cache) => cache.put(request, copy));
125
+ }
126
+ return response;
127
+ });
128
+ }),
129
+ );
130
+ }
131
+ });
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Notification clicks.
135
+ //
136
+ // The page posts desktop / OS notifications (see web/src/notify.ts) with
137
+ // `data.url` = the URL that raised them. A click must bring the app back:
138
+ // on Windows (and Linux) this handler is the only thing that can do that —
139
+ // the toast would otherwise just disappear, which is why "nothing happens when
140
+ // I click the reminder" is a platform-level dead end rather than a UI bug.
141
+ // Focus the window that is already open (a tab or the installed PWA — a
142
+ // minimised window still counts as open) and only open a new one when none
143
+ // exists. Windows never restores a window for us, so we match on URL first so
144
+ // the session that raised the notification is the one that comes forward.
145
+ self.addEventListener("notificationclick", (event) => {
146
+ event.notification.close();
147
+ const target = (event.notification.data && event.notification.data.url) || new URL(SCOPE, self.location.origin).href;
148
+
149
+ event.waitUntil(
150
+ self.clients
151
+ .matchAll({ type: "window", includeUncontrolled: true })
152
+ .then((clients) => {
153
+ const inScope = clients.filter((client) => appPath(new URL(client.url).pathname) !== null);
154
+ const match = inScope.find((client) => client.url === target) || inScope[0];
155
+ // focus() rejects when the browser refuses to raise the window
156
+ // (rare); fall back to just returning the client so the click
157
+ // never logs an unhandled rejection.
158
+ if (match) return match.focus ? match.focus().catch(() => match) : match;
159
+ return self.clients.openWindow(target);
160
+ })
161
+ .catch(() => undefined),
162
+ );
163
+ });
package/web/public/sw.js CHANGED
@@ -1,126 +1,163 @@
1
- /**
2
- * pi-web-ui — Progressive Web App service worker.
3
- *
4
- * Strategy overview
5
- * -----------------
6
- * pi-web-ui is a WebSocket-first app that needs a live backend, so we do NOT
7
- * try to make it fully offline. Instead the SW focuses on what makes it a
8
- * reliable *installable* PWA on mobile/desktop:
9
- *
10
- * - network-first for navigation requests (falls back to the cached app
11
- * shell when the network flaps), and
12
- * - cache-first for hashed static assets, which Vite fingerprints so a cache
13
- * hit is always the right version until a new deploy publishes new hashes.
14
- *
15
- * Real-time / dynamic / credential-bearing routes (/ws, /api, /themes,
16
- * /plugins) are always fetched from the network and never cached, so we never
17
- * risk serving stale theme/plugin code or caching anything sensitive.
18
- */
19
-
20
- const STATIC_CACHE = "pi-web-ui-static-v1";
21
- const SHELL_CACHE = "pi-web-ui-shell-v1";
22
-
23
- // App root within this origin — "/" for root deployments, "/pi/" behind an
24
- // nginx sub-path reverse proxy. All path checks below are relative to it, so
25
- // the worker behaves identically under either deployment layout.
26
- const SCOPE = new URL("./", self.registration.scope).pathname;
27
-
28
- /** Map a request pathname to an app-relative path ("/pi/ws" "/ws"), or null
29
- * when the request lies outside the registration scope (shouldn't happen). */
30
- function appPath(pathname) {
31
- if (SCOPE === "/") return pathname;
32
- if (pathname.startsWith(SCOPE)) return "/" + pathname.slice(SCOPE.length);
33
- return null;
34
- }
35
-
36
- self.addEventListener("install", (event) => {
37
- // Take control as soon as this version activates so the current page is
38
- // served by the new worker without requiring a second reload.
39
- self.skipWaiting();
40
- event.waitUntil(caches.open(SHELL_CACHE));
41
- });
42
-
43
- self.addEventListener("activate", (event) => {
44
- event.waitUntil(
45
- caches
46
- .keys()
47
- .then((keys) =>
48
- Promise.all(keys.filter((k) => k !== STATIC_CACHE && k !== SHELL_CACHE).map((k) => caches.delete(k))),
49
- )
50
- // Apply to already-open pages immediately.
51
- .then(() => self.clients.claim()),
52
- );
53
- });
54
-
55
- // Only cache simple, safe GET requests. Everything else goes straight through.
56
- function isCachable(request) {
57
- const method = request.method;
58
- if (method !== "GET") return false;
59
-
60
- const url = new URL(request.url);
61
- if (url.origin !== self.location.origin) return false;
62
-
63
- // Never cache real-time, dynamic or credential/data endpoints.
64
- const path = appPath(url.pathname);
65
- if (
66
- path === null ||
67
- path.startsWith("/ws") ||
68
- path.startsWith("/api") ||
69
- path.startsWith("/themes") ||
70
- path.startsWith("/plugins")
71
- ) {
72
- return false;
73
- }
74
- return true;
75
- }
76
-
77
- self.addEventListener("fetch", (event) => {
78
- const { request } = event;
79
- if (!isCachable(request)) {
80
- // Let the browser/backend handle WebSockets, API calls and cross-origin
81
- // requests normally.
82
- return;
83
- }
84
-
85
- const requestUrl = new URL(request.url);
86
-
87
- // Navigation → app shell. Network-first with cached fallback: users get the
88
- // latest build when online but can still reopen the app while flaky.
89
- if (request.mode === "navigate") {
90
- event.respondWith(
91
- fetch(request)
92
- .then((response) => {
93
- const copy = response.clone();
94
- caches.open(SHELL_CACHE).then((cache) => cache.put(request, copy));
95
- return response;
96
- })
97
- .catch(() => caches.match(request).then((cached) => cached || caches.match(SCOPE) || Response.error())),
98
- );
99
- return;
100
- }
101
-
102
- // Static assets (hashed by Vite) cache-first.
103
- const path = appPath(requestUrl.pathname);
104
- const isStatic =
105
- path !== null &&
106
- (path.startsWith("/assets/") ||
107
- path.startsWith("/icons/") ||
108
- path === "/favicon.svg" ||
109
- path === "/icon.ico" ||
110
- path === "/manifest.webmanifest");
111
-
112
- if (isStatic) {
113
- event.respondWith(
114
- caches.match(request).then((cached) => {
115
- if (cached) return cached;
116
- return fetch(request).then((response) => {
117
- if (response && response.ok) {
118
- const copy = response.clone();
119
- caches.open(STATIC_CACHE).then((cache) => cache.put(request, copy));
120
- }
121
- return response;
122
- });
123
- }),
124
- );
125
- }
126
- });
1
+ /**
2
+ * pi-web-ui — Progressive Web App service worker.
3
+ *
4
+ * Strategy overview
5
+ * -----------------
6
+ * pi-web-ui is a WebSocket-first app that needs a live backend, so we do NOT
7
+ * try to make it fully offline. Instead the SW focuses on what makes it a
8
+ * reliable *installable* PWA on mobile/desktop:
9
+ *
10
+ * - network-first for navigation requests (falls back to the cached app
11
+ * shell when the network flaps), and
12
+ * - cache-first for hashed static assets, which Vite fingerprints so a cache
13
+ * hit is always the right version until a new deploy publishes new hashes.
14
+ *
15
+ * It also owns notification clicks (`notificationclick`): the desktop / OS
16
+ * notifications posted from the page carry the page URL in `data.url`, and a
17
+ * click must bring the app back on Windows (and Linux) the click handler is
18
+ * the only thing that can do that, otherwise the toast just disappears.
19
+ *
20
+ * Real-time / dynamic / credential-bearing routes (/ws, /api, /themes,
21
+ * /plugins) are always fetched from the network and never cached, so we never
22
+ * risk serving stale theme/plugin code or caching anything sensitive.
23
+ */
24
+
25
+ const STATIC_CACHE = "pi-web-ui-static-v1";
26
+ const SHELL_CACHE = "pi-web-ui-shell-v1";
27
+
28
+ // App root within this origin "/" for root deployments, "/pi/" behind an
29
+ // nginx sub-path reverse proxy. All path checks below are relative to it, so
30
+ // the worker behaves identically under either deployment layout.
31
+ const SCOPE = new URL("./", self.registration.scope).pathname;
32
+
33
+ /** Map a request pathname to an app-relative path ("/pi/ws" → "/ws"), or null
34
+ * when the request lies outside the registration scope (shouldn't happen). */
35
+ function appPath(pathname) {
36
+ if (SCOPE === "/") return pathname;
37
+ if (pathname.startsWith(SCOPE)) return "/" + pathname.slice(SCOPE.length);
38
+ return null;
39
+ }
40
+
41
+ self.addEventListener("install", (event) => {
42
+ // Take control as soon as this version activates so the current page is
43
+ // served by the new worker without requiring a second reload.
44
+ self.skipWaiting();
45
+ event.waitUntil(caches.open(SHELL_CACHE));
46
+ });
47
+
48
+ self.addEventListener("activate", (event) => {
49
+ event.waitUntil(
50
+ caches
51
+ .keys()
52
+ .then((keys) =>
53
+ Promise.all(keys.filter((k) => k !== STATIC_CACHE && k !== SHELL_CACHE).map((k) => caches.delete(k))),
54
+ )
55
+ // Apply to already-open pages immediately.
56
+ .then(() => self.clients.claim()),
57
+ );
58
+ });
59
+
60
+ // Only cache simple, safe GET requests. Everything else goes straight through.
61
+ function isCachable(request) {
62
+ const method = request.method;
63
+ if (method !== "GET") return false;
64
+
65
+ const url = new URL(request.url);
66
+ if (url.origin !== self.location.origin) return false;
67
+
68
+ // Never cache real-time, dynamic or credential/data endpoints.
69
+ const path = appPath(url.pathname);
70
+ if (
71
+ path === null ||
72
+ path.startsWith("/ws") ||
73
+ path.startsWith("/api") ||
74
+ path.startsWith("/themes") ||
75
+ path.startsWith("/plugins")
76
+ ) {
77
+ return false;
78
+ }
79
+ return true;
80
+ }
81
+
82
+ self.addEventListener("fetch", (event) => {
83
+ const { request } = event;
84
+ if (!isCachable(request)) {
85
+ // Let the browser/backend handle WebSockets, API calls and cross-origin
86
+ // requests normally.
87
+ return;
88
+ }
89
+
90
+ const requestUrl = new URL(request.url);
91
+
92
+ // Navigation → app shell. Network-first with cached fallback: users get the
93
+ // latest build when online but can still reopen the app while flaky.
94
+ if (request.mode === "navigate") {
95
+ event.respondWith(
96
+ fetch(request)
97
+ .then((response) => {
98
+ const copy = response.clone();
99
+ caches.open(SHELL_CACHE).then((cache) => cache.put(request, copy));
100
+ return response;
101
+ })
102
+ .catch(() => caches.match(request).then((cached) => cached || caches.match(SCOPE) || Response.error())),
103
+ );
104
+ return;
105
+ }
106
+
107
+ // Static assets (hashed by Vite) → cache-first.
108
+ const path = appPath(requestUrl.pathname);
109
+ const isStatic =
110
+ path !== null &&
111
+ (path.startsWith("/assets/") ||
112
+ path.startsWith("/icons/") ||
113
+ path === "/favicon.svg" ||
114
+ path === "/icon.ico" ||
115
+ path === "/manifest.webmanifest");
116
+
117
+ if (isStatic) {
118
+ event.respondWith(
119
+ caches.match(request).then((cached) => {
120
+ if (cached) return cached;
121
+ return fetch(request).then((response) => {
122
+ if (response && response.ok) {
123
+ const copy = response.clone();
124
+ caches.open(STATIC_CACHE).then((cache) => cache.put(request, copy));
125
+ }
126
+ return response;
127
+ });
128
+ }),
129
+ );
130
+ }
131
+ });
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Notification clicks.
135
+ //
136
+ // The page posts desktop / OS notifications (see web/src/notify.ts) with
137
+ // `data.url` = the URL that raised them. A click must bring the app back:
138
+ // on Windows (and Linux) this handler is the only thing that can do that —
139
+ // the toast would otherwise just disappear, which is why "nothing happens when
140
+ // I click the reminder" is a platform-level dead end rather than a UI bug.
141
+ // Focus the window that is already open (a tab or the installed PWA — a
142
+ // minimised window still counts as open) and only open a new one when none
143
+ // exists. Windows never restores a window for us, so we match on URL first so
144
+ // the session that raised the notification is the one that comes forward.
145
+ self.addEventListener("notificationclick", (event) => {
146
+ event.notification.close();
147
+ const target = (event.notification.data && event.notification.data.url) || new URL(SCOPE, self.location.origin).href;
148
+
149
+ event.waitUntil(
150
+ self.clients
151
+ .matchAll({ type: "window", includeUncontrolled: true })
152
+ .then((clients) => {
153
+ const inScope = clients.filter((client) => appPath(new URL(client.url).pathname) !== null);
154
+ const match = inScope.find((client) => client.url === target) || inScope[0];
155
+ // focus() rejects when the browser refuses to raise the window
156
+ // (rare); fall back to just returning the client so the click
157
+ // never logs an unhandled rejection.
158
+ if (match) return match.focus ? match.focus().catch(() => match) : match;
159
+ return self.clients.openWindow(target);
160
+ })
161
+ .catch(() => undefined),
162
+ );
163
+ });