feat: improve status handling and user feedback in SurfSense plugin

- Added refreshStatus method to update connection status immediately after settings changes.
- Enhanced error reporting with reportAuthError method for better authentication feedback.
- Updated status visuals in the status modal to reflect the new centralized structure.
- Improved connection handling in the settings tab to ensure accurate status representation.
This commit is contained in:
Anish Sarkar 2026-04-25 03:24:17 +05:30
parent 4f1c870987
commit 937965b335
5 changed files with 30 additions and 18 deletions

View file

@ -183,6 +183,7 @@ export default class SurfSensePlugin extends Plugin {
this.settings.vaultId !== previousVaultId || this.settings.vaultId !== previousVaultId ||
this.settings.connectorId !== previousConnectorId; this.settings.connectorId !== previousConnectorId;
if (!changed) return; if (!changed) return;
this.engine?.refreshStatus();
this.notifyStatusChange(); this.notifyStatusChange();
if (this.settings.searchSpaceId !== null) { if (this.settings.searchSpaceId !== null) {
void this.engine.ensureConnected(); void this.engine.ensureConnected();
@ -242,6 +243,7 @@ export default class SurfSensePlugin extends Plugin {
} }
private notifyAuthError(): void { private notifyAuthError(): void {
this.engine?.reportAuthError();
const now = Date.now(); const now = Date.now();
if (now - this.lastAuthToastAt < 10_000) return; if (now - this.lastAuthToastAt < 10_000) return;
this.lastAuthToastAt = now; this.lastAuthToastAt = now;
@ -265,8 +267,6 @@ export default class SurfSensePlugin extends Plugin {
async saveSettings() { async saveSettings() {
await this.saveData(this.settings); await this.saveData(this.settings);
// Ensures the indicator reacts to settings edits (token paste, search-space pick)
// without waiting for the next sync trigger.
this.engine?.refreshStatus(); this.engine?.refreshStatus();
} }

View file

@ -91,6 +91,7 @@ export class SurfSenseSettingTab extends PluginSettingTab {
try { try {
await this.plugin.api.verifyToken(); await this.plugin.api.verifyToken();
new Notice("Surfsense: token verified."); new Notice("Surfsense: token verified.");
this.plugin.engine.refreshStatus({ force: true });
await this.refreshSearchSpaces(); await this.refreshSearchSpaces();
this.display(); this.display();
} catch (err) { } catch (err) {

View file

@ -1,5 +1,6 @@
import { type App, Modal, Notice, Setting } from "obsidian"; import { type App, Modal, Notice, Setting } from "obsidian";
import type SurfSensePlugin from "./main"; import type SurfSensePlugin from "./main";
import { STATUS_VISUALS } from "./status-visuals";
/** Live status panel reachable from the status bar / command palette. */ /** Live status panel reachable from the status bar / command palette. */
export class StatusModal extends Modal { export class StatusModal extends Modal {
@ -28,7 +29,7 @@ export class StatusModal extends Modal {
const s = plugin.settings; const s = plugin.settings;
const rows: Array<[string, string]> = [ const rows: Array<[string, string]> = [
["Status", plugin.lastStatus.kind], ["Status", STATUS_VISUALS[plugin.lastStatus.kind].label],
[ [
"Last sync", "Last sync",
s.lastSyncAt ? new Date(s.lastSyncAt).toLocaleString() : "—", s.lastSyncAt ? new Date(s.lastSyncAt).toLocaleString() : "—",

View file

@ -1,11 +1,6 @@
import type { StatusKind } from "./types"; import type { StatusKind } from "./types";
/** /** Shared by the status bar and the settings "Connection" heading. */
* Single source of truth for status icons + labels. Both the status bar
* and the settings "Connection" heading render from this table so a change
* here updates both surfaces.
*/
export interface StatusVisual { export interface StatusVisual {
icon: string; icon: string;
label: string; label: string;

View file

@ -71,6 +71,7 @@ export class SyncEngine {
private idleReconcileStreak = 0; private idleReconcileStreak = 0;
/** 2^streak is capped at this value (e.g. 8 → max ×8 backoff). */ /** 2^streak is capped at this value (e.g. 8 → max ×8 backoff). */
private readonly maxBackoffMultiplier = 8; private readonly maxBackoffMultiplier = 8;
private lastAppliedKind: StatusKind = "needs-setup";
constructor(deps: SyncEngineDeps) { constructor(deps: SyncEngineDeps) {
this.deps = deps; this.deps = deps;
@ -502,24 +503,38 @@ export class SyncEngine {
// ---- status helpers --------------------------------------------------- // ---- status helpers ---------------------------------------------------
/** /**
* Recomputes status from settings + queue depth. Call from main.ts after * Conservative by default: real errors are preserved while setup is
* settings change so the indicator reacts to token paste / search-space * complete, so unrelated edits don't optimistically clear the indicator.
* pick without waiting for the next sync trigger. * Pass `force: true` after an explicit verify/reconcile confirmation.
*/ */
refreshStatus(): void { refreshStatus(opts: { force?: boolean } = {}): void {
if (!opts.force) {
const last = this.lastAppliedKind;
const isError =
last === "auth-error" || last === "offline" || last === "error";
const s = this.deps.getSettings();
const setupComplete = !!(s.apiToken && s.searchSpaceId && s.connectorId);
if (isError && setupComplete) return;
}
this.setStatus(this.queueStatusKind(), this.statusDetail()); this.setStatus(this.queueStatusKind(), this.statusDetail());
} }
reportAuthError(message?: string): void {
this.setStatus("auth-error", message ?? "API token expired or invalid");
}
private setStatus(kind: StatusKind, detail?: string): void { private setStatus(kind: StatusKind, detail?: string): void {
// Errors carry meaningful signal; only "happy" kinds get downgraded
// to needs-setup when prerequisites are missing.
if (kind !== "auth-error" && kind !== "offline" && kind !== "error") {
const s = this.deps.getSettings(); const s = this.deps.getSettings();
if (!s.apiToken || !s.searchSpaceId || !s.connectorId) { if (!s.apiToken) {
kind = "needs-setup";
detail = this.setupHint(s);
} else if (kind !== "auth-error" && kind !== "offline" && kind !== "error") {
if (!s.searchSpaceId || !s.connectorId) {
kind = "needs-setup"; kind = "needs-setup";
detail = this.setupHint(s); detail = this.setupHint(s);
} }
} }
this.lastAppliedKind = kind;
this.deps.setStatus({ kind, detail, queueDepth: this.deps.queue.size }); this.deps.setStatus({ kind, detail, queueDepth: this.deps.queue.size });
} }