feat: enhance Obsidian plugin with folder management features, including inclusion/exclusion settings and a status modal for real-time updates

This commit is contained in:
Anish Sarkar 2026-04-20 23:13:49 +05:30
parent 2251e464c7
commit 28d3c628f1
11 changed files with 267 additions and 154 deletions

View file

@ -1,8 +1,8 @@
"""Obsidian plugin ingestion routes (``/api/v1/obsidian/*``). """Obsidian plugin ingestion routes (``/api/v1/obsidian/*``).
Wire surface for the ``surfsense_obsidian/`` plugin. API stability is the Wire surface for the ``surfsense_obsidian/`` plugin. Versioning anchor is
``/api/v1/`` prefix plus the additive ``capabilities`` array on /health; the ``/api/v1/`` URL prefix; additive feature detection rides the
no plugin-version gate. ``capabilities`` array on /health and /connect.
""" """
from __future__ import annotations from __future__ import annotations
@ -46,9 +46,6 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/obsidian", tags=["obsidian-plugin"]) router = APIRouter(prefix="/obsidian", tags=["obsidian-plugin"])
# Bumped only on non-additive wire changes; additive ones ride extra='ignore'.
OBSIDIAN_API_VERSION = "1"
# Plugins feature-gate on these. Add entries, never rename or remove. # Plugins feature-gate on these. Add entries, never rename or remove.
OBSIDIAN_CAPABILITIES: list[str] = ["sync", "rename", "delete", "manifest", "stats"] OBSIDIAN_CAPABILITIES: list[str] = ["sync", "rename", "delete", "manifest", "stats"]
@ -59,10 +56,7 @@ OBSIDIAN_CAPABILITIES: list[str] = ["sync", "rename", "delete", "manifest", "sta
def _build_handshake() -> dict[str, object]: def _build_handshake() -> dict[str, object]:
return { return {"capabilities": list(OBSIDIAN_CAPABILITIES)}
"api_version": OBSIDIAN_API_VERSION,
"capabilities": list(OBSIDIAN_CAPABILITIES),
}
async def _resolve_vault_connector( async def _resolve_vault_connector(

View file

@ -92,13 +92,11 @@ class ConnectResponse(_PluginBase):
connector_id: int connector_id: int
vault_id: str vault_id: str
search_space_id: int search_space_id: int
api_version: str
capabilities: list[str] capabilities: list[str]
class HealthResponse(_PluginBase): class HealthResponse(_PluginBase):
"""API contract handshake. ``capabilities`` is additive-only string list.""" """API contract handshake. ``capabilities`` is additive-only string list."""
api_version: str
capabilities: list[str] capabilities: list[str]
server_time_utc: datetime server_time_utc: datetime

View file

@ -64,3 +64,31 @@ export function parseExcludePatterns(raw: string): string[] {
.map((line) => line.trim()) .map((line) => line.trim())
.filter((line) => line.length > 0 && !line.startsWith("#")); .filter((line) => line.length > 0 && !line.startsWith("#"));
} }
/** Normalize a folder path: strip leading/trailing slashes; "" or "/" means vault root. */
export function normalizeFolder(folder: string): string {
return folder.replace(/^\/+|\/+$/g, "");
}
/** True if `path` lives inside `folder` (or `folder` is the vault root). */
export function isInFolder(path: string, folder: string): boolean {
const f = normalizeFolder(folder);
if (f === "") return true;
return path === f || path.startsWith(`${f}/`);
}
/** Exclude wins over include. Empty includeFolders means "include everything". */
export function isFolderFiltered(
path: string,
includeFolders: string[],
excludeFolders: string[],
): boolean {
for (const f of excludeFolders) {
if (isInFolder(path, f)) return true;
}
if (includeFolders.length === 0) return false;
for (const f of includeFolders) {
if (isInFolder(path, f)) return false;
}
return true;
}

View file

@ -0,0 +1,32 @@
import { type App, FuzzySuggestModal, type TFolder } from "obsidian";
/** Folder picker built on Obsidian's stock {@link FuzzySuggestModal}. */
export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
private readonly onPick: (path: string) => void;
private readonly excluded: Set<string>;
constructor(app: App, onPick: (path: string) => void, excluded: string[] = []) {
super(app);
this.onPick = onPick;
this.excluded = new Set(excluded.map((p) => p.replace(/^\/+|\/+$/g, "")));
this.setPlaceholder("Type to filter folders…");
}
getItems(): TFolder[] {
return this.app.vault
.getAllFolders(true)
.filter((f) => !this.excluded.has(this.toPath(f)));
}
getItemText(folder: TFolder): string {
return this.toPath(folder) || "/";
}
onChooseItem(folder: TFolder): void {
this.onPick(this.toPath(folder));
}
private toPath(folder: TFolder): string {
return folder.isRoot() ? "" : folder.path;
}
}

View file

@ -3,6 +3,7 @@ import { SurfSenseApiClient } from "./api-client";
import { PersistentQueue } from "./queue"; import { PersistentQueue } from "./queue";
import { SurfSenseSettingTab } from "./settings"; import { SurfSenseSettingTab } from "./settings";
import { StatusBar } from "./status-bar"; import { StatusBar } from "./status-bar";
import { StatusModal } from "./status-modal";
import { SyncEngine } from "./sync-engine"; import { SyncEngine } from "./sync-engine";
import { import {
DEFAULT_SETTINGS, DEFAULT_SETTINGS,
@ -20,8 +21,8 @@ export default class SurfSensePlugin extends Plugin {
private statusBar: StatusBar | null = null; private statusBar: StatusBar | null = null;
lastStatus: StatusState = { kind: "idle", queueDepth: 0 }; lastStatus: StatusState = { kind: "idle", queueDepth: 0 };
serverCapabilities: string[] = []; serverCapabilities: string[] = [];
serverApiVersion: string | null = null;
private settingTab: SurfSenseSettingTab | null = null; private settingTab: SurfSenseSettingTab | null = null;
private statusListeners = new Set<() => void>();
async onload() { async onload() {
await this.loadSettings(); await this.loadSettings();
@ -48,17 +49,16 @@ export default class SurfSensePlugin extends Plugin {
saveSettings: async (mut) => { saveSettings: async (mut) => {
mut(this.settings); mut(this.settings);
await this.saveSettings(); await this.saveSettings();
this.settingTab?.renderStatus(); this.notifyStatusChange();
}, },
setStatus: (s) => { setStatus: (s) => {
this.lastStatus = s; this.lastStatus = s;
this.statusBar?.update(s); this.statusBar?.update(s);
this.settingTab?.renderStatus(); this.notifyStatusChange();
}, },
onCapabilities: (caps, apiVersion) => { onCapabilities: (caps) => {
this.serverCapabilities = [...caps]; this.serverCapabilities = [...caps];
this.serverApiVersion = apiVersion; this.notifyStatusChange();
this.settingTab?.renderStatus();
}, },
}); });
@ -71,7 +71,7 @@ export default class SurfSensePlugin extends Plugin {
this.addSettingTab(this.settingTab); this.addSettingTab(this.settingTab);
const statusHost = this.addStatusBarItem(); const statusHost = this.addStatusBarItem();
this.statusBar = new StatusBar(statusHost); this.statusBar = new StatusBar(statusHost, () => this.openStatusModal());
this.statusBar.update(this.lastStatus); this.statusBar.update(this.lastStatus);
this.registerEvent( this.registerEvent(
@ -120,6 +120,12 @@ export default class SurfSensePlugin extends Plugin {
}, },
}); });
this.addCommand({
id: "open-status",
name: "Open sync status",
callback: () => this.openStatusModal(),
});
this.addCommand({ this.addCommand({
id: "open-settings", id: "open-settings",
name: "Open settings", name: "Open settings",
@ -150,6 +156,22 @@ export default class SurfSensePlugin extends Plugin {
return this.queue?.size ?? 0; return this.queue?.size ?? 0;
} }
openStatusModal(): void {
new StatusModal(this.app, this).open();
}
onStatusChange(listener: () => void): void {
this.statusListeners.add(listener);
}
offStatusChange(listener: () => void): void {
this.statusListeners.delete(listener);
}
private notifyStatusChange(): void {
for (const fn of this.statusListeners) fn();
}
async loadSettings() { async loadSettings() {
const data = (await this.loadData()) as Partial<SurfsensePluginSettings> | null; const data = (await this.loadData()) as Partial<SurfsensePluginSettings> | null;
this.settings = { this.settings = {
@ -157,6 +179,8 @@ export default class SurfSensePlugin extends Plugin {
...(data ?? {}), ...(data ?? {}),
queue: (data?.queue ?? []).map((i: QueueItem) => ({ ...i })), queue: (data?.queue ?? []).map((i: QueueItem) => ({ ...i })),
tombstones: { ...(data?.tombstones ?? {}) }, tombstones: { ...(data?.tombstones ?? {}) },
includeFolders: [...(data?.includeFolders ?? [])],
excludeFolders: [...(data?.excludeFolders ?? [])],
excludePatterns: data?.excludePatterns?.length excludePatterns: data?.excludePatterns?.length
? [...data.excludePatterns] ? [...data.excludePatterns]
: [...DEFAULT_SETTINGS.excludePatterns], : [...DEFAULT_SETTINGS.excludePatterns],
@ -172,9 +196,6 @@ export default class SurfSensePlugin extends Plugin {
if (!this.settings.vaultId) { if (!this.settings.vaultId) {
this.settings.vaultId = generateUuid(); this.settings.vaultId = generateUuid();
} }
if (!this.settings.vaultName) {
this.settings.vaultName = this.app.vault.getName();
}
} }
} }

View file

@ -5,7 +5,8 @@ import {
Setting, Setting,
} from "obsidian"; } from "obsidian";
import { AuthError } from "./api-client"; import { AuthError } from "./api-client";
import { parseExcludePatterns } from "./excludes"; import { normalizeFolder, parseExcludePatterns } from "./excludes";
import { FolderSuggestModal } from "./folder-suggest-modal";
import type SurfSensePlugin from "./main"; import type SurfSensePlugin from "./main";
import type { SearchSpace } from "./types"; import type { SearchSpace } from "./types";
@ -15,7 +16,6 @@ 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 statusEl: HTMLElement | null = null;
constructor(app: App, plugin: SurfSensePlugin) { constructor(app: App, plugin: SurfSensePlugin) {
super(app, plugin); super(app, plugin);
@ -25,7 +25,6 @@ export class SurfSenseSettingTab extends PluginSettingTab {
display(): void { display(): void {
const { containerEl } = this; const { containerEl } = this;
containerEl.empty(); containerEl.empty();
containerEl.addClass("surfsense-settings");
const settings = this.plugin.settings; const settings = this.plugin.settings;
@ -107,7 +106,6 @@ export class SurfSenseSettingTab extends PluginSettingTab {
this.handleApiError(err); this.handleApiError(err);
} }
} }
this.renderStatus();
}); });
}) })
.addExtraButton((btn) => .addExtraButton((btn) =>
@ -122,20 +120,6 @@ export class SurfSenseSettingTab extends PluginSettingTab {
new Setting(containerEl).setName("Vault").setHeading(); new Setting(containerEl).setName("Vault").setHeading();
new Setting(containerEl)
.setName("Vault name")
.setDesc(
"Friendly name for this vault. Defaults to your Obsidian vault folder name.",
)
.addText((text) =>
text
.setValue(settings.vaultName)
.onChange(async (value) => {
this.plugin.settings.vaultName = value.trim() || this.app.vault.getName();
await this.plugin.saveSettings();
}),
);
new Setting(containerEl) new Setting(containerEl)
.setName("Sync mode") .setName("Sync mode")
.setDesc("Auto syncs on every edit. Manual only syncs when you trigger it via the command palette.") .setDesc("Auto syncs on every edit. Manual only syncs when you trigger it via the command palette.")
@ -150,10 +134,30 @@ export class SurfSenseSettingTab extends PluginSettingTab {
}), }),
); );
this.renderFolderList(
containerEl,
"Include folders",
"Folders to sync (leave empty to sync entire vault).",
settings.includeFolders,
(next) => {
this.plugin.settings.includeFolders = next;
},
);
this.renderFolderList(
containerEl,
"Exclude folders",
"Folders to exclude from sync (takes precedence over includes).",
settings.excludeFolders,
(next) => {
this.plugin.settings.excludeFolders = next;
},
);
new Setting(containerEl) new Setting(containerEl)
.setName("Exclude patterns") .setName("Advanced exclude patterns")
.setDesc( .setDesc(
"One pattern per line. Supports * and **. Lines starting with # are comments. Files matching any pattern are skipped.", "Glob fallback for power users. One pattern per line, supports * and **. Lines starting with # are comments. Applied on top of the folder lists above.",
) )
.addTextArea((area) => { .addTextArea((area) => {
area.inputEl.rows = 4; area.inputEl.rows = 4;
@ -180,41 +184,12 @@ export class SurfSenseSettingTab extends PluginSettingTab {
}), }),
); );
new Setting(containerEl).setName("Identity").setHeading();
new Setting(containerEl)
.setName("Vault ID")
.setDesc(
"Stable identifier for this vault. Used by the backend to keep separate vaults distinct even if their folder names change.",
)
.addText((text) => {
text.inputEl.disabled = true;
text.setValue(settings.vaultId);
});
// Device ID is deliberately not exposed: it's an opaque per-install UUID
// (see seedIdentity in main.ts) and the web UI only shows a device count.
new Setting(containerEl).setName("Status").setHeading();
this.statusEl = containerEl.createDiv({ cls: "surfsense-settings__status" });
this.renderStatus();
new Setting(containerEl) new Setting(containerEl)
.addButton((btn) => .addButton((btn) =>
btn btn
.setButtonText("Re-sync entire vault") .setButtonText("View sync status")
.onClick(async () => { .setCta()
btn.setDisabled(true); .onClick(() => this.plugin.openStatusModal()),
try {
await this.plugin.engine.maybeReconcile(true);
new Notice("Surfsense: re-sync requested.");
} catch (err) {
this.handleApiError(err);
} finally {
btn.setDisabled(false);
this.renderStatus();
}
}),
) )
.addButton((btn) => .addButton((btn) =>
btn.setButtonText("Open releases").onClick(() => { btn.setButtonText("Open releases").onClick(() => {
@ -226,10 +201,6 @@ export class SurfSenseSettingTab extends PluginSettingTab {
); );
} }
hide(): void {
this.statusEl = null;
}
private async refreshSearchSpaces(): Promise<void> { private async refreshSearchSpaces(): Promise<void> {
this.loadingSpaces = true; this.loadingSpaces = true;
try { try {
@ -242,38 +213,46 @@ export class SurfSenseSettingTab extends PluginSettingTab {
} }
} }
renderStatus(): void { private renderFolderList(
if (!this.statusEl) return; containerEl: HTMLElement,
const s = this.plugin.settings; title: string,
this.statusEl.empty(); desc: string,
current: string[],
write: (next: string[]) => void,
): void {
const setting = new Setting(containerEl).setName(title).setDesc(desc);
const rows: { label: string; value: string }[] = [ const persist = async (next: string[]): Promise<void> => {
{ label: "Status", value: this.plugin.lastStatus.kind }, const dedup = Array.from(new Set(next.map(normalizeFolder)));
{ write(dedup);
label: "Last sync", await this.plugin.saveSettings();
value: s.lastSyncAt ? new Date(s.lastSyncAt).toLocaleString() : "—", this.display();
};
setting.addButton((btn) =>
btn
.setButtonText("Add Folder")
.setCta()
.onClick(() => {
new FolderSuggestModal(
this.app,
(picked) => {
void persist([...current, picked]);
}, },
{ current,
label: "Last reconcile", ).open();
value: s.lastReconcileAt ? new Date(s.lastReconcileAt).toLocaleString() : "—", }),
}, );
{ label: "Files synced", value: String(s.filesSynced ?? 0) },
{ label: "Queue depth", value: String(this.plugin.queueDepth) }, for (const folder of current) {
{ new Setting(containerEl).setName(folder || "/").addExtraButton((btn) =>
label: "API version", btn
value: this.plugin.serverApiVersion ?? "(not yet handshaken)", .setIcon("cross")
}, .setTooltip("Remove")
{ .onClick(() => {
label: "Capabilities", void persist(current.filter((f) => f !== folder));
value: this.plugin.serverCapabilities.length }),
? this.plugin.serverCapabilities.join(", ") );
: "(not yet handshaken)",
},
];
for (const row of rows) {
const wrap = this.statusEl.createDiv({ cls: "surfsense-settings__status-row" });
wrap.createSpan({ cls: "surfsense-settings__status-label", text: row.label });
wrap.createSpan({ cls: "surfsense-settings__status-value", text: row.value });
} }
} }

View file

@ -28,11 +28,15 @@ export class StatusBar {
private readonly icon: HTMLElement; private readonly icon: HTMLElement;
private readonly text: HTMLElement; private readonly text: HTMLElement;
constructor(host: HTMLElement) { constructor(host: HTMLElement, onClick?: () => void) {
this.el = host; this.el = host;
this.el.addClass("surfsense-status"); this.el.addClass("surfsense-status");
this.icon = this.el.createSpan({ cls: "surfsense-status__icon" }); this.icon = this.el.createSpan({ cls: "surfsense-status__icon" });
this.text = this.el.createSpan({ cls: "surfsense-status__text" }); this.text = this.el.createSpan({ cls: "surfsense-status__text" });
if (onClick) {
this.el.addClass("surfsense-status--clickable");
this.el.addEventListener("click", onClick);
}
this.update({ kind: "idle", queueDepth: 0 }); this.update({ kind: "idle", queueDepth: 0 });
} }

View file

@ -0,0 +1,82 @@
import { type App, Modal, Notice, Setting } from "obsidian";
import type SurfSensePlugin from "./main";
/**
* Read-only status panel. Mirrors what the settings tab used to embed inline,
* but as a modal so it's reachable from the status bar / command palette.
*
* Subscribes to plugin status changes while open so the numbers stay live;
* unsubscribes on close.
*/
export class StatusModal extends Modal {
private readonly plugin: SurfSensePlugin;
private readonly onChange = (): void => this.render();
constructor(app: App, plugin: SurfSensePlugin) {
super(app);
this.plugin = plugin;
}
onOpen(): void {
this.titleEl.setText("SurfSense status");
this.plugin.onStatusChange(this.onChange);
this.render();
}
onClose(): void {
this.plugin.offStatusChange(this.onChange);
this.contentEl.empty();
}
private render(): void {
const { contentEl, plugin } = this;
contentEl.empty();
const s = plugin.settings;
const rows: Array<[string, string]> = [
["Status", plugin.lastStatus.kind],
[
"Last sync",
s.lastSyncAt ? new Date(s.lastSyncAt).toLocaleString() : "—",
],
[
"Last reconcile",
s.lastReconcileAt
? new Date(s.lastReconcileAt).toLocaleString()
: "—",
],
["Files synced", String(s.filesSynced ?? 0)],
["Queue depth", String(plugin.queueDepth)],
[
"Capabilities",
plugin.serverCapabilities.length
? plugin.serverCapabilities.join(", ")
: "(not yet handshaken)",
],
];
for (const [label, value] of rows) {
new Setting(contentEl).setName(label).setDesc(value);
}
new Setting(contentEl)
.addButton((btn) =>
btn
.setButtonText("Re-sync entire vault")
.setCta()
.onClick(async () => {
btn.setDisabled(true);
try {
await plugin.engine.maybeReconcile(true);
new Notice("SurfSense: re-sync requested.");
} catch (err) {
new Notice(
`SurfSense: re-sync failed — ${(err as Error).message}`,
);
} finally {
btn.setDisabled(false);
}
}),
)
.addButton((btn) => btn.setButtonText("Close").onClick(() => this.close()));
}
}

View file

@ -6,7 +6,7 @@ import {
TransientError, TransientError,
VaultNotRegisteredError, VaultNotRegisteredError,
} from "./api-client"; } from "./api-client";
import { isExcluded } from "./excludes"; import { isExcluded, isFolderFiltered } from "./excludes";
import { buildNotePayload, computeContentHash } from "./payload"; import { buildNotePayload, computeContentHash } from "./payload";
import { type BatchResult, PersistentQueue } from "./queue"; import { type BatchResult, PersistentQueue } from "./queue";
import type { import type {
@ -31,14 +31,15 @@ export interface SyncEngineDeps {
getSettings: () => SyncEngineSettings; getSettings: () => SyncEngineSettings;
saveSettings: (mut: (s: SyncEngineSettings) => void) => Promise<void>; saveSettings: (mut: (s: SyncEngineSettings) => void) => Promise<void>;
setStatus: (s: StatusState) => void; setStatus: (s: StatusState) => void;
onCapabilities: (caps: string[], apiVersion: string) => void; onCapabilities: (caps: string[]) => void;
} }
export interface SyncEngineSettings { export interface SyncEngineSettings {
vaultId: string; vaultId: string;
vaultName: string;
connectorId: number | null; connectorId: number | null;
searchSpaceId: number | null; searchSpaceId: number | null;
includeFolders: string[];
excludeFolders: string[];
excludePatterns: string[]; excludePatterns: string[];
includeAttachments: boolean; includeAttachments: boolean;
syncMode: "auto" | "manual"; syncMode: "auto" | "manual";
@ -55,7 +56,6 @@ const PENDING_DEBOUNCE_MS = 1500;
export class SyncEngine { export class SyncEngine {
private readonly deps: SyncEngineDeps; private readonly deps: SyncEngineDeps;
private capabilities: string[] = []; private capabilities: string[] = [];
private apiVersion: string | null = null;
private pendingMdEdits = new Map<string, ReturnType<typeof setTimeout>>(); private pendingMdEdits = new Map<string, ReturnType<typeof setTimeout>>();
constructor(deps: SyncEngineDeps) { constructor(deps: SyncEngineDeps) {
@ -109,7 +109,7 @@ export class SyncEngine {
const resp = await this.deps.apiClient.connect({ const resp = await this.deps.apiClient.connect({
searchSpaceId: settings.searchSpaceId, searchSpaceId: settings.searchSpaceId,
vaultId: settings.vaultId, vaultId: settings.vaultId,
vaultName: settings.vaultName, vaultName: this.deps.app.vault.getName(),
}); });
this.applyHealth(resp); this.applyHealth(resp);
await this.deps.saveSettings((s) => { await this.deps.saveSettings((s) => {
@ -122,8 +122,7 @@ export class SyncEngine {
applyHealth(h: HealthResponse): void { applyHealth(h: HealthResponse): void {
this.capabilities = Array.isArray(h.capabilities) ? [...h.capabilities] : []; this.capabilities = Array.isArray(h.capabilities) ? [...h.capabilities] : [];
this.apiVersion = h.api_version ?? null; this.deps.onCapabilities(this.capabilities);
this.deps.onCapabilities(this.capabilities, this.apiVersion ?? "?");
} }
// ---- vault event handlers -------------------------------------------- // ---- vault event handlers --------------------------------------------
@ -485,6 +484,9 @@ export class SyncEngine {
} }
private isExcluded(path: string, settings: SyncEngineSettings): boolean { private isExcluded(path: string, settings: SyncEngineSettings): boolean {
if (isFolderFiltered(path, settings.includeFolders, settings.excludeFolders)) {
return true;
}
return isExcluded(path, settings.excludePatterns); return isExcluded(path, settings.excludePatterns);
} }

View file

@ -7,8 +7,9 @@ export interface SurfsensePluginSettings {
connectorId: number | null; connectorId: number | null;
/** UUID for the vault — lives here so Obsidian Sync replicates it across devices. */ /** UUID for the vault — lives here so Obsidian Sync replicates it across devices. */
vaultId: string; vaultId: string;
vaultName: string;
syncMode: "auto" | "manual"; syncMode: "auto" | "manual";
includeFolders: string[];
excludeFolders: string[];
excludePatterns: string[]; excludePatterns: string[];
includeAttachments: boolean; includeAttachments: boolean;
lastSyncAt: number | null; lastSyncAt: number | null;
@ -24,8 +25,9 @@ export const DEFAULT_SETTINGS: SurfsensePluginSettings = {
searchSpaceId: null, searchSpaceId: null,
connectorId: null, connectorId: null,
vaultId: "", vaultId: "",
vaultName: "",
syncMode: "auto", syncMode: "auto",
includeFolders: [],
excludeFolders: [],
excludePatterns: [".trash", "_attachments", "templates"], excludePatterns: [".trash", "_attachments", "templates"],
includeAttachments: false, includeAttachments: false,
lastSyncAt: null, lastSyncAt: null,
@ -96,14 +98,12 @@ export interface ConnectResponse {
connector_id: number; connector_id: number;
vault_id: string; vault_id: string;
search_space_id: number; search_space_id: number;
api_version: string;
capabilities: string[]; capabilities: string[];
server_time_utc: string; server_time_utc: string;
[key: string]: unknown; [key: string]: unknown;
} }
export interface HealthResponse { export interface HealthResponse {
api_version: string;
capabilities: string[]; capabilities: string[];
server_time_utc: string; server_time_utc: string;
[key: string]: unknown; [key: string]: unknown;

View file

@ -1,15 +1,14 @@
/* /*
* SurfSense Obsidian plugin styles. Kept tiny on purpose Obsidian * SurfSense Obsidian plugin styles. Status-bar widget only the settings
* theming should drive most of the look; we only add the bits we * tab uses Obsidian's stock Setting rows, no custom CSS needed.
* cannot express via the standard PluginSettingTab/Setting components.
*/ */
.surfsense-status { .surfsense-status {
display: inline-flex;
align-items: center;
gap: 6px; gap: 6px;
padding: 0 6px; }
cursor: default;
.surfsense-status--clickable {
cursor: pointer;
} }
.surfsense-status__icon { .surfsense-status__icon {
@ -23,10 +22,6 @@
height: 14px; height: 14px;
} }
.surfsense-status__text {
font-size: var(--font-ui-smaller);
}
.surfsense-status--ok .surfsense-status__icon { .surfsense-status--ok .surfsense-status__icon {
color: var(--color-green); color: var(--color-green);
} }
@ -42,25 +37,3 @@
.surfsense-status--err .surfsense-status__icon { .surfsense-status--err .surfsense-status__icon {
color: var(--color-red); color: var(--color-red);
} }
.surfsense-settings__status {
display: grid;
grid-template-columns: minmax(120px, max-content) 1fr;
row-gap: 4px;
column-gap: 12px;
margin: 8px 0 16px;
}
.surfsense-settings__status-row {
display: contents;
}
.surfsense-settings__status-label {
color: var(--text-muted);
font-size: var(--font-ui-smaller);
}
.surfsense-settings__status-value {
font-size: var(--font-ui-smaller);
word-break: break-word;
}