From 235a355a0594878cb5532a4fe6237bd25ee1218b Mon Sep 17 00:00:00 2001 From: Maximilian von Heyden Date: Fri, 13 Feb 2026 18:11:38 +0100 Subject: [PATCH] Add merge-on-save to WebDAV sync to prevent event loss Before saving to WebDAV, the remote logbook is loaded and merged with the local version. Events from other devices that don't exist locally are added before uploading. This prevents one device from overwriting events added by another device between sync intervals. Co-Authored-By: Claude Opus 4.6 --- .../lunatracker/MainActivity.kt | 4 ++ .../repository/WebDAVLogbookRepository.kt | 72 +++++++++++++++++-- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt b/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt index 4d2e425..183eaae 100644 --- a/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt +++ b/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt @@ -1460,6 +1460,10 @@ class MainActivity : AppCompatActivity() { runOnUiThread({ setLoading(false) + // Refresh list - merge may have added events from other devices + logbook?.sort() + recyclerView.adapter?.notifyDataSetChanged() + Toast.makeText( this@MainActivity, if (lastEventAdded != null) diff --git a/app/src/main/java/it/danieleverducci/lunatracker/repository/WebDAVLogbookRepository.kt b/app/src/main/java/it/danieleverducci/lunatracker/repository/WebDAVLogbookRepository.kt index f23da2e..e016bcf 100644 --- a/app/src/main/java/it/danieleverducci/lunatracker/repository/WebDAVLogbookRepository.kt +++ b/app/src/main/java/it/danieleverducci/lunatracker/repository/WebDAVLogbookRepository.kt @@ -133,13 +133,16 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p } private fun saveLogbook(context: Context, logbook: Logbook) { - // Lock logbook on WebDAV to avoid concurrent changes - //sardine.lock(getUrl()) - // Reload logbook from WebDAV - // Merge logbooks (based on time) - // Write logbook - // Unlock logbook on WebDAV - //sardine.unlock(getUrl()) + // Load remote logbook and merge before saving to avoid overwriting + // events added by other devices + try { + val remoteLogbook = loadLogbook(logbook.name) + mergeRemoteEvents(logbook, remoteLogbook) + Log.d(TAG, "Merged remote events, logbook now has ${logbook.logs.size} events") + } catch (e: Exception) { + // Remote not available (404, network error, etc.) - save local version as-is + Log.w(TAG, "Could not load remote logbook for merge: $e") + } val ja = JSONArray() for (l in logbook.logs) { @@ -148,6 +151,61 @@ class WebDAVLogbookRepository(val webDavURL: String, val username: String, val p sardine.put(getUrl(logbook.name), ja.toString().toByteArray()) } + /** + * Merges remote events into the local logbook. + * Events from the remote that don't exist locally are added. + * Ongoing events that were finalized locally are skipped. + */ + private fun mergeRemoteEvents(local: Logbook, remote: Logbook) { + val localFingerprints = local.logs.map { eventFingerprint(it) }.toHashSet() + var added = 0 + + for (remoteEvent in remote.logs) { + val fingerprint = eventFingerprint(remoteEvent) + if (localFingerprints.contains(fingerprint)) { + continue // Already exists locally + } + + // If remote event is ongoing, check if it was finalized locally + if (remoteEvent.ongoing && isOngoingSuperseded(remoteEvent, local.logs)) { + continue + } + + local.logs.add(remoteEvent) + localFingerprints.add(fingerprint) + added++ + } + + if (added > 0) { + local.sort() + Log.d(TAG, "Added $added events from remote during merge") + } + } + + private fun eventFingerprint(event: LunaEvent): String { + return "${event.time}_${event.type}_${event.quantity}_${event.notes}_${event.signature}_${event.ongoing}" + } + + /** + * Checks if a remote ongoing event was already finalized locally. + * Compares the ongoing event's start time with the calculated start time + * of finalized events of the same type. + */ + private fun isOngoingSuperseded(ongoingEvent: LunaEvent, localEvents: List): Boolean { + val ongoingStartTime = ongoingEvent.time + for (local in localEvents) { + if (local.ongoing) continue + if (local.type != ongoingEvent.type) continue + // Finalized events: time = end time, quantity = duration in minutes + // Calculate approximate start time + val localStartTime = local.time - local.quantity * 60 + if (Math.abs(ongoingStartTime - localStartTime) < 120) { + return true + } + } + return false + } + /** * Connect to server and check if a logbook already exists. * If it does not exist, try to upload the local one.