From bde472d6495bce2f0231400bfd3d31d001fd6176 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 5 Jan 2026 18:35:14 +0200 Subject: [PATCH 01/94] feat(client/standalone): basic service worker attempt --- apps/client/src/desktop.html | 25 ++++- apps/client/src/local-bridge.ts | 67 ++++++++++++ apps/client/src/local-server-worker.ts | 83 ++++++++++++++ apps/client/src/sw.ts | 144 +++++++++++++++++++++++++ 4 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 apps/client/src/local-bridge.ts create mode 100644 apps/client/src/local-server-worker.ts create mode 100644 apps/client/src/sw.ts diff --git a/apps/client/src/desktop.html b/apps/client/src/desktop.html index d4b7b190ca..81ef3b387b 100644 --- a/apps/client/src/desktop.html +++ b/apps/client/src/desktop.html @@ -6,7 +6,7 @@ - + Trilium Notes @@ -24,6 +24,24 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/client-standalone/vite.config.mts b/apps/client-standalone/vite.config.mts new file mode 100644 index 0000000000..5adb366482 --- /dev/null +++ b/apps/client-standalone/vite.config.mts @@ -0,0 +1,33 @@ +import { defineConfig } from 'vite'; +import { join } from 'path'; +import { viteStaticCopy } from 'vite-plugin-static-copy'; + +const assets = ["assets", "stylesheets", "fonts", "translations"]; + +export default defineConfig(() => ({ + root: __dirname, + base: "", + plugins: [ + viteStaticCopy({ + targets: assets.map((asset) => ({ + src: `../${asset}/*`, + dest: asset + })) + }) + ], + server: { + watch: { + ignored: ['!**/node_modules/@triliumnext/**'] + } + }, + build: { + target: "esnext", + outDir: join(__dirname, 'dist'), + emptyOutDir: true, + rollupOptions: { + input: { + main: join(__dirname, 'src', 'index.html') + } + } + } +})); \ No newline at end of file From 24e076cacf90fcad107f6112b8749e70f12932f1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 14:22:04 +0200 Subject: [PATCH 43/94] chore(client-standalone): integrate new files from client --- apps/client-standalone/package.json | 7 ++++ .../src/lightweight/browser_router.ts | 0 .../src/lightweight/browser_routes.ts | 0 .../src/lightweight/cls_provider.ts | 0 .../src/lightweight/crypto_provider.ts | 0 .../src/lightweight/db.sql | 0 .../src/lightweight/messaging_provider.ts | 0 .../src/lightweight/sql_provider.ts | 0 .../src/local-bridge.ts | 0 .../src/local-server-worker.ts | 0 apps/{client => client-standalone}/src/sw.ts | 0 apps/client-standalone/vite.config.mts | 15 ++++++++ apps/client/package.json | 6 +--- apps/client/vite.config.mts | 18 ---------- pnpm-lock.yaml | 34 ++++++++++--------- 15 files changed, 41 insertions(+), 39 deletions(-) rename apps/{client => client-standalone}/src/lightweight/browser_router.ts (100%) rename apps/{client => client-standalone}/src/lightweight/browser_routes.ts (100%) rename apps/{client => client-standalone}/src/lightweight/cls_provider.ts (100%) rename apps/{client => client-standalone}/src/lightweight/crypto_provider.ts (100%) rename apps/{client => client-standalone}/src/lightweight/db.sql (100%) rename apps/{client => client-standalone}/src/lightweight/messaging_provider.ts (100%) rename apps/{client => client-standalone}/src/lightweight/sql_provider.ts (100%) rename apps/{client => client-standalone}/src/local-bridge.ts (100%) rename apps/{client => client-standalone}/src/local-server-worker.ts (100%) rename apps/{client => client-standalone}/src/sw.ts (100%) diff --git a/apps/client-standalone/package.json b/apps/client-standalone/package.json index 55befe3603..11a353d3f5 100644 --- a/apps/client-standalone/package.json +++ b/apps/client-standalone/package.json @@ -5,5 +5,12 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@sqlite.org/sqlite-wasm": "3.51.1-build2", + "@triliumnext/commons": "workspace:*", + "@triliumnext/core": "workspace:*", + "js-sha1": "0.7.0", + "js-sha512": "0.9.0" } } diff --git a/apps/client/src/lightweight/browser_router.ts b/apps/client-standalone/src/lightweight/browser_router.ts similarity index 100% rename from apps/client/src/lightweight/browser_router.ts rename to apps/client-standalone/src/lightweight/browser_router.ts diff --git a/apps/client/src/lightweight/browser_routes.ts b/apps/client-standalone/src/lightweight/browser_routes.ts similarity index 100% rename from apps/client/src/lightweight/browser_routes.ts rename to apps/client-standalone/src/lightweight/browser_routes.ts diff --git a/apps/client/src/lightweight/cls_provider.ts b/apps/client-standalone/src/lightweight/cls_provider.ts similarity index 100% rename from apps/client/src/lightweight/cls_provider.ts rename to apps/client-standalone/src/lightweight/cls_provider.ts diff --git a/apps/client/src/lightweight/crypto_provider.ts b/apps/client-standalone/src/lightweight/crypto_provider.ts similarity index 100% rename from apps/client/src/lightweight/crypto_provider.ts rename to apps/client-standalone/src/lightweight/crypto_provider.ts diff --git a/apps/client/src/lightweight/db.sql b/apps/client-standalone/src/lightweight/db.sql similarity index 100% rename from apps/client/src/lightweight/db.sql rename to apps/client-standalone/src/lightweight/db.sql diff --git a/apps/client/src/lightweight/messaging_provider.ts b/apps/client-standalone/src/lightweight/messaging_provider.ts similarity index 100% rename from apps/client/src/lightweight/messaging_provider.ts rename to apps/client-standalone/src/lightweight/messaging_provider.ts diff --git a/apps/client/src/lightweight/sql_provider.ts b/apps/client-standalone/src/lightweight/sql_provider.ts similarity index 100% rename from apps/client/src/lightweight/sql_provider.ts rename to apps/client-standalone/src/lightweight/sql_provider.ts diff --git a/apps/client/src/local-bridge.ts b/apps/client-standalone/src/local-bridge.ts similarity index 100% rename from apps/client/src/local-bridge.ts rename to apps/client-standalone/src/local-bridge.ts diff --git a/apps/client/src/local-server-worker.ts b/apps/client-standalone/src/local-server-worker.ts similarity index 100% rename from apps/client/src/local-server-worker.ts rename to apps/client-standalone/src/local-server-worker.ts diff --git a/apps/client/src/sw.ts b/apps/client-standalone/src/sw.ts similarity index 100% rename from apps/client/src/sw.ts rename to apps/client-standalone/src/sw.ts diff --git a/apps/client-standalone/vite.config.mts b/apps/client-standalone/vite.config.mts index 5adb366482..c505e86b1d 100644 --- a/apps/client-standalone/vite.config.mts +++ b/apps/client-standalone/vite.config.mts @@ -18,8 +18,20 @@ export default defineConfig(() => ({ server: { watch: { ignored: ['!**/node_modules/@triliumnext/**'] + }, + headers: { + // Required for SharedArrayBuffer which is needed by SQLite WASM OPFS VFS + // See: https://sqlite.org/wasm/doc/trunk/persistence.md#coop-coep + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp" } }, + optimizeDeps: { + exclude: ['@sqlite.org/sqlite-wasm', '@triliumnext/core'] + }, + commonjsOptions: { + transformMixedEsModules: true, + }, build: { target: "esnext", outDir: join(__dirname, 'dist'), @@ -29,5 +41,8 @@ export default defineConfig(() => ({ main: join(__dirname, 'src', 'index.html') } } + }, + define: { + "process.env.IS_PREACT": JSON.stringify("true"), } })); \ No newline at end of file diff --git a/apps/client/package.json b/apps/client/package.json index 319fbae3c1..d4d549b725 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -28,11 +28,9 @@ "@mind-elixir/node-menu": "5.0.1", "@popperjs/core": "2.11.8", "@preact/signals": "2.5.1", - "@sqlite.org/sqlite-wasm": "3.51.1-build2", "@triliumnext/ckeditor5": "workspace:*", "@triliumnext/codemirror": "workspace:*", "@triliumnext/commons": "workspace:*", - "@triliumnext/core": "workspace:*", "@triliumnext/highlightjs": "workspace:*", "@triliumnext/share-theme": "workspace:*", "@triliumnext/split.js": "workspace:*", @@ -49,9 +47,7 @@ "i18next": "25.7.3", "i18next-http-backend": "3.0.2", "jquery": "3.7.1", - "jquery.fancytree": "2.38.5", - "js-sha1": "0.7.0", - "js-sha512": "0.9.0", + "jquery.fancytree": "2.38.5", "jsplumb": "2.15.6", "katex": "0.16.27", "knockout": "3.5.1", diff --git a/apps/client/vite.config.mts b/apps/client/vite.config.mts index e583e813cb..8994234ad0 100644 --- a/apps/client/vite.config.mts +++ b/apps/client/vite.config.mts @@ -56,24 +56,6 @@ export default defineConfig(() => ({ cacheDir: '../../node_modules/.vite/apps/client', base: "", plugins, - server: { - watch: { - // Watch workspace packages for changes - ignored: ['!**/node_modules/@triliumnext/**'] - }, - headers: { - // Required for SharedArrayBuffer which is needed by SQLite WASM OPFS VFS - // See: https://sqlite.org/wasm/doc/trunk/persistence.md#coop-coep - "Cross-Origin-Opener-Policy": "same-origin", - "Cross-Origin-Embedder-Policy": "require-corp" - } - }, - optimizeDeps: { - exclude: ['@sqlite.org/sqlite-wasm', '@triliumnext/core'] - }, - worker: { - format: 'es' - }, resolve: { alias: [ { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1512240a0..9f6ca114bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -193,9 +193,6 @@ importers: '@preact/signals': specifier: 2.5.1 version: 2.5.1(preact@10.28.1) - '@sqlite.org/sqlite-wasm': - specifier: 3.51.1-build2 - version: 3.51.1-build2 '@triliumnext/ckeditor5': specifier: workspace:* version: link:../../packages/ckeditor5 @@ -205,9 +202,6 @@ importers: '@triliumnext/commons': specifier: workspace:* version: link:../../packages/commons - '@triliumnext/core': - specifier: workspace:* - version: link:../../packages/trilium-core '@triliumnext/highlightjs': specifier: workspace:* version: link:../../packages/highlightjs @@ -259,12 +253,6 @@ importers: jquery.fancytree: specifier: 2.38.5 version: 2.38.5(jquery@3.7.1) - js-sha1: - specifier: 0.7.0 - version: 0.7.0 - js-sha512: - specifier: 0.9.0 - version: 0.9.0 jsplumb: specifier: 2.15.6 version: 2.15.6 @@ -360,6 +348,24 @@ importers: specifier: 3.1.4 version: 3.1.4(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.1.3)(lightningcss@1.30.1)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.1)) + apps/client-standalone: + dependencies: + '@sqlite.org/sqlite-wasm': + specifier: 3.51.1-build2 + version: 3.51.1-build2 + '@triliumnext/commons': + specifier: workspace:* + version: link:../../packages/commons + '@triliumnext/core': + specifier: workspace:* + version: link:../../packages/trilium-core + js-sha1: + specifier: 0.7.0 + version: 0.7.0 + js-sha512: + specifier: 0.9.0 + version: 0.9.0 + apps/db-compare: dependencies: colors: @@ -15608,8 +15614,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-editor-multi-root@47.3.0': dependencies: @@ -15632,8 +15636,6 @@ snapshots: '@ckeditor/ckeditor5-table': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-emoji@47.3.0': dependencies: From c0bf294457e766ce216799879ac33a3500823802 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 14:29:23 +0200 Subject: [PATCH 44/94] chore(client-standalone): basic integration for assets --- apps/client-standalone/package.json | 77 ++++++++++++++++++++-- apps/client-standalone/src/vite-env.d.ts | 9 +++ apps/client-standalone/tsconfig.app.json | 26 ++++++++ apps/client-standalone/tsconfig.json | 7 ++ apps/client-standalone/tsconfig.spec.json | 18 +++++ apps/client-standalone/vite.config.mts | 80 ++++++++++++++++++++--- 6 files changed, 205 insertions(+), 12 deletions(-) create mode 100644 apps/client-standalone/src/vite-env.d.ts create mode 100644 apps/client-standalone/tsconfig.app.json create mode 100644 apps/client-standalone/tsconfig.json create mode 100644 apps/client-standalone/tsconfig.spec.json diff --git a/apps/client-standalone/package.json b/apps/client-standalone/package.json index 11a353d3f5..747a05f3d9 100644 --- a/apps/client-standalone/package.json +++ b/apps/client-standalone/package.json @@ -1,16 +1,85 @@ { "name": "@triliumnext/client-standalone", "version": "1.0.0", - "description": "", - "main": "index.js", + "description": "Standalone client for TriliumNext with SQLite WASM backend", + "private": true, + "license": "AGPL-3.0-only", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build", + "dev": "vite dev", + "test": "vitest", + "coverage": "vitest --coverage" }, "dependencies": { + "@excalidraw/excalidraw": "0.18.0", + "@fullcalendar/core": "6.1.20", + "@fullcalendar/daygrid": "6.1.20", + "@fullcalendar/interaction": "6.1.20", + "@fullcalendar/list": "6.1.20", + "@fullcalendar/multimonth": "6.1.20", + "@fullcalendar/timegrid": "6.1.20", + "@maplibre/maplibre-gl-leaflet": "0.1.3", + "@mermaid-js/layout-elk": "0.2.0", + "@mind-elixir/node-menu": "5.0.1", + "@popperjs/core": "2.11.8", + "@preact/signals": "2.5.1", "@sqlite.org/sqlite-wasm": "3.51.1-build2", + "@triliumnext/ckeditor5": "workspace:*", + "@triliumnext/codemirror": "workspace:*", "@triliumnext/commons": "workspace:*", "@triliumnext/core": "workspace:*", + "@triliumnext/highlightjs": "workspace:*", + "@triliumnext/share-theme": "workspace:*", + "@triliumnext/split.js": "workspace:*", + "@zumer/snapdom": "2.0.1", + "autocomplete.js": "0.38.1", + "bootstrap": "5.3.8", + "boxicons": "2.1.4", + "clsx": "2.1.1", + "color": "5.0.3", + "debounce": "3.0.0", + "draggabilly": "3.0.0", + "force-graph": "1.51.0", + "globals": "17.0.0", + "i18next": "25.7.3", + "i18next-http-backend": "3.0.2", + "jquery": "3.7.1", + "jquery.fancytree": "2.38.5", "js-sha1": "0.7.0", - "js-sha512": "0.9.0" + "js-sha512": "0.9.0", + "jsplumb": "2.15.6", + "katex": "0.16.27", + "knockout": "3.5.1", + "leaflet": "1.9.4", + "leaflet-gpx": "2.2.0", + "mark.js": "8.11.1", + "marked": "17.0.1", + "mermaid": "11.12.2", + "mind-elixir": "5.4.0", + "normalize.css": "8.0.1", + "panzoom": "9.4.3", + "preact": "10.28.1", + "react-i18next": "16.5.1", + "react-window": "2.2.3", + "reveal.js": "5.2.1", + "svg-pan-zoom": "3.6.2", + "tabulator-tables": "6.3.1", + "vanilla-js-wheel-zoom": "9.0.4" + }, + "devDependencies": { + "@ckeditor/ckeditor5-inspector": "5.0.0", + "@preact/preset-vite": "2.10.2", + "@types/bootstrap": "5.2.10", + "@types/jquery": "3.5.33", + "@types/leaflet": "1.9.21", + "@types/leaflet-gpx": "1.3.8", + "@types/mark.js": "8.11.12", + "@types/reveal.js": "5.2.2", + "@types/tabulator-tables": "6.3.1", + "copy-webpack-plugin": "13.0.1", + "cross-env": "7.0.3", + "happy-dom": "20.0.11", + "script-loader": "0.7.2", + "vite-plugin-static-copy": "3.1.4" } } diff --git a/apps/client-standalone/src/vite-env.d.ts b/apps/client-standalone/src/vite-env.d.ts new file mode 100644 index 0000000000..28ae7076ee --- /dev/null +++ b/apps/client-standalone/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_APP_TITLE: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/apps/client-standalone/tsconfig.app.json b/apps/client-standalone/tsconfig.app.json new file mode 100644 index 0000000000..07d36e7acf --- /dev/null +++ b/apps/client-standalone/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": [ + "ES2022", + "dom", + "dom.iterable" + ], + "skipLibCheck": true, + "types": [ + "vite/client" + ], + "jsx": "react-jsx", + "jsxImportSource": "preact" + }, + "include": [ + "src/**/*", + "../client/src/**/*" + ], + "exclude": [ + "src/**/*.spec.ts", + "src/**/*.test.ts", + "../client/src/**/*.spec.ts", + "../client/src/**/*.test.ts" + ] +} \ No newline at end of file diff --git a/apps/client-standalone/tsconfig.json b/apps/client-standalone/tsconfig.json new file mode 100644 index 0000000000..a12172e9b2 --- /dev/null +++ b/apps/client-standalone/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.spec.json" } + ] +} \ No newline at end of file diff --git a/apps/client-standalone/tsconfig.spec.json b/apps/client-standalone/tsconfig.spec.json new file mode 100644 index 0000000000..856955c7fc --- /dev/null +++ b/apps/client-standalone/tsconfig.spec.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": [ + "ES2022", + "dom", + "dom.iterable" + ], + "types": [ + "vitest/globals", + "happy-dom" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.test.ts" + ] +} \ No newline at end of file diff --git a/apps/client-standalone/vite.config.mts b/apps/client-standalone/vite.config.mts index c505e86b1d..fbbc23dca2 100644 --- a/apps/client-standalone/vite.config.mts +++ b/apps/client-standalone/vite.config.mts @@ -1,20 +1,81 @@ +import preact from "@preact/preset-vite"; import { defineConfig } from 'vite'; import { join } from 'path'; import { viteStaticCopy } from 'vite-plugin-static-copy'; const assets = ["assets", "stylesheets", "fonts", "translations"]; +const isDev = process.env.NODE_ENV === "development"; + +// Always copy SQLite WASM files so they're available to the module +const sqliteWasmPlugin = viteStaticCopy({ + targets: [ + { + // Copy the entire jswasm directory to maintain the module's expected structure + src: "../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/*", + dest: "node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm" + } + ] +}); + +let plugins: any = [ + preact({ + babel: { + compact: !isDev + } + }), + sqliteWasmPlugin, // Always include SQLite WASM files + viteStaticCopy({ + targets: assets.map((asset) => ({ + src: `../client/src/${asset}/*`, + dest: asset + })) + }) +]; + +if (!isDev) { + plugins = [ + ...plugins, + viteStaticCopy({ + structured: true, + targets: [ + { + src: "../../node_modules/@excalidraw/excalidraw/dist/prod/fonts/*", + dest: "", + } + ] + }) + ] +} + export default defineConfig(() => ({ root: __dirname, + cacheDir: '../../node_modules/.vite/apps/client-standalone', base: "", - plugins: [ - viteStaticCopy({ - targets: assets.map((asset) => ({ - src: `../${asset}/*`, - dest: asset - })) - }) - ], + plugins, + resolve: { + alias: [ + { + find: "react", + replacement: "preact/compat" + }, + { + find: "react-dom", + replacement: "preact/compat" + }, + { + find: "@client", + replacement: join(__dirname, "../client/src") + } + ], + dedupe: [ + "react", + "react-dom", + "preact", + "preact/compat", + "preact/hooks" + ] + }, server: { watch: { ignored: ['!**/node_modules/@triliumnext/**'] @@ -42,6 +103,9 @@ export default defineConfig(() => ({ } } }, + test: { + environment: "happy-dom" + }, define: { "process.env.IS_PREACT": JSON.stringify("true"), } From 89fc89603ef3c2440cfbab7f7cc25a44498627d1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 14:30:29 +0200 Subject: [PATCH 45/94] chore(client-standalone): set up live reload for assets --- apps/client-standalone/vite.config.mts | 46 ++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/apps/client-standalone/vite.config.mts b/apps/client-standalone/vite.config.mts index fbbc23dca2..28b57e71d0 100644 --- a/apps/client-standalone/vite.config.mts +++ b/apps/client-standalone/vite.config.mts @@ -7,6 +7,24 @@ const assets = ["assets", "stylesheets", "fonts", "translations"]; const isDev = process.env.NODE_ENV === "development"; +// Watch client files and trigger reload in development +const clientWatchPlugin = () => ({ + name: 'client-watch', + configureServer(server: any) { + if (isDev) { + // Watch client source files + server.watcher.add('../client/src/**/*'); + server.watcher.on('change', (file: string) => { + if (file.includes('../client/src/')) { + server.ws.send({ + type: 'full-reload' + }); + } + }); + } + } +}); + // Always copy SQLite WASM files so they're available to the module const sqliteWasmPlugin = viteStaticCopy({ targets: [ @@ -29,8 +47,16 @@ let plugins: any = [ targets: assets.map((asset) => ({ src: `../client/src/${asset}/*`, dest: asset - })) - }) + })), + // Enable watching in development + ...(isDev && { + watch: { + reloadPageOnChange: true + } + }) + }), + // Watch client files for changes in development + ...(isDev ? [clientWatchPlugin()] : []) ]; if (!isDev) { @@ -78,7 +104,21 @@ export default defineConfig(() => ({ }, server: { watch: { - ignored: ['!**/node_modules/@triliumnext/**'] + // Watch workspace packages + ignored: ['!**/node_modules/@triliumnext/**'], + // Also watch client assets for live reload + usePolling: false, + interval: 100, + binaryInterval: 300 + }, + // Watch additional directories for changes + fs: { + allow: [ + // Allow access to workspace root + '../../', + // Explicitly allow client directory + '../client/src/' + ] }, headers: { // Required for SharedArrayBuffer which is needed by SQLite WASM OPFS VFS From e76c33c37a02342071ef5b629bd2411650ec0364 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 14:34:41 +0200 Subject: [PATCH 46/94] chore(client-standalone): relocate index file to root --- apps/client-standalone/src/index.html | 2 +- apps/client-standalone/vite.config.mts | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/client-standalone/src/index.html b/apps/client-standalone/src/index.html index 81ef3b387b..8df170bb36 100644 --- a/apps/client-standalone/src/index.html +++ b/apps/client-standalone/src/index.html @@ -36,7 +36,7 @@ // 3) Register SW if ("serviceWorker" in navigator) { - const reg = await navigator.serviceWorker.register("./sw.js", { scope: "/src/" }); + const reg = await navigator.serviceWorker.register("./sw.js", { scope: "/" }); // Optionally wait for activation await navigator.serviceWorker.ready; } diff --git a/apps/client-standalone/vite.config.mts b/apps/client-standalone/vite.config.mts index 28b57e71d0..52f9c5053a 100644 --- a/apps/client-standalone/vite.config.mts +++ b/apps/client-standalone/vite.config.mts @@ -12,10 +12,10 @@ const clientWatchPlugin = () => ({ name: 'client-watch', configureServer(server: any) { if (isDev) { - // Watch client source files - server.watcher.add('../client/src/**/*'); + // Watch client source files (adjusted for new root) + server.watcher.add('../../client/src/**/*'); server.watcher.on('change', (file: string) => { - if (file.includes('../client/src/')) { + if (file.includes('../../client/src/')) { server.ws.send({ type: 'full-reload' }); @@ -30,7 +30,7 @@ const sqliteWasmPlugin = viteStaticCopy({ targets: [ { // Copy the entire jswasm directory to maintain the module's expected structure - src: "../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/*", + src: "../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/*", dest: "node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm" } ] @@ -45,7 +45,7 @@ let plugins: any = [ sqliteWasmPlugin, // Always include SQLite WASM files viteStaticCopy({ targets: assets.map((asset) => ({ - src: `../client/src/${asset}/*`, + src: `../../client/src/${asset}/*`, dest: asset })), // Enable watching in development @@ -66,7 +66,7 @@ if (!isDev) { structured: true, targets: [ { - src: "../../node_modules/@excalidraw/excalidraw/dist/prod/fonts/*", + src: "../../../node_modules/@excalidraw/excalidraw/dist/prod/fonts/*", dest: "", } ] @@ -75,8 +75,8 @@ if (!isDev) { } export default defineConfig(() => ({ - root: __dirname, - cacheDir: '../../node_modules/.vite/apps/client-standalone', + root: join(__dirname, 'src'), // Set src as root so index.html is served from / + cacheDir: '../../../node_modules/.vite/apps/client-standalone', base: "", plugins, resolve: { @@ -115,9 +115,9 @@ export default defineConfig(() => ({ fs: { allow: [ // Allow access to workspace root - '../../', + '../../../', // Explicitly allow client directory - '../client/src/' + '../../client/src/' ] }, headers: { @@ -135,11 +135,11 @@ export default defineConfig(() => ({ }, build: { target: "esnext", - outDir: join(__dirname, 'dist'), + outDir: join(__dirname, 'dist'), // Keep output in parent directory emptyOutDir: true, rollupOptions: { input: { - main: join(__dirname, 'src', 'index.html') + main: join(__dirname, 'src', 'index.html') // Input relative to config file } } }, From cb5b491633f7ee6529058427531e5bcf1e2771d6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 14:42:02 +0200 Subject: [PATCH 47/94] fix(client-standalone): get client scripts to run --- apps/client-standalone/src/desktop.ts | 2 ++ apps/client-standalone/src/index.html | 6 +++--- apps/client-standalone/src/runtime.ts | 2 ++ apps/client-standalone/vite.config.mts | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 apps/client-standalone/src/desktop.ts create mode 100644 apps/client-standalone/src/runtime.ts diff --git a/apps/client-standalone/src/desktop.ts b/apps/client-standalone/src/desktop.ts new file mode 100644 index 0000000000..090b1fb88c --- /dev/null +++ b/apps/client-standalone/src/desktop.ts @@ -0,0 +1,2 @@ +// Re-export desktop from client +export * from "../../client/src/desktop"; diff --git a/apps/client-standalone/src/index.html b/apps/client-standalone/src/index.html index 8df170bb36..bb0300eda6 100644 --- a/apps/client-standalone/src/index.html +++ b/apps/client-standalone/src/index.html @@ -117,9 +117,9 @@ } async function loadScripts() { - const assetPath = glob.assetPath; - await import(`./${assetPath}/runtime.js`); - await import(`./${assetPath}/desktop.js`); + // Import from local re-export files that reference client source + await import("./runtime.ts"); + await import("./desktop.ts"); } bootstrap(); diff --git a/apps/client-standalone/src/runtime.ts b/apps/client-standalone/src/runtime.ts new file mode 100644 index 0000000000..d2a2dfe77b --- /dev/null +++ b/apps/client-standalone/src/runtime.ts @@ -0,0 +1,2 @@ +// Re-export runtime from client +export * from "../../client/src/runtime"; diff --git a/apps/client-standalone/vite.config.mts b/apps/client-standalone/vite.config.mts index 52f9c5053a..c6b21db814 100644 --- a/apps/client-standalone/vite.config.mts +++ b/apps/client-standalone/vite.config.mts @@ -135,11 +135,11 @@ export default defineConfig(() => ({ }, build: { target: "esnext", - outDir: join(__dirname, 'dist'), // Keep output in parent directory + outDir: join(__dirname, 'dist'), emptyOutDir: true, rollupOptions: { input: { - main: join(__dirname, 'src', 'index.html') // Input relative to config file + main: join(__dirname, 'src', 'index.html') } } }, From 4da20f4829fe31b0069a9dfb64ed929ac661667c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 15:11:01 +0200 Subject: [PATCH 48/94] fix(client-standalone): some assets could not be loaded --- apps/client-standalone/src/lightweight/browser_routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client-standalone/src/lightweight/browser_routes.ts b/apps/client-standalone/src/lightweight/browser_routes.ts index c8a9ca171f..8a57d92d84 100644 --- a/apps/client-standalone/src/lightweight/browser_routes.ts +++ b/apps/client-standalone/src/lightweight/browser_routes.ts @@ -47,7 +47,7 @@ export function registerRoutes(router: BrowserRouter): void { function bootstrapRoute() { const iconPacks = iconPackService.getIconPacks(); - const assetPath = "./"; + const assetPath = "."; return { assetPath, From 807ab4be8cdef7aa3ff05db727a37efe5510b5cc Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 15:16:38 +0200 Subject: [PATCH 49/94] fix(client-standalone): build missing .wasm --- apps/client-standalone/vite.config.mts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/client-standalone/vite.config.mts b/apps/client-standalone/vite.config.mts index c6b21db814..7ed6cd0e7c 100644 --- a/apps/client-standalone/vite.config.mts +++ b/apps/client-standalone/vite.config.mts @@ -30,8 +30,8 @@ const sqliteWasmPlugin = viteStaticCopy({ targets: [ { // Copy the entire jswasm directory to maintain the module's expected structure - src: "../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/*", - dest: "node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm" + src: "../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm", + dest: "assets" } ] }); @@ -130,6 +130,9 @@ export default defineConfig(() => ({ optimizeDeps: { exclude: ['@sqlite.org/sqlite-wasm', '@triliumnext/core'] }, + worker: { + format: "es" as const + }, commonjsOptions: { transformMixedEsModules: true, }, From 32c39384ffac16ead0848e5a3b997b7576b3074d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 15:20:59 +0200 Subject: [PATCH 50/94] fix(client-standalone): missing entry point for sw, local-bridge, local-server-worker --- apps/client-standalone/vite.config.mts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/client-standalone/vite.config.mts b/apps/client-standalone/vite.config.mts index 7ed6cd0e7c..2715eafd6c 100644 --- a/apps/client-standalone/vite.config.mts +++ b/apps/client-standalone/vite.config.mts @@ -142,7 +142,21 @@ export default defineConfig(() => ({ emptyOutDir: true, rollupOptions: { input: { - main: join(__dirname, 'src', 'index.html') + main: join(__dirname, 'src', 'index.html'), + sw: join(__dirname, 'src', 'sw.ts'), + 'local-bridge': join(__dirname, 'src', 'local-bridge.ts'), + 'local-server-worker': join(__dirname, 'src', 'local-server-worker.ts') + }, + output: { + entryFileNames: (chunkInfo) => { + // Service worker and other workers should be at root level + if (chunkInfo.name === 'sw' || chunkInfo.name === 'local-server-worker') { + return '[name].js'; + } + return 'src/[name].js'; + }, + chunkFileNames: "src/[name].js", + assetFileNames: "src/[name].[ext]" } } }, From edde0d0f908f9118ea6790857a48a4419222b2c8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 7 Jan 2026 15:50:34 +0200 Subject: [PATCH 51/94] fix(client-standalone): get it to start in prod --- apps/client-standalone/package.json | 1 + apps/client-standalone/src/index.html | 100 +------------------------- apps/client-standalone/src/main.ts | 92 ++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 99 deletions(-) create mode 100644 apps/client-standalone/src/main.ts diff --git a/apps/client-standalone/package.json b/apps/client-standalone/package.json index 747a05f3d9..dd16561cf0 100644 --- a/apps/client-standalone/package.json +++ b/apps/client-standalone/package.json @@ -8,6 +8,7 @@ "build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build", "dev": "vite dev", "test": "vitest", + "start-prod": "pnpm build && pnpm http-server dist -p 8888", "coverage": "vitest --coverage" }, "dependencies": { diff --git a/apps/client-standalone/src/index.html b/apps/client-standalone/src/index.html index bb0300eda6..e8a89912e3 100644 --- a/apps/client-standalone/src/index.html +++ b/apps/client-standalone/src/index.html @@ -24,106 +24,8 @@ - - - - + - - - - - - - -<%- include("./partials/windowGlobal.ejs", locals) %> - - - - - - - - From 7547371ba0e042cf81cfcfa1e8105607c4a8c6b9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 12 Jan 2026 18:44:48 +0200 Subject: [PATCH 80/94] feat(client-standalone): proper integration of server-side locale --- .../translation_provider.ts} | 7 +-- .../src/local-server-worker.ts | 4 +- apps/server/src/main.ts | 5 +- apps/server/src/routes/index.ts | 4 -- apps/server/src/services/i18n.ts | 45 ++-------------- packages/trilium-core/package.json | 15 +++--- packages/trilium-core/src/index.ts | 6 ++- .../src/services/bootstrap_utils.ts | 6 ++- packages/trilium-core/src/services/i18n.ts | 53 ++++++++++++++++--- .../trilium-core/src/services/sql_init.ts | 6 +++ pnpm-lock.yaml | 17 ++---- 11 files changed, 85 insertions(+), 83 deletions(-) rename apps/client-standalone/src/{i18n.ts => lightweight/translation_provider.ts} (72%) diff --git a/apps/client-standalone/src/i18n.ts b/apps/client-standalone/src/lightweight/translation_provider.ts similarity index 72% rename from apps/client-standalone/src/i18n.ts rename to apps/client-standalone/src/lightweight/translation_provider.ts index e2f63479e1..ee3a31f794 100644 --- a/apps/client-standalone/src/i18n.ts +++ b/apps/client-standalone/src/lightweight/translation_provider.ts @@ -1,11 +1,8 @@ +import { LOCALE_IDS } from "@triliumnext/commons"; import i18next from "i18next"; import I18NextHttpBackend from "i18next-http-backend"; -export default async function initTranslations() { - // TODO: Use proper locale. - const locale = "en"; - - // Initialize translations. +export default async function translationProvider(locale: LOCALE_IDS) { await i18next.use(I18NextHttpBackend).init({ lng: locale, fallbackLng: "en", diff --git a/apps/client-standalone/src/local-server-worker.ts b/apps/client-standalone/src/local-server-worker.ts index 786321585e..7f77525e28 100644 --- a/apps/client-standalone/src/local-server-worker.ts +++ b/apps/client-standalone/src/local-server-worker.ts @@ -2,13 +2,13 @@ // This will eventually import your core server and DB provider. // import { createCoreServer } from "@trilium/core"; (bundled) -import initTranslations from './i18n'; import { BrowserRouter } from './lightweight/browser_router'; import { createConfiguredRouter } from './lightweight/browser_routes'; import BrowserExecutionContext from './lightweight/cls_provider'; import BrowserCryptoProvider from './lightweight/crypto_provider'; import WorkerMessagingProvider from './lightweight/messaging_provider'; import BrowserSqlProvider from './lightweight/sql_provider'; +import translationProvider from './lightweight/translation_provider'; // Global error handlers - MUST be set up before any async imports self.onerror = (message, source, lineno, colno, error) => { @@ -101,12 +101,12 @@ async function initialize(): Promise { console.log("[Worker] Database loaded"); console.log("[Worker] Loading @triliumnext/core..."); - initTranslations(); coreModule = await import("@triliumnext/core"); coreModule.initializeCore({ executionContext: new BrowserExecutionContext(), crypto: new BrowserCryptoProvider(), messaging: messagingProvider, + translations: translationProvider, dbConfig: { provider: sqlProvider, isReadOnly: false, diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 7dd11fa64b..ceb5fca437 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -43,10 +43,9 @@ async function startApplication() { } }, crypto: new NodejsCryptoProvider(), - executionContext: new ClsHookedExecutionContext() + executionContext: new ClsHookedExecutionContext(), + translations: (await import("./services/i18n.js")).initializeTranslations }); - const { initializeTranslations } = (await import("./services/i18n.js")); - await initializeTranslations(); const startTriliumServer = (await import("./www.js")).default; await startTriliumServer(); } diff --git a/apps/server/src/routes/index.ts b/apps/server/src/routes/index.ts index e81b546448..4024bc9bd4 100644 --- a/apps/server/src/routes/index.ts +++ b/apps/server/src/routes/index.ts @@ -8,7 +8,6 @@ import appPath from "../services/app_path.js"; import assetPath from "../services/asset_path.js"; import attributeService from "../services/attributes.js"; import config from "../services/config.js"; -import { getCurrentLocale } from "../services/i18n.js"; import log from "../services/log.js"; import optionService from "../services/options.js"; import { isDev, isElectron, isWindows11 } from "../services/utils.js"; @@ -30,7 +29,6 @@ export function bootstrap(req: Request, res: Response) { const themeNote = attributeService.getNoteWithLabel("appTheme", theme); const nativeTitleBarVisible = options.nativeTitleBarVisible === "true"; const iconPacks = iconPackService.getIconPacks(); - const currentLocale = getCurrentLocale(); res.send({ ...getSharedBootstrapItems(assetPath), @@ -49,8 +47,6 @@ export function bootstrap(req: Request, res: Response) { triliumVersion: packageJson.version, appPath, baseApiUrl: 'api/', - currentLocale, - isRtl: !!currentLocale.rtl, iconPackCss: iconPacks .map((p: iconPackService.ProcessedIconPack) => iconPackService.generateCss(p, p.builtin ? `${assetPath}/fonts/${p.fontAttachmentId}.${iconPackService.MIME_TO_EXTENSION_MAPPINGS[p.fontMime]}` diff --git a/apps/server/src/services/i18n.ts b/apps/server/src/services/i18n.ts index cc43a95005..a52367a18a 100644 --- a/apps/server/src/services/i18n.ts +++ b/apps/server/src/services/i18n.ts @@ -1,16 +1,12 @@ -import { type Dayjs, dayjs, type Locale, type LOCALE_IDS,LOCALES, setDayjsLocale } from "@triliumnext/commons"; +import { LOCALE_IDS, setDayjsLocale } from "@triliumnext/commons"; import i18next from "i18next"; import { join } from "path"; -import hidden_subtree from "./hidden_subtree.js"; -import options from "./options.js"; -import sql_init from "./sql_init.js"; -import { getResourceDir } from "./utils.js"; +import { getResourceDir } from "./utils"; -export async function initializeTranslations() { +export async function initializeTranslations(locale: LOCALE_IDS) { const resourceDir = getResourceDir(); const Backend = (await import("i18next-fs-backend/cjs")).default; - const locale = getCurrentLanguage(); // Initialize translations await i18next.use(Backend).init({ @@ -25,38 +21,3 @@ export async function initializeTranslations() { // Initialize dayjs locale. await setDayjsLocale(locale); } - -export function ordinal(date: Dayjs) { - return dayjs(date) - .format("Do"); -} - -export function getLocales(): Locale[] { - return LOCALES; -} - -function getCurrentLanguage(): LOCALE_IDS { - let language: string | null = null; - if (sql_init.isDbInitialized()) { - language = options.getOptionOrNull("locale"); - } - - if (!language) { - console.info("Language option not found, falling back to en."); - language = "en"; - } - - return language as LOCALE_IDS; -} - -export async function changeLanguage(locale: string) { - await i18next.changeLanguage(locale); - hidden_subtree.checkHiddenSubtree(true, { restoreNames: true }); -} - -export function getCurrentLocale() { - const localeId = options.getOptionOrNull("locale") ?? "en"; - const currentLocale = LOCALES.find(l => l.id === localeId); - if (!currentLocale) return LOCALES.find(l => l.id === "en")!; - return currentLocale; -} diff --git a/packages/trilium-core/package.json b/packages/trilium-core/package.json index fd2f90a18c..a575eb1c08 100644 --- a/packages/trilium-core/package.json +++ b/packages/trilium-core/package.json @@ -7,17 +7,18 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { - "@triliumnext/commons": "workspace:*", - "sanitize-html": "2.17.0", "@braintree/sanitize-url": "7.1.1", - "sanitize-filename": "1.6.3", + "@triliumnext/commons": "workspace:*", + "escape-html": "1.0.3", + "i18next": "25.7.3", "mime-types": "3.0.2", - "unescape": "1.0.1", - "escape-html": "1.0.3" + "sanitize-filename": "1.6.3", + "sanitize-html": "2.17.0", + "unescape": "1.0.1" }, "devDependencies": { - "@types/sanitize-html": "2.16.0", + "@types/escape-html": "1.0.4", "@types/mime-types": "3.0.1", - "@types/escape-html": "1.0.4" + "@types/sanitize-html": "2.16.0" } } diff --git a/packages/trilium-core/src/index.ts b/packages/trilium-core/src/index.ts index 8544cd95fc..001e21c169 100644 --- a/packages/trilium-core/src/index.ts +++ b/packages/trilium-core/src/index.ts @@ -4,6 +4,7 @@ import { getLog, initLog } from "./services/log"; import { initSql } from "./services/sql/index"; import { SqlService, SqlServiceParams } from "./services/sql/sql"; import { initMessaging, MessagingProvider } from "./services/messaging/index"; +import { initTranslations, TranslationProvider } from "./services/i18n"; export type * from "./services/sql/types"; export * from "./services/sql/index"; @@ -24,6 +25,7 @@ export { default as hidden_subtree } from "./services/hidden_subtree"; export * as icon_packs from "./services/icon_packs"; export { getContext, type ExecutionContext } from "./services/context"; export * as cls from "./services/context"; +export * as i18n from "./services/i18n"; export * from "./errors"; export { default as getInstanceId } from "./services/instance_id"; export type { CryptoProvider } from "./services/encryption/crypto"; @@ -64,16 +66,18 @@ export type { NoteParams } from "./services/notes"; export * as sanitize from "./services/sanitizer"; export * as routes from "./routes"; -export function initializeCore({ dbConfig, executionContext, crypto, messaging }: { +export function initializeCore({ dbConfig, executionContext, crypto, translations, messaging }: { dbConfig: SqlServiceParams, executionContext: ExecutionContext, crypto: CryptoProvider, + translations: TranslationProvider, messaging?: MessagingProvider }) { initLog(); initCrypto(crypto); initSql(new SqlService(dbConfig, getLog())); initContext(executionContext); + initTranslations(translations); if (messaging) { initMessaging(messaging); } diff --git a/packages/trilium-core/src/services/bootstrap_utils.ts b/packages/trilium-core/src/services/bootstrap_utils.ts index 18762caa2a..19fa1b81c1 100644 --- a/packages/trilium-core/src/services/bootstrap_utils.ts +++ b/packages/trilium-core/src/services/bootstrap_utils.ts @@ -3,10 +3,12 @@ import { getSql } from "./sql"; import protected_session from "./protected_session"; import { generateCss, generateIconRegistry, getIconPacks, MIME_TO_EXTENSION_MAPPINGS } from "./icon_packs"; import options from "./options"; +import { getCurrentLocale } from "./i18n"; -export default function getSharedBootstrapItems(assetPath: string): Pick { +export default function getSharedBootstrapItems(assetPath: string): Pick { const sql = getSql(); const iconPacks = getIconPacks(); + const currentLocale = getCurrentLocale(); return { assetPath, @@ -15,6 +17,8 @@ export default function getSharedBootstrapItems(assetPath: string): Pick generateCss(p, p.builtin diff --git a/packages/trilium-core/src/services/i18n.ts b/packages/trilium-core/src/services/i18n.ts index 1cbf52ba85..c28c897787 100644 --- a/packages/trilium-core/src/services/i18n.ts +++ b/packages/trilium-core/src/services/i18n.ts @@ -1,10 +1,51 @@ -import { LOCALES } from "@triliumnext/commons"; +import { dayjs, Dayjs, Locale, LOCALE_IDS, LOCALES, setDayjsLocale } from "@triliumnext/commons"; +import sql_init from "./sql_init"; +import options from "./options"; +import i18next from "i18next"; +import hidden_subtree from "./hidden_subtree"; -// TODO: Real impl. -export function changeLanguage(languageCode: string) { - console.log("Got request to change language", languageCode); +export type TranslationProvider = (locale: LOCALE_IDS) => Promise; + +export async function initTranslations(translationProvider: TranslationProvider) { + const locale = getCurrentLanguage(); + + await translationProvider(locale); + + // Initialize dayjs locale. + await setDayjsLocale(locale); } -export function getLocales() { - return LOCALES; +export function ordinal(date: Dayjs) { + return dayjs(date) + .format("Do"); +} + +export function getLocales(): Locale[] { + return LOCALES; +} + +function getCurrentLanguage(): LOCALE_IDS { + let language: string | null = null; + if (sql_init.isDbInitialized()) { + language = options.getOptionOrNull("locale"); + } + + if (!language) { + console.info("Language option not found, falling back to en."); + language = "en"; + } + + return language as LOCALE_IDS; +} + +export async function changeLanguage(locale: string) { + await i18next.changeLanguage(locale); + hidden_subtree.checkHiddenSubtree(true, { restoreNames: true }); +} + +export function getCurrentLocale() { + const localeId = options.getOptionOrNull("locale") ?? "en"; + const currentLocale = LOCALES.find(l => l.id === localeId); + if (!currentLocale) return LOCALES.find(l => l.id === "en")!; + return currentLocale; } diff --git a/packages/trilium-core/src/services/sql_init.ts b/packages/trilium-core/src/services/sql_init.ts index ec44193cb7..6d3b509c3c 100644 --- a/packages/trilium-core/src/services/sql_init.ts +++ b/packages/trilium-core/src/services/sql_init.ts @@ -6,3 +6,9 @@ export const dbReady = deferred(); setTimeout(() => { dbReady.resolve(); }, 850); + +function isDbInitialized() { + return true; +} + +export default { isDbInitialized }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd9ba09627..e46ecc8a50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1639,6 +1639,9 @@ importers: escape-html: specifier: 1.0.3 version: 1.0.3 + i18next: + specifier: 25.7.3 + version: 25.7.3(typescript@5.9.3) mime-types: specifier: 3.0.2 version: 3.0.2 @@ -15784,8 +15787,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-highlight@47.3.0': dependencies: @@ -15794,8 +15795,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-horizontal-line@47.3.0': dependencies: @@ -15805,8 +15804,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.3.0 '@ckeditor/ckeditor5-widget': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-html-embed@47.3.0': dependencies: @@ -15816,8 +15813,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.3.0 '@ckeditor/ckeditor5-widget': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-html-support@47.3.0': dependencies: @@ -15833,8 +15828,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 47.3.0 ckeditor5: 47.3.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-icons@47.3.0': {} @@ -15875,8 +15868,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.3.0 '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-inspector@5.0.0': {} @@ -15979,6 +15970,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.3.0 ckeditor5: 47.3.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-merge-fields@47.3.0': dependencies: From f9731d9cfce8118f9805878b0a3e3fffc674ed30 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 12 Jan 2026 19:00:35 +0200 Subject: [PATCH 81/94] chore(text): re-enable emojis --- apps/client/src/widgets/type_widgets/text/config.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/text/config.ts b/apps/client/src/widgets/type_widgets/text/config.ts index e2b20192f2..29b1a02699 100644 --- a/apps/client/src/widgets/type_widgets/text/config.ts +++ b/apps/client/src/widgets/type_widgets/text/config.ts @@ -133,12 +133,11 @@ export async function buildConfig(opts: BuildEditorOptions): Promise { await ensureMimeTypesForHighlighting(); From 0c52b56e02a792d4bed82b84706c6eaad1892f41 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 12 Jan 2026 19:25:45 +0200 Subject: [PATCH 82/94] chore(core): integrate branches service and route --- apps/server/src/routes/routes.ts | 12 +---- .../server/src/services/backend_script_api.ts | 3 +- apps/server/src/services/bulk_actions.ts | 3 +- apps/server/src/services/utils.ts | 7 +-- packages/trilium-core/src/index.ts | 1 + .../trilium-core}/src/routes/api/branches.ts | 31 ++++++------ packages/trilium-core/src/routes/index.ts | 10 ++++ .../trilium-core/src/services/branches.ts | 50 +++++++++++++++++++ .../trilium-core/src/services/utils/index.ts | 5 ++ 9 files changed, 88 insertions(+), 34 deletions(-) rename {apps/server => packages/trilium-core}/src/routes/api/branches.ts (86%) create mode 100644 packages/trilium-core/src/services/branches.ts diff --git a/apps/server/src/routes/routes.ts b/apps/server/src/routes/routes.ts index ebd3244f01..55fa178458 100644 --- a/apps/server/src/routes/routes.ts +++ b/apps/server/src/routes/routes.ts @@ -1,3 +1,4 @@ +import { routes } from "@triliumnext/core"; import { createPartialContentHandler } from "@triliumnext/express-partial-content"; import express from "express"; import rateLimit from "express-rate-limit"; @@ -21,7 +22,6 @@ import appInfoRoute from "./api/app_info.js"; import attributesRoute from "./api/attributes.js"; import autocompleteApiRoute from "./api/autocomplete.js"; import backendLogRoute from "./api/backend_log.js"; -import branchesApiRoute from "./api/branches.js"; import bulkActionRoute from "./api/bulk_action.js"; import clipperRoute from "./api/clipper.js"; import cloningApiRoute from "./api/cloning.js"; @@ -62,7 +62,6 @@ import loginRoute from "./login.js"; import { apiResultHandler, apiRoute, asyncApiRoute, asyncRoute, route, router, uploadMiddlewareWithErrorHandling } from "./route_api.js"; // page routes import setupRoute from "./setup.js"; -import { routes } from "@triliumnext/core"; const GET = "get", PST = "post", @@ -124,15 +123,6 @@ function register(app: express.Application) { apiRoute(PST, "/api/notes/:noteId/save-to-tmp-dir", filesRoute.saveNoteToTmpDir); apiRoute(PST, "/api/notes/:noteId/upload-modified-file", filesRoute.uploadModifiedFileToNote); - apiRoute(PUT, "/api/branches/:branchId/move-to/:parentBranchId", branchesApiRoute.moveBranchToParent); - apiRoute(PUT, "/api/branches/:branchId/move-before/:beforeBranchId", branchesApiRoute.moveBranchBeforeNote); - apiRoute(PUT, "/api/branches/:branchId/move-after/:afterBranchId", branchesApiRoute.moveBranchAfterNote); - apiRoute(PUT, "/api/branches/:branchId/expanded/:expanded", branchesApiRoute.setExpanded); - apiRoute(PUT, "/api/branches/:branchId/expanded-subtree/:expanded", branchesApiRoute.setExpandedForSubtree); - apiRoute(DEL, "/api/branches/:branchId", branchesApiRoute.deleteBranch); - apiRoute(PUT, "/api/branches/:branchId/set-prefix", branchesApiRoute.setPrefix); - apiRoute(PUT, "/api/branches/set-prefix-batch", branchesApiRoute.setPrefixBatch); - // TODO: Bring back attachment uploading // route(PST, "/api/notes/:noteId/attachments/upload", [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], attachmentsApiRoute.uploadAttachment, apiResultHandler); route(GET, "/api/attachments/:attachmentId/image/:filename", [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage); diff --git a/apps/server/src/services/backend_script_api.ts b/apps/server/src/services/backend_script_api.ts index 5d1d86331f..6fb3e24ec8 100644 --- a/apps/server/src/services/backend_script_api.ts +++ b/apps/server/src/services/backend_script_api.ts @@ -1,5 +1,5 @@ import { type AttributeRow, dayjs, formatLogMessage } from "@triliumnext/commons"; -import { type AbstractBeccaEntity, Becca, NoteParams } from "@triliumnext/core"; +import { type AbstractBeccaEntity, Becca, branches as branchService, NoteParams } from "@triliumnext/core"; import axios from "axios"; import * as cheerio from "cheerio"; import xml2js from "xml2js"; @@ -16,7 +16,6 @@ import appInfo from "./app_info.js"; import attributeService from "./attributes.js"; import type { ApiParams } from "./backend_script_api_interface.js"; import backupService from "./backup.js"; -import branchService from "./branches.js"; import cloningService from "./cloning.js"; import config from "./config.js"; import dateNoteService from "./date_notes.js"; diff --git a/apps/server/src/services/bulk_actions.ts b/apps/server/src/services/bulk_actions.ts index 2c662fba2d..7387dc64a4 100644 --- a/apps/server/src/services/bulk_actions.ts +++ b/apps/server/src/services/bulk_actions.ts @@ -1,9 +1,8 @@ import { ActionHandlers, BulkAction, BulkActionData } from "@triliumnext/commons"; -import { erase as eraseService } from "@triliumnext/core"; +import { branches as branchService, erase as eraseService } from "@triliumnext/core"; import becca from "../becca/becca.js"; import type BNote from "../becca/entities/bnote.js"; -import branchService from "./branches.js"; import cloningService from "./cloning.js"; import log from "./log.js"; import { randomString } from "./utils.js"; diff --git a/apps/server/src/services/utils.ts b/apps/server/src/services/utils.ts index 86cfd87f3a..cf79115314 100644 --- a/apps/server/src/services/utils.ts +++ b/apps/server/src/services/utils.ts @@ -87,11 +87,6 @@ export function constantTimeCompare(a: string | null | undefined, b: string | nu return crypto.timingSafeEqual(bufA, bufB); } -export function isEmptyOrWhitespace(str: string | null | undefined) { - if (!str) return true; - return str.match(/^ *$/) !== null; -} - export function sanitizeSqlIdentifier(str: string) { return str.replace(/[^A-Za-z0-9_]/g, ""); } @@ -457,6 +452,8 @@ export const unescapeHtml = coreUtils.unescapeHtml; export const randomSecureToken = coreUtils.randomSecureToken; /** @deprecated */ export const safeExtractMessageAndStackFromError = coreUtils.safeExtractMessageAndStackFromError; +/** @deprecated */ +export const isEmptyOrWhitespace = coreUtils.isEmptyOrWhitespace; export default { compareVersions, diff --git a/packages/trilium-core/src/index.ts b/packages/trilium-core/src/index.ts index 001e21c169..de18e947c3 100644 --- a/packages/trilium-core/src/index.ts +++ b/packages/trilium-core/src/index.ts @@ -37,6 +37,7 @@ export { default as TaskContext } from "./services/task_context"; export { default as revisions } from "./services/revisions"; export { default as erase } from "./services/erase"; export { default as getSharedBootstrapItems } from "./services/bootstrap_utils"; +export { default as branches } from "./services/branches"; // Messaging system export * from "./services/messaging/index"; diff --git a/apps/server/src/routes/api/branches.ts b/packages/trilium-core/src/routes/api/branches.ts similarity index 86% rename from apps/server/src/routes/api/branches.ts rename to packages/trilium-core/src/routes/api/branches.ts index 26f030e471..86c926a7c7 100644 --- a/apps/server/src/routes/api/branches.ts +++ b/packages/trilium-core/src/routes/api/branches.ts @@ -1,14 +1,16 @@ -import { erase as eraseService, events as eventService, ValidationError } from "@triliumnext/core"; +import branchService from "../../services/branches.js"; +import eraseService from "../../services/erase.js"; +import eventService from "../../services/events.js"; import type { Request } from "express"; import becca from "../../becca/becca.js"; -import branchService from "../../services/branches.js"; import entityChangesService from "../../services/entity_changes.js"; -import log from "../../services/log.js"; -import sql from "../../services/sql.js"; +import { getLog } from "../../services/log.js"; import TaskContext from "../../services/task_context.js"; import treeService from "../../services/tree.js"; -import utils from "../../services/utils.js"; +import { isEmptyOrWhitespace, randomString } from "../../services/utils/index.js"; +import { getSql } from "../../services/sql/index.js"; +import { ValidationError } from "../../errors.js"; /** * Code in this file deals with moving and cloning branches. The relationship between note and parent note is unique @@ -45,7 +47,7 @@ function moveBranchBeforeNote(req: Request) { // we don't change utcDateModified, so other changes are prioritized in case of conflict // also we would have to sync all those modified branches otherwise hash checks would fail - sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition >= ? AND isDeleted = 0", [beforeBranch.parentNoteId, originalBeforeNotePosition]); + getSql().execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition >= ? AND isDeleted = 0", [beforeBranch.parentNoteId, originalBeforeNotePosition]); // also need to update becca positions const parentNote = becca.getNoteOrThrow(beforeBranch.parentNoteId); @@ -71,7 +73,7 @@ function moveBranchBeforeNote(req: Request) { // if sorting is not needed, then still the ordering might have changed above manually entityChangesService.putNoteReorderingEntityChange(parentNote.noteId); - log.info(`Moved note ${branchToMove.noteId}, branch ${branchId} before note ${beforeBranch.noteId}, branch ${beforeBranchId}`); + getLog().info(`Moved note ${branchToMove.noteId}, branch ${branchId} before note ${beforeBranch.noteId}, branch ${beforeBranchId}`); return { success: true }; } @@ -92,7 +94,7 @@ function moveBranchAfterNote(req: Request) { // we don't change utcDateModified, so other changes are prioritized in case of conflict // also we would have to sync all those modified branches otherwise hash checks would fail - sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0", [afterNote.parentNoteId, originalAfterNotePosition]); + getSql().execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0", [afterNote.parentNoteId, originalAfterNotePosition]); // also need to update becca positions const parentNote = becca.getNoteOrThrow(afterNote.parentNoteId); @@ -120,7 +122,7 @@ function moveBranchAfterNote(req: Request) { // if sorting is not needed, then still the ordering might have changed above manually entityChangesService.putNoteReorderingEntityChange(parentNote.noteId); - log.info(`Moved note ${branchToMove.noteId}, branch ${branchId} after note ${afterNote.noteId}, branch ${afterBranchId}`); + getLog().info(`Moved note ${branchToMove.noteId}, branch ${branchId} after note ${afterNote.noteId}, branch ${afterBranchId}`); return { success: true }; } @@ -130,7 +132,7 @@ function setExpanded(req: Request) { const expanded = parseInt(req.params.expanded); if (branchId !== "none_root") { - sql.execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]); + getSql().execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]); // we don't sync expanded label // also this does not trigger updates to the frontend, this would trigger too many reloads @@ -150,6 +152,7 @@ function setExpanded(req: Request) { function setExpandedForSubtree(req: Request) { const { branchId } = req.params; const expanded = parseInt(req.params.expanded); + const sql = getSql(); let branchIds = sql.getColumn( ` @@ -236,7 +239,7 @@ function deleteBranch(req: Request) { const taskContext = TaskContext.getInstance(req.query.taskId as string, "deleteNotes", null); - const deleteId = utils.randomString(10); + const deleteId = randomString(10); let noteDeleted; if (eraseNotes) { @@ -260,7 +263,7 @@ function deleteBranch(req: Request) { function setPrefix(req: Request) { const branchId = req.params.branchId; //TriliumNextTODO: req.body arrives as string, so req.body.prefix will be undefined – did the code below ever even work? - const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix; + const prefix = isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix; const branch = becca.getBranchOrThrow(branchId); branch.prefix = prefix; @@ -279,7 +282,7 @@ function setPrefixBatch(req: Request) { throw new ValidationError("prefix must be a string or null"); } - const normalizedPrefix = utils.isEmptyOrWhitespace(prefix) ? null : prefix; + const normalizedPrefix = isEmptyOrWhitespace(prefix) ? null : prefix; let updatedCount = 0; for (const branchId of branchIds) { @@ -289,7 +292,7 @@ function setPrefixBatch(req: Request) { branch.save(); updatedCount++; } else { - log.info(`Branch ${branchId} not found, skipping prefix update`); + getLog().info(`Branch ${branchId} not found, skipping prefix update`); } } diff --git a/packages/trilium-core/src/routes/index.ts b/packages/trilium-core/src/routes/index.ts index 7ee403724c..b87f85b982 100644 --- a/packages/trilium-core/src/routes/index.ts +++ b/packages/trilium-core/src/routes/index.ts @@ -6,6 +6,7 @@ import attachmentsApiRoute from "./api/attachments"; import noteMapRoute from "./api/note_map"; import recentNotesRoute from "./api/recent_notes"; import otherRoute from "./api/others"; +import branchesApiRoute from "./api/branches"; import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity"; // TODO: Deduplicate with routes.ts @@ -53,6 +54,15 @@ export function buildSharedApiRoutes(apiRoute: any) { apiRoute(PUT, "/api/attachments/:attachmentId/rename", attachmentsApiRoute.renameAttachment); apiRoute(GET, "/api/attachments/:attachmentId/blob", attachmentsApiRoute.getAttachmentBlob); + apiRoute(PUT, "/api/branches/:branchId/move-to/:parentBranchId", branchesApiRoute.moveBranchToParent); + apiRoute(PUT, "/api/branches/:branchId/move-before/:beforeBranchId", branchesApiRoute.moveBranchBeforeNote); + apiRoute(PUT, "/api/branches/:branchId/move-after/:afterBranchId", branchesApiRoute.moveBranchAfterNote); + apiRoute(PUT, "/api/branches/:branchId/expanded/:expanded", branchesApiRoute.setExpanded); + apiRoute(PUT, "/api/branches/:branchId/expanded-subtree/:expanded", branchesApiRoute.setExpandedForSubtree); + apiRoute(DEL, "/api/branches/:branchId", branchesApiRoute.deleteBranch); + apiRoute(PUT, "/api/branches/:branchId/set-prefix", branchesApiRoute.setPrefix); + apiRoute(PUT, "/api/branches/set-prefix-batch", branchesApiRoute.setPrefixBatch); + apiRoute(GET, "/api/note-map/:noteId/backlink-count", noteMapRoute.getBacklinkCount); apiRoute(PST, "/api/recent-notes", recentNotesRoute.addRecentNote); diff --git a/packages/trilium-core/src/services/branches.ts b/packages/trilium-core/src/services/branches.ts new file mode 100644 index 0000000000..3b27de5226 --- /dev/null +++ b/packages/trilium-core/src/services/branches.ts @@ -0,0 +1,50 @@ +import treeService from "./tree.js"; +import type BBranch from "../becca/entities/bbranch.js"; +import { getSql } from "./sql/index.js"; + +function moveBranchToNote(branchToMove: BBranch, targetParentNoteId: string) { + if (branchToMove.parentNoteId === targetParentNoteId) { + return { success: true }; // no-op + } + + const validationResult = treeService.validateParentChild(targetParentNoteId, branchToMove.noteId, branchToMove.branchId); + + if (!validationResult.success) { + return [200, validationResult]; + } + + const maxNotePos = getSql().getValue("SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [targetParentNoteId]); + const newNotePos = !maxNotePos ? 0 : maxNotePos + 10; + + const newBranch = branchToMove.createClone(targetParentNoteId, newNotePos); + newBranch.save(); + + branchToMove.markAsDeleted(); + + return { + success: true, + branch: newBranch + }; +} + +function moveBranchToBranch(branchToMove: BBranch, targetParentBranch: BBranch, branchId: string) { + // TODO: Unused branch ID argument. + const res = moveBranchToNote(branchToMove, targetParentBranch.noteId); + + if (!("success" in res) || !res.success) { + return res; + } + + // expanding so that the new placement of the branch is immediately visible + if (!targetParentBranch.isExpanded) { + targetParentBranch.isExpanded = true; + targetParentBranch.save(); + } + + return res; +} + +export default { + moveBranchToBranch, + moveBranchToNote +}; diff --git a/packages/trilium-core/src/services/utils/index.ts b/packages/trilium-core/src/services/utils/index.ts index f172452fff..bba53a0b93 100644 --- a/packages/trilium-core/src/services/utils/index.ts +++ b/packages/trilium-core/src/services/utils/index.ts @@ -130,3 +130,8 @@ export function randomSecureToken(bytes = 32) { export function safeExtractMessageAndStackFromError(err: unknown): [errMessage: string, errStack: string | undefined] { return (err instanceof Error) ? [err.message, err.stack] as const : ["Unknown Error", undefined] as const; } + +export function isEmptyOrWhitespace(str: string | null | undefined) { + if (!str) return true; + return str.match(/^ *$/) !== null; +} From e1c798561b9952a97321be7a00400fa1b773f8a8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 12 Jan 2026 20:46:08 +0200 Subject: [PATCH 83/94] fix(client-standalone): user guide not working --- apps/client/src/services/doc_renderer.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/apps/client/src/services/doc_renderer.ts b/apps/client/src/services/doc_renderer.ts index 1ae60fb9ca..63879d3ca7 100644 --- a/apps/client/src/services/doc_renderer.ts +++ b/apps/client/src/services/doc_renderer.ts @@ -5,7 +5,7 @@ import { formatCodeBlocks } from "./syntax_highlight.js"; export default function renderDoc(note: FNote) { return new Promise>((resolve) => { - let docName = note.getLabelValue("docName"); + const docName = note.getLabelValue("docName"); const $content = $("
"); if (docName) { @@ -16,7 +16,7 @@ export default function renderDoc(note: FNote) { if (status === "error") { const fallbackUrl = getUrl(docName, "en"); $content.load(fallbackUrl, async () => { - await processContent(fallbackUrl, $content) + await processContent(fallbackUrl, $content); resolve($content); }); return; @@ -37,9 +37,9 @@ async function processContent(url: string, $content: JQuery) { const dir = url.substring(0, url.lastIndexOf("/")); // Images are relative to the docnote but that will not work when rendered in the application since the path breaks. - $content.find("img").each((i, el) => { + $content.find("img").each((_i, el) => { const $img = $(el); - $img.attr("src", dir + "/" + $img.attr("src")); + $img.attr("src", `${dir}/${$img.attr("src")}`); }); formatCodeBlocks($content); @@ -51,7 +51,16 @@ async function processContent(url: string, $content: JQuery) { function getUrl(docNameValue: string, language: string) { // Cannot have spaces in the URL due to how JQuery.load works. docNameValue = docNameValue.replaceAll(" ", "%20"); - - const basePath = window.glob.isDev ? window.glob.assetPath + "/.." : window.glob.assetPath; - return `${basePath}/doc_notes/${language}/${docNameValue}.html`; + if (docNameValue.includes("User%20Guide")) language = "en"; + return `${getBasePath()}/doc_notes/${language}/${docNameValue}.html`; +} + +function getBasePath() { + if (window.glob.isStandalone) { + return `server-assets`; + } + if (window.glob.isDev) { + return `${window.glob.assetPath }/..`; + } + return window.glob.assetPath; } From 659573b864f3efabb20279e6e10308e4404de471 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 12 Jan 2026 20:50:12 +0200 Subject: [PATCH 84/94] fix(client-standalone): update version to match --- apps/client-standalone/package.json | 4 ++-- apps/client/package.json | 2 +- apps/server/package.json | 12 ++++++------ scripts/update-nightly-version.ts | 2 +- scripts/update-version.ts | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/client-standalone/package.json b/apps/client-standalone/package.json index dd16561cf0..7a59bc3d96 100644 --- a/apps/client-standalone/package.json +++ b/apps/client-standalone/package.json @@ -1,6 +1,6 @@ { "name": "@triliumnext/client-standalone", - "version": "1.0.0", + "version": "0.101.3", "description": "Standalone client for TriliumNext with SQLite WASM backend", "private": true, "license": "AGPL-3.0-only", @@ -83,4 +83,4 @@ "script-loader": "0.7.2", "vite-plugin-static-copy": "3.1.4" } -} +} \ No newline at end of file diff --git a/apps/client/package.json b/apps/client/package.json index 7d414a4929..1f08b90305 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -47,7 +47,7 @@ "i18next": "25.7.3", "i18next-http-backend": "3.0.2", "jquery": "3.7.1", - "jquery.fancytree": "2.38.5", + "jquery.fancytree": "2.38.5", "jsplumb": "2.15.6", "katex": "0.16.27", "knockout": "3.5.1", diff --git a/apps/server/package.json b/apps/server/package.json index daa44d3c81..805a0f9bcc 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -35,7 +35,7 @@ "sucrase": "3.35.1" }, "devDependencies": { - "@anthropic-ai/sdk": "0.71.2", + "@anthropic-ai/sdk": "0.71.2", "@electron/remote": "2.1.3", "@triliumnext/commons": "workspace:*", "@triliumnext/core": "workspace:*", @@ -48,14 +48,14 @@ "@types/compression": "1.8.1", "@types/cookie-parser": "1.4.10", "@types/debounce": "1.2.4", - "@types/ejs": "3.1.5", + "@types/ejs": "3.1.5", "@types/express-http-proxy": "1.6.7", "@types/express-session": "1.18.2", "@types/fs-extra": "11.0.4", "@types/html": "1.0.4", - "@types/ini": "4.1.1", + "@types/ini": "4.1.1", "@types/multer": "2.0.0", - "@types/safe-compare": "1.1.2", + "@types/safe-compare": "1.1.2", "@types/sax": "1.2.7", "@types/serve-favicon": "2.5.7", "@types/serve-static": "2.2.0", @@ -82,7 +82,7 @@ "ejs": "3.1.10", "electron": "39.2.7", "electron-debug": "4.1.0", - "electron-window-state": "5.0.3", + "electron-window-state": "5.0.3", "express": "5.2.1", "express-http-proxy": "2.1.2", "express-openid-connect": "2.19.4", @@ -109,7 +109,7 @@ "ollama": "0.6.3", "openai": "6.16.0", "rand-token": "1.0.1", - "safe-compare": "1.1.4", + "safe-compare": "1.1.4", "sax": "1.4.3", "serve-favicon": "2.5.1", "stream-throttle": "0.1.3", diff --git a/scripts/update-nightly-version.ts b/scripts/update-nightly-version.ts index 71f5a96847..39fb202774 100644 --- a/scripts/update-nightly-version.ts +++ b/scripts/update-nightly-version.ts @@ -46,7 +46,7 @@ function main() { const rootPackageJson = join(scriptDir, "..", "package.json"); patchPackageJson(rootPackageJson); - for (const app of ["server", "client"]) { + for (const app of ["server", "client", "client-standalone"]) { const appPackageJsonPath = join(scriptDir, "..", "apps", app, "package.json"); patchPackageJson(appPackageJsonPath); } diff --git a/scripts/update-version.ts b/scripts/update-version.ts index fd00ff35d2..a0a16c2cef 100644 --- a/scripts/update-version.ts +++ b/scripts/update-version.ts @@ -26,7 +26,7 @@ function getVersion(packageJsonPath: string) { function main() { const version = getVersion(join(__dirname, "..", "package.json")); - for (const appName of ["server", "client", "desktop"]) { + for (const appName of ["server", "client", "client-standalone", "desktop"]) { patchPackageJson(join(__dirname, "..", "apps", appName, "package.json"), version); } From 92f71e100f9e3330c9a8778f5d6556b40845e4cc Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 12 Jan 2026 20:54:18 +0200 Subject: [PATCH 85/94] chore(core): integrate app_info route --- apps/server/src/routes/routes.ts | 2 -- .../server => packages/trilium-core}/src/routes/api/app_info.ts | 0 packages/trilium-core/src/routes/index.ts | 2 ++ 3 files changed, 2 insertions(+), 2 deletions(-) rename {apps/server => packages/trilium-core}/src/routes/api/app_info.ts (100%) diff --git a/apps/server/src/routes/routes.ts b/apps/server/src/routes/routes.ts index 55fa178458..5fe774cbce 100644 --- a/apps/server/src/routes/routes.ts +++ b/apps/server/src/routes/routes.ts @@ -18,7 +18,6 @@ import openID from '../services/open_id.js'; import { isElectron } from "../services/utils.js"; import shareRoutes from "../share/routes.js"; import anthropicRoute from "./api/anthropic.js"; -import appInfoRoute from "./api/app_info.js"; import attributesRoute from "./api/attributes.js"; import autocompleteApiRoute from "./api/autocomplete.js"; import backendLogRoute from "./api/backend_log.js"; @@ -189,7 +188,6 @@ function register(app: express.Application) { route(PST, "/api/sync/queue-sector/:entityName/:sector", [auth.checkApiAuth], syncApiRoute.queueSector, apiResultHandler); route(GET, "/api/sync/stats", [], syncApiRoute.getStats, apiResultHandler); - apiRoute(GET, "/api/app-info", appInfoRoute.getAppInfo); apiRoute(GET, "/api/metrics", metricsRoute.getMetrics); apiRoute(GET, "/api/system-checks", systemInfoRoute.systemChecks); diff --git a/apps/server/src/routes/api/app_info.ts b/packages/trilium-core/src/routes/api/app_info.ts similarity index 100% rename from apps/server/src/routes/api/app_info.ts rename to packages/trilium-core/src/routes/api/app_info.ts diff --git a/packages/trilium-core/src/routes/index.ts b/packages/trilium-core/src/routes/index.ts index b87f85b982..59e463b7c3 100644 --- a/packages/trilium-core/src/routes/index.ts +++ b/packages/trilium-core/src/routes/index.ts @@ -7,6 +7,7 @@ import noteMapRoute from "./api/note_map"; import recentNotesRoute from "./api/recent_notes"; import otherRoute from "./api/others"; import branchesApiRoute from "./api/branches"; +import appInfoRoute from "./api/app_info"; import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity"; // TODO: Deduplicate with routes.ts @@ -70,6 +71,7 @@ export function buildSharedApiRoutes(apiRoute: any) { apiRoute(GET, "/api/keyboard-actions", keysApiRoute.getKeyboardActions); apiRoute(GET, "/api/keyboard-shortcuts-for-notes", keysApiRoute.getShortcutsForNotes); + apiRoute(GET, "/api/app-info", appInfoRoute.getAppInfo); apiRoute(GET, "/api/other/icon-usage", otherRoute.getIconUsage); } From 0d1c8ae01ec4b230f43ab7ae89a134f84ea7e342 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 12 Jan 2026 20:55:32 +0200 Subject: [PATCH 86/94] fix(server): login not working due to bad import to i18n --- apps/server/src/routes/login.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/server/src/routes/login.ts b/apps/server/src/routes/login.ts index bcb81ad85e..06bf37948c 100644 --- a/apps/server/src/routes/login.ts +++ b/apps/server/src/routes/login.ts @@ -1,4 +1,5 @@ import { ValidationError } from "@triliumnext/core"; +import { i18n } from "@triliumnext/core"; import crypto from "crypto"; import type { Request, Response } from 'express'; @@ -8,7 +9,6 @@ import myScryptService from "../services/encryption/my_scrypt.js"; import openIDEncryption from '../services/encryption/open_id_encryption.js'; import passwordService from "../services/encryption/password.js"; import recoveryCodeService from '../services/encryption/recovery_codes.js'; -import { getCurrentLocale } from "../services/i18n.js"; import log from "../services/log.js"; import openID from '../services/open_id.js'; import optionService from "../services/options.js"; @@ -27,7 +27,7 @@ function loginPage(req: Request, res: Response) { assetPath, assetPathFragment: assetUrlFragment, appPath, - currentLocale: getCurrentLocale() + currentLocale: i18n.getCurrentLocale() }); } @@ -36,7 +36,7 @@ function setPasswordPage(req: Request, res: Response) { error: false, assetPath, appPath, - currentLocale: getCurrentLocale() + currentLocale: i18n.getCurrentLocale() }); } @@ -62,7 +62,7 @@ function setPassword(req: Request, res: Response) { error, assetPath, appPath, - currentLocale: getCurrentLocale() + currentLocale: i18n.getCurrentLocale() }); return; } @@ -185,7 +185,7 @@ function sendLoginError(req: Request, res: Response, errorType: 'password' | 'to assetPath, assetPathFragment: assetUrlFragment, appPath, - currentLocale: getCurrentLocale() + currentLocale: i18n.getCurrentLocale() }); } From 83db37ed31acd44739d1def3c2e361728775f3f6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 12 Jan 2026 21:03:55 +0200 Subject: [PATCH 87/94] fix(server): app-info not showing data dir --- apps/client/src/widgets/dialogs/about.tsx | 27 ++++++++++--------- apps/server/src/main.ts | 10 +++++-- apps/server/src/services/app_info.ts | 12 ++------- packages/trilium-core/src/index.ts | 10 +++++-- .../trilium-core/src/services/app_info.ts | 6 +++-- 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/apps/client/src/widgets/dialogs/about.tsx b/apps/client/src/widgets/dialogs/about.tsx index f09cca3191..420fa16467 100644 --- a/apps/client/src/widgets/dialogs/about.tsx +++ b/apps/client/src/widgets/dialogs/about.tsx @@ -1,13 +1,14 @@ -import Modal from "../react/Modal.js"; +import type { AppInfo } from "@triliumnext/commons"; +import type { CSSProperties } from "preact/compat"; +import { useState } from "preact/hooks"; + import { t } from "../../services/i18n.js"; -import { formatDateTime } from "../../utils/formatters.js"; +import openService from "../../services/open.js"; import server from "../../services/server.js"; import utils from "../../services/utils.js"; -import openService from "../../services/open.js"; -import { useState } from "preact/hooks"; -import type { CSSProperties } from "preact/compat"; -import type { AppInfo } from "@triliumnext/commons"; +import { formatDateTime } from "../../utils/formatters.js"; import { useTriliumEvent } from "../react/hooks.jsx"; +import Modal from "../react/Modal.js"; export default function AboutDialog() { const [appInfo, setAppInfo] = useState(null); @@ -54,15 +55,15 @@ export default function AboutDialog() { {t("about.build_revision")} - {appInfo?.buildRevision && {appInfo.buildRevision}} + {appInfo?.buildRevision && {appInfo.buildRevision}} - + { appInfo?.dataDirectory && {t("about.data_directory")} {appInfo?.dataDirectory && ()} - + } @@ -76,8 +77,8 @@ function DirectoryLink({ directory, style }: { directory: string, style?: CSSPro openService.openDirectory(directory); }; - return {directory} - } else { - return {directory}; - } + return {directory}; + } + return {directory}; + } diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index ceb5fca437..4998f8c01b 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -4,9 +4,11 @@ */ import { initializeCore } from "@triliumnext/core"; +import path from "path"; import ClsHookedExecutionContext from "./cls_provider.js"; import NodejsCryptoProvider from "./crypto_provider.js"; +import dataDirs from "./services/data_dir.js"; import BetterSqlite3Provider from "./sql_provider.js"; async function startApplication() { @@ -40,11 +42,15 @@ async function startApplication() { // the maxEntityChangeId has been incremented during failed transaction, need to recalculate entity_changes.recalculateMaxEntityChangeId(); - } + }, }, crypto: new NodejsCryptoProvider(), executionContext: new ClsHookedExecutionContext(), - translations: (await import("./services/i18n.js")).initializeTranslations + translations: (await import("./services/i18n.js")).initializeTranslations, + extraAppInfo: { + nodeVersion: process.version, + dataDirectory: path.resolve(dataDirs.TRILIUM_DATA_DIR) + } }); const startTriliumServer = (await import("./www.js")).default; await startTriliumServer(); diff --git a/apps/server/src/services/app_info.ts b/apps/server/src/services/app_info.ts index 497bbb102c..d7402a847c 100644 --- a/apps/server/src/services/app_info.ts +++ b/apps/server/src/services/app_info.ts @@ -1,11 +1,3 @@ -import { AppInfo } from "@triliumnext/commons"; -import { app_info as coreAppInfo } from "@triliumnext/core"; -import path from "path"; +import { app_info } from "@triliumnext/core"; -import dataDir from "./data_dir.js"; - -export default { - ...coreAppInfo, - nodeVersion: process.version, - dataDirectory: path.resolve(dataDir.TRILIUM_DATA_DIR), -} satisfies AppInfo; +export default app_info; diff --git a/packages/trilium-core/src/index.ts b/packages/trilium-core/src/index.ts index de18e947c3..418d809a5e 100644 --- a/packages/trilium-core/src/index.ts +++ b/packages/trilium-core/src/index.ts @@ -5,6 +5,7 @@ import { initSql } from "./services/sql/index"; import { SqlService, SqlServiceParams } from "./services/sql/sql"; import { initMessaging, MessagingProvider } from "./services/messaging/index"; import { initTranslations, TranslationProvider } from "./services/i18n"; +import appInfo from "./services/app_info"; export type * from "./services/sql/types"; export * from "./services/sql/index"; @@ -67,18 +68,23 @@ export type { NoteParams } from "./services/notes"; export * as sanitize from "./services/sanitizer"; export * as routes from "./routes"; -export function initializeCore({ dbConfig, executionContext, crypto, translations, messaging }: { +export function initializeCore({ dbConfig, executionContext, crypto, translations, messaging, extraAppInfo }: { dbConfig: SqlServiceParams, executionContext: ExecutionContext, crypto: CryptoProvider, translations: TranslationProvider, - messaging?: MessagingProvider + messaging?: MessagingProvider, + extraAppInfo?: { + nodeVersion: string; + dataDirectory: string; + }; }) { initLog(); initCrypto(crypto); initSql(new SqlService(dbConfig, getLog())); initContext(executionContext); initTranslations(translations); + Object.assign(appInfo, extraAppInfo); if (messaging) { initMessaging(messaging); } diff --git a/packages/trilium-core/src/services/app_info.ts b/packages/trilium-core/src/services/app_info.ts index 269b15b35c..0aa1097c95 100644 --- a/packages/trilium-core/src/services/app_info.ts +++ b/packages/trilium-core/src/services/app_info.ts @@ -6,7 +6,7 @@ const APP_DB_VERSION = 233; const SYNC_VERSION = 36; const CLIPPER_PROTOCOL_VERSION = "1.0"; -export default { +const appInfo: AppInfo = { appVersion: packageJson.version, dbVersion: APP_DB_VERSION, syncVersion: SYNC_VERSION, @@ -14,4 +14,6 @@ export default { buildRevision: build.buildRevision, clipperProtocolVersion: CLIPPER_PROTOCOL_VERSION, utcDateTime: new Date().toISOString() -} satisfies AppInfo; +} + +export default appInfo; From c7edb71fed942be9e266d8c64b223c69d4f25ddb Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 12 Jan 2026 21:05:21 +0200 Subject: [PATCH 88/94] fix(client-standalone): missing favicon --- apps/client-standalone/public/favicon.ico | Bin 0 -> 114244 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/client-standalone/public/favicon.ico diff --git a/apps/client-standalone/public/favicon.ico b/apps/client-standalone/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..398e3854e623f28bb0387bb14bf555487ce96a0e GIT binary patch literal 114244 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 literal 0 HcmV?d00001 From 52610a7410eb88bc339ba1bcc41ae4dafc69b0ef Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 12 Jan 2026 21:06:00 +0200 Subject: [PATCH 89/94] fix(client-standalone): missing manifest --- .../public/manifest.webmanifest | 20 +++++++++++++++++++ apps/client-standalone/src/index.html | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 apps/client-standalone/public/manifest.webmanifest diff --git a/apps/client-standalone/public/manifest.webmanifest b/apps/client-standalone/public/manifest.webmanifest new file mode 100644 index 0000000000..1f8cb3b69a --- /dev/null +++ b/apps/client-standalone/public/manifest.webmanifest @@ -0,0 +1,20 @@ +{ + "name": "Trilium Notes", + "short_name": "Trilium", + "description": "Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases.", + "theme_color": "#333333", + "background_color": "#1F1F1F", + "display": "standalone", + "scope": "/", + "start_url": "/", + "display_override": [ + "window-controls-overlay" + ], + "icons": [ + { + "src": "icon.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/apps/client-standalone/src/index.html b/apps/client-standalone/src/index.html index 0a9a328b29..65d244ccb0 100644 --- a/apps/client-standalone/src/index.html +++ b/apps/client-standalone/src/index.html @@ -6,7 +6,7 @@ - + Trilium Notes From 5c5291745948d94055c8845c85aa70cf56d0b839 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 14 Jan 2026 17:31:06 +0200 Subject: [PATCH 90/94] fix(client-standalone): webmanifest icon path not correct --- apps/client-standalone/public/manifest.webmanifest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client-standalone/public/manifest.webmanifest b/apps/client-standalone/public/manifest.webmanifest index 1f8cb3b69a..d1ab2fc21a 100644 --- a/apps/client-standalone/public/manifest.webmanifest +++ b/apps/client-standalone/public/manifest.webmanifest @@ -12,7 +12,7 @@ ], "icons": [ { - "src": "icon.png", + "src": "assets/icon.png", "sizes": "512x512", "type": "image/png" } From 411fdf3114fd17e603a4e2ab6746450fb9e3feab Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 14 Jan 2026 17:33:57 +0200 Subject: [PATCH 91/94] chore(client-standalone): disable WS error notification --- apps/client/src/services/ws.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/services/ws.ts b/apps/client/src/services/ws.ts index 488000ba16..963e2e33be 100644 --- a/apps/client/src/services/ws.ts +++ b/apps/client/src/services/ws.ts @@ -304,7 +304,7 @@ async function sendPing() { } setTimeout(() => { - if (glob.device === "print") return; + if (glob.device === "print" || glob.isStandalone) return; ws = connectWebSocket(); From 7633e3d48e523d01759aafbd871411a19f4778df Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 14 Jan 2026 17:41:24 +0200 Subject: [PATCH 92/94] chore(client-standalone): address requested changes --- apps/server/src/services/export/zip.ts | 6 ++++-- apps/server/src/services/export/zip/share_theme.ts | 8 +++++--- packages/trilium-core/src/services/blob.ts | 3 +-- packages/trilium-core/src/services/events.ts | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/server/src/services/export/zip.ts b/apps/server/src/services/export/zip.ts index a0c32ef6bd..e9eec69bf5 100644 --- a/apps/server/src/services/export/zip.ts +++ b/apps/server/src/services/export/zip.ts @@ -343,7 +343,9 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch, content = prepareContent(noteMeta.title, content, noteMeta, undefined); - archive.append(content as Buffer, { name: filePathPrefix + noteMeta.dataFileName }); + archive.append(typeof content === "string" ? content : Buffer.from(content), { + name: filePathPrefix + noteMeta.dataFileName + }); return; } @@ -375,7 +377,7 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch, const attachment = note.getAttachmentById(attachmentMeta.attachmentId); const content = attachment.getContent(); - archive.append(content as Buffer, { + archive.append(typeof content === "string" ? content : Buffer.from(content), { name: filePathPrefix + attachmentMeta.dataFileName, date: dateUtils.parseDateTime(note.utcDateModified) }); diff --git a/apps/server/src/services/export/zip/share_theme.ts b/apps/server/src/services/export/zip/share_theme.ts index 0998a7244c..a8b3496c03 100644 --- a/apps/server/src/services/export/zip/share_theme.ts +++ b/apps/server/src/services/export/zip/share_theme.ts @@ -149,8 +149,8 @@ export default class ShareThemeExportProvider extends ZipExportProvider { } const note = this.branch.getNote(); - const fullHtml = this.prepareContent(rootMeta.title ?? "", note.getContent(), rootMeta, note, this.branch); - this.archive.append(fullHtml as Buffer, { name: this.indexMeta.dataFileName }); + const content = this.prepareContent(rootMeta.title ?? "", note.getContent(), rootMeta, note, this.branch); + this.archive.append(typeof content === "string" ? content : Buffer.from(content), { name: this.indexMeta.dataFileName }); } #saveAssets(rootMeta: NoteMeta, assetsMeta: NoteMeta[]) { @@ -178,7 +178,9 @@ export default class ShareThemeExportProvider extends ZipExportProvider { continue; }; const fontFileName = `assets/icon-pack-${iconPack.prefix.toLowerCase()}.${extension}`; - this.archive.append(fontData as Buffer, { name: fontFileName }); + this.archive.append(typeof fontData === "string" ? fontData : Buffer.from(fontData), { + name: fontFileName + }); } } diff --git a/packages/trilium-core/src/services/blob.ts b/packages/trilium-core/src/services/blob.ts index 6792c75270..09e72ee951 100644 --- a/packages/trilium-core/src/services/blob.ts +++ b/packages/trilium-core/src/services/blob.ts @@ -39,8 +39,7 @@ function processContent(content: Uint8Array | string | null, isProtected: boolea if (isStringContent) { if (content === null) return ""; - if (typeof content === "string") return content; - return decodeUtf8(content as Uint8Array); + return decodeUtf8(content); } // see https://github.com/zadam/trilium/issues/3523 // IIRC a zero-sized buffer can be returned as null from the database diff --git a/packages/trilium-core/src/services/events.ts b/packages/trilium-core/src/services/events.ts index dcdc5793c4..f37eef1d6e 100644 --- a/packages/trilium-core/src/services/events.ts +++ b/packages/trilium-core/src/services/events.ts @@ -1,4 +1,4 @@ -import log, { getLog } from "./log.js"; +import { getLog } from "./log.js"; const NOTE_TITLE_CHANGED = "NOTE_TITLE_CHANGED"; const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION"; From ab29caff7bb83a2d9f72d7925433f32a258e09a1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 14 Jan 2026 17:48:29 +0200 Subject: [PATCH 93/94] fix(client-standalone): CK premium features not working --- apps/client-standalone/.env | 4 ++++ apps/client-standalone/.env.production | 1 + apps/client-standalone/vite.config.mts | 1 + 3 files changed, 6 insertions(+) create mode 100644 apps/client-standalone/.env create mode 100644 apps/client-standalone/.env.production diff --git a/apps/client-standalone/.env b/apps/client-standalone/.env new file mode 100644 index 0000000000..18a7bcf954 --- /dev/null +++ b/apps/client-standalone/.env @@ -0,0 +1,4 @@ +# The development license key for premium CKEditor features. +# Note: This key must only be used for the Trilium Notes project. +VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3ODcyNzA0MDAsImp0aSI6IjkyMWE1MWNlLTliNDMtNGRlMC1iOTQwLTc5ZjM2MDBkYjg1NyIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOiJ0cmlsaXVtIiwiZmVhdHVyZXMiOlsiVFJJTElVTSJdLCJ2YyI6ImU4YzRhMjBkIn0.hny77p-U4-jTkoqbwPytrEar5ylGCWBN7Ez3SlB8i6_mJCBIeCSTOlVQk_JMiOEq3AGykUMHzWXzjdMFwgniOw +VITE_CKEDITOR_ENABLE_INSPECTOR=false \ No newline at end of file diff --git a/apps/client-standalone/.env.production b/apps/client-standalone/.env.production new file mode 100644 index 0000000000..efd1fd5179 --- /dev/null +++ b/apps/client-standalone/.env.production @@ -0,0 +1 @@ +VITE_CKEDITOR_ENABLE_INSPECTOR=false diff --git a/apps/client-standalone/vite.config.mts b/apps/client-standalone/vite.config.mts index e936e0b7b0..f0f48d1086 100644 --- a/apps/client-standalone/vite.config.mts +++ b/apps/client-standalone/vite.config.mts @@ -85,6 +85,7 @@ if (!isDev) { export default defineConfig(() => ({ root: join(__dirname, 'src'), // Set src as root so index.html is served from / + envDir: __dirname, // Load .env files from client-standalone directory, not src/ cacheDir: '../../../node_modules/.vite/apps/client-standalone', base: "", plugins, From 64a8c3b00552ac96350e5baadf506c27b3121bc9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 14 Jan 2026 18:27:53 +0200 Subject: [PATCH 94/94] chore(client-standalone): address requested changes --- .github/workflows/deploy-app.yml | 2 +- apps/client-standalone/package.json | 2 +- .../src/lightweight/messaging_provider.ts | 2 - .../src/lightweight/sql_provider.ts | 9 +++-- apps/client-standalone/src/local-bridge.ts | 9 ++--- .../src/local-server-worker.ts | 15 -------- apps/client-standalone/src/main.ts | 37 +------------------ apps/client/src/services/doc_renderer.ts | 1 + 8 files changed, 14 insertions(+), 63 deletions(-) diff --git a/.github/workflows/deploy-app.yml b/.github/workflows/deploy-app.yml index cb9408f652..fda15a5453 100644 --- a/.github/workflows/deploy-app.yml +++ b/.github/workflows/deploy-app.yml @@ -54,7 +54,7 @@ jobs: - name: Deploy uses: ./.github/actions/deploy-to-cloudflare-pages - if: github.repository == ${{ vars.REPO_MAIN }} + if: github.repository == vars.REPO_MAIN with: project_name: "trilium-app" comment_body: "🖥️ App preview is ready" diff --git a/apps/client-standalone/package.json b/apps/client-standalone/package.json index 7a59bc3d96..bee9bcd22c 100644 --- a/apps/client-standalone/package.json +++ b/apps/client-standalone/package.json @@ -59,7 +59,7 @@ "mind-elixir": "5.4.0", "normalize.css": "8.0.1", "panzoom": "9.4.3", - "preact": "10.28.1", + "preact": "10.28.2", "react-i18next": "16.5.1", "react-window": "2.2.3", "reveal.js": "5.2.1", diff --git a/apps/client-standalone/src/lightweight/messaging_provider.ts b/apps/client-standalone/src/lightweight/messaging_provider.ts index 4a178fbb0e..07f233c1d0 100644 --- a/apps/client-standalone/src/lightweight/messaging_provider.ts +++ b/apps/client-standalone/src/lightweight/messaging_provider.ts @@ -19,7 +19,6 @@ export default class WorkerMessagingProvider implements MessagingProvider { constructor() { // Listen for incoming messages from the main thread self.addEventListener("message", this.handleIncomingMessage); - console.log("[WorkerMessagingProvider] Initialized"); } private handleIncomingMessage = (event: MessageEvent) => { @@ -87,6 +86,5 @@ export default class WorkerMessagingProvider implements MessagingProvider { this.isDisposed = true; self.removeEventListener("message", this.handleIncomingMessage); this.messageHandlers = []; - console.log("[WorkerMessagingProvider] Disposed"); } } diff --git a/apps/client-standalone/src/lightweight/sql_provider.ts b/apps/client-standalone/src/lightweight/sql_provider.ts index f9edf43d52..0890f454b5 100644 --- a/apps/client-standalone/src/lightweight/sql_provider.ts +++ b/apps/client-standalone/src/lightweight/sql_provider.ts @@ -19,7 +19,8 @@ class WasmStatement implements Statement { constructor( private stmt: Sqlite3PreparedStatement, - private db: Sqlite3Database + private db: Sqlite3Database, + private sqlite3: Sqlite3Module ) {} run(...params: unknown[]): RunResult { @@ -33,10 +34,12 @@ class WasmStatement implements Statement { // This allows the statement to be reused this.stmt.step(); const changes = this.db.changes(); + // Get the last insert row ID using the C API + const lastInsertRowid = this.db.pointer ? this.sqlite3.capi.sqlite3_last_insert_rowid(this.db.pointer) : 0; this.stmt.reset(); return { changes, - lastInsertRowid: 0 // Would need sqlite3_last_insert_rowid for this + lastInsertRowid }; } catch (e) { // Reset on error to allow reuse @@ -490,7 +493,7 @@ export default class BrowserSqlProvider implements DatabaseProvider { // Create new statement and cache it const stmt = this.db!.prepare(query); - const wasmStatement = new WasmStatement(stmt, this.db!); + const wasmStatement = new WasmStatement(stmt, this.db!, this.sqlite3!); this.statementCache.set(query, wasmStatement); return wasmStatement; } diff --git a/apps/client-standalone/src/local-bridge.ts b/apps/client-standalone/src/local-bridge.ts index f96e39a04b..79abba934d 100644 --- a/apps/client-standalone/src/local-bridge.ts +++ b/apps/client-standalone/src/local-bridge.ts @@ -1,4 +1,3 @@ -// public/local-bridge.js let localWorker: Worker | null = null; const pending = new Map(); @@ -11,7 +10,7 @@ export function startLocalServerWorker() { localWorker.onerror = (event) => { console.error("[LocalBridge] Worker error:", event); // Reject all pending requests - for (const [id, resolver] of pending) { + for (const [, resolver] of pending) { resolver.reject(new Error(`Worker error: ${event.message}`)); } pending.clear(); @@ -19,18 +18,18 @@ export function startLocalServerWorker() { localWorker.onmessage = (event) => { const msg = event.data; - + // Handle worker error reports if (msg?.type === "WORKER_ERROR") { console.error("[LocalBridge] Worker reported error:", msg.error); // Reject all pending requests with the error - for (const [id, resolver] of pending) { + for (const [, resolver] of pending) { resolver.reject(new Error(msg.error?.message || "Unknown worker error")); } pending.clear(); return; } - + if (!msg || msg.type !== "LOCAL_RESPONSE") return; const { id, response, error } = msg; diff --git a/apps/client-standalone/src/local-server-worker.ts b/apps/client-standalone/src/local-server-worker.ts index 7f77525e28..fff7e48f4f 100644 --- a/apps/client-standalone/src/local-server-worker.ts +++ b/apps/client-standalone/src/local-server-worker.ts @@ -1,7 +1,3 @@ -// public/local-server-worker.js -// This will eventually import your core server and DB provider. -// import { createCoreServer } from "@trilium/core"; (bundled) - import { BrowserRouter } from './lightweight/browser_router'; import { createConfiguredRouter } from './lightweight/browser_routes'; import BrowserExecutionContext from './lightweight/cls_provider'; @@ -151,17 +147,6 @@ async function ensureInitialized() { return router; } -const encoder = new TextEncoder(); - -function jsonResponse(obj: unknown, status = 200, extraHeaders = {}) { - const body = encoder.encode(JSON.stringify(obj)).buffer; - return { - status, - headers: { "content-type": "application/json; charset=utf-8", ...extraHeaders }, - body - }; -} - interface LocalRequest { method: string; url: string; diff --git a/apps/client-standalone/src/main.ts b/apps/client-standalone/src/main.ts index 9be9186d8f..97e6f65547 100644 --- a/apps/client-standalone/src/main.ts +++ b/apps/client-standalone/src/main.ts @@ -14,7 +14,7 @@ async function waitForServiceWorkerControl(): Promise { console.log("[Bootstrap] Waiting for service worker to take control..."); // Register service worker - const registration = await navigator.serviceWorker.register("./sw.js", { scope: "/" }); + await navigator.serviceWorker.register("./sw.js", { scope: "/" }); // Wait for it to be ready (installed + activated) await navigator.serviceWorker.ready; @@ -39,41 +39,6 @@ async function waitForServiceWorkerControl(): Promise { throw new Error("Reloading for service worker activation"); } -async function fetchWithRetry(url: string, maxRetries = 3, delayMs = 500): Promise { - let lastError: Error | null = null; - - for (let attempt = 0; attempt < maxRetries; attempt++) { - try { - console.log(`[Bootstrap] Fetching ${url} (attempt ${attempt + 1}/${maxRetries})`); - const response = await fetch(url); - - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - - // Check if response has content - const contentType = response.headers.get("content-type"); - if (!contentType || !contentType.includes("application/json")) { - throw new Error(`Invalid content-type: ${contentType || "none"}`); - } - - return response; - } catch (err) { - lastError = err as Error; - console.warn(`[Bootstrap] Fetch attempt ${attempt + 1} failed:`, err); - - if (attempt < maxRetries - 1) { - // Exponential backoff - const delay = delayMs * Math.pow(2, attempt); - console.log(`[Bootstrap] Retrying in ${delay}ms...`); - await new Promise(resolve => setTimeout(resolve, delay)); - } - } - } - - throw new Error(`Failed to fetch ${url} after ${maxRetries} attempts: ${lastError?.message}`); -} - async function bootstrap() { /* fixes https://github.com/webpack/webpack/issues/10035 */ window.global = globalThis; diff --git a/apps/client/src/services/doc_renderer.ts b/apps/client/src/services/doc_renderer.ts index 63879d3ca7..279b554c1e 100644 --- a/apps/client/src/services/doc_renderer.ts +++ b/apps/client/src/services/doc_renderer.ts @@ -51,6 +51,7 @@ async function processContent(url: string, $content: JQuery) { function getUrl(docNameValue: string, language: string) { // Cannot have spaces in the URL due to how JQuery.load works. docNameValue = docNameValue.replaceAll(" ", "%20"); + // The user guide is available only in English, so make sure we are requesting correctly since 404s in standalone client are treated differently. if (docNameValue.includes("User%20Guide")) language = "en"; return `${getBasePath()}/doc_notes/${language}/${docNameValue}.html`; }