4141 SyncAckItem ,
4242 SyncBatchRequest ,
4343)
44+ from app .services .notification_service import NotificationService
4445from app .services .obsidian_plugin_indexer import (
4546 delete_note ,
4647 get_manifest ,
@@ -68,6 +69,105 @@ def _build_handshake() -> dict[str, object]:
6869 return {"capabilities" : list (OBSIDIAN_CAPABILITIES )}
6970
7071
72+ def _connector_type_value (connector : SearchSourceConnector ) -> str :
73+ connector_type = connector .connector_type
74+ if hasattr (connector_type , "value" ):
75+ return str (connector_type .value )
76+ return str (connector_type )
77+
78+
79+ async def _start_obsidian_sync_notification (
80+ session : AsyncSession ,
81+ * ,
82+ user : User ,
83+ connector : SearchSourceConnector ,
84+ total_count : int ,
85+ ):
86+ """Create/update the rolling inbox item for Obsidian plugin sync.
87+
88+ Obsidian sync is continuous and batched, so we keep one stable
89+ operation_id per connector instead of creating a new notification per batch.
90+ """
91+ handler = NotificationService .connector_indexing
92+ operation_id = f"obsidian_sync_connector_{ connector .id } "
93+ connector_name = connector .name or "Obsidian"
94+ notification = await handler .find_or_create_notification (
95+ session = session ,
96+ user_id = user .id ,
97+ operation_id = operation_id ,
98+ title = f"Syncing: { connector_name } " ,
99+ message = "Syncing from Obsidian plugin" ,
100+ search_space_id = connector .search_space_id ,
101+ initial_metadata = {
102+ "connector_id" : connector .id ,
103+ "connector_name" : connector_name ,
104+ "connector_type" : _connector_type_value (connector ),
105+ "sync_stage" : "processing" ,
106+ "indexed_count" : 0 ,
107+ "failed_count" : 0 ,
108+ "total_count" : total_count ,
109+ "source" : "obsidian_plugin" ,
110+ },
111+ )
112+ return await handler .update_notification (
113+ session = session ,
114+ notification = notification ,
115+ status = "in_progress" ,
116+ metadata_updates = {
117+ "sync_stage" : "processing" ,
118+ "total_count" : total_count ,
119+ },
120+ )
121+
122+
123+ async def _finish_obsidian_sync_notification (
124+ session : AsyncSession ,
125+ * ,
126+ notification ,
127+ indexed : int ,
128+ failed : int ,
129+ ):
130+ """Mark the rolling Obsidian sync inbox item complete or failed."""
131+ handler = NotificationService .connector_indexing
132+ connector_name = notification .notification_metadata .get (
133+ "connector_name" , "Obsidian"
134+ )
135+ if failed > 0 and indexed == 0 :
136+ title = f"Failed: { connector_name } "
137+ message = (
138+ f"Sync failed: { failed } file(s) failed"
139+ if failed > 1
140+ else "Sync failed: 1 file failed"
141+ )
142+ status_value = "failed"
143+ stage = "failed"
144+ else :
145+ title = f"Ready: { connector_name } "
146+ if failed > 0 :
147+ message = f"Partially synced: { indexed } file(s) synced, { failed } failed."
148+ elif indexed == 0 :
149+ message = "Already up to date!"
150+ elif indexed == 1 :
151+ message = "Now searchable! 1 file synced."
152+ else :
153+ message = f"Now searchable! { indexed } files synced."
154+ status_value = "completed"
155+ stage = "completed"
156+
157+ await handler .update_notification (
158+ session = session ,
159+ notification = notification ,
160+ title = title ,
161+ message = message ,
162+ status = status_value ,
163+ metadata_updates = {
164+ "indexed_count" : indexed ,
165+ "failed_count" : failed ,
166+ "sync_stage" : stage ,
167+ },
168+ )
169+
170+
71171async def _resolve_vault_connector (
72172 session : AsyncSession ,
73173 * ,
@@ -175,9 +275,7 @@ async def _find_by_fingerprint(
175275 return (await session .execute (stmt )).scalars ().first ()
176276
177277
178- def _build_config (
179- payload : ConnectRequest , * , now_iso : str
180- ) -> dict [str , object ]:
278+ def _build_config (payload : ConnectRequest , * , now_iso : str ) -> dict [str , object ]:
181279 return {
182280 "vault_id" : payload .vault_id ,
183281 "vault_name" : payload .vault_name ,
@@ -188,7 +286,7 @@ def _build_config(
188286
189287
190288def _display_name (vault_name : str ) -> str :
191- return f"Obsidian \u2014 { vault_name } "
289+ return f"Obsidian - { vault_name } "
192290
193291
194292@router .post ("/connect" , response_model = ConnectResponse )
@@ -335,6 +433,18 @@ async def obsidian_sync(
335433 connector = await _resolve_vault_connector (
336434 session , user = user , vault_id = payload .vault_id
337435 )
436+ notification = None
437+ try :
438+ notification = await _start_obsidian_sync_notification (
439+ session , user = user , connector = connector , total_count = len (payload .notes )
440+ )
441+ except Exception :
442+ logger .warning (
443+ "obsidian sync notification start failed connector=%s user=%s" ,
444+ connector .id ,
445+ user .id ,
446+ exc_info = True ,
447+ )
338448
339449 items : list [SyncAckItem ] = []
340450 indexed = 0
@@ -346,9 +456,7 @@ async def obsidian_sync(
346456 session , connector = connector , payload = note , user_id = str (user .id )
347457 )
348458 indexed += 1
349- items .append (
350- SyncAckItem (path = note .path , status = "ok" , document_id = doc .id )
351- )
459+ items .append (SyncAckItem (path = note .path , status = "ok" , document_id = doc .id ))
352460 except HTTPException :
353461 raise
354462 except Exception as exc :
@@ -362,6 +470,22 @@ async def obsidian_sync(
362470 SyncAckItem (path = note .path , status = "error" , error = str (exc )[:300 ])
363471 )
364472
473+ if notification is not None :
474+ try :
475+ await _finish_obsidian_sync_notification (
476+ session ,
477+ notification = notification ,
478+ indexed = indexed ,
479+ failed = failed ,
480+ )
481+ except Exception :
482+ logger .warning (
483+ "obsidian sync notification finish failed connector=%s user=%s" ,
484+ connector .id ,
485+ user .id ,
486+ exc_info = True ,
487+ )
488+
365489 return SyncAck (
366490 vault_id = payload .vault_id ,
367491 indexed = indexed ,
@@ -471,9 +595,7 @@ async def obsidian_delete_notes(
471595 path ,
472596 payload .vault_id ,
473597 )
474- items .append (
475- DeleteAckItem (path = path , status = "error" , error = str (exc )[:300 ])
476- )
598+ items .append (DeleteAckItem (path = path , status = "error" , error = str (exc )[:300 ]))
477599
478600 return DeleteAck (
479601 vault_id = payload .vault_id ,
@@ -490,9 +612,7 @@ async def obsidian_manifest(
490612 session : AsyncSession = Depends (get_async_session ),
491613) -> ManifestResponse :
492614 """Return ``{path: {hash, mtime}}`` for the plugin's onload reconcile diff."""
493- connector = await _resolve_vault_connector (
494- session , user = user , vault_id = vault_id
495- )
615+ connector = await _resolve_vault_connector (session , user = user , vault_id = vault_id )
496616 return await get_manifest (session , connector = connector , vault_id = vault_id )
497617
498618
@@ -507,9 +627,7 @@ async def obsidian_stats(
507627 ``files_synced`` excludes tombstones so it matches ``/manifest``;
508628 ``last_sync_at`` includes them so deletes advance the freshness signal.
509629 """
510- connector = await _resolve_vault_connector (
511- session , user = user , vault_id = vault_id
512- )
630+ connector = await _resolve_vault_connector (session , user = user , vault_id = vault_id )
513631
514632 is_active = Document .document_metadata ["deleted_at" ].as_string ().is_ (None )
515633
0 commit comments