function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); const rawData = atob(base64); return Uint8Array.from([...rawData].map((c) => c.charCodeAt(0))); } function isIOS() { return /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); } function isStandalone() { return window.matchMedia('(display-mode: standalone)').matches || navigator.standalone === true; } const notificationsMessage = document.getElementById('notificationsMessage'); const notificationsBtn = document.getElementById('notificationsBtn'); function showMessage(text) { notificationsMessage.textContent = text; notificationsMessage.style.display = 'block'; notificationsBtn.style.display = 'none'; } function showButton(label, onClick) { notificationsMessage.style.display = 'none'; notificationsBtn.textContent = label; notificationsBtn.style.display = 'inline-block'; notificationsBtn.onclick = onClick; } let cachedPublicKey = null; async function subscribe() { const reg = await navigator.serviceWorker.ready; const permission = await Notification.requestPermission(); if (permission !== 'granted') { return initNotifications(); } const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(cachedPublicKey), }); await api.post('/api/push/subscribe', { subscription: sub.toJSON() }); initNotifications(); } async function unsubscribe() { const reg = await navigator.serviceWorker.ready; const sub = await reg.pushManager.getSubscription(); if (sub) { const endpoint = sub.endpoint; await sub.unsubscribe(); await api.del('/api/push/subscribe', { endpoint }); } initNotifications(); } async function initNotifications() { if (!notificationsMessage) return; if (!('serviceWorker' in navigator) || !('PushManager' in window)) { return showMessage('Push notifications aren\'t supported in this browser.'); } if (!window.isSecureContext) { return showMessage('Notifications require HTTPS. Set up a reverse proxy with a real certificate to use this.'); } let vapid; try { vapid = await api.get('/api/push/vapid-public-key'); } catch { return showMessage('Could not check notification status.'); } if (!vapid.enabled) { return showMessage('Push notifications aren\'t configured on this server yet.'); } cachedPublicKey = vapid.publicKey; if (isIOS() && !isStandalone()) { return showMessage('On iPhone/iPad: tap Share → Add to Home Screen, then open the app icon from your Home Screen and come back here to enable notifications.'); } if (Notification.permission === 'denied') { return showMessage('Notifications are blocked for this site in your browser settings.'); } const reg = await navigator.serviceWorker.register('/sw.js'); await navigator.serviceWorker.ready; const existingSub = await reg.pushManager.getSubscription(); if (existingSub) { showButton('Disable notifications on this device', unsubscribe); } else { showButton('Enable notifications on this device', subscribe); } } if (notificationsMessage) { initNotifications(); }