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/CHANGELOG.md +35 -1
- package/dist/server/marker-service.js +5 -1
- package/package.json +3 -2
- package/themes/transparent.css +3 -3
- package/web/dist/assets/{TerminalPanel-DgR_8K2X.js → TerminalPanel-CX6GKT5R.js} +1 -1
- package/web/dist/assets/{index-BplWuHvM.js → index-B-YjvCxZ.js} +31 -31
- package/web/dist/assets/index-d1VZAfby.css +10 -0
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +163 -126
- package/web/public/sw.js +163 -126
- package/web/dist/assets/index-CbshsMO6.css +0 -10
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
});
|