(function(root, factory) { if (typeof module === 'object' && module.exports) module.exports = factory; else root.createPushNotifications = factory; })(typeof self !== 'undefined' ? self : this, function createPushNotifications({ control, status, testControl, deadlineControl, deadlineStatus, deadlineHour, deadlineDays, deadlineSnooze, deadlineSnoozeStatus, deadlineSnoozeReview, onReviewDeadlines, notification, serviceWorker, fetchJson, }) { let configuration = null; function renderDeliveryHealth() { const health = Object.values(configuration?.delivery_health || {}); const degraded = health.find(item => item?.state === 'degraded'); if (testControl) testControl.hidden = !configuration?.subscribed; if (configuration?.subscribed && degraded) { const count = Number(degraded.consecutive_failures || 1); status.textContent = `Update notifications need attention after ${count} failed ${count === 1 ? 'delivery' : 'deliveries'}. Send a test notification.`; return; } status.textContent = configuration?.subscribed ? 'New update notifications enabled for this device.' : 'New update notifications are off for this device.'; } async function testDelivery() { if (!testControl) return; testControl.disabled = true; status.textContent = 'Sending a test notification…'; try { await fetchJson('api/v1/push-subscription/test', {method:'POST'}); configuration.delivery_health = {unread:{state:'healthy',consecutive_failures:0}}; status.textContent = 'Test delivered. Update notifications are working on this device.'; } catch (error) { status.textContent = 'Test delivery failed. Check your connection, then try again.'; } finally { testControl.disabled = false; } } function formattedHour(value) { return `${String(Number(value)).padStart(2, '0')}:00`; } function enabledDeadlineText(hour, days) { const value = Number(days ?? 2); const horizon = value === 0 ? 'due today' : `next ${value} days`; return `Deadline reminders enabled for ${formattedHour(hour)} local time, ${horizon}.`; } function renderDeadlineSnooze() { if (!deadlineSnooze) return; const wakeAt = Number(configuration?.snoozed_until || 0); deadlineSnooze.hidden = !wakeAt; if (wakeAt && deadlineSnoozeStatus) { const localTime = new Intl.DateTimeFormat(undefined, { hour:'numeric', minute:'2-digit', }).format(new Date(wakeAt * 1000)); deadlineSnoozeStatus.textContent = `Deadline reminders snoozed until ${localTime}.`; } } async function reviewSnoozedDeadlines() { deadlineSnoozeReview.disabled = true; try { await fetchJson('api/v1/push-subscription/deadlines/snooze', {method:'DELETE'}); configuration.snoozed_until = null; renderDeadlineSnooze(); onReviewDeadlines?.(); } catch (error) { deadlineSnoozeStatus.textContent = 'Could not resume deadline reminders. Check your connection and try again.'; } finally { deadlineSnoozeReview.disabled = false; } } function deadlineReadiness() { if (!configuration?.available || deadlineControl?.disabled) { return {state:'unavailable', detail:deadlineStatus?.textContent || 'Deadline reminders are unavailable.'}; } return configuration.deadline_enabled ? {state:'complete', detail:enabledDeadlineText(configuration.reminder_hour, configuration.reminder_days)} : {state:'incomplete', detail:deadlineStatus?.textContent || 'Choose when to receive deadline reminders.'}; } function applicationServerKey(value) { const padded = value.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - value.length % 4) % 4); if (typeof atob === 'function') { return Uint8Array.from(atob(padded), character => character.charCodeAt(0)); } return Uint8Array.from(Buffer.from(padded, 'base64')); } async function disable() { const registration = await serviceWorker.ready; const subscription = await registration.pushManager.getSubscription(); await fetchJson('api/v1/push-subscription', {method:'DELETE'}); await subscription?.unsubscribe?.(); control.checked = false; if (testControl) testControl.hidden = true; if (deadlineControl) deadlineControl.checked = false; status.textContent = 'New update notifications are off for this device.'; if (deadlineStatus) deadlineStatus.textContent = 'Deadline reminders are off for this device.'; } async function ensureSubscription() { const permission = await notification.requestPermission(); if (permission !== 'granted') { control.checked = false; status.textContent = 'Notifications are blocked. Allow them in your browser settings to enable updates.'; return null; } const registration = await serviceWorker.ready; let subscription = await registration.pushManager.getSubscription(); if (!subscription) { subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: applicationServerKey(configuration.public_key), }); } await fetchJson('api/v1/push-subscription', { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify(subscription.toJSON()), }); control.checked = true; status.textContent = 'New update notifications enabled for this device.'; configuration.subscribed = true; if (testControl) testControl.hidden = false; return subscription; } async function enable() { await ensureSubscription(); } async function change() { control.disabled = true; try { if (control.checked) await enable(); else await disable(); } catch (error) { control.checked = !control.checked; status.textContent = 'Could not change update notifications. Check your connection and try again.'; } finally { control.disabled = false; } } async function changeDeadline() { deadlineControl.disabled = true; if (deadlineHour) deadlineHour.disabled = true; if (deadlineDays) deadlineDays.disabled = true; try { const registration = await serviceWorker.ready; let subscription = await registration.pushManager.getSubscription(); if (deadlineControl.checked && !subscription) subscription = await ensureSubscription(); if (deadlineControl.checked && !subscription) { deadlineControl.checked = false; deadlineStatus.textContent = status.textContent; return false; } const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; const reminderHour = Number(deadlineHour?.value ?? configuration?.reminder_hour ?? 9); const reminderDays = Number(deadlineDays?.value ?? configuration?.reminder_days ?? 2); await fetchJson('api/v1/push-subscription/deadlines', { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({enabled:deadlineControl.checked, timezone, reminder_hour:reminderHour, reminder_days:reminderDays}), }); configuration.deadline_enabled = deadlineControl.checked; configuration.reminder_hour = reminderHour; configuration.reminder_days = reminderDays; configuration.timezone = timezone; deadlineStatus.textContent = deadlineControl.checked ? enabledDeadlineText(reminderHour, reminderDays) : 'Deadline reminders are off for this device.'; return true; } catch (error) { deadlineControl.checked = !deadlineControl.checked; deadlineStatus.textContent = 'Could not change deadline reminders. Check your connection and try again.'; return false; } finally { deadlineControl.disabled = false; if (deadlineHour) deadlineHour.disabled = false; if (deadlineDays) deadlineDays.disabled = false; } } async function enableDeadline() { deadlineControl.checked = true; return changeDeadline(); } async function init() { if (!control || !notification || !serviceWorker) return; control.addEventListener('change', change); testControl?.addEventListener('click', testDelivery); deadlineControl?.addEventListener('change', changeDeadline); deadlineSnoozeReview?.addEventListener('click', reviewSnoozedDeadlines); configuration = await fetchJson('api/v1/push-subscription'); if (!configuration.available) { control.disabled = true; if (deadlineControl) deadlineControl.disabled = true; status.textContent = 'New update notifications are not available on this server.'; return; } control.checked = Boolean(configuration.subscribed); if (deadlineControl) deadlineControl.checked = Boolean(configuration.deadline_enabled); if (deadlineHour) deadlineHour.value = String(configuration.reminder_hour ?? 9); if (deadlineDays) deadlineDays.value = String(configuration.reminder_days ?? 2); renderDeliveryHealth(); if (deadlineStatus) deadlineStatus.textContent = configuration.deadline_enabled ? enabledDeadlineText(configuration.reminder_hour, configuration.reminder_days) : 'Deadline reminders are off for this device.'; renderDeadlineSnooze(); } return {init, change, changeDeadline, enableDeadline, deadlineReadiness}; });