diff --git a/surfsense_backend/app/services/obsidian_plugin_indexer.py b/surfsense_backend/app/services/obsidian_plugin_indexer.py index f024d36ed..8fbdad269 100644 --- a/surfsense_backend/app/services/obsidian_plugin_indexer.py +++ b/surfsense_backend/app/services/obsidian_plugin_indexer.py @@ -159,7 +159,7 @@ async def _extract_binary_attachment_markdown( logger.warning("obsidian attachment payload had invalid base64: %s", payload.path) return "", {"attachment_extraction_status": "invalid_binary_payload"} - suffix = f".{payload.extension.lstrip('.')}" if payload.extension else "" + suffix = f".{payload.extension.lstrip('.')}" temp_path: str | None = None filename = payload.path.rsplit("/", 1)[-1] or payload.name try: diff --git a/surfsense_obsidian/src/api-client.ts b/surfsense_obsidian/src/api-client.ts index 5c75100f5..2cf81da0f 100644 --- a/surfsense_obsidian/src/api-client.ts +++ b/surfsense_obsidian/src/api-client.ts @@ -1,4 +1,4 @@ -import { Notice, requestUrl, type RequestUrlParam, type RequestUrlResponse } from "obsidian"; +import { requestUrl, type RequestUrlParam, type RequestUrlResponse } from "obsidian"; import type { ConnectResponse, DeleteAck, @@ -72,8 +72,11 @@ export interface ApiClientOptions { onAuthError?: () => void; } +const AUTH_BLOCK_MS = 60_000; + export class SurfSenseApiClient { private readonly opts: ApiClientOptions; + private authBlockedUntil = 0; constructor(opts: ApiClientOptions) { this.opts = opts; @@ -83,6 +86,10 @@ export class SurfSenseApiClient { Object.assign(this.opts, partial); } + resetAuthBlock(): void { + this.authBlockedUntil = 0; + } + async health(): Promise { return await this.request("GET", "/api/v1/obsidian/health"); } @@ -198,6 +205,9 @@ export class SurfSenseApiClient { if (!token) { throw new AuthError("Missing API token. Open SurfSense settings to paste one."); } + if (Date.now() < this.authBlockedUntil) { + throw new AuthError("Token rejected. Paste a fresh one in settings."); + } const headers: Record = { Authorization: `Bearer ${token}`, Accept: "application/json", @@ -224,8 +234,8 @@ export class SurfSenseApiClient { const detail = extractDetail(resp); if (resp.status === 401) { + this.authBlockedUntil = Date.now() + AUTH_BLOCK_MS; this.opts.onAuthError?.(); - new Notice("Surfsense: token expired or invalid. Paste a fresh token in settings."); throw new AuthError(detail || "Unauthorized"); } diff --git a/surfsense_obsidian/src/main.ts b/surfsense_obsidian/src/main.ts index 22f316986..e9dda860b 100644 --- a/surfsense_obsidian/src/main.ts +++ b/surfsense_obsidian/src/main.ts @@ -25,6 +25,7 @@ export default class SurfSensePlugin extends Plugin { private settingTab: SurfSenseSettingTab | null = null; private statusListeners = new Set<() => void>(); private reconcileTimerId: number | null = null; + private lastAuthToastAt = 0; async onload() { await this.loadSettings(); @@ -34,6 +35,7 @@ export default class SurfSensePlugin extends Plugin { this.api = new SurfSenseApiClient({ getServerUrl: () => this.settings.serverUrl, getToken: () => this.settings.apiToken, + onAuthError: () => this.notifyAuthError(), }); this.queue = new PersistentQueue(this.settings.queue ?? [], { @@ -239,6 +241,13 @@ export default class SurfSensePlugin extends Plugin { for (const fn of this.statusListeners) fn(); } + private notifyAuthError(): void { + const now = Date.now(); + if (now - this.lastAuthToastAt < 10_000) return; + this.lastAuthToastAt = now; + new Notice("Surfsense: API token expired or invalid. Paste a fresh token in settings.", 8000); + } + async loadSettings() { const data = (await this.loadData()) as Partial | null; this.settings = { diff --git a/surfsense_obsidian/src/settings.ts b/surfsense_obsidian/src/settings.ts index eb1e5b9f5..212667283 100644 --- a/surfsense_obsidian/src/settings.ts +++ b/surfsense_obsidian/src/settings.ts @@ -75,6 +75,7 @@ export class SurfSenseSettingTab extends PluginSettingTab { } this.plugin.settings.apiToken = next; await this.plugin.saveSettings(); + this.plugin.api.resetAuthBlock(); }); }) .addButton((btn) => @@ -299,7 +300,7 @@ export class SurfSenseSettingTab extends PluginSettingTab { const kind = this.plugin.lastStatus.kind; if (kind === "auth-error") { - return { icon: "lock", label: "Token invalid or expired", tone: "err" }; + return { icon: "lock", label: "API token invalid or expired", tone: "err" }; } if (kind === "error") { return { icon: "alert-circle", label: "Connection error", tone: "err" }; @@ -381,10 +382,7 @@ export class SurfSenseSettingTab extends PluginSettingTab { } private handleApiError(err: unknown): void { - if (err instanceof AuthError) { - new Notice(`SurfSense: ${err.message}`); - return; - } + if (err instanceof AuthError) return; new Notice( `SurfSense: request failed — ${(err as Error).message ?? "unknown error"}`, ); diff --git a/surfsense_obsidian/src/status-bar.ts b/surfsense_obsidian/src/status-bar.ts index ec470978f..9ba85318b 100644 --- a/surfsense_obsidian/src/status-bar.ts +++ b/surfsense_obsidian/src/status-bar.ts @@ -15,11 +15,11 @@ interface StatusVisual { } const VISUALS: Record = { - idle: { icon: "check-circle", label: "Synced", cls: "surfsense-status--ok" }, - syncing: { icon: "refresh-ccw", label: "Syncing", cls: "surfsense-status--syncing" }, - queued: { icon: "upload", label: "Queued", cls: "surfsense-status--syncing" }, - offline: { icon: "wifi-off", label: "Offline", cls: "surfsense-status--warn" }, - "auth-error": { icon: "lock", label: "Auth error", cls: "surfsense-status--err" }, + idle: { icon: "check-circle", label: "Synced", cls: "" }, + syncing: { icon: "refresh-ccw", label: "Syncing", cls: "" }, + queued: { icon: "clock", label: "Queued", cls: "" }, + offline: { icon: "wifi-off", label: "Offline", cls: "" }, + "auth-error": { icon: "user-x", label: "Auth error", cls: "surfsense-status--err" }, error: { icon: "alert-circle", label: "Error", cls: "surfsense-status--err" }, }; @@ -42,13 +42,8 @@ export class StatusBar { update(state: StatusState): void { const visual = VISUALS[state.kind]; - this.el.removeClass( - "surfsense-status--ok", - "surfsense-status--syncing", - "surfsense-status--warn", - "surfsense-status--err", - ); - this.el.addClass(visual.cls); + this.el.removeClass("surfsense-status--err"); + if (visual.cls) this.el.addClass(visual.cls); setIcon(this.icon, visual.icon); let label = `SurfSense: ${visual.label}`; diff --git a/surfsense_obsidian/src/sync-engine.ts b/surfsense_obsidian/src/sync-engine.ts index fafb8135b..5ef41b633 100644 --- a/surfsense_obsidian/src/sync-engine.ts +++ b/surfsense_obsidian/src/sync-engine.ts @@ -120,11 +120,11 @@ export class SyncEngine { * (Re)register the vault. Adopts server's `vault_id` in case fingerprint * dedup routed us to an existing row from another device. */ - async ensureConnected(): Promise { + async ensureConnected(): Promise { const settings = this.deps.getSettings(); if (!settings.searchSpaceId) { this.setStatus("idle", "Pick a search space in settings."); - return; + return false; } this.setStatus("syncing", "Connecting to SurfSense"); try { @@ -141,8 +141,10 @@ export class SyncEngine { s.connectorId = resp.connector_id; }); this.setStatus(this.queueStatusKind(), this.statusDetail()); + return true; } catch (err) { this.handleStartupError(err); + return false; } } @@ -238,7 +240,8 @@ export class SyncEngine { if (this.deps.queue.size === 0) return; // Shared gate for every flush trigger so the first /sync can't race /connect. if (!this.deps.getSettings().connectorId) { - await this.ensureConnected(); + const connected = await this.ensureConnected(); + if (!connected) return; if (!this.deps.getSettings().connectorId) return; } this.setStatus("syncing", `Syncing ${this.deps.queue.size} item(s)…`); @@ -409,7 +412,8 @@ export class SyncEngine { // Re-handshake first: if the vault grew enough to match another // device's fingerprint, the server merges and routes us to the // survivor row, which the /manifest call below then uses. - await this.ensureConnected(); + const connected = await this.ensureConnected(); + if (!connected) return; const refreshed = this.deps.getSettings(); if (!refreshed.connectorId) return; @@ -552,7 +556,8 @@ export class SyncEngine { } private classifyAndStatus(err: unknown, prefix: string): void { - this.classify(err); + const verdict = this.classify(err); + if (verdict === "stop") return; this.setStatus(this.queueStatusKind(), `${prefix}: ${(err as Error).message}`); } diff --git a/surfsense_obsidian/styles.css b/surfsense_obsidian/styles.css index 3d1ec6ab8..f31590224 100644 --- a/surfsense_obsidian/styles.css +++ b/surfsense_obsidian/styles.css @@ -22,18 +22,6 @@ height: 14px; } -.surfsense-status--ok .surfsense-status__icon { - color: var(--color-green); -} - -.surfsense-status--syncing .surfsense-status__icon { - color: var(--color-blue); -} - -.surfsense-status--warn .surfsense-status__icon { - color: var(--color-yellow); -} - .surfsense-status--err .surfsense-status__icon { color: var(--color-red); } @@ -55,18 +43,6 @@ height: 14px; } -.surfsense-connection-indicator--ok { - color: var(--color-green); -} - -.surfsense-connection-indicator--syncing { - color: var(--color-blue); -} - -.surfsense-connection-indicator--warn { - color: var(--color-yellow); -} - .surfsense-connection-indicator--err { color: var(--color-red); }