On-demand offline caching with all stories

Changes:
- Caching now only starts when user clicks "Download for Offline Use" on /offline page
- Added all 186 Bible stories (including kids mode) to cache list
- Added progress bar and status indicators on /offline page
- Bumped cache version to v4
- Total cacheable pages: ~1,500 (1189 chapters + 186 stories + 66 books + resources)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-30 01:27:24 -05:00
parent 357243c396
commit c6ae359e16
2 changed files with 341 additions and 14 deletions
+214 -14
View File
@@ -1,5 +1,5 @@
// KJV Study Service Worker - Offline Bible Access
const CACHE_VERSION = 'v3';
const CACHE_VERSION = 'v4';
const STATIC_CACHE = 'kjvstudy-static-' + CACHE_VERSION;
const BIBLE_CACHE = 'kjvstudy-bible-' + CACHE_VERSION;
const PAGE_CACHE = 'kjvstudy-pages-' + CACHE_VERSION;
@@ -9,8 +9,6 @@ const STATIC_ASSETS = [
'/',
'/offline',
'/static/tufte.css',
'/static/style.css',
'/static/app.js',
'/static/manifest.json',
'/static/verses-1769.json',
'/books'
@@ -1209,6 +1207,196 @@ const CHAPTER_PAGES = [
"/book/Revelation/chapter/22",
];
// All Bible story pages (186 stories - includes kids mode)
const STORY_PAGES = [
"/stories/god-creates-the-world",
"/stories/adam-and-eve-in-the-garden",
"/stories/the-fall-of-man",
"/stories/cain-and-abel",
"/stories/noah-and-the-ark",
"/stories/the-tower-of-babel",
"/stories/the-call-of-abraham",
"/stories/abraham-and-lot-separate",
"/stories/the-birth-of-isaac",
"/stories/abraham-offers-isaac",
"/stories/jacob-and-esau",
"/stories/jacobs-ladder",
"/stories/jacob-wrestles-with-god",
"/stories/josephs-coat-and-dreams",
"/stories/joseph-sold-into-slavery",
"/stories/joseph-reveals-himself",
"/stories/jobs-righteousness-and-prosperity",
"/stories/satans-challenge-and-jobs-first-test",
"/stories/jobs-physical-affliction",
"/stories/job-cries-out-and-friends-accuse",
"/stories/elihu-speaks-words-of-wisdom",
"/stories/god-speaks-from-the-whirlwind",
"/stories/jobs-restoration-and-blessing",
"/stories/baby-moses-in-the-basket",
"/stories/the-burning-bush",
"/stories/the-ten-plagues",
"/stories/the-passover",
"/stories/crossing-the-red-sea",
"/stories/the-ten-commandments",
"/stories/the-golden-calf",
"/stories/rahab-and-the-spies",
"/stories/the-walls-of-jericho",
"/stories/gideon-and-the-fleece",
"/stories/gideons-three-hundred",
"/stories/samson-the-strong-man",
"/stories/samson-and-delilah",
"/stories/samsons-final-victory",
"/stories/ruth-and-naomi",
"/stories/ruth-and-boaz",
"/stories/samsons-birth-announced",
"/stories/samsons-riddle-and-wedding",
"/stories/samsons-exploits",
"/stories/samson-and-delilah",
"/stories/samsons-final-victory",
"/stories/naomis-loss-and-return",
"/stories/ruth-meets-boaz",
"/stories/ruth-at-the-threshing-floor",
"/stories/boaz-redeems-ruth",
"/stories/hannahs-prayer-for-a-son",
"/stories/the-boy-samuel-hears-god",
"/stories/the-ark-is-captured",
"/stories/israel-demands-a-king",
"/stories/samuel-anoints-saul",
"/stories/sauls-disobedience",
"/stories/samuel-anoints-david",
"/stories/samuels-death",
"/stories/hannahs-prayer",
"/stories/god-calls-samuel",
"/stories/david-and-goliath",
"/stories/david-and-jonathan",
"/stories/solomons-wisdom",
"/stories/elijah-and-the-prophets-of-baal",
"/stories/elijah-and-the-still-small-voice",
"/stories/elijah-and-the-ravens",
"/stories/elijah-and-the-widows-oil",
"/stories/fire-from-heaven",
"/stories/elijah-taken-to-heaven",
"/stories/elisha-and-the-widows-oil",
"/stories/naaman-is-healed",
"/stories/jonah-and-the-great-fish",
"/stories/jonah-and-nineveh",
"/stories/the-fiery-furnace",
"/stories/daniel-in-the-lions-den",
"/stories/isaiahs-vision",
"/stories/ezekiel-valley-of-dry-bones",
"/stories/jonah-runs-from-god",
"/stories/jonah-prays-from-the-fish",
"/stories/nineveh-repents",
"/stories/jonahs-anger-and-gods-compassion",
"/stories/daniel-refuses-kings-food",
"/stories/nebuchadnezzars-dream",
"/stories/fiery-furnace",
"/stories/writing-on-the-wall",
"/stories/daniel-in-lions-den",
"/stories/daniels-visions",
"/stories/esther-becomes-queen",
"/stories/hamans-plot",
"/stories/for-such-a-time-as-this",
"/stories/esthers-brave-plan",
"/stories/the-jews-are-delivered",
"/stories/nehemiahs-burden",
"/stories/permission-to-rebuild",
"/stories/rebuilding-despite-opposition",
"/stories/justice-and-generosity",
"/stories/wall-completed",
"/stories/ezra-reads-the-law",
"/stories/renewal-and-covenant",
"/stories/angel-visits-mary",
"/stories/no-room-at-the-inn",
"/stories/shepherds-and-angels",
"/stories/the-wise-men",
"/stories/escape-to-egypt",
"/stories/jesus-baptized",
"/stories/jesus-tempted",
"/stories/water-into-wine",
"/stories/heals-paralyzed-man",
"/stories/calms-the-storm",
"/stories/feeds-five-thousand",
"/stories/walks-on-water",
"/stories/heals-blind-man",
"/stories/raises-lazarus",
"/stories/heals-ten-lepers",
"/stories/jesus-and-children",
"/stories/heals-on-sabbath",
"/stories/forgives-sinful-woman",
"/stories/heals-jairus-daughter",
"/stories/transfiguration",
"/stories/man-with-many-spirits",
"/stories/woman-at-the-well",
"/stories/jesus-and-zacchaeus",
"/stories/woman-touched-his-cloak",
"/stories/centurions-servant",
"/stories/mary-and-martha",
"/stories/raises-widows-son",
"/stories/jesus-calms-the-storm",
"/stories/jesus-feeds-five-thousand",
"/stories/jesus-walks-on-water",
"/stories/jesus-heals-the-blind-man",
"/stories/jesus-raises-lazarus",
"/stories/jesus-heals-ten-lepers",
"/stories/jesus-heals-the-paralyzed-man",
"/stories/the-good-samaritan",
"/stories/the-prodigal-son",
"/stories/the-lost-sheep",
"/stories/the-sower",
"/stories/the-talents",
"/stories/the-pharisee-and-tax-collector",
"/stories/the-rich-fool",
"/stories/the-unmerciful-servant",
"/stories/the-lost-coin",
"/stories/the-ten-virgins",
"/stories/the-mustard-seed",
"/stories/the-wheat-and-weeds",
"/stories/hidden-treasure-and-pearl",
"/stories/rich-man-and-lazarus",
"/stories/wise-and-foolish-builders",
"/stories/workers-in-the-vineyard",
"/stories/the-great-banquet",
"/stories/the-persistent-widow",
"/stories/the-sheep-and-goats",
"/stories/triumphal-entry",
"/stories/the-last-supper",
"/stories/garden-of-gethsemane",
"/stories/peter-denies-jesus",
"/stories/the-crucifixion",
"/stories/the-resurrection",
"/stories/jesus-appears-to-thomas",
"/stories/road-to-emmaus",
"/stories/jesus-restores-peter",
"/stories/great-commission",
"/stories/the-ascension",
"/stories/day-of-pentecost",
"/stories/peter-john-heal-lame-man",
"/stories/conversion-of-saul",
"/stories/peters-vision",
"/stories/paul-silas-in-prison",
"/stories/pauls-shipwreck",
"/stories/from-persecutor-to-preacher",
"/stories/first-missionary-journey",
"/stories/prison-songs-at-midnight",
"/stories/teaching-in-athens",
"/stories/shipwreck-and-malta",
"/stories/johns-vision-of-christ",
"/stories/letters-to-seven-churches",
"/stories/throne-room-of-heaven",
"/stories/seven-seals",
"/stories/two-witnesses",
"/stories/woman-and-dragon",
"/stories/beast-and-mark",
"/stories/fall-of-babylon",
"/stories/return-of-christ",
"/stories/new-heaven-new-earth",
"/stories/jobs-faith-through-suffering",
"/stories/ruths-loyalty-and-love",
"/stories/esther-saves-her-people",
"/stories/shadrach-meshach-and-abednego",
];
// Resource and reference pages
const RESOURCE_PAGES = [
"/anthropology",
@@ -1250,6 +1438,7 @@ const RESOURCE_PAGES = [
"/soteriology",
"/spirits-and-demons",
"/stories",
"/stories/kids",
"/strongs",
"/strongs/greek",
"/strongs/hebrew",
@@ -1337,9 +1526,9 @@ const BOOK_PAGES = [
];
// Combine all pages for background caching
const PAGES_TO_CACHE = [...CHAPTER_PAGES, ...RESOURCE_PAGES, ...BOOK_PAGES];
const PAGES_TO_CACHE = [...CHAPTER_PAGES, ...STORY_PAGES, ...RESOURCE_PAGES, ...BOOK_PAGES];
// Install event - cache static assets
// Install event - cache static assets only (NOT pages)
self.addEventListener('install', (event) => {
console.log('[SW] Installing service worker...');
event.waitUntil(
@@ -1358,7 +1547,7 @@ self.addEventListener('install', (event) => {
);
});
// Activate event - clean up old caches and start background caching
// Activate event - clean up old caches (NO automatic page caching)
self.addEventListener('activate', (event) => {
console.log('[SW] Activating service worker...');
event.waitUntil(
@@ -1381,20 +1570,18 @@ self.addEventListener('activate', (event) => {
console.log('[SW] Service worker activated');
return self.clients.claim();
})
.then(() => {
// Start background pre-caching after activation
startBackgroundCaching();
})
);
});
// Background pre-caching - cache all pages gradually
// 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] Starting background pre-cache of', PAGES_TO_CACHE.length, 'pages...');
@@ -1409,7 +1596,14 @@ async function startBackgroundCaching() {
}
}
console.log('[SW] Need to cache', uncachedPages.length, 'pages');
totalToCache = uncachedPages.length;
console.log('[SW] Need to cache', totalToCache, 'pages');
if (totalToCache === 0) {
notifyClients({ type: 'CACHE_COMPLETE', total: PAGES_TO_CACHE.length });
cachingInProgress = false;
return;
}
// Cache pages in batches with delay to avoid overwhelming the server
const BATCH_SIZE = 10;
@@ -1426,11 +1620,11 @@ async function startBackgroundCaching() {
await cache.put(url, response);
cachedCount++;
// Notify clients of progress every 10 pages
if (cachedCount % 10 === 0) {
if (cachedCount % 10 === 0 || cachedCount === totalToCache) {
notifyClients({
type: 'CACHE_PROGRESS',
cached: cachedCount,
total: uncachedPages.length
total: totalToCache
});
}
}
@@ -1582,6 +1776,12 @@ self.addEventListener('message', (event) => {
self.skipWaiting();
}
// START_CACHING - triggered from /offline page
if (event.data && event.data.type === 'START_CACHING') {
console.log('[SW] Received START_CACHING request');
startBackgroundCaching();
}
// Pre-cache specific book/chapter on demand
if (event.data && event.data.type === 'CACHE_CHAPTER') {
const { book, chapter } = event.data;
+127
View File
@@ -161,12 +161,93 @@
font-weight: bold;
color: var(--success-color);
}
.cache-controls {
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
padding: 1.5rem;
border-radius: 8px;
margin: 1.5rem 0;
border: 1px solid #bae6fd;
}
[data-theme="dark"] .cache-controls {
background: linear-gradient(135deg, #1e3a5f 0%, #1e293b 100%);
border-color: #334155;
}
.cache-controls h3 {
margin: 0 0 0.5rem 0;
color: var(--text-color);
}
.cache-controls p {
margin: 0 0 1rem 0;
color: var(--text-secondary);
font-size: 0.95rem;
}
.cache-btn {
padding: 0.75rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border: none;
border-radius: 6px;
background: var(--success-color);
color: white;
cursor: pointer;
transition: all 0.2s;
}
.cache-btn:hover {
filter: brightness(1.1);
transform: translateY(-1px);
}
.cache-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.cache-progress {
margin-top: 1rem;
display: none;
}
.cache-progress.active {
display: block;
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--border-color);
border-radius: 4px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.progress-fill {
height: 100%;
background: var(--success-color);
width: 0%;
transition: width 0.3s ease;
}
.progress-text {
font-size: 0.85rem;
color: var(--text-secondary);
}
.cache-complete {
color: var(--success-color);
font-weight: 600;
}
</style>
</head>
<body>
<h1>Offline Mode <span class="badge" id="connection-badge">checking...</span></h1>
<p class="subtitle">KJV Study - Available without internet</p>
<div class="cache-controls">
<h3>Enable Offline Access</h3>
<p>Download the entire site for offline use: all 1,189 Bible chapters, 186 stories (including kids mode), study guides, and resources. Total: ~1,500 pages.</p>
<button class="cache-btn" id="start-cache-btn">Download for Offline Use</button>
<div class="cache-progress" id="cache-progress">
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<div class="progress-text" id="progress-text">Preparing...</div>
</div>
</div>
<h2>Service Worker Status</h2>
<div class="debug-section">
<dl>
@@ -384,6 +465,52 @@
checkCaches();
checkBibleData();
// Cache controls
const startCacheBtn = document.getElementById('start-cache-btn');
const cacheProgress = document.getElementById('cache-progress');
const progressFill = document.getElementById('progress-fill');
const progressText = document.getElementById('progress-text');
startCacheBtn.addEventListener('click', async function() {
if (!('serviceWorker' in navigator)) {
alert('Service workers are not supported in this browser.');
return;
}
const registration = await navigator.serviceWorker.getRegistration();
if (!registration || !registration.active) {
alert('Service worker is not active. Please refresh the page and try again.');
return;
}
// Disable button and show progress
startCacheBtn.disabled = true;
startCacheBtn.textContent = 'Downloading...';
cacheProgress.classList.add('active');
progressText.textContent = 'Starting download...';
// Send message to service worker to start caching
registration.active.postMessage({ type: 'START_CACHING' });
});
// Listen for progress messages from service worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', function(event) {
const data = event.data;
if (data.type === 'CACHE_PROGRESS') {
const pct = Math.round((data.cached / data.total) * 100);
progressFill.style.width = pct + '%';
progressText.textContent = 'Downloading: ' + data.cached + ' / ' + data.total + ' pages (' + pct + '%)';
} else if (data.type === 'CACHE_COMPLETE') {
progressFill.style.width = '100%';
progressText.innerHTML = '<span class="cache-complete">✓ Download complete! ' + data.total + ' pages available offline.</span>';
startCacheBtn.textContent = 'Downloaded!';
// Refresh the cache list
setTimeout(checkCaches, 500);
}
});
}
// Bible Reader functionality
let bibleData = null;
let bookStructure = {};