Skip to content

Commit e9e4b87

Browse files
committed
chore(release): v1.7.2 - Critical Bugfixes & Performance Improvements
CRITICAL BUGFIXES: - IMPL_014: JSON/Markdown Timestamp Sync - Server mtime source of truth - IMPL_015: SyncStatus PENDING Fix - Set before JSON serialization - IMPL_001: Deletion Tracker Race Condition - Mutex-based sync - IMPL_002: ISO8601 Timezone Parsing - Multi-format support - IMPL_003: Memory Leak Prevention - SafeSardine Closeable - IMPL_004: E-Tag Batch Caching - ~50-100ms performance gain FEATURES: - Auto-updating timestamps in UI (every 30s) - Performance optimizations for Staggered Grid scrolling BUILD: - versionCode: 19 - versionName: 1.7.2 This release prepares for a new cross-platform Markdown editor (Web, Desktop Windows + Linux, Mobile) with proper JSON ↔ Markdown synchronization and resolves critical sync issues for external editor integration.
1 parent 45f528e commit e9e4b87

16 files changed

Lines changed: 600 additions & 48 deletions

File tree

CHANGELOG.de.md

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,77 @@ Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
88

99
---
1010

11+
## [1.7.2] - 2026-02-04
12+
13+
### 🐛 Kritische Fehlerbehebungen
14+
15+
#### JSON/Markdown Timestamp-Synchronisation
16+
17+
**Problem:** Externe Editoren (Obsidian, Typora, VS Code, eigene Editoren) aktualisieren Markdown-Inhalt, aber nicht den YAML `updated:` Timestamp, wodurch die Android-App Änderungen überspringt.
18+
19+
**Lösung:**
20+
- Server-Datei Änderungszeit (`mtime`) wird jetzt als Source of Truth statt YAML-Timestamp verwendet
21+
- Inhaltsänderungen werden via Hash-Vergleich erkannt
22+
- Notizen nach Markdown-Import als `PENDING` markiert → JSON automatisch beim nächsten Sync hochgeladen
23+
- Behebt Sortierungsprobleme nach externen Bearbeitungen
24+
25+
#### SyncStatus auf Server immer PENDING
26+
27+
**Problem:** Alle JSON-Dateien auf dem Server enthielten `"syncStatus": "PENDING"` auch nach erfolgreichem Sync, was externe Clients verwirrte.
28+
29+
**Lösung:**
30+
- Status wird jetzt auf `SYNCED` gesetzt **vor** JSON-Serialisierung
31+
- Server- und lokale Kopien sind jetzt konsistent
32+
- Externe Web/Tauri-Editoren können Sync-Status korrekt interpretieren
33+
34+
#### Deletion Tracker Race Condition
35+
36+
**Problem:** Batch-Löschungen konnten Lösch-Einträge verlieren durch konkurrierenden Dateizugriff.
37+
38+
**Lösung:**
39+
- Mutex-basierte Synchronisation für Deletion Tracking
40+
- Neue `trackDeletionSafe()` Funktion verhindert Race Conditions
41+
- Garantiert Zombie-Note-Prevention auch bei schnellen Mehrfach-Löschungen
42+
43+
#### ISO8601 Timezone-Parsing
44+
45+
**Problem:** Markdown-Importe schlugen fehl mit Timezone-Offsets wie `+01:00` oder `-05:00`.
46+
47+
**Lösung:**
48+
- Multi-Format ISO8601 Parser mit Fallback-Kette
49+
- Unterstützt UTC (Z), Timezone-Offsets (+01:00, +0100) und Millisekunden
50+
- Kompatibel mit Obsidian, Typora, VS Code Timestamps
51+
52+
### ⚡ Performance-Verbesserungen
53+
54+
#### E-Tag Batch Caching
55+
56+
- E-Tags werden jetzt in einer einzigen Batch-Operation geschrieben statt N einzelner Schreibvorgänge
57+
- Performance-Gewinn: ~50-100ms pro Sync mit mehreren Notizen
58+
- Reduzierte Disk-I/O-Operationen
59+
60+
#### Memory Leak Prevention
61+
62+
- `SafeSardineWrapper` implementiert jetzt `Closeable` für explizites Resource-Cleanup
63+
- HTTP Connection Pool wird nach Sync korrekt aufgeräumt
64+
- Verhindert Socket-Exhaustion bei häufigen Syncs
65+
66+
### 🔧 Technische Details
67+
68+
- **IMPL_001:** `kotlinx.coroutines.sync.Mutex` für thread-sicheres Deletion Tracking
69+
- **IMPL_002:** Pattern-basierter ISO8601 Parser mit 8 Format-Varianten
70+
- **IMPL_003:** Connection Pool Eviction + Dispatcher Shutdown in `close()`
71+
- **IMPL_004:** Batch `SharedPreferences.Editor` Updates
72+
- **IMPL_014:** Server `mtime` Parameter in `Note.fromMarkdown()`
73+
- **IMPL_015:** `syncStatus` vor `toJson()` Aufruf gesetzt
74+
75+
### 📚 Dokumentation
76+
77+
- External Editor Specification für Web/Tauri-Editor-Entwickler
78+
- Detaillierte Implementierungs-Dokumentation für alle Bugfixes
79+
80+
---
81+
1182
## [1.7.1] - 2026-02-02
1283

1384
### 🐛 Kritische Fehlerbehebungen
@@ -569,8 +640,8 @@ Das komplette UI wurde von XML-Views auf Jetpack Compose migriert. Die App ist j
569640

570641
### Documentation
571642
- Added WebDAV mount instructions (Windows, macOS, Linux)
572-
- Created [SYNC_ARCHITECTURE.md](../project-docs/simple-notes-sync/architecture/SYNC_ARCHITECTURE.md) - Complete sync documentation
573-
- Created [MARKDOWN_DESKTOP_REALITY_CHECK.md](../project-docs/simple-notes-sync/markdown-desktop-plan/MARKDOWN_DESKTOP_REALITY_CHECK.md) - Desktop integration analysis
643+
- Complete sync architecture documentation
644+
- Desktop integration analysis
574645

575646
---
576647

CHANGELOG.md

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,77 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
88

99
---
1010

11+
## [1.7.2] - 2026-02-04
12+
13+
### 🐛 Critical Bug Fixes
14+
15+
#### JSON/Markdown Timestamp Sync
16+
17+
**Problem:** External editors (Obsidian, Typora, VS Code, custom editors) update Markdown content but don't update YAML `updated:` timestamp, causing the Android app to skip changes.
18+
19+
**Solution:**
20+
- Server file modification time (`mtime`) is now used as source of truth instead of YAML timestamp
21+
- Content changes detected via hash comparison
22+
- Notes marked as `PENDING` after Markdown import → JSON automatically re-uploaded on next sync
23+
- Fixes sorting issues after external edits
24+
25+
#### SyncStatus on Server Always PENDING
26+
27+
**Problem:** All JSON files on server contained `"syncStatus": "PENDING"` even after successful sync, confusing external clients.
28+
29+
**Solution:**
30+
- Status is now set to `SYNCED` **before** JSON serialization
31+
- Server and local copies are now consistent
32+
- External web/Tauri editors can correctly interpret sync state
33+
34+
#### Deletion Tracker Race Condition
35+
36+
**Problem:** Batch deletes could lose deletion records due to concurrent file access.
37+
38+
**Solution:**
39+
- Mutex-based synchronization for deletion tracking
40+
- New `trackDeletionSafe()` function prevents race conditions
41+
- Guarantees zombie note prevention even with rapid deletes
42+
43+
#### ISO8601 Timezone Parsing
44+
45+
**Problem:** Markdown imports failed with timezone offsets like `+01:00` or `-05:00`.
46+
47+
**Solution:**
48+
- Multi-format ISO8601 parser with fallback chain
49+
- Supports UTC (Z), timezone offsets (+01:00, +0100), and milliseconds
50+
- Compatible with Obsidian, Typora, VS Code timestamps
51+
52+
### ⚡ Performance Improvements
53+
54+
#### E-Tag Batch Caching
55+
56+
- E-Tags are now written in single batch operation instead of N individual writes
57+
- Performance gain: ~50-100ms per sync with multiple notes
58+
- Reduced disk I/O operations
59+
60+
#### Memory Leak Prevention
61+
62+
- `SafeSardineWrapper` now implements `Closeable` for explicit resource cleanup
63+
- HTTP connection pool is properly evicted after sync
64+
- Prevents socket exhaustion during frequent syncs
65+
66+
### 🔧 Technical Details
67+
68+
- **IMPL_001:** `kotlinx.coroutines.sync.Mutex` for thread-safe deletion tracking
69+
- **IMPL_002:** Pattern-based ISO8601 parser with 8 format variants
70+
- **IMPL_003:** Connection pool eviction + dispatcher shutdown in `close()`
71+
- **IMPL_004:** Batch `SharedPreferences.Editor` updates
72+
- **IMPL_014:** Server `mtime` parameter in `Note.fromMarkdown()`
73+
- **IMPL_015:** `syncStatus` set before `toJson()` call
74+
75+
### 📚 Documentation
76+
77+
- External Editor Specification for web/Tauri editor developers
78+
- Detailed implementation documentation for all bugfixes
79+
80+
---
81+
1182
## [1.7.1] - 2026-02-02
1283

1384
### 🐛 Critical Bug Fixes
@@ -568,8 +639,8 @@ The complete UI has been migrated from XML Views to Jetpack Compose. The app is
568639

569640
### Documentation
570641
- Added WebDAV mount instructions (Windows, macOS, Linux)
571-
- Created [SYNC_ARCHITECTURE.md](../project-docs/simple-notes-sync/architecture/SYNC_ARCHITECTURE.md) - Complete sync documentation
572-
- Created [MARKDOWN_DESKTOP_REALITY_CHECK.md](../project-docs/simple-notes-sync/markdown-desktop-plan/MARKDOWN_DESKTOP_REALITY_CHECK.md) - Desktop integration analysis
642+
- Complete sync architecture documentation
643+
- Desktop integration analysis
573644

574645
---
575646

android/app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ android {
2020
applicationId = "dev.dettmer.simplenotes"
2121
minSdk = 24
2222
targetSdk = 36
23-
versionCode = 18 // 🔧 v1.7.1: Android 9 getForegroundInfo Fix (Issue #15)
24-
versionName = "1.7.1" // 🔧 v1.7.1: Android 9 getForegroundInfo Fix
23+
versionCode = 19 // 🔧 v1.7.2: Critical Bugfixes (Timestamp Sync, SyncStatus, etc.)
24+
versionName = "1.7.2" // 🔧 v1.7.2: Critical Bugfixes
2525

2626
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2727
}

android/app/src/main/java/dev/dettmer/simplenotes/SimpleNotesApplication.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package dev.dettmer.simplenotes
22

33
import android.app.Application
44
import android.content.Context
5-
import androidx.appcompat.app.AppCompatDelegate
65
import dev.dettmer.simplenotes.utils.Logger
76
import dev.dettmer.simplenotes.sync.NetworkMonitor
87
import dev.dettmer.simplenotes.utils.NotificationHelper

android/app/src/main/java/dev/dettmer/simplenotes/models/Note.kt

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,13 @@ type: ${noteType.name.lowercase()}
210210
/**
211211
* Parst Markdown zurück zu Note-Objekt (Task #1.2.0-09)
212212
* v1.4.0: Unterstützt jetzt auch Checklisten-Format
213+
* 🔧 v1.7.2 (IMPL_014): Optional serverModifiedTime für korrekte Timestamp-Sync
213214
*
214215
* @param md Markdown-String mit YAML Frontmatter
216+
* @param serverModifiedTime Optionaler Server-Datei mtime (Priorität über YAML timestamp)
215217
* @return Note-Objekt oder null bei Parse-Fehler
216218
*/
217-
fun fromMarkdown(md: String): Note? {
219+
fun fromMarkdown(md: String, serverModifiedTime: Long? = null): Note? {
218220
return try {
219221
// Parse YAML Frontmatter + Markdown Content
220222
val frontmatterRegex = Regex("^---\\n(.+?)\\n---\\n(.*)$", RegexOption.DOT_MATCHES_ALL)
@@ -279,12 +281,22 @@ type: ${noteType.name.lowercase()}
279281
checklistItems = null
280282
}
281283

284+
// 🔧 v1.7.2 (IMPL_014): Server mtime hat Priorität über YAML timestamp
285+
val yamlUpdatedAt = parseISO8601(metadata["updated"] ?: "")
286+
val effectiveUpdatedAt = when {
287+
serverModifiedTime != null && serverModifiedTime > yamlUpdatedAt -> {
288+
Logger.d(TAG, "Using server mtime ($serverModifiedTime) over YAML ($yamlUpdatedAt)")
289+
serverModifiedTime
290+
}
291+
else -> yamlUpdatedAt
292+
}
293+
282294
Note(
283295
id = metadata["id"] ?: UUID.randomUUID().toString(),
284296
title = title,
285297
content = content,
286298
createdAt = parseISO8601(metadata["created"] ?: ""),
287-
updatedAt = parseISO8601(metadata["updated"] ?: ""),
299+
updatedAt = effectiveUpdatedAt,
288300
deviceId = metadata["device"] ?: "desktop",
289301
syncStatus = SyncStatus.SYNCED, // Annahme: Vom Server importiert
290302
noteType = noteType,
@@ -307,18 +319,71 @@ type: ${noteType.name.lowercase()}
307319
}
308320

309321
/**
310-
* Parst ISO8601 zurück zu Timestamp (Task #1.2.0-10)
322+
* 🔧 v1.7.2 (IMPL_002): Robustes ISO8601 Parsing mit Multi-Format Unterstützung
323+
*
324+
* Unterstützte Formate (in Prioritätsreihenfolge):
325+
* 1. 2024-12-21T18:00:00Z (UTC mit Z)
326+
* 2. 2024-12-21T18:00:00+01:00 (mit Offset)
327+
* 3. 2024-12-21T18:00:00+0100 (Offset ohne Doppelpunkt)
328+
* 4. 2024-12-21T18:00:00.123Z (mit Millisekunden)
329+
* 5. 2024-12-21T18:00:00.123+01:00 (Millisekunden + Offset)
330+
* 6. 2024-12-21 18:00:00 (Leerzeichen statt T)
331+
*
311332
* Fallback: Aktueller Timestamp bei Fehler
333+
*
334+
* @param dateString ISO8601 Datum-String
335+
* @return Unix Timestamp in Millisekunden
312336
*/
313337
private fun parseISO8601(dateString: String): Long {
314-
return try {
315-
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
316-
sdf.timeZone = TimeZone.getTimeZone("UTC")
317-
sdf.parse(dateString)?.time ?: System.currentTimeMillis()
318-
} catch (e: Exception) {
319-
Logger.w(TAG, "Failed to parse ISO8601 date '$dateString': ${e.message}")
320-
System.currentTimeMillis() // Fallback
338+
if (dateString.isBlank()) {
339+
return System.currentTimeMillis()
340+
}
341+
342+
// Normalisiere: Leerzeichen → T
343+
val normalized = dateString.trim().replace(' ', 'T')
344+
345+
// Format-Patterns in Prioritätsreihenfolge
346+
val patterns = listOf(
347+
// Mit Timezone Z
348+
"yyyy-MM-dd'T'HH:mm:ss'Z'",
349+
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
350+
351+
// Mit Offset XXX (+01:00)
352+
"yyyy-MM-dd'T'HH:mm:ssXXX",
353+
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX",
354+
355+
// Mit Offset ohne Doppelpunkt (+0100)
356+
"yyyy-MM-dd'T'HH:mm:ssZ",
357+
"yyyy-MM-dd'T'HH:mm:ss.SSSZ",
358+
359+
// Ohne Timezone (interpretiere als UTC)
360+
"yyyy-MM-dd'T'HH:mm:ss",
361+
"yyyy-MM-dd'T'HH:mm:ss.SSS"
362+
)
363+
364+
// Versuche alle Patterns nacheinander
365+
for (pattern in patterns) {
366+
@Suppress("SwallowedException") // Intentional: try all patterns before logging
367+
try {
368+
val sdf = SimpleDateFormat(pattern, Locale.US)
369+
// Für Patterns ohne Timezone: UTC annehmen
370+
if (!pattern.contains("XXX") && !pattern.contains("Z")) {
371+
sdf.timeZone = TimeZone.getTimeZone("UTC")
372+
}
373+
val parsed = sdf.parse(normalized)
374+
if (parsed != null) {
375+
return parsed.time
376+
}
377+
} catch (e: Exception) {
378+
// 🔇 Exception intentionally swallowed - try next pattern
379+
// Only log if no pattern matches (see fallback below)
380+
continue
381+
}
321382
}
383+
384+
// Fallback wenn kein Pattern passt
385+
Logger.w(TAG, "Failed to parse ISO8601 date '$dateString' with any pattern, using current time")
386+
return System.currentTimeMillis()
322387
}
323388
}
324389
}

android/app/src/main/java/dev/dettmer/simplenotes/storage/NotesStorage.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ import dev.dettmer.simplenotes.models.DeletionTracker
55
import dev.dettmer.simplenotes.models.Note
66
import dev.dettmer.simplenotes.utils.DeviceIdGenerator
77
import dev.dettmer.simplenotes.utils.Logger
8+
import kotlinx.coroutines.sync.Mutex
9+
import kotlinx.coroutines.sync.withLock
810
import java.io.File
911

1012
class NotesStorage(private val context: Context) {
1113

1214
companion object {
1315
private const val TAG = "NotesStorage"
16+
// 🔒 v1.7.2 (IMPL_001): Mutex für thread-sichere Deletion Tracker Operationen
17+
private val deletionTrackerMutex = Mutex()
1418
}
1519

1620
private val notesDir: File = File(context.filesDir, "notes").apply {
@@ -107,6 +111,30 @@ class NotesStorage(private val context: Context) {
107111
}
108112
}
109113

114+
/**
115+
* 🔒 v1.7.2 (IMPL_001): Thread-sichere Deletion-Tracking mit Mutex
116+
*
117+
* Verhindert Race Conditions bei Batch-Deletes durch exklusiven Zugriff
118+
* auf den Deletion Tracker.
119+
*
120+
* @param noteId ID der gelöschten Notiz
121+
* @param deviceId Geräte-ID für Konflikt-Erkennung
122+
*/
123+
suspend fun trackDeletionSafe(noteId: String, deviceId: String) {
124+
deletionTrackerMutex.withLock {
125+
val tracker = loadDeletionTracker()
126+
tracker.addDeletion(noteId, deviceId)
127+
saveDeletionTracker(tracker)
128+
Logger.d(TAG, "📝 Tracked deletion (mutex-protected): $noteId")
129+
}
130+
}
131+
132+
/**
133+
* Legacy-Methode ohne Mutex-Schutz.
134+
* Verwendet für synchrone Aufrufe wo Coroutines nicht verfügbar sind.
135+
*
136+
* @deprecated Verwende trackDeletionSafe() für Thread-Safety wo möglich
137+
*/
110138
fun trackDeletion(noteId: String, deviceId: String) {
111139
val tracker = loadDeletionTracker()
112140
tracker.addDeletion(noteId, deviceId)

0 commit comments

Comments
 (0)