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 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 18:11:38 +01:00
parent 81473f8f9f
commit 235a355a05
2 changed files with 69 additions and 7 deletions

View File

@@ -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)

View File

@@ -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<LunaEvent>): 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.