From a005e386a658a9d693a46781c3b8373999bdec10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Mon, 24 Aug 2026 09:50:07 +0300 Subject: [PATCH] feat: serve all precached data offline via DataCache fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DataCache.fetchWithFallback: network-first with IDB write-through under the same url: keys the preload writes; falls back to precached IDB copy when offline — closes the preload(IDB) vs read(SW cache) gap that made the Bible reader (and every other view) fail offline despite precaching - Convert all 33 raw /data fetches across bible/service/menaion/prayer/ calendar/settings views; in-memory caches and notFound maps preserved - fetchCommandText: switch to lowercase services/commands/ (already in preload manifest); tolerate both TEXT/text node casings per migrate-data - Remove stale duplicate static/data/en/services/Command/ (15 files) - fetchLives: fix misc fallback to real lives/misc/{N}.json bundles --- src/core/data-cache.ts | 24 +++ src/ui/bible-view.ts | 11 +- src/ui/calendar-view.ts | 35 +-- src/ui/menaion-view.ts | 12 +- src/ui/prayer-view.ts | 6 +- src/ui/service-view.ts | 201 ++++++------------ src/ui/settings-view.ts | 12 +- .../data/en/services/Command/AfterEach.json | 1 - static/data/en/services/Command/Bow.json | 1 - static/data/en/services/Command/Fourty.json | 1 - static/data/en/services/Command/LT1.json | 1 - .../data/en/services/Command/Prostration.json | 1 - static/data/en/services/Command/S1.json | 1 - static/data/en/services/Command/S2.json | 1 - static/data/en/services/Command/Tone1.json | 1 - static/data/en/services/Command/Tone2.json | 1 - static/data/en/services/Command/Tone3.json | 1 - static/data/en/services/Command/Tone4.json | 1 - static/data/en/services/Command/Tone5.json | 1 - static/data/en/services/Command/Tone6.json | 1 - static/data/en/services/Command/Tone7.json | 1 - static/data/en/services/Command/Tone8.json | 1 - 22 files changed, 118 insertions(+), 198 deletions(-) delete mode 100644 static/data/en/services/Command/AfterEach.json delete mode 100644 static/data/en/services/Command/Bow.json delete mode 100644 static/data/en/services/Command/Fourty.json delete mode 100644 static/data/en/services/Command/LT1.json delete mode 100644 static/data/en/services/Command/Prostration.json delete mode 100644 static/data/en/services/Command/S1.json delete mode 100644 static/data/en/services/Command/S2.json delete mode 100644 static/data/en/services/Command/Tone1.json delete mode 100644 static/data/en/services/Command/Tone2.json delete mode 100644 static/data/en/services/Command/Tone3.json delete mode 100644 static/data/en/services/Command/Tone4.json delete mode 100644 static/data/en/services/Command/Tone5.json delete mode 100644 static/data/en/services/Command/Tone6.json delete mode 100644 static/data/en/services/Command/Tone7.json delete mode 100644 static/data/en/services/Command/Tone8.json diff --git a/src/core/data-cache.ts b/src/core/data-cache.ts index 387f55e..e266628 100644 --- a/src/core/data-cache.ts +++ b/src/core/data-cache.ts @@ -204,6 +204,30 @@ export class DataCache { } } + /** + * Network-first fetch with IndexedDB fallback. + * + * Online: fetches, parses and writes through to IDB under the same + * `url:` keys the offline preload uses. Offline: serves the precached + * IDB copy, or null when the file was never preloaded. + */ + static async fetchWithFallback(url: string, type: 'json' | 'text' = 'json'): Promise { + try { + const resp = await fetch(url); + const ct = resp.headers.get('content-type'); + if (resp.ok && !(ct && ct.includes('text/html'))) { + const data: unknown = type === 'text' ? await resp.text() : await resp.json(); + await DataCache.set(`url:${url}`, data); + return data as T; + } + } catch { /* offline or invalid response — fall through to IDB */ } + try { + return ((await DataCache.get(`url:${url}`)) as T) ?? null; + } catch { + return null; + } + } + /** * Cache-first fetch: tries IndexedDB first, falls back to network. * If the data is fetched from network, it's cached for future use. diff --git a/src/ui/bible-view.ts b/src/ui/bible-view.ts index bb4de77..aa2e04f 100644 --- a/src/ui/bible-view.ts +++ b/src/ui/bible-view.ts @@ -9,6 +9,7 @@ import { formatPassageHTML, } from '../core/bible-reader'; import { getTranslations, type LanguageCode } from '../core/i18n'; +import { DataCache } from '../core/data-cache'; const SETTINGS_KEY = 'ponomar-settings'; const BIBLE_STATE_KEY = 'ponomar-bible-state'; @@ -138,8 +139,9 @@ export class BibleView { private async loadVersions() { try { - const resp = await fetch('/data/bible/versions.json'); - this.versions = await resp.json(); + const versions = await DataCache.fetchWithFallback('/data/bible/versions.json'); + if (!versions || !Array.isArray(versions) || versions.length === 0) throw new Error('unavailable'); + this.versions = versions; this.sortVersionsByLanguage(); } catch { this.versions = [ @@ -473,9 +475,8 @@ export class BibleView { if (!text) { // Version IDs are full paths like "en/bible/kjv" - const resp = await fetch(`/data/${this.currentVersion}/${this.currentBook}.text`); - if (!resp.ok) throw new Error('Not found'); - text = await resp.text(); + text = (await DataCache.fetchWithFallback(`/data/${this.currentVersion}/${this.currentBook}.text`, 'text')) ?? undefined; + if (!text) throw new Error('Not found'); this.loadedText.set(cacheKey, text); } diff --git a/src/ui/calendar-view.ts b/src/ui/calendar-view.ts index c443f10..61d7501 100644 --- a/src/ui/calendar-view.ts +++ b/src/ui/calendar-view.ts @@ -52,12 +52,10 @@ async function loadBookAbbrevs(lang: string): Promise { bookAbbrevLang = lang; for (let attempt = 0; attempt < 2; attempt++) { try { - const resp = await fetch('/data/bible/versions.json'); - if (!resp.ok) { if (attempt === 0) await new Promise(r => setTimeout(r, 300)); continue; } - const ct = resp.headers.get('content-type'); - if (ct && ct.includes('text/html')) { if (attempt === 0) await new Promise(r => setTimeout(r, 300)); continue; } - const versions = await resp.json() as any[]; - const ver = versions.find((v: any) => v.id.startsWith(lang + '/')) || versions[0]; + const versions = await DataCache.fetchWithFallback('/data/bible/versions.json'); + const ver = Array.isArray(versions) + ? (versions.find((v: any) => v.id.startsWith(lang + '/')) || versions[0]) + : null; if (ver) { const map: Record = {}; if (ver.books) { @@ -72,8 +70,9 @@ async function loadBookAbbrevs(lang: string): Promise { return; } } catch { - if (attempt === 0) await new Promise(r => setTimeout(r, 300)); + // fall through } + if (attempt === 0) await new Promise(r => setTimeout(r, 300)); } } @@ -295,11 +294,7 @@ async function loadLife(lang: LanguageCode, cid: string): Promise { try { - const resp = await fetch(`/data/shared/commemorations/${sharedId}.json`); - if (!resp.ok) return null; - const ct = resp.headers.get('content-type'); - if (ct && ct.includes('text/html')) return null; - return await resp.json(); + return await DataCache.fetchWithFallback(`/data/shared/commemorations/${sharedId}.json`); } catch { return null; } @@ -350,12 +345,8 @@ async function loadMenaion(lang: LanguageCode, month: number, day: number): Prom async function loadTriodionData(): Promise> { if (cachedTriodion) return cachedTriodion; try { - const resp = await fetch('/data/shared/calendar/triodion.json'); - if (!resp.ok) return {}; - const ct = resp.headers.get('content-type'); - if (ct && ct.includes('text/html')) return {}; - cachedTriodion = await resp.json(); - return cachedTriodion!; + cachedTriodion = (await DataCache.fetchWithFallback>('/data/shared/calendar/triodion.json')) ?? {}; + return cachedTriodion; } catch { return {}; } @@ -371,12 +362,8 @@ async function loadTriodionFile(index: number): Promise { async function loadPentecostarionData(): Promise> { if (cachedPentecostarion) return cachedPentecostarion; try { - const resp = await fetch('/data/shared/calendar/pentecostarion.json'); - if (!resp.ok) return {}; - const ct = resp.headers.get('content-type'); - if (ct && ct.includes('text/html')) return {}; - cachedPentecostarion = await resp.json(); - return cachedPentecostarion!; + cachedPentecostarion = (await DataCache.fetchWithFallback>('/data/shared/calendar/pentecostarion.json')) ?? {}; + return cachedPentecostarion; } catch { return {}; } diff --git a/src/ui/menaion-view.ts b/src/ui/menaion-view.ts index 6fd8cff..f1e97f3 100644 --- a/src/ui/menaion-view.ts +++ b/src/ui/menaion-view.ts @@ -7,6 +7,7 @@ import { assembleService, type ServiceContext } from '../core/service-assembler' import type { EvalContext, ServiceNode } from '../core/types'; import { getTranslations, type LanguageCode } from '../core/i18n'; import { loadSettings, fontClass } from './settings-view'; +import { DataCache } from '../core/data-cache'; export class MenaionView { private container: HTMLElement; @@ -45,9 +46,9 @@ export class MenaionView { async render() { try { - const resp = await fetch('/data/shared/menaion-daily/index.json'); - if (resp.ok) { - this.index = await resp.json() as Record; + const index = await DataCache.fetchWithFallback>('/data/shared/menaion-daily/index.json'); + if (index) { + this.index = index; } } catch { /* empty */ } @@ -164,9 +165,8 @@ export class MenaionView { let html = ''; for (let i = 1; i <= count; i++) { try { - const resp = await fetch(`/data/shared/menaion-daily/${dateKey}/${i}.json`); - if (!resp.ok) continue; - const nodes: ServiceNode[] = await resp.json(); + const nodes = await DataCache.fetchWithFallback(`/data/shared/menaion-daily/${dateKey}/${i}.json`); + if (!nodes) continue; const evalCtx: EvalContext = {}; const ctx: ServiceContext = { evalCtx, lang: 'cu', t: this.t, diff --git a/src/ui/prayer-view.ts b/src/ui/prayer-view.ts index 38276a9..54a69ed 100644 --- a/src/ui/prayer-view.ts +++ b/src/ui/prayer-view.ts @@ -8,6 +8,7 @@ import { assembleService, type ServiceContext } from '../core/service-assembler' import type { EvalContext, ServiceNode } from '../core/types'; import { getTranslations, type LanguageCode } from '../core/i18n'; import { loadSettings, fontClass } from './settings-view'; +import { DataCache } from '../core/data-cache'; export type PrayerCollection = 'prayer-rule' | 'akathists' | 'parimii' | 'horologion' | 'sbornik' | 'paraclete' | 'irmologion' | 'triodion'; @@ -270,12 +271,11 @@ export class PrayerView { } dataPath = `/data/shared/services/paraclete/tone${m[1]}/${m[2]}.json`; } - const resp = await fetch(dataPath); - if (!resp.ok) { + const nodes = await DataCache.fetchWithFallback(dataPath); + if (!nodes) { contentEl.innerHTML = `

${this.t.prayer.notFound}

`; return; } - const nodes: ServiceNode[] = await resp.json(); const evalCtx: EvalContext = {}; const ctx: ServiceContext = { diff --git a/src/ui/service-view.ts b/src/ui/service-view.ts index 8462c8b..bc3279e 100644 --- a/src/ui/service-view.ts +++ b/src/ui/service-view.ts @@ -15,6 +15,7 @@ import { HOLY_WEEK, holyWeekVarName, type ServiceTabDef, } from '../core/service-tabs'; import { loadSettings, fontClass } from './settings-view'; +import { DataCache } from '../core/data-cache'; interface ServiceDef { id: string; @@ -74,9 +75,8 @@ export class ServiceView { // Load the daily Menaion index (date -> section count) for the Служба дня tabs try { - const resp = await fetch('/data/shared/menaion-daily/index.json'); - if (resp.ok) { - const index = await resp.json() as Record; + const index = await DataCache.fetchWithFallback>('/data/shared/menaion-daily/index.json'); + if (index) { for (const [dk, n] of Object.entries(index)) { this.menaionDayCount.set(dk, n); } @@ -386,9 +386,8 @@ export class ServiceView { try { const tryFetch = async (lang: string) => { const url = `/data/${lang}/services/octoecheos/tone${toneNum}/${weekday.toLowerCase()}.json`; - const resp = await fetch(url); - if (!resp.ok) return null; - const nodes: ServiceNode[] = await resp.json(); + const nodes = await DataCache.fetchWithFallback(url); + if (!nodes) return null; for (const node of nodes) { if (node.type === serviceType) { if (!node.cmd || evalBool(node.cmd as string, this.evalContext)) { @@ -417,11 +416,10 @@ export class ServiceView { try { const tryLoad = async (url: string): Promise<{ cid: string }[] | null> => { try { - const resp = await fetch(url); - if (!resp.ok) return null; - const all = await resp.json(); + const all = await DataCache.fetchWithFallback[]>(url); + if (!all) return null; if (Array.isArray(all)) return all.map((e: Record) => ({ cid: String(e.id ?? '') })); - const entries = all[dateKey] as Record[] | undefined; + const entries = (all as Record)[dateKey] as Record[] | undefined; if (Array.isArray(entries)) return entries.map((e: Record) => ({ cid: String(e.id ?? '') })); return null; } catch { return null; } @@ -435,11 +433,10 @@ export class ServiceView { // Load from merged calendar files const loadCalendarEntry = async (file: string, index: number): Promise<{ cid: string }[] | null> => { try { - const resp = await fetch(`/data/shared/calendar/${file}.json`); - if (!resp.ok) return null; - const data = await resp.json(); + const data = await DataCache.fetchWithFallback>(`/data/shared/calendar/${file}.json`); + if (!data) return null; const key = String(index).padStart(2, '0'); - const entry = data[key]; + const entry = data[key] as { id?: unknown } | undefined; return entry ? [{ cid: String(entry.id ?? '') }] : null; } catch { return null; } }; @@ -457,9 +454,8 @@ export class ServiceView { for (const entry of allEntries) { if (!entry.cid) continue; try { - const shmResp = await fetch(`/data/shared/commemorations/${entry.cid}.json`); - if (!shmResp.ok) continue; - const shm = await shmResp.json(); + const shm = await DataCache.fetchWithFallback>(`/data/shared/commemorations/${entry.cid}.json`); + if (!shm) continue; const troparion = shm.TROPARION as { Tone?: string; text?: string } | undefined; const kontakion = shm.KONTAKION as { Tone?: string; text?: string } | undefined; if (troparion?.text || kontakion?.text) { @@ -507,9 +503,8 @@ export class ServiceView { const toneNum = tone === 8 ? 0 : tone; const url = `/data/shared/services/canons/tone${toneNum}/sunday.json`; try { - const resp = await fetch(url); - if (!resp.ok) return null; - const canonData: CanonData = await resp.json(); + const canonData = await DataCache.fetchWithFallback(url); + if (!canonData) return null; const ODE_SLAV = ['', 'а', 'в', 'г', 'д', 'є', 'ѕ', 'з', 'и', 'ѳ']; const nodes: ServiceNode[] = []; @@ -543,62 +538,27 @@ export class ServiceView { const dayKey = WEEKDAY[dow] ?? ''; if (!dayKey) return null; const url = `/data/shared/services/canons/paraclete/tone${tone}/${dayKey}.json`; - try { - const resp = await fetch(url); - if (!resp.ok) return null; - const nodes: ServiceNode[] = await resp.json(); - return nodes; - } catch { - return null; - } + return DataCache.fetchWithFallback(url); } private async loadMenaionDay(dateKey: string, index: number): Promise { const url = `/data/shared/menaion-daily/${dateKey}/${index}.json`; - try { - const resp = await fetch(url); - if (!resp.ok) return null; - const nodes: ServiceNode[] = await resp.json(); - return nodes; - } catch { - return null; - } + return DataCache.fetchWithFallback(url); } private async loadTriodion(index: number): Promise { const url = `/data/shared/triodion/${String(index).padStart(2, '0')}.json`; - try { - const resp = await fetch(url); - if (!resp.ok) return null; - const nodes: ServiceNode[] = await resp.json(); - return nodes; - } catch { - return null; - } + return DataCache.fetchWithFallback(url); } private async loadMariasStandingData(): Promise { const url = `/data/shared/services/marias-standing/full.json`; - try { - const resp = await fetch(url); - if (!resp.ok) return null; - const nodes: ServiceNode[] = await resp.json(); - return nodes; - } catch { - return null; - } + return DataCache.fetchWithFallback(url); } private async loadHolyWeekData(service: string): Promise { const url = `/data/shared/services/${service}/full.json`; - try { - const resp = await fetch(url); - if (!resp.ok) return null; - const nodes: ServiceNode[] = await resp.json(); - return nodes; - } catch { - return null; - } + return DataCache.fetchWithFallback(url); } private async loadGreatCanonData(): Promise { @@ -617,9 +577,8 @@ export class ServiceView { const url = `/data/shared/services/canons/great-canon/part${part}.json`; try { - const resp = await fetch(url); - if (!resp.ok) return null; - const canonData: GreatCanonPart = await resp.json(); + const canonData = await DataCache.fetchWithFallback(url); + if (!canonData) return null; const ODE_SLAV = ['', 'а҃', 'в҃', 'г҃', 'д҃', 'є҃', 'ѕ҃', 'з҃', 'и҃', 'ѳ҃']; const nodes: ServiceNode[] = []; @@ -872,12 +831,11 @@ export class ServiceView { const dd = String(this.currentDate.getDay()).padStart(2, '0'); const dateKey = `${mm}-${dd}`; try { - const resp = await fetch(`/data/${this.serviceLang}/menaion-bundle.json`); - if (resp.ok) { - const bundle = await resp.json(); + const bundle = await DataCache.fetchWithFallback>(`/data/${this.serviceLang}/menaion-bundle.json`); + if (bundle) { const entries = bundle[dateKey]; if (entries) { - for (const e of entries) { + for (const e of entries as Record[]) { if (e.id) ids.push(String(e.id)); } } @@ -885,9 +843,8 @@ export class ServiceView { } catch {} for (const cid of ids) { try { - const resp = await fetch(`/data/shared/commemorations/${cid}.json`); - if (!resp.ok) continue; - const data = await resp.json(); + const data = await DataCache.fetchWithFallback>(`/data/shared/commemorations/${cid}.json`); + if (!data) continue; const scripts = data.SCRIPTURE; if (!Array.isArray(scripts)) continue; let epistlePassage = ''; @@ -1000,11 +957,9 @@ export class ServiceView { const tryFetch = async (lang: string): Promise => { const url = `/data/${lang}/bible/${version}/${book}.text`; if (this.notFound.has(url)) return null; - const resp = await fetch(url); - if (!resp.ok) { this.notFound.add(url); return null; } - const ct = resp.headers.get('content-type'); - if (ct && ct.includes('text/html')) { this.notFound.add(url); return null; } - return resp.text(); + const text = await DataCache.fetchWithFallback(url, 'text'); + if (!text) { this.notFound.add(url); return null; } + return text; }; let text = await tryFetch(this.serviceLang); @@ -1103,15 +1058,12 @@ export class ServiceView { const cached = this.livesCache.get(cacheKey); if (cached) return cached; - const bundleName = /^(0[1-9]|1[0-2])\d+/.test(id) ? id.substring(0, 2) : 'misc'; + const bundleName = /^(0[1-9]|1[0-2])\d+/.test(id) ? id.substring(0, 2) : `misc/${id.charAt(0)}`; const tryFetch = async (lang: string): Promise>> | null> => { const url = `/data/${lang}/lives/${bundleName}.json`; if (this.notFound.has(url)) return null; - const resp = await fetch(url); - if (!resp.ok) { this.notFound.add(url); return null; } - const ct = resp.headers.get('content-type'); - if (ct && ct.includes('text/html')) { this.notFound.add(url); return null; } - const bundle = await resp.json() as Record>; + const bundle = await DataCache.fetchWithFallback>>(url); + if (!bundle) { this.notFound.add(url); return null; } const entry = bundle[id] as { services?: Record>> } | undefined; return entry?.services || null; }; @@ -1129,15 +1081,12 @@ export class ServiceView { if (cached) return cached; const tryFetch = async (lang: string): Promise => { - const url = `/data/${lang}/services/Command/${name}.json`; + const url = `/data/${lang}/services/commands/${name}.json`; if (this.notFound.has(url)) return null; - const resp = await fetch(url); - if (!resp.ok) { this.notFound.add(url); return null; } - const ct = resp.headers.get('content-type'); - if (ct && ct.includes('text/html')) { this.notFound.add(url); return null; } - const json = await resp.json() as Record[]; + const json = await DataCache.fetchWithFallback[]>(url); + if (!json) { this.notFound.add(url); return null; } return json - .filter(n => n.type === 'TEXT') + .filter(n => n.type === 'TEXT' || n.type === 'text') .map(n => String(n.value ?? '')) .filter(Boolean) .join(' ') || null; @@ -1157,11 +1106,8 @@ export class ServiceView { const tryFetch = async (lang: string): Promise<{ cmd: string; value: string }[] | null> => { const url = `/data/${lang}/commands/Times.json`; if (this.notFound.has(url)) return null; - const resp = await fetch(url); - if (!resp.ok) { this.notFound.add(url); return null; } - const ct = resp.headers.get('content-type'); - if (ct && ct.includes('text/html')) { this.notFound.add(url); return null; } - const json = await resp.json() as { DATA: { TIMES: { '@_Value': string; '@_Cmd': string }[] } }; + const json = await DataCache.fetchWithFallback<{ DATA?: { TIMES?: { '@_Value': string; '@_Cmd': string }[] } }>(url); + if (!json) { this.notFound.add(url); return null; } const arr = json?.DATA?.TIMES; if (!Array.isArray(arr)) return null; return arr.map(r => ({ value: r['@_Value'], cmd: r['@_Cmd'] })); @@ -1227,22 +1173,13 @@ export class ServiceView { if (this.notFound.has(url)) return null; const cached = this.jsonCache.get(url); if (cached) return cached; - try { - const resp = await fetch(url); - if (!resp.ok) { this.notFound.add(url); return null; } - const ct = resp.headers.get('content-type'); - if (ct && ct.includes('text/html')) { this.notFound.add(url); return null; } - const json = JSON.parse(await resp.text()); - if (Array.isArray(json)) { - this.jsonCache.set(url, json); - return json; - } - this.notFound.add(url); - return null; - } catch { - this.notFound.add(url); - return null; + const json = await DataCache.fetchWithFallback[]>(url); + if (Array.isArray(json)) { + this.jsonCache.set(url, json); + return json; } + this.notFound.add(url); + return null; }; const filterNodes = (json: Record[], header: boolean): string | null => { @@ -1254,7 +1191,7 @@ export class ServiceView { .join(' ') || null; } return json - .filter(n => n.type === 'TEXT') + .filter(n => n.type === 'TEXT' || n.type === 'text') .map(n => String(n.value ?? '').replace(/\n/g, ' ')) .filter(Boolean) .join('
') || null; @@ -1293,27 +1230,18 @@ export class ServiceView { return `/data/${lang}/services/${fetchPath}`; }; - const tryFetch = async (url: string): Promise => { - if (this.notFound.has(url)) return null; - const cached = this.nodeCache.get(url); - if (cached) return cached; - try { - const resp = await fetch(url); - if (!resp.ok) { this.notFound.add(url); return null; } - const ct = resp.headers.get('content-type') || ''; - if (ct.includes('text/html')) { this.notFound.add(url); return null; } - const nodes = await resp.json(); - if (Array.isArray(nodes)) { - this.nodeCache.set(url, nodes); - return nodes; - } - this.notFound.add(url); - return null; - } catch { - this.notFound.add(url); - return null; - } - }; + const tryFetch = async (url: string): Promise => { + if (this.notFound.has(url)) return null; + const cached = this.nodeCache.get(url); + if (cached) return cached; + const nodes = await DataCache.fetchWithFallback(url); + if (Array.isArray(nodes)) { + this.nodeCache.set(url, nodes); + return nodes; + } + this.notFound.add(url); + return null; + }; try { let url = resolvePath(this.serviceLang); @@ -1373,15 +1301,10 @@ export class ServiceView { if (this.notFound.has(url)) return null; const cached = this.nodeCache.get(url); if (cached) return cached; - try { - const resp = await fetch(url); - if (!resp.ok) { this.notFound.add(url); return null; } - const ct = resp.headers.get('content-type') || ''; - if (ct.includes('text/html')) { this.notFound.add(url); return null; } - const nodes = await resp.json(); - this.nodeCache.set(url, nodes); - return nodes; - } catch { this.notFound.add(url); return null; } + const nodes = await DataCache.fetchWithFallback(url); + if (!nodes) { this.notFound.add(url); return null; } + this.nodeCache.set(url, nodes); + return nodes; }; let url = resolvePath(this.serviceLang); if (url) { diff --git a/src/ui/settings-view.ts b/src/ui/settings-view.ts index 21ac3d1..8f47687 100644 --- a/src/ui/settings-view.ts +++ b/src/ui/settings-view.ts @@ -326,8 +326,8 @@ export class SettingsView { // Load default Bible versions dynamically (async () => { try { - const resp = await fetch('/data/bible/versions.json'); - const versions = await resp.json(); + const versions = await DataCache.fetchWithFallback('/data/bible/versions.json'); + if (!versions || !Array.isArray(versions)) return; const userLang = this.settings.language; // Sort by language priority @@ -479,8 +479,8 @@ export class SettingsView { // Load translations on first expand if (container.innerHTML === '') { try { - const resp = await fetch('/data/bible/versions.json'); - const versions = await resp.json(); + const versions = await DataCache.fetchWithFallback('/data/bible/versions.json'); + if (!versions || !Array.isArray(versions)) throw new Error('unavailable'); const userLang = this.settings.language; versions.sort((a: any, b: any) => { const pa = versionPriority(a.language, userLang); @@ -637,8 +637,8 @@ export class SettingsView { // Count cached Bible translations (async () => { try { - const resp = await fetch('/data/bible/versions.json'); - const versions = await resp.json(); + const versions = await DataCache.fetchWithFallback('/data/bible/versions.json'); + if (!versions || !Array.isArray(versions)) return; const total = versions.length; let cached = 0; for (const v of versions) { diff --git a/static/data/en/services/Command/AfterEach.json b/static/data/en/services/Command/AfterEach.json deleted file mode 100644 index bf95766..0000000 --- a/static/data/en/services/Command/AfterEach.json +++ /dev/null @@ -1 +0,0 @@ -[{"type":"TEXT","value":", after each"}] \ No newline at end of file diff --git a/static/data/en/services/Command/Bow.json b/static/data/en/services/Command/Bow.json deleted file mode 100644 index 9f68f29..0000000 --- a/static/data/en/services/Command/Bow.json +++ /dev/null @@ -1 +0,0 @@ -[{"type":"TEXT","value":"Bow"}] \ No newline at end of file diff --git a/static/data/en/services/Command/Fourty.json b/static/data/en/services/Command/Fourty.json deleted file mode 100644 index 78dd28f..0000000 --- a/static/data/en/services/Command/Fourty.json +++ /dev/null @@ -1 +0,0 @@ -[{"type":"TEXT","value":"(Forty times)"}] \ No newline at end of file diff --git a/static/data/en/services/Command/LT1.json b/static/data/en/services/Command/LT1.json deleted file mode 100644 index 0bf4c73..0000000 --- a/static/data/en/services/Command/LT1.json +++ /dev/null @@ -1 +0,0 @@ -[{"type":"TEXT","value":"(Sticheron Melody: Tone 6)"}] \ No newline at end of file diff --git a/static/data/en/services/Command/Prostration.json b/static/data/en/services/Command/Prostration.json deleted file mode 100644 index 5d9d582..0000000 --- a/static/data/en/services/Command/Prostration.json +++ /dev/null @@ -1 +0,0 @@ -[{"type":"TEXT","value":"Prostration"}] \ No newline at end of file diff --git a/static/data/en/services/Command/S1.json b/static/data/en/services/Command/S1.json deleted file mode 100644 index dbda819..0000000 --- a/static/data/en/services/Command/S1.json +++ /dev/null @@ -1 +0,0 @@ -[{"type":"TEXT","value":"(Stichos 1)"}] \ No newline at end of file diff --git a/static/data/en/services/Command/S2.json b/static/data/en/services/Command/S2.json deleted file mode 100644 index cea0478..0000000 --- a/static/data/en/services/Command/S2.json +++ /dev/null @@ -1 +0,0 @@ -[{"type":"TEXT","value":"(Stichos 2)"}] \ No newline at end of file diff --git a/static/data/en/services/Command/Tone1.json b/static/data/en/services/Command/Tone1.json deleted file mode 100644 index 3d5eaa2..0000000 --- a/static/data/en/services/Command/Tone1.json +++ /dev/null @@ -1 +0,0 @@ -[{"type":"TEXT","value":"(Tone 1)"}] \ No newline at end of file diff --git a/static/data/en/services/Command/Tone2.json b/static/data/en/services/Command/Tone2.json deleted file mode 100644 index 98400ed..0000000 --- a/static/data/en/services/Command/Tone2.json +++ /dev/null @@ -1 +0,0 @@ -[{"type":"TEXT","value":"(Tone 2)"}] \ No newline at end of file diff --git a/static/data/en/services/Command/Tone3.json b/static/data/en/services/Command/Tone3.json deleted file mode 100644 index 4cf2207..0000000 --- a/static/data/en/services/Command/Tone3.json +++ /dev/null @@ -1 +0,0 @@ -[{"type":"TEXT","value":"(Tone 3)"}] \ No newline at end of file diff --git a/static/data/en/services/Command/Tone4.json b/static/data/en/services/Command/Tone4.json deleted file mode 100644 index ca9d6c0..0000000 --- a/static/data/en/services/Command/Tone4.json +++ /dev/null @@ -1 +0,0 @@ -[{"type":"TEXT","value":"(Tone 4)"}] \ No newline at end of file diff --git a/static/data/en/services/Command/Tone5.json b/static/data/en/services/Command/Tone5.json deleted file mode 100644 index f9231f0..0000000 --- a/static/data/en/services/Command/Tone5.json +++ /dev/null @@ -1 +0,0 @@ -[{"type":"TEXT","value":"(Tone 5)"}] \ No newline at end of file diff --git a/static/data/en/services/Command/Tone6.json b/static/data/en/services/Command/Tone6.json deleted file mode 100644 index 76473e6..0000000 --- a/static/data/en/services/Command/Tone6.json +++ /dev/null @@ -1 +0,0 @@ -[{"type":"TEXT","value":"(Tone 6)"}] \ No newline at end of file diff --git a/static/data/en/services/Command/Tone7.json b/static/data/en/services/Command/Tone7.json deleted file mode 100644 index d46899b..0000000 --- a/static/data/en/services/Command/Tone7.json +++ /dev/null @@ -1 +0,0 @@ -[{"type":"TEXT","value":"(Tone 7)"}] \ No newline at end of file diff --git a/static/data/en/services/Command/Tone8.json b/static/data/en/services/Command/Tone8.json deleted file mode 100644 index 9014526..0000000 --- a/static/data/en/services/Command/Tone8.json +++ /dev/null @@ -1 +0,0 @@ -[{"type":"TEXT","value":"(Tone 8)"}] \ No newline at end of file