diff --git a/kjvstudy_org/server.py b/kjvstudy_org/server.py
index 6bb0391..905ddd1 100644
--- a/kjvstudy_org/server.py
+++ b/kjvstudy_org/server.py
@@ -219,23 +219,6 @@ current_dir = PathLib(__file__).parent
static_dir = current_dir / "static"
templates_dir = current_dir / "templates"
-# Serve service worker with proper header to allow root scope
-@app.get("/sw.js")
-async def service_worker():
- """Serve service worker from root with Service-Worker-Allowed header."""
- from fastapi.responses import FileResponse
- sw_path = static_dir / "sw.js"
- return FileResponse(
- sw_path,
- media_type="application/javascript",
- headers={
- "Service-Worker-Allowed": "/",
- "Cache-Control": "no-cache, no-store, must-revalidate",
- "Pragma": "no-cache",
- "Expires": "0"
- }
- )
-
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
templates = Jinja2Templates(directory=str(templates_dir))
@@ -1310,15 +1293,6 @@ def get_daily_verse(date_str=None):
-@app.get("/offline", response_class=HTMLResponse)
-async def offline_reader(request: Request):
- """Offline Bible reader - renders chapters from cached JSON."""
- return templates.TemplateResponse(
- "offline.html",
- {"request": request}
- )
-
-
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
books = bible.get_books()
diff --git a/kjvstudy_org/static/manifest.json b/kjvstudy_org/static/manifest.json
deleted file mode 100644
index 41730e9..0000000
--- a/kjvstudy_org/static/manifest.json
+++ /dev/null
@@ -1,58 +0,0 @@
-{
- "name": "KJV Study - Bible Commentary Platform",
- "short_name": "KJV Study",
- "description": "Study the King James Bible with AI-powered commentary and insights",
- "start_url": "/",
- "display": "standalone",
- "background_color": "#fdfcf9",
- "theme_color": "#4b2e83",
- "orientation": "portrait-primary",
- "scope": "/",
- "lang": "en",
- "categories": ["education", "books", "reference"],
- "icons": [
- {
- "src": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 192 192'%3E%3Crect width='192' height='192' fill='%234b2e83' rx='24'/%3E%3Ctext x='96' y='120' text-anchor='middle' font-size='80' fill='white'%3E📖%3C/text%3E%3C/svg%3E",
- "sizes": "192x192",
- "type": "image/svg+xml",
- "purpose": "any maskable"
- },
- {
- "src": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Crect width='512' height='512' fill='%234b2e83' rx='64'/%3E%3Ctext x='256' y='320' text-anchor='middle' font-size='200' fill='white'%3E📖%3C/text%3E%3C/svg%3E",
- "sizes": "512x512",
- "type": "image/svg+xml",
- "purpose": "any"
- }
- ],
- "shortcuts": [
- {
- "name": "Browse Books",
- "short_name": "Books",
- "description": "Browse all Bible books",
- "url": "/",
- "icons": [
- {
- "src": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%234b2e83'%3E%3Cpath d='M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z'/%3E%3C/svg%3E",
- "sizes": "96x96"
- }
- ]
- }
- ],
- "screenshots": [
- {
- "src": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 540 720'%3E%3Crect width='540' height='720' fill='%23fdfcf9'/%3E%3Crect x='20' y='20' width='500' height='680' fill='white' stroke='%23e5e7eb' stroke-width='1' rx='8'/%3E%3Ctext x='270' y='80' text-anchor='middle' font-size='32' fill='%234b2e83' font-family='serif'%3EGenesis 1%3C/text%3E%3Ctext x='40' y='140' font-size='18' fill='%23333' font-family='serif'%3E1 In the beginning God created the heaven and the earth.%3C/text%3E%3Ctext x='40' y='180' font-size='18' fill='%23333' font-family='serif'%3E2 And the earth was without form, and void...%3C/text%3E%3C/svg%3E",
- "sizes": "540x720",
- "type": "image/svg+xml",
- "form_factor": "narrow"
- }
- ],
- "prefer_related_applications": false,
- "related_applications": [],
- "features": [
- "Cross Platform",
- "offline-reading",
- "note-taking",
- "highlighting",
- "search"
- ]
-}
\ No newline at end of file
diff --git a/kjvstudy_org/static/sw.js b/kjvstudy_org/static/sw.js
deleted file mode 100644
index d1eb59b..0000000
--- a/kjvstudy_org/static/sw.js
+++ /dev/null
@@ -1,345 +0,0 @@
-// KJV Study Service Worker - Offline Bible Access
-const CACHE_VERSION = 'v6';
-const STATIC_CACHE = 'kjvstudy-static-' + CACHE_VERSION;
-const PAGE_CACHE = 'kjvstudy-pages-' + CACHE_VERSION;
-
-// Static assets to cache immediately on install
-const STATIC_ASSETS = [
- '/',
- '/offline',
- '/static/tufte.css',
- '/static/style.css',
- '/static/manifest.json',
- '/static/verses-1769.json',
- '/books'
-];
-
-// Install event - cache static assets only
-self.addEventListener('install', (event) => {
- console.log('[SW] Installing service worker...');
- event.waitUntil(
- caches.open(STATIC_CACHE)
- .then((cache) => {
- console.log('[SW] Caching static assets');
- return cache.addAll(STATIC_ASSETS);
- })
- .then(() => {
- console.log('[SW] Static assets cached');
- return self.skipWaiting();
- })
- .catch((err) => {
- console.error('[SW] Failed to cache static assets:', err);
- })
- );
-});
-
-// Activate event - clean up old caches
-self.addEventListener('activate', (event) => {
- console.log('[SW] Activating service worker...');
- event.waitUntil(
- caches.keys()
- .then((cacheNames) => {
- return Promise.all(
- cacheNames.map((cacheName) => {
- if (cacheName.startsWith('kjvstudy-') &&
- cacheName !== STATIC_CACHE &&
- cacheName !== PAGE_CACHE) {
- console.log('[SW] Deleting old cache:', cacheName);
- return caches.delete(cacheName);
- }
- })
- );
- })
- .then(() => {
- console.log('[SW] Service worker activated');
- return self.clients.claim();
- })
- );
-});
-
-// Parse sitemap XML and extract URLs
-async function parseSitemap(url) {
- try {
- const response = await fetch(url);
- const text = await response.text();
- const urls = [];
-
- // Extract URLs from tags
- const locRegex = /([^<]+)<\/loc>/g;
- let match;
- while ((match = locRegex.exec(text)) !== null) {
- // Convert absolute URL to relative path
- let path = match[1].replace(/^https?:\/\/[^\/]+/, '');
- if (path) urls.push(path);
- }
-
- return urls;
- } catch (err) {
- console.error('[SW] Failed to parse sitemap:', url, err);
- return [];
- }
-}
-
-// Get all URLs from sitemaps
-async function getAllUrlsFromSitemaps() {
- const allUrls = new Set();
-
- // First, fetch the sitemap index
- try {
- const indexResponse = await fetch('/sitemap.xml');
- const indexText = await indexResponse.text();
-
- // Check if it's a sitemap index (has )
- if (indexText.includes('([^<]+)<\/loc>/g;
- let match;
- while ((match = locRegex.exec(indexText)) !== null) {
- let sitemapUrl = match[1].replace(/^https?:\/\/[^\/]+/, '');
- sitemapUrls.push(sitemapUrl);
- }
-
- // Fetch each child sitemap
- for (const sitemapUrl of sitemapUrls) {
- const urls = await parseSitemap(sitemapUrl);
- urls.forEach(url => allUrls.add(url));
- }
- } else {
- // It's a regular sitemap
- const urls = await parseSitemap('/sitemap.xml');
- urls.forEach(url => allUrls.add(url));
- }
- } catch (err) {
- console.error('[SW] Failed to fetch sitemap index:', err);
- }
-
- return Array.from(allUrls);
-}
-
-// Background pre-caching - only triggered when user requests it
-let cachingInProgress = false;
-let cachedCount = 0;
-let totalToCache = 0;
-
-async function startBackgroundCaching() {
- if (cachingInProgress) return;
- cachingInProgress = true;
- cachedCount = 0;
-
- console.log('[SW] Fetching URLs from sitemaps...');
- notifyClients({ type: 'CACHE_STATUS', status: 'Fetching page list from sitemaps...' });
-
- // Get all URLs from sitemaps
- const allUrls = await getAllUrlsFromSitemaps();
-
- console.log('[SW] Found', allUrls.length, 'URLs in sitemaps');
-
- if (allUrls.length === 0) {
- notifyClients({ type: 'CACHE_ERROR', error: 'No URLs found in sitemaps' });
- cachingInProgress = false;
- return;
- }
-
- const cache = await caches.open(PAGE_CACHE);
-
- // Check which pages are already cached
- const uncachedPages = [];
- for (const url of allUrls) {
- const cached = await cache.match(url);
- if (!cached) {
- uncachedPages.push(url);
- }
- }
-
- totalToCache = uncachedPages.length;
- console.log('[SW] Need to cache', totalToCache, 'pages');
-
- if (totalToCache === 0) {
- notifyClients({ type: 'CACHE_COMPLETE', total: allUrls.length });
- cachingInProgress = false;
- return;
- }
-
- notifyClients({
- type: 'CACHE_PROGRESS',
- cached: 0,
- total: totalToCache,
- status: `Downloading ${totalToCache.toLocaleString()} pages...`
- });
-
- // Concurrent pool - keep N requests in flight at all times
- const CONCURRENCY = 80; // Number of concurrent requests
- const PROGRESS_INTERVAL = 200; // Notify every N completions
-
- let nextIndex = 0;
- let lastNotified = 0;
-
- async function cacheUrl(url) {
- try {
- const response = await fetch(url);
- if (response.ok) {
- await cache.put(url, response);
- cachedCount++;
-
- // Notify progress periodically
- if (cachedCount - lastNotified >= PROGRESS_INTERVAL) {
- lastNotified = cachedCount;
- notifyClients({
- type: 'CACHE_PROGRESS',
- cached: cachedCount,
- total: totalToCache
- });
- }
- }
- } catch (err) {
- // Silent fail for individual pages
- }
- }
-
- async function worker() {
- while (nextIndex < uncachedPages.length) {
- const url = uncachedPages[nextIndex++];
- await cacheUrl(url);
- }
- }
-
- // Start concurrent workers
- const workers = [];
- for (let i = 0; i < Math.min(CONCURRENCY, uncachedPages.length); i++) {
- workers.push(worker());
- }
-
- // Wait for all workers to complete
- await Promise.all(workers);
-
- console.log('[SW] Background caching complete!', cachedCount, 'pages cached');
- cachingInProgress = false;
-
- notifyClients({ type: 'CACHE_COMPLETE', total: allUrls.length });
-}
-
-// Notify all clients of caching progress
-async function notifyClients(message) {
- const clients = await self.clients.matchAll();
- clients.forEach(client => client.postMessage(message));
-}
-
-// Fetch event - serve from cache, fallback to network
-self.addEventListener('fetch', (event) => {
- const url = new URL(event.request.url);
-
- // Only handle same-origin requests
- if (url.origin !== location.origin) {
- return;
- }
-
- // Skip non-GET requests
- if (event.request.method !== 'GET') {
- return;
- }
-
- // Skip API requests - network first with cache fallback
- if (url.pathname.startsWith('/api/')) {
- event.respondWith(
- fetch(event.request)
- .then((response) => {
- if (response.ok) {
- const responseClone = response.clone();
- caches.open(PAGE_CACHE).then((cache) => {
- cache.put(event.request, responseClone);
- });
- }
- return response;
- })
- .catch(() => caches.match(event.request))
- );
- return;
- }
-
- // Static assets - cache first
- if (url.pathname.startsWith('/static/')) {
- event.respondWith(
- caches.match(event.request)
- .then((cachedResponse) => {
- if (cachedResponse) {
- return cachedResponse;
- }
- return fetch(event.request).then((response) => {
- if (response.ok) {
- const responseClone = response.clone();
- caches.open(STATIC_CACHE).then((cache) => {
- cache.put(event.request, responseClone);
- });
- }
- return response;
- });
- })
- );
- return;
- }
-
- // All other requests - network first, cache fallback, offline reader as last resort
- event.respondWith(
- fetch(event.request)
- .then((response) => {
- if (response.ok) {
- const responseClone = response.clone();
- caches.open(PAGE_CACHE).then((cache) => {
- cache.put(event.request, responseClone);
- });
- }
- return response;
- })
- .catch(() => {
- return caches.match(event.request).then((cachedResponse) => {
- if (cachedResponse) {
- return cachedResponse;
- }
-
- // For HTML pages, try to redirect to offline reader with context
- if (event.request.headers.get('Accept')?.includes('text/html')) {
- // Try to extract book/chapter from various URL patterns
- const bookChapterMatch = url.pathname.match(/\/(?:book|interlinear)\/([^\/]+)(?:\/(?:chapter\/)?(\d+))?/);
- if (bookChapterMatch) {
- const book = decodeURIComponent(bookChapterMatch[1]);
- const chapter = bookChapterMatch[2] || '1';
- return Response.redirect('/offline?book=' + encodeURIComponent(book) + '&chapter=' + chapter, 302);
- }
-
- // Default to offline page
- return caches.match('/offline');
- }
- });
- })
- );
-});
-
-// Handle messages from the main thread
-self.addEventListener('message', (event) => {
- if (event.data && event.data.type === 'SKIP_WAITING') {
- self.skipWaiting();
- }
-
- if (event.data && event.data.type === 'START_CACHING') {
- console.log('[SW] Received START_CACHING request');
- startBackgroundCaching();
- }
-
- // Report current caching status
- if (event.data && event.data.type === 'GET_CACHE_STATUS') {
- if (cachingInProgress) {
- event.source.postMessage({
- type: 'CACHE_PROGRESS',
- cached: cachedCount,
- total: totalToCache,
- inProgress: true
- });
- } else {
- event.source.postMessage({
- type: 'CACHE_IDLE',
- inProgress: false
- });
- }
- }
-});
diff --git a/kjvstudy_org/templates/base.html b/kjvstudy_org/templates/base.html
index 66d0957..51a49ea 100644
--- a/kjvstudy_org/templates/base.html
+++ b/kjvstudy_org/templates/base.html
@@ -1306,15 +1306,6 @@
-
-
-
@@ -2255,122 +2246,6 @@
s.parentNode.insertBefore(t, s);
})();
{% endif %}
-
- // Service Worker for offline support
- (function() {
- if ('serviceWorker' in navigator) {
- window.addEventListener('load', function() {
- navigator.serviceWorker.register('/sw.js', { scope: '/' })
- .then(function(registration) {
- console.log('[App] Service Worker registered:', registration.scope);
-
- // Check for updates periodically
- setInterval(function() {
- registration.update();
- }, 60 * 60 * 1000); // Check every hour
- })
- .catch(function(error) {
- console.log('[App] Service Worker registration failed:', error);
- });
- });
-
- // Listen for cache progress messages from service worker
- navigator.serviceWorker.addEventListener('message', function(event) {
- var data = event.data;
- if (data.type === 'CACHE_PROGRESS') {
- showCacheProgress(data.cached, data.total);
- } else if (data.type === 'CACHE_COMPLETE') {
- showCacheComplete(data.total);
- }
- });
- }
-
- // Cache progress indicator
- var cacheIndicator = null;
- function showCacheProgress(cached, total) {
- if (!cacheIndicator) {
- cacheIndicator = document.createElement('a');
- cacheIndicator.id = 'cache-indicator';
- cacheIndicator.href = '/offline';
- cacheIndicator.style.cssText = 'position:fixed;bottom:1rem;left:1rem;padding:0.5rem 1rem;background:#4a7c59;color:white;border-radius:4px;font-size:0.85rem;z-index:9999;box-shadow:0 2px 8px rgba(0,0,0,0.2);transition:opacity 0.3s;text-decoration:none;cursor:pointer;';
- document.body.appendChild(cacheIndicator);
- }
- var pct = Math.round((cached / total) * 100);
- cacheIndicator.innerHTML = 'Caching for offline: ' + pct + '%';
- cacheIndicator.style.opacity = '1';
- }
-
- function showCacheComplete(total) {
- if (cacheIndicator) {
- cacheIndicator.innerHTML = 'Ready for offline! (' + total.toLocaleString() + ' pages)';
- setTimeout(function() {
- cacheIndicator.style.opacity = '0';
- setTimeout(function() {
- if (cacheIndicator && cacheIndicator.parentNode) {
- cacheIndicator.parentNode.removeChild(cacheIndicator);
- cacheIndicator = null;
- }
- }, 300);
- }, 3000);
- }
- }
-
- // Offline/Online indicator and PDF button handling
- function updateOnlineStatus() {
- var indicator = document.getElementById('offline-indicator');
- if (!indicator) {
- indicator = document.createElement('div');
- indicator.id = 'offline-indicator';
- indicator.style.cssText = 'position:fixed;bottom:1rem;right:1rem;padding:0.5rem 1rem;background:#c41e3a;color:white;border-radius:4px;font-size:0.85rem;z-index:9999;display:none;box-shadow:0 2px 8px rgba(0,0,0,0.2);';
- indicator.innerHTML = 'Offline Mode';
- document.body.appendChild(indicator);
- }
-
- // Find all PDF links/buttons
- var pdfLinks = document.querySelectorAll('a[href*="/pdf"], .pdf-btn, a[href$=".pdf"]');
-
- if (!navigator.onLine) {
- indicator.style.display = 'block';
- // Disable PDF buttons when offline
- pdfLinks.forEach(function(link) {
- link.dataset.originalHref = link.href;
- link.removeAttribute('href');
- link.style.opacity = '0.4';
- link.style.cursor = 'not-allowed';
- link.style.textDecoration = 'line-through';
- link.title = 'PDF unavailable offline';
- link.addEventListener('click', preventClick);
- });
- } else {
- indicator.style.display = 'none';
- // Re-enable PDF buttons when online
- pdfLinks.forEach(function(link) {
- if (link.dataset.originalHref) {
- link.href = link.dataset.originalHref;
- }
- link.style.opacity = '';
- link.style.cursor = '';
- link.style.textDecoration = '';
- link.title = '';
- link.removeEventListener('click', preventClick);
- });
- }
- }
-
- function preventClick(e) {
- e.preventDefault();
- e.stopPropagation();
- }
-
- window.addEventListener('online', updateOnlineStatus);
- window.addEventListener('offline', updateOnlineStatus);
- // Run after DOM is ready to catch all PDF links
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', updateOnlineStatus);
- } else {
- updateOnlineStatus();
- }
- })();