From 70eb182eb4eea5236f335f2858def4b72958892e Mon Sep 17 00:00:00 2001 From: iamdoubz <> Date: Sat, 11 Jul 2026 22:37:51 -0500 Subject: [PATCH] feat(sync): auto-resync artifacts on edit (notes/summary/transcript/tags/action items) (FR-SYNC-5) --- src-tauri/src/commands.rs | 41 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index e65952f..effc5ea 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1732,6 +1732,7 @@ pub async fn reprocess_transcript( template.as_ref(), ); let _ = state.store.update_notes(&meeting_id, ¬es_md).await; + spawn_auto_sync(&app, state.store.clone(), meeting_id.clone()); let _ = app.emit( "transcript://finalized", @@ -2044,6 +2045,7 @@ pub async fn list_meetings( /// Replaces a meeting's complete tag set (Phase 8, FR-SEARCH-2). #[tauri::command] pub async fn set_tags( + app: AppHandle, state: State<'_, AppState>, meeting_id: MeetingId, tags: Vec, @@ -2052,7 +2054,9 @@ pub async fn set_tags( .store .set_tags(&meeting_id, &tags) .await - .map_err(|e| WaError::new("storage", e.to_string())) + .map_err(|e| WaError::new("storage", e.to_string()))?; + spawn_auto_sync(&app, state.store.clone(), meeting_id); + Ok(()) } /// All known tag names, sorted, for filter/autocomplete UI (Phase 8, FR-SEARCH-2). @@ -2250,6 +2254,7 @@ pub(crate) fn cleanup_playback_temp() { #[tauri::command] pub async fn update_notes( + app: AppHandle, state: State<'_, AppState>, meeting_id: MeetingId, markdown: String, @@ -2258,7 +2263,9 @@ pub async fn update_notes( .store .update_notes(&meeting_id, &markdown) .await - .map_err(|e| WaError::new("storage", e.to_string())) + .map_err(|e| WaError::new("storage", e.to_string()))?; + spawn_auto_sync(&app, state.store.clone(), meeting_id); + Ok(()) } /// `format` ∈ `md | pdf | docx | bundle`. `dest` is a file path for @@ -2710,6 +2717,7 @@ pub async fn generate_summary( if let Err(e) = state.store.reindex_fts(&meeting_id).await { tracing::warn!("failed to reindex search after summary generation: {e}"); } + spawn_auto_sync(&app, state.store.clone(), meeting_id.clone()); let _ = app.emit( "llm://done", @@ -2723,6 +2731,7 @@ pub async fn generate_summary( /// T8.6, FR-CAL-5). #[tauri::command] pub async fn confirm_action_items( + app: AppHandle, state: State<'_, AppState>, meeting_id: MeetingId, items: Vec, @@ -2758,6 +2767,7 @@ pub async fn confirm_action_items( _ => crate::reminders::cancel(id), } } + spawn_auto_sync(&app, state.store.clone(), meeting_id); Ok(()) } @@ -3689,6 +3699,33 @@ pub(crate) async fn enqueue_meeting_sync( Ok(queued) } +/// Re-sync a meeting's artifacts after an edit (notes / summary / transcript / +/// tags / action items), mirroring sync-on-finalize (T9.5, FR-SYNC-5). A no-op +/// unless sync is enabled; only finalize-trigger targets are touched and +/// unchanged files are deduped by SHA-256, so a save that didn't alter an +/// uploaded artifact queues (and uploads) nothing. Fire-and-forget so the edit +/// command returns immediately. +/// +/// ponytail: fires on every (debounced) save; if WebDAV PUT volume from live +/// note-typing ever matters, coarsen the debounce or trigger on blur instead. +pub(crate) fn spawn_auto_sync( + app: &AppHandle, + store: std::sync::Arc, + meeting_id: MeetingId, +) { + if !load_settings().sync_enabled { + return; + } + let app = app.clone(); + tauri::async_runtime::spawn(async move { + match enqueue_meeting_sync(store.as_ref(), &meeting_id, None, true).await { + Ok(0) => {} // nothing changed + Ok(_) => pump_sync(&app, store.as_ref()).await, // upload the changes + Err(e) => tracing::warn!("auto-sync enqueue failed: {e:?}"), + } + }); +} + /// Upload one job's file: ensure the remote dir, PUT, report bytes sent. async fn upload_job( target: &dyn crate::sync::SyncTarget,