fix(offline): surface degraded/missing preloads, join re-entrant runs, cache manifest to IDB

- loadManifest() goes through DataCache.fetchWithFallback: the file list is
  written to IDB and survives SW-cache eviction; a run with the reduced
  fallback set is flagged and shown as degraded (new i18n en/ru/cu).
- 404 answers are counted as 'missing' (deployment drift) with a
  preload-missing diag entry, instead of silently reporting Done.
- preload() is re-entrancy safe: concurrent calls join the in-flight run.
- versions.json is loaded once per session (was fetched per translation).
- Write-through failures in fetchWithFallback()/fetch() log idb-write-fail.
- preload-manifest.json SW route is NetworkFirst (fresh coverage online).
- Manifest now also covers the shared/lives + shared/commands fallbacks.
This commit is contained in:
catarrh committed 2026-10-04 17:14:33 +03:00
1 parent 1201e16d5e
commit 8c442bf7b6
7 files changed
+184 -55

No files matched your search

+18 -21
View File
@@ -3,11 +3,26 @@
*/
import { getTranslations, type LanguageCode } from '../core/i18n';
import { OfflineManager } from '../core/offline-manager';
import { OfflineManager, type PreloadProgress } from '../core/offline-manager';
import { DataCache } from '../core/data-cache';
import { diagTail } from '../core/diag-log';
import { isPwaInstalled } from '../core/pwa-install';
/** Human message for a finished preload run (shared by manual + auto preload). */
function preloadResultText(
prog: PreloadProgress,
s: { offlineAborted: string; offlineDone: string; offlineFailed: string; offlineDegraded: string; offlineMissing: string },
): string {
if (prog.aborted) return s.offlineAborted;
if (prog.manifestMissing) return s.offlineDegraded;
if (prog.failed > 0) {
const failList = prog.failedFiles?.length ? `: ${prog.failedFiles.join(', ')}` : '';
return `${s.offlineDone} ${prog.failed} ${s.offlineFailed}${failList}`;
}
if ((prog.missing ?? 0) > 0) return s.offlineMissing.replace('{0}', String(prog.missing));
return s.offlineDone;
}
const LANGUAGES = [
{ code: 'en', name: 'English', local: 'English' },
{ code: 'ru', name: 'Russian', local: 'Русский' },
@@ -563,16 +578,7 @@ export class SettingsView {
bibleTranslations: bibleTrans,
}).then((prog) => {
if (progressBar) progressBar.style.width = '100%';
if (progressText) {
if (prog.aborted) {
progressText.textContent = t.settings.offlineAborted;
} else if (prog.failed > 0) {
const failList = prog.failedFiles?.length ? `: ${prog.failedFiles.join(', ')}` : '';
progressText.textContent = `${t.settings.offlineDone} ${prog.failed} ${t.settings.offlineFailed}${failList}`;
} else {
progressText.textContent = t.settings.offlineDone;
}
}
if (progressText) progressText.textContent = preloadResultText(prog, t.settings);
const diagEl = document.getElementById('diag-log');
if (diagEl) diagEl.textContent = diagTail();
OfflineManager.getStats().then(stats => {
@@ -696,16 +702,7 @@ export class SettingsView {
bibleTranslations: 'all',
}).then((prog) => {
if (progressBar) progressBar.style.width = '100%';
if (progressText) {
if (prog.aborted) {
progressText.textContent = t.settings.offlineAborted;
} else if (prog.failed > 0) {
const failList = prog.failedFiles?.length ? `: ${prog.failedFiles.join(', ')}` : '';
progressText.textContent = `${t.settings.offlineDone} ${prog.failed} ${t.settings.offlineFailed}${failList}`;
} else {
progressText.textContent = t.settings.offlineDone;
}
}
if (progressText) progressText.textContent = preloadResultText(prog, t.settings);
const diagEl = document.getElementById('diag-log');
if (diagEl) diagEl.textContent = diagTail();
OfflineManager.getStats().then(stats => {