Add offline debug page with service worker status and cached pages list

- Shows service worker registration status
- Lists all cache storage names
- Checks if Bible data (31,102 verses) is cached
- Displays all cached pages grouped by category (clickable links)
- Shows online/offline connection badge
- Includes Bible reader for reading any chapter from cached JSON

Visit /offline to see what's available without internet.

🤖 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:18:06 -05:00
parent e21ecfdf44
commit 214d4c565b
+274 -91
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Offline Bible Reader - KJV Study</title>
<title>Offline Mode - KJV Study</title>
<link rel="stylesheet" href="/static/tufte.css"/>
<link rel="manifest" href="/static/manifest.json"/>
<style>
@@ -13,6 +13,8 @@
--text-secondary: #666;
--border-color: #ddd;
--link-color: #333;
--success-color: #4a7c59;
--error-color: #c41e3a;
}
[data-theme="dark"] {
--bg-color: #1a1a1a;
@@ -24,26 +26,73 @@
body {
background: var(--bg-color);
color: var(--text-color);
max-width: 800px;
max-width: 900px;
margin: 0 auto;
padding: 2rem;
}
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
h2 { font-size: 1.4rem; margin-top: 2rem; border-bottom: 1px solid var(--border-color); padding-bottom: 0.5rem; }
h3 { font-size: 1.1rem; margin-top: 1.5rem; color: var(--text-secondary); }
.subtitle { color: var(--text-secondary); font-style: italic; margin-bottom: 2rem; }
.offline-badge {
.badge {
display: inline-block;
background: #c41e3a;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
margin-left: 0.5rem;
vertical-align: middle;
}
.badge-offline { background: var(--error-color); color: white; }
.badge-online { background: var(--success-color); color: white; }
.debug-section {
background: var(--border-color);
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
font-family: monospace;
font-size: 0.85rem;
}
.debug-section dt { font-weight: bold; margin-top: 0.5rem; }
.debug-section dd { margin-left: 1rem; color: var(--text-secondary); }
.status-ok { color: var(--success-color); }
.status-error { color: var(--error-color); }
.cached-links {
column-count: 3;
column-gap: 2rem;
margin: 1rem 0;
}
@media (max-width: 768px) {
.cached-links { column-count: 2; }
}
@media (max-width: 480px) {
.cached-links { column-count: 1; }
}
.cached-links a {
display: block;
padding: 0.25rem 0;
color: var(--link-color);
text-decoration: none;
font-size: 0.9rem;
}
.cached-links a:hover {
text-decoration: underline;
}
.category {
break-inside: avoid;
margin-bottom: 1rem;
}
.category-title {
font-weight: bold;
font-size: 0.85rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.25rem;
}
.nav-section {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
margin: 2rem 0;
flex-wrap: wrap;
align-items: center;
}
@@ -74,6 +123,10 @@
#chapter-content {
line-height: 2;
font-size: 1.2rem;
display: none;
}
#chapter-content.active {
display: block;
}
#chapter-content p {
margin: 1rem 0;
@@ -89,13 +142,6 @@
padding: 2rem;
color: var(--text-secondary);
}
.error {
background: #fee;
border: 1px solid #c00;
padding: 1rem;
border-radius: 4px;
color: #c00;
}
#keyboard-hint {
font-size: 0.85rem;
color: var(--text-secondary);
@@ -110,12 +156,39 @@
.back-link a {
color: var(--link-color);
}
#cached-count {
font-size: 1.5rem;
font-weight: bold;
color: var(--success-color);
}
</style>
</head>
<body>
<h1>Bible Reader <span class="offline-badge">Offline</span></h1>
<p class="subtitle">King James Version - Available without internet</p>
<h1>Offline Mode <span class="badge" id="connection-badge">checking...</span></h1>
<p class="subtitle">KJV Study - Available without internet</p>
<h2>Service Worker Status</h2>
<div class="debug-section">
<dl>
<dt>Service Worker</dt>
<dd id="sw-status">Checking...</dd>
<dt>Cache Storage</dt>
<dd id="cache-status">Checking...</dd>
<dt>Bible Data (verses-1769.json)</dt>
<dd id="bible-status">Checking...</dd>
<dt>Pages Cached</dt>
<dd id="pages-cached">Checking...</dd>
</dl>
</div>
<h2>Cached Pages <span id="cached-count">0</span></h2>
<p>These pages are available offline:</p>
<div id="cached-links" class="cached-links">
<p class="loading">Scanning cache...</p>
</div>
<h2>Bible Reader</h2>
<p>Read any chapter using cached Bible data:</p>
<div class="nav-section">
<select id="book-select">
<option value="">Select a book...</option>
@@ -130,13 +203,13 @@
</div>
<div id="chapter-content">
<p class="loading">Loading Bible data...</p>
<p class="loading">Select a book and chapter above...</p>
</div>
<p id="keyboard-hint">Tip: Use ← → arrow keys to navigate chapters</p>
<div class="back-link">
<a href="/">← Back to KJV Study</a> (when online)
<a href="/">← Back to KJV Study</a>
</div>
<script>
@@ -145,13 +218,178 @@
const theme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', theme);
// Bible data
// Debug elements
const connectionBadge = document.getElementById('connection-badge');
const swStatus = document.getElementById('sw-status');
const cacheStatus = document.getElementById('cache-status');
const bibleStatus = document.getElementById('bible-status');
const pagesCached = document.getElementById('pages-cached');
const cachedLinks = document.getElementById('cached-links');
const cachedCount = document.getElementById('cached-count');
// Update connection status
function updateConnectionStatus() {
if (navigator.onLine) {
connectionBadge.textContent = 'Online';
connectionBadge.className = 'badge badge-online';
} else {
connectionBadge.textContent = 'Offline';
connectionBadge.className = 'badge badge-offline';
}
}
updateConnectionStatus();
window.addEventListener('online', updateConnectionStatus);
window.addEventListener('offline', updateConnectionStatus);
// Check service worker status
async function checkServiceWorker() {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
const state = registration.active ? 'Active' :
registration.waiting ? 'Waiting' :
registration.installing ? 'Installing' : 'Unknown';
swStatus.innerHTML = '<span class="status-ok">✓ Registered (' + state + ')</span>';
swStatus.innerHTML += '<br>Scope: ' + registration.scope;
} else {
swStatus.innerHTML = '<span class="status-error">✗ Not registered</span>';
swStatus.innerHTML += '<br>Try refreshing the page or check if content blockers are blocking it.';
}
} catch (err) {
swStatus.innerHTML = '<span class="status-error">✗ Error: ' + err.message + '</span>';
}
} else {
swStatus.innerHTML = '<span class="status-error">✗ Not supported in this browser</span>';
}
}
// Check cache status and list cached pages
async function checkCaches() {
if ('caches' in window) {
try {
const cacheNames = await caches.keys();
if (cacheNames.length > 0) {
cacheStatus.innerHTML = '<span class="status-ok">✓ Available</span>';
cacheStatus.innerHTML += '<br>Caches: ' + cacheNames.join(', ');
} else {
cacheStatus.innerHTML = '<span class="status-error">✗ No caches found</span>';
}
// List all cached URLs
const allUrls = [];
for (const cacheName of cacheNames) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
keys.forEach(req => {
const url = new URL(req.url);
if (url.pathname && !url.pathname.startsWith('/static/')) {
allUrls.push(url.pathname);
}
});
}
// Remove duplicates and sort
const uniqueUrls = [...new Set(allUrls)].sort();
pagesCached.innerHTML = '<span class="status-ok">' + uniqueUrls.length + ' pages</span>';
cachedCount.textContent = uniqueUrls.length;
// Group by category
const categories = {
'Main': [],
'Books': [],
'Topics': [],
'Stories': [],
'Study Guides': [],
'Reading Plans': [],
'Parables': [],
'Resources': [],
'Other': []
};
uniqueUrls.forEach(url => {
if (url === '/' || url === '/books' || url === '/offline' || url === '/search') {
categories['Main'].push(url);
} else if (url.startsWith('/book/')) {
categories['Books'].push(url);
} else if (url.startsWith('/topics/')) {
categories['Topics'].push(url);
} else if (url.startsWith('/stories/')) {
categories['Stories'].push(url);
} else if (url.startsWith('/study-guides/')) {
categories['Study Guides'].push(url);
} else if (url.startsWith('/reading-plans/')) {
categories['Reading Plans'].push(url);
} else if (url.startsWith('/parables/')) {
categories['Parables'].push(url);
} else if (url.match(/^\/(christology|names-of-god|biblical-|fruits-|messianic-|miracles-|prayers-|spirits-|types-|women-|ten-|the-twelve|trinity|worship|grace|eschatology|ecclesiology|soteriology|pneumatology|hamartiology|anthropology|bibliology|theology-proper|justification|sanctification|providence|kingdom-of-god|law-and-gospel|armor-of-god|beatitudes|i-am-statements|names-of-christ|personifications|tetragrammaton|blood-in-scripture)/)) {
categories['Resources'].push(url);
} else {
categories['Other'].push(url);
}
});
// Render links
let html = '';
for (const [category, urls] of Object.entries(categories)) {
if (urls.length > 0) {
html += '<div class="category">';
html += '<div class="category-title">' + category + ' (' + urls.length + ')</div>';
urls.forEach(url => {
const label = url === '/' ? 'Home' : url.replace(/^\//, '').replace(/-/g, ' ');
html += '<a href="' + url + '">' + label + '</a>';
});
html += '</div>';
}
}
cachedLinks.innerHTML = html || '<p>No pages cached yet. Browse the site while online to cache pages.</p>';
} catch (err) {
cacheStatus.innerHTML = '<span class="status-error">✗ Error: ' + err.message + '</span>';
cachedLinks.innerHTML = '<p class="status-error">Could not read cache.</p>';
}
} else {
cacheStatus.innerHTML = '<span class="status-error">✗ Not supported</span>';
}
}
// Check Bible data
async function checkBibleData() {
try {
// Try cache first
let found = false;
const cacheNames = await caches.keys();
for (const cacheName of cacheNames) {
const cache = await caches.open(cacheName);
const response = await cache.match('/static/verses-1769.json');
if (response) {
const data = await response.json();
const verseCount = Object.keys(data).length;
bibleStatus.innerHTML = '<span class="status-ok">✓ Cached (' + verseCount.toLocaleString() + ' verses)</span>';
found = true;
break;
}
}
if (!found) {
bibleStatus.innerHTML = '<span class="status-error">✗ Not cached</span>';
bibleStatus.innerHTML += '<br>Visit the site while online to cache Bible data.';
}
} catch (err) {
bibleStatus.innerHTML = '<span class="status-error">✗ Error: ' + err.message + '</span>';
}
}
// Run all checks
checkServiceWorker();
checkCaches();
checkBibleData();
// Bible Reader functionality
let bibleData = null;
let bookStructure = {};
let currentBook = '';
let currentChapter = 1;
// Book order
const BOOKS = [
'Genesis', 'Exodus', 'Leviticus', 'Numbers', 'Deuteronomy',
'Joshua', 'Judges', 'Ruth', '1 Samuel', '2 Samuel',
@@ -176,36 +414,30 @@
const nextBtn = document.getElementById('next-btn');
const content = document.getElementById('chapter-content');
// Load Bible data from cache
async function loadBibleData() {
try {
// Try to fetch from cache first
const cache = await caches.open('kjvstudy-static-v1');
let response = await cache.match('/static/verses-1769.json');
let response = null;
const cacheNames = await caches.keys();
for (const cacheName of cacheNames) {
const cache = await caches.open(cacheName);
response = await cache.match('/static/verses-1769.json');
if (response) break;
}
if (!response) {
// Try network as fallback
response = await fetch('/static/verses-1769.json');
}
if (!response || !response.ok) {
throw new Error('Could not load Bible data');
}
bibleData = await response.json();
buildBookStructure();
populateBookSelect();
content.innerHTML = '<p>Select a book and chapter to begin reading.</p>';
// Check URL for book/chapter
parseUrlAndNavigate();
} catch (err) {
content.innerHTML = '<div class="error">Could not load Bible data. Please visit the site while online first to cache the data.</div>';
console.error('Failed to load Bible:', err);
}
}
// Build structure: { book: { chapter: [verse numbers] } }
function buildBookStructure() {
bookStructure = {};
for (const ref in bibleData) {
@@ -217,7 +449,6 @@
bookStructure[book][chapter].push(parseInt(verse));
}
}
// Sort verses
for (const book in bookStructure) {
for (const chapter in bookStructure[book]) {
bookStructure[book][chapter].sort((a, b) => a - b);
@@ -225,7 +456,6 @@
}
}
// Populate book dropdown
function populateBookSelect() {
BOOKS.forEach(book => {
if (bookStructure[book]) {
@@ -237,11 +467,9 @@
});
}
// Populate chapter dropdown
function populateChapterSelect(book) {
chapterSelect.innerHTML = '<option value="">Chapter</option>';
if (!bookStructure[book]) return;
const chapters = Object.keys(bookStructure[book]).map(Number).sort((a, b) => a - b);
chapters.forEach(ch => {
const opt = document.createElement('option');
@@ -252,115 +480,76 @@
chapterSelect.disabled = false;
}
// Render chapter
function renderChapter(book, chapter) {
if (!bookStructure[book] || !bookStructure[book][chapter]) {
content.innerHTML = '<p>Chapter not found.</p>';
return;
}
currentBook = book;
currentChapter = parseInt(chapter);
content.classList.add('active');
const verses = bookStructure[book][chapter];
let html = '<h2>' + book + ' ' + chapter + '</h2>';
let html = '<h3>' + book + ' ' + chapter + '</h3>';
verses.forEach(verseNum => {
const ref = book + ' ' + chapter + ':' + verseNum;
const text = bibleData[ref] || '';
// Remove paragraph markers (#)
const cleanText = text.replace(/^#\s*/, '');
html += '<p><span class="verse-num">' + verseNum + '</span> ' + cleanText + '</p>';
});
content.innerHTML = html;
// Update navigation
updateNavButtons();
// Update URL
history.replaceState(null, '', '/offline?book=' + encodeURIComponent(book) + '&chapter=' + chapter);
// Scroll to top
window.scrollTo(0, 0);
}
// Update prev/next buttons
function updateNavButtons() {
const chapters = Object.keys(bookStructure[currentBook] || {}).map(Number).sort((a, b) => a - b);
const bookIndex = BOOKS.indexOf(currentBook);
// Previous
if (currentChapter > chapters[0]) {
prevBtn.disabled = false;
} else if (bookIndex > 0) {
prevBtn.disabled = false;
} else {
prevBtn.disabled = true;
}
// Next
if (currentChapter < chapters[chapters.length - 1]) {
nextBtn.disabled = false;
} else if (bookIndex < BOOKS.length - 1) {
nextBtn.disabled = false;
} else {
nextBtn.disabled = true;
}
prevBtn.disabled = !(currentChapter > chapters[0] || bookIndex > 0);
nextBtn.disabled = !(currentChapter < chapters[chapters.length - 1] || bookIndex < BOOKS.length - 1);
}
// Navigate to previous chapter
function goPrev() {
const chapters = Object.keys(bookStructure[currentBook] || {}).map(Number).sort((a, b) => a - b);
const chapterIndex = chapters.indexOf(currentChapter);
if (chapterIndex > 0) {
chapterSelect.value = chapters[chapterIndex - 1];
renderChapter(currentBook, chapters[chapterIndex - 1]);
} else {
// Go to previous book's last chapter
const bookIndex = BOOKS.indexOf(currentBook);
if (bookIndex > 0) {
const prevBook = BOOKS[bookIndex - 1];
const prevChapters = Object.keys(bookStructure[prevBook] || {}).map(Number).sort((a, b) => a - b);
const lastChapter = prevChapters[prevChapters.length - 1];
bookSelect.value = prevBook;
populateChapterSelect(prevBook);
chapterSelect.value = lastChapter;
renderChapter(prevBook, lastChapter);
chapterSelect.value = prevChapters[prevChapters.length - 1];
renderChapter(prevBook, prevChapters[prevChapters.length - 1]);
}
}
}
// Navigate to next chapter
function goNext() {
const chapters = Object.keys(bookStructure[currentBook] || {}).map(Number).sort((a, b) => a - b);
const chapterIndex = chapters.indexOf(currentChapter);
if (chapterIndex < chapters.length - 1) {
chapterSelect.value = chapters[chapterIndex + 1];
renderChapter(currentBook, chapters[chapterIndex + 1]);
} else {
// Go to next book's first chapter
const bookIndex = BOOKS.indexOf(currentBook);
if (bookIndex < BOOKS.length - 1) {
const nextBook = BOOKS[bookIndex + 1];
const nextChapters = Object.keys(bookStructure[nextBook] || {}).map(Number).sort((a, b) => a - b);
const firstChapter = nextChapters[0];
bookSelect.value = nextBook;
populateChapterSelect(nextBook);
chapterSelect.value = firstChapter;
renderChapter(nextBook, firstChapter);
chapterSelect.value = nextChapters[0];
renderChapter(nextBook, nextChapters[0]);
}
}
}
// Parse URL parameters
function parseUrlAndNavigate() {
const params = new URLSearchParams(window.location.search);
const book = params.get('book');
const chapter = params.get('chapter');
if (book && bookStructure[book]) {
bookSelect.value = book;
populateChapterSelect(book);
@@ -371,12 +560,10 @@
}
}
// Event listeners
bookSelect.addEventListener('change', function() {
const book = this.value;
if (book) {
populateChapterSelect(book);
// Auto-select first chapter
const chapters = Object.keys(bookStructure[book]).map(Number).sort((a, b) => a - b);
if (chapters.length > 0) {
chapterSelect.value = chapters[0];
@@ -386,19 +573,16 @@
});
chapterSelect.addEventListener('change', function() {
const chapter = this.value;
if (chapter && currentBook) {
renderChapter(currentBook, chapter);
if (this.value && currentBook) {
renderChapter(currentBook, this.value);
}
});
prevBtn.addEventListener('click', goPrev);
nextBtn.addEventListener('click', goNext);
// Keyboard navigation
document.addEventListener('keydown', function(e) {
if (e.target.tagName === 'SELECT') return;
if (e.key === 'ArrowLeft' || e.key === 'h') {
e.preventDefault();
if (!prevBtn.disabled) goPrev();
@@ -408,7 +592,6 @@
}
});
// Load data
loadBibleData();
})();
</script>