Remove offline functionality and PWA features

Removed service worker, PWA manifest, offline page, and all related UI components to simplify the application and reduce bloat.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-30 08:51:54 -05:00
parent fb4e970567
commit 8537fb548f
5 changed files with 0 additions and 1910 deletions
-26
View File
@@ -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()
-58
View File
@@ -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"
]
}
-345
View File
@@ -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 <loc> tags
const locRegex = /<loc>([^<]+)<\/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 <sitemapindex>)
if (indexText.includes('<sitemapindex')) {
// Extract child sitemap URLs
const sitemapUrls = [];
const locRegex = /<loc>([^<]+)<\/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
});
}
}
});
-125
View File
@@ -1306,15 +1306,6 @@
</p>
</div>
<!-- Enable Offline -->
<div style="margin-top: 1rem;">
<p style="font-size: 0.85rem; color: var(--text-tertiary); text-align: center;">
<a href="/offline" style="color: var(--text-tertiary); text-decoration: none; border-bottom: 1px dotted var(--border-color-dark);">
Enable Offline
</a>
</p>
</div>
<!-- Flourish -->
<div class="sidebar-flourish"></div>
</nav>
@@ -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();
}
})();
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff