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();
|
||||
return await new Promise<boolean | null>((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);
|
||||
});
|
||||
|
||||
+17
-6
@@ -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<string, unknown>) {
|
||||
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<string, unknown>) {
|
||||
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 { 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
|
||||
|
||||
+13
-23
@@ -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<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