netdiag-app/src/lib/sync.svelte.ts
Eduard Wisch 250e0f9eba
Some checks failed
Build APK / build-apk (push) Failing after 3m21s
CI-Build: Kotlin aktivieren + Sync-Button mit Fehler-Feedback [apk]
- build.yml: kotlin-android-Plugin (1.9.24) + JVM-Target 17 aktiviert — .kt-Dateien
  wurden vorher stillschweigend ignoriert (kein Kotlin-Compiler), NetDiagScannerPlugin
  fehlte im APK -> 'plugin not implemented' fuer alle nativen Scan-Methoden
- build.yml: MainActivity.java durch .kt ersetzen (rm + cat), vermeidet
  Klassenkollision und registriert das Plugin korrekt
- sync.svelte.ts: syncManual() -- gibt immer sichtbares Toast-Feedback
  (Fehler-Details, offline, nicht angemeldet, alles synchronisiert)
- AppHeader.svelte: Sync-Button ruft syncManual() statt syncNow()

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:05:23 +02:00

122 lines
3.5 KiB
TypeScript

/**
* Offline-Sync der Diagnose-Protokolle (Svelte 5 Runes).
*
* Auf der Baustelle ist oft keine Verbindung zum Dolibarr-Server. Protokolle
* werden lokal gespeichert (db.ts) und hier zum Server geschoben, sobald
* wieder Netz da ist. Der Server-Endpunkt ist idempotent (clientUuid), ein
* doppelter Sync schadet also nicht.
*/
import { Network } from '@capacitor/network';
import { ApiError, isLoggedIn, syncProtocol } from './api';
import { getDirtyProtocols, saveProtocol } from './db';
import { toast } from './toast.svelte';
type SyncStatus = 'idle' | 'syncing' | 'offline' | 'error';
const SYNC_INTERVAL_MS = 30_000;
class SyncState {
status = $state<SyncStatus>('idle');
pendingCount = $state(0);
lastError = $state('');
online = $state(true);
private timer: ReturnType<typeof setInterval> | null = null;
/** Sync-Dienst starten: Netz-Listener + periodischer Lauf */
async start(): Promise<void> {
const st = await Network.getStatus();
this.online = st.connected;
Network.addListener('networkStatusChange', (s) => {
this.online = s.connected;
if (s.connected) void this.syncNow();
else this.status = 'offline';
});
this.timer = setInterval(() => void this.syncNow(), SYNC_INTERVAL_MS);
await this.refreshPending();
void this.syncNow();
}
/** Sync-Dienst stoppen */
stop(): void {
if (this.timer) clearInterval(this.timer);
this.timer = null;
}
/** Anzahl offener (dirty) Protokolle neu zählen */
async refreshPending(): Promise<void> {
this.pendingCount = (await getDirtyProtocols()).length;
}
/**
* Alle offenen Protokolle synchronisieren.
* Läuft still im Hintergrund; Fehler werden gemerkt, nicht geworfen.
*/
async syncNow(): Promise<void> {
if (this.status === 'syncing' || !this.online || !isLoggedIn()) return;
const dirty = await getDirtyProtocols();
this.pendingCount = dirty.length;
if (dirty.length === 0) {
this.status = 'idle';
return;
}
this.status = 'syncing';
this.lastError = '';
for (const p of dirty) {
try {
const res = await syncProtocol(p);
p.serverId = res.protocolId;
p.ref = res.ref;
p.dirty = false;
await saveProtocol(p);
} catch (e) {
this.lastError = e instanceof ApiError ? e.message : 'Sync-Fehler';
this.status = 'error';
await this.refreshPending();
return; // beim nächsten Lauf erneut versuchen
}
}
await this.refreshPending();
this.status = 'idle';
}
/**
* Manueller Sync per Tap auf die Sync-Ampel im Header.
* Gibt IMMER sichtbares Feedback (Toast) — der `syncNow()`-Hintergrundlauf
* bricht still ab, dadurch wirkte der Button vorher "tot".
*/
async syncManual(): Promise<void> {
if (!this.online) {
toast.show('Offline — keine Verbindung zum Server', 'info');
return;
}
if (!isLoggedIn()) {
toast.show('Nicht angemeldet — bitte erst einloggen', 'info');
return;
}
if (this.status === 'syncing') {
toast.show('Synchronisierung läuft bereits …', 'info');
return;
}
await this.refreshPending();
if (this.pendingCount === 0) {
toast.show('Alles synchronisiert', 'success');
return;
}
await this.syncNow();
if (this.status === 'error') {
toast.show(this.lastError || 'Sync fehlgeschlagen', 'error', 5000);
} else {
toast.show('Protokolle übertragen', 'success');
}
}
}
export const sync = new SyncState();