@unbrained/pm-web 2026.7.11 → 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.
Files changed (63) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +26 -0
  3. package/dist/app.d.ts +1 -0
  4. package/dist/app.js +38 -2
  5. package/dist/app.js.map +1 -1
  6. package/dist/auth.d.ts +2 -1
  7. package/dist/auth.js +10 -0
  8. package/dist/auth.js.map +1 -1
  9. package/dist/db.js +17 -1
  10. package/dist/db.js.map +1 -1
  11. package/dist/ical.js.map +1 -1
  12. package/dist/index.d.ts +1 -22
  13. package/dist/index.js +2 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/oidc.d.ts +89 -0
  16. package/dist/oidc.js +305 -0
  17. package/dist/oidc.js.map +1 -0
  18. package/dist/routes/auth.js +3 -15
  19. package/dist/routes/auth.js.map +1 -1
  20. package/dist/routes/github.js +3 -3
  21. package/dist/routes/github.js.map +1 -1
  22. package/dist/routes/oidc.d.ts +4 -0
  23. package/dist/routes/oidc.js +147 -0
  24. package/dist/routes/oidc.js.map +1 -0
  25. package/dist/routes/pm.js +121 -107
  26. package/dist/routes/pm.js.map +1 -1
  27. package/dist/routes/projects.js +9 -2
  28. package/dist/routes/projects.js.map +1 -1
  29. package/dist/server.js +7 -2
  30. package/dist/server.js.map +1 -1
  31. package/dist/services/pm-runner.d.ts +11 -3
  32. package/dist/services/pm-runner.js +167 -54
  33. package/dist/services/pm-runner.js.map +1 -1
  34. package/dist/services/realtime-bus.d.ts +1 -0
  35. package/dist/services/realtime-bus.js +148 -0
  36. package/dist/services/realtime-bus.js.map +1 -0
  37. package/dist/services/sse.d.ts +3 -1
  38. package/dist/services/sse.js +83 -48
  39. package/dist/services/sse.js.map +1 -1
  40. package/manifest.json +1 -1
  41. package/package.json +11 -11
  42. package/public/cookie-consent.js +54 -39
  43. package/public/cookie-settings.html +7 -7
  44. package/public/index.html +2 -0
  45. package/public/legal-notice.html +10 -12
  46. package/public/privacy-policy.html +14 -24
  47. package/public/src/app.js +4 -4
  48. package/public/src/app.js.map +1 -1
  49. package/public/src/app.ts +4 -4
  50. package/public/src/components/toast.js.map +1 -1
  51. package/public/src/cookie-consent.ts +68 -0
  52. package/public/src/sw.ts +443 -0
  53. package/public/src/views/auth.js +22 -0
  54. package/public/src/views/auth.js.map +1 -1
  55. package/public/src/views/auth.ts +20 -0
  56. package/public/src/views/graph.js.map +1 -1
  57. package/public/styles.css +2 -0
  58. package/public/sw.js +320 -256
  59. package/public/terms.html +7 -9
  60. package/public/tsconfig.json +1 -1
  61. package/public/tsconfig.scripts.json +18 -0
  62. package/public/tsconfig.sw.json +18 -0
  63. package/sql/schema.sql +16 -0
@@ -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
+ });
@@ -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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"}
@@ -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
  }