- Add comprehensive alarm scheduling and notification system - Support for VAlarm data structures from calendar events - Browser notification API integration with permission handling - localStorage persistence for alarms across sessions - Background service worker for alarm checking when app inactive - Real-time alarm detection with 30-second intervals - Debug logging for troubleshooting notification issues - Automatic permission requests when events with alarms are created - Support for Display action alarms with Duration and DateTime triggers - Clean alarm management (create, update, remove, expire) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
156 lines
5.0 KiB
JavaScript
156 lines
5.0 KiB
JavaScript
// Calendar Alarms Service Worker
|
|
// Handles background alarm checking when the main app is not active
|
|
|
|
const SW_VERSION = 'v1.0.0';
|
|
const CACHE_NAME = `calendar-alarms-${SW_VERSION}`;
|
|
const STORAGE_KEY = 'calendar_alarms';
|
|
|
|
// Install event
|
|
self.addEventListener('install', event => {
|
|
console.log(`Service Worker ${SW_VERSION} installing...`);
|
|
self.skipWaiting(); // Activate immediately
|
|
});
|
|
|
|
// Activate event
|
|
self.addEventListener('activate', event => {
|
|
console.log(`Service Worker ${SW_VERSION} activated`);
|
|
event.waitUntil(self.clients.claim()); // Take control immediately
|
|
});
|
|
|
|
// Message handler for communication with main app
|
|
self.addEventListener('message', event => {
|
|
const { type, data } = event.data;
|
|
|
|
switch (type) {
|
|
case 'CHECK_ALARMS':
|
|
handleCheckAlarms(event, data);
|
|
break;
|
|
case 'SCHEDULE_ALARM':
|
|
handleScheduleAlarm(data, event);
|
|
break;
|
|
case 'REMOVE_ALARM':
|
|
handleRemoveAlarm(data, event);
|
|
break;
|
|
case 'PING':
|
|
event.ports[0].postMessage({ type: 'PONG', version: SW_VERSION });
|
|
break;
|
|
default:
|
|
console.warn('Unknown message type:', type);
|
|
}
|
|
});
|
|
|
|
// Handle alarm checking request
|
|
function handleCheckAlarms(event, data) {
|
|
try {
|
|
// Main app sends alarms data to check
|
|
const allAlarms = data?.alarms || [];
|
|
const dueAlarms = checkProvidedAlarms(allAlarms);
|
|
|
|
// Send results back to main app
|
|
event.ports[0].postMessage({
|
|
type: 'ALARMS_DUE',
|
|
data: dueAlarms
|
|
});
|
|
|
|
console.log(`Checked ${allAlarms.length} alarms, found ${dueAlarms.length} due`);
|
|
} catch (error) {
|
|
console.error('Error checking alarms:', error);
|
|
event.ports[0].postMessage({
|
|
type: 'ALARM_CHECK_ERROR',
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
// Process alarms sent from main app
|
|
function checkProvidedAlarms(alarms) {
|
|
const now = new Date();
|
|
const nowStr = formatDateTimeForComparison(now);
|
|
|
|
// Filter alarms that should trigger and are pending
|
|
const dueAlarms = alarms.filter(alarm => {
|
|
return alarm.status === 'Pending' && alarm.trigger_time <= nowStr;
|
|
});
|
|
|
|
return dueAlarms;
|
|
}
|
|
|
|
// Handle schedule alarm request (not needed with localStorage approach)
|
|
function handleScheduleAlarm(alarmData, event) {
|
|
// Service worker doesn't handle storage with localStorage approach
|
|
// Main app handles all storage operations
|
|
event.ports[0].postMessage({
|
|
type: 'ALARM_SCHEDULED',
|
|
data: { success: true, alarmId: alarmData.id }
|
|
});
|
|
}
|
|
|
|
// Handle remove alarm request (not needed with localStorage approach)
|
|
function handleRemoveAlarm(alarmData, event) {
|
|
// Service worker doesn't handle storage with localStorage approach
|
|
// Main app handles all storage operations
|
|
event.ports[0].postMessage({
|
|
type: 'ALARM_REMOVED',
|
|
data: { success: true, eventUid: alarmData.eventUid }
|
|
});
|
|
}
|
|
|
|
|
|
// Format date time for comparison (YYYY-MM-DDTHH:MM:SS)
|
|
function formatDateTimeForComparison(date) {
|
|
return date.getFullYear() + '-' +
|
|
String(date.getMonth() + 1).padStart(2, '0') + '-' +
|
|
String(date.getDate()).padStart(2, '0') + 'T' +
|
|
String(date.getHours()).padStart(2, '0') + ':' +
|
|
String(date.getMinutes()).padStart(2, '0') + ':' +
|
|
String(date.getSeconds()).padStart(2, '0');
|
|
}
|
|
|
|
// Background alarm checking (runs periodically)
|
|
// Note: Service worker can't access localStorage, so this just pings the main app
|
|
setInterval(async () => {
|
|
try {
|
|
// Notify all clients to check their alarms
|
|
const clients = await self.clients.matchAll();
|
|
|
|
clients.forEach(client => {
|
|
client.postMessage({
|
|
type: 'BACKGROUND_ALARM_CHECK_REQUEST'
|
|
});
|
|
});
|
|
|
|
console.log('Requested alarm check from main app');
|
|
} catch (error) {
|
|
console.error('Background alarm check failed:', error);
|
|
}
|
|
}, 60000); // Check every minute
|
|
|
|
// Handle push notifications (for future enhancement)
|
|
self.addEventListener('push', event => {
|
|
console.log('Push notification received:', event);
|
|
// Future: Handle server-sent alarm notifications
|
|
});
|
|
|
|
// Handle notification clicks
|
|
self.addEventListener('notificationclick', event => {
|
|
console.log('Notification clicked:', event);
|
|
|
|
event.notification.close();
|
|
|
|
// Focus or open the calendar app
|
|
event.waitUntil(
|
|
self.clients.matchAll().then(clients => {
|
|
// Try to focus existing client
|
|
for (const client of clients) {
|
|
if (client.url.includes('localhost') || client.url.includes(self.location.origin)) {
|
|
return client.focus();
|
|
}
|
|
}
|
|
|
|
// Open new window if no client exists
|
|
return self.clients.openWindow('/');
|
|
})
|
|
);
|
|
});
|
|
|
|
console.log(`Calendar Alarms Service Worker ${SW_VERSION} loaded`); |