Skip to content

Commit 02795e0

Browse files
committed
feat: add server time to obsidian connect responses and enhance error handling
- Included server_time_utc in the connect response schema for better synchronization. - Updated obsidian_connect function to set server_time_utc during connection handling. - Enhanced integration tests to verify the presence of server_time_utc in responses. - Improved connectivity status recovery in the sync engine for better error management.
1 parent 937965b commit 02795e0

6 files changed

Lines changed: 101 additions & 25 deletions

File tree

surfsense_backend/app/routes/obsidian_plugin_routes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ async def obsidian_connect(
348348
connector_id=collision.id,
349349
vault_id=collision_cfg["vault_id"],
350350
search_space_id=collision.search_space_id,
351+
server_time_utc=datetime.now(UTC),
351352
**_build_handshake(),
352353
)
353354
await session.commit()
@@ -361,6 +362,7 @@ async def obsidian_connect(
361362
connector_id=existing_by_vid.id,
362363
vault_id=payload.vault_id,
363364
search_space_id=existing_by_vid.search_space_id,
365+
server_time_utc=datetime.now(UTC),
364366
**_build_handshake(),
365367
)
366368
await session.commit()
@@ -379,6 +381,7 @@ async def obsidian_connect(
379381
connector_id=existing_by_fp.id,
380382
vault_id=survivor_cfg["vault_id"],
381383
search_space_id=existing_by_fp.search_space_id,
384+
server_time_utc=datetime.now(UTC),
382385
**_build_handshake(),
383386
)
384387
await session.commit()
@@ -410,6 +413,7 @@ async def obsidian_connect(
410413
connector_id=inserted.id,
411414
vault_id=payload.vault_id,
412415
search_space_id=inserted.search_space_id,
416+
server_time_utc=datetime.now(UTC),
413417
**_build_handshake(),
414418
)
415419
await session.commit()
@@ -431,6 +435,7 @@ async def obsidian_connect(
431435
connector_id=winner.id,
432436
vault_id=(winner.config or {})["vault_id"],
433437
search_space_id=winner.search_space_id,
438+
server_time_utc=datetime.now(UTC),
434439
**_build_handshake(),
435440
)
436441
await session.commit()

surfsense_backend/app/schemas/obsidian_plugin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ class ConnectResponse(_PluginBase):
169169
vault_id: str
170170
search_space_id: int
171171
capabilities: list[str]
172+
server_time_utc: datetime
172173

173174

174175
class HealthResponse(_PluginBase):

surfsense_backend/tests/integration/test_obsidian_plugin_routes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ async def test_full_flow_returns_typed_payloads(
343343
assert connect_resp.connector_id > 0
344344
assert connect_resp.vault_id == vault_id
345345
assert "sync" in connect_resp.capabilities
346+
assert connect_resp.server_time_utc is not None
346347

347348
# 2. /sync — stub the indexer so the call doesn't drag the LLM /
348349
# embedding pipeline in. We're testing the wire contract, not the

surfsense_obsidian/src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export default class SurfSensePlugin extends Plugin {
149149
});
150150

151151
const onNetChange = () => {
152+
void this.engine.recoverConnectivityStatus();
152153
if (this.shouldAutoSync()) void this.engine.flushQueue();
153154
};
154155
this.registerDomEvent(window, "online", onNetChange);

surfsense_obsidian/src/settings.ts

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
type App,
3+
type ButtonComponent,
34
Notice,
45
Platform,
56
PluginSettingTab,
@@ -58,6 +59,11 @@ export class SurfSenseSettingTab extends PluginSettingTab {
5859
}),
5960
);
6061

62+
let verifyButton: ButtonComponent | null = null;
63+
const updateVerifyDisabled = (): void => {
64+
verifyButton?.setDisabled(this.plugin.settings.apiToken.trim().length === 0);
65+
};
66+
6167
new Setting(containerEl)
6268
.setName("API token")
6369
.setDesc(
@@ -78,29 +84,33 @@ export class SurfSenseSettingTab extends PluginSettingTab {
7884
this.plugin.settings.connectorId = null;
7985
}
8086
this.plugin.settings.apiToken = next;
87+
updateVerifyDisabled();
8188
await this.plugin.saveSettings();
8289
this.plugin.api.resetAuthBlock();
8390
});
8491
})
85-
.addButton((btn) =>
86-
btn
87-
.setButtonText("Verify")
88-
.setCta()
89-
.onClick(async () => {
90-
btn.setDisabled(true);
91-
try {
92-
await this.plugin.api.verifyToken();
93-
new Notice("Surfsense: token verified.");
94-
this.plugin.engine.refreshStatus({ force: true });
95-
await this.refreshSearchSpaces();
96-
this.display();
97-
} catch (err) {
98-
this.handleApiError(err);
99-
} finally {
100-
btn.setDisabled(false);
101-
}
102-
}),
103-
);
92+
.addButton((btn) => {
93+
verifyButton = btn;
94+
updateVerifyDisabled();
95+
btn.setButtonText("Verify").setCta().onClick(async () => {
96+
if (this.plugin.settings.apiToken.trim().length === 0) {
97+
new Notice("Surfsense: paste an API token before verifying.");
98+
return;
99+
}
100+
btn.setDisabled(true);
101+
try {
102+
await this.plugin.api.verifyToken();
103+
new Notice("Surfsense: token verified.");
104+
this.plugin.engine.refreshStatus({ force: true });
105+
await this.refreshSearchSpaces();
106+
this.display();
107+
} catch (err) {
108+
this.handleApiError(err);
109+
} finally {
110+
updateVerifyDisabled();
111+
}
112+
});
113+
});
104114

105115
new Setting(containerEl)
106116
.setName("Search space")
@@ -233,12 +243,10 @@ export class SurfSenseSettingTab extends PluginSettingTab {
233243
}),
234244
);
235245

236-
if (Platform.isMobileApp) {
246+
if (Platform.isAndroidApp) {
237247
new Setting(containerEl)
238248
.setName("Sync only on WiFi")
239-
.setDesc(
240-
"Pause automatic syncing on cellular. Note: only Android can detect network type, on iOS this toggle has no effect.",
241-
)
249+
.setDesc("Pause automatic syncing on cellular.")
242250
.addToggle((toggle) =>
243251
toggle
244252
.setValue(settings.wifiOnly)
@@ -367,7 +375,13 @@ export class SurfSenseSettingTab extends PluginSettingTab {
367375
}
368376

369377
private handleApiError(err: unknown): void {
370-
if (err instanceof AuthError) return;
378+
if (err instanceof AuthError) {
379+
if (err.message.startsWith("Missing API token")) {
380+
new Notice("Surfsense: paste an API token before verifying.");
381+
}
382+
return;
383+
}
384+
this.plugin.engine.reportError(err);
371385
new Notice(
372386
`SurfSense: request failed — ${(err as Error).message ?? "unknown error"}`,
373387
);

surfsense_obsidian/src/sync-engine.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,10 @@ export class SyncEngine {
239239
// ---- queue draining ---------------------------------------------------
240240

241241
async flushQueue(): Promise<void> {
242-
if (this.deps.queue.size === 0) return;
242+
if (this.deps.queue.size === 0) {
243+
await this.recoverStatusIfNeeded();
244+
return;
245+
}
243246
// Shared gate for every flush trigger so the first /sync can't race /connect.
244247
if (!this.deps.getSettings().connectorId) {
245248
const connected = await this.ensureConnected();
@@ -259,6 +262,31 @@ export class SyncEngine {
259262
this.setStatus(this.queueStatusKind(), this.statusDetail());
260263
}
261264

265+
/**
266+
* Lightweight status recovery path used after network-change signals.
267+
* Clears stale offline/auth/error only when connectivity/auth is explicitly re-validated.
268+
*/
269+
async recoverConnectivityStatus(): Promise<void> {
270+
const settings = this.deps.getSettings();
271+
if (!settings.apiToken) {
272+
this.refreshStatus({ force: true });
273+
return;
274+
}
275+
if (!settings.searchSpaceId) {
276+
try {
277+
const health = await this.deps.apiClient.health();
278+
this.applyHealth(health);
279+
this.refreshStatus({ force: true });
280+
} catch (err) {
281+
this.handleStartupError(err);
282+
}
283+
return;
284+
}
285+
const connected = await this.ensureConnected();
286+
if (!connected) return;
287+
this.refreshStatus({ force: true });
288+
}
289+
262290
private async processBatch(batch: QueueItem[]): Promise<BatchResult> {
263291
const settings = this.deps.getSettings();
264292
const upserts = batch.filter((b): b is QueueItem & { op: "upsert" } => b.op === "upsert");
@@ -510,6 +538,7 @@ export class SyncEngine {
510538
refreshStatus(opts: { force?: boolean } = {}): void {
511539
if (!opts.force) {
512540
const last = this.lastAppliedKind;
541+
if (last === "syncing") return;
513542
const isError =
514543
last === "auth-error" || last === "offline" || last === "error";
515544
const s = this.deps.getSettings();
@@ -523,6 +552,18 @@ export class SyncEngine {
523552
this.setStatus("auth-error", message ?? "API token expired or invalid");
524553
}
525554

555+
reportError(err: unknown): void {
556+
if (err instanceof AuthError) {
557+
this.reportAuthError(err.message);
558+
return;
559+
}
560+
if (err instanceof TransientError) {
561+
this.setStatus("offline", err.message);
562+
return;
563+
}
564+
this.setStatus("error", (err as Error).message ?? "Unknown error");
565+
}
566+
526567
private setStatus(kind: StatusKind, detail?: string): void {
527568
const s = this.deps.getSettings();
528569
if (!s.apiToken) {
@@ -601,6 +642,19 @@ export class SyncEngine {
601642
this.setStatus(this.queueStatusKind(), `${prefix}: ${(err as Error).message}`);
602643
}
603644

645+
private async recoverStatusIfNeeded(): Promise<void> {
646+
if (!this.isRecoverableErrorState()) return;
647+
await this.recoverConnectivityStatus();
648+
}
649+
650+
private isRecoverableErrorState(): boolean {
651+
return (
652+
this.lastAppliedKind === "offline" ||
653+
this.lastAppliedKind === "auth-error" ||
654+
this.lastAppliedKind === "error"
655+
);
656+
}
657+
604658
// ---- predicates -------------------------------------------------------
605659

606660
private shouldTrack(file: TAbstractFile): boolean {

0 commit comments

Comments
 (0)