diff --git a/surfsense_backend/app/routes/obsidian_plugin_routes.py b/surfsense_backend/app/routes/obsidian_plugin_routes.py index 08e0f7d50..096058d8a 100644 --- a/surfsense_backend/app/routes/obsidian_plugin_routes.py +++ b/surfsense_backend/app/routes/obsidian_plugin_routes.py @@ -41,6 +41,7 @@ from app.schemas.obsidian_plugin import ( SyncAckItem, SyncBatchRequest, ) +from app.services.notification_service import NotificationService from app.services.obsidian_plugin_indexer import ( delete_note, get_manifest, @@ -68,6 +69,103 @@ def _build_handshake() -> dict[str, object]: return {"capabilities": list(OBSIDIAN_CAPABILITIES)} +def _connector_type_value(connector: SearchSourceConnector) -> str: + connector_type = connector.connector_type + if hasattr(connector_type, "value"): + return str(connector_type.value) + return str(connector_type) + + +async def _start_obsidian_sync_notification( + session: AsyncSession, + *, + user: User, + connector: SearchSourceConnector, + total_count: int, +): + """Create/update the rolling inbox item for Obsidian plugin sync. + + Obsidian sync is continuous and batched, so we keep one stable + operation_id per connector instead of creating a new notification per batch. + """ + handler = NotificationService.connector_indexing + operation_id = f"obsidian_sync_connector_{connector.id}" + connector_name = connector.name or "Obsidian" + notification = await handler.find_or_create_notification( + session=session, + user_id=user.id, + operation_id=operation_id, + title=f"Syncing: {connector_name}", + message="Syncing from Obsidian plugin", + search_space_id=connector.search_space_id, + initial_metadata={ + "connector_id": connector.id, + "connector_name": connector_name, + "connector_type": _connector_type_value(connector), + "sync_stage": "processing", + "indexed_count": 0, + "failed_count": 0, + "total_count": total_count, + "source": "obsidian_plugin", + }, + ) + return await handler.update_notification( + session=session, + notification=notification, + status="in_progress", + metadata_updates={ + "sync_stage": "processing", + "total_count": total_count, + }, + ) + + +async def _finish_obsidian_sync_notification( + session: AsyncSession, + *, + notification, + indexed: int, + failed: int, +): + """Mark the rolling Obsidian sync inbox item complete or failed.""" + handler = NotificationService.connector_indexing + connector_name = notification.notification_metadata.get("connector_name", "Obsidian") + if failed > 0 and indexed == 0: + title = f"Failed: {connector_name}" + message = ( + f"Sync failed: {failed} file(s) failed" + if failed > 1 + else "Sync failed: 1 file failed" + ) + status_value = "failed" + stage = "failed" + else: + title = f"Ready: {connector_name}" + if failed > 0: + message = f"Partially synced: {indexed} file(s) synced, {failed} failed." + elif indexed == 0: + message = "Already up to date!" + elif indexed == 1: + message = "Now searchable! 1 file synced." + else: + message = f"Now searchable! {indexed} files synced." + status_value = "completed" + stage = "completed" + + await handler.update_notification( + session=session, + notification=notification, + title=title, + message=message, + status=status_value, + metadata_updates={ + "indexed_count": indexed, + "failed_count": failed, + "sync_stage": stage, + }, + ) + + async def _resolve_vault_connector( session: AsyncSession, *, @@ -188,7 +286,7 @@ def _build_config( def _display_name(vault_name: str) -> str: - return f"Obsidian \u2014 {vault_name}" + return f"Obsidian - {vault_name}" @router.post("/connect", response_model=ConnectResponse) @@ -335,6 +433,18 @@ async def obsidian_sync( connector = await _resolve_vault_connector( session, user=user, vault_id=payload.vault_id ) + notification = None + try: + notification = await _start_obsidian_sync_notification( + session, user=user, connector=connector, total_count=len(payload.notes) + ) + except Exception: + logger.warning( + "obsidian sync notification start failed connector=%s user=%s", + connector.id, + user.id, + exc_info=True, + ) items: list[SyncAckItem] = [] indexed = 0 @@ -362,6 +472,22 @@ async def obsidian_sync( SyncAckItem(path=note.path, status="error", error=str(exc)[:300]) ) + if notification is not None: + try: + await _finish_obsidian_sync_notification( + session, + notification=notification, + indexed=indexed, + failed=failed, + ) + except Exception: + logger.warning( + "obsidian sync notification finish failed connector=%s user=%s", + connector.id, + user.id, + exc_info=True, + ) + return SyncAck( vault_id=payload.vault_id, indexed=indexed, diff --git a/surfsense_backend/tests/integration/test_obsidian_plugin_routes.py b/surfsense_backend/tests/integration/test_obsidian_plugin_routes.py index 0ddb9d713..449e1473d 100644 --- a/surfsense_backend/tests/integration/test_obsidian_plugin_routes.py +++ b/surfsense_backend/tests/integration/test_obsidian_plugin_routes.py @@ -183,7 +183,7 @@ class TestConnectRace: async with AsyncSession(async_engine) as s: s.add( SearchSourceConnector( - name="Obsidian \u2014 First", + name="Obsidian - First", connector_type=SearchSourceConnectorType.OBSIDIAN_CONNECTOR, is_indexable=False, config={ @@ -202,7 +202,7 @@ class TestConnectRace: async with AsyncSession(async_engine) as s: s.add( SearchSourceConnector( - name="Obsidian \u2014 Second", + name="Obsidian - Second", connector_type=SearchSourceConnectorType.OBSIDIAN_CONNECTOR, is_indexable=False, config={ @@ -228,7 +228,7 @@ class TestConnectRace: async with AsyncSession(async_engine) as s: s.add( SearchSourceConnector( - name="Obsidian \u2014 Desktop", + name="Obsidian - Desktop", connector_type=SearchSourceConnectorType.OBSIDIAN_CONNECTOR, is_indexable=False, config={ @@ -247,7 +247,7 @@ class TestConnectRace: async with AsyncSession(async_engine) as s: s.add( SearchSourceConnector( - name="Obsidian \u2014 Mobile", + name="Obsidian - Mobile", connector_type=SearchSourceConnectorType.OBSIDIAN_CONNECTOR, is_indexable=False, config={ diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts index c8d63f309..c897489ff 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts @@ -180,7 +180,7 @@ export const OTHER_CONNECTORS = [ { id: "obsidian-connector", title: "Obsidian", - description: "Sync your Obsidian vault on desktop or mobile via the SurfSense plugin", + description: "Sync your Obsidian vault on desktop or mobile", connectorType: EnumConnectorName.OBSIDIAN_CONNECTOR, }, ] as const; diff --git a/surfsense_web/content/docs/connectors/obsidian.mdx b/surfsense_web/content/docs/connectors/obsidian.mdx index 1efa4ff8f..5f939e277 100644 --- a/surfsense_web/content/docs/connectors/obsidian.mdx +++ b/surfsense_web/content/docs/connectors/obsidian.mdx @@ -30,7 +30,7 @@ This works for cloud and self-hosted deployments, including desktop and mobile c 4. Paste your SurfSense API token from the user settings section. 5. Paste your Server URL in the plugin setting: either your SurfSense main domain (if `/api/v1` rewrites are enabled) or your direct backend URL. 6. Choose the Search Space in the plugin, then the first sync should run automatically. -7. Confirm the connector appears as **Obsidian — <vault>** in SurfSense. +7. Confirm the connector appears as **Obsidian - <vault>** in SurfSense. ## Migrating from the legacy connector @@ -38,7 +38,7 @@ If you previously used the legacy Obsidian connector architecture, migrate to th 1. Delete the old legacy Obsidian connector from SurfSense. 2. Install and configure the SurfSense Obsidian plugin using the quick start above. -3. Run the first plugin sync and verify the new **Obsidian — <vault>** connector is active. +3. Run the first plugin sync and verify the new **Obsidian - <vault>** connector is active. Deleting the legacy connector also deletes all documents that were indexed by that connector. Always finish and verify plugin sync before deleting the old connector.