From 813de6e31d9a91bf39848939c01524ecb5e7fd48 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 18 Nov 2025 20:42:10 +0000 Subject: [PATCH 01/14] Fix error on init --- .../src/views/cursors/remote-cursors-plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/obsidian-plugin/src/views/cursors/remote-cursors-plugin.ts b/frontend/obsidian-plugin/src/views/cursors/remote-cursors-plugin.ts index 86ddd6cd..5f867f90 100644 --- a/frontend/obsidian-plugin/src/views/cursors/remote-cursors-plugin.ts +++ b/frontend/obsidian-plugin/src/views/cursors/remote-cursors-plugin.ts @@ -33,7 +33,7 @@ export class RemoteCursorsPluginValue implements PluginValue { isOutdated: boolean; }[] = []; - private static app: App; + private static app?: App; public decorations: DecorationSet = RangeSet.of([]); public static setCursors( @@ -88,7 +88,7 @@ export class RemoteCursorsPluginValue implements PluginValue { private static findFileForEditor( editor: EditorView ): RelativePath | undefined { - return RemoteCursorsPluginValue.app.workspace + return RemoteCursorsPluginValue.app?.workspace .getLeavesOfType("markdown") .map((leaf) => leaf.view) .filter((view) => view instanceof MarkdownView) -- 2.47.2 From 4f477cffa02598f78b15f87ac9a0b3b5acea311f Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 18 Nov 2025 21:18:20 +0000 Subject: [PATCH 02/14] Use stderr for logging --- sync-server/src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sync-server/src/main.rs b/sync-server/src/main.rs index aba6574e..ea4ad4fc 100644 --- a/sync-server/src/main.rs +++ b/sync-server/src/main.rs @@ -92,9 +92,9 @@ fn set_up_logging( .with_line_number(is_debug_mode) .compact(); - let stdout_layer = tracing_subscriber::fmt::layer() + let stderr_layer = tracing_subscriber::fmt::layer() .with_ansi(use_colors) - .with_writer(std::io::stdout) + .with_writer(std::io::stderr) .event_format(format.clone()); let file_layer = tracing_subscriber::fmt::layer() @@ -104,7 +104,7 @@ fn set_up_logging( tracing_subscriber::registry() .with(env_filter) - .with(stdout_layer) + .with(stderr_layer) .with(file_layer) .try_init() .context("Failed to initialise tracing") -- 2.47.2 From 418e09f08afe5c41b446ad773c3d511016d463ed Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 18 Nov 2025 21:36:00 +0000 Subject: [PATCH 03/14] Fix ansi logging --- sync-server/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sync-server/src/main.rs b/sync-server/src/main.rs index ea4ad4fc..82b75721 100644 --- a/sync-server/src/main.rs +++ b/sync-server/src/main.rs @@ -104,8 +104,8 @@ fn set_up_logging( tracing_subscriber::registry() .with(env_filter) - .with(stderr_layer) .with(file_layer) + .with(stderr_layer) .try_init() .context("Failed to initialise tracing") .map_err(init_error)?; -- 2.47.2 From c92aa63d71f151255e0eddf1cc80b20e33f88602 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 18 Nov 2025 21:49:35 +0000 Subject: [PATCH 04/14] Add healthcheck for client --- frontend/local-client-cli/Dockerfile | 6 +- frontend/local-client-cli/src/args.ts | 9 ++- frontend/local-client-cli/src/cli.ts | 30 +++++++++- frontend/local-client-cli/src/healthcheck.ts | 59 ++++++++++++++++++++ frontend/local-client-cli/webpack.config.js | 7 ++- 5 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 frontend/local-client-cli/src/healthcheck.ts diff --git a/frontend/local-client-cli/Dockerfile b/frontend/local-client-cli/Dockerfile index 6b8e1d6c..35bd81eb 100644 --- a/frontend/local-client-cli/Dockerfile +++ b/frontend/local-client-cli/Dockerfile @@ -16,10 +16,14 @@ LABEL org.opencontainers.image.licenses="MIT" LABEL org.opencontainers.image.authors="andras@schmelczer.dev" COPY --from=builder /build/local-client-cli/dist/cli.js /app/cli.js +COPY --from=builder /build/local-client-cli/dist/healthcheck.js /app/healthcheck.js + +HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=1 \ + CMD node /app/healthcheck.js /tmp/vaultlink-health.json WORKDIR /vault VOLUME ["/vault"] -ENTRYPOINT ["node", "/app/cli.js"] +ENTRYPOINT ["node", "/app/cli.js", "--health", "/tmp/vaultlink-health.json"] CMD ["--help"] diff --git a/frontend/local-client-cli/src/args.ts b/frontend/local-client-cli/src/args.ts index 08ef2a6b..961cadb5 100644 --- a/frontend/local-client-cli/src/args.ts +++ b/frontend/local-client-cli/src/args.ts @@ -12,6 +12,7 @@ export interface CliArgs { ignorePatterns?: string[]; webSocketRetryIntervalMs?: number; logLevel: LogLevel; + health?: string; } export function parseArgs(argv: string[]): CliArgs { @@ -51,6 +52,10 @@ export function parseArgs(argv: string[]): CliArgs { "[OPTIONAL] Log level (DEBUG, INFO, WARNING, ERROR)", "INFO" ) + .option( + "--health ", + "[OPTIONAL] Path to health status file for Docker healthcheck" + ) .addHelpText( "after", ` @@ -78,6 +83,7 @@ Examples: | number | undefined; const logLevelStr = (opts.logLevel as string | undefined) ?? "INFO"; + const health = opts.health as string | undefined; /* eslint-enable @typescript-eslint/no-unsafe-type-assertion */ if (localPath === undefined) { @@ -117,6 +123,7 @@ Examples: maxFileSizeMB: maxFileSizeMb, ignorePatterns: ignorePattern, webSocketRetryIntervalMs: websocketRetryIntervalMs, - logLevel + logLevel, + health }; } diff --git a/frontend/local-client-cli/src/cli.ts b/frontend/local-client-cli/src/cli.ts index 5a3c6546..8ef7a45a 100644 --- a/frontend/local-client-cli/src/cli.ts +++ b/frontend/local-client-cli/src/cli.ts @@ -1,5 +1,7 @@ import * as path from "path"; import * as fs from "fs/promises"; +import * as fsSync from "fs"; +import type { NetworkConnectionStatus } from "sync-client"; import { SyncClient, DEFAULT_SETTINGS, @@ -13,6 +15,19 @@ import { FileWatcher } from "./file-watcher"; import { formatLogLine, colorize, styleText } from "./logger-formatter"; import packageJson from "../package.json"; +function writeHealthStatus( + filePath: string, + connectionStatus: NetworkConnectionStatus +): void { + try { + fsSync.writeFileSync(filePath, JSON.stringify(connectionStatus)); + } catch (error) { + console.error( + `Failed to write health status to ${filePath}: ${error instanceof Error ? error.message : String(error)}` + ); + } +} + const LOG_LEVEL_ORDER = { [LogLevel.DEBUG]: 0, [LogLevel.INFO]: 1, @@ -78,6 +93,7 @@ async function main(): Promise { syncConcurrency: args.syncConcurrency ?? DEFAULT_SETTINGS.syncConcurrency, maxFileSizeMB: args.maxFileSizeMB ?? DEFAULT_SETTINGS.maxFileSizeMB, + diffCacheSizeMB: DEFAULT_SETTINGS.diffCacheSizeMB, ignorePatterns, webSocketRetryIntervalMs: args.webSocketRetryIntervalMs ?? @@ -119,6 +135,15 @@ async function main(): Promise { nativeLineEndings: process.platform === "win32" ? "\r\n" : "\n" }); + if (args.health !== undefined) { + const healthFile = args.health; + setInterval(() => { + void client.checkConnection().then((status) => { + writeHealthStatus(healthFile, status); + }); + }, 30 * 1000); // every 30 seconds + } + // Add colored log formatter with level filtering client.logger.addOnMessageListener((logLine) => { // Only show messages at or above the configured log level @@ -132,7 +157,10 @@ async function main(): Promise { const fileWatcher = new FileWatcher(absolutePath, client); client.addWebSocketStatusChangeListener(() => { - client.logger.info("WebSocket status changed"); + const isConnected = client.isWebSocketConnected; + client.logger.info( + `WebSocket status changed: ${isConnected ? "connected" : "disconnected"}` + ); }); client.addRemainingSyncOperationsListener((remaining) => { diff --git a/frontend/local-client-cli/src/healthcheck.ts b/frontend/local-client-cli/src/healthcheck.ts new file mode 100644 index 00000000..a16292d1 --- /dev/null +++ b/frontend/local-client-cli/src/healthcheck.ts @@ -0,0 +1,59 @@ +#!/usr/bin/env node + +/** + * Healthcheck script for Docker container + * Checks if the sync client is connected to the server + */ + +import * as fs from "fs"; +import type { NetworkConnectionStatus } from "sync-client"; + +function isHealthStatus(value: unknown): value is NetworkConnectionStatus { + if (typeof value !== "object" || value === null) { + return false; + } + + return true; +} + +function main(): void { + if (process.argv.length < 3) { + console.error("Usage: healthcheck "); + process.exit(1); + } + const [, , healthFile] = process.argv; + + try { + // Check if health file exists + if (!fs.existsSync(healthFile)) { + console.error(`Health file does not exist: ${healthFile}`); + process.exit(1); + } + + // Read and parse health status + const content = fs.readFileSync(healthFile, "utf-8"); + const parsed: unknown = JSON.parse(content); + + // Validate the parsed object using type guard + if (!isHealthStatus(parsed)) { + throw new Error("Invalid health status format"); + } + + const status = parsed; + + if (!status.isSuccessful || !status.isWebSocketConnected) { + console.error("Not connected to server: " + status.serverMessage); + process.exit(1); + } + + console.log("Healthy: Connected to server"); + process.exit(0); + } catch (error) { + console.error( + `Health check failed: ${error instanceof Error ? error.message : String(error)}` + ); + process.exit(1); + } +} + +main(); diff --git a/frontend/local-client-cli/webpack.config.js b/frontend/local-client-cli/webpack.config.js index e17754b2..32b3b125 100644 --- a/frontend/local-client-cli/webpack.config.js +++ b/frontend/local-client-cli/webpack.config.js @@ -2,7 +2,10 @@ const path = require("path"); const webpack = require("webpack"); module.exports = { - entry: "./src/cli.ts", + entry: { + cli: "./src/cli.ts", + healthcheck: "./src/healthcheck.ts" + }, target: "node", mode: "production", optimization: { @@ -21,7 +24,7 @@ module.exports = { }, output: { globalObject: "this", - filename: "cli.js", + filename: "[name].js", path: path.resolve(__dirname, "dist") }, plugins: [ -- 2.47.2 From 0bfd8a6df42f4beca2b5106ce3b8ffef558bb10f Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 18 Nov 2025 21:50:06 +0000 Subject: [PATCH 05/14] Add diff cache size setting --- .../sync-client/src/persistence/settings.ts | 4 ++- frontend/sync-client/src/sync-client.ts | 22 +++++++++--- .../sync-client/src/sync-operations/syncer.ts | 4 +-- .../src/utils/fix-sized-cache.test.ts | 36 +++++++++++++++++++ .../sync-client/src/utils/fix-sized-cache.ts | 28 +++++++++------ 5 files changed, 75 insertions(+), 19 deletions(-) diff --git a/frontend/sync-client/src/persistence/settings.ts b/frontend/sync-client/src/persistence/settings.ts index b0aff937..bd895d15 100644 --- a/frontend/sync-client/src/persistence/settings.ts +++ b/frontend/sync-client/src/persistence/settings.ts @@ -9,6 +9,7 @@ export interface SyncSettings { maxFileSizeMB: number; ignorePatterns: string[]; webSocketRetryIntervalMs: number; + diffCacheSizeMB: number; } export const DEFAULT_SETTINGS: SyncSettings = { @@ -19,7 +20,8 @@ export const DEFAULT_SETTINGS: SyncSettings = { isSyncEnabled: false, maxFileSizeMB: 10, ignorePatterns: [], - webSocketRetryIntervalMs: 3500 + webSocketRetryIntervalMs: 3500, + diffCacheSizeMB: 4 }; export class Settings { diff --git a/frontend/sync-client/src/sync-client.ts b/frontend/sync-client/src/sync-client.ts index 33a1cac5..2bfc2654 100644 --- a/frontend/sync-client/src/sync-client.ts +++ b/frontend/sync-client/src/sync-client.ts @@ -38,7 +38,8 @@ export class SyncClient { private readonly _logger: Logger, private readonly connectionStatus: ConnectionStatus, private readonly cursorTracker: CursorTracker, - private readonly fileChangeNotifier: FileChangeNotifier + private readonly fileChangeNotifier: FileChangeNotifier, + private readonly contentCache: FixedSizeDocumentCache ) { this.settings.addOnSettingsChangeListener( async (newSettings, oldSettings) => { @@ -53,6 +54,14 @@ export class SyncClient { this.stop(); } } + + if ( + newSettings.diffCacheSizeMB !== oldSettings.diffCacheSizeMB + ) { + this.contentCache.resize( + newSettings.diffCacheSizeMB * 1024 * 1024 + ); + } } ); } @@ -65,6 +74,10 @@ export class SyncClient { return this.database.length; } + public get isWebSocketConnected(): boolean { + return this.webSocketManager.isWebSocketConnected; + } + public static async create({ fs, persistence, @@ -152,8 +165,7 @@ export class SyncClient { settings, syncService, fileOperations, - unrestrictedSyncer, - contentCache + unrestrictedSyncer ); const webSocketManager = new WebSocketManager( @@ -182,7 +194,8 @@ export class SyncClient { logger, connectionStatus, cursorTracker, - fileChangeNotifier + fileChangeNotifier, + contentCache ); logger.info("SyncClient initialised"); @@ -235,6 +248,7 @@ export class SyncClient { public async reset(): Promise { this.stop(); this.connectionStatus.startReset(); + this.contentCache.clear(); await this.syncer.reset(); this.history.reset(); this.database.reset(); diff --git a/frontend/sync-client/src/sync-operations/syncer.ts b/frontend/sync-client/src/sync-operations/syncer.ts index 1c8ac36e..a4badd9a 100644 --- a/frontend/sync-client/src/sync-operations/syncer.ts +++ b/frontend/sync-client/src/sync-operations/syncer.ts @@ -34,8 +34,7 @@ export class Syncer { settings: Settings, private readonly syncService: SyncService, private readonly operations: FileOperations, - private readonly internalSyncer: UnrestrictedSyncer, - private readonly contentCache: FixedSizeDocumentCache + private readonly internalSyncer: UnrestrictedSyncer ) { this.syncQueue = new PQueue({ concurrency: settings.getSettings().syncConcurrency @@ -252,7 +251,6 @@ export class Syncer { public async reset(): Promise { await this.waitUntilFinished(); - this.contentCache.clear(); } public async syncRemotelyUpdatedFile( diff --git a/frontend/sync-client/src/utils/fix-sized-cache.test.ts b/frontend/sync-client/src/utils/fix-sized-cache.test.ts index 46bc4144..4a24aafb 100644 --- a/frontend/sync-client/src/utils/fix-sized-cache.test.ts +++ b/frontend/sync-client/src/utils/fix-sized-cache.test.ts @@ -236,4 +236,40 @@ describe("fixedSizeDocumentCache", () => { assert.equal(cache.get(2), doc2); assert.equal(cache.get(3), doc3); }); + + it("resizeToLargerSizeNoEviction", async () => { + const cache = new FixedSizeDocumentCache(4); + const doc1 = new Uint8Array([1, 2]); + const doc2 = new Uint8Array([3, 4]); + + cache.put(1, doc1); + cache.put(2, doc2); + + cache.resize(10); + + assert.equal(cache.get(1), doc1); + assert.equal(cache.get(2), doc2); + }); + + it("resizeCausesMultipleEvictions", async () => { + const cache = new FixedSizeDocumentCache(10); + const doc1 = new Uint8Array([1, 2]); + const doc2 = new Uint8Array([3, 4]); + const doc3 = new Uint8Array([5, 6]); + const doc4 = new Uint8Array([7, 8]); + + cache.put(1, doc1); + cache.put(2, doc2); + cache.put(3, doc3); + cache.put(4, doc4); + // Cache has 8 bytes total + + cache.resize(2); + + // Should evict doc1, doc2, doc3 to get down to 2 bytes + assert.equal(cache.get(1), undefined); + assert.equal(cache.get(2), undefined); + assert.equal(cache.get(3), undefined); + assert.equal(cache.get(4), doc4); + }); }); diff --git a/frontend/sync-client/src/utils/fix-sized-cache.ts b/frontend/sync-client/src/utils/fix-sized-cache.ts index 7adee7b0..cf0ba47e 100644 --- a/frontend/sync-client/src/utils/fix-sized-cache.ts +++ b/frontend/sync-client/src/utils/fix-sized-cache.ts @@ -14,14 +14,12 @@ class LRUNode { // evicting the least recently used documents when the size limit is exceeded. export class FixedSizeDocumentCache { - private readonly maxSizeInBytes: number; private currentSizeInBytes: number; private readonly cache: Map; private head: LRUNode | null; // Least recently used private tail: LRUNode | null; // Most recently used - public constructor(maxSizeInBytes: number) { - this.maxSizeInBytes = maxSizeInBytes; + public constructor(private maxSizeInBytes: number) { this.currentSizeInBytes = 0; this.cache = new Map(); this.head = null; @@ -56,14 +54,7 @@ export class FixedSizeDocumentCache { this.cache.set(updateId, newNode); this.addToTail(newNode); this.currentSizeInBytes += content.byteLength; - - // Evict least recently used documents if over size limit - while (this.currentSizeInBytes > this.maxSizeInBytes && this.head) { - const lruNode = this.head; - this.removeNode(lruNode); - this.cache.delete(lruNode.key); - this.currentSizeInBytes -= lruNode.value.byteLength; - } + this.fitBelowMaxSize(); } public clear(): void { @@ -73,6 +64,21 @@ export class FixedSizeDocumentCache { this.currentSizeInBytes = 0; } + public resize(newMaxSizeInBytes: number): void { + this.maxSizeInBytes = newMaxSizeInBytes; + this.fitBelowMaxSize(); + } + + private fitBelowMaxSize(): void { + // Evict least recently used documents if over size limit + while (this.currentSizeInBytes > this.maxSizeInBytes && this.head) { + const lruNode = this.head; + this.removeNode(lruNode); + this.cache.delete(lruNode.key); + this.currentSizeInBytes -= lruNode.value.byteLength; + } + } + private removeNode(node: LRUNode): void { if (node.prev) { node.prev.next = node.next; -- 2.47.2 From 377abe136258668fa04f0b26f8858169b692fb43 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 18 Nov 2025 22:03:48 +0000 Subject: [PATCH 06/14] Move telemetry into sync-client --- frontend/obsidian-plugin/package.json | 4 -- .../obsidian-plugin/src/vault-link-plugin.ts | 41 ------------------- frontend/package-lock.json | 26 +----------- frontend/sync-client/package.json | 2 + .../sync-client/src/persistence/settings.ts | 4 +- frontend/sync-client/src/sync-client.ts | 16 ++++++++ .../sync-client/src/utils/set-up-telemetry.ts | 41 +++++++++++++++++++ package-lock.json | 6 +++ 8 files changed, 70 insertions(+), 70 deletions(-) create mode 100644 frontend/sync-client/src/utils/set-up-telemetry.ts create mode 100644 package-lock.json diff --git a/frontend/obsidian-plugin/package.json b/frontend/obsidian-plugin/package.json index 12a52f28..0c09800b 100644 --- a/frontend/obsidian-plugin/package.json +++ b/frontend/obsidian-plugin/package.json @@ -13,8 +13,6 @@ "author": "", "license": "MIT", "devDependencies": { - "@plausible-analytics/tracker": "^0.4.3", - "@sentry/browser": "^10.8.0", "@types/node": "^24.8.1", "css-loader": "^7.1.2", "date-fns": "^4.1.0", @@ -22,7 +20,6 @@ "fs-extra": "^11.3.0", "mini-css-extract-plugin": "^2.9.2", "obsidian": "1.10.2", - "reconcile-text": "^0.5.0", "resolve-url-loader": "^5.0.0", "sass": "^1.91.0", "sass-loader": "^16.0.5", @@ -33,7 +30,6 @@ "tsx": "^4.20.5", "typescript": "5.8.3", "url": "^0.11.4", - "virtual-scroller": "^1.13.1", "webpack": "^5.99.9", "webpack-cli": "^6.0.1" } diff --git a/frontend/obsidian-plugin/src/vault-link-plugin.ts b/frontend/obsidian-plugin/src/vault-link-plugin.ts index 25a03ff6..fc16aae2 100644 --- a/frontend/obsidian-plugin/src/vault-link-plugin.ts +++ b/frontend/obsidian-plugin/src/vault-link-plugin.ts @@ -11,8 +11,6 @@ import { HistoryView } from "./views/history/history-view"; import { StatusBar } from "./views/status-bar/status-bar"; import { LogsView } from "./views/logs/logs-view"; import { StatusDescription } from "./views/status-description/status-description"; -import * as Sentry from "@sentry/browser"; -import { init as plausibleInit } from "@plausible-analytics/tracker"; import { SyncClient, rateLimit, @@ -50,45 +48,6 @@ export default class VaultLinkPlugin extends Plugin { ".trash/**" ); - plausibleInit({ - domain: "vault-link", - endpoint: "https://stats.schmelczer.dev/status", - autoCapturePageviews: true, - captureOnLocalhost: true, - logging: true - }); - - Sentry.init({ - dsn: "https://56accd39d92442e788a457a04623cf57@bugs.schmelczer.dev/1", - skipBrowserExtensionCheck: false - }); - - const onError = (event: ErrorEvent): void => { - Sentry.captureException(event.error, { - extra: { - message: event.message, - filename: event.filename, - lineno: event.lineno, - colno: event.colno - } - }); - }; - window.addEventListener("error", onError); - this.disposables.push(() => { - window.removeEventListener("error", onError); - }); - - const onUnhandledRejection = (event: PromiseRejectionEvent): void => { - Sentry.captureException(event.reason); - }; - window.addEventListener("unhandledrejection", onUnhandledRejection); - this.disposables.push(() => { - window.removeEventListener( - "unhandledrejection", - onUnhandledRejection - ); - }); - const isDebugBuild = process.env.NODE_ENV === "development"; const debugOptions = isDebugBuild ? { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2b52eb1d..e27a36f0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3487,23 +3487,11 @@ "node": ">= 10.13.0" } }, - "node_modules/reconcile-text": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/reconcile-text/-/reconcile-text-0.5.0.tgz", - "integrity": "sha512-zki3lqw9Oxdhm9ZvDN17VyYoL1Isc8BEL07ILVDE2yGfNEI7thrkczoNCUr+hkFU2rzZtfxECTG0b7p61AJ6wg==", - "dev": true, - "license": "MIT" - }, "node_modules/regex-parser": { "version": "2.3.1", "dev": true, "license": "MIT" }, - "node_modules/request-animation-frame-timeout": { - "version": "2.0.4", - "dev": true, - "license": "MIT" - }, "node_modules/require-directory": { "version": "2.1.1", "dev": true, @@ -4329,14 +4317,6 @@ "resolved": "obsidian-plugin", "link": true }, - "node_modules/virtual-scroller": { - "version": "1.13.1", - "dev": true, - "license": "MIT", - "dependencies": { - "request-animation-frame-timeout": "^2.0.3" - } - }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -4661,8 +4641,6 @@ "version": "0.10.0", "license": "MIT", "devDependencies": { - "@plausible-analytics/tracker": "^0.4.3", - "@sentry/browser": "^10.8.0", "@types/node": "^24.8.1", "css-loader": "^7.1.2", "date-fns": "^4.1.0", @@ -4670,7 +4648,6 @@ "fs-extra": "^11.3.0", "mini-css-extract-plugin": "^2.9.2", "obsidian": "1.10.2", - "reconcile-text": "^0.5.0", "resolve-url-loader": "^5.0.0", "sass": "^1.91.0", "sass-loader": "^16.0.5", @@ -4681,7 +4658,6 @@ "tsx": "^4.20.5", "typescript": "5.8.3", "url": "^0.11.4", - "virtual-scroller": "^1.13.1", "webpack": "^5.99.9", "webpack-cli": "^6.0.1" } @@ -4696,6 +4672,8 @@ "uuid": "^13.0.0" }, "devDependencies": { + "@plausible-analytics/tracker": "^0.4.3", + "@sentry/browser": "^10.8.0", "@types/node": "^24.8.1", "ts-loader": "^9.5.2", "tslib": "2.8.1", diff --git a/frontend/sync-client/package.json b/frontend/sync-client/package.json index d35e5a3d..b26cc67d 100644 --- a/frontend/sync-client/package.json +++ b/frontend/sync-client/package.json @@ -28,6 +28,8 @@ "webpack": "^5.99.9", "webpack-cli": "^6.0.1", "webpack-merge": "^6.0.1", + "@plausible-analytics/tracker": "^0.4.3", + "@sentry/browser": "^10.8.0", "ws": "^8.18.3" } } diff --git a/frontend/sync-client/src/persistence/settings.ts b/frontend/sync-client/src/persistence/settings.ts index bd895d15..87821728 100644 --- a/frontend/sync-client/src/persistence/settings.ts +++ b/frontend/sync-client/src/persistence/settings.ts @@ -10,6 +10,7 @@ export interface SyncSettings { ignorePatterns: string[]; webSocketRetryIntervalMs: number; diffCacheSizeMB: number; + enableTelemetry: boolean; } export const DEFAULT_SETTINGS: SyncSettings = { @@ -21,7 +22,8 @@ export const DEFAULT_SETTINGS: SyncSettings = { maxFileSizeMB: 10, ignorePatterns: [], webSocketRetryIntervalMs: 3500, - diffCacheSizeMB: 4 + diffCacheSizeMB: 4, + enableTelemetry: false }; export class Settings { diff --git a/frontend/sync-client/src/sync-client.ts b/frontend/sync-client/src/sync-client.ts index 2bfc2654..9547af65 100644 --- a/frontend/sync-client/src/sync-client.ts +++ b/frontend/sync-client/src/sync-client.ts @@ -22,11 +22,13 @@ import type { CursorSpan } from "./services/types/CursorSpan"; import type { MaybeOutdatedClientCursors } from "./types/maybe-outdated-client-cursors"; import { FileChangeNotifier } from "./sync-operations/file-change-notifier"; import { FixedSizeDocumentCache } from "./utils/fix-sized-cache"; +import { setUpTelemetry } from "./utils/set-up-telemetry"; export class SyncClient { private static readonly MINIMUM_SAVE_INTERVAL_MS = 1000; private hasStartedOfflineSync = false; private hasFinishedOfflineSync = false; + private unloadTelemetry?: () => void; private constructor( private readonly history: SyncHistory, @@ -41,6 +43,10 @@ export class SyncClient { private readonly fileChangeNotifier: FileChangeNotifier, private readonly contentCache: FixedSizeDocumentCache ) { + if (settings.getSettings().enableTelemetry) { + this.unloadTelemetry = setUpTelemetry(); + } + this.settings.addOnSettingsChangeListener( async (newSettings, oldSettings) => { if (newSettings.vaultName !== oldSettings.vaultName) { @@ -62,6 +68,16 @@ export class SyncClient { newSettings.diffCacheSizeMB * 1024 * 1024 ); } + + if ( + newSettings.enableTelemetry !== oldSettings.enableTelemetry + ) { + if (newSettings.enableTelemetry) { + this.unloadTelemetry = setUpTelemetry(); + } else { + this.unloadTelemetry?.(); + } + } } ); } diff --git a/frontend/sync-client/src/utils/set-up-telemetry.ts b/frontend/sync-client/src/utils/set-up-telemetry.ts new file mode 100644 index 00000000..cf065dd6 --- /dev/null +++ b/frontend/sync-client/src/utils/set-up-telemetry.ts @@ -0,0 +1,41 @@ +import * as Sentry from "@sentry/browser"; +import { init as plausibleInit } from "@plausible-analytics/tracker"; + +export const setUpTelemetry = (): (() => void) => { + plausibleInit({ + domain: "vault-link", + endpoint: "https://stats.schmelczer.dev/status", + autoCapturePageviews: true, + captureOnLocalhost: true, + logging: true + }); + + Sentry.init({ + dsn: "https://56accd39d92442e788a457a04623cf57@bugs.schmelczer.dev/1", + skipBrowserExtensionCheck: false + }); + + const onError = (event: ErrorEvent): void => { + Sentry.captureException(event.error, { + extra: { + message: event.message, + filename: event.filename, + lineno: event.lineno, + colno: event.colno + } + }); + }; + window.addEventListener("error", onError); + + const onUnhandledRejection = (event: PromiseRejectionEvent): void => { + Sentry.captureException(event.reason); + }; + window.addEventListener("unhandledrejection", onUnhandledRejection); + + return (): void => { + window.removeEventListener("error", onError); + window.removeEventListener("unhandledrejection", onUnhandledRejection); + Sentry.close(5000); + // unloading plausible requires reloading + }; +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..9e0474fd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "vault-link", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} -- 2.47.2 From 402a37c6a602a7ed9fde3a1c71d77bcb7f9f2639 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 18 Nov 2025 22:06:59 +0000 Subject: [PATCH 07/14] Expose "enable telemetry" --- frontend/obsidian-plugin/package.json | 1 + .../src/views/settings/settings-tab.ts | 21 +++++++++++++++++++ frontend/package-lock.json | 13 ++++++------ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/frontend/obsidian-plugin/package.json b/frontend/obsidian-plugin/package.json index 0c09800b..8c4fcee2 100644 --- a/frontend/obsidian-plugin/package.json +++ b/frontend/obsidian-plugin/package.json @@ -20,6 +20,7 @@ "fs-extra": "^11.3.0", "mini-css-extract-plugin": "^2.9.2", "obsidian": "1.10.2", + "reconcile-text": "^0.7.1", "resolve-url-loader": "^5.0.0", "sass": "^1.91.0", "sass-loader": "^16.0.5", diff --git a/frontend/obsidian-plugin/src/views/settings/settings-tab.ts b/frontend/obsidian-plugin/src/views/settings/settings-tab.ts index 2d129edc..cf4da91b 100644 --- a/frontend/obsidian-plugin/src/views/settings/settings-tab.ts +++ b/frontend/obsidian-plugin/src/views/settings/settings-tab.ts @@ -72,6 +72,7 @@ export class SyncSettingsTab extends PluginSettingTab { this.renderSettingsHeader(containerEl); this.renderConnectionSettings(containerEl); this.renderSyncSettings(containerEl); + this.renderMiscSettings(containerEl); } public hide(): void { @@ -339,6 +340,26 @@ export class SyncSettingsTab extends PluginSettingTab { ); } + private renderMiscSettings(containerEl: HTMLElement): void { + containerEl.createEl("h3", { text: "Other" }); + + new Setting(containerEl) + .setName("Enable telemetry") + .setDesc( + "Allow sending anonymous usage data & error reports to help improve the plugin. The data collected is never shared with third parties." + ) + .setTooltip( + "Allow sending anonymous usage data & error reports to help improve the plugin. The data collected is never shared with third parties." + ) + .addToggle((toggle) => + toggle + .setValue(this.syncClient.getSettings().enableTelemetry) + .onChange(async (value) => + this.syncClient.setSetting("enableTelemetry", value) + ) + ); + } + private setStatusDescriptionSubscription( newSubscription?: () => unknown ): void { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e27a36f0..989a1284 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3487,6 +3487,12 @@ "node": ">= 10.13.0" } }, + "node_modules/reconcile-text": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/reconcile-text/-/reconcile-text-0.7.1.tgz", + "integrity": "sha512-khedcYvAKs7ELKh5Z8mz2vyomMY5TqznV1dB+k/7qUAX9cheMNN5/EPJVQYZepOMunYbnQitvhFJX3kD4IMcNw==", + "license": "MIT" + }, "node_modules/regex-parser": { "version": "2.3.1", "dev": true, @@ -4648,6 +4654,7 @@ "fs-extra": "^11.3.0", "mini-css-extract-plugin": "^2.9.2", "obsidian": "1.10.2", + "reconcile-text": "^0.7.1", "resolve-url-loader": "^5.0.0", "sass": "^1.91.0", "sass-loader": "^16.0.5", @@ -4707,12 +4714,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "sync-client/node_modules/reconcile-text": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/reconcile-text/-/reconcile-text-0.7.1.tgz", - "integrity": "sha512-khedcYvAKs7ELKh5Z8mz2vyomMY5TqznV1dB+k/7qUAX9cheMNN5/EPJVQYZepOMunYbnQitvhFJX3kD4IMcNw==", - "license": "MIT" - }, "test-client": { "version": "0.10.0", "bin": { -- 2.47.2 From 5cd8cd87253877a21f29535b66b19b8e611c1a7f Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 18 Nov 2025 22:12:16 +0000 Subject: [PATCH 08/14] Remove apply settings button --- .../src/views/settings/settings-tab.ts | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/frontend/obsidian-plugin/src/views/settings/settings-tab.ts b/frontend/obsidian-plugin/src/views/settings/settings-tab.ts index cf4da91b..e4c16e6e 100644 --- a/frontend/obsidian-plugin/src/views/settings/settings-tab.ts +++ b/frontend/obsidian-plugin/src/views/settings/settings-tab.ts @@ -194,38 +194,28 @@ export class SyncSettingsTab extends PluginSettingTab { }) ); - new Setting(containerEl) - .addButton((button) => - button.setButtonText("Apply").onClick(async () => { + new Setting(containerEl).addButton((button) => + button + .setButtonText("Apply & test connection") + .onClick(async () => { if (this.areThereUnsavedChanges()) { await this.syncClient.setSettings({ vaultName: this.editedVaultName, remoteUri: this.editedServerUri, token: this.editedToken }); + new Notice("Checking connection to the server..."); new Notice( - "The changes have been applied successfully!" + ( + await this.syncClient.checkConnection() + ).serverMessage ); await this.statusDescription.updateConnectionState(); } else { new Notice("No changes to apply"); } }) - ) - .addButton((button) => - button.setButtonText("Test connection").onClick(async () => { - if (this.areThereUnsavedChanges()) { - new Notice( - "There are unsaved changes, testing with the currently saved settings" - ); - } - - new Notice( - (await this.syncClient.checkConnection()).serverMessage - ); - await this.statusDescription.updateConnectionState(); - }) - ); + ); } private areThereUnsavedChanges(): boolean { -- 2.47.2 From ffcdbac8e24dab2f5367df64c48b5667b31ebf28 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 18 Nov 2025 22:14:02 +0000 Subject: [PATCH 09/14] Remove plausible --- frontend/package-lock.json | 8 -------- frontend/sync-client/package.json | 1 - frontend/sync-client/src/utils/set-up-telemetry.ts | 10 ---------- 3 files changed, 19 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 989a1284..bbb98a1a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -871,13 +871,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@plausible-analytics/tracker": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@plausible-analytics/tracker/-/tracker-0.4.3.tgz", - "integrity": "sha512-RKTgH5xu7Pa77VS4OEnS4woPhDxRgWLJlt9f6JhwgBC9ilknCfJIVEN2A1D8OR7hzgxMQF/hPyls9iN9ReAm3Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@sentry-internal/browser-utils": { "version": "10.8.0", "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.8.0.tgz", @@ -4679,7 +4672,6 @@ "uuid": "^13.0.0" }, "devDependencies": { - "@plausible-analytics/tracker": "^0.4.3", "@sentry/browser": "^10.8.0", "@types/node": "^24.8.1", "ts-loader": "^9.5.2", diff --git a/frontend/sync-client/package.json b/frontend/sync-client/package.json index b26cc67d..75ad6e49 100644 --- a/frontend/sync-client/package.json +++ b/frontend/sync-client/package.json @@ -28,7 +28,6 @@ "webpack": "^5.99.9", "webpack-cli": "^6.0.1", "webpack-merge": "^6.0.1", - "@plausible-analytics/tracker": "^0.4.3", "@sentry/browser": "^10.8.0", "ws": "^8.18.3" } diff --git a/frontend/sync-client/src/utils/set-up-telemetry.ts b/frontend/sync-client/src/utils/set-up-telemetry.ts index cf065dd6..8f6de17d 100644 --- a/frontend/sync-client/src/utils/set-up-telemetry.ts +++ b/frontend/sync-client/src/utils/set-up-telemetry.ts @@ -1,15 +1,6 @@ import * as Sentry from "@sentry/browser"; -import { init as plausibleInit } from "@plausible-analytics/tracker"; export const setUpTelemetry = (): (() => void) => { - plausibleInit({ - domain: "vault-link", - endpoint: "https://stats.schmelczer.dev/status", - autoCapturePageviews: true, - captureOnLocalhost: true, - logging: true - }); - Sentry.init({ dsn: "https://56accd39d92442e788a457a04623cf57@bugs.schmelczer.dev/1", skipBrowserExtensionCheck: false @@ -36,6 +27,5 @@ export const setUpTelemetry = (): (() => void) => { window.removeEventListener("error", onError); window.removeEventListener("unhandledrejection", onUnhandledRejection); Sentry.close(5000); - // unloading plausible requires reloading }; }; -- 2.47.2 From 3729778b12aa397b9e2f17d431534968176244cb Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 18 Nov 2025 22:17:34 +0000 Subject: [PATCH 10/14] Add telemtery cli arg --- frontend/local-client-cli/src/args.ts | 9 ++++++++- frontend/local-client-cli/src/cli.ts | 3 ++- frontend/sync-client/src/utils/set-up-telemetry.ts | 4 +++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/local-client-cli/src/args.ts b/frontend/local-client-cli/src/args.ts index 961cadb5..fc2d4a95 100644 --- a/frontend/local-client-cli/src/args.ts +++ b/frontend/local-client-cli/src/args.ts @@ -13,6 +13,7 @@ export interface CliArgs { webSocketRetryIntervalMs?: number; logLevel: LogLevel; health?: string; + enableTelemetry?: boolean; } export function parseArgs(argv: string[]): CliArgs { @@ -56,6 +57,10 @@ export function parseArgs(argv: string[]): CliArgs { "--health ", "[OPTIONAL] Path to health status file for Docker healthcheck" ) + .option( + "--enable-telemetry", + "[OPTIONAL] Enable telemetry (disabled by default)" + ) .addHelpText( "after", ` @@ -84,6 +89,7 @@ Examples: | undefined; const logLevelStr = (opts.logLevel as string | undefined) ?? "INFO"; const health = opts.health as string | undefined; + const enableTelemetry = opts.enableTelemetry as boolean | undefined; /* eslint-enable @typescript-eslint/no-unsafe-type-assertion */ if (localPath === undefined) { @@ -124,6 +130,7 @@ Examples: ignorePatterns: ignorePattern, webSocketRetryIntervalMs: websocketRetryIntervalMs, logLevel, - health + health, + enableTelemetry }; } diff --git a/frontend/local-client-cli/src/cli.ts b/frontend/local-client-cli/src/cli.ts index 8ef7a45a..b0ce096e 100644 --- a/frontend/local-client-cli/src/cli.ts +++ b/frontend/local-client-cli/src/cli.ts @@ -98,7 +98,8 @@ async function main(): Promise { webSocketRetryIntervalMs: args.webSocketRetryIntervalMs ?? DEFAULT_SETTINGS.webSocketRetryIntervalMs, - isSyncEnabled: true + isSyncEnabled: true, + enableTelemetry: args.enableTelemetry ?? false }; const client = await SyncClient.create({ diff --git a/frontend/sync-client/src/utils/set-up-telemetry.ts b/frontend/sync-client/src/utils/set-up-telemetry.ts index 8f6de17d..e4a4d881 100644 --- a/frontend/sync-client/src/utils/set-up-telemetry.ts +++ b/frontend/sync-client/src/utils/set-up-telemetry.ts @@ -26,6 +26,8 @@ export const setUpTelemetry = (): (() => void) => { return (): void => { window.removeEventListener("error", onError); window.removeEventListener("unhandledrejection", onUnhandledRejection); - Sentry.close(5000); + Sentry.close(5000).catch(() => { + // Ignore errors during shutdown + }); }; }; -- 2.47.2 From eec05bd6abe4229823099de85055420f3a09c6a6 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 18 Nov 2025 22:20:20 +0000 Subject: [PATCH 11/14] Update frontend/local-client-cli/src/healthcheck.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- frontend/local-client-cli/src/healthcheck.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/local-client-cli/src/healthcheck.ts b/frontend/local-client-cli/src/healthcheck.ts index a16292d1..75c1906f 100644 --- a/frontend/local-client-cli/src/healthcheck.ts +++ b/frontend/local-client-cli/src/healthcheck.ts @@ -13,7 +13,12 @@ function isHealthStatus(value: unknown): value is NetworkConnectionStatus { return false; } - return true; + const obj = value as Record; + return ( + typeof obj.isSuccessful === "boolean" && + typeof obj.isWebSocketConnected === "boolean" && + typeof obj.serverMessage === "string" + ); } function main(): void { -- 2.47.2 From 0736047487b0205bde1a72330947d8e362c58879 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 18 Nov 2025 22:20:38 +0000 Subject: [PATCH 12/14] Update frontend/local-client-cli/src/cli.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- frontend/local-client-cli/src/cli.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/local-client-cli/src/cli.ts b/frontend/local-client-cli/src/cli.ts index b0ce096e..2655bd38 100644 --- a/frontend/local-client-cli/src/cli.ts +++ b/frontend/local-client-cli/src/cli.ts @@ -138,11 +138,15 @@ async function main(): Promise { if (args.health !== undefined) { const healthFile = args.health; - setInterval(() => { + const healthInterval = setInterval(() => { void client.checkConnection().then((status) => { writeHealthStatus(healthFile, status); }); }, 30 * 1000); // every 30 seconds + const clearHealthInterval = () => clearInterval(healthInterval); + process.on("SIGINT", clearHealthInterval); + process.on("SIGTERM", clearHealthInterval); + process.on("exit", clearHealthInterval); } // Add colored log formatter with level filtering -- 2.47.2 From 7bb7765c84e9f02bf72b77f36b4326e0dc608dbc Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 18 Nov 2025 22:26:53 +0000 Subject: [PATCH 13/14] Lint --- frontend/local-client-cli/src/cli.ts | 4 +++- frontend/local-client-cli/src/healthcheck.ts | 10 ++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/local-client-cli/src/cli.ts b/frontend/local-client-cli/src/cli.ts index 2655bd38..2a4cef98 100644 --- a/frontend/local-client-cli/src/cli.ts +++ b/frontend/local-client-cli/src/cli.ts @@ -143,7 +143,9 @@ async function main(): Promise { writeHealthStatus(healthFile, status); }); }, 30 * 1000); // every 30 seconds - const clearHealthInterval = () => clearInterval(healthInterval); + const clearHealthInterval = (): void => { + clearInterval(healthInterval); + }; process.on("SIGINT", clearHealthInterval); process.on("SIGTERM", clearHealthInterval); process.on("exit", clearHealthInterval); diff --git a/frontend/local-client-cli/src/healthcheck.ts b/frontend/local-client-cli/src/healthcheck.ts index 75c1906f..256cd2d8 100644 --- a/frontend/local-client-cli/src/healthcheck.ts +++ b/frontend/local-client-cli/src/healthcheck.ts @@ -13,11 +13,13 @@ function isHealthStatus(value: unknown): value is NetworkConnectionStatus { return false; } - const obj = value as Record; return ( - typeof obj.isSuccessful === "boolean" && - typeof obj.isWebSocketConnected === "boolean" && - typeof obj.serverMessage === "string" + "isSuccessful" in value && + typeof value.isSuccessful === "boolean" && + "isWebSocketConnected" in value && + typeof value.isWebSocketConnected === "boolean" && + "serverMessage" in value && + typeof value.serverMessage === "string" ); } -- 2.47.2 From 6e5f9d9e47c86498d7fe7239a9b728f0cc57fbec Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 18 Nov 2025 22:27:08 +0000 Subject: [PATCH 14/14] Increase start period --- frontend/local-client-cli/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/local-client-cli/Dockerfile b/frontend/local-client-cli/Dockerfile index 35bd81eb..695ab587 100644 --- a/frontend/local-client-cli/Dockerfile +++ b/frontend/local-client-cli/Dockerfile @@ -18,7 +18,7 @@ LABEL org.opencontainers.image.authors="andras@schmelczer.dev" COPY --from=builder /build/local-client-cli/dist/cli.js /app/cli.js COPY --from=builder /build/local-client-cli/dist/healthcheck.js /app/healthcheck.js -HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=1 \ +HEALTHCHECK --interval=10s --timeout=5s --start-period=60s --retries=1 \ CMD node /app/healthcheck.js /tmp/vaultlink-health.json WORKDIR /vault -- 2.47.2