From d08cefa5c9f3ce2160719f49b1ec5ab8dc4427f9 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:25:35 +0200 Subject: [PATCH] feat(pwa): Sync-Status-Store mit localStorage-Persistierung Spiegelt SW-Messages (sync-start/progress/done/error) in einen Svelte-State. lastSynced wird in localStorage persistiert, damit der User nach einem Reload sieht, wann zuletzt synchronisiert wurde. Wird vom SyncIndicator und der Admin-App-Tab konsumiert. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/client/sync-status.svelte.ts | 53 ++++++++++++++++++++++++++++ tests/unit/sync-status-store.test.ts | 30 ++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 src/lib/client/sync-status.svelte.ts create mode 100644 tests/unit/sync-status-store.test.ts diff --git a/src/lib/client/sync-status.svelte.ts b/src/lib/client/sync-status.svelte.ts new file mode 100644 index 0000000..bdc0d54 --- /dev/null +++ b/src/lib/client/sync-status.svelte.ts @@ -0,0 +1,53 @@ +// State, den der Service-Worker per postMessage befüllt. Die App +// spiegelt den Sync-Fortschritt im SyncIndicator. +export type SyncState = + | { kind: 'idle' } + | { kind: 'syncing'; current: number; total: number } + | { kind: 'error'; message: string }; + +export type SWMessage = + | { type: 'sync-start'; total: number } + | { type: 'sync-progress'; current: number; total: number } + | { type: 'sync-done'; lastSynced: number } + | { type: 'sync-error'; message: string }; + +const STORAGE_KEY = 'kochwas.sw.lastSynced'; + +function loadLastSynced(): number | null { + if (typeof localStorage === 'undefined') return null; + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + const n = Number(raw); + return Number.isFinite(n) ? n : null; +} + +function saveLastSynced(ts: number): void { + if (typeof localStorage === 'undefined') return; + localStorage.setItem(STORAGE_KEY, String(ts)); +} + +class SyncStatusStore { + state = $state({ kind: 'idle' }); + lastSynced = $state(loadLastSynced()); + + handle(msg: SWMessage): void { + switch (msg.type) { + case 'sync-start': + this.state = { kind: 'syncing', current: 0, total: msg.total }; + break; + case 'sync-progress': + this.state = { kind: 'syncing', current: msg.current, total: msg.total }; + break; + case 'sync-done': + this.state = { kind: 'idle' }; + this.lastSynced = msg.lastSynced; + saveLastSynced(msg.lastSynced); + break; + case 'sync-error': + this.state = { kind: 'error', message: msg.message }; + break; + } + } +} + +export const syncStatus = new SyncStatusStore(); diff --git a/tests/unit/sync-status-store.test.ts b/tests/unit/sync-status-store.test.ts new file mode 100644 index 0000000..d017ddb --- /dev/null +++ b/tests/unit/sync-status-store.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect, beforeEach } from 'vitest'; + +describe('sync-status store', () => { + beforeEach(async () => { + const mod = await import('../../src/lib/client/sync-status.svelte'); + mod.syncStatus.state = { kind: 'idle' }; + mod.syncStatus.lastSynced = null; + }); + + it('processes progress messages', async () => { + const { syncStatus } = await import('../../src/lib/client/sync-status.svelte'); + syncStatus.handle({ type: 'sync-progress', current: 3, total: 10 }); + expect(syncStatus.state).toEqual({ kind: 'syncing', current: 3, total: 10 }); + }); + + it('transitions to idle on sync-done and records timestamp', async () => { + const { syncStatus } = await import('../../src/lib/client/sync-status.svelte'); + syncStatus.handle({ type: 'sync-start', total: 5 }); + expect(syncStatus.state.kind).toBe('syncing'); + syncStatus.handle({ type: 'sync-done', lastSynced: 1700000000000 }); + expect(syncStatus.state).toEqual({ kind: 'idle' }); + expect(syncStatus.lastSynced).toBe(1700000000000); + }); + + it('sets error state on sync-error', async () => { + const { syncStatus } = await import('../../src/lib/client/sync-status.svelte'); + syncStatus.handle({ type: 'sync-error', message: 'Quota exceeded' }); + expect(syncStatus.state).toEqual({ kind: 'error', message: 'Quota exceeded' }); + }); +});