Files
Hermes Bot a440160249 Add sales automation stack: strategy docs, MCP scripts, PWA refinements
- SALES_STRATEGY.md: $1K/day plan (37 orders @ $27.50 AOV, 1,700 visits)
- TRIPWIRE_FUNNEL.md: 7-email Mautic sequence + lead scoring
- MONETIZATION.md: 9 monetization options for Nūr PWA (no ads, no registration)
- EXECUTABLE_PLAN.md: MCP-executable automation plan
- DAILY_OPS.md: daily KPI checklist (visits, orders, sends, recoveries)
- scripts/: automation.py, mautic_setup.py, gumroad_setup.py, scout_scrape.py, analytics.py, crontab
- quality_leads.json/.csv: 12 enriched Islamic-tech leads for Mautic import
- content_queue.json: 10 SEO blog posts for Ghost
- PWA: icons, sw.js, e2e + vitest coverage, generator scripts
2026-08-06 23:33:21 +08:00

197 lines
5.3 KiB
JavaScript

// Custom Service Worker for Nūr — Muslim Companion
// Handles push notifications for prayer times
// Workbox precache manifest injection point
self.__WB_MANIFEST;
const CACHE_NAME = 'nur-v1';
const PRAYER_CACHE = 'prayer-times-cache';
// Install event
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/manifest.webmanifest',
'/icon-192.png',
'/icon-512.png',
'/favicon.svg',
'/apple-touch-icon.png'
]);
})
);
self.skipWaiting();
});
// Activate event
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME && name !== PRAYER_CACHE)
.map((name) => caches.delete(name))
);
})
);
self.clients.claim();
});
// Fetch event - network first for API, cache first for static
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// API calls - network first with cache fallback
if (url.origin === 'https://api.aladhan.com' || url.origin === 'https://api.alquran.cloud') {
event.respondWith(
fetch(event.request)
.then((response) => {
const responseClone = response.clone();
caches.open(PRAYER_CACHE).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => caches.match(event.request))
);
return;
}
// Static assets - cache first
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) return cachedResponse;
return fetch(event.request).then((response) => {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
});
})
);
});
// Push event - handle push notifications
self.addEventListener('push', (event) => {
if (!event.data) return;
const data = event.data.json();
const options = {
body: data.body,
icon: '/icon-192.png',
badge: '/icon-192.png',
vibrate: [200, 100, 200],
tag: data.tag || 'prayer-notification',
renotify: true,
requireInteraction: true,
actions: [
{ action: 'open', title: 'Open App' },
{ action: 'dismiss', title: 'Dismiss' }
],
data: data.data || {}
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// Notification click event
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'dismiss') return;
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
// If app is already open, focus it
for (const client of clientList) {
if (client.url.includes(self.location.origin) && 'focus' in client) {
return client.focus();
}
}
// Otherwise open new window
if (clients.openWindow) {
return clients.openWindow('/');
}
})
);
});
// Background sync for periodic prayer time updates
self.addEventListener('sync', (event) => {
if (event.tag === 'prayer-times-sync') {
event.waitUntil(syncPrayerTimes());
}
});
// Periodic background sync (if supported)
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'prayer-times-periodic') {
event.waitUntil(syncPrayerTimes());
}
});
async function syncPrayerTimes() {
try {
// Get cached prayer times or fetch new ones
const cache = await caches.open(PRAYER_CACHE);
const cached = await cache.match('/prayer-times');
if (cached) {
const data = await cached.json();
scheduleNotifications(data);
}
} catch (e) {
console.error('Failed to sync prayer times:', e);
}
}
function scheduleNotifications(prayerData) {
const now = Date.now();
const prayers = ['Fajr', 'Dhuhr', 'Asr', 'Maghrib', 'Isha'];
prayers.forEach((prayer) => {
const prayerTimeStr = prayerData.timings[prayer];
if (!prayerTimeStr) return;
const [hours, minutes] = prayerTimeStr.split(':').map(Number);
const prayerDate = new Date();
prayerDate.setHours(hours, minutes, 0, 0);
const timeUntilPrayer = prayerDate.getTime() - now;
// Schedule notification 5 minutes before prayer
const notifyTime = timeUntilPrayer - 5 * 60 * 1000;
if (notifyTime > 0 && notifyTime < 24 * 60 * 60 * 1000) {
setTimeout(() => {
self.registration.showNotification(`Time for ${prayer}`, {
body: `It's time for ${prayer} prayer (${prayerTimeStr})`,
icon: '/icon-192.png',
badge: '/icon-192.png',
vibrate: [200, 100, 200],
tag: `prayer-${prayer}`,
renotify: true,
requireInteraction: true
});
}, notifyTime);
}
});
}
// Message event - communicate with main thread
self.addEventListener('message', (event) => {
if (event.data.type === 'SCHEDULE_PRAYERS') {
scheduleNotifications(event.data.payload);
}
if (event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
console.log('[Nūr SW] Service Worker loaded');