feat: implement sync notifications for Obsidian plugin

- Added functionality to create and update notifications during the Obsidian sync process.
- Improved handling of sync completion and failure notifications.
- Updated connector naming convention in various locations for consistency.
This commit is contained in:
Anish Sarkar 2026-04-22 06:38:51 +05:30
parent 3b7f27cff9
commit 4a75603d4f
4 changed files with 134 additions and 8 deletions

View file

@ -41,6 +41,7 @@ from app.schemas.obsidian_plugin import (
SyncAckItem, SyncAckItem,
SyncBatchRequest, SyncBatchRequest,
) )
from app.services.notification_service import NotificationService
from app.services.obsidian_plugin_indexer import ( from app.services.obsidian_plugin_indexer import (
delete_note, delete_note,
get_manifest, get_manifest,
@ -68,6 +69,103 @@ def _build_handshake() -> dict[str, object]:
return {"capabilities": list(OBSIDIAN_CAPABILITIES)} 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( async def _resolve_vault_connector(
session: AsyncSession, session: AsyncSession,
*, *,
@ -188,7 +286,7 @@ def _build_config(
def _display_name(vault_name: str) -> str: def _display_name(vault_name: str) -> str:
return f"Obsidian \u2014 {vault_name}" return f"Obsidian - {vault_name}"
@router.post("/connect", response_model=ConnectResponse) @router.post("/connect", response_model=ConnectResponse)
@ -335,6 +433,18 @@ async def obsidian_sync(
connector = await _resolve_vault_connector( connector = await _resolve_vault_connector(
session, user=user, vault_id=payload.vault_id 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] = [] items: list[SyncAckItem] = []
indexed = 0 indexed = 0
@ -362,6 +472,22 @@ async def obsidian_sync(
SyncAckItem(path=note.path, status="error", error=str(exc)[:300]) 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( return SyncAck(
vault_id=payload.vault_id, vault_id=payload.vault_id,
indexed=indexed, indexed=indexed,

View file

@ -183,7 +183,7 @@ class TestConnectRace:
async with AsyncSession(async_engine) as s: async with AsyncSession(async_engine) as s:
s.add( s.add(
SearchSourceConnector( SearchSourceConnector(
name="Obsidian \u2014 First", name="Obsidian - First",
connector_type=SearchSourceConnectorType.OBSIDIAN_CONNECTOR, connector_type=SearchSourceConnectorType.OBSIDIAN_CONNECTOR,
is_indexable=False, is_indexable=False,
config={ config={
@ -202,7 +202,7 @@ class TestConnectRace:
async with AsyncSession(async_engine) as s: async with AsyncSession(async_engine) as s:
s.add( s.add(
SearchSourceConnector( SearchSourceConnector(
name="Obsidian \u2014 Second", name="Obsidian - Second",
connector_type=SearchSourceConnectorType.OBSIDIAN_CONNECTOR, connector_type=SearchSourceConnectorType.OBSIDIAN_CONNECTOR,
is_indexable=False, is_indexable=False,
config={ config={
@ -228,7 +228,7 @@ class TestConnectRace:
async with AsyncSession(async_engine) as s: async with AsyncSession(async_engine) as s:
s.add( s.add(
SearchSourceConnector( SearchSourceConnector(
name="Obsidian \u2014 Desktop", name="Obsidian - Desktop",
connector_type=SearchSourceConnectorType.OBSIDIAN_CONNECTOR, connector_type=SearchSourceConnectorType.OBSIDIAN_CONNECTOR,
is_indexable=False, is_indexable=False,
config={ config={
@ -247,7 +247,7 @@ class TestConnectRace:
async with AsyncSession(async_engine) as s: async with AsyncSession(async_engine) as s:
s.add( s.add(
SearchSourceConnector( SearchSourceConnector(
name="Obsidian \u2014 Mobile", name="Obsidian - Mobile",
connector_type=SearchSourceConnectorType.OBSIDIAN_CONNECTOR, connector_type=SearchSourceConnectorType.OBSIDIAN_CONNECTOR,
is_indexable=False, is_indexable=False,
config={ config={

View file

@ -180,7 +180,7 @@ export const OTHER_CONNECTORS = [
{ {
id: "obsidian-connector", id: "obsidian-connector",
title: "Obsidian", 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, connectorType: EnumConnectorName.OBSIDIAN_CONNECTOR,
}, },
] as const; ] as const;

View file

@ -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. 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. 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. 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 ## 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. 1. Delete the old legacy Obsidian connector from SurfSense.
2. Install and configure the SurfSense Obsidian plugin using the quick start above. 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.
<Callout type="warn"> <Callout type="warn">
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. 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.