From 9cc0946f90ad63a448064fc571b2b470e8ceff00 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 27 Apr 2026 14:53:35 +0200 Subject: [PATCH] Refactor timestamp generation to use helper function Introduces `PendingStore.nowRfc3339()` to consistently generate RFC3339 timestamps with second granularity. This avoids potential issues with sub-second precision in filenames and aligns with common timestamp formatting practices. The helper function is used in `CaseDetailScreen` and `RecordingViewModel` to replace direct calls to `Instant.now().toString()`. A new unit test verifies the expected behavior of the helper function. --- CLAUDE.md | 4 ++++ .../java/com/doctate/watch/audio/PendingStore.kt | 12 ++++++++++++ .../doctate/watch/presentation/CaseDetailScreen.kt | 3 ++- .../watch/presentation/RecordingViewModel.kt | 5 ++--- .../com/doctate/watch/audio/PendingStoreTest.kt | 13 +++++++++++++ 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f56be5a..55c31ad 100755 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,6 +19,10 @@ * CRITICAL: "cargo clippy" hat immer Recht! +## Java + +* Java Home ist in /opt/android-studio/jbr + ## HTML * Innerhalb von HTML-Seiten sind JS-Einbettungen ausdrücklich erlaubt und gewünscht, wenn sie für den modernen Look-And-Feel der Webseite erforderlich sind. diff --git a/watch/wearos/app/src/main/java/com/doctate/watch/audio/PendingStore.kt b/watch/wearos/app/src/main/java/com/doctate/watch/audio/PendingStore.kt index 6cf8684..01b4db2 100644 --- a/watch/wearos/app/src/main/java/com/doctate/watch/audio/PendingStore.kt +++ b/watch/wearos/app/src/main/java/com/doctate/watch/audio/PendingStore.kt @@ -4,6 +4,8 @@ import android.util.Log import java.io.File import java.nio.file.Files import java.nio.file.StandardCopyOption +import java.time.Instant +import java.time.temporal.ChronoUnit import org.json.JSONObject /** @@ -99,5 +101,15 @@ class PendingStore(private val dir: File) { * `doctate-common::timestamp::recorded_at_to_filename_stem`. */ fun recordedAtToFilenameStem(recordedAt: String): String = recordedAt.replace(':', '-') + + /** Current UTC time as RFC3339 with second granularity, e.g. + * `2026-04-15T10:32:00Z`. 1:1 to + * `doctate-common::timestamp::now_rfc3339`. The truncation is + * load-bearing: the server's filename parser expects exactly + * `YYYY-MM-DDTHH-MM-SSZ` after the `:` → `-` substitution, and + * `Instant.now().toString()` would otherwise leak microseconds + * from the platform clock. */ + fun nowRfc3339(): String = + Instant.now().truncatedTo(ChronoUnit.SECONDS).toString() } } diff --git a/watch/wearos/app/src/main/java/com/doctate/watch/presentation/CaseDetailScreen.kt b/watch/wearos/app/src/main/java/com/doctate/watch/presentation/CaseDetailScreen.kt index 9a7514a..e9e1c44 100644 --- a/watch/wearos/app/src/main/java/com/doctate/watch/presentation/CaseDetailScreen.kt +++ b/watch/wearos/app/src/main/java/com/doctate/watch/presentation/CaseDetailScreen.kt @@ -34,6 +34,7 @@ import androidx.wear.compose.material3.ScreenScaffold import androidx.wear.compose.material3.Text import androidx.wear.input.RemoteInputIntentHelper import com.doctate.watch.DoctateApp +import com.doctate.watch.audio.PendingStore import com.doctate.watch.domain.OnelinerState import com.doctate.watch.domain.displayText import com.doctate.watch.domain.isManual @@ -79,7 +80,7 @@ fun CaseDetailScreen( // Optimistic local update — UI flips to Manual immediately. app.caseStore.setOnelinerLocal( caseId, - OnelinerState.Manual(text, setAt = Instant.now().toString()), + OnelinerState.Manual(text, setAt = PendingStore.nowRfc3339()), ) // Push the edit to the server. Fire-and-forget: the local // state stays even if the network is down. Edits before diff --git a/watch/wearos/app/src/main/java/com/doctate/watch/presentation/RecordingViewModel.kt b/watch/wearos/app/src/main/java/com/doctate/watch/presentation/RecordingViewModel.kt index e144ee5..2e75670 100644 --- a/watch/wearos/app/src/main/java/com/doctate/watch/presentation/RecordingViewModel.kt +++ b/watch/wearos/app/src/main/java/com/doctate/watch/presentation/RecordingViewModel.kt @@ -14,7 +14,6 @@ import com.doctate.watch.audio.PendingUpload import com.doctate.watch.domain.CaseStore import com.doctate.watch.sync.SyncKick import com.doctate.watch.sync.SyncTrigger -import java.time.Instant import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -98,7 +97,7 @@ class RecordingViewModel( private fun startRecordingFlow() { finalizationStarted.set(false) recordingJob = viewModelScope.launch { - val recordedAt = Instant.now().toString() + val recordedAt = PendingStore.nowRfc3339() val target = pendingStore.audioPathFor(caseId, recordedAt) try { withContext(Dispatchers.IO) { recorder.start(target) } @@ -140,7 +139,7 @@ class RecordingViewModel( dispatch(RecordingEvent.UploadFailed("Stop failed: ${e.message}", transient = false)) return } - val recordedAt = pendingRecordedAt ?: Instant.now().toString() + val recordedAt = pendingRecordedAt ?: PendingStore.nowRfc3339() pendingRecordedAt = null try { withContext(Dispatchers.IO) { diff --git a/watch/wearos/app/src/test/java/com/doctate/watch/audio/PendingStoreTest.kt b/watch/wearos/app/src/test/java/com/doctate/watch/audio/PendingStoreTest.kt index 8becbbb..2d82a2f 100644 --- a/watch/wearos/app/src/test/java/com/doctate/watch/audio/PendingStoreTest.kt +++ b/watch/wearos/app/src/test/java/com/doctate/watch/audio/PendingStoreTest.kt @@ -100,4 +100,17 @@ class PendingStoreTest { assertThat(found.recordedAt).isEqualTo("2026-04-26T10:00:00Z") assertThat(found.file.absolutePath).isEqualTo(audio.absolutePath) } + + /** Regression guard: subsecond precision must be stripped. A single + * microsecond leak would feed the server filename parser + * `YYYY-MM-DDTHH-MM-SS.NNNNNNZ.m4a`, which it treats as malformed — + * the resulting `