mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-08 20:25:19 +02:00
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.
This commit is contained in:
parent
937965b335
commit
02795e08e3
6 changed files with 101 additions and 25 deletions
|
|
@ -348,6 +348,7 @@ async def obsidian_connect(
|
|||
connector_id=collision.id,
|
||||
vault_id=collision_cfg["vault_id"],
|
||||
search_space_id=collision.search_space_id,
|
||||
server_time_utc=datetime.now(UTC),
|
||||
**_build_handshake(),
|
||||
)
|
||||
await session.commit()
|
||||
|
|
@ -361,6 +362,7 @@ async def obsidian_connect(
|
|||
connector_id=existing_by_vid.id,
|
||||
vault_id=payload.vault_id,
|
||||
search_space_id=existing_by_vid.search_space_id,
|
||||
server_time_utc=datetime.now(UTC),
|
||||
**_build_handshake(),
|
||||
)
|
||||
await session.commit()
|
||||
|
|
@ -379,6 +381,7 @@ async def obsidian_connect(
|
|||
connector_id=existing_by_fp.id,
|
||||
vault_id=survivor_cfg["vault_id"],
|
||||
search_space_id=existing_by_fp.search_space_id,
|
||||
server_time_utc=datetime.now(UTC),
|
||||
**_build_handshake(),
|
||||
)
|
||||
await session.commit()
|
||||
|
|
@ -410,6 +413,7 @@ async def obsidian_connect(
|
|||
connector_id=inserted.id,
|
||||
vault_id=payload.vault_id,
|
||||
search_space_id=inserted.search_space_id,
|
||||
server_time_utc=datetime.now(UTC),
|
||||
**_build_handshake(),
|
||||
)
|
||||
await session.commit()
|
||||
|
|
@ -431,6 +435,7 @@ async def obsidian_connect(
|
|||
connector_id=winner.id,
|
||||
vault_id=(winner.config or {})["vault_id"],
|
||||
search_space_id=winner.search_space_id,
|
||||
server_time_utc=datetime.now(UTC),
|
||||
**_build_handshake(),
|
||||
)
|
||||
await session.commit()
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ class ConnectResponse(_PluginBase):
|
|||
vault_id: str
|
||||
search_space_id: int
|
||||
capabilities: list[str]
|
||||
server_time_utc: datetime
|
||||
|
||||
|
||||
class HealthResponse(_PluginBase):
|
||||
|
|
|
|||
|
|
@ -343,6 +343,7 @@ class TestWireContractSmoke:
|
|||
assert connect_resp.connector_id > 0
|
||||
assert connect_resp.vault_id == vault_id
|
||||
assert "sync" in connect_resp.capabilities
|
||||
assert connect_resp.server_time_utc is not None
|
||||
|
||||
# 2. /sync — stub the indexer so the call doesn't drag the LLM /
|
||||
# embedding pipeline in. We're testing the wire contract, not the
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ export default class SurfSensePlugin extends Plugin {
|
|||
});
|
||||
|
||||
const onNetChange = () => {
|
||||
void this.engine.recoverConnectivityStatus();
|
||||
if (this.shouldAutoSync()) void this.engine.flushQueue();
|
||||
};
|
||||
this.registerDomEvent(window, "online", onNetChange);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
type App,
|
||||
type ButtonComponent,
|
||||
Notice,
|
||||
Platform,
|
||||
PluginSettingTab,
|
||||
|
|
@ -58,6 +59,11 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
}),
|
||||
);
|
||||
|
||||
let verifyButton: ButtonComponent | null = null;
|
||||
const updateVerifyDisabled = (): void => {
|
||||
verifyButton?.setDisabled(this.plugin.settings.apiToken.trim().length === 0);
|
||||
};
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("API token")
|
||||
.setDesc(
|
||||
|
|
@ -78,29 +84,33 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
this.plugin.settings.connectorId = null;
|
||||
}
|
||||
this.plugin.settings.apiToken = next;
|
||||
updateVerifyDisabled();
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.api.resetAuthBlock();
|
||||
});
|
||||
})
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Verify")
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
btn.setDisabled(true);
|
||||
try {
|
||||
await this.plugin.api.verifyToken();
|
||||
new Notice("Surfsense: token verified.");
|
||||
this.plugin.engine.refreshStatus({ force: true });
|
||||
await this.refreshSearchSpaces();
|
||||
this.display();
|
||||
} catch (err) {
|
||||
this.handleApiError(err);
|
||||
} finally {
|
||||
btn.setDisabled(false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
.addButton((btn) => {
|
||||
verifyButton = btn;
|
||||
updateVerifyDisabled();
|
||||
btn.setButtonText("Verify").setCta().onClick(async () => {
|
||||
if (this.plugin.settings.apiToken.trim().length === 0) {
|
||||
new Notice("Surfsense: paste an API token before verifying.");
|
||||
return;
|
||||
}
|
||||
btn.setDisabled(true);
|
||||
try {
|
||||
await this.plugin.api.verifyToken();
|
||||
new Notice("Surfsense: token verified.");
|
||||
this.plugin.engine.refreshStatus({ force: true });
|
||||
await this.refreshSearchSpaces();
|
||||
this.display();
|
||||
} catch (err) {
|
||||
this.handleApiError(err);
|
||||
} finally {
|
||||
updateVerifyDisabled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Search space")
|
||||
|
|
@ -233,12 +243,10 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
}),
|
||||
);
|
||||
|
||||
if (Platform.isMobileApp) {
|
||||
if (Platform.isAndroidApp) {
|
||||
new Setting(containerEl)
|
||||
.setName("Sync only on WiFi")
|
||||
.setDesc(
|
||||
"Pause automatic syncing on cellular. Note: only Android can detect network type, on iOS this toggle has no effect.",
|
||||
)
|
||||
.setDesc("Pause automatic syncing on cellular.")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(settings.wifiOnly)
|
||||
|
|
@ -367,7 +375,13 @@ export class SurfSenseSettingTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
private handleApiError(err: unknown): void {
|
||||
if (err instanceof AuthError) return;
|
||||
if (err instanceof AuthError) {
|
||||
if (err.message.startsWith("Missing API token")) {
|
||||
new Notice("Surfsense: paste an API token before verifying.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.plugin.engine.reportError(err);
|
||||
new Notice(
|
||||
`SurfSense: request failed — ${(err as Error).message ?? "unknown error"}`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -239,7 +239,10 @@ export class SyncEngine {
|
|||
// ---- queue draining ---------------------------------------------------
|
||||
|
||||
async flushQueue(): Promise<void> {
|
||||
if (this.deps.queue.size === 0) return;
|
||||
if (this.deps.queue.size === 0) {
|
||||
await this.recoverStatusIfNeeded();
|
||||
return;
|
||||
}
|
||||
// Shared gate for every flush trigger so the first /sync can't race /connect.
|
||||
if (!this.deps.getSettings().connectorId) {
|
||||
const connected = await this.ensureConnected();
|
||||
|
|
@ -259,6 +262,31 @@ export class SyncEngine {
|
|||
this.setStatus(this.queueStatusKind(), this.statusDetail());
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight status recovery path used after network-change signals.
|
||||
* Clears stale offline/auth/error only when connectivity/auth is explicitly re-validated.
|
||||
*/
|
||||
async recoverConnectivityStatus(): Promise<void> {
|
||||
const settings = this.deps.getSettings();
|
||||
if (!settings.apiToken) {
|
||||
this.refreshStatus({ force: true });
|
||||
return;
|
||||
}
|
||||
if (!settings.searchSpaceId) {
|
||||
try {
|
||||
const health = await this.deps.apiClient.health();
|
||||
this.applyHealth(health);
|
||||
this.refreshStatus({ force: true });
|
||||
} catch (err) {
|
||||
this.handleStartupError(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const connected = await this.ensureConnected();
|
||||
if (!connected) return;
|
||||
this.refreshStatus({ force: true });
|
||||
}
|
||||
|
||||
private async processBatch(batch: QueueItem[]): Promise<BatchResult> {
|
||||
const settings = this.deps.getSettings();
|
||||
const upserts = batch.filter((b): b is QueueItem & { op: "upsert" } => b.op === "upsert");
|
||||
|
|
@ -510,6 +538,7 @@ export class SyncEngine {
|
|||
refreshStatus(opts: { force?: boolean } = {}): void {
|
||||
if (!opts.force) {
|
||||
const last = this.lastAppliedKind;
|
||||
if (last === "syncing") return;
|
||||
const isError =
|
||||
last === "auth-error" || last === "offline" || last === "error";
|
||||
const s = this.deps.getSettings();
|
||||
|
|
@ -523,6 +552,18 @@ export class SyncEngine {
|
|||
this.setStatus("auth-error", message ?? "API token expired or invalid");
|
||||
}
|
||||
|
||||
reportError(err: unknown): void {
|
||||
if (err instanceof AuthError) {
|
||||
this.reportAuthError(err.message);
|
||||
return;
|
||||
}
|
||||
if (err instanceof TransientError) {
|
||||
this.setStatus("offline", err.message);
|
||||
return;
|
||||
}
|
||||
this.setStatus("error", (err as Error).message ?? "Unknown error");
|
||||
}
|
||||
|
||||
private setStatus(kind: StatusKind, detail?: string): void {
|
||||
const s = this.deps.getSettings();
|
||||
if (!s.apiToken) {
|
||||
|
|
@ -601,6 +642,19 @@ export class SyncEngine {
|
|||
this.setStatus(this.queueStatusKind(), `${prefix}: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
private async recoverStatusIfNeeded(): Promise<void> {
|
||||
if (!this.isRecoverableErrorState()) return;
|
||||
await this.recoverConnectivityStatus();
|
||||
}
|
||||
|
||||
private isRecoverableErrorState(): boolean {
|
||||
return (
|
||||
this.lastAppliedKind === "offline" ||
|
||||
this.lastAppliedKind === "auth-error" ||
|
||||
this.lastAppliedKind === "error"
|
||||
);
|
||||
}
|
||||
|
||||
// ---- predicates -------------------------------------------------------
|
||||
|
||||
private shouldTrack(file: TAbstractFile): boolean {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue