From 142b95d226b2e533ce25d499982b6d92c347043a 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: Sun, 23 Aug 2026 10:23:36 +0300 Subject: [PATCH] refactor: address code-review findings for install detection and diag log - pwa-install.ts: permanent 'ponomar-ever-installed' marker keeps install button hidden on Firefox/Safari after appinstalled clears the offer flag; isPwaInstalled moved to core for reuse - diag-log: self-heal corrupted storage, show MM-DD in tail - settings: refresh diag panel after clear-cache; uniform completion style - data-cache.has: plain count(key); document HTML-as-failure intent --- src/core/data-cache.ts | 2 +- src/core/diag-log.ts | 23 ++++++++++++++----- src/core/offline-manager.ts | 2 ++ src/core/pwa-install.ts | 46 +++++++++++++++++++++++++++++++++++++ src/ui/app.ts | 14 ++++++++--- src/ui/settings-view.ts | 36 +++++++++++------------------ 6 files changed, 90 insertions(+), 33 deletions(-) create mode 100644 src/core/pwa-install.ts diff --git a/src/core/data-cache.ts b/src/core/data-cache.ts index 9236fbd..387f55e 100644 --- a/src/core/data-cache.ts +++ b/src/core/data-cache.ts @@ -102,7 +102,7 @@ export class DataCache { const db = await getDB(); return await new Promise((resolve) => { const tx = db.transaction(STORE_NAME, 'readonly'); - const request = tx.objectStore(STORE_NAME).count(IDBKeyRange.only(key)); + const request = tx.objectStore(STORE_NAME).count(key); request.onsuccess = () => resolve(request.result > 0); request.onerror = () => resolve(null); }); diff --git a/src/core/diag-log.ts b/src/core/diag-log.ts index bbafde2..68b5a1f 100644 --- a/src/core/diag-log.ts +++ b/src/core/diag-log.ts @@ -15,10 +15,21 @@ export interface DiagEvent { [key: string]: unknown; } +function readEvents(): DiagEvent[] { + const raw = localStorage.getItem(DIAG_KEY); + if (!raw) return []; + try { + const arr = JSON.parse(raw); + if (Array.isArray(arr)) return arr as DiagEvent[]; + } catch { /* fall through */ } + // Corrupted or non-array value — reset so logging can recover + localStorage.removeItem(DIAG_KEY); + return []; +} + export function diagLog(event: string, detail?: Record) { try { - const raw = localStorage.getItem(DIAG_KEY); - const arr: DiagEvent[] = raw ? JSON.parse(raw) : []; + const arr = readEvents(); arr.push({ t: new Date().toISOString(), event, ...(detail ?? {}) }); while (arr.length > MAX_EVENTS) arr.shift(); localStorage.setItem(DIAG_KEY, JSON.stringify(arr)); @@ -27,16 +38,16 @@ export function diagLog(event: string, detail?: Record) { export function getDiagLog(): DiagEvent[] { try { - const raw = localStorage.getItem(DIAG_KEY); - const arr = raw ? JSON.parse(raw) : []; - return Array.isArray(arr) ? arr : []; + return readEvents(); } catch { return []; } } function formatEvent(e: DiagEvent): string { - const time = (e.t || '').slice(11, 19); + // MM-DD HH:MM:SS (local relevance comes from ISO UTC; day boundary matters + // when events span days) + const time = `${(e.t || '').slice(5, 10)} ${(e.t || '').slice(11, 19)}`; const parts: string[] = []; for (const [k, v] of Object.entries(e)) { if (k === 't' || k === 'event') continue; diff --git a/src/core/offline-manager.ts b/src/core/offline-manager.ts index f42a6b0..17421b0 100644 --- a/src/core/offline-manager.ts +++ b/src/core/offline-manager.ts @@ -188,6 +188,8 @@ export class OfflineManager { } if (!resp.ok) continue; const ct = resp.headers.get('content-type'); + // HTML body (SPA fallback / captive portal) is not our data — + // counts as a failure and feeds the circuit-breaker streak if (ct && ct.includes('text/html')) break; let data: unknown; diff --git a/src/core/pwa-install.ts b/src/core/pwa-install.ts new file mode 100644 index 0000000..d856676 --- /dev/null +++ b/src/core/pwa-install.ts @@ -0,0 +1,46 @@ +/** + * PWA installation state detection. + * + * 'ponomar-ever-installed' is a permanent marker (never removed): set when + * the app is installed, launched in standalone mode, or inherited from the + * legacy 'ponomar-installed' offer flag. Used to hide the Settings install + * button on browsers without getInstalledRelatedApps() (Firefox, Safari). + * Distinct from 'ponomar-installed', which only gates the first-launch + * offline-offer modal and is cleared on appinstalled for re-offer semantics. + */ + +const EVER_INSTALLED_KEY = 'ponomar-ever-installed'; + +export function isStandaloneDisplay(): boolean { + return ( + window.matchMedia('(display-mode: standalone)').matches || + (window.navigator as any).standalone === true + ); +} + +export function markEverInstalled(): void { + try { + localStorage.setItem(EVER_INSTALLED_KEY, '1'); + } catch { /* best-effort */ } +} + +export function hasEverInstalledMarker(): boolean { + try { + return localStorage.getItem(EVER_INSTALLED_KEY) === '1'; + } catch { + return false; + } +} + +export async function isPwaInstalled(): Promise { + if (isStandaloneDisplay()) return true; + if (hasEverInstalledMarker()) return true; + try { + const navAny = navigator as any; + if (typeof navAny.getInstalledRelatedApps === 'function') { + const apps = await navAny.getInstalledRelatedApps(); + if (Array.isArray(apps) && apps.length > 0) return true; + } + } catch { /* not supported */ } + return false; +} diff --git a/src/ui/app.ts b/src/ui/app.ts index 24484c9..5dafd8a 100644 --- a/src/ui/app.ts +++ b/src/ui/app.ts @@ -12,6 +12,7 @@ import { loadSettings, type Settings } from './settings-view'; import { getTranslations, type LanguageCode } from '../core/i18n'; import { DataCache } from '../core/data-cache'; import { diagLog } from '../core/diag-log'; +import { markEverInstalled, hasEverInstalledMarker } from '../core/pwa-install'; type View = 'calendar' | 'bible' | 'prayer' | 'akathists' | 'parimii' | 'horologion' | 'sbornik' | 'paraclete' | 'irmologion' | 'menaion' | 'triodion' | 'lives' | 'settings'; @@ -139,13 +140,20 @@ export class App { // Clear first-launch flag on re-install window.addEventListener('appinstalled', () => { localStorage.removeItem('ponomar-installed'); + markEverInstalled(); }); // Show offline offer on first standalone launch const isStandalone = () => window.matchMedia('(display-mode: standalone)').matches || (window.navigator as any).standalone; - if (isStandalone() && !localStorage.getItem('ponomar-installed')) { - localStorage.setItem('ponomar-installed', '1'); - this.showOfflineOfferModal(); + if (isStandalone()) { + markEverInstalled(); + if (!localStorage.getItem('ponomar-installed')) { + localStorage.setItem('ponomar-installed', '1'); + this.showOfflineOfferModal(); + } + } else if (localStorage.getItem('ponomar-installed') && !hasEverInstalledMarker()) { + // One-time migration: legacy offer flag implies the app was installed + markEverInstalled(); } // Auto-preload essential data for the current language diff --git a/src/ui/settings-view.ts b/src/ui/settings-view.ts index 56549c2..f6a1a39 100644 --- a/src/ui/settings-view.ts +++ b/src/ui/settings-view.ts @@ -6,6 +6,7 @@ import { getTranslations, type LanguageCode } from '../core/i18n'; import { OfflineManager } from '../core/offline-manager'; import { DataCache } from '../core/data-cache'; import { diagTail } from '../core/diag-log'; +import { isPwaInstalled } from '../core/pwa-install'; const LANGUAGES = [ { code: 'en', name: 'English', local: 'English' }, @@ -417,7 +418,7 @@ export class SettingsView { if (!hintEl) return; const ua = navigator.userAgent; let hint: string; - if (await this.isPwaInstalled()) { + if (await isPwaInstalled()) { hint = t.settings.installPwaAlreadyInstalled; } else if (/firefox/i.test(ua)) { hint = t.settings.installPwaHintFirefox; @@ -435,7 +436,7 @@ export class SettingsView { const btn = document.getElementById('install-pwa'); if (!btn || !('serviceWorker' in navigator)) return; try { - if (!(await this.isPwaInstalled())) btn.classList.remove('hidden'); + if (!(await isPwaInstalled())) btn.classList.remove('hidden'); } catch { /* stay hidden */ } })(); @@ -602,6 +603,8 @@ export class SettingsView { if (statsEl) statsEl.textContent = t.settings.offlineCleared; const progressDiv = document.getElementById('offline-progress'); if (progressDiv) progressDiv.classList.add('hidden'); + const diagEl = document.getElementById('diag-log'); + if (diagEl) diagEl.textContent = diagTail(); }); // Show initial storage stats @@ -694,12 +697,14 @@ export class SettingsView { }).then((prog) => { if (progressBar) progressBar.style.width = '100%'; if (progressText) { - const failList = prog.failedFiles?.length ? `: ${prog.failedFiles.join(', ')}` : ''; - progressText.textContent = prog.aborted - ? t.settings.offlineAborted - : prog.failed > 0 - ? `${t.settings.offlineDone} ${prog.failed} ${t.settings.offlineFailed}${failList}` - : t.settings.offlineDone; + 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; + } } const diagEl = document.getElementById('diag-log'); if (diagEl) diagEl.textContent = diagTail(); @@ -737,19 +742,4 @@ export class SettingsView { preview.style.fontSize = `${this.settings.fontSize}px`; } } - - private async isPwaInstalled(): Promise { - if (window.matchMedia('(display-mode: standalone)').matches || (window.navigator as any).standalone === true) { - return true; - } - if (localStorage.getItem('ponomar-installed')) return true; - try { - const navAny = navigator as any; - if (typeof navAny.getInstalledRelatedApps === 'function') { - const apps = await navAny.getInstalledRelatedApps(); - if (Array.isArray(apps) && apps.length > 0) return true; - } - } catch { /* not supported */ } - return false; - } }