Refactor timestamp handling for activity sorting
Introduce `last_recording_at` to `OnelinerEntry` and use it as the primary sort key for cases. This ensures that cases with recent dictation activity are prioritized, even if their initial creation date is older. The logic for determining a case's activity has been updated to consider the most recent `.m4a` file's modification time (`last_recording_at`), falling back to `updated_at` or `created_at` if necessary. This change also refactors the timestamp formatting and handling within the web interface to correctly display and sort cases based on their actual last activity time, improving user experience and data relevance. The `now_rfc3339` function is updated to strip sub-second precision for consistent filename generation.
This commit is contained in:
@@ -144,6 +144,12 @@ impl CaseStore {
|
||||
/// snapshot: insert (with `synced_to_server = true`) or update the
|
||||
/// local marker (oneliner + last_activity_at from server timestamps,
|
||||
/// taking the newer of local vs. server). Never deletes.
|
||||
///
|
||||
/// The `last_activity_at` seed from the server prefers
|
||||
/// `last_recording_at` (most recent m4a mtime — the authoritative
|
||||
/// "when was this case last worked on?") over `updated_at` (oneliner
|
||||
/// regeneration time, which collapses to the recovery-scan moment
|
||||
/// after a restart). Falls back to `created_at` if neither is set.
|
||||
pub async fn merge_server_snapshot(
|
||||
&self,
|
||||
snapshot: &OnelinersResponse,
|
||||
@@ -155,8 +161,9 @@ impl CaseStore {
|
||||
continue;
|
||||
};
|
||||
let server_activity = entry
|
||||
.updated_at
|
||||
.last_recording_at
|
||||
.clone()
|
||||
.or_else(|| entry.updated_at.clone())
|
||||
.unwrap_or_else(|| entry.created_at.clone());
|
||||
|
||||
let updated_marker = match state.get(&case_id) {
|
||||
@@ -308,6 +315,7 @@ mod tests {
|
||||
case_id: case_id.to_string(),
|
||||
oneliner: oneliner.map(str::to_owned),
|
||||
created_at: created.to_owned(),
|
||||
last_recording_at: Some(created.to_owned()),
|
||||
updated_at: Some(created.to_owned()),
|
||||
}
|
||||
}
|
||||
@@ -486,6 +494,58 @@ mod tests {
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
|
||||
/// The client must seed `last_activity_at` from `last_recording_at`
|
||||
/// (the authoritative dictation-activity timestamp), not from
|
||||
/// `updated_at` (oneliner regeneration time, which collapses to a
|
||||
/// narrow window after a startup recovery scan and breaks ordering).
|
||||
#[tokio::test]
|
||||
async fn merge_prefers_last_recording_over_updated_at() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let (store, _rx) = CaseStore::open(tmp.path().to_owned()).await.unwrap();
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
// Server: case created yesterday, last dictation 30 min ago,
|
||||
// oneliner regenerated just now in a recovery scan.
|
||||
let entry = OnelinerEntry {
|
||||
case_id: id.to_string(),
|
||||
oneliner: Some("x".into()),
|
||||
created_at: "2026-04-17T09:00:00Z".into(),
|
||||
last_recording_at: Some("2026-04-18T09:30:00Z".into()),
|
||||
updated_at: Some("2026-04-18T10:00:00Z".into()),
|
||||
};
|
||||
store
|
||||
.merge_server_snapshot(&server_snapshot(vec![entry]))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let list = store.list().await;
|
||||
assert_eq!(list[0].last_activity_at, "2026-04-18T09:30:00Z");
|
||||
}
|
||||
|
||||
/// Backward-compat: if the server response omits `last_recording_at`
|
||||
/// (older server or future schema shift), the client falls back to
|
||||
/// `updated_at` then `created_at`.
|
||||
#[tokio::test]
|
||||
async fn merge_falls_back_to_updated_at_when_last_recording_missing() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let (store, _rx) = CaseStore::open(tmp.path().to_owned()).await.unwrap();
|
||||
let id = Uuid::new_v4();
|
||||
let entry = OnelinerEntry {
|
||||
case_id: id.to_string(),
|
||||
oneliner: Some("x".into()),
|
||||
created_at: "2026-04-17T09:00:00Z".into(),
|
||||
last_recording_at: None,
|
||||
updated_at: Some("2026-04-18T10:00:00Z".into()),
|
||||
};
|
||||
store
|
||||
.merge_server_snapshot(&server_snapshot(vec![entry]))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let list = store.list().await;
|
||||
assert_eq!(list[0].last_activity_at, "2026-04-18T10:00:00Z");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sorted_newest_first() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
@@ -282,6 +282,7 @@ mod tests {
|
||||
case_id: case_id.into(),
|
||||
oneliner: oneliner.map(str::to_owned),
|
||||
created_at: "2026-04-18T09:30:00Z".into(),
|
||||
last_recording_at: Some("2026-04-18T09:45:00Z".into()),
|
||||
updated_at: Some("2026-04-18T09:45:00Z".into()),
|
||||
}],
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ mod tests {
|
||||
case_id: "550e8400-e29b-41d4-a716-446655440000".into(),
|
||||
oneliner: Some("Kniegelenk".into()),
|
||||
created_at: "2026-04-18T10:00:00Z".into(),
|
||||
last_recording_at: Some("2026-04-18T10:30:00Z".into()),
|
||||
updated_at: Some("2026-04-18T10:30:00Z".into()),
|
||||
}],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user