@unbrained/pm-web 2026.7.10 → 2026.7.13
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 +13 -0
- package/README.md +26 -0
- package/dist/app.d.ts +1 -0
- package/dist/app.js +38 -2
- package/dist/app.js.map +1 -1
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +10 -0
- package/dist/auth.js.map +1 -1
- package/dist/db.js +17 -1
- package/dist/db.js.map +1 -1
- package/dist/ical.js.map +1 -1
- package/dist/index.d.ts +1 -22
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/oidc.d.ts +89 -0
- package/dist/oidc.js +305 -0
- package/dist/oidc.js.map +1 -0
- package/dist/routes/auth.js +3 -15
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/github.js +3 -3
- package/dist/routes/github.js.map +1 -1
- package/dist/routes/oidc.d.ts +4 -0
- package/dist/routes/oidc.js +147 -0
- package/dist/routes/oidc.js.map +1 -0
- package/dist/routes/pm.js +121 -107
- package/dist/routes/pm.js.map +1 -1
- package/dist/routes/projects.js +9 -2
- package/dist/routes/projects.js.map +1 -1
- package/dist/server.js +7 -2
- package/dist/server.js.map +1 -1
- package/dist/services/pm-runner.d.ts +11 -3
- package/dist/services/pm-runner.js +167 -54
- package/dist/services/pm-runner.js.map +1 -1
- package/dist/services/realtime-bus.d.ts +1 -0
- package/dist/services/realtime-bus.js +148 -0
- package/dist/services/realtime-bus.js.map +1 -0
- package/dist/services/sse.d.ts +3 -1
- package/dist/services/sse.js +83 -48
- package/dist/services/sse.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +11 -11
- package/public/cookie-consent.js +54 -39
- package/public/cookie-settings.html +7 -7
- package/public/index.html +2 -0
- package/public/legal-notice.html +10 -12
- package/public/privacy-policy.html +14 -24
- package/public/src/app.js +4 -4
- package/public/src/app.js.map +1 -1
- package/public/src/app.ts +4 -4
- package/public/src/components/toast.js.map +1 -1
- package/public/src/cookie-consent.ts +68 -0
- package/public/src/sw.ts +443 -0
- package/public/src/views/auth.js +22 -0
- package/public/src/views/auth.js.map +1 -1
- package/public/src/views/auth.ts +20 -0
- package/public/src/views/graph.js.map +1 -1
- package/public/styles.css +2 -0
- package/public/sw.js +320 -256
- package/public/terms.html +7 -9
- package/public/tsconfig.json +1 -1
- package/public/tsconfig.scripts.json +18 -0
- package/public/tsconfig.sw.json +18 -0
- package/sql/schema.sql +16 -0
package/public/src/sw.ts
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2
|
+
// SERVICE WORKER — pm-web PWA
|
|
3
|
+
// Cache versioning: auto-bust based on build timestamp
|
|
4
|
+
// Offline fallback page, mutation queue via IndexedDB
|
|
5
|
+
//
|
|
6
|
+
// This is the TypeScript source for /sw.js. It is compiled with the
|
|
7
|
+
// `WebWorker` lib (see public/tsconfig.sw.json) so the ServiceWorker
|
|
8
|
+
// global scope (`ServiceWorkerGlobalScope`, `caches`, `clients`,
|
|
9
|
+
// `skipWaiting`, IndexedDB, fetch) is fully typed. The emitted
|
|
10
|
+
// `public/sw.js` is plain JavaScript served at the same URL.
|
|
11
|
+
// ═══════════════════════════════════════════════════════════════
|
|
12
|
+
|
|
13
|
+
// `self` is the ServiceWorkerGlobalScope inside a service worker. The
|
|
14
|
+
// `WebWorker` lib types the ambient `self` as the generic WorkerGlobalScope,
|
|
15
|
+
// so narrow it once at the top via a `unknown` cast (no `any`).
|
|
16
|
+
const sw = self as unknown as ServiceWorkerGlobalScope;
|
|
17
|
+
|
|
18
|
+
// `__BUILD_TIME__` is an optional build-time substitution placeholder.
|
|
19
|
+
// No substitution is performed by the default build, so the literal is
|
|
20
|
+
// retained verbatim and the cache name falls back to a runtime stamp.
|
|
21
|
+
// Kept as a widened `string` so the placeholder comparison stays a
|
|
22
|
+
// runtime check and the emitted output is deterministic.
|
|
23
|
+
const BUILD_TIMESTAMP: string = '__BUILD_TIME__';
|
|
24
|
+
const CACHE_NAME = 'pm-web-' + (BUILD_TIMESTAMP !== '__BUILD_TIME__' ? BUILD_TIMESTAMP : Date.now().toString(36));
|
|
25
|
+
const MUTATION_DB = 'pm-web-offline';
|
|
26
|
+
const MUTATION_STORE = 'mutations';
|
|
27
|
+
|
|
28
|
+
const STATIC_ASSETS: readonly string[] = [
|
|
29
|
+
'/',
|
|
30
|
+
'/styles.css',
|
|
31
|
+
'/manifest.json',
|
|
32
|
+
'/icons/icon-192.png',
|
|
33
|
+
'/icons/icon-512.png',
|
|
34
|
+
'/src/api.js',
|
|
35
|
+
'/src/app.js',
|
|
36
|
+
'/src/components/modals.js',
|
|
37
|
+
'/src/components/toast.js',
|
|
38
|
+
'/src/constants.js',
|
|
39
|
+
'/src/filters.js',
|
|
40
|
+
'/src/state.js',
|
|
41
|
+
'/src/theme.js',
|
|
42
|
+
'/src/types.js',
|
|
43
|
+
'/src/utils.js',
|
|
44
|
+
'/src/views/activity.js',
|
|
45
|
+
'/src/views/admin.js',
|
|
46
|
+
'/src/views/auth.js',
|
|
47
|
+
'/src/views/calendar.js',
|
|
48
|
+
'/src/views/comments-audit.js',
|
|
49
|
+
'/src/views/config.js',
|
|
50
|
+
'/src/views/context.js',
|
|
51
|
+
'/src/views/create.js',
|
|
52
|
+
'/src/views/dedupe.js',
|
|
53
|
+
'/src/views/export.js',
|
|
54
|
+
'/src/views/github.js',
|
|
55
|
+
'/src/views/graph-canvas.js',
|
|
56
|
+
'/src/views/graph.js',
|
|
57
|
+
'/src/views/groups.js',
|
|
58
|
+
'/src/views/guide.js',
|
|
59
|
+
'/src/views/health.js',
|
|
60
|
+
'/src/views/items.js',
|
|
61
|
+
'/src/views/normalize.js',
|
|
62
|
+
'/src/views/plan.js',
|
|
63
|
+
'/src/views/plan-execution.js',
|
|
64
|
+
'/src/views/projects.js',
|
|
65
|
+
'/src/views/router.js',
|
|
66
|
+
'/src/views/search.js',
|
|
67
|
+
'/src/views/settings.js',
|
|
68
|
+
'/src/views/shared.js',
|
|
69
|
+
'/src/views/sharing.js',
|
|
70
|
+
'/src/views/stats.js',
|
|
71
|
+
'/src/views/templates.js',
|
|
72
|
+
'/src/views/validate.js',
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// ── Offline fallback page ──
|
|
76
|
+
const OFFLINE_HTML = `<!DOCTYPE html>
|
|
77
|
+
<html lang="en">
|
|
78
|
+
<head>
|
|
79
|
+
<meta charset="UTF-8">
|
|
80
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
81
|
+
<title>pm-web — Offline</title>
|
|
82
|
+
<style>
|
|
83
|
+
body{font-family:'Inter',system-ui,sans-serif;background:#0a0f1e;color:#f1f5f9;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:20px;text-align:center}
|
|
84
|
+
.offline-icon{font-size:48px;margin-bottom:20px;opacity:0.5}
|
|
85
|
+
.offline-title{font-size:22px;font-weight:600;margin-bottom:8px}
|
|
86
|
+
.offline-text{color:#94a3b8;max-width:400px;line-height:1.7;margin-bottom:24px}
|
|
87
|
+
.btn{display:inline-flex;align-items:center;gap:6px;padding:9px 16px;border:none;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;transition:0.15s}
|
|
88
|
+
.btn-primary{background:#2dd4bf;color:#0f172a}
|
|
89
|
+
.btn-primary:hover{background:#34ead4}
|
|
90
|
+
</style>
|
|
91
|
+
</head>
|
|
92
|
+
<body>
|
|
93
|
+
<div>
|
|
94
|
+
<div class="offline-icon">📡</div>
|
|
95
|
+
<div class="offline-title">You're offline</div>
|
|
96
|
+
<div class="offline-text">pm-web needs an internet connection to load. Please check your connection and try again.</div>
|
|
97
|
+
<button class="btn btn-primary" onclick="location.reload()">Try Again</button>
|
|
98
|
+
</div>
|
|
99
|
+
</body>
|
|
100
|
+
</html>`;
|
|
101
|
+
|
|
102
|
+
// ── IndexedDB Mutation Queue ──
|
|
103
|
+
|
|
104
|
+
interface QueuedMutation {
|
|
105
|
+
id: number;
|
|
106
|
+
method: string;
|
|
107
|
+
path: string;
|
|
108
|
+
body: string | null;
|
|
109
|
+
timestamp: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface StoredMutation {
|
|
113
|
+
method: string;
|
|
114
|
+
path: string;
|
|
115
|
+
body: string | null;
|
|
116
|
+
timestamp: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Minimal Background Sync event typing. The `WebWorker` lib does not ship
|
|
120
|
+
// `SyncEvent`, so declare the surface we use (it extends ExtendableEvent).
|
|
121
|
+
interface SyncEvent extends ExtendableEvent {
|
|
122
|
+
readonly tag: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function openMutationDB(): Promise<IDBDatabase> {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const request = indexedDB.open(MUTATION_DB, 1);
|
|
128
|
+
request.onupgradeneeded = () => {
|
|
129
|
+
const db = request.result;
|
|
130
|
+
if (!db.objectStoreNames.contains(MUTATION_STORE)) {
|
|
131
|
+
const store = db.createObjectStore(MUTATION_STORE, { keyPath: 'id', autoIncrement: true });
|
|
132
|
+
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
request.onsuccess = () => resolve(request.result);
|
|
136
|
+
request.onerror = () => reject(request.error);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolve when an IDB transaction has durably committed; reject on abort or
|
|
142
|
+
* error so callers can never mistake a half-applied (or never-applied) write
|
|
143
|
+
* for a persisted mutation. The transaction auto-commits once all queued
|
|
144
|
+
* requests settle, so awaiting this is sufficient to know the write is durable.
|
|
145
|
+
*/
|
|
146
|
+
function transactionDone(tx: IDBTransaction): Promise<void> {
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
tx.oncomplete = () => resolve();
|
|
149
|
+
tx.onabort = () => reject(tx.error ?? new Error('IDB transaction aborted'));
|
|
150
|
+
tx.onerror = () => reject(tx.error ?? new Error('IDB transaction error'));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Queue a mutation for later replay. Returns `true` only when the mutation has
|
|
156
|
+
* been durably persisted to IndexedDB; `false` when persistence failed so the
|
|
157
|
+
* caller can respond with an explicit error instead of claiming it was queued.
|
|
158
|
+
*/
|
|
159
|
+
async function queueMutation(method: string, path: string, body: unknown): Promise<boolean> {
|
|
160
|
+
try {
|
|
161
|
+
const db = await openMutationDB();
|
|
162
|
+
const tx = db.transaction(MUTATION_STORE, 'readwrite');
|
|
163
|
+
const store = tx.objectStore(MUTATION_STORE);
|
|
164
|
+
const record: StoredMutation = {
|
|
165
|
+
method,
|
|
166
|
+
path,
|
|
167
|
+
body: body !== undefined ? JSON.stringify(body) : null,
|
|
168
|
+
timestamp: Date.now(),
|
|
169
|
+
};
|
|
170
|
+
store.add(record);
|
|
171
|
+
// Await the transaction commit (not just the request dispatch) so the
|
|
172
|
+
// promise only resolves after the mutation is durably persisted. A request
|
|
173
|
+
// error triggers a transaction abort, surfaced via `transactionDone`.
|
|
174
|
+
await transactionDone(tx);
|
|
175
|
+
return true;
|
|
176
|
+
} catch (e) {
|
|
177
|
+
// Persistence failed — do NOT claim the mutation was queued.
|
|
178
|
+
console.warn('Failed to queue mutation for offline:', e);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function getQueuedMutations(): Promise<QueuedMutation[]> {
|
|
184
|
+
try {
|
|
185
|
+
const db = await openMutationDB();
|
|
186
|
+
const tx = db.transaction(MUTATION_STORE, 'readonly');
|
|
187
|
+
const store = tx.objectStore(MUTATION_STORE);
|
|
188
|
+
return await new Promise<QueuedMutation[]>((resolve, reject) => {
|
|
189
|
+
const request = store.getAll();
|
|
190
|
+
request.onsuccess = () => resolve(request.result as QueuedMutation[]);
|
|
191
|
+
request.onerror = () => reject(request.error);
|
|
192
|
+
});
|
|
193
|
+
} catch {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Remove a replayed mutation from the queue. Returns `true` only when the
|
|
200
|
+
* deletion has committed, so the caller can stop replay on a persistence
|
|
201
|
+
* failure instead of silently leaving duplicate entries to be retried.
|
|
202
|
+
*/
|
|
203
|
+
async function clearMutation(id: number): Promise<boolean> {
|
|
204
|
+
try {
|
|
205
|
+
const db = await openMutationDB();
|
|
206
|
+
const tx = db.transaction(MUTATION_STORE, 'readwrite');
|
|
207
|
+
tx.objectStore(MUTATION_STORE).delete(id);
|
|
208
|
+
await transactionDone(tx);
|
|
209
|
+
return true;
|
|
210
|
+
} catch (e) {
|
|
211
|
+
console.warn('Failed to clear queued mutation:', e);
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function flushMutationQueue(): Promise<void> {
|
|
217
|
+
const mutations = await getQueuedMutations();
|
|
218
|
+
if (mutations.length === 0) return;
|
|
219
|
+
|
|
220
|
+
for (const mut of mutations) {
|
|
221
|
+
try {
|
|
222
|
+
const opts: RequestInit = {
|
|
223
|
+
method: mut.method,
|
|
224
|
+
headers: { 'Content-Type': 'application/json' },
|
|
225
|
+
credentials: 'include',
|
|
226
|
+
};
|
|
227
|
+
if (mut.body !== null) opts.body = mut.body;
|
|
228
|
+
const res = await fetch('/api' + mut.path, opts);
|
|
229
|
+
if (res.ok) {
|
|
230
|
+
const cleared = await clearMutation(mut.id);
|
|
231
|
+
if (!cleared) {
|
|
232
|
+
// Could not remove the replayed mutation from the queue — stop to
|
|
233
|
+
// avoid duplicate replays on the next flush; it will retry later.
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
console.warn('Offline mutation failed:', mut.method, mut.path, res.status);
|
|
238
|
+
// Stop processing on first failure — try again later
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
// Network failed again — stop processing
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Notify clients about replayed mutations
|
|
248
|
+
const remaining = await getQueuedMutations();
|
|
249
|
+
const clients = await sw.clients.matchAll();
|
|
250
|
+
if (remaining.length === 0 && mutations.length > 0) {
|
|
251
|
+
clients.forEach((client) => {
|
|
252
|
+
client.postMessage({ type: 'MUTATIONS_REPLAYED', count: mutations.length });
|
|
253
|
+
});
|
|
254
|
+
} else if (remaining.length > 0) {
|
|
255
|
+
clients.forEach((client) => {
|
|
256
|
+
client.postMessage({
|
|
257
|
+
type: 'MUTATIONS_PARTIAL',
|
|
258
|
+
replayed: mutations.length - remaining.length,
|
|
259
|
+
remaining: remaining.length,
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── Install ──
|
|
266
|
+
sw.addEventListener('install', (event: ExtendableEvent) => {
|
|
267
|
+
event.waitUntil(
|
|
268
|
+
caches.open(CACHE_NAME).then((cache) =>
|
|
269
|
+
Promise.all(STATIC_ASSETS.map((asset) => cache.add(asset).catch(() => null)))
|
|
270
|
+
)
|
|
271
|
+
);
|
|
272
|
+
sw.skipWaiting();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ── Activate ──
|
|
276
|
+
sw.addEventListener('activate', (event: ExtendableEvent) => {
|
|
277
|
+
event.waitUntil(
|
|
278
|
+
caches.keys().then((keys) =>
|
|
279
|
+
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
|
|
280
|
+
)
|
|
281
|
+
);
|
|
282
|
+
sw.clients.claim();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ── Fetch strategy ──
|
|
286
|
+
sw.addEventListener('fetch', (event: FetchEvent) => {
|
|
287
|
+
const url = new URL(event.request.url);
|
|
288
|
+
|
|
289
|
+
// API calls: try network, queue mutations if offline
|
|
290
|
+
if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/healthz')) {
|
|
291
|
+
// Queue write operations (POST, PUT, PATCH, DELETE) when offline
|
|
292
|
+
if (event.request.method !== 'GET' && event.request.method !== 'HEAD') {
|
|
293
|
+
event.respondWith(
|
|
294
|
+
fetch(event.request).catch(async () => {
|
|
295
|
+
// Network failed — queue the mutation for later.
|
|
296
|
+
let body: unknown = undefined;
|
|
297
|
+
try {
|
|
298
|
+
body = await event.request.clone().json();
|
|
299
|
+
} catch { /* no body */ }
|
|
300
|
+
const queued = await queueMutation(
|
|
301
|
+
event.request.method,
|
|
302
|
+
url.pathname.replace('/api', ''),
|
|
303
|
+
body,
|
|
304
|
+
);
|
|
305
|
+
if (queued) {
|
|
306
|
+
return new Response(
|
|
307
|
+
JSON.stringify({ queued: true, message: 'Request queued for when you are back online' }),
|
|
308
|
+
{ status: 202, headers: { 'Content-Type': 'application/json' } },
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
// Persistence failed — do not claim the mutation was queued.
|
|
312
|
+
return new Response(
|
|
313
|
+
JSON.stringify({ error: 'Offline and unable to queue mutation', queued: false }),
|
|
314
|
+
{ status: 503, headers: { 'Content-Type': 'application/json' } },
|
|
315
|
+
);
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// GET/HEAD API calls: network-only, return offline JSON error
|
|
322
|
+
event.respondWith(
|
|
323
|
+
fetch(event.request)
|
|
324
|
+
.catch(() => new Response(JSON.stringify({ error: 'Offline — check your connection', queued: 0 }), {
|
|
325
|
+
status: 503,
|
|
326
|
+
headers: { 'Content-Type': 'application/json' },
|
|
327
|
+
}))
|
|
328
|
+
);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Navigation (SPA shell): network-first, cache fallback, offline page fallback
|
|
333
|
+
if (event.request.mode === 'navigate') {
|
|
334
|
+
event.respondWith(
|
|
335
|
+
fetch(event.request)
|
|
336
|
+
.then((res) => {
|
|
337
|
+
if (res.ok) {
|
|
338
|
+
const clone = res.clone();
|
|
339
|
+
caches.open(CACHE_NAME).then((c) => c.put('/', clone));
|
|
340
|
+
}
|
|
341
|
+
return res;
|
|
342
|
+
})
|
|
343
|
+
.catch(async () => {
|
|
344
|
+
// Try cached shell first
|
|
345
|
+
const cached = await caches.match('/');
|
|
346
|
+
if (cached) return cached;
|
|
347
|
+
// Return offline fallback page
|
|
348
|
+
return new Response(OFFLINE_HTML, {
|
|
349
|
+
status: 503,
|
|
350
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
351
|
+
});
|
|
352
|
+
})
|
|
353
|
+
);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Static assets: stale-while-revalidate
|
|
358
|
+
if (
|
|
359
|
+
url.pathname.endsWith('.css') ||
|
|
360
|
+
url.pathname.endsWith('.js') ||
|
|
361
|
+
url.pathname.endsWith('.json') ||
|
|
362
|
+
url.pathname.endsWith('.png') ||
|
|
363
|
+
url.pathname.endsWith('.svg') ||
|
|
364
|
+
url.pathname.endsWith('.ico') ||
|
|
365
|
+
url.pathname.endsWith('.woff2') ||
|
|
366
|
+
url.hostname.includes('fonts.googleapis.com') ||
|
|
367
|
+
url.hostname.includes('fonts.gstatic.com')
|
|
368
|
+
) {
|
|
369
|
+
event.respondWith(
|
|
370
|
+
(async (): Promise<Response> => {
|
|
371
|
+
const cache = await caches.open(CACHE_NAME);
|
|
372
|
+
const cached = await cache.match(event.request);
|
|
373
|
+
// Kick off revalidation in the background. The network promise resolves
|
|
374
|
+
// to `undefined` on failure (no unsafe cast — the type is explicit).
|
|
375
|
+
const network = fetch(event.request)
|
|
376
|
+
.then((res): Response => {
|
|
377
|
+
if (res.ok) void cache.put(event.request, res.clone());
|
|
378
|
+
return res;
|
|
379
|
+
})
|
|
380
|
+
.catch((): Response | undefined => undefined);
|
|
381
|
+
// Stale-while-revalidate: serve cached immediately when present.
|
|
382
|
+
if (cached) {
|
|
383
|
+
void network;
|
|
384
|
+
return cached;
|
|
385
|
+
}
|
|
386
|
+
// No cached entry — must wait for the network.
|
|
387
|
+
const res = await network;
|
|
388
|
+
if (res) return res;
|
|
389
|
+
return new Response('Unavailable offline', {
|
|
390
|
+
status: 503,
|
|
391
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
392
|
+
});
|
|
393
|
+
})(),
|
|
394
|
+
);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Default: network, fallback to cache, then explicit 503.
|
|
399
|
+
event.respondWith(
|
|
400
|
+
(async (): Promise<Response> => {
|
|
401
|
+
try {
|
|
402
|
+
return await fetch(event.request);
|
|
403
|
+
} catch {
|
|
404
|
+
const cached = await caches.match(event.request);
|
|
405
|
+
if (cached) return cached;
|
|
406
|
+
return new Response('Unavailable offline', {
|
|
407
|
+
status: 503,
|
|
408
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
})(),
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// ── Messages ──
|
|
416
|
+
sw.addEventListener('message', (event: ExtendableMessageEvent) => {
|
|
417
|
+
const data = event.data as { type?: string; urls?: string[] } | null;
|
|
418
|
+
if (data && data.type === 'SKIP_WAITING') {
|
|
419
|
+
sw.skipWaiting();
|
|
420
|
+
}
|
|
421
|
+
if (data && data.type === 'CACHE_URLS') {
|
|
422
|
+
const urls = data.urls ?? [];
|
|
423
|
+
caches.open(CACHE_NAME).then((cache) => cache.addAll(urls).catch(() => {}));
|
|
424
|
+
}
|
|
425
|
+
if (data && data.type === 'FLUSH_QUEUE') {
|
|
426
|
+
void flushMutationQueue();
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// ── Background sync ──
|
|
431
|
+
// The `WebWorker` lib has no `SyncEvent`, so receive the generic Event and
|
|
432
|
+
// narrow to our minimal SyncEvent interface (no `any`).
|
|
433
|
+
sw.addEventListener('sync', (event: Event) => {
|
|
434
|
+
const syncEvent = event as unknown as SyncEvent;
|
|
435
|
+
if (syncEvent.tag === 'pm-sync') {
|
|
436
|
+
syncEvent.waitUntil(flushMutationQueue());
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// ── Online event: flush queue when connectivity returns ──
|
|
441
|
+
sw.addEventListener('online', () => {
|
|
442
|
+
void flushMutationQueue();
|
|
443
|
+
});
|
package/public/src/views/auth.js
CHANGED
|
@@ -4,6 +4,27 @@
|
|
|
4
4
|
import { state } from '../state.js';
|
|
5
5
|
import { api } from '../api.js';
|
|
6
6
|
import { bootApp } from '../app.js';
|
|
7
|
+
async function configureOidcLogin() {
|
|
8
|
+
const button = document.getElementById('oidc-login');
|
|
9
|
+
const divider = document.getElementById('oidc-divider');
|
|
10
|
+
if (!button)
|
|
11
|
+
return;
|
|
12
|
+
try {
|
|
13
|
+
const config = await api('GET', '/auth/oidc/config');
|
|
14
|
+
button.hidden = !config.enabled;
|
|
15
|
+
if (divider)
|
|
16
|
+
divider.hidden = !config.enabled;
|
|
17
|
+
button.textContent = `Continue with ${config.label}`;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
button.hidden = true;
|
|
21
|
+
if (divider)
|
|
22
|
+
divider.hidden = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function startOidcLogin() {
|
|
26
|
+
window.location.assign('/api/auth/oidc/start');
|
|
27
|
+
}
|
|
7
28
|
export function switchAuthTab(tab) {
|
|
8
29
|
state.authTab = tab;
|
|
9
30
|
document.getElementById('tab-login')?.classList.toggle('active', tab === 'login');
|
|
@@ -77,5 +98,6 @@ export function showAuth() {
|
|
|
77
98
|
authScreen.style.display = 'flex';
|
|
78
99
|
if (mainApp)
|
|
79
100
|
mainApp.style.display = 'none';
|
|
101
|
+
void configureOidcLogin();
|
|
80
102
|
}
|
|
81
103
|
//# sourceMappingURL=auth.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"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"}
|
package/public/src/views/auth.ts
CHANGED
|
@@ -6,6 +6,25 @@ import { api } from '../api.js';
|
|
|
6
6
|
import { bootApp } from '../app.js';
|
|
7
7
|
import type { User } from '../types.js';
|
|
8
8
|
|
|
9
|
+
async function configureOidcLogin(): Promise<void> {
|
|
10
|
+
const button = document.getElementById('oidc-login') as HTMLButtonElement | null;
|
|
11
|
+
const divider = document.getElementById('oidc-divider') as HTMLElement | null;
|
|
12
|
+
if (!button) return;
|
|
13
|
+
try {
|
|
14
|
+
const config = await api('GET', '/auth/oidc/config') as { enabled: boolean; label: string };
|
|
15
|
+
button.hidden = !config.enabled;
|
|
16
|
+
if (divider) divider.hidden = !config.enabled;
|
|
17
|
+
button.textContent = `Continue with ${config.label}`;
|
|
18
|
+
} catch {
|
|
19
|
+
button.hidden = true;
|
|
20
|
+
if (divider) divider.hidden = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function startOidcLogin(): void {
|
|
25
|
+
window.location.assign('/api/auth/oidc/start');
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
export function switchAuthTab(tab: 'login' | 'register'): void {
|
|
10
29
|
state.authTab = tab;
|
|
11
30
|
document.getElementById('tab-login')?.classList.toggle('active', tab==='login');
|
|
@@ -71,4 +90,5 @@ export function showAuth(): void {
|
|
|
71
90
|
const mainApp = document.getElementById('main-app');
|
|
72
91
|
if (authScreen) authScreen.style.display = 'flex';
|
|
73
92
|
if (mainApp) mainApp.style.display = 'none';
|
|
93
|
+
void configureOidcLogin();
|
|
74
94
|
}
|