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
This commit is contained in:
1 parent
193038a671
commit
142b95d226
6 files changed
+90
-33
No files matched your search
@@ -102,7 +102,7 @@ export class DataCache {
|
|||||||
const db = await getDB();
|
const db = await getDB();
|
||||||
return await new Promise<boolean | null>((resolve) => {
|
return await new Promise<boolean | null>((resolve) => {
|
||||||
const tx = db.transaction(STORE_NAME, 'readonly');
|
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.onsuccess = () => resolve(request.result > 0);
|
||||||
request.onerror = () => resolve(null);
|
request.onerror = () => resolve(null);
|
||||||
});
|
});
|
||||||
|
|||||||
+17
-6
@@ -15,10 +15,21 @@ export interface DiagEvent {
|
|||||||
[key: string]: unknown;
|
[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<string, unknown>) {
|
export function diagLog(event: string, detail?: Record<string, unknown>) {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(DIAG_KEY);
|
const arr = readEvents();
|
||||||
const arr: DiagEvent[] = raw ? JSON.parse(raw) : [];
|
|
||||||
arr.push({ t: new Date().toISOString(), event, ...(detail ?? {}) });
|
arr.push({ t: new Date().toISOString(), event, ...(detail ?? {}) });
|
||||||
while (arr.length > MAX_EVENTS) arr.shift();
|
while (arr.length > MAX_EVENTS) arr.shift();
|
||||||
localStorage.setItem(DIAG_KEY, JSON.stringify(arr));
|
localStorage.setItem(DIAG_KEY, JSON.stringify(arr));
|
||||||
@@ -27,16 +38,16 @@ export function diagLog(event: string, detail?: Record<string, unknown>) {
|
|||||||
|
|
||||||
export function getDiagLog(): DiagEvent[] {
|
export function getDiagLog(): DiagEvent[] {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(DIAG_KEY);
|
return readEvents();
|
||||||
const arr = raw ? JSON.parse(raw) : [];
|
|
||||||
return Array.isArray(arr) ? arr : [];
|
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatEvent(e: DiagEvent): string {
|
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[] = [];
|
const parts: string[] = [];
|
||||||
for (const [k, v] of Object.entries(e)) {
|
for (const [k, v] of Object.entries(e)) {
|
||||||
if (k === 't' || k === 'event') continue;
|
if (k === 't' || k === 'event') continue;
|
||||||
|
|||||||
@@ -188,6 +188,8 @@ export class OfflineManager {
|
|||||||
}
|
}
|
||||||
if (!resp.ok) continue;
|
if (!resp.ok) continue;
|
||||||
const ct = resp.headers.get('content-type');
|
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;
|
if (ct && ct.includes('text/html')) break;
|
||||||
|
|
||||||
let data: unknown;
|
let data: unknown;
|
||||||
|
|||||||
@@ -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<boolean> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
+11
-3
@@ -12,6 +12,7 @@ import { loadSettings, type Settings } from './settings-view';
|
|||||||
import { getTranslations, type LanguageCode } from '../core/i18n';
|
import { getTranslations, type LanguageCode } from '../core/i18n';
|
||||||
import { DataCache } from '../core/data-cache';
|
import { DataCache } from '../core/data-cache';
|
||||||
import { diagLog } from '../core/diag-log';
|
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';
|
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
|
// Clear first-launch flag on re-install
|
||||||
window.addEventListener('appinstalled', () => {
|
window.addEventListener('appinstalled', () => {
|
||||||
localStorage.removeItem('ponomar-installed');
|
localStorage.removeItem('ponomar-installed');
|
||||||
|
markEverInstalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show offline offer on first standalone launch
|
// Show offline offer on first standalone launch
|
||||||
const isStandalone = () => window.matchMedia('(display-mode: standalone)').matches || (window.navigator as any).standalone;
|
const isStandalone = () => window.matchMedia('(display-mode: standalone)').matches || (window.navigator as any).standalone;
|
||||||
if (isStandalone() && !localStorage.getItem('ponomar-installed')) {
|
if (isStandalone()) {
|
||||||
localStorage.setItem('ponomar-installed', '1');
|
markEverInstalled();
|
||||||
this.showOfflineOfferModal();
|
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
|
// Auto-preload essential data for the current language
|
||||||
|
|||||||
+13
-23
@@ -6,6 +6,7 @@ import { getTranslations, type LanguageCode } from '../core/i18n';
|
|||||||
import { OfflineManager } from '../core/offline-manager';
|
import { OfflineManager } from '../core/offline-manager';
|
||||||
import { DataCache } from '../core/data-cache';
|
import { DataCache } from '../core/data-cache';
|
||||||
import { diagTail } from '../core/diag-log';
|
import { diagTail } from '../core/diag-log';
|
||||||
|
import { isPwaInstalled } from '../core/pwa-install';
|
||||||
|
|
||||||
const LANGUAGES = [
|
const LANGUAGES = [
|
||||||
{ code: 'en', name: 'English', local: 'English' },
|
{ code: 'en', name: 'English', local: 'English' },
|
||||||
@@ -417,7 +418,7 @@ export class SettingsView {
|
|||||||
if (!hintEl) return;
|
if (!hintEl) return;
|
||||||
const ua = navigator.userAgent;
|
const ua = navigator.userAgent;
|
||||||
let hint: string;
|
let hint: string;
|
||||||
if (await this.isPwaInstalled()) {
|
if (await isPwaInstalled()) {
|
||||||
hint = t.settings.installPwaAlreadyInstalled;
|
hint = t.settings.installPwaAlreadyInstalled;
|
||||||
} else if (/firefox/i.test(ua)) {
|
} else if (/firefox/i.test(ua)) {
|
||||||
hint = t.settings.installPwaHintFirefox;
|
hint = t.settings.installPwaHintFirefox;
|
||||||
@@ -435,7 +436,7 @@ export class SettingsView {
|
|||||||
const btn = document.getElementById('install-pwa');
|
const btn = document.getElementById('install-pwa');
|
||||||
if (!btn || !('serviceWorker' in navigator)) return;
|
if (!btn || !('serviceWorker' in navigator)) return;
|
||||||
try {
|
try {
|
||||||
if (!(await this.isPwaInstalled())) btn.classList.remove('hidden');
|
if (!(await isPwaInstalled())) btn.classList.remove('hidden');
|
||||||
} catch { /* stay hidden */ }
|
} catch { /* stay hidden */ }
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -602,6 +603,8 @@ export class SettingsView {
|
|||||||
if (statsEl) statsEl.textContent = t.settings.offlineCleared;
|
if (statsEl) statsEl.textContent = t.settings.offlineCleared;
|
||||||
const progressDiv = document.getElementById('offline-progress');
|
const progressDiv = document.getElementById('offline-progress');
|
||||||
if (progressDiv) progressDiv.classList.add('hidden');
|
if (progressDiv) progressDiv.classList.add('hidden');
|
||||||
|
const diagEl = document.getElementById('diag-log');
|
||||||
|
if (diagEl) diagEl.textContent = diagTail();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show initial storage stats
|
// Show initial storage stats
|
||||||
@@ -694,12 +697,14 @@ export class SettingsView {
|
|||||||
}).then((prog) => {
|
}).then((prog) => {
|
||||||
if (progressBar) progressBar.style.width = '100%';
|
if (progressBar) progressBar.style.width = '100%';
|
||||||
if (progressText) {
|
if (progressText) {
|
||||||
const failList = prog.failedFiles?.length ? `: ${prog.failedFiles.join(', ')}` : '';
|
if (prog.aborted) {
|
||||||
progressText.textContent = prog.aborted
|
progressText.textContent = t.settings.offlineAborted;
|
||||||
? t.settings.offlineAborted
|
} else if (prog.failed > 0) {
|
||||||
: prog.failed > 0
|
const failList = prog.failedFiles?.length ? `: ${prog.failedFiles.join(', ')}` : '';
|
||||||
? `${t.settings.offlineDone} ${prog.failed} ${t.settings.offlineFailed}${failList}`
|
progressText.textContent = `${t.settings.offlineDone} ${prog.failed} ${t.settings.offlineFailed}${failList}`;
|
||||||
: t.settings.offlineDone;
|
} else {
|
||||||
|
progressText.textContent = t.settings.offlineDone;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const diagEl = document.getElementById('diag-log');
|
const diagEl = document.getElementById('diag-log');
|
||||||
if (diagEl) diagEl.textContent = diagTail();
|
if (diagEl) diagEl.textContent = diagTail();
|
||||||
@@ -737,19 +742,4 @@ export class SettingsView {
|
|||||||
preview.style.fontSize = `${this.settings.fontSize}px`;
|
preview.style.fontSize = `${this.settings.fontSize}px`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async isPwaInstalled(): Promise<boolean> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user