feat: update status handling and connection indicator in SurfSense plugin

- Changed initial status to "needs-setup" to prompt user configuration.
- Added a refreshStatus method to SyncEngine for immediate status updates after settings changes.
- Enhanced connection indicator logic in the settings tab to reflect the new status.
- Introduced "needs-setup" status visual for improved user feedback on configuration requirements.
This commit is contained in:
Anish Sarkar 2026-04-25 02:16:38 +05:30
parent 68156d2e74
commit 4f1c870987
5 changed files with 51 additions and 5 deletions

View file

@ -20,7 +20,7 @@ export default class SurfSensePlugin extends Plugin {
queue!: PersistentQueue; queue!: PersistentQueue;
engine!: SyncEngine; engine!: SyncEngine;
private statusBar: StatusBar | null = null; private statusBar: StatusBar | null = null;
lastStatus: StatusState = { kind: "idle", queueDepth: 0 }; lastStatus: StatusState = { kind: "needs-setup", queueDepth: 0 };
serverCapabilities: string[] = []; serverCapabilities: string[] = [];
private settingTab: SurfSenseSettingTab | null = null; private settingTab: SurfSenseSettingTab | null = null;
private statusListeners = new Set<() => void>(); private statusListeners = new Set<() => void>();
@ -265,6 +265,9 @@ 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();
} }
/** /**

View file

@ -20,6 +20,8 @@ export class SurfSenseSettingTab extends PluginSettingTab {
private readonly plugin: SurfSensePlugin; private readonly plugin: SurfSensePlugin;
private searchSpaces: SearchSpace[] = []; private searchSpaces: SearchSpace[] = [];
private loadingSpaces = false; private loadingSpaces = false;
private connectionIndicator: HTMLElement | null = null;
private readonly onStatusChange = (): void => this.updateConnectionIndicator();
constructor(app: App, plugin: SurfSensePlugin) { constructor(app: App, plugin: SurfSensePlugin) {
super(app, plugin); super(app, plugin);
@ -29,6 +31,7 @@ export class SurfSenseSettingTab extends PluginSettingTab {
display(): void { display(): void {
const { containerEl } = this; const { containerEl } = this;
containerEl.empty(); containerEl.empty();
this.plugin.onStatusChange(this.onStatusChange);
const settings = this.plugin.settings; const settings = this.plugin.settings;
@ -279,13 +282,26 @@ export class SurfSenseSettingTab extends PluginSettingTab {
); );
} }
hide(): void {
this.plugin.offStatusChange(this.onStatusChange);
this.connectionIndicator = null;
}
private renderConnectionHeading(containerEl: HTMLElement): void { private renderConnectionHeading(containerEl: HTMLElement): void {
const heading = new Setting(containerEl).setName("Connection").setHeading(); const heading = new Setting(containerEl).setName("Connection").setHeading();
heading.nameEl.addClass("surfsense-connection-heading"); heading.nameEl.addClass("surfsense-connection-heading");
const indicator = heading.nameEl.createSpan({ this.connectionIndicator = heading.nameEl.createSpan({
cls: "surfsense-connection-indicator", cls: "surfsense-connection-indicator",
}); });
this.updateConnectionIndicator();
}
private updateConnectionIndicator(): void {
const indicator = this.connectionIndicator;
if (!indicator) return;
const visual = STATUS_VISUALS[this.plugin.lastStatus.kind]; const visual = STATUS_VISUALS[this.plugin.lastStatus.kind];
indicator.empty();
indicator.removeClass("surfsense-connection-indicator--err");
if (visual.isError) { if (visual.isError) {
indicator.addClass("surfsense-connection-indicator--err"); indicator.addClass("surfsense-connection-indicator--err");
} }

View file

@ -16,7 +16,8 @@ export const STATUS_VISUALS: Record<StatusKind, StatusVisual> = {
idle: { icon: "check-circle", label: "Synced", isError: false }, idle: { icon: "check-circle", label: "Synced", isError: false },
syncing: { icon: "refresh-ccw", label: "Syncing", isError: false }, syncing: { icon: "refresh-ccw", label: "Syncing", isError: false },
queued: { icon: "clock", label: "Queued", isError: false }, queued: { icon: "clock", label: "Queued", isError: false },
"needs-setup": { icon: "cloud-off", label: "Setup required", isError: false },
offline: { icon: "wifi-off", label: "Offline", isError: false }, offline: { icon: "wifi-off", label: "Offline", isError: false },
"auth-error": { icon: "user-x", label: "Reauthenticate", isError: true }, "auth-error": { icon: "alert-circle", label: "Reauthenticate", isError: true },
error: { icon: "alert-circle", label: "Error", isError: true }, error: { icon: "alert-circle", label: "Error", isError: true },
}; };

View file

@ -46,6 +46,7 @@ export interface SyncEngineDeps {
export interface SyncEngineSettings { export interface SyncEngineSettings {
vaultId: string; vaultId: string;
apiToken: string;
connectorId: number | null; connectorId: number | null;
searchSpaceId: number | null; searchSpaceId: number | null;
includeFolders: string[]; includeFolders: string[];
@ -103,7 +104,7 @@ export class SyncEngine {
this.handleStartupError(err); this.handleStartupError(err);
return; return;
} }
this.setStatus("idle", "Pick a search space in settings to start syncing."); this.setStatus("idle");
return; return;
} }
@ -123,7 +124,7 @@ export class SyncEngine {
async ensureConnected(): Promise<boolean> { async ensureConnected(): Promise<boolean> {
const settings = this.deps.getSettings(); const settings = this.deps.getSettings();
if (!settings.searchSpaceId) { if (!settings.searchSpaceId) {
this.setStatus("idle", "Pick a search space in settings."); this.setStatus("idle");
return false; return false;
} }
this.setStatus("syncing", "Connecting to SurfSense"); this.setStatus("syncing", "Connecting to SurfSense");
@ -500,10 +501,34 @@ export class SyncEngine {
// ---- status helpers --------------------------------------------------- // ---- status helpers ---------------------------------------------------
/**
* Recomputes status from settings + queue depth. Call from main.ts after
* settings change so the indicator reacts to token paste / search-space
* pick without waiting for the next sync trigger.
*/
refreshStatus(): void {
this.setStatus(this.queueStatusKind(), this.statusDetail());
}
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();
if (!s.apiToken || !s.searchSpaceId || !s.connectorId) {
kind = "needs-setup";
detail = this.setupHint(s);
}
}
this.deps.setStatus({ kind, detail, queueDepth: this.deps.queue.size }); this.deps.setStatus({ kind, detail, queueDepth: this.deps.queue.size });
} }
private setupHint(s: SyncEngineSettings): string {
if (!s.apiToken) return "Paste your API token in settings.";
if (!s.searchSpaceId) return "Pick a search space in settings.";
return "Connecting…";
}
private queueStatusKind(): StatusKind { private queueStatusKind(): StatusKind {
if (this.deps.queue.size > 0) return "queued"; if (this.deps.queue.size > 0) return "queued";
return "idle"; return "idle";

View file

@ -190,6 +190,7 @@ export type StatusKind =
| "idle" | "idle"
| "syncing" | "syncing"
| "queued" | "queued"
| "needs-setup"
| "offline" | "offline"
| "auth-error" | "auth-error"
| "error"; | "error";