feat(v1.8.0): IMPL_022 Multi-Client Deletion Enhancement

Defensive improvements for server deletion detection:

1. Enhanced logging in detectServerDeletions():
   - Statistics: server/local/synced note counts
   - Summary log when deletions found

2. Explicit documentation:
   - Comment clarifying checklists are included
   - Both Notes and Checklists use same detection mechanism

3. Sync banner now shows deletion count:
   - '3 synced · 2 deleted on server'
   - New strings: sync_deleted_on_server_count (en + de)

4. DELETED_ON_SERVER → PENDING on edit:
   - Verified existing logic works correctly
   - All edited notes → PENDING (re-upload to server)
   - Added comments for clarity

Cross-client analysis confirmed:
-  Android/Desktop/Web deletions detected correctly
- ⚠️ Obsidian .md-only deletions not detected (by design: JSON = source of truth)

IMPL_022_MULTI_CLIENT_DELETION.md
This commit is contained in:
inventory69
2026-02-09 09:31:19 +01:00
parent 07607fc095
commit bf7a74ec30
5 changed files with 41 additions and 7 deletions

View File

@@ -1055,6 +1055,10 @@ class WebDavSyncService(private val context: Context) {
* Keine zusätzlichen HTTP-Requests! Nutzt die bereits geladene * Keine zusätzlichen HTTP-Requests! Nutzt die bereits geladene
* serverNoteIds-Liste aus dem PROPFIND-Request. * serverNoteIds-Liste aus dem PROPFIND-Request.
* *
* Prüft ALLE Notizen (Notes + Checklists), da beide als
* JSON in /notes/{id}.json gespeichert werden.
* NoteType (NOTE vs CHECKLIST) spielt keine Rolle für die Detection.
*
* @param serverNoteIds Set aller Note-IDs auf dem Server (aus PROPFIND) * @param serverNoteIds Set aller Note-IDs auf dem Server (aus PROPFIND)
* @param localNotes Alle lokalen Notizen * @param localNotes Alle lokalen Notizen
* @return Anzahl der als DELETED_ON_SERVER markierten Notizen * @return Anzahl der als DELETED_ON_SERVER markierten Notizen
@@ -1064,23 +1068,35 @@ class WebDavSyncService(private val context: Context) {
localNotes: List<Note> localNotes: List<Note>
): Int { ): Int {
var deletedCount = 0 var deletedCount = 0
val syncedNotes = localNotes.filter { it.syncStatus == SyncStatus.SYNCED }
localNotes.forEach { note -> // 🆕 v1.8.0 (IMPL_022): Statistik-Log für Debugging
Logger.d(TAG, "🔍 detectServerDeletions: " +
"serverNotes=${serverNoteIds.size}, " +
"localSynced=${syncedNotes.size}, " +
"localTotal=${localNotes.size}")
syncedNotes.forEach { note ->
// Nur SYNCED-Notizen prüfen: // Nur SYNCED-Notizen prüfen:
// - LOCAL_ONLY: War nie auf Server → irrelevant // - LOCAL_ONLY: War nie auf Server → irrelevant
// - PENDING: Soll hochgeladen werden → nicht überschreiben // - PENDING: Soll hochgeladen werden → nicht überschreiben
// - CONFLICT: Wird separat behandelt // - CONFLICT: Wird separat behandelt
// - DELETED_ON_SERVER: Bereits markiert // - DELETED_ON_SERVER: Bereits markiert
if (note.syncStatus == SyncStatus.SYNCED && note.id !in serverNoteIds) { if (note.id !in serverNoteIds) {
val updatedNote = note.copy(syncStatus = SyncStatus.DELETED_ON_SERVER) val updatedNote = note.copy(syncStatus = SyncStatus.DELETED_ON_SERVER)
storage.saveNote(updatedNote) storage.saveNote(updatedNote)
deletedCount++ deletedCount++
Logger.d(TAG, "Note '${note.title}' (${note.id}) " + Logger.d(TAG, "🗑️ Note '${note.title}' (${note.id}) " +
"was deleted on server, marked as DELETED_ON_SERVER") "was deleted on server, marked as DELETED_ON_SERVER")
} }
} }
if (deletedCount > 0) {
Logger.d(TAG, "📊 Server deletion detection complete: " +
"$deletedCount of ${syncedNotes.size} synced notes deleted on server")
}
return deletedCount return deletedCount
} }

View File

@@ -231,6 +231,8 @@ class NoteEditorViewModel(
} }
val note = if (existingNote != null) { val note = if (existingNote != null) {
// 🆕 v1.8.0 (IMPL_022): syncStatus wird immer auf PENDING gesetzt
// beim Bearbeiten - gilt für SYNCED, CONFLICT, DELETED_ON_SERVER, etc.
existingNote!!.copy( existingNote!!.copy(
title = title, title = title,
content = content, content = content,
@@ -272,6 +274,8 @@ class NoteEditorViewModel(
} }
val note = if (existingNote != null) { val note = if (existingNote != null) {
// 🆕 v1.8.0 (IMPL_022): syncStatus wird immer auf PENDING gesetzt
// beim Bearbeiten - gilt für SYNCED, CONFLICT, DELETED_ON_SERVER, etc.
existingNote!!.copy( existingNote!!.copy(
title = title, title = title,
content = "", // Empty for checklists content = "", // Empty for checklists

View File

@@ -559,10 +559,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
if (result.isSuccess) { if (result.isSuccess) {
val bannerMessage = if (result.syncedCount > 0) { // 🆕 v1.8.0 (IMPL_022): Erweiterte Banner-Nachricht mit Löschungen
getString(R.string.toast_sync_success, result.syncedCount) val bannerMessage = buildString {
} else { if (result.syncedCount > 0) {
getString(R.string.snackbar_nothing_to_sync) append(getString(R.string.toast_sync_success, result.syncedCount))
}
if (result.deletedOnServerCount > 0) {
if (isNotEmpty()) append(" · ")
append(getString(R.string.sync_deleted_on_server_count, result.deletedOnServerCount))
}
if (isEmpty()) {
append(getString(R.string.snackbar_nothing_to_sync))
}
} }
SyncStateManager.markCompleted(bannerMessage) SyncStateManager.markCompleted(bannerMessage)
loadNotes() loadNotes()

View File

@@ -73,6 +73,9 @@
<string name="sync_legend_deleted_label">Auf Server gelöscht</string> <string name="sync_legend_deleted_label">Auf Server gelöscht</string>
<string name="sync_legend_deleted_desc">Diese Notiz wurde auf einem anderen Gerät oder direkt auf dem Server gelöscht. Sie existiert noch lokal.</string> <string name="sync_legend_deleted_desc">Diese Notiz wurde auf einem anderen Gerät oder direkt auf dem Server gelöscht. Sie existiert noch lokal.</string>
<!-- 🆕 v1.8.0 (IMPL_022): Sync-Banner Löschungsanzahl -->
<string name="sync_deleted_on_server_count">%d auf Server gelöscht</string>
<!-- ============================= --> <!-- ============================= -->
<!-- DELETE DIALOGS --> <!-- DELETE DIALOGS -->
<!-- ============================= --> <!-- ============================= -->

View File

@@ -80,6 +80,9 @@
<string name="sync_legend_deleted_label">Deleted on server</string> <string name="sync_legend_deleted_label">Deleted on server</string>
<string name="sync_legend_deleted_desc">This note was deleted on another device or directly on the server. It still exists locally.</string> <string name="sync_legend_deleted_desc">This note was deleted on another device or directly on the server. It still exists locally.</string>
<!-- 🆕 v1.8.0 (IMPL_022): Sync banner deletion count -->
<string name="sync_deleted_on_server_count">%d deleted on server</string>
<!-- ============================= --> <!-- ============================= -->
<!-- DELETE DIALOGS --> <!-- DELETE DIALOGS -->
<!-- ============================= --> <!-- ============================= -->