mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
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:
parent
3b7f27cff9
commit
4a75603d4f
4 changed files with 134 additions and 8 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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={
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue