Compare commits

...

1707 Commits

Author SHA1 Message Date
Elian Doran
8c2e2cc9ba chore(standalone): address requested changes 2026-04-12 23:35:50 +03:00
Elian Doran
1fdc623ebc fix(core): corruption caused by encryption 2026-04-12 20:03:41 +03:00
Elian Doran
91d4e77a48 chore(standalone): remove async encryption 2026-04-12 19:57:30 +03:00
Elian Doran
395c71fa0d fix(standalone): encrypt subtree not working 2026-04-12 19:52:34 +03:00
Elian Doran
de037b3ced feat(standalone): disable LLM features 2026-04-12 19:50:18 +03:00
Elian Doran
8f3f2cc8c1 chore(core): reintroduce encryption and password reset 2026-04-12 19:34:19 +03:00
Elian Doran
4b4ef35272 feat(standalone): start introducing crypto 2026-04-12 19:24:16 +03:00
Elian Doran
3b437d85c8 chore(core): fix type issue 2026-04-12 19:20:15 +03:00
Elian Doran
262ac05483 fix(standalone): PDFjs not working in prod mode 2026-04-12 19:10:03 +03:00
Elian Doran
f75adfe6a3 fix(standalone): PDFjs not working in dev mode 2026-04-12 19:03:07 +03:00
Elian Doran
b46c1e6d57 feat(standalone): allow downloading backups 2026-04-12 18:47:43 +03:00
Elian Doran
9f24a44e15 fix(standalone): crash in logs due to refresh 2026-04-12 18:43:03 +03:00
Elian Doran
89b3dec84a refactor(backup): pass in options service directly 2026-04-12 18:39:38 +03:00
Elian Doran
3ad20e43f1 refactor(core): get rid of as any for image routes 2026-04-12 18:37:21 +03:00
Elian Doran
f034454ec9 refactor(backup): constructor-based dependency injection for options 2026-04-12 18:36:21 +03:00
Elian Doran
35317b3dab fix(core): error due to CLS on standalone 2026-04-12 18:34:19 +03:00
Elian Doran
0d5c9986b6 chore(backup): implement standalone regular backup 2026-04-12 18:30:57 +03:00
Elian Doran
745374050e chore(core): reintroduce image routes 2026-04-12 18:23:17 +03:00
Elian Doran
b921c3c587 chore(core): integrate backup routes 2026-04-12 18:20:58 +03:00
Elian Doran
9d4ff506dc feat(standalone): start working on an image service 2026-04-12 18:17:40 +03:00
Elian Doran
065afd0214 chore(standalone): start implementing backup service 2026-04-12 18:17:02 +03:00
Elian Doran
876008ef01 refactor(backup): keep only one implementation in core with abstract methods 2026-04-12 18:01:31 +03:00
Elian Doran
8c61cc88e9 core(standalone): integrate backup management using provider 2026-04-12 17:52:43 +03:00
Elian Doran
24112a9b6f feat(standalone): introduce backend log handling 2026-04-12 17:39:13 +03:00
Elian Doran
e7c931d997 chore(core): integrate backend log route 2026-04-12 17:27:07 +03:00
Elian Doran
814a961608 chore(client): dev server not working due to prefresh bug
See https://github.com/preactjs/prefresh/issues/610
2026-04-12 17:23:08 +03:00
Elian Doran
73743b6236 fix(server): reintegrate backend log mechanism 2026-04-12 17:22:27 +03:00
Elian Doran
102cf4c4ad chore(standalone): reintegrate changes from main lost in the merge 2026-04-12 11:43:31 +03:00
Elian Doran
8494e0c08a Merge remote-tracking branch 'origin/main' into standalone 2026-04-12 11:34:56 +03:00
Elian Doran
2dd1dd1fd0 fix(standalone): cyclic dependency breaking prod 2026-04-12 11:16:55 +03:00
Elian Doran
fadbc906e2 chore(deps): update dependency @prefresh/vite to v3 (#9392) 2026-04-12 09:39:14 +03:00
renovate[bot]
5ea615da1e chore(deps): update dependency @prefresh/vite to v3 2026-04-12 00:47:10 +00:00
Elian Doran
a7f9032347 feat(llm): add note mutation tools (rename, delete, move, clone) (#9339) 2026-04-12 01:32:14 +03:00
Elian Doran
f137868f92 feat(llm): add stop generation button (#9341) 2026-04-12 00:48:55 +03:00
Elian Doran
175e200d88 fix(llm): stopping a tool call leaves an infinite spinner 2026-04-12 00:48:01 +03:00
Elian Doran
74f951023b Merge remote-tracking branch 'origin/main' into feat/llm-stop-generation 2026-04-12 00:37:27 +03:00
Elian Doran
3e697338e1 Translations update from Hosted Weblate (#9381) 2026-04-11 22:18:18 +03:00
Hosted Weblate
4bffc1c156 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/
2026-04-11 16:38:08 +00:00
green
ac4c5f7d8c Translated using Weblate (Japanese)
Currently translated at 99.9% (1851 of 1852 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-04-11 16:38:04 +00:00
AggelosPnS
8f41e55b3c Translated using Weblate (Greek)
Currently translated at 100.0% (116 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/el/
2026-04-11 16:38:02 +00:00
AggelosPnS
ad8aab7b15 Translated using Weblate (Greek)
Currently translated at 96.2% (152 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/el/
2026-04-11 16:38:00 +00:00
AggelosPnS
7e779669ea Translated using Weblate (Greek)
Currently translated at 2.6% (49 of 1852 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/el/
2026-04-11 16:37:58 +00:00
green
5b01791021 Translated using Weblate (Japanese)
Currently translated at 100.0% (401 of 401 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ja/
2026-04-11 16:37:55 +00:00
Elian Doran
14bb068626 fix(tree): deleting ancestor of the current note doesn't correctly navigate 2026-04-11 19:36:50 +03:00
Elian Doran
1c93636538 fix(tree): navigating to parent note when deleting a non-active note (closes #9380) 2026-04-11 19:31:44 +03:00
Elian Doran
b402a7a32b Easy fixes v2 (#9377) 2026-04-11 19:19:57 +03:00
Elian Doran
3a7167a65d chore: address requested changes 2026-04-11 19:19:32 +03:00
Elian Doran
6dd7e9cb38 chore(delete): address requested changes 2026-04-11 14:30:44 +03:00
Elian Doran
4ffa016045 fix(sidebar): highlights with math split in read-only text 2026-04-11 14:28:58 +03:00
Elian Doran
2d6f1ee9b7 fix(sidebar): editable mode equations sometimes not rendering 2026-04-11 14:26:22 +03:00
Elian Doran
a1f0615afe chore: fix typecheck 2026-04-11 14:25:01 +03:00
Elian Doran
03ff9c4b27 fix(sidebar): highlights not rendering math in read-only text 2026-04-11 14:23:07 +03:00
Elian Doran
67a48bbec7 fix(sidebar): duplicate equations rendering 2026-04-11 14:14:18 +03:00
Elian Doran
2b63af82ec fix(sidebar): equations not rendered for read-only text 2026-04-11 13:53:10 +03:00
Elian Doran
c5ee7083d8 chore(sidebar): deduplicate math rendering 2026-04-11 13:49:13 +03:00
Elian Doran
0696f7724d chore(react): add an option to make options row stacked 2026-04-11 13:43:10 +03:00
Elian Doran
b7231e3464 feat(delete): improve translations 2026-04-11 13:35:27 +03:00
Elian Doran
214c6c93fd feat(similarity): filter out hidden notes (closes #4584) 2026-04-11 13:34:04 +03:00
Elian Doran
7037ae4ba8 feat(delete): hide removal of clones completely if no clones are affected 2026-04-11 13:24:09 +03:00
Elian Doran
46d6d6fdee feat(include_note): remember value of box size (closes #1623) 2026-04-11 13:23:13 +03:00
Elian Doran
ae751bfb91 feat(delete): improve layout of the note path 2026-04-11 13:20:28 +03:00
Elian Doran
bd0117c52f feat(delete): borderless table 2026-04-11 13:10:38 +03:00
Elian Doran
1402695dbe feat(delete): improve table for broken relations 2026-04-11 13:01:18 +03:00
Elian Doran
72c42afb50 feat(delete): use proper note links and show icons 2026-04-11 12:53:36 +03:00
Elian Doran
2752e0998e feat(delete): render broken relations as a table 2026-04-11 12:50:34 +03:00
Elian Doran
52114e08ba chore(delete): remove redundant list of clones 2026-04-11 12:44:28 +03:00
Elian Doran
a98721c016 fix(ckeditor/include_note): changing expandability doesn't refresh 2026-04-11 12:43:34 +03:00
Elian Doran
c3ab2d09d5 feat(ckeditor/include_note): add a new size for expandable items (closes #4134) 2026-04-11 12:43:17 +03:00
Elian Doran
9ef7802651 chore(delete): remove self-descriptive title 2026-04-11 12:35:27 +03:00
Elian Doran
a913d33a9e chore(ckeditor/include_note): remove debug logs 2026-04-11 12:35:01 +03:00
Elian Doran
49dc7135a7 feat(delete): different behavior when only deleted clones 2026-04-11 12:34:34 +03:00
Elian Doran
7e77560d70 fix(ckeditor/include_note): undo not working after select mechanism 2026-04-11 12:23:22 +03:00
Elian Doran
35cb110151 chore(delete): add missing translations 2026-04-11 12:20:06 +03:00
Elian Doran
4e49c2458d refactor(delete): deduplicate form toggle 2026-04-11 12:19:47 +03:00
Elian Doran
755e5fc416 feat(delete): improve dialog slightly by using cards and options rows 2026-04-11 12:17:25 +03:00
Elian Doran
5d4fd0269f refactor(ckeditor/include_note): use different method for intercepting selection 2026-04-11 12:13:24 +03:00
Elian Doran
461abf768c feat(ckeditor/include_note): add a way to change size after creation (closes #3705) 2026-04-11 12:07:16 +03:00
Elian Doran
602bebe498 feat(server): improve note path display to use chevrons instead of slashes to separate notes (closes #762) 2026-04-11 11:43:57 +03:00
Elian Doran
6c31b35f08 refactor(delete): reuse components for delete note list 2026-04-11 11:42:14 +03:00
Elian Doran
ccf95ad885 feat(delete): clarify "Delete also all clones" based on actual number of clones (closes #2362) 2026-04-11 11:39:05 +03:00
Elian Doran
fb33921308 feat(script): add warning if trying to render an unavailable protected server-side note (closes #21) 2026-04-11 11:17:58 +03:00
Elian Doran
1121ee0133 feat(script): add warning if trying to render a protected note without the session active 2026-04-11 11:15:31 +03:00
Elian Doran
77af4bd288 feat(link): allow bookends: and highlights: protocols (closes #2817) 2026-04-11 11:11:43 +03:00
Elian Doran
a1a2119e37 fix(server): indentation in HTML not preserved (closes #3151) 2026-04-11 11:07:59 +03:00
Elian Doran
afd2806a67 feat(script): increase warning toast time 2026-04-11 11:02:56 +03:00
Elian Doran
3410f0f5bc feat(script): warn if user is trying to run the script in a wrong environment (closes #342) 2026-04-11 11:01:04 +03:00
Elian Doran
4ed2226206 fix(script): logging api.startNote not working (closes #3751) 2026-04-11 10:57:05 +03:00
Elian Doran
b8d7277d88 feat(server): remove old keyboard shortcuts from options (closes #4543) 2026-04-11 10:48:09 +03:00
Elian Doran
1becc18354 fix(ckeditor5): internal link enabled in code block (closes #1712) 2026-04-11 10:41:24 +03:00
Elian Doran
9366d351e0 chore(edit-demo): ensure proper tree expansion state 2026-04-11 10:32:27 +03:00
Elian Doran
e27f5cd419 docs(demo): statistics not rendering (closes #4178) 2026-04-11 10:27:49 +03:00
Elian Doran
b7c1116738 chore(deps): update vitest monorepo to v4.1.3 (#9374) 2026-04-11 10:03:26 +03:00
renovate[bot]
a6a3d743f7 chore(deps): update vitest monorepo to v4.1.3 2026-04-11 06:39:38 +00:00
Elian Doran
dd3f3e9e5c fix(deps): update ai sdk (#9375) 2026-04-11 09:34:36 +03:00
Elian Doran
ad2732b249 chore(deps): update typescript-eslint monorepo to v8.58.1 (#9373) 2026-04-11 09:33:57 +03:00
Elian Doran
10c04bdda0 Translations update from Hosted Weblate (#9371) 2026-04-11 09:00:34 +03:00
noobhjy
26d88afeb7 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (401 of 401 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/zh_Hans/
2026-04-11 07:59:57 +02:00
noobhjy
376d19563d Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 98.4% (1823 of 1852 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-04-11 07:59:57 +02:00
Francis C.
d2895f0f42 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 99.9% (1851 of 1852 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-04-11 07:59:56 +02:00
Francis C.
30310ef2ba Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (401 of 401 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/zh_Hant/
2026-04-11 07:59:56 +02:00
Hosted Weblate
924a9747f1 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/
2026-04-11 07:59:55 +02:00
Elian Doran
f8ed48d2d2 fix(deps): update dependency marked to v18 (#9376) 2026-04-11 08:59:47 +03:00
renovate[bot]
9cdb2a73e3 chore(deps): update typescript-eslint monorepo to v8.58.1 2026-04-11 05:58:20 +00:00
Elian Doran
ad3258b88e chore(deps): update dependency vite to v8.0.7 (#9372) 2026-04-11 08:52:36 +03:00
renovate[bot]
4461ab080a fix(deps): update ai sdk 2026-04-11 05:44:10 +00:00
renovate[bot]
9b07f156b2 fix(deps): update dependency marked to v18 2026-04-11 01:54:33 +00:00
renovate[bot]
94c7967800 chore(deps): update dependency vite to v8.0.7 2026-04-11 01:50:19 +00:00
Elian Doran
a5b248e663 feat(ckeditor): match style for admonitions in floating toolbar 2026-04-11 00:50:20 +03:00
Elian Doran
1ec43722e8 fix(ckeditor): admonitions overshadowing floating toolbar 2026-04-11 00:47:51 +03:00
Elian Doran
88c548cc70 feat(ckeditor): add a toolbar to switch admonition types 2026-04-11 00:47:36 +03:00
Elian Doran
daafe251da feat(text): click to copy inline code in read-only text 2026-04-11 00:40:41 +03:00
Elian Doran
147ecbccda feat(ckeditor): add copy button for inline code 2026-04-11 00:36:43 +03:00
Elian Doran
be5d2d07bc Easy fixes v1 (#9370) 2026-04-11 00:29:15 +03:00
Elian Doran
adbe8f6c42 feat(options/sync): improve timeout layout 2026-04-11 00:16:41 +03:00
Elian Doran
18aec84be5 chore(client): address requested changes 2026-04-11 00:16:20 +03:00
Elian Doran
5f68958aa7 chore(client): address requested changes 2026-04-10 23:54:27 +03:00
Elian Doran
4787f644a6 feat(options): friendlier zoom factor selection (closes #5444) 2026-04-10 23:38:29 +03:00
Elian Doran
524f8df866 feat(search): add an option to open all results (closes #5376) 2026-04-10 23:36:29 +03:00
Elian Doran
bb381c1349 refactor(highlights): remove unnecessary logic in old layout (closes #5375) 2026-04-10 23:21:00 +03:00
Elian Doran
36c31dac14 refactor(client): remove unused translation 2026-04-10 23:20:35 +03:00
Elian Doran
01b6926054 test(server): sync options with various scenarios 2026-04-10 23:20:24 +03:00
Elian Doran
84cfa0a9f7 fix(server): overriding sync_options affected by the timeScale 2026-04-10 23:17:47 +03:00
Elian Doran
cb83c51632 chore(ai): update system prompt regarding tests 2026-04-10 23:17:09 +03:00
Elian Doran
97256ba291 feat(options): add nicer sync timeout selector (closes #5513) 2026-04-10 23:12:07 +03:00
Elian Doran
d3c596aaa0 feat(highlights): render highlighted equations in new layout 2026-04-10 23:03:30 +03:00
Elian Doran
3d2fa57873 fix(toc): equations sometimes duplicated 2026-04-10 23:01:07 +03:00
Elian Doran
c435050018 refactor(client): deduplicate checks for title/icon editability 2026-04-10 22:36:13 +03:00
Elian Doran
14f761de36 fix(options): icons can be modified 2026-04-10 22:35:06 +03:00
Elian Doran
626438d8f5 fix(options): titles can be modified (closes #5371) 2026-04-10 22:33:39 +03:00
Elian Doran
e29555a89b fix(collections/calendar): displaying deep children (closes #7944) 2026-04-10 22:17:55 +03:00
Elian Doran
05da2d7a50 fix(collections/table): unable to set number cell to zero (closes #6555) 2026-04-10 22:11:10 +03:00
Elian Doran
1124533557 fix(edit-docs): wrong starting note 2026-04-10 22:01:41 +03:00
Elian Doran
878603c7b0 fix(jump_to_note): caret at the end when entering command mode (closes #7942) 2026-04-10 21:17:38 +03:00
Elian Doran
19583cd84a fix(edit-demo): cloned notes lost due to async issue 2026-04-10 21:14:39 +03:00
Elian Doran
9f26d6efdc feat(text): render note icons in autocompletion (closes #8188) 2026-04-10 21:11:49 +03:00
Elian Doran
043e620231 fix(setup): trailing slash affects sync (closes #8045) 2026-04-10 21:09:29 +03:00
Elian Doran
d3dbdd4ceb docs(scripting): typos in "Trilium Demo" note (closes #8230) 2026-04-10 21:02:05 +03:00
Elian Doran
0859165072 docs(scripting): missing step in word count widget (closes #8561) 2026-04-10 20:54:13 +03:00
Elian Doran
ca7ab6105d chore(ai): keep system prompts in sync 2026-04-10 20:48:15 +03:00
Elian Doran
3af2b32783 fix(react): workaround for bootstrap tooltip error (closes #8900) 2026-04-10 20:43:41 +03:00
Elian Doran
8d5df7e888 chore(ai): update system prompt for reusing components and using translations 2026-04-10 20:42:33 +03:00
Elian Doran
126ee27505 feat(search): some error messages were not translated (closes #8850) 2026-04-10 20:38:13 +03:00
Elian Doran
fc2d8452b5 feat(search): clarify error message for full-text search after expressions 2026-04-10 20:31:23 +03:00
Elian Doran
1b8c234f30 feat(search): clarify error message for use of unquoted note 2026-04-10 20:28:37 +03:00
Elian Doran
540b607459 fix(note_map): freezing the app if there are too many notes (closes #8916) 2026-04-10 20:13:00 +03:00
Elian Doran
ee229bd0d7 fix(client): note title doesn't get selected anymore when creating new note (closes #8407) 2026-04-10 20:04:23 +03:00
Elian Doran
439d39d8fa Editing quirks (#9362) 2026-04-10 13:42:03 +03:00
Elian Doran
8c379d03a9 Update apps/client/src/widgets/collections/calendar/index.tsx
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-10 13:41:25 +03:00
Elian Doran
ff31104b99 fix(collections/calendar): unnecessary start date set when editing a note in quick edit 2026-04-10 13:31:44 +03:00
Elian Doran
dfe6063929 fix(client): spaced update saving more times than necesssary and causing performance issues 2026-04-10 12:00:08 +03:00
Elian Doran
a4b716f8c7 fix(board): clicking on a URL would open th quick edit panel 2026-04-10 11:38:42 +03:00
Elian Doran
7efc36efef fix(collections): not reacting to changes in reordering 2026-04-10 11:35:32 +03:00
Elian Doran
1554c9907e fix(server): not starting due to dependency update 2026-04-10 11:34:46 +03:00
Elian Doran
df46ddcf60 chore(deps): update pnpm lock 2026-04-10 11:28:18 +03:00
Elian Doran
6fb19d0287 feat: add download button for backups (#9190) 2026-04-10 11:00:04 +03:00
Elian Doran
d702f69415 Update dependency minimatch@3>brace-expansion to v5 (#9307) 2026-04-10 10:41:05 +03:00
Elian Doran
eb81e830a1 Update dependency eslint-linter-browserify to v10.2.0 (#9334) 2026-04-10 10:38:45 +03:00
Elian Doran
a24b9d7a38 fix(web-clipper): Remove trailing / from triliumServerUrl (#9344) 2026-04-10 10:37:14 +03:00
Elian Doran
efeaa1e895 chore(deps): audit fix 2026-04-10 10:29:50 +03:00
Elian Doran
a239eba6ce chore(llm): update backend script to be aware of the changes 2026-04-10 10:24:44 +03:00
Elian Doran
d009582252 feat(script): mark cheerio as deprecated and provide alternative 2026-04-10 10:22:15 +03:00
Elian Doran
fe710823c1 docs(user): add breaking change documentation for axios 2026-04-10 10:15:24 +03:00
Elian Doran
bfe593ae52 feat(server): remove axios 2026-04-10 09:59:51 +03:00
Elian Doran
f653a22557 chore(deps): remove upath 2026-04-10 09:51:49 +03:00
Elian Doran
96e7f22520 Update ai sdk (#9357) 2026-04-10 09:49:30 +03:00
Elian Doran
e6d3d22db7 Update dependency fuse.js to v7.3.0 (#9335) 2026-04-10 09:46:01 +03:00
Elian Doran
1258dedab3 Update dependency marked to v17.0.6 (#9348) 2026-04-10 08:18:26 +03:00
Elian Doran
ec15c7e63e Update dependency eslint-plugin-simple-import-sort to v13 (#9359) 2026-04-10 08:17:44 +03:00
Elian Doran
5037eaf205 Update codemirror themes (#9358) 2026-04-10 08:16:43 +03:00
renovate[bot]
cb706453aa Update dependency eslint-plugin-simple-import-sort to v13 2026-04-10 02:14:33 +00:00
renovate[bot]
772ebbf929 Update codemirror themes 2026-04-10 02:13:55 +00:00
renovate[bot]
60e1aca3b1 Update ai sdk 2026-04-10 02:13:17 +00:00
Elian Doran
741ae4b070 chore(server): fix dist creation 2026-04-09 22:31:50 +03:00
Elian Doran
64764a78ab fix(build-docs): process hanging 2026-04-09 22:16:42 +03:00
Tomas Adamek
49476d72fc Update apps/server/src/services/llm/tools/hierarchy_tools.ts
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-09 20:50:59 +02:00
Elian Doran
231d099004 chore(deps): align with main 2026-04-09 21:20:26 +03:00
Elian Doran
047b6ff3fe Merge remote-tracking branch 'origin/main' into standalone 2026-04-09 21:19:01 +03:00
Elian Doran
10dd50669c Reintegrate tests (#9352) 2026-04-09 21:14:11 +03:00
Elian Doran
9f32717d25 Update apps/server/src/in_app_help_provider.ts
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-09 20:44:15 +03:00
Elian Doran
7e02e6ae96 ci(dev): run standalone tests as well 2026-04-09 20:31:19 +03:00
Elian Doran
c041c25e0f fix(server): cannot run server in e2e 2026-04-09 19:34:20 +03:00
Elian Doran
8e7bd16a98 fix(docker): cannot access schema and DB 2026-04-09 19:16:26 +03:00
Elian Doran
f3f1ce5052 test(standalone): happy-dom issue with Markdown import 2026-04-09 18:48:10 +03:00
Elian Doran
c83531a3f1 e2e(server): bring back loading of the integration test database 2026-04-09 18:46:09 +03:00
Elian Doran
746367411c fix(standalone): zip fix wasn't integrated 2026-04-09 18:36:59 +03:00
Elian Doran
21302e4142 test(standalone): get most tests to pass 2026-04-09 18:34:39 +03:00
Elian Doran
2c2a20b80d chore(server): bypass build warning 2026-04-09 18:29:00 +03:00
Elian Doran
aac8c8053d test(standalone): use real platform provider 2026-04-09 18:13:49 +03:00
Elian Doran
de050b3adc test(core): crash due to default test script 2026-04-09 18:12:47 +03:00
Elian Doran
2f7c054d64 test(standalone): start running tests 2026-04-09 18:11:21 +03:00
Elian Doran
515ea96616 refactor(core): cleanup expected fails 2026-04-09 18:08:19 +03:00
Elian Doran
86da56d35b test(e2e): broken due to missing rebuild mechanism 2026-04-09 18:05:41 +03:00
renovate[bot]
31eaa4181d Update dependency fuse.js to v7.3.0 2026-04-09 15:03:57 +00:00
Elian Doran
ca13a8accd Update dependency katex to v0.16.45 (#9347) 2026-04-09 18:01:24 +03:00
Elian Doran
78b1f119dc Update dependency dotenv to v17.4.1 (#9346) 2026-04-09 18:00:51 +03:00
Elian Doran
bfb9df48b1 test(ocr): broken due to change in architecture 2026-04-09 17:53:06 +03:00
Elian Doran
acf9aa8b41 fix(core): issues with some utils 2026-04-09 17:52:48 +03:00
Elian Doran
6e0e7847e4 fix(server): share tests no longer working 2026-04-09 17:46:09 +03:00
Elian Doran
f40de0a017 test(core): fix more tests 2026-04-09 17:41:47 +03:00
Elian Doran
3a7ce0c284 test(core): fix initialization issues due to SQL 2026-04-09 16:55:36 +03:00
Elian Doran
dc0fcad843 test(server): run core tests 2026-04-09 16:45:24 +03:00
Elian Doran
66a18d12dc fix(server): in-app help not integrated 2026-04-09 16:37:13 +03:00
Elian Doran
2908b29c0d Translations update from Hosted Weblate (#9351) 2026-04-09 15:25:40 +03:00
Elian Doran
91afa08cdc Apply suggestions from code review
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-09 15:24:53 +03:00
Lorinc936
9e701645d5 Merge branch 'TriliumNext:main' into main 2026-04-09 11:23:49 +00:00
Giovi
d93b0442d2 Translated using Weblate (Italian)
Currently translated at 100.0% (395 of 395 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/it/
2026-04-09 11:10:04 +00:00
Giovi
ce4f9f5f01 Translated using Weblate (Italian)
Currently translated at 100.0% (1846 of 1846 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-04-09 11:10:03 +00:00
Aindriú Mac Giolla Eoin
353d638823 Translated using Weblate (Irish)
Currently translated at 100.0% (395 of 395 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ga/
2026-04-09 11:10:01 +00:00
Ali Kaya
995a774140 Translated using Weblate (Turkish)
Currently translated at 7.5% (30 of 395 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/tr/
2026-04-09 11:10:00 +00:00
green
c131b245bc Translated using Weblate (Japanese)
Currently translated at 100.0% (1846 of 1846 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-04-09 11:09:58 +00:00
Ali Kaya
42aabaf9b5 Translated using Weblate (Turkish)
Currently translated at 6.5% (121 of 1846 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/tr/
2026-04-09 11:09:56 +00:00
Bas Wouters
84cce151b8 Translated using Weblate (Dutch)
Currently translated at 4.1% (76 of 1846 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/nl/
2026-04-09 11:09:54 +00:00
Bas Wouters
e3e6316af7 Translated using Weblate (Dutch)
Currently translated at 26.7% (31 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/nl/
2026-04-09 11:09:53 +00:00
green
96e64c4f17 Translated using Weblate (Japanese)
Currently translated at 100.0% (395 of 395 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ja/
2026-04-09 11:09:52 +00:00
Aindriú Mac Giolla Eoin
3005917256 Translated using Weblate (Irish)
Currently translated at 100.0% (1846 of 1846 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-04-09 11:09:50 +00:00
Elian Doran
d34ba8b6f3 test(server): integration database not initialized properly 2026-04-09 13:46:44 +03:00
Elian Doran
d35b55f7d3 fix(server): tests failing due to SQL initialization order 2026-04-09 13:37:20 +03:00
Elian Doran
94de760fb5 Merge remote-tracking branch 'origin/main' into standalone 2026-04-09 12:57:06 +03:00
renovate[bot]
0fa121cdf2 fix(deps): update dependency marked to v17.0.6 2026-04-09 01:14:36 +00:00
renovate[bot]
3bf6215249 fix(deps): update dependency katex to v0.16.45 2026-04-09 01:13:57 +00:00
renovate[bot]
2ef045a66d chore(deps): update dependency dotenv to v17.4.1 2026-04-09 01:13:19 +00:00
renovate[bot]
2316f38978 chore(deps): update dependency minimatch@3>brace-expansion to v5 2026-04-08 21:14:49 +00:00
renovate[bot]
b65bf12247 fix(deps): update dependency eslint-linter-browserify to v10.2.0 2026-04-08 21:13:29 +00:00
Bart Visscher
55291d43a6 fix(web-clipper): Remove trailing / from triliumServerUrl 2026-04-08 16:38:17 +02:00
Tomáš Adámek
01bee95833 fix: extract finalizeStream helper, re-throw non-AbortError exceptions
- Extract duplicated cleanup logic into shared finalizeStream() function
- Add else branch to re-throw non-AbortError exceptions instead of swallowing them
2026-04-08 16:17:37 +02:00
Tomáš Adámek
5938fa6ffb fix: address review — shared PROTECTED_SYSTEM_NOTES, protection checks, soft delete description
- Move PROTECTED_SYSTEM_NOTES to helpers.ts for shared use
- move_note: check against full system notes set, add protected parent check
- clone_note: add source note protection + protected parent checks
- delete_note: fix description to say 'soft delete' (recoverable)
2026-04-08 16:08:02 +02:00
Elian Doran
743fe5a75d chore(deps): update dependency vite-plugin-static-copy to v4.0.1 (#9333) 2026-04-08 08:03:36 +03:00
renovate[bot]
0c2fdba586 chore(deps): update dependency vite-plugin-static-copy to v4.0.1 2026-04-08 01:34:23 +00:00
Elian Doran
a2c5adec3d Extra bugfixes (#9332) 2026-04-07 21:28:45 +03:00
Elian Doran
6089c8c7c6 Merge branch 'feature/extra_bugfixes' of https://github.com/TriliumNext/Trilium into feature/extra_bugfixes 2026-04-07 20:52:26 +03:00
Elian Doran
f28f725519 fix(server,desktop): not running correctly if placed in dot-hidden directory 2026-04-07 20:46:25 +03:00
Elian Doran
22d853e0b0 Apply suggestions from code review
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Elian Doran <contact@eliandoran.me>
2026-04-07 20:26:55 +03:00
Elian Doran
0f1d395651 refactor(script): move runScript to executeScript at BNote level 2026-04-07 19:59:14 +03:00
Elian Doran
3a0bab217d fix(share): hard-coded root-level paths 2026-04-07 19:48:06 +03:00
Elian Doran
f824cb5f15 feat(script): add API to execute backend 2026-04-07 19:40:12 +03:00
Elian Doran
40fd8d6d1a fix(quick_search): ctrl+click & middle click not working (closes #9220) 2026-04-07 19:18:16 +03:00
Elian Doran
e37f73bce0 fix(tab_bar): changing note icon reflect in the tab icon (closes #8994) 2026-04-07 19:11:26 +03:00
Elian Doran
d1cd08972f chore(share): use i18n for more strings 2026-04-07 19:07:20 +03:00
Elian Doran
5a13ca6409 feat(share): render dates on the client side 2026-04-07 19:06:33 +03:00
Elian Doran
eb3fd73415 fix(share): translation not used in template (closes #8722) 2026-04-07 18:54:13 +03:00
Elian Doran
1764fcbba2 fix(script): useContext not provided in imports (closes #9152) 2026-04-07 18:49:54 +03:00
Elian Doran
19f3552bfc fix(calendar): colors unreadable on dark theme (closes #8989)
The calendar event has a light yellow background with light yellow text in dark theme, making it nearly unreadable.

The root cause is a CSS load order issue. The :root defaults in index.css:1-10 are loaded after the dark theme's :root overrides (since component CSS loads after global CSS in Vite), so the defaults (95% lightness, 80% saturation) stomp over the dark theme values (20% lightness, 25% saturation). The background stays light, but --custom-color correctly gets the dark-adjusted (light) text color → light-on-light = bad contrast.

The fix: remove the :root block and use var() fallbacks instead, so there's no :root competition.
2026-04-07 18:48:32 +03:00
Elian Doran
cedce6cf32 feat(relation_map): rename relations through context menu (closes #442) 2026-04-07 18:47:00 +03:00
Elian Doran
26cf215150 fix(share): webviews occupied very little height (closes #9215) 2026-04-07 18:08:23 +03:00
Elian Doran
d21557069c fix(import/zip): ZIPs without language encoding flag importing garbage (closes #3013) 2026-04-07 17:01:34 +03:00
Elian Doran
b2e886fa26 fix(core): wrong package version 2026-04-07 16:54:07 +03:00
Elian Doran
28b2547229 Translations update from Hosted Weblate (#9327) 2026-04-07 16:13:33 +03:00
Elian Doran
6945ef5201 Merge remote-tracking branch 'origin/main' into standalone 2026-04-07 16:04:00 +03:00
Hosted Weblate
d75f556074 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/
2026-04-07 15:03:00 +02:00
Ali Kaya
eb66810e59 Translated using Weblate (Turkish)
Currently translated at 5.6% (104 of 1842 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/tr/
2026-04-07 15:02:55 +02:00
Tomas Adamek
540b39206d Translated using Weblate (Czech)
Currently translated at 100.0% (1842 of 1842 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/cs/
2026-04-07 15:02:55 +02:00
Tomas Adamek
5baea04c5d Translated using Weblate (Czech)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/cs/
2026-04-07 15:02:54 +02:00
Giovi
f5e65748a7 Translated using Weblate (Italian)
Currently translated at 100.0% (1842 of 1842 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-04-07 15:02:53 +02:00
Elian Doran
de84e09062 Custom dictionary (#9317) 2026-04-07 16:02:34 +03:00
Elian Doran
9beb756ccd feat(standalone): update build info 2026-04-07 16:02:08 +03:00
Elian Doran
34c5cfb638 fix(scripts): update build info no longer updating 2026-04-07 16:00:44 +03:00
Elian Doran
c81c88c930 fix(log): occassional race condition when creating log dir 2026-04-07 15:54:11 +03:00
Elian Doran
0b1122d9af Merge remote-tracking branch 'origin/main' into standalone 2026-04-07 15:50:59 +03:00
Elian Doran
2cb39ea7e3 fix(migration): don't crash on idempotent column creation 2026-04-07 15:30:24 +03:00
Elian Doran
6986963e45 e2e(server): update after changing spellcheck settings 2026-04-07 15:23:19 +03:00
Elian Doran
dc9b0093d9 fix(server): server-side rendered pages use old style 2026-04-07 15:18:38 +03:00
Elian Doran
40f9927842 docs(user): mention updates to spell check 2026-04-07 14:44:51 +03:00
Elian Doran
ff02f5f3ed Merge remote-tracking branch 'origin/main' into feature/custom_dictionary 2026-04-07 14:36:20 +03:00
Tomáš Adámek
dc40f6b530 feat(llm): add stop generation button
Allow users to stop an in-progress LLM generation by aborting the
SSE connection. The send button transforms into a red stop button
during streaming.

- AbortController passed to fetch() signal for stream cancellation
- On abort, partial content is finalized and saved as a message
- Stop button replaces send button during streaming with danger color
- Button is always clickable during streaming (not disabled)
2026-04-07 13:28:23 +02:00
Elian Doran
22149b94a1 chore(spellcheck): address requested changes 2026-04-07 14:26:55 +03:00
Tomáš Adámek
d771454aa5 feat(llm): add note mutation tools (rename, delete, move, clone)
Add four new LLM tools for note management:
- rename_note: Change the title of an existing note
- delete_note: Delete a note with system note protection
- move_note: Move a note to a new parent using branch service
- clone_note: Clone a note to an additional parent

All mutation tools are marked with mutates: true for the tool
approval system. Protected and system notes are guarded against
modification.
2026-04-07 13:19:16 +02:00
Elian Doran
372d25667f fix(deps): update codemirror themes (#9322) 2026-04-07 14:08:32 +03:00
Elian Doran
21f6cc00eb feat(options/spellcheck): improve display in browser 2026-04-07 13:31:22 +03:00
Elian Doran
620a080128 feat(options/media): hide spellcheck related setting in browser 2026-04-07 13:28:44 +03:00
Elian Doran
6a972aaf3d feat(codemirror): add four more themes 2026-04-07 13:25:25 +03:00
Elian Doran
d878d6b20b chore(deps): update dependency eslint to v10.2.0 (#9324) 2026-04-07 10:57:43 +03:00
Elian Doran
ec075311f4 fix(deps): update dependency preact to v10.29.1 (#9323) 2026-04-07 08:26:20 +03:00
Elian Doran
237c9bb62a fix(deps): update ai sdk (#9321) 2026-04-07 08:22:23 +03:00
Elian Doran
5aa9733bd7 chore(deps): update dependency typedoc-plugin-missing-exports to v4.1.3 (#9320) 2026-04-07 08:20:42 +03:00
renovate[bot]
a157a003c5 chore(deps): update dependency eslint to v10.2.0 2026-04-07 05:20:05 +00:00
renovate[bot]
e40869d3f8 fix(deps): update dependency preact to v10.29.1 2026-04-07 05:19:03 +00:00
Elian Doran
edaecfad4d fix(deps): update univer monorepo to v0.20.0 (#9325) 2026-04-07 08:18:48 +03:00
Elian Doran
983a98ae15 chore(deps): update dependency turndown to v7.2.4 (#9319) 2026-04-07 08:18:10 +03:00
Elian Doran
20ad902feb chore(deps): update dependency @types/node to v24.12.2 (#9318) 2026-04-07 08:15:55 +03:00
renovate[bot]
05de9c6e41 fix(deps): update univer monorepo to v0.20.0 2026-04-07 01:09:37 +00:00
renovate[bot]
df281cbbaa fix(deps): update codemirror themes 2026-04-07 01:07:46 +00:00
renovate[bot]
a979d11b8c fix(deps): update ai sdk 2026-04-07 01:07:09 +00:00
renovate[bot]
f7ff9c114f chore(deps): update dependency typedoc-plugin-missing-exports to v4.1.3 2026-04-07 01:06:33 +00:00
renovate[bot]
807dbdd133 chore(deps): update dependency turndown to v7.2.4 2026-04-07 01:05:58 +00:00
renovate[bot]
4aa944237f chore(deps): update dependency @types/node to v24.12.2 2026-04-07 01:05:24 +00:00
Elian Doran
48db55e3da chore(deps): update dependency vite to v8.0.5 [security] (#9313) 2026-04-06 22:33:44 +03:00
Elian Doran
bd1491e6e5 feat(options/i18n): add reference to spell check 2026-04-06 22:08:23 +03:00
Elian Doran
ac35730e3b feat(options/spellcheck): add button to reload app 2026-04-06 21:56:31 +03:00
Elian Doran
00023adbc0 Revert "feat(options/spellcheck): merge into single card"
This reverts commit 7b056fe1af.
2026-04-06 21:53:17 +03:00
Elian Doran
a70142a4dc feat(options/spellcheck): add button to edit custom words 2026-04-06 21:50:54 +03:00
Elian Doran
7b056fe1af feat(options/spellcheck): merge into single card 2026-04-06 21:44:49 +03:00
Elian Doran
467be38bd1 feat(options/spellcheck): improve language selection 2026-04-06 21:39:58 +03:00
renovate[bot]
933054a095 chore(deps): update dependency vite to v8.0.5 [security] 2026-04-06 18:36:10 +00:00
Elian Doran
f56482157c chore(ai): update system prompt 2026-04-06 21:25:54 +03:00
Elian Doran
5d0c91d91d fix(spellcheck): don't remove local words every time 2026-04-06 20:46:38 +03:00
Elian Doran
03136611a1 fix(spellcheck): don't merge words every time 2026-04-06 20:44:13 +03:00
Elian Doran
3e7488e4f3 feat(spellcheck): clean up local words 2026-04-06 20:36:51 +03:00
Elian Doran
3ed7d48d42 feat(spellcheck): save new words to custom dictionary 2026-04-06 20:28:22 +03:00
Elian Doran
ef72d89172 fix(spellcheck): custom dictionary not actually saved due to CLS 2026-04-06 20:16:02 +03:00
Elian Doran
ad97071862 feat(spellcheck): basic logic to save words 2026-04-06 20:09:29 +03:00
Elian Doran
2291892946 chore(server): create hidden note for the dictionary 2026-04-06 19:55:42 +03:00
Elian Doran
bf8cfa1421 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-04-06 19:48:42 +03:00
Elian Doran
bdd806efff refactor: delegate theme management completely to client via bootstrap 2026-04-06 19:45:18 +03:00
Elian Doran
c912c4af7b fix(webview): refresh content for SPAs with "query string" in hash (#8883) 2026-04-06 18:50:16 +03:00
Elian Doran
fc7f359f28 Update Web Clipper.md (#9294) 2026-04-06 16:51:46 +03:00
Elian Doran
2432112d68 chore(standalone): update version 2026-04-06 15:37:50 +03:00
Elian Doran
3cc52b2da2 fix(standalone): start-prod doesn't have CORS headers 2026-04-06 15:30:50 +03:00
Elian Doran
60192891ed fix(standalone): sqlite3-opfs failing 2026-04-06 15:26:34 +03:00
Elian Doran
21598f6189 chore(desktop): change more electron fuses for increased safety 2026-04-06 15:15:47 +03:00
Elian Doran
ae3a96b8d2 fix(standalone): broken due to vite-plugin-static-copy update 2026-04-06 15:00:23 +03:00
Elian Doran
38385ac936 Merge remote-tracking branch 'origin/main' into standalone 2026-04-06 14:28:40 +03:00
Elian Doran
a1987ea193 Feature/cleanup ck modules (#9310) 2026-04-06 13:27:26 +03:00
Elian Doran
480d167131 fix(desktop): cannot print/export to PDF on Linux wayland (closes #7967) 2026-04-06 13:11:06 +03:00
Elian Doran
d873accf3e Merge remote-tracking branch 'origin/main' into feature/cleanup_ck_modules 2026-04-06 12:41:45 +03:00
Elian Doran
94b448863c chore(deps): update dependency esbuild to v0.28.0 (#9302) 2026-04-06 12:34:15 +03:00
Elian Doran
32acc8555d fix(deps): update dependency @eslint/js to v10 (#9308) 2026-04-06 12:33:56 +03:00
Elian Doran
d68ad84155 Refactor/build warnings due to imports (#9309) 2026-04-06 12:32:32 +03:00
Elian Doran
45e82b7f33 test(ckeditor5-mermaid): fix type errors 2026-04-06 12:31:53 +03:00
Elian Doran
55ad0fe9f0 test(ckeditor5-mermaid): broken tests after change in rendering 2026-04-06 12:30:33 +03:00
Elian Doran
559815273e fix(ckeditor5-mermaid): protect against multiple init 2026-04-06 12:27:12 +03:00
Elian Doran
af76740fd9 fix(ckeditor5-mermaid): protect against stale renders 2026-04-06 12:26:17 +03:00
Elian Doran
7dadd50bfe chore(ckeditor5-mermaid): don't remove parent element on error 2026-04-06 12:25:50 +03:00
Elian Doran
dd4cab22c1 chore(client): address requested changes 2026-04-06 12:22:00 +03:00
Elian Doran
c4d3e776a1 refactor(client): the last circular dependency 2026-04-06 12:20:40 +03:00
Elian Doran
19bb7f5ddb refactor(server): remove unnecessary route 2026-04-06 12:18:36 +03:00
Elian Doran
d212120f9b refactor(client): read locales from common instead of going through the server 2026-04-06 12:16:32 +03:00
Elian Doran
42da1872e7 fix(client): crashing due to circular dependency 2026-04-06 12:10:33 +03:00
Elian Doran
a080b50c45 refactor(client): duplicate toast import 2026-04-06 12:06:27 +03:00
Elian Doran
6d31e9b028 feat(ckeditor5-mermaid): use more modern mechanism for rendering with less flicker 2026-04-06 12:03:29 +03:00
Elian Doran
b606afa858 refactor(ckeditor5-mermaid): get rid of any runtime dependencies 2026-04-06 11:59:24 +03:00
Elian Doran
f9446304b3 refactor(ckeditor5-mermaid): switch to es-toolkit 2026-04-06 11:57:31 +03:00
renovate[bot]
fbe312d580 chore(deps): update dependency esbuild to v0.28.0 2026-04-06 08:54:56 +00:00
Elian Doran
8d383caaff chore(deps): update dependency @electron/fuses to v2 (#9304) 2026-04-06 11:53:07 +03:00
Elian Doran
6caf4fa7ce chore(deps): update dependency copy-webpack-plugin to v14 (#9305) 2026-04-06 11:52:29 +03:00
Elian Doran
606d58b08c chore(ckeditor5-*): remove unnecessary publishing stack 2026-04-06 11:49:47 +03:00
Elian Doran
09258179f0 refactor(client): one more ineffective dynamic import due to appContext 2026-04-06 11:46:35 +03:00
Elian Doran
40e986b188 refactor(client): ineffective dynamic imports 2026-04-06 11:41:35 +03:00
Elian Doran
37e47041bf Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-04-06 11:41:09 +03:00
Elian Doran
543438bca0 chore(ai): mention instructions for adding new locale 2026-04-06 11:28:10 +03:00
Elian Doran
b31290c1fc fix(deps): update dependency fuse.js to v7.2.0 (#9303) 2026-04-06 11:22:33 +03:00
Elian Doran
d41111a209 docs(dev): remove unnecessary step for adding new locale 2026-04-06 11:21:17 +03:00
Elian Doran
828b523382 feat(i18n): enable Czech 2026-04-06 11:20:58 +03:00
Elian Doran
32409ecbee Translations update from Hosted Weblate (#9295) 2026-04-06 11:16:03 +03:00
renovate[bot]
3ca2cec63a chore(deps): update dependency copy-webpack-plugin to v14 2026-04-06 08:14:46 +00:00
Francis C.
1ed2db0c82 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1842 of 1842 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-04-06 08:13:48 +00:00
Francis C.
2423b74dd0 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (391 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/zh_Hant/
2026-04-06 08:13:48 +00:00
Tomas Adamek
3f781ea298 Translated using Weblate (Czech)
Currently translated at 98.9% (1822 of 1842 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/cs/
2026-04-06 08:13:47 +00:00
Tomas Adamek
30c5c49aef Translated using Weblate (Czech)
Currently translated at 100.0% (391 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/cs/
2026-04-06 08:13:47 +00:00
Tomas Adamek
9421e39c34 Translated using Weblate (Czech)
Currently translated at 100.0% (116 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/cs/
2026-04-06 08:13:46 +00:00
Tomas Adamek
c46805cf4f Translated using Weblate (Czech)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/cs/
2026-04-06 08:13:46 +00:00
Elian Doran
f181343fca chore(deps): update dependency @redocly/cli to v2.25.4 (#9297) 2026-04-06 11:13:37 +03:00
Elian Doran
8a512e4f73 chore(deps): update dependency electron to v41 (#9306) 2026-04-06 11:13:01 +03:00
Elian Doran
06a3750168 chore(renovate): group AI SDK updates 2026-04-06 11:09:07 +03:00
renovate[bot]
35c1a5642d fix(deps): update dependency @eslint/js to v10 2026-04-06 01:05:51 +00:00
renovate[bot]
f29df2ad28 chore(deps): update dependency electron to v41 2026-04-06 01:03:55 +00:00
renovate[bot]
75a5714451 chore(deps): update dependency @electron/fuses to v2 2026-04-06 01:01:53 +00:00
renovate[bot]
2882863b5b fix(deps): update dependency fuse.js to v7.2.0 2026-04-06 01:00:57 +00:00
renovate[bot]
773b6cca14 chore(deps): update dependency @redocly/cli to v2.25.4 2026-04-06 00:54:32 +00:00
Elian Doran
f97370c8f7 Dependency cleanup (#9293) 2026-04-05 23:04:03 +03:00
Elian Doran
afad96a375 Merge remote-tracking branch 'origin/main' into feature/dependency_cleanup 2026-04-05 22:56:13 +03:00
Elian Doran
9e5ababfcb chore(deps): update dependency electron to v41.1.1 (#9277) 2026-04-05 22:51:05 +03:00
Elian Doran
dc1e0e8db4 fix(desktop): tesseract.js not copied 2026-04-05 22:22:58 +03:00
Elian Doran
1e861d1125 chore(ocr): externalize tesseract.js completely 2026-04-05 22:20:38 +03:00
Elian Doran
baa93cb371 chore(ocr): expose needed dependencies 2026-04-05 22:14:01 +03:00
Elian Doran
61dcc8db47 Revert "fix(ocr): not working in server prod"
This reverts commit f4f881e839.
2026-04-05 21:53:53 +03:00
ce603
15505ffcd8 Update docs/User Guide/User Guide/Installation & Setup/Web Clipper.md
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-05 14:41:25 -04:00
Elian Doran
2c557eb015 Revert "fix(desktop): failing in prod due to tesseract"
This reverts commit 9e34fcb8a8.
2026-04-05 21:36:11 +03:00
Elian Doran
f5a80526ab fix(deps): update dependency mermaid to v11.14.0 (#9282) 2026-04-05 21:35:45 +03:00
ce603
96cef35f09 Update Web Clipper.md
Update web clipper docs with published store links
2026-04-05 14:28:51 -04:00
Elian Doran
27e1455874 fix(mermaid): treeview clipped when padding 2026-04-05 21:27:39 +03:00
Elian Doran
278d8428de feat(mermaid): integrate two new note types 2026-04-05 21:26:42 +03:00
Elian Doran
164e667158 chore: remove empty dependencies list in JSON 2026-04-05 21:05:11 +03:00
Elian Doran
28b31791e7 fix(codemirror): broken dependency on electron-window-state 2026-04-05 21:03:24 +03:00
Elian Doran
9515768e62 fix(server): broken dependency on electron-window-state 2026-04-05 21:02:03 +03:00
Elian Doran
fbbad19cb7 chore(deps): update dependency electron to v40.8.5 [security] (#9291) 2026-04-05 20:59:58 +03:00
Elian Doran
eab353ca2e chore(deps): remove unnecessary depedencies 2026-04-05 20:58:02 +03:00
Elian Doran
cb9ee20763 chore(deps): remove hard-coded dependency to @smithy/middleware-retry 2026-04-05 20:55:43 +03:00
Elian Doran
dac12532bc Merge branch 'main' into renovate/electron-41.x 2026-04-05 20:49:33 +03:00
Elian Doran
1d99734ea0 chore(ci): try to bypass operation not permitted in Electron build
7
node_modules/fs-xattr install: gyp http 200 https://nodejs.org/download/release/v24.14.1/node-v24.14.1-headers.tar.gz
node_modules/wxt/node_modules/esbuild postinstall$ node install.js
node_modules/wxt/node_modules/esbuild postinstall: Done
node_modules/macos-alias install: gyp http GET https://nodejs.org/download/release/v24.14.1/SHASUMS256.txt
node_modules/macos-alias install: gyp http 200 https://nodejs.org/download/release/v24.14.1/SHASUMS256.txt
node_modules/fs-xattr install: gyp http GET https://nodejs.org/download/release/v24.14.1/SHASUMS256.txt
node_modules/fs-xattr install: gyp http 200 https://nodejs.org/download/release/v24.14.1/SHASUMS256.txt
node_modules/electron postinstall: Done
.../remote/node_modules/electron postinstall: Done
 ERR_PNPM_EPERM  EPERM: operation not permitted, link '/Users/runner/work/Trilium/Trilium/node_modules/@electron/remote/node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Helpers' -> 'apps/desktop/node_modules/_tmp_3196_65c494775712c8b30c73644d84dc191e/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Helpers'
2026-04-05 20:49:06 +03:00
Elian Doran
3e764c762a chore(desktop): remove unnecessary dependencies 2026-04-05 20:43:37 +03:00
Elian Doran
7be51168d3 Merge branch 'main' into renovate/electron-41.x 2026-04-05 20:38:36 +03:00
Elian Doran
530d193734 fix(forge): build no longer working due to audit 2026-04-05 20:37:33 +03:00
Elian Doran
aba5ff75af fix(server): sync version not increased after breaking changes 2026-04-05 20:22:49 +03:00
Elian Doran
9e34fcb8a8 fix(desktop): failing in prod due to tesseract 2026-04-05 20:15:08 +03:00
Elian Doran
055dd9cd01 chore(toast): fix button alignment if no title & make buttons full-width 2026-04-05 20:14:54 +03:00
Elian Doran
1437fdc4e3 feat(ocr): warn if text wasn't retrieved on manual to due low confidence 2026-04-05 20:14:38 +03:00
Elian Doran
e5c67b16ac fix(flake): failing due to symlinks to /build 2026-04-05 20:12:59 +03:00
Elian Doran
94987314b8 feat(ocr): warn about OCR confidence too low 2026-04-05 20:03:12 +03:00
Elian Doran
f4f881e839 fix(ocr): not working in server prod 2026-04-05 19:58:48 +03:00
renovate[bot]
92f5901b95 chore(deps): update dependency electron to v41.1.1 2026-04-05 16:44:14 +00:00
renovate[bot]
1c0cb601cb chore(deps): update dependency electron to v40.8.5 [security] 2026-04-05 16:43:32 +00:00
Elian Doran
109f06f8bb Merge branch 'release/v0.102.2'
; Conflicts:
;	apps/desktop/package.json
;	apps/server/src/routes/api/image.ts
;	apps/server/src/share/routes.ts
;	pnpm-lock.yaml
2026-04-05 19:41:24 +03:00
Elian Doran
bf23439792 chore(release): prepare for v0.102.2 2026-04-05 19:30:04 +03:00
Elian Doran
b7a0bc08be Various bugfixes (#9274) 2026-04-05 19:28:59 +03:00
Elian Doran
9d6a26dda9 docs(security): add more details & change reporting mechanism 2026-04-05 19:28:30 +03:00
Elian Doran
a01ce2c3fc docs(release): release notes for v0.102.2 2026-04-05 19:28:03 +03:00
Elian Doran
ba6298af27 Translations update from Hosted Weblate (#9289) 2026-04-05 17:11:26 +03:00
green
3d17e0aa75 Translated using Weblate (Japanese)
Currently translated at 100.0% (1837 of 1837 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-04-05 14:10:34 +00:00
Elian Doran
7e18166160 chore(deps): update dependency esbuild to v0.27.5 (#9278) 2026-04-05 17:10:26 +03:00
Elian Doran
40d8571797 Translations update from Hosted Weblate (#9288) 2026-04-05 17:09:36 +03:00
Elian Doran
25e04e358a Apply suggestions from code review
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-05 17:08:56 +03:00
Aindriú Mac Giolla Eoin
e473e12c0e Translated using Weblate (Irish)
Currently translated at 100.0% (1837 of 1837 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-04-05 14:09:52 +02:00
Aindriú Mac Giolla Eoin
dfb20df16f Translated using Weblate (Irish)
Currently translated at 100.0% (391 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ga/
2026-04-05 14:09:49 +02:00
Elian Doran
efcbf439ee chore(deps): update dependency http-proxy-agent to v9 (#9283) 2026-04-05 13:54:41 +03:00
renovate[bot]
514f7fedbc chore(deps): update dependency http-proxy-agent to v9 2026-04-05 10:35:14 +00:00
Elian Doran
ee88fedacd chore(deps): update dependency https-proxy-agent to v9 (#9284) 2026-04-05 13:32:40 +03:00
renovate[bot]
2933f9c49f chore(deps): update dependency esbuild to v0.27.5 2026-04-05 10:26:58 +00:00
Elian Doran
1cca5d989c chore(deps): update dependency @playwright/test to v1.59.1 (#9276) 2026-04-05 13:25:47 +03:00
Elian Doran
9981020728 chore(deps): update dependency dotenv to v17.4.0 (#9280) 2026-04-05 13:25:17 +03:00
Elian Doran
56843dcf8b chore(deps): update dependency @ckeditor/ckeditor5-dev-build-tools to v55.3.1 (#9275) 2026-04-05 13:24:29 +03:00
Elian Doran
e661118192 fix(deps): update dependency @codemirror/view to v6.41.0 (#9281) 2026-04-05 13:23:16 +03:00
Elian Doran
54a7de6cb0 fix(deps): update dependency mathlive to v0.109.1 (#9279) 2026-04-05 13:22:48 +03:00
Elian Doran
13b1e0afbb fix(desktop): make failing due to wrong version of fuses 2026-04-05 12:46:39 +03:00
Elian Doran
4a48796142 chore(ci): trigger dev on release branches as well 2026-04-05 12:37:33 +03:00
Elian Doran
9a4fef80b9 chore(deps): fix pnpm lock 2026-04-05 12:15:07 +03:00
Elian Doran
79dc4b39f1 chore(client): address requested changes 2026-04-05 12:11:05 +03:00
Elian Doran
9bc18b774e test(server): add unit tests for sanitizeSvg 2026-04-05 12:11:05 +03:00
Elian Doran
465c36407c Update apps/server/src/etapi/notes.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 12:10:52 +03:00
Elian Doran
b99486259e Update apps/server/src/etapi/notes.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 12:10:44 +03:00
Elian Doran
ecf5475966 Update apps/desktop/package.json
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 12:10:29 +03:00
Elian Doran
90822cc8a3 chore: address requested changes 2026-04-05 11:59:45 +03:00
Elian Doran
5c46209ddc feat(server): improve request handling for SVGs 2026-04-05 11:28:28 +03:00
Elian Doran
176de87b6b feat(desktop): add Electron fuses 2026-04-05 11:01:22 +03:00
Elian Doran
7f199c527b feat(share): improve request handling for SVGs 2026-04-05 10:52:36 +03:00
Elian Doran
2432e230c5 chore(etapi): enforce MIME for image upload 2026-04-05 10:44:47 +03:00
Elian Doran
fc1be0d23d fix(ckeditor5-mermaid): use textContent for diagram source rendering 2026-04-05 10:17:16 +03:00
renovate[bot]
d084b9e941 chore(deps): update dependency https-proxy-agent to v9 2026-04-05 01:33:43 +00:00
renovate[bot]
6678c0af49 fix(deps): update dependency mermaid to v11.14.0 2026-04-05 01:32:26 +00:00
renovate[bot]
37754ecf31 fix(deps): update dependency @codemirror/view to v6.41.0 2026-04-05 01:31:45 +00:00
renovate[bot]
709d9633a1 chore(deps): update dependency dotenv to v17.4.0 2026-04-05 01:31:06 +00:00
renovate[bot]
7ca57efaad fix(deps): update dependency mathlive to v0.109.1 2026-04-05 01:30:27 +00:00
renovate[bot]
342fedca1c chore(deps): update dependency @playwright/test to v1.59.1 2026-04-05 01:28:20 +00:00
renovate[bot]
b1262b0448 chore(deps): update dependency @ckeditor/ckeditor5-dev-build-tools to v55.3.1 2026-04-05 01:27:37 +00:00
Elian Doran
626aca5181 fix(client): toasts could render HTML content 2026-04-04 22:21:25 +03:00
Elian Doran
8204322b46 fix(openid): use more secure RNG 2026-04-04 22:02:33 +03:00
Elian Doran
70ce86cd53 fix(scripts): electron rebuild failing in flake 2026-04-04 22:01:43 +03:00
Elian Doran
ed3b86cd49 fix(import): no longer preserve named note IDs 2026-04-04 21:27:37 +03:00
Elian Doran
b371675494 chore(commons): mark docName as a dangerous attribute 2026-04-04 21:25:05 +03:00
Elian Doran
ff06c8e7bd fix(client): validate docName attribute in doc renderer 2026-04-04 21:21:50 +03:00
Elian Doran
8ff41d8fa9 fix(server): align attachment upload validation with note upload 2026-04-04 20:46:03 +03:00
Elian Doran
65176ac140 chore(standalone): fix accidental port change 2026-04-04 17:36:47 +03:00
Elian Doran
62a34e90dd chore(standalone): fix type errors after merge with main 2026-04-04 17:35:48 +03:00
Elian Doran
b52e65278e Merge remote-tracking branch 'origin/main' into standalone
; Conflicts:
;	CLAUDE.md
;	apps/client/src/widgets/collections/board/data.spec.ts
;	apps/server/package.json
;	apps/server/src/routes/routes.ts
;	apps/server/src/services/app_info.ts
;	apps/server/src/services/blob-interface.ts
;	apps/server/src/services/entity_changes.ts
;	apps/server/src/services/handlers.ts
;	apps/server/src/services/hidden_subtree.ts
;	apps/server/src/services/image.ts
;	apps/server/src/services/options_init.ts
;	apps/server/src/services/search/services/search.ts
;	packages/trilium-core/src/services/blob.ts
;	packages/trilium-core/src/services/import/markdown.ts
;	packages/trilium-core/src/services/import/markdown/wikilink_internal_link.ts
;	packages/trilium-core/src/services/import/markdown/wikilink_transclusion.ts
;	packages/trilium-core/src/services/search/expressions/ocr_content.ts
;	packages/trilium-core/src/services/search/search_result.ts
;	packages/trilium-core/src/services/search/services/parse.ts
;	pnpm-lock.yaml
2026-04-04 17:16:52 +03:00
Elian Doran
5f5b9ba8cb Clean up dependencies (#9272) 2026-04-04 14:03:41 +03:00
Elian Doran
a3221470e7 refactor(ckeditor): get rid of lint-staged 2026-04-04 13:34:59 +03:00
Elian Doran
0e115bd92a refactor(ckeditor): get rid of unnecessary http-server & ts-node 2026-04-04 13:32:45 +03:00
Elian Doran
95a50c0ba6 refactor(ckeditor): get rid of ckeditor5-package-tools 2026-04-04 13:27:07 +03:00
Elian Doran
e323ccb259 refactor(turndown-plugin-gfm): convert tests from turndown-attendant to vite 2026-04-04 13:23:49 +03:00
Elian Doran
3294d0b93b refactor(splitjs): convert tests from karma to vitest 2026-04-04 13:13:07 +03:00
Elian Doran
55e8694990 test(server): remove redundant log 2026-04-04 13:10:03 +03:00
Elian Doran
b3888b391a chore(deps): fix minimatch issue 2026-04-04 13:09:53 +03:00
Elian Doran
f2907ab40f chore(deps): clean up some redundancies in overrides 2026-04-04 13:06:56 +03:00
Elian Doran
7e7218cbdf Merge remote-tracking branch 'origin/main' into chore/audit
; Conflicts:
;	pnpm-lock.yaml
2026-04-04 12:58:46 +03:00
Elian Doran
e41c9cb7f4 chore(deps): revert override for file-type 2026-04-04 12:57:09 +03:00
Elian Doran
20f96c88e4 Translations update from Hosted Weblate (#9271) 2026-04-04 12:53:52 +03:00
Elian Doran
66afda1343 Apply suggestions from code review
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-04 12:53:35 +03:00
Hosted Weblate
c5a6212065 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/
2026-04-04 11:50:19 +02:00
noobhjy
3e7e355575 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.1% (1800 of 1815 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-04-04 11:50:18 +02:00
green
fb9eb3e4b5 Translated using Weblate (Japanese)
Currently translated at 100.0% (391 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ja/
2026-04-04 11:50:17 +02:00
green
a35ac82f24 Translated using Weblate (Japanese)
Currently translated at 100.0% (1815 of 1815 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-04-04 11:50:17 +02:00
Elian Doran
66add6b9e4 fix(deps): update ckeditor monorepo to v48 (major) (#9270) 2026-04-04 12:50:07 +03:00
Elian Doran
fe81bde1c9 chore(scripts): fix electron rebuild failing due to Python on NixOS 2026-04-04 12:49:03 +03:00
Elian Doran
6b223098ab chore(deps): auto-fix deps 2026-04-04 12:45:11 +03:00
Elian Doran
788e867a6c fix(scripts): use flake when rebuilding Electron in postinstall script 2026-04-04 12:38:23 +03:00
Elian Doran
7ad8d307dc Merge remote-tracking branch 'origin/main' into renovate/major-ckeditor-monorepo
; Conflicts:
;	pnpm-lock.yaml
2026-04-04 12:34:17 +03:00
Elian Doran
b6d4ac5ada fix(text): signature change in CK watchdog 2026-04-04 12:30:02 +03:00
Elian Doran
0a069854e5 chore(deps): update dependency pdfjs-dist to v5.6.205 (#9227) 2026-04-04 12:26:08 +03:00
Elian Doran
8770afa211 fix(ckeditor): changes in icon package structure 2026-04-04 12:25:07 +03:00
Elian Doran
312c193b1a fix(text): patches no longer applying after version upgrade 2026-04-04 12:22:52 +03:00
Elian Doran
3700e2bb93 chore(ai): update copilot instructions for PDF.js update 2026-04-04 12:17:43 +03:00
renovate[bot]
a9be72081c fix(deps): update ckeditor monorepo to v48 2026-04-04 09:16:52 +00:00
Elian Doran
f57b57791b fix(pdfjs): potential cache issue with PDF.js (closes #9176) 2026-04-04 12:16:10 +03:00
Elian Doran
5cf249afa4 fix(deps): update dependency i18next to v26.0.3 (#9264) 2026-04-04 12:13:12 +03:00
Elian Doran
3f24627f67 chore(deps): update dependency tesseract.js to v7 (#9269) 2026-04-04 12:12:41 +03:00
Elian Doran
806c3fdc00 Merge remote-tracking branch 'origin/main' into renovate/pdfjs-dist-5.x 2026-04-04 12:11:47 +03:00
Elian Doran
e81ee88cda feat(pdfjs): update viewer to v5.6.205 2026-04-04 12:09:26 +03:00
Elian Doran
db46f63337 chore(deps): update dependency @smithy/middleware-retry to v4.4.46 (#9261) 2026-04-04 12:05:39 +03:00
Elian Doran
395102026d test(ocr): image processor with PNG 2026-04-04 12:04:19 +03:00
renovate[bot]
b62c078de6 fix(deps): update dependency i18next to v26.0.3 2026-04-04 08:57:28 +00:00
Elian Doran
47c1c08bed Improved tools & MC (#9256) 2026-04-04 11:53:14 +03:00
Elian Doran
a23c4f03e0 fix(deps): update dependency @ai-sdk/google to v3.0.55 (#9263) 2026-04-04 11:52:34 +03:00
Elian Doran
5a6da60fe8 chore(deps): update dependency @playwright/test to v1.59.0 (#9267) 2026-04-04 11:52:12 +03:00
Elian Doran
588c47aee7 chore(deps): update dependency yauzl to v3.3.0 (#9268) 2026-04-04 11:51:39 +03:00
Elian Doran
36fd51219a chore(deps): update dependency i18next-fs-backend to v2.6.3 (#9262) 2026-04-04 11:50:41 +03:00
Elian Doran
bc43a79d97 fix(deps): update dependency i18next-http-backend to v3.0.4 (#9265) 2026-04-04 11:50:28 +03:00
Elian Doran
5c22c029d7 fix(deps): update dependency react-i18next to v17.0.2 (#9266) 2026-04-04 11:50:15 +03:00
Elian Doran
126d9be9d8 fix(llm): one more async tool 2026-04-04 11:44:34 +03:00
Elian Doran
09be2822e0 fix(llm): some tools were async 2026-04-04 11:35:38 +03:00
Elian Doran
a93029f789 fix(llm): misuse of transactions in tool use due to async 2026-04-04 11:21:10 +03:00
Elian Doran
48cf214f4c chore(deps): address requested changes 2026-04-04 11:07:06 +03:00
Elian Doran
6834bad7b0 chore(deps): update pnpm lock 2026-04-04 10:44:16 +03:00
Elian Doran
855458bab0 feat(options): improve alignment of option rows 2026-04-04 10:40:31 +03:00
Elian Doran
5be48bf8c8 feat(options/advanced): use tabular layout for experimental features 2026-04-04 10:35:57 +03:00
Elian Doran
80ac0eea62 feat(options/llm): don't show settings unless the experimental setting is on 2026-04-04 10:33:24 +03:00
Elian Doran
5995ec468d feat(options/llm): improve layout for MCP card 2026-04-04 10:26:27 +03:00
Elian Doran
e9a876e8f0 feat(options/llm): display endpoint URL 2026-04-04 10:24:11 +03:00
Elian Doran
90223a5ffd chore(mcp): address requested changes 2026-04-04 10:15:05 +03:00
Elian Doran
8331daae5b chore(mcp): better loopback detection 2026-04-04 10:11:27 +03:00
Elian Doran
027280954a chore(llm): remove some lesser used fields from LLM response 2026-04-04 09:59:00 +03:00
Elian Doran
5138a63d23 chore(llm): encourage not to duplicate reference links with note titles 2026-04-04 09:44:11 +03:00
Elian Doran
be95cf5510 refactor(commons): deduplicate wikilink plugins 2026-04-04 09:40:48 +03:00
Elian Doran
4082328c2b feat(llm): encourage LLM to use reference links 2026-04-04 09:34:15 +03:00
Elian Doran
729e840af2 refactor(llm): build system prompt using arrays 2026-04-04 09:26:26 +03:00
Elian Doran
e4a38fe277 feat(llm): improve prompt when no access to web 2026-04-04 09:24:26 +03:00
Elian Doran
a5cb9c7de6 feat(llm): improve prompt when no access to notes 2026-04-04 09:23:17 +03:00
Elian Doran
7543109583 chore(llm): redesign thinking card 2026-04-04 09:15:40 +03:00
renovate[bot]
bff2f10fa4 chore(deps): update dependency tesseract.js to v7 2026-04-04 01:10:29 +00:00
renovate[bot]
37120bf153 chore(deps): update dependency yauzl to v3.3.0 2026-04-04 01:09:23 +00:00
renovate[bot]
b88c85db5e chore(deps): update dependency @playwright/test to v1.59.0 2026-04-04 01:08:16 +00:00
renovate[bot]
c682e3dfc0 fix(deps): update dependency react-i18next to v17.0.2 2026-04-04 01:07:09 +00:00
renovate[bot]
6c0bbb7778 fix(deps): update dependency i18next-http-backend to v3.0.4 2026-04-04 01:05:58 +00:00
renovate[bot]
bde8c40d16 fix(deps): update dependency @ai-sdk/google to v3.0.55 2026-04-04 01:03:43 +00:00
renovate[bot]
c4d352ba26 chore(deps): update dependency i18next-fs-backend to v2.6.3 2026-04-04 01:02:39 +00:00
renovate[bot]
cc1c0696ad chore(deps): update dependency @smithy/middleware-retry to v4.4.46 2026-04-04 01:01:28 +00:00
Elian Doran
186b784004 feat(llm): improve bubble layout 2026-04-03 22:52:31 +03:00
Elian Doran
5441d15654 refactor(llm): use separate component for expandable card 2026-04-03 22:38:11 +03:00
Elian Doran
bd61af89ae feat(llm): further improve display of citations 2026-04-03 22:34:01 +03:00
Elian Doran
eddd77f97f feat(llm): group sources in expandable header 2026-04-03 22:31:58 +03:00
Elian Doran
ab0338c318 fix(llm): duplicate citations 2026-04-03 22:17:40 +03:00
Elian Doran
1892bec772 fix(llm): tools calls not displayed while in progress 2026-04-03 22:13:51 +03:00
Elian Doran
bf7070a7da fix(llm): tools calls not displayed during streaming 2026-04-03 22:02:19 +03:00
Elian Doran
314331b956 chore(llm): improve tool call slightly 2026-04-03 21:36:31 +03:00
Elian Doran
6ff949fdb5 feat(llm): improve tool call icons 2026-04-03 21:30:55 +03:00
Elian Doran
21d24b7bea feat(llm): group tool calls 2026-04-03 21:25:16 +03:00
Elian Doran
8522151949 refactor(llm): remove legacy tool use 2026-04-03 21:20:44 +03:00
Elian Doran
3720099b1d chore(llm): use boxicons chevron 2026-04-03 21:16:53 +03:00
Elian Doran
073873c33c chore(llm): improve tool card slightly 2026-04-03 21:15:02 +03:00
Elian Doran
25bf62faa3 refactor(llm): split CSS into components 2026-04-03 21:06:50 +03:00
Elian Doran
e54cb9c626 feat(llm): basic nesting support 2026-04-03 21:00:22 +03:00
Elian Doran
208330d73a feat(llm): display tool calls as table 2026-04-03 20:53:52 +03:00
Elian Doran
343e3e67ed refactor(llm): extract tool call card to separate file 2026-04-03 20:47:44 +03:00
Elian Doran
6447003927 chore(llm): increase maximum number of steps 2026-04-03 20:38:11 +03:00
Elian Doran
cbdf925703 fix(llm): cannot create non-standard note types 2026-04-03 20:31:24 +03:00
Elian Doran
7440e4a610 feat(llm): limit number of results in note meta 2026-04-03 20:26:53 +03:00
Elian Doran
54a5c3fac0 feat(llm): mention child notes directly in system prompt 2026-04-03 20:12:54 +03:00
Elian Doran
42e60da127 feat(llm): mention total number of results in search 2026-04-03 19:55:11 +03:00
Elian Doran
325dc9c8a8 feat(llm): add content preview & parent title to search 2026-04-03 19:52:47 +03:00
Elian Doran
877427f0db refactor(llm): extract helpers out of tools 2026-04-03 19:49:01 +03:00
Elian Doran
1a64e7ba63 feat(llm): provide attachments list directly in note meta 2026-04-03 19:44:23 +03:00
Elian Doran
7dfa59a845 feat(llm): encourage through system prompt 2026-04-03 19:37:58 +03:00
Elian Doran
62fd19368d feat(llm): display content preview for attachments 2026-04-03 19:33:07 +03:00
Elian Doran
058518fcba feat(llm): allow reading attachment content with OCR integration 2026-04-03 19:30:03 +03:00
Elian Doran
6e1d10f052 chore(ai): update system prompt for tool creation 2026-04-03 19:24:47 +03:00
Elian Doran
af988fec69 refactor(llm): wrong types in MCP server 2026-04-03 19:20:25 +03:00
Elian Doran
dd5979aec8 refactor(llm): don't rely on ETAPI mappers 2026-04-03 19:17:59 +03:00
Elian Doran
657fbeba79 refactor(llm): use same method for meta between get_note and system prompt 2026-04-03 19:12:15 +03:00
Elian Doran
4a0d45ad7d feat(llm): get_attachment + get_note_attachments 2026-04-03 19:09:43 +03:00
Elian Doran
f47ec21aa8 feat(llm): provide content preview in system prompt 2026-04-03 18:59:46 +03:00
Elian Doran
be40d65982 feat(llm): format system prompt metadata as YAML 2026-04-03 18:54:58 +03:00
Elian Doran
faebacb883 feat(llm): inject meta-data directly in the system prompt 2026-04-03 18:51:33 +03:00
Elian Doran
df0efc39d5 refactor(llm): get rid of context-aware tools 2026-04-03 18:48:59 +03:00
Elian Doran
57a299de8f feat(llm): inject current note ID in the system prompt 2026-04-03 18:48:32 +03:00
Elian Doran
be724ec45f feat(llm/tools): split read_note into get_note and get_content_note 2026-04-03 18:42:00 +03:00
Elian Doran
98c70e662d feat(llm/tools): get attachments by note 2026-04-03 18:25:00 +03:00
Elian Doran
4ed9b84d75 chore(llm): synchronize provider configuration 2026-04-03 18:16:46 +03:00
Elian Doran
b7f05acfd3 fix(mcp): issues after merge 2026-04-03 18:09:33 +03:00
Elian Doran
45ebb37a01 Merge remote-tracking branch 'origin/main' into feature/mcp 2026-04-03 17:57:47 +03:00
Elian Doran
f77adea800 chore(deps): update typescript-eslint monorepo to v8.58.0 (#9237) 2026-04-03 17:17:10 +03:00
Elian Doran
88b855ed47 Translations update from Hosted Weblate (#9243) 2026-04-03 17:16:25 +03:00
Skriep
4fa689873f Translated using Weblate (Italian)
Currently translated at 99.2% (1774 of 1787 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-04-03 08:36:34 +00:00
Skriep
d76b9329fc Translated using Weblate (Russian)
Currently translated at 100.0% (391 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ru/
2026-04-03 08:36:32 +00:00
noobhjy
1c43ddd3a9 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 97.1% (1736 of 1787 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-04-03 08:36:31 +00:00
Skriep
1aedbcef94 Translated using Weblate (Russian)
Currently translated at 100.0% (1787 of 1787 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ru/
2026-04-03 08:36:29 +00:00
Skriep
295280861a Translated using Weblate (English)
Currently translated at 100.0% (1787 of 1787 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/en/
2026-04-03 08:36:28 +00:00
noobhjy
9f70e20fa0 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.7% (390 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/zh_Hans/
2026-04-03 08:36:26 +00:00
Aindriú Mac Giolla Eoin
a20e96eb6a Translated using Weblate (Irish)
Currently translated at 100.0% (391 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ga/
2026-04-03 08:36:25 +00:00
Aindriú Mac Giolla Eoin
9b238a3ac6 Translated using Weblate (Irish)
Currently translated at 100.0% (1787 of 1787 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-04-03 08:36:24 +00:00
Marc
0167597ae0 Translated using Weblate (French)
Currently translated at 100.0% (1787 of 1787 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/
2026-04-03 08:36:22 +00:00
Marc
a4f6071c8b Translated using Weblate (French)
Currently translated at 100.0% (119 of 119 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/fr/
2026-04-03 08:36:21 +00:00
Marc
aa0b0bd249 Translated using Weblate (French)
Currently translated at 100.0% (391 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/fr/
2026-04-03 08:36:19 +00:00
green
c6185a51c2 Translated using Weblate (Japanese)
Currently translated at 100.0% (1787 of 1787 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-04-03 08:36:18 +00:00
green
9c9c717025 Translated using Weblate (Japanese)
Currently translated at 100.0% (391 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ja/
2026-04-03 08:36:16 +00:00
Marc
00342ed569 Translated using Weblate (French)
Currently translated at 89.7% (1604 of 1787 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/
2026-04-03 08:36:14 +00:00
Elian Doran
1f0a6b4a79 feat(ocr): add OCR (#5834) 2026-04-03 11:35:36 +03:00
Elian Doran
3e767b4723 chore(ocr): remove accidentally commited file 2026-04-03 11:25:41 +03:00
Elian Doran
e539b11718 chore(ocr): upgrade to officeprocessor v6 to avoid pdfjs issues 2026-04-03 11:11:53 +03:00
Elian Doran
2fca8c3850 fix(build): missing pdfjs-dist 2026-04-03 10:33:19 +03:00
Elian Doran
0d3f70a231 chore(server): try to bypass officeparser PDFjs issue 2026-04-03 10:02:54 +03:00
Elian Doran
a3a52aaafe chore(ocr): switch to unpdf due to issues with pdfjs-dist 2026-04-03 09:22:56 +03:00
Elian Doran
a6c4401973 chore(server): remove pdf-parse dependency 2026-04-03 09:04:56 +03:00
Elian Doran
2e34ec2a17 chore(server): remove sharp from externals 2026-04-03 09:04:04 +03:00
Elian Doran
927afec83c chore(ocr): remove multi-page TIFF support for now to remove dependency to sharp 2026-04-03 08:50:02 +03:00
Elian Doran
8bd1da0552 fix(deps): update dependency i18next to v26.0.2 (#9255) 2026-04-03 08:42:50 +03:00
renovate[bot]
4f571fc3d7 fix(deps): update dependency i18next to v26.0.2 2026-04-03 00:49:43 +00:00
Elian Doran
c3f8e523cc fix(deps): update dependency lodash-es to v4.18.1 [security] (#9252) 2026-04-02 23:14:01 +03:00
Elian Doran
9878f76f65 fix(ocr): sharp failing on Alpine 2026-04-02 22:56:22 +03:00
Elian Doran
23799562ae refactor(ocr): reuse office processor for PDFs 2026-04-02 22:53:57 +03:00
Elian Doran
f441a145b5 fix(server): prod not starting due to bundling issues 2026-04-02 22:42:53 +03:00
Elian Doran
7189764916 chore(ocr): support overriding cache dir 2026-04-02 22:00:37 +03:00
Elian Doran
70bc707e3a chore(ocr): address requested changes 2026-04-02 21:58:54 +03:00
Elian Doran
90215bde8b chore(ocr): remove unnecessary index 2026-04-02 21:55:07 +03:00
Elian Doran
2b3ae5285b test(server): update integration DB to latest migration 2026-04-02 21:49:19 +03:00
Elian Doran
9b6d0db5b6 test(server): fix outdated tests in search result 2026-04-02 21:48:42 +03:00
Elian Doran
723da88ff8 chore(ocr): disable auto-processing by default 2026-04-02 21:46:05 +03:00
Elian Doran
5bcf2f4356 chore(deps): remove deprecated types for tesseract 2026-04-02 21:34:32 +03:00
Elian Doran
42680574c1 chore(deps): update pnpm lock 2026-04-02 21:34:03 +03:00
Elian Doran
82e723c915 test(ocr): fix broken tests 2026-04-02 21:27:46 +03:00
renovate[bot]
ac9560d9d7 chore(deps): update typescript-eslint monorepo to v8.58.0 2026-04-02 18:18:05 +00:00
Elian Doran
32f95efa54 fix(ocr): image OCR in search results not shown 2026-04-02 21:14:56 +03:00
Elian Doran
3da416908d feat(ocr): display content snippet in quick search 2026-04-02 21:04:18 +03:00
Elian Doran
d79d2e9ad2 fix(ocr): too many blob queries in search 2026-04-02 20:58:11 +03:00
Elian Doran
30ba36894d chore(ocr): optimize search algorithm
OCRContentExpression now takes all tokens as an array (like NoteContentFulltextExp), iterates over the input note set from becca, and checks text representations in-memory — zero SQL queries.
parse.ts creates a single OCRContentExpression(tokens) instead of N separate instances.
The LIMIT 50 and the N+1 blob→note/attachment queries are gone entirely.
2026-04-02 20:54:22 +03:00
Elian Doran
b747402352 chore(ocr): get rid of costly ranking for OCR 2026-04-02 20:48:41 +03:00
Elian Doran
0398a9bda3 refactor(ocr): potential race condition with image upload 2026-04-02 20:40:17 +03:00
Elian Doran
72dff88384 refactor(ocr): get rid of unused routes and services 2026-04-02 20:34:37 +03:00
Elian Doran
0314a9755f refactor(ocr): minor changes 2026-04-02 20:32:58 +03:00
Elian Doran
bc967b15b2 chore(server): fix accidental changes 2026-04-02 20:28:17 +03:00
Elian Doran
8ac686a19f fix(ocr): TIFF overlapping with image processor 2026-04-02 20:26:31 +03:00
Elian Doran
aafecaa3a4 refactor(ocr): get rid of fake metadata 2026-04-02 20:24:31 +03:00
Elian Doran
bb23b08b15 refactor(ocr): get rid of unused clean up 2026-04-02 20:23:03 +03:00
Elian Doran
476396da53 refactor(ocr): deduplicate batch processing 2026-04-02 20:19:32 +03:00
Elian Doran
5112971848 refactor(ocr): reduce duplication 2026-04-02 20:17:24 +03:00
Elian Doran
2d852c38ec feat(ocr): automatic processing of attachments 2026-04-02 20:00:55 +03:00
Elian Doran
f163cacddc feat(ocr): integrate viewing attachment OCR 2026-04-02 19:51:11 +03:00
Elian Doran
6ecb1cb2b0 feat(settings): cross-reference OCR and language & region settings 2026-04-02 17:09:27 +03:00
Elian Doran
24fefe0711 refactor(ocr): remove unnecessary methods 2026-04-02 13:17:38 +03:00
Elian Doran
e5eba69d0d fix(ocr): cannot handle image/tiff 2026-04-02 12:51:58 +03:00
Elian Doran
bdd2b7e317 fix(ocr): properly handle office MIME types 2026-04-02 12:41:45 +03:00
Elian Doran
ad29375975 chore(ocr): remove unimplemented logic 2026-04-02 12:36:10 +03:00
Elian Doran
cf73a4ef43 feat(llm): integrate with OCR 2026-04-02 12:16:17 +03:00
Elian Doran
60a2621928 chore(ocr): remove last extraction date
Wasn't useful because blobs are hash-identified
2026-04-02 12:04:27 +03:00
Elian Doran
b4e5d9dbc2 feat(ocr): not well integrate with sync 2026-04-02 11:43:19 +03:00
Elian Doran
650b700415 feat(options/media): use a slider for JPEG quality 2026-04-02 11:17:54 +03:00
renovate[bot]
212f742164 fix(deps): update dependency lodash-es to v4.18.1 [security] 2026-04-02 08:16:15 +00:00
Elian Doran
6f2296eb05 feat(ocr): use a slider for confidence 2026-04-02 11:09:36 +03:00
Elian Doran
722efd74c2 fix(ocr): default confidence level is too low 2026-04-02 11:06:58 +03:00
Elian Doran
5dc9b6defe chore(ocr): deduplicate & fix percentage for confidence in log 2026-04-02 11:04:26 +03:00
Elian Doran
605fbaaa4a fix(ocr): automatic OCR not respecting language 2026-04-02 11:01:20 +03:00
Elian Doran
23b46865c5 refactor(ocr): simplify initialization of image processor 2026-04-02 10:59:58 +03:00
Elian Doran
ac310eaaf5 feat(ocr): handle cache dir properly 2026-04-02 10:54:15 +03:00
Elian Doran
010f59df8a chore(ocr): make OCR text representation selectable 2026-04-02 10:25:41 +03:00
Elian Doran
44a5dccd61 chore(ocr): remove master switch 2026-04-02 10:22:34 +03:00
Elian Doran
acbbf021a1 refactor(ocr): remove unnecessary translations 2026-04-02 10:13:03 +03:00
Elian Doran
731fece258 feat(ocr): reintroduce batch processing 2026-04-02 10:08:24 +03:00
Elian Doran
8d255d1b89 feat(ocr): make "process OCR" always reprocess 2026-04-02 10:02:06 +03:00
Elian Doran
64318c92e7 fix(ocr): route default interfering with content language 2026-04-02 10:00:12 +03:00
Elian Doran
49fc7e48d4 feat(ocr): integrate with content language 2026-04-02 09:52:28 +03:00
Elian Doran
ec9fa0baee chore(options): rename options to match media scope 2026-04-01 22:42:17 +03:00
Elian Doran
ba91d91fd1 chore(options): start adding options for OCR 2026-04-01 22:37:32 +03:00
Elian Doran
0aa1fea9dc chore(options): improve media layout slightly 2026-04-01 22:30:41 +03:00
renovate[bot]
1551f01f49 chore(deps): update dependency pdfjs-dist to v5.6.205 2026-04-01 19:04:32 +00:00
Elian Doran
d46748602e chore(settings): rebrand Images settings page to Media 2026-04-01 22:01:21 +03:00
Elian Doran
9cfad0fe6a refactor(ocr): move TextRepresentationResponse into server_api 2026-04-01 21:45:28 +03:00
Elian Doran
6d3cff84a4 feat(ocr): allow reprocessing of a file 2026-04-01 17:21:12 +03:00
Elian Doran
010230645c fix(ocr): text displayed in monospace 2026-04-01 17:20:10 +03:00
Elian Doran
5979290f0c refactor(ocr): get rid of inline styles 2026-04-01 17:18:58 +03:00
Elian Doran
e648872257 fix(ocr): incorrect date display 2026-04-01 17:17:49 +03:00
Elian Doran
e4910ae31a fix(ocr): pdf extraction not working due to import 2026-04-01 17:14:37 +03:00
Elian Doran
d8ea0c7bcf feat(ocr): allow manual processing of OCR 2026-04-01 17:09:26 +03:00
Elian Doran
6393d2c188 chore(ocr): remove trainneddata artifact 2026-04-01 17:08:15 +03:00
Elian Doran
d9f0a163cf refactor(ocr): use idiomatic status handling 2026-04-01 17:04:36 +03:00
Elian Doran
6534beec14 fix(ocr): errors not properly shown due to lack of convention 2026-04-01 16:58:34 +03:00
Elian Doran
6d050340ee fix(client): server errors don't reject the promise 2026-04-01 16:53:50 +03:00
Elian Doran
0e7f7fa208 chore(ocr): fix type issues & integrate ReadOnlyTextRepresentation 2026-04-01 16:45:38 +03:00
Elian Doran
287be0bd25 chore(scripts): integrate filter-tsc-output from standalone branch 2026-04-01 16:39:54 +03:00
Elian Doran
18cf2ff873 test(ocr): fix type issues 2026-04-01 16:35:45 +03:00
Elian Doran
b626fb448b refactor(ocr): get rid of require imports 2026-04-01 16:30:27 +03:00
Elian Doran
38f6fb5a7f refactor(ocr): rename ocr_last_processed to textExtractionLastProcessed 2026-04-01 16:26:16 +03:00
Elian Doran
5846df7d02 refactor(ocr): rename ocr_text to textRepresentation 2026-04-01 16:14:08 +03:00
Elian Doran
9462d6109c Merge remote-tracking branch 'origin/main' into feat/add-ocr-capabilities 2026-04-01 15:59:05 +03:00
Elian Doran
f0c93cd06e feat(llm): improve display of blocks while streaming 2026-04-01 15:38:23 +03:00
Elian Doran
14e0507689 fix(llm): web search not translated 2026-04-01 15:28:49 +03:00
Elian Doran
393b90f7be feat(llm): display skill read 2026-04-01 15:27:31 +03:00
Elian Doran
47ee5c1d84 feat(llm): display affected note in read current note 2026-04-01 15:11:34 +03:00
Elian Doran
1cb6f2d351 chore(llm): improve layout for tool card 2026-04-01 15:09:45 +03:00
Elian Doran
bb72b0cdfc refactor(llm): proper translation use for element interpolation 2026-04-01 15:04:07 +03:00
Elian Doran
ab2467b074 feat(llm): display note creation result 2026-04-01 14:57:45 +03:00
Elian Doran
2d652523bb feat(llm): display a reference to the affected note in tool calls 2026-04-01 14:55:18 +03:00
Elian Doran
55df50253f feat(llm): improve tool call style slightly 2026-04-01 14:51:17 +03:00
Elian Doran
d009914ff9 chore(llm): update system prompt for tool creation 2026-04-01 14:48:13 +03:00
Elian Doran
5e97222206 feat(llm): display friendly tool names 2026-04-01 14:47:17 +03:00
Elian Doran
038705483b refactor(llm): integrate tools requiring context 2026-04-01 12:34:14 +03:00
Elian Doran
10c9ba5783 refactor(llm): different way to register tools 2026-04-01 12:20:08 +03:00
Elian Doran
a1d008688b chore(llm): harden MCP against uninitialized database 2026-04-01 11:56:46 +03:00
Elian Doran
78a043c536 test(llm): test MCP using supertest 2026-04-01 11:52:49 +03:00
Elian Doran
acdc840f17 feat(llm): improve MCP settings card 2026-04-01 11:46:54 +03:00
Elian Doran
63d4b8894b feat(llm): gate MCP access behind option 2026-04-01 11:44:01 +03:00
Elian Doran
23ccbf9642 chore(llm): add instructions for MCP use 2026-04-01 11:30:47 +03:00
Elian Doran
a5793ff768 chore(mcp): add MCP config for localhost 2026-04-01 11:29:29 +03:00
Elian Doran
a84e2f72c3 feat(llm/mcp): first implementation 2026-04-01 11:19:10 +03:00
Elian Doran
0d805a01c1 fix(deps): update dependency i18next to v26 (#9224) 2026-04-01 10:58:03 +03:00
Elian Doran
ba90a1c396 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-04-01 10:38:47 +03:00
Elian Doran
465927e730 chore(deps): update dependency vite-plugin-static-copy to v4 (#9147) 2026-04-01 10:28:46 +03:00
Elian Doran
74f3c14a62 fix(llm): sidebar chat lost when saving to note 2026-04-01 10:26:33 +03:00
Elian Doran
2eb40c7b42 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-04-01 09:30:37 +03:00
Elian Doran
457c5f85af chore(client/i18n): fix weird translation 2026-04-01 09:30:34 +03:00
copilot-swe-agent[bot]
c6ef3d774a fix: update vite.config.mts for vite-plugin-static-copy v4 breaking change
Agent-Logs-Url: https://github.com/TriliumNext/Trilium/sessions/df2e0038-ab36-4d77-b73a-f4739f9db838

Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2026-03-31 20:31:34 +00:00
copilot-swe-agent[bot]
12b946157a Merge remote-tracking branch 'origin/main' into standalone
# Conflicts:
#	pnpm-lock.yaml

Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2026-03-31 20:14:04 +00:00
copilot-swe-agent[bot]
7f1e4c0969 fix: remove showSupportNotice from i18next init options (removed in v26)
Agent-Logs-Url: https://github.com/TriliumNext/Trilium/sessions/41f772f7-49b7-4905-8b17-cf90165fc736

Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2026-03-31 20:13:27 +00:00
renovate[bot]
e55cd7841f fix(deps): update dependency i18next to v26 2026-03-31 20:03:35 +00:00
Elian Doran
8b5b32fecb chore(deps): update dependency typescript to v6 (#9162) 2026-03-31 23:01:09 +03:00
Elian Doran
93b126d92b chore(deps): update pnpm lock 2026-03-31 22:45:17 +03:00
Elian Doran
5fce7283f1 Merge remote-tracking branch 'origin/main' into standalone 2026-03-31 22:43:39 +03:00
copilot-swe-agent[bot]
819c9a7506 fix: resolve TypeScript 6 typecheck issues
- Remove deprecated `downlevelIteration` from tsconfig.base.json (not needed for ES2022+ target)
- Add `noUncheckedSideEffectImports: false` to tsconfig.base.json and ckeditor5 package tsconfigs to allow CSS/plugin side-effect imports
- Remove deprecated `baseUrl: "."` from 6 package tsconfig.lib.json files (unused without `paths`)
- Replace `NodeJS.Timeout` with `ReturnType<typeof setTimeout>` in debounce.ts

Agent-Logs-Url: https://github.com/TriliumNext/Trilium/sessions/8e861e56-2be6-4c61-9558-a666abbe3ff0

Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2026-03-31 19:22:16 +00:00
Elian Doran
4b3ef50d4b Feature/llm tools (#9241) 2026-03-31 22:10:16 +03:00
Elian Doran
bc945c5196 Translations update from Hosted Weblate (#9242) 2026-03-31 22:08:37 +03:00
Giovi
57ea3c576e Translated using Weblate (Italian)
Currently translated at 100.0% (1775 of 1775 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-03-31 19:06:02 +00:00
Marc
450e15f558 Translated using Weblate (French)
Currently translated at 89.0% (1581 of 1775 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/
2026-03-31 19:06:01 +00:00
Marc
a66ef977a0 Translated using Weblate (French)
Currently translated at 100.0% (391 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/fr/
2026-03-31 19:05:59 +00:00
Marc
96a474adc1 Translated using Weblate (French)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/fr/
2026-03-31 19:05:59 +00:00
Giovi
1fe22aeef1 Translated using Weblate (Italian)
Currently translated at 100.0% (391 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/it/
2026-03-31 19:05:58 +00:00
Elian Doran
a97897527e fix(deps): update univer monorepo to v0.19.0 (#9223) 2026-03-31 22:05:49 +03:00
Elian Doran
86bbb4d885 chore(deps): update dependency @redocly/cli to v2.25.3 (#9233) 2026-03-31 21:59:25 +03:00
Elian Doran
041f8314ab fix(deps): update dependency mind-elixir to v5.10.0 (#9228) 2026-03-31 21:58:13 +03:00
Elian Doran
dffdeff798 chore(deps): fix flake lock 2026-03-31 21:52:55 +03:00
copilot-swe-agent[bot]
6f08dc3ada Merge branch 'main' into renovate/mind-elixir-5.x - resolve translations conflict
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2026-03-31 18:21:21 +00:00
copilot-swe-agent[bot]
07e1b86586 chore: keep only English mind-map translations (others handled by Weblate)
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2026-03-31 18:20:11 +00:00
copilot-swe-agent[bot]
2deda8947e feat: migrate mind-elixir i18n to use own translations integrated with Weblate
- Remove deprecated `locale` option and LOCALE_MAPPINGS constant from MindMap.tsx
- Add `buildMindElixirLangPack()` function using i18next translations for contextMenu.locale
- Add mind-map translation keys to all 37 locale translation files
- Languages with specific translations: de, es, fr, it, ja, pt, pt_br, ru, ro, cn, tw, fi, ko, nl, nb-NO, sv
- Other languages fall back to English via i18next

Agent-Logs-Url: https://github.com/TriliumNext/Trilium/sessions/f2cb95ee-9a97-4618-ba9a-5fb7f31ab965

Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2026-03-31 18:08:38 +00:00
Elian Doran
adb9532d1b chore(deps): update dependency @smithy/middleware-retry to v4.4.45 (#9234) 2026-03-31 21:06:22 +03:00
Elian Doran
a2959342a9 chore(deps): update dependency express-rate-limit to v8.3.2 (#9236) 2026-03-31 21:05:58 +03:00
Elian Doran
f528833232 chore(llm): relocate skills to assets 2026-03-31 20:52:17 +03:00
Elian Doran
a6b8785341 chore(llm): address requested changes 2026-03-31 20:32:19 +03:00
Elian Doran
6e7a14fb3e chore(llm): update to AI SDK 6 2026-03-31 20:24:49 +03:00
Elian Doran
708180a037 fix(llm): sending empty messages crashes on Anthropic 2026-03-31 19:47:39 +03:00
Elian Doran
04efa2742c feat(llm): basic support for Google Gemini 2026-03-31 19:28:42 +03:00
Elian Doran
0e2c96d544 feat(llm): add web search to OpenAI 2026-03-31 19:08:41 +03:00
Elian Doran
a45c1818a5 refactor(llm): deduplicate logic between providers 2026-03-31 19:05:38 +03:00
Elian Doran
f04f47d17a fix(llm): not returning full list of models 2026-03-31 18:59:02 +03:00
Elian Doran
cabce14a49 chore(llm): set up for ChatGPT 2026-03-31 18:51:19 +03:00
Elian Doran
5f669684c4 feat(llm): enforce MIME type in code notes 2026-03-31 18:39:47 +03:00
Elian Doran
4d169809bd chore(llm): improve render notes skill 2026-03-31 18:12:42 +03:00
Elian Doran
2929d64fa0 chore(llm): improve TSX import skill 2026-03-31 18:07:28 +03:00
Elian Doran
20311d31f6 chore(llm): modify frontend script to prefer Preact 2026-03-31 16:04:48 +03:00
Elian Doran
c13b68ef42 feat(llm): basic skill to write scripts 2026-03-31 16:01:20 +03:00
Elian Doran
8eff623b67 Merge remote-tracking branch 'origin/main' into feature/llm_tools 2026-03-31 15:52:10 +03:00
Elian Doran
f4b9207379 fix(llm/sidebar): no longer properly persisting the chat 2026-03-31 15:52:05 +03:00
Elian Doran
90930e19e7 feat(llm): improve search discoverability 2026-03-31 15:41:56 +03:00
Elian Doran
8c0dacd6d7 feat(llm): basic skill to do search 2026-03-31 15:36:50 +03:00
Elian Doran
c617bea45a feat(llm): basic tool to get subtree 2026-03-31 15:15:14 +03:00
Elian Doran
bac25c9173 feat(llm): basic tool to get child notes 2026-03-31 15:04:02 +03:00
renovate[bot]
acfc3f617e chore(deps): update dependency typescript to v6 2026-03-31 11:14:01 +00:00
Elian Doran
4c6aa3baf1 Translations update from Hosted Weblate (#9240) 2026-03-31 14:11:37 +03:00
Elian Doran
ed2d72c008 AI reintegration test (#9225) 2026-03-31 14:11:02 +03:00
Marc
3cb82c58a1 Translated using Weblate (French)
Currently translated at 99.3% (157 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/fr/
2026-03-31 13:09:51 +02:00
Marc
d87e3cb24d Translated using Weblate (French)
Currently translated at 90.2% (1551 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/
2026-03-31 13:09:50 +02:00
Elian Doran
8a4c46c40b feat(server): protect becca against protoype pollution 2026-03-31 14:03:49 +03:00
Elian Doran
5f3dcdb7e5 fix(renovate): set up a minimum release age before doing updates 2026-03-31 10:53:37 +03:00
Elian Doran
8964c316b8 Revert "chore(deps): update dependency axios to v1.14.1" (#9239) 2026-03-31 10:46:43 +03:00
Elian Doran
230f682a27 Revert "chore(deps): update dependency axios to v1.14.1" 2026-03-31 10:46:30 +03:00
Elian Doran
8f25d048df chore(deps): update dependency axios to v1.14.1 (#9235) 2026-03-31 07:32:25 +03:00
renovate[bot]
90fcf3153c chore(deps): update dependency express-rate-limit to v8.3.2 2026-03-31 01:48:59 +00:00
renovate[bot]
069c4cf5c4 chore(deps): update dependency axios to v1.14.1 2026-03-31 01:48:18 +00:00
renovate[bot]
f10e55ad71 chore(deps): update dependency @smithy/middleware-retry to v4.4.45 2026-03-31 01:47:36 +00:00
renovate[bot]
a934c7842b chore(deps): update dependency @redocly/cli to v2.25.3 2026-03-31 01:46:56 +00:00
Elian Doran
a2b6bc0493 chore(llm): address requested changes 2026-03-30 22:20:44 +03:00
Elian Doran
24e418bf7c Translations update from Hosted Weblate (#9232) 2026-03-30 22:03:35 +03:00
Hosted Weblate
3fc3ef4ea8 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-03-30 18:59:12 +00:00
Elian Doran
952d6b9851 feat(db): add missing sqlite indices to help with performance (#9141) 2026-03-30 21:58:54 +03:00
Elian Doran
841c58ca8c chore: fix type errors 2026-03-30 20:23:00 +03:00
Elian Doran
41164add15 chore(deps): fix OOM caused by Zod
See https://github.com/vercel/ai/issues/7351
2026-03-30 20:17:37 +03:00
Elian Doran
f4858d3684 refactor(llm): simplify the saving process 2026-03-30 19:40:38 +03:00
Elian Doran
be60479122 fix(llm): XSS risk when displaying the message 2026-03-30 19:36:22 +03:00
Elian Doran
948f160d14 fix(llm): XSS risk when displaying the message 2026-03-30 19:31:56 +03:00
Elian Doran
768c733f92 fix(llm): missing translation for name 2026-03-30 19:31:44 +03:00
Elian Doran
1a02be7c91 fix(llm): usage not reset when opening an empty chat 2026-03-30 19:23:42 +03:00
Elian Doran
ac75f6f7a6 feat(llm): hide the feature behind an experimental flag 2026-03-30 19:19:04 +03:00
Elian Doran
b2befb4feb feat(llm): automatic refresh of note title 2026-03-30 19:08:54 +03:00
Elian Doran
3e49399f82 fix(llm): automatic title not working for standalone chats 2026-03-30 19:03:17 +03:00
Elian Doran
eaaaf3effd fix(llm): automatic title not persisted 2026-03-30 18:59:49 +03:00
Elian Doran
f2cd1be3af fix(llm): history doesn't show last notes correctly 2026-03-30 18:55:41 +03:00
Elian Doran
b4fcf41420 feat(llm): basic auto-title 2026-03-30 18:52:22 +03:00
Elian Doran
5feccae2a0 feat(llm): enable cache control in Anthropic 2026-03-30 18:26:49 +03:00
Elian Doran
d28318005d feat(llm): basic support for attributes 2026-03-30 18:26:23 +03:00
Elian Doran
fcf39d7786 feat(llm): show footer only on hover 2026-03-30 18:14:23 +03:00
Elian Doran
5e9fc614d7 feat(llm): display message time 2026-03-30 18:08:20 +03:00
Elian Doran
a860803cc4 feat(llm): add usage underneath the message 2026-03-30 18:02:06 +03:00
Elian Doran
c40f5953fa feat(llm): make the prompt usage more compact 2026-03-30 17:56:07 +03:00
Elian Doran
241282296e fix(llm): report append to note not supporting all string content types 2026-03-30 17:50:28 +03:00
Elian Doran
8a8143167f feat(llm): report tool call errors 2026-03-30 17:45:58 +03:00
Elian Doran
12797293f0 feat(llm): improve model name display 2026-03-30 17:40:57 +03:00
Elian Doran
af0eb9551a feat(llm): save revision before changing content 2026-03-30 17:32:40 +03:00
Elian Doran
8a492450da feat(llm): render tools inline 2026-03-30 17:29:25 +03:00
Elian Doran
f3cb356b2b chore(llm): allow editing all string note types 2026-03-30 17:20:18 +03:00
Elian Doran
8ea1b7afba chore(llm): always mention note type 2026-03-30 17:16:49 +03:00
Elian Doran
911c1bdd0c feat(llm): use Markdown instead of HTML 2026-03-30 17:13:20 +03:00
Elian Doran
41f3274c7e feat(llm): use tool-based approach for reading current note 2026-03-30 17:08:47 +03:00
Elian Doran
0fc62dda78 chore(llm): styling of history menu 2026-03-30 16:38:11 +03:00
Elian Doran
e482c911c4 chore(desktop): add script to start prod with no dir 2026-03-30 12:45:30 +03:00
renovate[bot]
0e59126c52 fix(deps): update dependency mind-elixir to v5.10.0 2026-03-30 01:32:10 +00:00
Elian Doran
abbe6437a9 chore(llm): use NoItems for type widget as well 2026-03-29 23:58:30 +03:00
Elian Doran
f2d67d4128 fix(desktop): stream not working on Electron 2026-03-29 23:50:23 +03:00
Elian Doran
7c9e02996e fix(desktop): unable to list providers 2026-03-29 23:47:37 +03:00
Elian Doran
dc560edb7c fix(deps): update dependency preact-render-to-string to v6.6.7 (#9221) 2026-03-29 23:23:55 +03:00
renovate[bot]
f7bbcee386 fix(deps): update dependency preact-render-to-string to v6.6.7 2026-03-29 20:23:27 +00:00
Elian Doran
2182d4b440 fix(deps): update dependency react-i18next to v17.0.1 (#9222) 2026-03-29 23:21:15 +03:00
Elian Doran
c43e10c4af feat(llm): add tool to create note 2026-03-29 23:01:05 +03:00
Elian Doran
25037324ab feat(llm): improve handling when there is no provider set 2026-03-29 22:55:28 +03:00
Elian Doran
b8f9916d13 feat(llm): add tools to append or replace note content 2026-03-29 22:53:06 +03:00
Elian Doran
ed8b9cc943 feat(llm): integrate API keys with provider settings 2026-03-29 22:46:07 +03:00
Elian Doran
efbe7e0a21 feat(llm): add provider config in options 2026-03-29 22:42:05 +03:00
Elian Doran
46dd500d37 chore(llm): improve button for note access 2026-03-29 22:21:42 +03:00
Elian Doran
261c95fb06 feat(llm): add button to toggle access to the note 2026-03-29 22:20:26 +03:00
Elian Doran
41a122f722 feat(llm): allow the sidebar chat access to the note content 2026-03-29 22:09:29 +03:00
Elian Doran
490406e12a feat(llm): create empty settings page 2026-03-29 22:03:52 +03:00
Elian Doran
d12677094d chore(llm): improve chat bar size in sidebar 2026-03-29 21:54:50 +03:00
Elian Doran
3c69792744 feat(llm): improve layout with send button & context window 2026-03-29 21:52:35 +03:00
Elian Doran
395e79adbf fix(llm): sidebar chat box required scrolling to reach 2026-03-29 21:46:04 +03:00
Elian Doran
d5e56d8e29 feat(llm): integrate chat options into model selector 2026-03-29 21:43:27 +03:00
Elian Doran
e4c4873aa7 feat(llm): group legacy models into submenu 2026-03-29 21:35:33 +03:00
Elian Doran
293da1d4ef feat(llm): display cost next to the title 2026-03-29 21:29:59 +03:00
Elian Doran
d1c206a05a feat(llm): add same selectors in sidebar 2026-03-29 21:22:54 +03:00
Elian Doran
37b370511f chore(llm): get rid of different chat bar for sidebar 2026-03-29 21:14:09 +03:00
Elian Doran
734ef5533a refactor(llm): extract chat input bar into separate component 2026-03-29 21:11:51 +03:00
Elian Doran
0eb9b9fdac fix(llm): wrong icon size 2026-03-29 21:05:58 +03:00
Elian Doran
7817890cfe feat(llm): history button 2026-03-29 21:00:43 +03:00
Elian Doran
23dbedd139 refactor(llm): deduplicate LLM chat widgets 2026-03-29 20:28:19 +03:00
Elian Doran
2c8e2251fa feat(llm): use a better placeholder 2026-03-29 20:13:11 +03:00
Elian Doran
4c27ed9997 fix(sidebar): pressing a sidebar button would collapse the section 2026-03-29 20:11:16 +03:00
Elian Doran
d2fd1362c0 feat(llm): redesign sidebar to work on a single conversation 2026-03-29 20:09:00 +03:00
Elian Doran
45e57f0d5e chore(llm): always show AI chat sidebar 2026-03-29 20:00:22 +03:00
Elian Doran
660facea96 fix(llm): hide sidebar item if already in a chat 2026-03-29 19:52:44 +03:00
Elian Doran
9fa2e940d6 fix(llm): chat note created for every note navigated to 2026-03-29 19:49:13 +03:00
Elian Doran
0ffcfb8f43 feat(llm): identify sidebar chat notes by note ID 2026-03-29 19:45:45 +03:00
Elian Doran
ad1b3df74e fix(llm): sidebar not collapsing properly 2026-03-29 19:36:58 +03:00
Elian Doran
0ccf10bbbb feat(llm): basic sidebar implementation 2026-03-29 19:35:33 +03:00
Elian Doran
59c007e801 feat(llm): API to create LLM notes similar to search 2026-03-29 18:55:43 +03:00
Elian Doran
0654bc1049 fix(llm): wrong context window 2026-03-29 15:20:08 +03:00
Elian Doran
9fabefc847 feat(llm): minimize context window indicator 2026-03-29 15:17:27 +03:00
Elian Doran
e70ded0be1 fix(llm): content window progress bar not shown at startup 2026-03-29 15:12:18 +03:00
Elian Doran
16806275e0 feat(llm): basic context window progress bar 2026-03-29 15:10:49 +03:00
Elian Doran
e8214c3aae chore(llm): update list of models 2026-03-29 15:03:53 +03:00
Elian Doran
3a8e148301 chore(llm): correct pricing 2026-03-29 14:54:51 +03:00
Elian Doran
a0b546614f chore(llm): make multiplier relative to default 2026-03-29 14:47:41 +03:00
Elian Doran
5fcea86b94 feat(llm): basic cost multiplier 2026-03-29 14:44:40 +03:00
Elian Doran
d8c00ed6c0 chore(llm): use FormDropdownList 2026-03-29 14:39:53 +03:00
Elian Doran
863e68ec88 feat(llm): add model switcher 2026-03-29 14:34:31 +03:00
Elian Doran
046ee343dc feat(llm): display the model that was used 2026-03-29 14:06:23 +03:00
Elian Doran
2db9e376d5 refactor(llm): delegate pricings to provider 2026-03-29 14:02:33 +03:00
Elian Doran
9458128ad6 feat(llm): display estimated cost 2026-03-29 13:57:25 +03:00
Elian Doran
89638e3f56 feat(llm): display usage info (prompt + completion) 2026-03-29 13:53:13 +03:00
Elian Doran
8d492d7d4b feat(llm): show tool calls as references 2026-03-29 13:37:35 +03:00
Elian Doran
246c561b64 feat(llm): basic tool use 2026-03-29 13:30:04 +03:00
Elian Doran
88295f2462 refactor(llm): use vercel/AI instead 2026-03-29 13:07:21 +03:00
Elian Doran
d2d4e1cbac refactor(llm): use vercel/AI instead 2026-03-29 13:03:05 +03:00
Elian Doran
261e5b59e0 refactor(llm): use shared types in commons 2026-03-29 12:44:53 +03:00
Elian Doran
fa7ec01329 fix(llm): use of crypto.randomUUID 2026-03-29 12:27:18 +03:00
Elian Doran
4c4a29f9cf chore(llm): fix type issues 2026-03-29 12:24:13 +03:00
Elian Doran
9ddcaf4552 refactor(server): add triliumResponseHandled to typings 2026-03-29 12:01:06 +03:00
Elian Doran
c806a99fbc feat(llm): display thinking process 2026-03-29 11:51:39 +03:00
Elian Doran
ad91d360ce fix(llm): thinking budget mismatch 2026-03-29 11:41:28 +03:00
Elian Doran
cf8d7cd71f feat(llm): persist errors 2026-03-29 11:37:12 +03:00
Elian Doran
f370799b1d chore(llm): start working on extended thjinking 2026-03-29 11:26:10 +03:00
Elian Doran
f8655b5de4 fix(llm): errors not selectable 2026-03-29 11:25:54 +03:00
renovate[bot]
ed3a5778d0 fix(deps): update univer monorepo to v0.19.0 2026-03-29 00:54:35 +00:00
renovate[bot]
19d213059f fix(deps): update dependency react-i18next to v17.0.1 2026-03-29 00:53:30 +00:00
Elian Doran
276a802ab2 chore(deps): update dependency @ckeditor/ckeditor5-dev-build-tools to v55.3.0 (#9209) 2026-03-28 23:28:14 +02:00
Elian Doran
e756ded89f fix(deps): update dependency @zumer/snapdom to v2.7.0 (#9213) 2026-03-28 23:27:22 +02:00
Elian Doran
b551f0fe2d feat(llm): basic Markdown rendering 2026-03-28 21:19:59 +02:00
Elian Doran
f6e8bdb0fd fix(llm): text not selectable 2026-03-28 21:07:54 +02:00
Elian Doran
9029ea8085 fix(llm): last response not saved 2026-03-28 21:06:20 +02:00
Elian Doran
d61ade9fe9 feat(llm): add basic web search support 2026-03-28 21:00:53 +02:00
Elian Doran
aa1fe549c7 feat(llm): make source viewable 2026-03-28 20:52:40 +02:00
Elian Doran
e3701bbcb4 fix(llm): streaming not working due to compression 2026-03-28 20:45:35 +02:00
Elian Doran
fb7fc4bf0c feat(llm): basic chat interface 2026-03-28 20:39:09 +02:00
Lorinc936
f8c59a1730 Merge branch 'main' into main 2026-03-28 17:26:36 +00:00
Elian Doran
ca0c64094c fix(build-docs): backend script generation fails 2026-03-28 14:45:11 +02:00
Elian Doran
5158df21c7 fix(build-docs): wailing due to introduction of core 2026-03-28 14:38:12 +02:00
Elian Doran
39b2e8ec05 Feature/standalone scripting (#9219) 2026-03-28 13:57:35 +02:00
Elian Doran
9d6c9ac04e chore(core): address requested changes 2026-03-28 13:42:25 +02:00
Elian Doran
8e50c9baf3 chore(core): remove unnecessary newlines 2026-03-28 13:32:40 +02:00
Elian Doran
936165fba8 fix(standalone): hidden subtree is slow due to lack of transaction 2026-03-28 13:21:43 +02:00
Elian Doran
377e874ef2 chore(core): integrate scheduler 2026-03-28 13:17:20 +02:00
Elian Doran
4d98558019 chore(core): set up sucrase 2026-03-28 13:07:30 +02:00
Elian Doran
ef70fd2d2a chore(server): fix references to script service 2026-03-28 13:00:07 +02:00
Elian Doran
3bd6777070 chore(core): integrate scripting routes 2026-03-28 12:57:53 +02:00
Elian Doran
b02e9ba52b chore(core): integrate scripting services 2026-03-28 12:54:44 +02:00
Elian Doran
3a053d3104 refactor(client): fix types related to script execution 2026-03-28 12:39:16 +02:00
Elian Doran
4f6de0c68d Merge remote-tracking branch 'origin/main' into standalone 2026-03-28 12:30:58 +02:00
Elian Doran
d084c426fd Feature/standalone export (#9205) 2026-03-28 12:26:49 +02:00
Elian Doran
b4802e9abf chore: address requested changes 2026-03-28 12:17:18 +02:00
Elian Doran
7f6a43c2fa chore(server): fix error in Electron port handling 2026-03-28 12:14:59 +02:00
Elian Doran
0b784af4ca chore(core): reintroduce basic non-blocking import for ENEX 2026-03-28 12:03:00 +02:00
Elian Doran
fa6e70a13a feat(standalone): get enex import to work 2026-03-28 12:01:01 +02:00
Elian Doran
9b6c7966de fix(server): ws not working 2026-03-28 11:37:51 +02:00
Elian Doran
f04f295b21 feat(core): use real console width in console banner 2026-03-28 11:25:03 +02:00
Elian Doran
8ada23c9be feat(server): improve error logging using banner 2026-03-28 11:23:44 +02:00
Elian Doran
82bac7b18f fix(standalone): OPML export failing 2026-03-28 11:20:11 +02:00
Elian Doran
362429451d feat(standalone): hide share export 2026-03-28 11:19:14 +02:00
Elian Doran
dc50ca157d chore(deps): update dependency electron to v41.1.0 (#9211) 2026-03-28 11:11:11 +02:00
Elian Doran
ff2e775b5e chore(deps): update node.js to v24.14.1 (#9184) 2026-03-28 11:10:44 +02:00
Elian Doran
6dea4aec89 chore(server): address requested changes 2026-03-28 11:09:23 +02:00
renovate[bot]
584d48c5ab chore(deps): update dependency vite-plugin-static-copy to v4 2026-03-28 09:06:29 +00:00
Elian Doran
25df43b0be chore(deps): update dependency vite to v8.0.3 (#9194) 2026-03-28 11:02:24 +02:00
Elian Doran
1af1fcd148 chore(deps): update dependency @redocly/cli to v2.25.2 (#9206) 2026-03-28 10:54:11 +02:00
Elian Doran
516f9aad45 fix(deps): update dependency @preact/signals to v2.9.0 (#9212) 2026-03-28 10:53:55 +02:00
Elian Doran
79a420de0f chore(deps): update dependency express-openid-connect to v2.20.1 (#9207) 2026-03-28 10:50:27 +02:00
Elian Doran
ac213b6664 fix(deps): update dependency katex to v0.16.44 (#9208) 2026-03-28 10:50:01 +02:00
Elian Doran
ff2d74029a chore(deps): update dependency axios to v1.14.0 (#9210) 2026-03-28 10:49:46 +02:00
Elian Doran
31ac1d3f2d fix(deps): update dependency react-i18next to v17 (#9214) 2026-03-28 10:49:21 +02:00
renovate[bot]
2c32382ca6 fix(deps): update dependency react-i18next to v17 2026-03-28 01:18:11 +00:00
renovate[bot]
0d94c20deb fix(deps): update dependency @zumer/snapdom to v2.7.0 2026-03-28 01:17:16 +00:00
renovate[bot]
9904df1611 fix(deps): update dependency @preact/signals to v2.9.0 2026-03-28 01:16:17 +00:00
renovate[bot]
2d945d4fb2 chore(deps): update dependency electron to v41.1.0 2026-03-28 01:15:19 +00:00
renovate[bot]
c1f9a22bf3 chore(deps): update dependency axios to v1.14.0 2026-03-28 01:14:20 +00:00
renovate[bot]
22e2e2339e chore(deps): update dependency @ckeditor/ckeditor5-dev-build-tools to v55.3.0 2026-03-28 01:13:17 +00:00
renovate[bot]
b6435bbfc9 fix(deps): update dependency katex to v0.16.44 2026-03-28 01:12:21 +00:00
renovate[bot]
63387cb958 chore(deps): update dependency express-openid-connect to v2.20.1 2026-03-28 01:11:16 +00:00
renovate[bot]
a8d104ec57 chore(deps): update dependency @redocly/cli to v2.25.2 2026-03-28 01:10:12 +00:00
Elian Doran
d0abcfe355 chore(export): bring back content CSS 2026-03-28 00:29:13 +02:00
Elian Doran
8b1d0063ff fix(standalone): unable to download ZIPs 2026-03-28 00:26:11 +02:00
Elian Doran
8cd7e48e85 fix(server): unable to export as share 2026-03-27 23:54:20 +02:00
Elian Doran
aee005b624 refactor(core): move zip provider out of import 2026-03-27 23:23:26 +02:00
Elian Doran
1d050e8784 fix(core): use of Node.js path 2026-03-27 23:18:39 +02:00
Elian Doran
0c37b2ce5c fix(export/single): crash due to use of Buffer 2026-03-27 23:13:40 +02:00
Elian Doran
73f401f106 fix(standalone/export): redirects to URL without downloading 2026-03-27 23:13:30 +02:00
Elian Doran
d2a0c540ba fix(core): get rid of Node dependencies from ZIP export 2026-03-27 23:10:39 +02:00
Elian Doran
4458d5b8f7 chore(core): fix more errors related to export 2026-03-27 22:27:18 +02:00
Elian Doran
a59d6dfb11 chore(core): fix most errors with export 2026-03-27 22:20:59 +02:00
Elian Doran
21e2cf10c2 chore(core): relocate export route 2026-03-27 21:57:43 +02:00
Elian Doran
c94ca00daa chore(core): relocate export service 2026-03-27 21:54:51 +02:00
Elian Doran
0ec2160eff Standalone import (#9204) 2026-03-27 21:52:02 +02:00
Elian Doran
6c75df70e0 chore: solve type errors 2026-03-27 21:40:19 +02:00
Elian Doran
0211535f73 fix(edit-docs): missing zip primitives 2026-03-27 19:26:27 +02:00
Elian Doran
2d4027c214 fix(server): depending on unexported zip import service 2026-03-27 19:18:31 +02:00
Elian Doran
5b3fb315d7 fix(core): on new database, opening hidden notes instead of the root 2026-03-27 19:16:23 +02:00
renovate[bot]
10377b527f chore(deps): update dependency vite to v8.0.3 2026-03-27 17:05:56 +00:00
Elian Doran
24650edd62 fix(setup): demo DB not respected 2026-03-27 19:03:39 +02:00
Elian Doran
d29d1428ed feat(standalone/import): import demo DB 2026-03-27 18:55:18 +02:00
Elian Doran
91d526b15f feat(standalone/import): improve importing speed 2026-03-27 18:27:19 +02:00
Elian Doran
22c86cf3b5 feat(standalone): basic ZIP support 2026-03-27 18:11:59 +02:00
Elian Doran
a0573c439b fix(core): extension lookup failing in standalone 2026-03-27 17:54:37 +02:00
JYC333
4413566e14 chore(deps): update dependency happy-dom to v20.8.9 (#9192) 2026-03-27 15:46:18 +00:00
Elian Doran
050cdd0a85 chore(core): add a few missing constants 2026-03-27 17:00:13 +02:00
Elian Doran
55f09fe21a chore(core): fix usage of Buffer 2026-03-27 16:45:44 +02:00
Elian Doran
f069b41df6 chore(standalone): upload middleware with error handling 2026-03-27 16:40:23 +02:00
Elian Doran
f81369d643 feat(core): support md5 hash 2026-03-27 14:33:52 +02:00
Elian Doran
f1d7d34f1a chore(core): align tsconfig 2026-03-27 14:28:48 +02:00
Elian Doran
ce1f7a4274 chore(scripts): deduplicate errors listing 2026-03-27 14:28:40 +02:00
Elian Doran
6ce1d31ceb chore(import): integrate import route into core 2026-03-27 11:45:02 +02:00
Elian Doran
ecb467f2b7 chore(import): fix a few type errors 2026-03-27 11:40:48 +02:00
Elian Doran
4ffaadd481 chore(import): move all services to core (with errors) 2026-03-27 11:40:06 +02:00
Elian Doran
4c933669b9 Standalone extra improvements (#9191) 2026-03-27 09:15:03 +02:00
Elian Doran
a7001beced chore(standalone): addres requested changes 2026-03-27 09:04:21 +02:00
Elian Doran
b864c338dd chore(standalone): align deps with client 2026-03-27 08:57:51 +02:00
renovate[bot]
6c295611cc chore(deps): update node.js to v24.14.1 2026-03-27 06:55:05 +00:00
renovate[bot]
c1c98a6955 chore(deps): update dependency happy-dom to v20.8.9 2026-03-27 06:53:56 +00:00
Elian Doran
61d37c4c19 Merge remote-tracking branch 'origin/main' into feature/standalone_extra_improvements 2026-03-27 08:52:25 +02:00
Elian Doran
6e222bb901 chore(deps): update dependency user-agent-data-types to v0.4.3 (#9193) 2026-03-27 08:49:31 +02:00
Elian Doran
82b8601e0b chore(deps): update vitest monorepo to v4.1.2 (#9195) 2026-03-27 08:49:02 +02:00
Elian Doran
47e515bc77 fix(deps): update dependency i18next to v25.10.10 (#9196) 2026-03-27 08:48:25 +02:00
Elian Doran
eef35c3a5f fix(deps): update dependency panzoom to v9.4.4 (#9198) 2026-03-27 08:43:36 +02:00
Elian Doran
a18d0484c5 chore(deps): update dependency express-openid-connect to v2.20.0 (#9199) 2026-03-27 08:42:31 +02:00
Elian Doran
4eaa3d7ac1 chore(deps): update dependency stylelint to v17.6.0 (#9200) 2026-03-27 08:42:15 +02:00
Elian Doran
ad24cf9ab9 fix(deps): update dependency katex to v0.16.43 (#9197) 2026-03-27 08:41:39 +02:00
renovate[bot]
5467d7719d chore(deps): update dependency stylelint to v17.6.0 2026-03-27 01:56:44 +00:00
renovate[bot]
875b3a3f9a chore(deps): update dependency express-openid-connect to v2.20.0 2026-03-27 01:56:02 +00:00
renovate[bot]
4ab6a66c75 fix(deps): update dependency panzoom to v9.4.4 2026-03-27 01:55:20 +00:00
renovate[bot]
53e157567d fix(deps): update dependency katex to v0.16.43 2026-03-27 01:54:38 +00:00
renovate[bot]
5725680d3a fix(deps): update dependency i18next to v25.10.10 2026-03-27 01:53:56 +00:00
renovate[bot]
07fe884fd8 chore(deps): update vitest monorepo to v4.1.2 2026-03-27 01:53:12 +00:00
renovate[bot]
8d57a593d8 chore(deps): update dependency user-agent-data-types to v0.4.3 2026-03-27 01:51:38 +00:00
Elian Doran
296579fa87 test(server): initialize core 2026-03-27 00:06:22 +02:00
Elian Doran
995f39dfdf Revert "chore(core): set up basic vitest"
This reverts commit c7cf8d5255.
2026-03-27 00:02:43 +02:00
Elian Doran
c7cf8d5255 chore(core): set up basic vitest 2026-03-26 23:38:13 +02:00
Elian Doran
e1079f954e chore(core): fix one more type error 2026-03-26 23:26:14 +02:00
Elian Doran
d2524adcd2 fix(server): wrong use of isElectron 2026-03-26 23:23:22 +02:00
Elian Doran
e778942711 fix(server): custom route depending on helper function 2026-03-26 23:22:54 +02:00
Elian Doran
04136cd9c0 chore(desktop): strange cannot write file because it would overwrite input file 2026-03-26 23:22:16 +02:00
Elian Doran
247108f347 fix(core): desktop crashing due to missing platform check 2026-03-26 23:09:42 +02:00
Lorinc936
c833c3591f docs: documentation for downloading backups 2026-03-26 22:09:01 +01:00
Lorinc936
ccbd962e0b Backend for backup download button 2026-03-26 21:57:53 +01:00
Lorinc936
966d2afe69 Feat: backup download frontend and locales 2026-03-26 21:36:54 +01:00
Elian Doran
1a8075e2f1 fix(server): server-side translations missing 2026-03-26 22:07:14 +02:00
Elian Doran
b47ede7772 Merge remote-tracking branch 'origin/main' into feature/standalone_extra_improvements 2026-03-26 22:00:05 +02:00
Elian Doran
ebbb8b396c fix(standalone): unable to switch themes 2026-03-26 21:58:28 +02:00
Elian Doran
a2cace6c0f feat(standalone): add support for environment variables 2026-03-26 21:52:58 +02:00
Elian Doran
c0593707f2 refactor(core): use own path replacement 2026-03-26 21:41:11 +02:00
Elian Doran
8b98fdcba1 feat(standalone): support app CSS 2026-03-26 21:37:35 +02:00
Elian Doran
a05c5821b3 chore: fix the rest of the type errors 2026-03-26 21:19:12 +02:00
Elian Doran
140fbc1524 chore: fix various type errors 2026-03-26 21:15:37 +02:00
Elian Doran
6bb093e6d3 chore(client): fix a few type errors 2026-03-26 21:09:04 +02:00
Elian Doran
609ec19e06 chore(edit-docs): fix missing references to core 2026-03-26 21:03:21 +02:00
Elian Doran
acb3030d56 chore(core): fix most bootstrap-related type errors 2026-03-26 20:57:04 +02:00
Elian Doran
0fc5b2e997 chore(core): fix various type errors 2026-03-26 20:35:45 +02:00
Elian Doran
41a7d6738b chore(core): introduce becca_easy_mocking and becca_mocking 2026-03-26 20:24:44 +02:00
Elian Doran
11461221ba chore: solve a few more type errors 2026-03-26 20:15:20 +02:00
Elian Doran
ce25bd10ff chore(core): fix meta types 2026-03-26 20:12:24 +02:00
Elian Doran
9c5bac5741 refactor(core): integrate more utils into core 2026-03-26 19:58:29 +02:00
Elian Doran
9a42536205 chore(core): fix various type errors 2026-03-26 19:58:11 +02:00
Elian Doran
74e0ab071c chore(desktop): forge type config interfering 2026-03-26 19:39:51 +02:00
Elian Doran
0b136f3aae chore(client): typecheck issues due to change in bootstrap definition 2026-03-26 19:38:27 +02:00
Elian Doran
01dae831a4 chore(scripts): improve typecheck with numbers & total count 2026-03-26 19:35:49 +02:00
Elian Doran
e2062558b7 chore(core): typecheck issues due to TypeScript module setting 2026-03-26 19:32:36 +02:00
Elian Doran
259405d707 chore(core): fix typechecks regarding SQL 2026-03-26 19:27:52 +02:00
Elian Doran
ef7502be34 chore(scripts): filter typecheck to avoid cascading errors 2026-03-26 19:27:25 +02:00
Elian Doran
13e26c5b3f chore(core): remove redundant log 2026-03-26 19:16:23 +02:00
Elian Doran
5fec715e3f chore(core): integrate the rest of the note map route 2026-03-26 19:16:12 +02:00
Elian Doran
97443c0682 chore(llm): mention main project distinction 2026-03-26 19:07:00 +02:00
Elian Doran
53c0b920e2 chore(llm): re-init CLAUDE.md 2026-03-26 19:05:36 +02:00
Elian Doran
79b2bc8b93 Standalone setup (#9180) 2026-03-26 19:00:09 +02:00
Elian Doran
360d9d5202 fix(desktop/setup): window with no traffic lights or draggable on macOS 2026-03-26 18:52:06 +02:00
Elian Doran
bf7af98739 fix(client): runtime error due to missing entry 2026-03-26 18:51:49 +02:00
Elian Doran
b574237dfb feat(setup): add a nice banner when DB not initialized 2026-03-26 18:24:36 +02:00
Elian Doran
afe597c811 feat(core): unified crash system using platform provider 2026-03-26 18:17:24 +02:00
Elian Doran
fb9f33b9ff chore(deps): update dependency @codemirror/language to v6.12.3 (#9182) 2026-03-26 17:27:53 +02:00
Elian Doran
2c690d4dd2 chore(deps): update dependency electron to v41.0.4 (#9183) 2026-03-26 17:27:18 +02:00
Elian Doran
48219f54fc chore(server): remove old translations 2026-03-26 15:25:30 +02:00
Elian Doran
d171409301 chore(setup): remove old files 2026-03-26 15:18:15 +02:00
Elian Doran
e508a4cd43 feat(setup): functional sync from desktop with automatic status update 2026-03-26 14:41:26 +02:00
Elian Doran
a5da35b7ae fix(setup): redirects to /setup on browser 2026-03-26 11:45:52 +02:00
Elian Doran
2016c97a12 chore(scripts): add a way to wipe node modules 2026-03-26 11:27:46 +02:00
Elian Doran
9595f52a9c chore(core): address requested changes 2026-03-26 10:39:29 +02:00
renovate[bot]
7db7dc287f chore(deps): update dependency electron to v41.0.4 2026-03-26 01:15:29 +00:00
renovate[bot]
dece273c2b chore(deps): update dependency @codemirror/language to v6.12.3 2026-03-26 01:14:45 +00:00
Elian Doran
9ee17445a5 fix(desktop/setup): not finishing setup properly 2026-03-26 00:23:41 +02:00
Elian Doran
cd97e2c861 feat(desktop/setup): add background effects 2026-03-26 00:13:03 +02:00
Elian Doran
db6f034cb5 feat(setup): display network addresses on browser as well 2026-03-25 23:59:52 +02:00
Elian Doran
46b478ec17 feat(desktop/setup): improve waiting display 2026-03-25 23:55:38 +02:00
Elian Doran
de57a39df6 feat(desktop/setup): improve addresses display 2026-03-25 23:45:47 +02:00
Elian Doran
8eb45e2814 feat(desktop/setup): display port in desktop-to-desktop sync 2026-03-25 23:40:40 +02:00
Elian Doran
5bb0887d8b fix(desktop/setup): misleading IP in desktop-to-desktop sync 2026-03-25 23:25:43 +02:00
Elian Doran
b5f7f89c27 feat(desktop/setup): improve sync illustration 2026-03-25 23:15:11 +02:00
Elian Doran
fa7d1d3f80 feat(desktop): improve integration of setup 2026-03-25 23:09:26 +02:00
Elian Doran
2eef2f801f chore(core): don't log language option not found if DB not initialized 2026-03-25 22:55:59 +02:00
Elian Doran
6ebf9f59a0 fix(server): translations not working 2026-03-25 22:53:12 +02:00
Elian Doran
eddb47c9c4 chore(core): bring back SQL initialization with message 2026-03-25 22:48:15 +02:00
Elian Doran
8d38b818c0 feat(core): reintroduce DB migration 2026-03-25 22:16:07 +02:00
Elian Doran
af462ab0f9 chore(standalone/setup): basic mobile support 2026-03-25 22:02:18 +02:00
Elian Doran
07753a6253 refactor(standalone/setup): get rid of warnings 2026-03-25 21:32:55 +02:00
Elian Doran
54b12cf560 chore(standalone/setup): add autocomplete attributes to sync setup 2026-03-25 21:32:05 +02:00
Elian Doran
f97f5da837 fix(standalone/setup): sync from desktop button no longer working 2026-03-25 21:29:41 +02:00
Elian Doran
19e315dc1a fix(server): crash due to session cleanup with unitialized DB 2026-03-25 21:27:35 +02:00
Elian Doran
96d01d6379 i18n(client): minor change 2026-03-25 21:26:10 +02:00
Elian Doran
ee156f1183 fix(server): random error due to font loading while not initialized 2026-03-25 21:26:03 +02:00
Elian Doran
f83e184fcd fix(standalone/setup): current language not restored when going back 2026-03-25 21:14:23 +02:00
Elian Doran
a2ead45c83 style(standalone/setup): make language selection slightly narrower 2026-03-25 21:13:49 +02:00
Elian Doran
b295f1e957 chore(standalone/setup): increase size of setup dialog 2026-03-25 21:11:04 +02:00
Elian Doran
cbd4fd3820 i18n(client): translate setup into Romanian 2026-03-25 21:10:18 +02:00
Elian Doran
b27fa2a555 chore(standalone/setup): set up navigation 2026-03-25 21:10:10 +02:00
Elian Doran
2afd9b474c fix(server): trying to connect to web socket while in setup 2026-03-25 20:56:14 +02:00
Elian Doran
680ac80526 feat(standalone/setup): start working on language selection page 2026-03-25 20:56:01 +02:00
Elian Doran
4b08a33307 feat(standalone/setup): add icon on first page 2026-03-25 20:30:04 +02:00
Elian Doran
04db52145d feat(standalone/setup): use segmented cards for sync setup 2026-03-25 20:23:57 +02:00
Elian Doran
ae996e8847 feat(standalone/setup): dedicated back button 2026-03-25 20:07:37 +02:00
Elian Doran
06cb568fbd feat(standalone/setup): improve creating new document screen 2026-03-25 19:54:55 +02:00
Elian Doran
39a1aa360d feat(standalone/setup): pass information regarding demo 2026-03-25 19:42:42 +02:00
Elian Doran
51ed4dece2 feat(standalone/setup): page to select whether to import demo or not 2026-03-25 19:37:27 +02:00
Elian Doran
1620b0be62 chore(core): fix type issue with async import 2026-03-25 18:57:52 +02:00
Elian Doran
4c7c8a19c5 fix(standalone/setup): progress bar jumps back to zero before finishing sync 2026-03-25 18:51:19 +02:00
Elian Doran
93f825e970 chore(standalone): reduce verbosity of request errors 2026-03-25 18:49:01 +02:00
Elian Doran
310035be1b feat(standalone/setup): dedicated handling for wrong password 2026-03-25 18:48:38 +02:00
Elian Doran
4ec90e5575 feat(standalone/setup): dismissable error 2026-03-25 18:38:44 +02:00
Elian Doran
5ba5aee160 feat(standalone/setup): improve display of sync error 2026-03-25 18:25:47 +02:00
Elian Doran
aecca66972 style(standalone/setup): fix some spacing issues 2026-03-25 18:18:37 +02:00
Elian Doran
a872664789 feat(standalone/setup): use normal form groups for sync settings 2026-03-25 18:09:21 +02:00
JYC333
bf7449bc90 Translations update from Hosted Weblate (#9165) 2026-03-25 15:24:42 +00:00
noobhjy
6f3c9e2883 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1719 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-03-25 16:04:33 +01:00
TS
49248a636a Translated using Weblate (Polish)
Currently translated at 100.0% (387 of 387 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/pl/
2026-03-25 16:04:32 +01:00
Wojciech O
f51b0eb4de Translated using Weblate (Polish)
Currently translated at 100.0% (387 of 387 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/pl/
2026-03-25 16:04:31 +01:00
Luk On
f0d06815ec Translated using Weblate (Polish)
Currently translated at 100.0% (387 of 387 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/pl/
2026-03-25 16:04:30 +01:00
TS
070701ee9e Translated using Weblate (Polish)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/pl/
2026-03-25 16:04:30 +01:00
TS
57fefaae1d Translated using Weblate (Polish)
Currently translated at 100.0% (116 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/pl/
2026-03-25 16:04:29 +01:00
TS
1d109f592b Translated using Weblate (Polish)
Currently translated at 100.0% (1719 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pl/
2026-03-25 16:04:28 +01:00
Mik Piet
29b01c3fe6 Translated using Weblate (Polish)
Currently translated at 100.0% (1719 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pl/
2026-03-25 16:04:27 +01:00
Giovi
6cd263a897 Translated using Weblate (Italian)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/it/
2026-03-25 16:04:27 +01:00
Giovi
c9ca1de271 Translated using Weblate (Italian)
Currently translated at 100.0% (1719 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-03-25 16:04:26 +01:00
Francis C.
c369ba416c Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1719 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-03-25 16:04:25 +01:00
Lluís Forns
4b3d923d29 Translated using Weblate (Catalan)
Currently translated at 6.5% (112 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ca/
2026-03-25 16:04:24 +01:00
JYC333
64c3d0b36d chore(deps): update dependency happy-dom to v20.8.8 (#9166) 2026-03-25 15:04:13 +00:00
Elian Doran
7b639f2718 refactor(standalone/setup): component for pages 2026-03-25 10:23:04 +02:00
Elian Doran
7dcc1496ec feat(standalone/setup): disable "Connect a desktop app" in standalone 2026-03-25 10:07:41 +02:00
Elian Doran
0dc7d71d1b style(standalone/setup): full-width footer 2026-03-25 10:00:47 +02:00
Elian Doran
0fdc3590dc fix(deps): update dependency i18next to v25.10.9 (#9168) 2026-03-25 09:52:26 +02:00
Elian Doran
26fd6a573d chore(deps): update node.js to v24.14.1 (#9167) 2026-03-25 09:52:13 +02:00
renovate[bot]
59d8961111 fix(deps): update dependency i18next to v25.10.9 2026-03-25 06:27:01 +00:00
Elian Doran
9b733849a9 fix(deps): update dependency katex to v0.16.42 (#9169) 2026-03-25 08:24:47 +02:00
Elian Doran
133b847b15 fix(deps): update dependency react-i18next to v16.6.6 (#9170) 2026-03-25 08:24:14 +02:00
Elian Doran
ecdbed6bac chore(deps): update dependency @redocly/cli to v2.25.1 (#9171) 2026-03-25 08:23:49 +02:00
Elian Doran
d1deccc23c Merge branch 'main' into renovate/redocly-cli-2.x 2026-03-25 08:23:39 +02:00
Elian Doran
c71d8a87b9 chore(deps): update dependency image-type to v6.1.0 (#9172) 2026-03-25 08:23:19 +02:00
Elian Doran
0614d92597 chore(deps): update pnpm to v10.33.0 (#9173) 2026-03-25 08:22:55 +02:00
renovate[bot]
9ab7e8e2b7 chore(deps): update pnpm to v10.33.0 2026-03-25 01:37:38 +00:00
renovate[bot]
0a5543cc72 chore(deps): update dependency image-type to v6.1.0 2026-03-25 01:37:27 +00:00
renovate[bot]
6d000d7b7c chore(deps): update dependency @redocly/cli to v2.25.1 2026-03-25 01:36:35 +00:00
renovate[bot]
ac4ca16e85 fix(deps): update dependency react-i18next to v16.6.6 2026-03-25 01:35:37 +00:00
renovate[bot]
e248d93e29 fix(deps): update dependency katex to v0.16.42 2026-03-25 01:34:41 +00:00
renovate[bot]
acd786da67 chore(deps): update node.js to v24.14.1 2026-03-25 01:32:38 +00:00
renovate[bot]
ef19d6260c chore(deps): update dependency happy-dom to v20.8.8 2026-03-25 01:32:31 +00:00
JYC333
638e1ebd1d chore(deps): update dependency webdriverio to v9.27.0 (#9160) 2026-03-24 21:26:56 +00:00
Elian Doran
dd67710b12 feat(standalone/setup): improve layout & design of sync in progress 2026-03-24 19:36:40 +02:00
Elian Doran
6d376731e3 fix(server): unable to do first setup 2026-03-24 19:13:49 +02:00
Elian Doran
5157fd9ecd feat(standalone/setup): decode slashes in error message 2026-03-24 18:42:53 +02:00
Elian Doran
4226827b5d chore(standalone/setup): improve error reporting 2026-03-24 18:38:06 +02:00
renovate[bot]
0c5efc3dcb chore(deps): update dependency webdriverio to v9.27.0 2026-03-24 16:25:45 +00:00
Elian Doran
cb3b362bad feat(standalone/setup): report errors in initial sync request 2026-03-24 18:24:29 +02:00
JYC333
a774218429 fix(deps): update dependency @zumer/snapdom to v2.6.0 (#9161) 2026-03-24 16:20:58 +00:00
renovate[bot]
e305be9e75 fix(deps): update dependency @zumer/snapdom to v2.6.0 2026-03-24 16:03:21 +00:00
JYC333
f267dd5fc1 fix(deps): update dependency diff to v8.0.4 (#9159) 2026-03-24 15:57:59 +00:00
JYC333
6ba736b83f chore(deps): update dependency vite to v8.0.2 (#9156) 2026-03-24 15:57:40 +00:00
Elian Doran
4dcb08745b fix(standalone/setup): clicking on advanced options submits form 2026-03-24 16:49:23 +02:00
Elian Doran
28c57813db chore(standalone/setup): make fields required 2026-03-24 16:47:03 +02:00
Elian Doran
49868362cd chore(standalone/setup): add back proxy setting for server sync 2026-03-24 16:41:20 +02:00
Elian Doran
c2b965c24b fix(standalone/setup): lost connection to websocket 2026-03-24 16:31:44 +02:00
Elian Doran
6c3e16db20 chore(standalone/setup): basic spinner for desktop sync 2026-03-24 16:27:58 +02:00
Elian Doran
b880d81104 refactor(core): deduplicate some bootstrap items 2026-03-24 16:27:31 +02:00
Elian Doran
ef8db52ebe refactor(core): use different mechanism for shared bootstrap items 2026-03-24 16:24:59 +02:00
renovate[bot]
5eb8715295 fix(deps): update dependency diff to v8.0.4 2026-03-24 12:32:24 +00:00
renovate[bot]
7654be5132 chore(deps): update dependency vite to v8.0.2 2026-03-24 12:31:24 +00:00
JYC333
3f4358a422 chore(deps): update typescript-eslint monorepo to v8.57.2 (#9157) 2026-03-24 12:23:36 +00:00
JYC333
b3ca412bbd chore(deps): update dependency happy-dom to v20.8.7 (#9154) 2026-03-24 12:23:03 +00:00
Elian Doran
185a88e655 fix(desktop): not starting due to lack of core initialization 2026-03-24 14:18:46 +02:00
renovate[bot]
d1f60840a2 chore(deps): update typescript-eslint monorepo to v8.57.2 2026-03-24 12:04:49 +00:00
renovate[bot]
a337ace856 chore(deps): update dependency happy-dom to v20.8.7 2026-03-24 12:00:19 +00:00
JYC333
0b6f6dee7f chore(deps): update vitest monorepo to v4.1.1 (#9158) 2026-03-24 11:58:29 +00:00
JYC333
93f1743432 chore(deps): update dependency typedoc to v0.28.18 (#9155) 2026-03-24 11:55:50 +00:00
Elian Doran
3eef1a1c59 chore(standalone/setup): improve layout of title 2026-03-24 13:55:12 +02:00
Elian Doran
78451b9721 feat(standalone/setup): add steps for desktop syncing 2026-03-24 13:25:04 +02:00
Elian Doran
26973681ec chore(standalone/setup): clarify syncing 2026-03-24 13:14:24 +02:00
Elian Doran
f48b67f872 feat(standalone/setup): add a sync illustration 2026-03-24 12:56:40 +02:00
Elian Doran
8d5ccb5ba8 chore(standalone/setup): add a nice background 2026-03-24 12:40:53 +02:00
Elian Doran
619751a8aa chore(standalone/setup): create empty page for sync from desktop 2026-03-24 12:32:33 +02:00
Elian Doran
be9c55acae feat(standalone/setup): add transition between pages 2026-03-24 12:17:55 +02:00
Elian Doran
ffd37755a3 chore(standalone/setup): fix typo in translation 2026-03-24 12:12:52 +02:00
Elian Doran
9991b8f1e2 feat(standalone/setup): intermediate screen for creating new document 2026-03-24 12:05:37 +02:00
Elian Doran
13eb8152e0 feat(standalone/setup): add syncing steps 2026-03-24 11:57:58 +02:00
Elian Doran
7bf6db7817 feat(standalone/setup): add a progress bar for sync status 2026-03-24 11:33:15 +02:00
Elian Doran
a1eb79fcb0 feat(standalone/setup): increase option creation speed 2026-03-24 10:30:45 +02:00
Elian Doran
3f5cdc533e feat(standalone/setup): sync from server without refresh 2026-03-24 10:18:23 +02:00
Elian Doran
697ea995cb fix(server): not detecting DB init state properly 2026-03-24 10:10:28 +02:00
Elian Doran
a2002b8e9c fix(server): not starting due to schema loading 2026-03-24 09:32:29 +02:00
Elian Doran
c1d8637fec chore(standalone/setup): bring back spinner 2026-03-24 09:09:21 +02:00
Elian Doran
b6ea29ffc9 chore(standalone/setup): basic sync page 2026-03-24 09:06:25 +02:00
Elian Doran
6aa0c573fb chore(standalone/setup): improve alignment of home screen 2026-03-24 08:59:08 +02:00
renovate[bot]
3fb4ab1a31 chore(deps): update vitest monorepo to v4.1.1 2026-03-24 00:42:19 +00:00
renovate[bot]
8970d02404 chore(deps): update dependency typedoc to v0.28.18 2026-03-24 00:40:07 +00:00
Elian Doran
fcc575c508 feat(standalone/setup): reload after creating new document 2026-03-23 23:05:57 +02:00
Elian Doran
62d6ce08a0 fix(standalone): database initialization slow 2026-03-23 21:35:26 +02:00
Elian Doran
b50127b0d3 fix(client): froca initialization incorrect due to DB init check 2026-03-23 21:29:38 +02:00
Elian Doran
669a58cc0e fix(standalone): database not initialized after first setup 2026-03-23 21:08:56 +02:00
Elian Doran
bf4b5dad5a feat(standalone/setup): set up new document 2026-03-23 21:06:30 +02:00
Elian Doran
39972a9bd7 feat(standalone/setup): basic server sync form 2026-03-23 20:27:44 +02:00
Elian Doran
44f519c1d6 feat(standalone/setup): basic footer 2026-03-23 20:21:47 +02:00
Elian Doran
dd6c5bbf12 chore(standalone/setup): more concise descriptions 2026-03-23 20:10:56 +02:00
Elian Doran
20d4db2608 style(standalone/setup): add a shadow 2026-03-23 20:07:32 +02:00
Elian Doran
3151e86665 feat(standalone/setup): add icons 2026-03-23 20:02:20 +02:00
Elian Doran
96a0d483f5 feat(standalone/setup): add hover effect 2026-03-23 19:50:53 +02:00
Elian Doran
3faefdbc85 feat(standalone/setup): basic styling of cards 2026-03-23 19:47:44 +02:00
Elian Doran
12347d5c4a chore(standalone/setup): basic layout 2026-03-23 19:30:00 +02:00
Elian Doran
4dbaadf9cc chore(standalone/setup): replace properly for hot reload 2026-03-23 19:26:26 +02:00
Elian Doran
2a1c165a54 fix(standalone/setup): translations not initializing due to missing asset path 2026-03-23 19:25:01 +02:00
Elian Doran
939f931809 chore(standalone/setup): setup translation partially 2026-03-23 19:20:30 +02:00
Elian Doran
4fd09bf1f8 chore(standalone/setup): prevent error in froca due to not initialized DB 2026-03-23 19:20:24 +02:00
Elian Doran
3231db3c3f fix(standalone/setup): server API missing when DB not initialized 2026-03-23 19:19:56 +02:00
Elian Doran
c07ea1bfa7 feat(standalone/setup): dedicated setup page with React 2026-03-23 18:59:56 +02:00
Elian Doran
79db638bf4 chore(standalone): get bootstrap to report not initialized state 2026-03-23 18:54:44 +02:00
Elian Doran
794dab2894 chore(standalone): port most of sql_init 2026-03-23 18:49:06 +02:00
Elian Doran
97b303aea6 chore(standalone): remove default seed 2026-03-23 18:34:16 +02:00
Elian Doran
a259b65085 feat(core): port image route 2026-03-23 17:11:09 +02:00
Elian Doran
5ea014cc37 fix(standalone): component ID not preserved in WS 2026-03-23 16:47:28 +02:00
Elian Doran
3210dbb6d8 feat(core): integrate similar_notes route 2026-03-23 16:29:59 +02:00
Elian Doran
64cbb2c7d2 Revert "chore(client): bypass autocomplete count for now"
This reverts commit b19bf62d7e.
2026-03-23 16:20:44 +02:00
Elian Doran
3b35dc50c5 feat(core): integrate autocomplete route 2026-03-23 16:20:18 +02:00
Elian Doran
a768d2f7a7 chore(core): relative imports broken by base path 2026-03-23 16:17:41 +02:00
Elian Doran
b671aa6204 fix(deps): update dependency i18next to v25.10.5 (#9144) 2026-03-23 15:59:06 +02:00
Elian Doran
7ffb8b0202 chore(deps): update dependency vite-plugin-static-copy to v3.4.0 (#9146) 2026-03-23 15:58:47 +02:00
renovate[bot]
6564ea2738 fix(deps): update dependency i18next to v25.10.5 2026-03-23 13:40:08 +00:00
Elian Doran
0a673d2f1b fix(deps): update dependency react-i18next to v16.6.2 (#9145) 2026-03-23 15:35:20 +02:00
renovate[bot]
05eea0d1f1 fix(deps): update dependency react-i18next to v16.6.2 2026-03-23 09:25:16 +00:00
renovate[bot]
1215fbf3e1 chore(deps): update dependency vite-plugin-static-copy to v3.4.0 2026-03-23 01:07:30 +00:00
Elian Doran
ea206116cb Translations update from Hosted Weblate (#9142) 2026-03-22 23:25:09 +02:00
Elian Doran
156ac3be6d Feature/standalone ws (#9143) 2026-03-22 23:23:03 +02:00
Elian Doran
ccc0038d4e chore(server): fix type issue 2026-03-22 23:04:51 +02:00
Elian Doran
3684f4727c Feature/standalone search integration (#9139) 2026-03-22 22:51:57 +02:00
Elian Doran
efd294d53b fix(search): wrong escape of highlighted tokens 2026-03-22 21:52:14 +02:00
Elian Doran
f9eb4bf574 chore(core): address requested changes 2026-03-22 21:48:40 +02:00
Elian Doran
b49912bf71 fix(standalone): sync failing due to credentials 2026-03-22 21:23:05 +02:00
Elian Doran
f5f11de58e fix(standalone): sync crashing due to use of Buffer 2026-03-22 21:18:39 +02:00
Marcel
7d87c89668 Translated using Weblate (German)
Currently translated at 100.0% (1719 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-03-22 19:09:50 +00:00
Aindriú Mac Giolla Eoin
b0431f2338 Translated using Weblate (Irish)
Currently translated at 100.0% (1719 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-03-22 19:09:48 +00:00
Elian Doran
a8ea40b2e1 fix(standalone): missing hmac implementation 2026-03-22 21:00:15 +02:00
Elian Doran
308bab8a3c fix(server): CORS for syncing with standalone 2026-03-22 20:53:19 +02:00
Elian Doran
ef8c4cef8a fix(server): web socket initialization not working 2026-03-22 20:44:07 +02:00
Elian Doran
63198a03ab fix(server): imports after moving to core 2026-03-22 20:38:41 +02:00
Elian Doran
ed808abd22 fix(core): sync-related issues 2026-03-22 20:17:48 +02:00
Elian Doran
9fe23442f5 chore(core): integrate content_hash 2026-03-22 20:10:59 +02:00
Elian Doran
0e2e86e7d3 chore(core): integrate consistency_checks 2026-03-22 20:09:18 +02:00
Elian Doran
ea0e3fd248 chore(core): integrate sync service and route 2026-03-22 20:02:08 +02:00
Elian Doran
2ac85a1d1c chore(core): add provider for requests 2026-03-22 19:32:51 +02:00
Elian Doran
cb71dc4202 chore(standalone): wrap requests 2026-03-22 19:17:38 +02:00
Elian Doran
6637542e7c chore(git): ignore Claude local settings 2026-03-22 19:15:53 +02:00
Elian Doran
971ce09811 chore(standalone): remove superfluos log for requests 2026-03-22 19:14:39 +02:00
Elian Doran
04826074f4 fix(standalone): error in WS initialization 2026-03-22 19:13:40 +02:00
Elian Doran
bcd4baff3d feat(standalone): basic WS functionality 2026-03-22 19:11:08 +02:00
Elian Doran
3bcf7b22be chore(standalone): add workspace-level run script 2026-03-22 19:00:17 +02:00
Elian Doran
ee8c54bdd3 chore(core): integrate sync mutex 2026-03-22 19:00:04 +02:00
Elian Doran
1af8699fc0 chore(core): integrate CLS getAndClearEntityChangeIds 2026-03-22 18:56:22 +02:00
perfectra1n
81f02209ea feat(db): update index and fix suggestion from gemini 2026-03-22 09:22:55 -07:00
perfectra1n
124d456c60 feat(db): add missing sqlite indices to help with performance 2026-03-22 09:14:33 -07:00
Elian Doran
5bc1fc71ef chore(standalone/wasm): different client-side subscriber 2026-03-22 16:12:27 +02:00
Elian Doran
0b5ce95093 fix(standalone): some sql queries not executing properly 2026-03-22 15:48:40 +02:00
Elian Doran
77971a10d1 feat(core): integrate special notes with route 2026-03-22 14:30:33 +02:00
Elian Doran
28a56ff7bf feat(core): integrate search with route 2026-03-22 14:03:48 +02:00
Elian Doran
d7d28bcf58 chore(standalone): align version with the rest 2026-03-22 13:37:52 +02:00
Elian Doran
682e1549f8 fix(standalone): failing due to type error 2026-03-22 13:03:54 +02:00
Elian Doran
d7d2b21935 feat(standalone): improve error handling on initialization 2026-03-22 13:02:50 +02:00
Elian Doran
1b7d2da6cb Merge remote-tracking branch 'origin/main' into standalone
; Conflicts:
;	apps/client/src/layouts/mobile_layout.tsx
;	apps/client/src/services/promoted_attribute_definition_parser.ts
;	apps/server/package.json
;	apps/server/src/becca/entities/bnote.ts
;	apps/server/src/etapi/etapi_utils.ts
;	apps/server/src/etapi/notes.ts
;	apps/server/src/routes/api/clipper.ts
;	apps/server/src/routes/api/export.ts
;	apps/server/src/routes/api/files.ts
;	apps/server/src/routes/api/image.ts
;	apps/server/src/routes/api/import.ts
;	apps/server/src/routes/api/note_map.ts
;	apps/server/src/routes/api/search.ts
;	apps/server/src/routes/api/similar_notes.ts
;	apps/server/src/routes/api/sync.ts
;	apps/server/src/routes/error_handlers.ts
;	apps/server/src/routes/index.ts
;	apps/server/src/routes/route_api.ts
;	apps/server/src/routes/routes.ts
;	apps/server/src/services/anonymization.ts
;	apps/server/src/services/app_info.ts
;	apps/server/src/services/builtin_attributes.ts
;	apps/server/src/services/export/zip.ts
;	apps/server/src/services/hidden_subtree.ts
;	apps/server/src/services/llm/ai_service_manager.ts
;	apps/server/src/services/llm/context/modules/context_formatter.ts
;	apps/server/src/services/llm/context/note_content.ts
;	apps/server/src/services/llm/formatters/base_formatter.ts
;	apps/server/src/services/llm/formatters/ollama_formatter.ts
;	apps/server/src/services/llm/formatters/openai_formatter.ts
;	apps/server/src/services/llm/tools/read_note_tool.ts
;	apps/server/src/services/note_types.ts
;	apps/server/src/services/notes.ts
;	apps/server/src/services/options.ts
;	apps/server/src/services/options_init.ts
;	apps/server/src/services/search/expressions/note_content_fulltext.ts
;	apps/server/src/services/utils.ts
;	apps/server/src/services/ws.ts
;	apps/server/src/share/content_renderer.ts
;	packages/commons/src/lib/builtin_attributes.ts
;	packages/commons/src/lib/rows.ts
;	packages/trilium-core/src/routes/api/attachments.ts
;	packages/trilium-core/src/routes/api/attributes.ts
;	packages/trilium-core/src/routes/api/branches.ts
;	packages/trilium-core/src/routes/api/notes.ts
;	packages/trilium-core/src/routes/api/recent_changes.ts
;	packages/trilium-core/src/routes/api/revisions.ts
;	packages/trilium-core/src/routes/api/sql.ts
;	packages/trilium-core/src/routes/api/stats.ts
;	packages/trilium-core/src/services/attributes.ts
;	packages/trilium-core/src/services/builtin_attributes.ts
;	packages/trilium-core/src/services/promoted_attribute_definition_parser.ts
;	pnpm-lock.yaml
2026-03-22 12:56:14 +02:00
Elian Doran
76fc9eaeb0 chore(deps): update dependency ws to v8.20.0 (#9136) 2026-03-22 11:40:00 +02:00
Elian Doran
a4b7f54c64 fix(nix): build failing due to rolldown optional deps 2026-03-22 11:37:05 +02:00
Elian Doran
53192d202d chore(nix): add electron & python to shell 2026-03-22 11:37:05 +02:00
Elian Doran
6896ed2c70 chore(nix): update flake lock for new Electron version 2026-03-22 11:37:05 +02:00
Elian Doran
5a96b9c48d fix(deps): update dependency i18next to v25.10.3 (#9135) 2026-03-22 10:56:13 +02:00
renovate[bot]
6113bfc57f fix(deps): update dependency i18next to v25.10.3 2026-03-22 08:49:05 +00:00
Elian Doran
9d7bc20f26 fix(deps): update dependency react-i18next to v16.6.0 (#9137) 2026-03-22 10:47:18 +02:00
renovate[bot]
79788937b9 fix(deps): update dependency react-i18next to v16.6.0 2026-03-22 01:08:10 +00:00
renovate[bot]
66873f16f2 chore(deps): update dependency ws to v8.20.0 2026-03-22 01:07:33 +00:00
Elian Doran
532e001ef0 chore(deps): update dependency stylelint to v17.5.0 (#9115) 2026-03-21 19:29:30 +02:00
Elian Doran
17991bf31f chore(deps): update dependency @preact/preset-vite to v2.10.5 (#9125) 2026-03-21 19:28:47 +02:00
renovate[bot]
2b21b1f75e chore(deps): update dependency @preact/preset-vite to v2.10.5 2026-03-21 17:28:07 +00:00
Elian Doran
dae1f9302c chore(deps): update dependency @redocly/cli to v2.24.1 (#9126) 2026-03-21 19:27:55 +02:00
Elian Doran
33365cdaf1 Translations update from Hosted Weblate (#9124) 2026-03-21 19:25:38 +02:00
green
3ac66ffe72 Translated using Weblate (Japanese)
Currently translated at 100.0% (1719 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-03-21 18:24:53 +01:00
Francis C.
81baf13720 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1719 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-03-21 18:24:52 +01:00
AggelosPnS
e0e96350d6 Translated using Weblate (Greek)
Currently translated at 2.8% (49 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/el/
2026-03-21 18:24:52 +01:00
Elian Doran
c539c21ced chore(deps): update dependency eslint to v10.1.0 (#9130) 2026-03-21 19:24:44 +02:00
Elian Doran
3f7f6cf982 fix(deps): update dependency i18next to v25.10.2 (#9113) 2026-03-21 19:23:13 +02:00
Elian Doran
271d87ae33 fix(deps): update dependency katex to v0.16.40 (#9127) 2026-03-21 19:22:03 +02:00
Elian Doran
533a77e606 fix(deps): update dependency marked to v17.0.5 (#9128) 2026-03-21 19:21:19 +02:00
Elian Doran
77cf2d4dd9 fix(deps): update dependency sanitize-filename to v1.6.4 (#9129) 2026-03-21 19:20:42 +02:00
Elian Doran
890cb247c1 fix(deps): update dependency eslint-linter-browserify to v10.1.0 (#9131) 2026-03-21 19:19:18 +02:00
renovate[bot]
8d7f4dd0fa fix(deps): update dependency i18next to v25.10.2 2026-03-21 16:55:05 +00:00
Elian Doran
00c4933344 fix(collections/grid): full-width images are too small in preview (closes #9116) 2026-03-21 09:15:13 +02:00
Elian Doran
cd9b46e1c7 fix(attributes): attribute detail not showing up for first item (closes #6948) 2026-03-21 09:06:21 +02:00
Elian Doran
b356b355ca fix(layout): attribute details not visible in new layout (closes #9005) 2026-03-21 08:58:13 +02:00
renovate[bot]
d1aebb7bb0 fix(deps): update dependency eslint-linter-browserify to v10.1.0 2026-03-21 02:04:29 +00:00
renovate[bot]
6cbb595ae8 chore(deps): update dependency eslint to v10.1.0 2026-03-21 02:03:50 +00:00
renovate[bot]
fcf238bc35 fix(deps): update dependency sanitize-filename to v1.6.4 2026-03-21 02:03:10 +00:00
renovate[bot]
8c82468ecc fix(deps): update dependency marked to v17.0.5 2026-03-21 02:02:32 +00:00
renovate[bot]
965905ce00 fix(deps): update dependency katex to v0.16.40 2026-03-21 02:01:52 +00:00
renovate[bot]
ed280775bd chore(deps): update dependency @redocly/cli to v2.24.1 2026-03-21 02:01:10 +00:00
Elian Doran
8834899012 fix(math): limit size of popup and add back overflow (closes #9117) 2026-03-20 20:57:07 +02:00
Elian Doran
55dea474e9 chore(deps): update dependency @ckeditor/ckeditor5-dev-build-tools to v55.2.0 (#9099) 2026-03-20 13:45:51 +02:00
Elian Doran
bc74455a64 chore(deps): update dependency @smithy/middleware-retry to v4.4.44 (#9111) 2026-03-20 13:45:21 +02:00
Elian Doran
2d0b28367f chore(deps): update dependency vite to v8.0.1 (#9112) 2026-03-20 13:45:00 +02:00
Elian Doran
7d8a3e2811 fix(deps): update dependency katex to v0.16.39 (#9114) 2026-03-20 13:44:32 +02:00
renovate[bot]
79e5d9595a chore(deps): update dependency @ckeditor/ckeditor5-dev-build-tools to v55.2.0 2026-03-20 00:11:04 +00:00
renovate[bot]
1f0fa57218 chore(deps): update dependency stylelint to v17.5.0 2026-03-20 00:09:32 +00:00
renovate[bot]
0310626025 fix(deps): update dependency katex to v0.16.39 2026-03-20 00:08:50 +00:00
renovate[bot]
fefbb40c03 chore(deps): update dependency vite to v8.0.1 2026-03-20 00:07:33 +00:00
renovate[bot]
12f89078b8 chore(deps): update dependency @smithy/middleware-retry to v4.4.44 2026-03-20 00:06:57 +00:00
Elian Doran
8d873c5869 fix(share): prevent crash when accessing /share on uninitialized setup (#9088) 2026-03-19 20:13:32 +02:00
Elian Doran
27f4ac1d03 Textarea Label Support (#9077) 2026-03-19 20:02:11 +02:00
Elian Doran
d533360903 chore(attributes): rename textarea to multiline text 2026-03-19 20:01:55 +02:00
Elian Doran
49f5dc1c26 fix(table): text jumping when editing multiline text 2026-03-19 20:00:43 +02:00
Elian Doran
16419ed4ac Merge remote-tracking branch 'origin/main' into textarea-labels 2026-03-19 19:54:16 +02:00
Elian Doran
1595d1b5c9 Fix setup page error (#9102) 2026-03-19 19:50:07 +02:00
Elian Doran
50eb11997c fix(setup): contrast issue in alert (closes #8915) 2026-03-19 19:49:32 +02:00
Elian Doran
d974dfbc31 Translations update from Hosted Weblate (#9109) 2026-03-19 19:26:35 +02:00
Hosted Weblate
e9b63e50d4 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/
2026-03-19 18:08:13 +01:00
Elian Doran
b6efb7c9ab feat(relation_map): add common note opening options to context menu 2026-03-19 19:07:54 +02:00
Elian Doran
f84479b1c2 fix(deps): update univer monorepo to v0.18.0 (#9101) 2026-03-19 17:48:23 +02:00
JYC333
8597fa560b Merge branch 'main' into fix/share-uninitialized-crash 2026-03-19 11:49:55 +00:00
JYC333
b29ab93fd5 fix: limit the scope of DB check only for share page 2026-03-19 11:46:35 +00:00
JYC333
225cdaff46 Update apps/client/src/setup.ts
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-19 11:34:53 +00:00
JYC333
61dfba8c32 fix: remove knockout dependency 2026-03-19 11:24:45 +00:00
JYC333
4fee91d219 fix: change html to use DOM status 2026-03-19 11:24:09 +00:00
JYC333
12e4f76a8b fix: move setup to DOM implementation to fix knockout issue 2026-03-19 11:23:06 +00:00
JYC333
ec30598397 chore(deps): update dependency eslint-plugin-playwright to v2.10.1 (#9097) 2026-03-19 10:55:01 +00:00
JYC333
fdd2cc77e6 Merge branch 'main' into renovate/eslint-plugin-playwright-2.x 2026-03-19 10:30:16 +00:00
JYC333
6f5c618bcd chore(deps): update dependency @redocly/cli to v2.24.0 (#9100) 2026-03-19 10:29:43 +00:00
JYC333
8041112414 chore(deps): update dependency sanitize-html to v2.17.2 (#9098) 2026-03-19 10:28:18 +00:00
renovate[bot]
40ab2bc798 fix(deps): update univer monorepo to v0.18.0 2026-03-19 00:46:10 +00:00
renovate[bot]
3a3029cf3a chore(deps): update dependency @redocly/cli to v2.24.0 2026-03-19 00:45:28 +00:00
renovate[bot]
e7d1c75cdb chore(deps): update dependency sanitize-html to v2.17.2 2026-03-19 00:44:03 +00:00
renovate[bot]
bc907ee6ad chore(deps): update dependency eslint-plugin-playwright to v2.10.1 2026-03-19 00:43:25 +00:00
Elian Doran
0bff3f1fbc chore(deps): update dependency electron to v41.0.3 (#9090) 2026-03-18 20:18:44 +02:00
Elian Doran
377864b2c6 chore(deps): update dependency wxt to v0.20.20 (#9091) 2026-03-18 19:50:35 +02:00
Elian Doran
3db13df245 fix(deps): update dependency @zumer/snapdom to v2.5.0 (#9093) 2026-03-18 19:48:36 +02:00
Elian Doran
a2e09b40fa chore(deps): update dependency sax to v1.6.0 (#9092) 2026-03-18 19:43:21 +02:00
Elian Doran
aa590753d5 Translations update from Hosted Weblate (#9095) 2026-03-18 13:28:23 +02:00
Hosted Weblate
1c8519d7ec Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-03-18 12:26:56 +01:00
Elian Doran
686a614bfa Translations update from Hosted Weblate (#9085) 2026-03-18 13:26:47 +02:00
Elian Doran
01261220e3 Apply suggestion from @gemini-code-assist[bot]
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-18 13:26:25 +02:00
허석
1bf92ab19a Translated using Weblate (Korean)
Currently translated at 93.9% (109 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/ko/
2026-03-18 12:15:30 +01:00
Yunho Park
c0d0d1868d Translated using Weblate (Korean)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/
2026-03-18 12:15:30 +01:00
허석
da2dec16cc Translated using Weblate (Korean)
Currently translated at 7.3% (126 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ko/
2026-03-18 12:15:29 +01:00
Ulices
787ef7d513 Translated using Weblate (Spanish)
Currently translated at 100.0% (1719 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-03-18 12:15:28 +01:00
허석
dcb0ce79e6 Translated using Weblate (Korean)
Currently translated at 51.4% (199 of 387 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ko/
2026-03-18 12:15:27 +01:00
Elian Doran
9d8202539d chore(deps): update pnpm/action-setup action to v5 (#9094) 2026-03-18 13:15:19 +02:00
renovate[bot]
c46af4bca9 chore(deps): update pnpm/action-setup action to v5 2026-03-18 01:26:54 +00:00
renovate[bot]
610f6652df fix(deps): update dependency @zumer/snapdom to v2.5.0 2026-03-18 01:26:47 +00:00
renovate[bot]
b33635381b chore(deps): update dependency sax to v1.6.0 2026-03-18 01:26:06 +00:00
renovate[bot]
f20af8cac9 chore(deps): update dependency wxt to v0.20.20 2026-03-18 01:25:25 +00:00
renovate[bot]
29f3b987aa chore(deps): update dependency electron to v41.0.3 2026-03-18 01:24:43 +00:00
argusagent
e9987b40e6 fix(share): wire assertShareDbReady into router middleware — address code review 2026-03-17 20:27:04 -04:00
argusagent
1990a990c3 fix(share): return 503 when app is still initializing (#5677) 2026-03-17 20:22:06 -04:00
argusagent
d1159d3af9 fix(share): guard against uninitialized DB connection on /share routes (#5677) 2026-03-17 20:22:05 -04:00
JYC333
723dada78a chore(deps): update typescript-eslint monorepo to v8.57.1 (#9080) 2026-03-17 21:56:25 +00:00
JYC333
0e089af677 chore(deps): update dependency @smithy/middleware-retry to v4.4.43 (#9079) 2026-03-17 20:47:42 +00:00
renovate[bot]
f4aed5d012 chore(deps): update typescript-eslint monorepo to v8.57.1 2026-03-17 20:42:50 +00:00
Elian Doran
860f953962 chore(deps): update dependency @redocly/cli to v2.22.1 (#9081) 2026-03-17 22:37:46 +02:00
Elian Doran
ded692ead7 chore(deps): update ckeditor5 config packages to v14 (major) (#9082) 2026-03-17 22:37:20 +02:00
renovate[bot]
4f5413ebbe chore(deps): update ckeditor5 config packages to v14 2026-03-17 01:14:47 +00:00
renovate[bot]
8f3e210740 chore(deps): update dependency @redocly/cli to v2.22.1 2026-03-17 01:14:03 +00:00
renovate[bot]
783cb8b4e9 chore(deps): update dependency @smithy/middleware-retry to v4.4.43 2026-03-17 01:12:33 +00:00
Mystler
2b94d96930 fix(labels): Code review issue 2026-03-16 18:21:08 +01:00
Elian Doran
ca349e03f2 feat(editor): add catppuccin theme to highlightjs (#9075) 2026-03-16 18:39:40 +02:00
Mystler
5b5222b846 feat(labels): Add textarea label type with Table support 2026-03-16 17:07:37 +01:00
Giulia Ye
850f8ad939 feat(editor): make theme selector scoped to code tag replace regex more robust
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-16 11:55:06 +01:00
Giulia Ye
50e5f89e9a feat(editor): make theme selector scoped to code tag regex more robust
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-16 11:54:14 +01:00
giuxtaposition
603b47f9b0 feat(editor): add catppuccin theme to highlightjs 2026-03-16 10:35:49 +01:00
Elian Doran
92227c364e chore(deps): update dependency @preact/preset-vite to v2.10.4 (#9070) 2026-03-16 08:05:58 +02:00
Elian Doran
10ac18a7cc chore(deps): update dependency webdriverio to v9.26.1 (#9071) 2026-03-16 08:04:54 +02:00
Elian Doran
e06123e4bd chore(deps): update softprops/action-gh-release action to v2.6.1 (#9072) 2026-03-16 08:04:23 +02:00
Elian Doran
b44bd544cd chore(deps): update dependency node-abi to v4.28.0 (#9073) 2026-03-16 08:01:45 +02:00
renovate[bot]
4c3a448330 chore(deps): update softprops/action-gh-release action to v2.6.1 2026-03-16 01:31:50 +00:00
renovate[bot]
7f07c249af chore(deps): update dependency node-abi to v4.28.0 2026-03-16 01:31:44 +00:00
renovate[bot]
51958d2ac0 chore(deps): update dependency webdriverio to v9.26.1 2026-03-16 00:10:44 +00:00
renovate[bot]
67f474d794 chore(deps): update dependency @preact/preset-vite to v2.10.4 2026-03-16 00:10:12 +00:00
Adorian Doran
e6e8ebd881 pdf.js: add ability to comment selected text (#9068) 2026-03-16 02:08:31 +02:00
Elian Doran
b138fedd35 CSRF fixes (#9067) 2026-03-15 20:13:46 +02:00
contributor
a92d846b57 pdf.js: add ability to comment selected text 2026-03-15 20:05:59 +02:00
Elian Doran
7a544482d1 chore(server): request bootstrap with no cache 2026-03-15 19:39:27 +02:00
Elian Doran
53739ee8d4 e2e(server): address flaky test 2026-03-15 19:33:21 +02:00
Elian Doran
495145e033 chore(server): use random UUID for session ID 2026-03-15 19:33:06 +02:00
Elian Doran
6701d09df5 feat(client): refresh CSRF if request fails 2026-03-15 19:02:28 +02:00
Elian Doran
e36d7121f1 fix(desktop): broken due to CSRF failing 2026-03-15 18:54:37 +02:00
JYC333
9290a60b23 chore(deps): update dependency csrf-csrf to v4.0.3 (#9061) 2026-03-15 15:08:17 +00:00
Elian Doran
761de79a8c chore(deps): update dependency eslint-plugin-playwright to v2.10.0 (#9064) 2026-03-15 16:37:03 +02:00
Elian Doran
b8e9beff1b chore(deps): update dependency lint-staged to v16.4.0 (#9065) 2026-03-15 16:23:52 +02:00
Elian Doran
5b13e0ba4f chore(deps): update softprops/action-gh-release action to v2.5.3 (#9063) 2026-03-15 16:23:07 +02:00
Elian Doran
b0bab18d00 chore(deps): update dependency wxt to v0.20.19 (#9062) 2026-03-15 16:21:31 +02:00
Elian Doran
b4fa1392de Translations update from Hosted Weblate (#9066) 2026-03-15 16:07:45 +02:00
Elian Doran
4e58002e13 Apply suggestions from code review
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-15 16:07:18 +02:00
Aindriú Mac Giolla Eoin
adc149aac8 Translated using Weblate (Irish)
Currently translated at 100.0% (1719 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-03-15 11:09:59 +00:00
Marcel
4c02773ddc Translated using Weblate (German)
Currently translated at 100.0% (1719 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-03-15 11:09:56 +00:00
green
892abe1d70 Translated using Weblate (Japanese)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ja/
2026-03-15 11:09:54 +00:00
green
71c23d33ff Translated using Weblate (Japanese)
Currently translated at 100.0% (1719 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-03-15 11:09:52 +00:00
msnx0
9c110e896e Translated using Weblate (Polish)
Currently translated at 100.0% (1719 of 1719 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pl/
2026-03-15 11:09:50 +00:00
renovate[bot]
afe7a748e1 chore(deps): update softprops/action-gh-release action to v2.5.3 2026-03-15 05:14:23 +00:00
renovate[bot]
325e582593 chore(deps): update dependency lint-staged to v16.4.0 2026-03-15 00:53:10 +00:00
renovate[bot]
ccebe6a423 chore(deps): update dependency eslint-plugin-playwright to v2.10.0 2026-03-15 00:52:28 +00:00
renovate[bot]
f7c92fa4b2 chore(deps): update dependency wxt to v0.20.19 2026-03-15 00:51:42 +00:00
renovate[bot]
d07c2d118f chore(deps): update dependency csrf-csrf to v4.0.3 2026-03-15 00:51:00 +00:00
Elian Doran
94a09edd1d Renovate/csrf csrf 4.x (#5831) 2026-03-15 00:19:23 +02:00
Elian Doran
f6f939c245 chore(server): address requested changes 2026-03-14 23:49:36 +02:00
Elian Doran
0d889426e8 refactor(server): use different approach to handling the CSRF token 2026-03-14 23:48:06 +02:00
Elian Doran
c8a546ef1e fix(server): uninitialized sessions causing bad CSRF errors 2026-03-14 23:31:17 +02:00
Elian Doran
693919b21a Merge remote-tracking branch 'origin/main' into renovate/csrf-csrf-4.x 2026-03-14 22:48:43 +02:00
Elian Doran
7c1a2039b1 feat(editor): add catppuccin theme to codemirror (#9060) 2026-03-14 22:05:50 +02:00
Elian Doran
c66e4e0475 pdf.js floating highlight button (#9048) 2026-03-14 22:03:55 +02:00
Elian Doran
6bdfbf0d7d chore(deps): fix package lock 2026-03-14 21:52:55 +02:00
Giulia Ye
61393bca90 fix(editor): catppuccin theme declared following alphabetical order based by id
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-14 16:03:03 +01:00
giuxtaposition
c41b649bff feat(editor): add catppuccin theme to codemirror 2026-03-14 15:58:37 +01:00
Elian Doran
ba87487714 Space mobile launcher container evenly (#9031) 2026-03-14 13:49:20 +02:00
Elian Doran
3e73f38ae2 chore(deps): update dependency vite to v8 (#9043) 2026-03-14 13:45:25 +02:00
Elian Doran
51dd55c3fd Merge remote-tracking branch 'origin/main' into renovate/vite-8.x
; Conflicts:
;	pnpm-lock.yaml
2026-03-14 12:54:04 +02:00
Elian Doran
de49ca37b9 fix(deps): update ckeditor monorepo (#8884) 2026-03-14 12:51:11 +02:00
Elian Doran
8d8080ee09 Merge remote-tracking branch 'origin/main' into renovate/ckeditor-monorepo
; Conflicts:
;	pnpm-lock.yaml
2026-03-14 12:38:35 +02:00
Elian Doran
f3613ccb25 chore(client): typecheck failing due to changes in JSX handling 2026-03-14 12:35:44 +02:00
Elian Doran
ad7b5700f3 chore(client): tests not running due to change in Vite 2026-03-14 12:29:57 +02:00
Elian Doran
0d17c62d02 chore(client): remove deprecated vite manual chunks 2026-03-14 12:15:43 +02:00
Elian Doran
826690982a chore(deps): update dependency pdfjs-dist to v5.5.207 (#8874) 2026-03-14 12:10:41 +02:00
Elian Doran
b438ff9c62 chore(deps): update dependency electron to v41 (#9025) 2026-03-14 12:08:15 +02:00
Elian Doran
43fb9d1a23 chore(deps): update docker/setup-buildx-action action to v4 (#8931) 2026-03-14 12:07:53 +02:00
Elian Doran
fb17ce8c8a Merge remote-tracking branch 'origin/main' into renovate/ckeditor-monorepo
; Conflicts:
;	pnpm-lock.yaml
2026-03-14 12:06:40 +02:00
Elian Doran
3a8e12535e fix(ckeditor5): version misalignment 2026-03-14 12:04:32 +02:00
Elian Doran
ce0caa3f6d chore(deps): update dependency @smithy/middleware-retry to v4.4.42 (#9035) 2026-03-14 12:00:14 +02:00
Elian Doran
00a0315f12 chore(pdfjs): fix type error after update 2026-03-14 11:59:57 +02:00
renovate[bot]
da38d56dc7 chore(deps): update docker/setup-buildx-action action to v4 2026-03-14 09:55:13 +00:00
renovate[bot]
83e47cba2c chore(deps): update dependency electron to v41 2026-03-14 09:54:14 +00:00
Elian Doran
f4d1eebed4 fix(deps): update dependency knockout to v3.5.2 (#8970) 2026-03-14 11:47:51 +02:00
Elian Doran
f27b394099 Merge remote-tracking branch 'origin/main' into renovate/ckeditor-monorepo
; Conflicts:
;	pnpm-lock.yaml
2026-03-14 11:47:16 +02:00
renovate[bot]
a66c9ccc1f chore(deps): update dependency @smithy/middleware-retry to v4.4.42 2026-03-14 09:45:48 +00:00
Elian Doran
0bc6a830c8 chore(pdfjs): update viewer to latest 2026-03-14 11:42:39 +02:00
Elian Doran
f3008b29af chore(deps): bump yauzl from 2.10.0 to 3.2.1 (#9047) 2026-03-14 11:33:15 +02:00
Elian Doran
5b16ff8be1 chore(client): fix type error after update of knockout 2026-03-14 11:32:44 +02:00
Elian Doran
86621e3388 fix(deps): update dependency reveal.js to v6 (#9028) 2026-03-14 11:30:40 +02:00
Elian Doran
1beb0668c3 fix(deps): update codemirror (#9039) 2026-03-14 11:30:21 +02:00
Elian Doran
34b09f90fb fix(deps): wrong ckeditor version 2026-03-14 11:24:49 +02:00
Elian Doran
a6f964925b chore(presentation): fix paths to themes 2026-03-14 11:17:21 +02:00
Elian Doran
196416bb9f chore(presentation): fix type issues 2026-03-14 11:15:04 +02:00
Elian Doran
1535db9f7d chore(presentation): remove now redundant type definitions 2026-03-14 11:13:50 +02:00
renovate[bot]
ae38ac4de8 fix(deps): update dependency knockout to v3.5.2 2026-03-14 09:11:59 +00:00
Elian Doran
e0aa8d8ecf fix(codemirror): version misalignment causing type errors 2026-03-14 11:09:48 +02:00
renovate[bot]
23c1eacf2b fix(deps): update ckeditor monorepo 2026-03-14 09:03:13 +00:00
contributor
10e28789e2 add optional chaining to access window.parent in callback 2026-03-14 11:03:10 +02:00
contributor
940f7f77f5 add automatic removal of the event 2026-03-14 11:03:10 +02:00
renovate[bot]
83f8b4fcb4 fix(deps): update codemirror 2026-03-14 09:02:35 +00:00
Elian Doran
42dc801ddf fix(deps): update univer monorepo to v0.17.0 (#9057) 2026-03-14 11:01:08 +02:00
renovate[bot]
5e6e5bfbec chore(deps): update dependency vite to v8 2026-03-14 09:00:57 +00:00
Elian Doran
28fc99dd45 fix(deps): update dependency mermaid to v11.13.0 (#8988) 2026-03-14 10:59:16 +02:00
Elian Doran
9ef501c399 Mermaid diagram switcher (#9058) 2026-03-14 10:54:47 +02:00
Elian Doran
6bba908654 feat(mermaid): add new sample diagrams for Ishikawa & Venn 2026-03-14 10:54:33 +02:00
Elian Doran
d423e43312 Merge branch 'feature/mermaid_diagram_switcher' into renovate/mermaid-11.x 2026-03-14 10:50:33 +02:00
Elian Doran
91718f218b Merge remote-tracking branch 'origin/main' into renovate/mermaid-11.x 2026-03-14 10:50:18 +02:00
Elian Doran
e623e91a82 docs(user): mention changes to Mermaid diagrams 2026-03-14 10:44:55 +02:00
Elian Doran
dce9f50911 fix(mermaid): not recentering when using the sample switcher 2026-03-14 10:37:26 +02:00
Elian Doran
5f1486cf6a feat(mermaid): use custom placeholder 2026-03-14 10:31:28 +02:00
Elian Doran
2c6bdc79af chore(mermaid): rounded corners for editor only on desktop 2026-03-14 10:25:59 +02:00
Elian Doran
a6b89cfa30 chore(mermaid): use translations for sample names 2026-03-14 10:22:35 +02:00
Elian Doran
21d1cd395b feat(mermaid): add a text for sample diagram switcher 2026-03-14 10:14:21 +02:00
Elian Doran
981466cbe8 fix(deps): update dependency better-sqlite3 to v12.8.0 (#9056) 2026-03-14 10:08:38 +02:00
Elian Doran
0209573fce chore(mermaid): remove default placeholder content 2026-03-14 10:03:15 +02:00
Elian Doran
92f8459f28 feat(mermaid): show switcher only when note is empty 2026-03-14 10:02:05 +02:00
Elian Doran
da193b456b fix(mermaid): error when diagram is empty 2026-03-14 10:00:18 +02:00
Elian Doran
6c151afca3 fix(mermaid): error text not selectable 2026-03-14 09:58:45 +02:00
Elian Doran
aba6750c18 chore(mermaid): add rounded corners to code editor 2026-03-14 09:44:06 +02:00
Elian Doran
b9a8e4e4ba feat(mermaid): add all official samples 2026-03-14 09:42:35 +02:00
Elian Doran
4134c4ddd0 feat(mermaid): replace note content on switch 2026-03-14 09:29:28 +02:00
Elian Doran
72038fb2ec chore(mermaid): basic logic for content switcher 2026-03-14 09:26:14 +02:00
Elian Doran
069d8b1ae4 refactor(mermaid): move into own directory 2026-03-14 09:20:07 +02:00
Elian Doran
ce71068f6d chore(mermaid): add a bottom section for switching between samples 2026-03-14 09:18:38 +02:00
renovate[bot]
dc298a44e1 fix(deps): update univer monorepo to v0.17.0 2026-03-14 06:54:06 +00:00
renovate[bot]
306bfd7673 fix(deps): update dependency better-sqlite3 to v12.8.0 2026-03-14 06:53:12 +00:00
Elian Doran
25cf23f507 chore(deps): update dependency http-proxy-agent to v8 (#9026) 2026-03-14 08:39:04 +02:00
Elian Doran
096d5f7c65 chore: remove old references to svelte 2026-03-14 08:06:46 +02:00
Elian Doran
0e2dee1609 chore(renovate): group univerjs packages 2026-03-14 08:05:50 +02:00
Elian Doran
ec927d25a9 chore(deps): update dependency @redocly/cli to v2.21.1 (#8905) 2026-03-14 07:44:45 +02:00
renovate[bot]
7c8aefb4ef chore(deps): update dependency http-proxy-agent to v8 2026-03-14 05:44:33 +00:00
Elian Doran
e840769dd6 fix(deps): update dependency react-i18next to v16.5.8 (#9014) 2026-03-14 07:44:00 +02:00
Elian Doran
4a5d3f01b8 chore(deps): update dependency node-abi to v4.27.0 (#9015) 2026-03-14 07:43:26 +02:00
Elian Doran
71d975f339 fix(deps): update dependency force-graph to v1.51.2 (#9050) 2026-03-14 07:42:10 +02:00
Elian Doran
a4051fc372 chore(deps): update pnpm to v10.32.1 (#9012) 2026-03-14 07:41:44 +02:00
Elian Doran
359b2a68b8 chore(deps): update dependency https-proxy-agent to v8 (#9027) 2026-03-14 07:41:11 +02:00
Elian Doran
8124e4e589 chore(deps): update dependency electron to v40.8.2 (#9049) 2026-03-14 07:40:06 +02:00
Elian Doran
5f699996f8 chore(deps): update dependency rollup-plugin-webpack-stats to v3.1.0 (#9051) 2026-03-14 07:35:09 +02:00
Elian Doran
c7c0bc4185 chore(deps): update marocchino/sticky-pull-request-comment action to v3 (#9053) 2026-03-14 07:34:47 +02:00
Elian Doran
d422ac7bc5 chore(deps): update dependency vite-plugin-static-copy to v3.3.0 (#9052) 2026-03-14 07:33:52 +02:00
Elian Doran
0bd912d18a Translations update from Hosted Weblate (#9054) 2026-03-14 07:33:19 +02:00
Ulices
252f2fe72c Translated using Weblate (Spanish)
Currently translated at 100.0% (1693 of 1693 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-03-14 06:09:49 +01:00
renovate[bot]
7f29c347e2 chore(deps): update marocchino/sticky-pull-request-comment action to v3 2026-03-14 00:40:57 +00:00
renovate[bot]
7a3dc824d6 chore(deps): update dependency vite-plugin-static-copy to v3.3.0 2026-03-14 00:40:47 +00:00
renovate[bot]
8550c62771 chore(deps): update dependency rollup-plugin-webpack-stats to v3.1.0 2026-03-14 00:39:43 +00:00
renovate[bot]
66cd657cd8 fix(deps): update dependency force-graph to v1.51.2 2026-03-14 00:38:45 +00:00
renovate[bot]
beac80e175 chore(deps): update dependency electron to v40.8.2 2026-03-14 00:37:41 +00:00
contributor
5ae9952ba1 use pagehide event instead of unload 2026-03-14 01:30:59 +02:00
contributor
d4bc1ec444 add typings 2026-03-14 01:24:50 +02:00
contributor
d52f529b24 extract handler setup into a function 2026-03-14 01:24:50 +02:00
contributor
9a9cfdec2b add unload handler 2026-03-14 01:24:50 +02:00
contributor
6ab421ffa0 check target iframe 2026-03-14 01:24:50 +02:00
contributor
53b0aafb98 disable preferences warning 2026-03-14 01:24:49 +02:00
contributor
6b02ad8421 enable floating highlight button 2026-03-14 01:24:43 +02:00
renovate[bot]
242ebfccc0 chore(deps): update dependency https-proxy-agent to v8 2026-03-13 22:26:27 +00:00
renovate[bot]
545cc0782f fix(deps): update dependency react-i18next to v16.5.8 2026-03-13 22:24:39 +00:00
renovate[bot]
bf6a2917cd fix(deps): update dependency reveal.js to v6 2026-03-13 22:22:26 +00:00
renovate[bot]
eaba2a8395 fix(deps): update dependency mermaid to v11.13.0 2026-03-13 22:19:14 +00:00
Elian Doran
c581fb17bc chore(deps): update dependency @ckeditor/ckeditor5-dev-build-tools to v55 (#9042) 2026-03-14 00:14:41 +02:00
renovate[bot]
9b05b95d77 chore(deps): update dependency pdfjs-dist to v5.5.207 2026-03-13 22:12:07 +00:00
renovate[bot]
b3ba18ddd0 chore(deps): update dependency node-abi to v4.27.0 2026-03-13 22:11:25 +00:00
renovate[bot]
bb2a633ba7 chore(deps): update dependency @redocly/cli to v2.21.1 2026-03-13 22:10:44 +00:00
renovate[bot]
913efdef03 chore(deps): update pnpm to v10.32.1 2026-03-13 22:07:56 +00:00
Elian Doran
bf0bea18b1 fix(deps): update dependency i18next to v25.8.18 (#9013) 2026-03-14 00:06:31 +02:00
Elian Doran
cdebd1f63a chore(deps): update dependency esbuild to v0.27.4 (#9037) 2026-03-14 00:06:07 +02:00
Elian Doran
a6a2635836 chore(deps): update dependency happy-dom to v20.8.4 (#9038) 2026-03-14 00:05:38 +02:00
dependabot[bot]
024b57c2b4 chore(deps): bump yauzl from 2.10.0 to 3.2.1
Bumps [yauzl](https://github.com/thejoshwolfe/yauzl) from 2.10.0 to 3.2.1.
- [Commits](https://github.com/thejoshwolfe/yauzl/compare/2.10.0...3.2.1)

---
updated-dependencies:
- dependency-name: yauzl
  dependency-version: 3.2.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-13 20:13:10 +00:00
renovate[bot]
6fbc85cbc7 chore(deps): update dependency @ckeditor/ckeditor5-dev-build-tools to v55 2026-03-13 05:51:53 +00:00
renovate[bot]
5f8a0aee13 chore(deps): update dependency happy-dom to v20.8.4 2026-03-13 05:47:50 +00:00
renovate[bot]
0c67b292ef chore(deps): update dependency esbuild to v0.27.4 2026-03-13 05:46:42 +00:00
Elian Doran
ba663e6162 chore(deps): update dependency electron to v40.8.1 (#9036) 2026-03-13 07:42:35 +02:00
Elian Doran
14925266cf fix(deps): update dependency dayjs to v1.11.20 (#9040) 2026-03-13 07:41:36 +02:00
Elian Doran
702e29bd8c chore(deps): update vitest monorepo to v4.1.0 (#9041) 2026-03-13 07:41:10 +02:00
Elian Doran
27ac3e58c5 fix(deps): update dependency sqlite3 to v6 (#9044) 2026-03-13 07:40:07 +02:00
Elian Doran
86e268c06d Translations update from Hosted Weblate (#9046) 2026-03-13 07:39:12 +02:00
Ulices
6e4b231319 Translated using Weblate (Spanish)
Currently translated at 99.2% (1681 of 1693 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-03-13 05:09:49 +01:00
green
041eff6cbd Translated using Weblate (Japanese)
Currently translated at 100.0% (1693 of 1693 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-03-13 05:09:48 +01:00
renovate[bot]
1a3471a516 fix(deps): update dependency sqlite3 to v6 2026-03-13 01:10:11 +00:00
renovate[bot]
57c8727bb1 chore(deps): update vitest monorepo to v4.1.0 2026-03-13 01:06:52 +00:00
renovate[bot]
bd451d0738 fix(deps): update dependency dayjs to v1.11.20 2026-03-13 01:05:48 +00:00
renovate[bot]
ced062842d chore(deps): update dependency electron to v40.8.1 2026-03-13 01:01:51 +00:00
Mystler
f7067fb968 fix(mobile): Space mobile launcher container evenly 2026-03-12 19:24:27 +01:00
renovate[bot]
309a81a0fe fix(deps): update dependency i18next to v25.8.18 2026-03-12 01:14:10 +00:00
contributor
ac24c69858 fix(webview): refresh content for SPAs with "query string" in hash 2026-03-02 23:35:53 +02:00
Elian Doran
9350c43e5b chore(core): port bulk actions route 2026-02-09 19:49:07 +02:00
Elian Doran
0fae11d54c chore(core): port bulk actions service 2026-02-09 19:46:34 +02:00
Elian Doran
1ed3999639 chore(core): port recent changes route 2026-02-09 19:43:53 +02:00
Elian Doran
7d30771f05 chore(core): port relation map route 2026-02-09 19:41:31 +02:00
Elian Doran
08f1d44d90 chore(core): port revisions route 2026-02-09 19:38:24 +02:00
Elian Doran
969860c344 chore(core): port attribute route 2026-02-09 19:32:46 +02:00
Elian Doran
ed905c9d64 chore(core): integrate builtin_attributes 2026-02-09 19:29:59 +02:00
Elian Doran
c5518b64b7 chore(core): integrate attribute_formatter 2026-02-09 19:24:06 +02:00
Elian Doran
a7b2b631c5 feat(standalone): add warning about stability 2026-02-09 18:59:44 +02:00
Elian Doran
dcfc1119eb chore(core): port sql route 2026-02-09 18:38:51 +02:00
Elian Doran
88add55ebc chore(standalone): wrap routes in a transaction 2026-02-09 18:35:29 +02:00
Elian Doran
ad41a58904 chore(standalone): use CLS with per-request context isolation 2026-02-09 18:20:14 +02:00
Elian Doran
49ce312ab2 chore(standalone): use a simpler CLS mechanism considering lack of multi-threading 2026-02-09 18:16:15 +02:00
Elian Doran
223d69206c fix(standalone): missing context menu cover 2026-02-09 18:00:11 +02:00
Elian Doran
d68ada1026 fix(standalone): translations not working in prod 2026-02-08 22:38:28 +02:00
Elian Doran
e0a23f6b63 fix(bootstrap): background effects are enabled 2026-02-08 21:30:19 +02:00
Elian Doran
bd147ea72e Merge remote-tracking branch 'origin/main' into standalone 2026-02-08 21:14:12 +02:00
Elian Doran
4494aed1cf chore(standalone): use async for init 2026-01-30 15:55:20 +02:00
Elian Doran
788eaad61c fix(standalone): wrong server translation path in production 2026-01-30 15:49:32 +02:00
Elian Doran
0cfd6bae0e refactor(standalone): use different mechanism for importing local server worker 2026-01-30 15:24:53 +02:00
Elian Doran
82c435b916 chore(ci): deploy app on workflow change 2026-01-30 07:55:21 +02:00
Elian Doran
bc5b9708c7 Merge remote-tracking branch 'origin/main' into standalone 2026-01-30 07:51:36 +02:00
Elian Doran
7e87e6f832 chore(ci): deploy app on standalone branch 2026-01-30 07:48:11 +02:00
Elian Doran
e5a7a32439 chore(core): port cloning route 2026-01-29 22:20:54 +02:00
Elian Doran
e9214d84b7 chore(core): port stats route 2026-01-29 21:51:47 +02:00
Elian Doran
da7a61a8b6 Merge remote-tracking branch 'origin/main' into HEAD
; Conflicts:
;	apps/client/src/index.ts
;	apps/client/src/widgets/sql_table_schemas.tsx
;	apps/server/package.json
;	apps/server/src/app.ts
;	apps/server/src/becca/entities/bnote.ts
;	apps/server/src/services/import/single.ts
;	apps/server/src/services/import/zip.ts
;	apps/server/src/services/note-interface.ts
;	apps/server/src/services/notes.ts
;	apps/server/src/services/tree.ts
;	apps/server/src/services/utils.ts
;	apps/server/src/share/shaca/entities/snote.ts
;	pnpm-lock.yaml
;	scripts/update-nightly-version.ts
;	scripts/update-version.ts
2026-01-29 21:47:06 +02:00
Elian Doran
458e858b24 fix(standalone): error due to SQL returning bigint 2026-01-17 20:01:46 +02:00
Elian Doran
ec84e72b4c Lightweight/browser api (#8287) 2026-01-14 18:30:05 +02:00
Elian Doran
64a8c3b005 chore(client-standalone): address requested changes 2026-01-14 18:27:53 +02:00
Elian Doran
0b5cf2e6c8 Merge remote-tracking branch 'origin/standalone' into lightweight/browser_api 2026-01-14 18:04:54 +02:00
Elian Doran
7ed4e1c284 Lightweight/decouple server api (#8284) 2026-01-14 18:01:54 +02:00
Elian Doran
9dd7616f7d chore(client-standalone): address requested changes 2026-01-14 18:00:10 +02:00
Elian Doran
ab29caff7b fix(client-standalone): CK premium features not working 2026-01-14 17:48:29 +02:00
Elian Doran
7633e3d48e chore(client-standalone): address requested changes 2026-01-14 17:41:24 +02:00
Elian Doran
411fdf3114 chore(client-standalone): disable WS error notification 2026-01-14 17:33:57 +02:00
Elian Doran
5c52917459 fix(client-standalone): webmanifest icon path not correct 2026-01-14 17:31:06 +02:00
Elian Doran
51753ad82a chore(ci): run tests on standalone branch as well 2026-01-12 21:51:26 +02:00
Elian Doran
7e00634f3d chore(deps): align package lock 2026-01-12 21:44:25 +02:00
Elian Doran
daf41804d4 chore(core): address requested changes 2026-01-12 21:43:57 +02:00
Elian Doran
43d087f886 chore(deps): update lock file 2026-01-12 21:32:06 +02:00
Elian Doran
503a6e520d Merge remote-tracking branch 'origin/main' into lightweight/decouple_server_api 2026-01-12 21:31:32 +02:00
Elian Doran
52610a7410 fix(client-standalone): missing manifest 2026-01-12 21:06:00 +02:00
Elian Doran
c7edb71fed fix(client-standalone): missing favicon 2026-01-12 21:05:21 +02:00
Elian Doran
83db37ed31 fix(server): app-info not showing data dir 2026-01-12 21:03:55 +02:00
Elian Doran
0d1c8ae01e fix(server): login not working due to bad import to i18n 2026-01-12 20:55:32 +02:00
Elian Doran
92f71e100f chore(core): integrate app_info route 2026-01-12 20:54:18 +02:00
Elian Doran
659573b864 fix(client-standalone): update version to match 2026-01-12 20:50:12 +02:00
Elian Doran
e1c798561b fix(client-standalone): user guide not working 2026-01-12 20:46:08 +02:00
Elian Doran
0c52b56e02 chore(core): integrate branches service and route 2026-01-12 19:25:45 +02:00
Elian Doran
f9731d9cfc chore(text): re-enable emojis 2026-01-12 19:00:35 +02:00
Elian Doran
7547371ba0 feat(client-standalone): proper integration of server-side locale 2026-01-12 18:44:48 +02:00
Elian Doran
84e1d45d2a fix(client-standalone): print not working 2026-01-11 23:05:27 +02:00
Elian Doran
364c9cda27 chore(client-standalone): reduce verbosity in logs for requests 2026-01-11 23:05:26 +02:00
Elian Doran
af944c29a8 feat(client-standalone): support more globals 2026-01-11 23:04:53 +02:00
Elian Doran
45577f1585 feat(client-standalone): better device detection 2026-01-11 23:04:53 +02:00
Elian Doran
1648c67467 feat(client-standalone): initialize server-side translations 2026-01-11 22:46:47 +02:00
Elian Doran
882793e794 chore(client-standalone): basic support for mobile 2026-01-11 18:29:47 +02:00
Elian Doran
4a4a7d79c2 chore(client-standalone): integrate faster preact 2026-01-11 17:52:56 +02:00
Elian Doran
a955eb80da chore(client-standalone): integrate main client script 2026-01-11 17:34:25 +02:00
Elian Doran
cd64a1ee18 chore(client-standalone): fix noscript 2026-01-11 17:31:15 +02:00
Elian Doran
9894d4256c chore(deps): update lock 2026-01-11 17:31:07 +02:00
Elian Doran
3b5f1dabd6 Merge remote-tracking branch 'origin/lightweight/decouple_server_api' into lightweight/browser_api 2026-01-11 17:21:37 +02:00
Elian Doran
750fa2e647 Merge remote-tracking branch 'origin/main' into lightweight/decouple_server_api 2026-01-11 17:15:35 +02:00
Elian Doran
4f0021e44e Merge remote-tracking branch 'origin/main' into lightweight/browser_api
; Conflicts:
;	apps/client/src/widgets/layout/StatusBar.tsx
2026-01-07 19:41:51 +02:00
Elian Doran
2546e4c0dc fix(client): server worker in client 2026-01-07 18:29:00 +02:00
Elian Doran
eac5dbb210 chore(client-standalone): async-proxy missing in prod 2026-01-07 17:58:12 +02:00
Elian Doran
8b6da981f7 chore(client-standalone): try to use plain header file 2026-01-07 17:49:25 +02:00
Elian Doran
7433ca069f chore(client-standalone): wrong file name to CORS 2026-01-07 17:35:37 +02:00
Elian Doran
128049b672 chore(core): integrate icon usage API 2026-01-07 17:33:44 +02:00
Elian Doran
0eb3cb1118 feat(client-standalone): proper startup without requiring refresh 2026-01-07 17:19:52 +02:00
Elian Doran
8fc28716a7 feat(client-standalone): set up CORS for Cloudflare Pages 2026-01-07 17:14:31 +02:00
Elian Doran
af346f455a fix(client-standalone): version check was broken 2026-01-07 16:53:37 +02:00
Elian Doran
3e5a6c1e51 chore(client-standalone): fake two more routes 2026-01-07 16:43:17 +02:00
Elian Doran
9e3b4435cd fix(client): request to recent changes for undefined note 2026-01-07 16:43:11 +02:00
Elian Doran
3a793a3549 chore(client-standalone): fake two more routes 2026-01-07 16:41:19 +02:00
Elian Doran
4f139552f4 chore(core): integrate recent-notes 2026-01-07 16:41:08 +02:00
Elian Doran
13f25e9fed chore(client-standalone): integrate note map backlink count 2026-01-07 16:36:49 +02:00
Elian Doran
91db73703b chore(client-standalone): add two dummy routes 2026-01-07 16:32:29 +02:00
Elian Doran
d690985b58 fix(client): SQL schemas loaded even when not needed 2026-01-07 16:27:48 +02:00
Elian Doran
b5bcf73531 chore(client-standalone): bring back window.global 2026-01-07 16:21:35 +02:00
Elian Doran
2e905c8292 fix(deps): lock file out of sync 2026-01-07 16:06:15 +02:00
Elian Doran
4374c92032 feat(ci): add deployment script for standalone client 2026-01-07 16:04:04 +02:00
Elian Doran
edde0d0f90 fix(client-standalone): get it to start in prod 2026-01-07 15:50:34 +02:00
Elian Doran
32c39384ff fix(client-standalone): missing entry point for sw, local-bridge, local-server-worker 2026-01-07 15:20:59 +02:00
Elian Doran
807ab4be8c fix(client-standalone): build missing .wasm 2026-01-07 15:16:38 +02:00
Elian Doran
4da20f4829 fix(client-standalone): some assets could not be loaded 2026-01-07 15:11:01 +02:00
Elian Doran
cb5b491633 fix(client-standalone): get client scripts to run 2026-01-07 14:42:02 +02:00
Elian Doran
e76c33c37a chore(client-standalone): relocate index file to root 2026-01-07 14:34:41 +02:00
Elian Doran
89fc89603e chore(client-standalone): set up live reload for assets 2026-01-07 14:30:29 +02:00
Elian Doran
c0bf294457 chore(client-standalone): basic integration for assets 2026-01-07 14:29:23 +02:00
Elian Doran
24e076cacf chore(client-standalone): integrate new files from client 2026-01-07 14:22:04 +02:00
Elian Doran
1e381b13ca chore(client-standalone): create empty project 2026-01-07 14:14:52 +02:00
Elian Doran
f83121ce1d chore(core): integrate attachments route 2026-01-07 13:48:59 +02:00
Elian Doran
b32480f1d3 feat(client/lightweight): basic WS support 2026-01-07 13:42:42 +02:00
Elian Doran
d4468bd97b feat(client/lightweight): basic OPFS support for persistence 2026-01-07 13:27:17 +02:00
Elian Doran
e8711d7cd5 fix(client/lightweight): not handling returning backend entities 2026-01-07 13:04:24 +02:00
Elian Doran
35f4d2aaad chore(client/lightweight): improve route error handling 2026-01-07 12:55:58 +02:00
Elian Doran
b1f3fe5345 fix(client/lightweight): saving not working 2026-01-07 12:53:07 +02:00
Elian Doran
9f1b0ac449 fix(client/lightweight): saved statements causing issues 2026-01-07 12:41:08 +02:00
Elian Doran
a84e804fc3 fix(client/lightweight): CLS not available in routes 2026-01-07 12:37:29 +02:00
Elian Doran
3371a31c70 fix(client/lightweight): crypto hash not working 2026-01-07 12:32:45 +02:00
Elian Doran
724af8e103 fix(client/lightweight): statements with parameters not working 2026-01-07 12:21:27 +02:00
Elian Doran
c5803a2650 fix(client/lightweight): missing pluck implementation 2026-01-07 12:16:09 +02:00
Elian Doran
baf18835be fix(client/lightweight): SQL nested transactions not supported 2026-01-07 12:14:30 +02:00
Elian Doran
3d1c93e58c fix(client/lightweight): note content not rendering 2026-01-07 12:07:49 +02:00
Elian Doran
ab0800a9f3 chore(core): integrate notes route 2026-01-07 12:00:38 +02:00
Elian Doran
dd58eac4b0 fix(client/lightweight): boxicons not loading 2026-01-07 11:50:25 +02:00
Elian Doran
c6d1457ad7 refactor(client/lightweight): bootstrap route as part of the new router 2026-01-07 11:48:22 +02:00
Elian Doran
f05fda871c chore(core): integrate icon_packs service 2026-01-07 11:45:40 +02:00
Elian Doran
22590596da feat(core): shared router between lightweight and server 2026-01-07 11:37:50 +02:00
Elian Doran
8274f9a220 feat(client): lightweight router implementation 2026-01-07 11:30:52 +02:00
Elian Doran
b19bf62d7e chore(client): bypass autocomplete count for now 2026-01-07 11:26:31 +02:00
Elian Doran
7b436bdf70 chore(client): vite not reloading core module 2026-01-07 11:24:04 +02:00
Elian Doran
a1c4a17d64 chore(core): integrate keyboard actions route 2026-01-07 11:23:46 +02:00
Elian Doran
7966cfd09c chore(client/lightweight): wait for becca to load before processing requests 2026-01-07 11:13:25 +02:00
Elian Doran
0fe299250e chore(client/lightweight): tree route import not seen 2026-01-07 11:07:23 +02:00
Elian Doran
adfe490480 chore(core): fix import 2026-01-07 10:43:02 +02:00
Elian Doran
872ab0864b chore(client/lightweight): handle routes properly 2026-01-06 23:19:41 +02:00
Elian Doran
6633b4233d chore(client/lightweight): initialize database earlier 2026-01-06 23:05:40 +02:00
Elian Doran
a2d873d16f chore(client/lightweight): port tree integration 2026-01-06 22:59:18 +02:00
Elian Doran
a6f52fff3e fix(client/lightweight): raw SQL queries not working 2026-01-06 22:20:53 +02:00
Elian Doran
7832f20c89 feat(client/lightweight): import demo database 2026-01-06 21:45:02 +02:00
Elian Doran
405db7cedb chore(client/lightweight): fix errors in SQL provider & implement crypto provider 2026-01-06 21:05:53 +02:00
Elian Doran
ccf4df8e86 chore(client/lightweight): basic SQL implementation 2026-01-06 20:59:52 +02:00
Elian Doran
1beda05e6c chore(client/lightweight): basic CLS implementation 2026-01-06 20:59:49 +02:00
Elian Doran
18a3d9d71a fix(client/lightweight): TypeScript not processed 2026-01-06 20:39:11 +02:00
Elian Doran
25dc9201bf feat(client/lightweight): improve error handling 2026-01-06 20:27:35 +02:00
Elian Doran
b60501dd3f chore(core) integrate options route 2026-01-06 20:12:03 +02:00
Elian Doran
cbd2fc3966 chore(client/lightweight): fix asset and API base path 2026-01-06 19:41:31 +02:00
Elian Doran
9bce12a85b Merge remote-tracking branch 'origin/lightweight/decouple_server_api' into lightweight/browser_api 2026-01-06 19:33:35 +02:00
Elian Doran
8523c369e1 fix(server): imports preventing start-up 2026-01-06 19:15:53 +02:00
Elian Doran
7c16aeca4a chore(core): crash due to dbReady before CLS init 2026-01-06 16:30:45 +02:00
Elian Doran
8399600e79 chore(core): address some missing methods in utils 2026-01-06 16:29:30 +02:00
Elian Doran
edac58f3fa chore(core): integrate revisions 2026-01-06 16:24:14 +02:00
Elian Doran
51d0d848c5 chore(core): no-op request service 2026-01-06 16:22:47 +02:00
Elian Doran
1edab8e8da chore(core): no-op image service 2026-01-06 16:21:42 +02:00
Elian Doran
e1e294914a chore(core): no-op search 2026-01-06 16:20:10 +02:00
Elian Doran
4668fdc15c chore(core): no-op sqlInit 2026-01-06 16:18:06 +02:00
Elian Doran
f1e0d5558c chore(core): integrate erase 2026-01-06 16:16:54 +02:00
Elian Doran
c94c54c641 chore(core): integrate task_context with ws no-op 2026-01-06 16:09:21 +02:00
Elian Doran
18416eb89a chore(core): no op script 2026-01-06 16:07:30 +02:00
Elian Doran
263c9028e2 chore(core): integrate hidden_subtree 2026-01-06 16:05:16 +02:00
Elian Doran
0b528e9937 chore(core): integrate handlers 2026-01-06 15:57:36 +02:00
Elian Doran
e905c1ec11 chore(core): integrate cloning service 2026-01-06 15:52:37 +02:00
Elian Doran
ecb27fe9f7 chore(core): integrate tree service 2026-01-06 15:48:48 +02:00
Elian Doran
a8f6db4b20 chore(core): fix some imports 2026-01-06 15:45:07 +02:00
Elian Doran
78262e55ec chore(core): integrate escape/unescape & toMap 2026-01-06 15:43:36 +02:00
Elian Doran
c6197e520d chore(core): integrate some more utils 2026-01-06 15:41:34 +02:00
Elian Doran
299c06c1a6 chore(core): fix inaccessible NoteParams 2026-01-06 15:33:48 +02:00
Elian Doran
674593b38c chore(core): integrate html_sanitizer 2026-01-06 15:31:22 +02:00
Elian Doran
f5535657ad chore(core): port note_types 2026-01-06 15:17:10 +02:00
Elian Doran
de4d07e904 chore(core): get rid of note_interface 2026-01-06 15:15:12 +02:00
Elian Doran
5508b505c8 chore(core): port notes service partially 2026-01-06 15:14:08 +02:00
Elian Doran
8cdfc108ba fix(core): wrong imports to src 2026-01-06 13:52:57 +02:00
Elian Doran
6a0f6fab83 fix(core): server not starting due to crypto not initialized 2026-01-06 13:50:22 +02:00
Elian Doran
ad3be73e1b chore(core): integrate note_set 2026-01-06 13:45:53 +02:00
Elian Doran
64b212b93e chore(core): integrate entity_changes 2026-01-06 13:42:29 +02:00
Elian Doran
60cb8d950e chore(core): integrate promoted_attribute_definition_parser 2026-01-06 13:30:21 +02:00
Elian Doran
61f6f94295 chore(core): integrate sanitize_attribute_name 2026-01-06 13:26:19 +02:00
Elian Doran
ebe7276f40 chore(core): fix some use of logs 2026-01-06 13:21:39 +02:00
Elian Doran
26d299aa44 chore(core): integrate options, options_init & keyboard_actions 2026-01-06 13:20:42 +02:00
Elian Doran
bd45c32251 chore(core): integrate utils partially 2026-01-06 13:06:14 +02:00
Elian Doran
321558a01f chore(core): fix minor type issue 2026-01-06 12:43:45 +02:00
Elian Doran
f5a77477aa chore(core): fix missing CLS method 2026-01-06 12:42:35 +02:00
Elian Doran
20c90d1296 chore(server): fix incompatibility with Uint8Array 2026-01-06 12:40:43 +02:00
Elian Doran
bbfef0315f chore(core): fix incompatibility with Uint8Array 2026-01-06 12:34:16 +02:00
Elian Doran
321fcf34f2 chore(core): fix references to getHoistedNoteId 2026-01-06 12:29:13 +02:00
Elian Doran
b9a59fe0c4 chore(core): fixs some imports to protected_session 2026-01-06 12:27:00 +02:00
Elian Doran
01f3c32d92 refactor(server): remove Blob interface in favor of BlobRow 2026-01-06 12:24:09 +02:00
Elian Doran
05b9e2ec2a chore(core): fix references to core 2026-01-06 12:20:01 +02:00
Elian Doran
c8d3b091fd chore(commons): fix Node reference 2026-01-06 12:19:42 +02:00
Elian Doran
d717a89163 chore(core): fix references to Buffer 2026-01-06 12:16:38 +02:00
Elian Doran
8149460547 chore(commons): fix issues with Buffer 2026-01-06 12:07:16 +02:00
Elian Doran
b7ad76827a chore(server): various references to core 2026-01-06 12:05:52 +02:00
Elian Doran
e19e9b3830 chore(core): fix references to blob-service 2026-01-06 12:01:18 +02:00
Elian Doran
40b07c3e8a chore(core): fix references to becca-interface 2026-01-06 11:59:45 +02:00
Elian Doran
a15b84b4e5 chore(core): fix type error in getFlatText 2026-01-06 11:56:52 +02:00
Elian Doran
544c52931c chore(server): fix references to abstract becca entity 2026-01-06 11:55:10 +02:00
Elian Doran
9391159413 chore(server): fix references to becca service 2026-01-06 11:52:25 +02:00
Elian Doran
f9e22a9ba9 chore(server): fix references to becca loader 2026-01-06 11:49:22 +02:00
Elian Doran
f88ac5dfae chore(server): fix imports to becca entities 2026-01-06 11:46:15 +02:00
Elian Doran
3459d2906e chore(server): fix imports to validation error 2026-01-06 11:41:06 +02:00
Elian Doran
4506b717d5 chore(server): fix imports to becca 2026-01-06 11:38:25 +02:00
Elian Doran
af8744ef2a chore(core): integrate errors 2026-01-06 11:31:13 +02:00
Elian Doran
320d8e3b45 chore(core): partially integrate becca 2026-01-06 11:23:52 +02:00
Elian Doran
c20da77f83 chore(core): integrate events service 2026-01-06 11:10:09 +02:00
Elian Doran
14e2e85da7 chore(core): integrate date_utils 2026-01-06 11:03:33 +02:00
Elian Doran
c7f0d541c2 fix(server): blob errors out 2026-01-06 10:41:50 +02:00
Elian Doran
5d474150da feat(client/lightweight): integrate SQLite 2026-01-05 20:00:00 +02:00
Elian Doran
d3941752f1 chore(client/lightweight): disable caching for now 2026-01-05 19:10:25 +02:00
Elian Doran
56b305b1de fix(client/lightweight): html aggressively cached 2026-01-05 18:50:09 +02:00
Elian Doran
bde472d649 feat(client/standalone): basic service worker attempt 2026-01-05 18:35:14 +02:00
Elian Doran
c1548b0f54 chore(server): integrate data_encryption, and protected_session 2026-01-05 17:47:25 +02:00
Elian Doran
6f04738629 chore(core): add documentation for SQL 2026-01-05 16:07:17 +02:00
Elian Doran
f79af7b045 fix(server): request content empty due to CLS 2026-01-05 16:01:27 +02:00
Elian Doran
527f502083 fix(server): requests failing due to cls namespacing issue 2026-01-05 15:58:24 +02:00
Elian Doran
d61e2c6f2c chore(server): get DB to be loaded 2026-01-05 15:52:31 +02:00
Elian Doran
ea31d2f446 chore(core): basic integration of SQL + CLS + log 2026-01-05 15:45:45 +02:00
Elian Doran
62803a1817 chore(server): set up dependency to trilium-core 2026-01-05 14:42:32 +02:00
Elian Doran
a67464b4a0 refactor(server): decouple bettersqlite3 from sql service 2026-01-05 14:03:03 +02:00
Elian Doran
00e7482968 chore(core): create empty package 2026-01-05 12:26:13 +02:00
Elian Doran
b9cef158d8 Merge remote-tracking branch 'origin/main' into feat/add-ocr-capabilities 2025-07-31 08:25:30 +03:00
Elian Doran
a3a3b3cb5c Merge remote-tracking branch 'origin/main' into renovate/csrf-csrf-4.x 2025-07-26 15:49:50 +03:00
Elian Doran
5ec6141369 feat(ocr): filter out text based on confidence 2025-07-26 14:57:12 +03:00
Elian Doran
55ac1e01f2 chore(ocr): improve ocr search result style 2025-07-26 14:15:45 +03:00
Elian Doran
65b58c3668 feat(ocr): auto-process images only if enabled in settings 2025-07-26 14:12:22 +03:00
Elian Doran
2cb4e5e8dc feat(ocr): run the image operation in the background 2025-07-26 14:07:23 +03:00
Elian Doran
72cea245f1 feat(ocr): automatically process images 2025-07-26 14:00:35 +03:00
Elian Doran
08ca86c68a chore(deps): move workspace dependencies to server 2025-07-26 13:48:28 +03:00
Elian Doran
925c9c1e7b feat(ocr): display OCR text only in search results 2025-07-26 12:55:52 +03:00
Elian Doran
6212ea0304 feat(ocr): display OCR text in search results 2025-07-26 12:41:30 +03:00
Elian Doran
f295592134 fix(ocr): search error due to scoring 2025-07-26 12:33:45 +03:00
Elian Doran
69b0973e6d feat(ocr): add a button to trigger an OCR manually 2025-07-26 12:18:20 +03:00
Elian Doran
422d318dac feat(ocr): add an option to display OCR text 2025-07-26 12:08:04 +03:00
Elian Doran
c55aa6ee88 refactor(ocr): unnecessary initialization logic 2025-07-26 11:56:48 +03:00
Elian Doran
090b175152 refactor(ocr): deduplicate mime types partially 2025-07-26 11:51:53 +03:00
Elian Doran
11e9b097a2 feat(ocr): basic processing of new files 2025-07-26 11:46:28 +03:00
Elian Doran
2adfc1d32b chore(ci): remove unnecessary change 2025-07-26 11:24:42 +03:00
Elian Doran
99fa5d89e7 Merge remote-tracking branch 'origin/main' into feat/add-ocr-capabilities 2025-07-26 10:33:01 +03:00
perf3ct
ca8cbf8ccf feat(ocr): add additional processors for OCR feature 2025-07-16 20:10:56 +00:00
perf3ct
6722d2d266 feat(ocr): implement new language selection form 2025-07-16 20:10:41 +00:00
perf3ct
508cbeaa1b feat(ocr): update this new migration to also add a ocr_last_processed column 2025-07-16 20:10:07 +00:00
perf3ct
e040865905 feat(ocr): add officeparser, pdf-parse, and sharp dependencies for ocr 2025-07-16 20:09:41 +00:00
perf3ct
a7878dd2c6 Merge branch 'main' into feat/add-ocr-capabilities 2025-07-16 17:54:32 +00:00
Jon Fuller
02980834ad Merge branch 'main' into feat/add-ocr-capabilities 2025-07-15 10:10:47 -07:00
perf3ct
2a8c8871c4 fix(dev): resolve issues with pnpm-lock.yaml 2025-07-14 16:41:02 +00:00
perf3ct
893be24c1d merge main into feature branch 2025-07-14 16:38:22 +00:00
perf3ct
9029f59410 feat(ocr): swap from custom table to using the blobs table, with a new column 2025-07-14 16:15:15 +00:00
Jon Fuller
d4aaf4ca9b Merge branch 'develop' into renovate/csrf-csrf-4.x 2025-06-11 12:44:51 -07:00
Jon Fuller
4b5e8d33a6 Update playwright.yml 2025-06-10 15:37:05 -07:00
perf3ct
09196c045f fix(ocr): obviously don't need this migration file anymore 2025-06-10 20:59:17 +00:00
perf3ct
7868ebec1e fix(unit): also fix broken llm test 2025-06-10 20:51:34 +00:00
perf3ct
80a9182f05 feat(unit): ocr tests almost pass... 2025-06-10 20:41:40 +00:00
perf3ct
d20b3d854f feat(unit): ocr tests almost pass... 2025-06-10 20:36:52 +00:00
perf3ct
f1356228a3 feat(unit): ocr unit tests almost pass 2025-06-10 20:22:31 +00:00
perf3ct
a4adc51e50 fix(unit): resolve typecheck errors 2025-06-10 19:48:48 +00:00
perf3ct
864543e4f9 feat(ocr): drop confidence down a little bit 2025-06-10 19:22:46 +00:00
perf3ct
33a549202b fix(package): referenced wrong tesseract.js lol 2025-06-10 19:19:17 +00:00
perf3ct
c4a0219b18 feat(ocr): add unit tests, resolve double sent headers, and fix the wonderful tesseract.js path issues 2025-06-10 19:12:50 +00:00
Elian Doran
e7450b5143 Merge branch 'develop' into renovate/csrf-csrf-4.x 2025-06-08 14:31:55 +03:00
Elian Doran
fd90454eb6 Merge branch 'develop' into renovate/csrf-csrf-4.x 2025-05-17 09:51:02 +03:00
Elian Doran
f327b54c0e feat(csrf): use different token to avoid issues with old token 2025-05-16 19:45:32 +03:00
Elian Doran
f38105ef05 Merge remote-tracking branch 'origin/develop' into renovate/csrf-csrf-4.x 2025-05-16 19:34:19 +03:00
Elian Doran
6f6041ee7b fix(server): migrate csrf to v4 2025-05-15 20:39:31 +03:00
renovate[bot]
2c1517d259 chore(deps): update dependency csrf-csrf to v4 2025-05-15 16:12:11 +00:00
945 changed files with 65455 additions and 40871 deletions

View File

@@ -8,7 +8,7 @@ inputs:
runs:
using: composite
steps:
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:

View File

@@ -69,7 +69,7 @@ runs:
# Post github action comment
- name: Post comment
uses: marocchino/sticky-pull-request-comment@v2
uses: marocchino/sticky-pull-request-comment@v3
if: ${{ steps.bundleSize.outputs.hasDifferences == 'true' }} # post only in case of changes
with:
number: ${{ github.event.pull_request.number }}

View File

@@ -1,5 +1,7 @@
# Trilium Notes - AI Coding Agent Instructions
> **Note**: When updating this file, also update `CLAUDE.md` in the repository root to keep both AI coding assistants in sync.
## Project Overview
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. Built as a TypeScript monorepo using pnpm, it implements a three-layer caching architecture (Becca/Froca/Shaca) with a widget-based UI system and supports extensive user scripting capabilities.
@@ -115,6 +117,15 @@ class MyNoteWidget extends NoteContextAwareWidget {
**Important**: Widgets use jQuery (`this.$widget`) for DOM manipulation. Don't mix React patterns here.
### Reusable Preact Components
Common UI components are available in `apps/client/src/widgets/react/` — prefer reusing these over creating custom implementations:
- `NoItems` - Empty state placeholder with icon and message (use for "no results", "too many items", error states)
- `ActionButton` - Consistent button styling with icon support
- `FormTextBox` - Text input with validation and controlled input handling
- `Slider` - Range slider with label
- `Checkbox`, `RadioButton` - Form controls
- `CollapsibleSection` - Expandable content sections
## Development Workflow
### Running & Testing
@@ -186,6 +197,14 @@ When adding query parameters to ETAPI endpoints (`apps/server/src/etapi/`), main
**Auth note**: ETAPI uses basic auth with tokens. Internal API endpoints trust the frontend.
### Adding New LLM Tools
Tools are defined using `defineTools()` in `apps/server/src/services/llm/tools/` and automatically registered for both the LLM chat and MCP server.
1. Add the tool definition in the appropriate module (`note_tools.ts`, `attribute_tools.ts`, `hierarchy_tools.ts`) or create a new module
2. Each tool needs: `description`, `inputSchema` (Zod), `execute` function, and optionally `mutates: true` for write operations or `needsContext: true` for tools that need the current note context
3. If creating a new module, wrap tools in `defineTools({...})` and add the registry to `allToolRegistries` in `tools/index.ts`
4. Add a client-side friendly name in `apps/client/src/translations/en/translation.json` under `llm.tools.<tool_name>` — use **imperative tense** (e.g. "Search notes", "Create note", "Get attributes"), not present continuous
### Database Migrations
- Add scripts in `apps/server/src/migrations/YYMMDD_HHMM__description.sql`
- Update schema in `apps/server/src/assets/db/schema.sql`
@@ -213,6 +232,12 @@ When adding query parameters to ETAPI endpoints (`apps/server/src/etapi/`), main
10. **Attribute inheritance can be complex** - When checking for labels/relations, use `note.getOwnedAttribute()` for direct attributes or `note.getAttribute()` for inherited ones. Don't assume attributes are directly on the note.
## MCP Server
- Trilium exposes an MCP (Model Context Protocol) server at `http://localhost:8080/mcp`, configured in `.mcp.json`
- The MCP server is **only available when the Trilium server is running** (`pnpm run server:start`)
- It provides tools for reading, searching, and modifying notes directly from the AI assistant
- Use it to interact with actual note data when developing or debugging note-related features
## TypeScript Configuration
- **Project references**: Monorepo uses TypeScript project references (`tsconfig.json`)
@@ -275,6 +300,12 @@ View types are configured via `#viewType` label (e.g., `#viewType=table`). Each
- Register in `packages/ckeditor5/src/plugins.ts`
- See `ckeditor5-admonition`, `ckeditor5-footnotes`, `ckeditor5-math`, `ckeditor5-mermaid` for examples
### Updating PDF.js
1. Update `pdfjs-dist` version in `packages/pdfjs-viewer/package.json`
2. Run `npx tsx scripts/update-viewer.ts` from that directory
3. Run `pnpm build` to verify success
4. Commit all changes including updated viewer files
### Database Migrations
- Add migration scripts in `apps/server/src/migrations/YYMMDD_HHMM__description.sql`
- Update schema in `apps/server/src/assets/db/schema.sql`
@@ -299,9 +330,29 @@ Trilium provides powerful user scripting capabilities:
- Translation files in `apps/client/src/translations/`
- Use translation system via `t()` function
- Automatic pluralization: Add `_other` suffix to translation keys (e.g., `item` and `item_other` for singular/plural)
- When a translated string contains **interpolated components** (e.g. links, note references) whose order may vary across languages, use `<Trans>` from `react-i18next` instead of `t()`. This lets translators reorder components freely (e.g. `"<Note/> in <Parent/>"` vs `"in <Parent/>, <Note/>"`)
- When adding a new locale, follow the step-by-step guide in `docs/Developer Guide/Developer Guide/Concepts/Internationalisation Translations/Adding a new locale.md`
#### Client vs Server Translation Usage
- **Client-side**: `import { t } from "../services/i18n"` with keys in `apps/client/src/translations/en/translation.json`
- **Server-side**: `import { t } from "i18next"` with keys in `apps/server/src/assets/translations/en/server.json`
- **Interpolation**: Use `{{variable}}` for normal interpolation; use `{{- variable}}` (with hyphen) for **unescaped** interpolation when the value contains special characters like quotes that shouldn't be HTML-escaped
### Storing User Preferences
- **Do not use `localStorage`** for user preferences — Trilium has a synced options system that persists across devices
- To add a new user preference:
1. Add the option type to `OptionDefinitions` in `packages/commons/src/lib/options_interface.ts`
2. Add a default value in `apps/server/src/services/options_init.ts` in the `defaultOptions` array
3. **Whitelist the option** in `apps/server/src/routes/api/options.ts` by adding it to `ALLOWED_OPTIONS` (required for client updates)
4. Use `useTriliumOption("optionName")` hook in React components to read/write the option
- Available hooks: `useTriliumOption` (string), `useTriliumOptionBool`, `useTriliumOptionInt`, `useTriliumOptionJson`
- See `docs/Developer Guide/Developer Guide/Concepts/Options/Creating a new option.md` for detailed documentation
## Testing Conventions
- **Write concise tests**: Group related assertions together in a single test case rather than creating many one-shot tests
- **Extract and test business logic**: When adding pure business logic (e.g., data transformations, migrations, validations), extract it as a separate function and always write unit tests for it
```typescript
// ETAPI test pattern
describe("etapi/feature", () => {

70
.github/workflows/deploy-app.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
name: Deploy Standalone App
on:
# Trigger on push to main branch
push:
branches:
- standalone
# Only run when app files change
paths:
- 'apps/client/**'
- 'apps/client-standalone/**'
- 'packages/trilium-core/**'
- '.github/workflows/deploy-app.yml'
# Allow manual triggering from Actions tab
workflow_dispatch:
# Run on pull requests for preview deployments
pull_request:
paths:
- 'apps/client/**'
- 'apps/client-standalone/**'
- 'packages/trilium-core/**'
- '.github/workflows/deploy-app.yml'
jobs:
build-and-deploy:
name: Build and Deploy App
runs-on: ubuntu-latest
timeout-minutes: 10
# Required permissions for deployment
permissions:
contents: read
deployments: write
pull-requests: write # For PR preview comments
id-token: write # For OIDC authentication (if needed)
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Update build info
run: pnpm run chore:update-build-info
- name: Trigger build of app
run: pnpm --filter=client-standalone build
- name: Deploy
uses: ./.github/actions/deploy-to-cloudflare-pages
if: github.repository == vars.REPO_MAIN
with:
project_name: "trilium-app"
comment_body: "🖥️ App preview is ready"
production_url: "https://app.triliumnotes.org"
deploy_dir: "apps/client-standalone/dist"
cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare_account_id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -45,7 +45,7 @@ jobs:
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v5
- name: Setup Node.js
uses: actions/setup-node@v6

View File

@@ -1,9 +1,15 @@
name: Dev
on:
push:
branches: [ main ]
branches:
- main
- standalone
- "release/*"
pull_request:
branches: [ main ]
branches:
- main
- standalone
- "release/*"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -26,7 +32,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
@@ -59,13 +65,20 @@ jobs:
path: apps/server/test-output/vitest/html/
retention-days: 30
- name: Run the client-standalone tests
# Runs the same trilium-core spec set as the server suite, but in
# happy-dom + sql.js WASM via BrowserSqlProvider (see
# apps/client-standalone/src/test_setup.ts). Catches differences
# between the Node-side and browser-side runtimes.
run: pnpm run --filter=client-standalone test
- name: Run CKEditor e2e tests
run: |
pnpm run --filter=ckeditor5-mermaid test
pnpm run --filter=ckeditor5-math test
- name: Run the rest of the tests
run: pnpm run --filter=\!client --filter=\!server --filter=\!ckeditor5-mermaid --filter=\!ckeditor5-math test
run: pnpm run --filter=\!client --filter=\!client-standalone --filter=\!server --filter=\!ckeditor5-mermaid --filter=\!ckeditor5-math test
build_docker:
name: Build Docker image
@@ -74,7 +87,7 @@ jobs:
- test_dev
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Update build info
@@ -89,7 +102,7 @@ jobs:
key: ${{ secrets.RELATIVE_CI_CLIENT_KEY }}
- name: Trigger server build
run: pnpm run server:build
- uses: docker/setup-buildx-action@v3
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v7
with:
context: apps/server
@@ -109,7 +122,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -124,7 +137,7 @@ jobs:
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Build and export to Docker
uses: docker/build-push-action@v7

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:

View File

@@ -40,9 +40,9 @@ jobs:
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
@@ -142,7 +142,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
@@ -178,7 +178,7 @@ jobs:
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to GHCR
uses: docker/login-action@v4

View File

@@ -61,7 +61,7 @@ jobs:
runs-on: ${{ matrix.os.image }}
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
@@ -69,6 +69,8 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
env:
npm_config_package_import_method: copy
- name: Update nightly version
run: pnpm run chore:ci-update-nightly-version
- name: Run the build
@@ -91,7 +93,7 @@ jobs:
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
- name: Publish release
uses: softprops/action-gh-release@v2.5.0
uses: softprops/action-gh-release@v2.6.1
if: ${{ github.event_name != 'pull_request' }}
with:
make_latest: false
@@ -132,7 +134,7 @@ jobs:
arch: ${{ matrix.arch }}
- name: Publish release
uses: softprops/action-gh-release@v2.5.0
uses: softprops/action-gh-release@v2.6.1
if: ${{ github.event_name != 'pull_request' }}
with:
make_latest: false

View File

@@ -38,7 +38,7 @@ jobs:
filter: tree:0
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: 24

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
@@ -66,7 +66,7 @@ jobs:
runs-on: ${{ matrix.os.image }}
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
@@ -150,7 +150,7 @@ jobs:
path: upload
- name: Publish stable release
uses: softprops/action-gh-release@v2.5.0
uses: softprops/action-gh-release@v2.6.1
with:
draft: false
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md

View File

@@ -32,7 +32,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
@@ -58,7 +58,7 @@ jobs:
compression-level: 0
- name: Release web clipper extension
uses: softprops/action-gh-release@v2.5.0
uses: softprops/action-gh-release@v2.6.1
if: ${{ startsWith(github.ref, 'refs/tags/web-clipper-v') }}
with:
draft: false

View File

@@ -26,7 +26,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:

4
.gitignore vendored
View File

@@ -46,9 +46,11 @@ upload
/.direnv
/result
.svelte-kit
# docs
site/
apps/*/coverage
scripts/translation/.language*.json
# AI
.claude/settings.local.json

8
.mcp.json Normal file
View File

@@ -0,0 +1,8 @@
{
"mcpServers": {
"trilium": {
"type": "http",
"url": "http://localhost:8080/mcp"
}
}
}

2
.nvmrc
View File

@@ -1 +1 @@
24.14.0
24.14.1

363
CLAUDE.md
View File

@@ -2,151 +2,318 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
> **Note**: When updating this file, also update `.github/copilot-instructions.md` to keep both AI coding assistants in sync.
## Overview
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. It's built as a TypeScript monorepo using pnpm, with multiple applications and shared packages.
Trilium Notes is a hierarchical note-taking application with synchronization, scripting, and rich text editing. TypeScript monorepo using pnpm with multiple apps and shared packages.
## Development Commands
### Setup
- `pnpm install` - Install all dependencies
- `corepack enable` - Enable pnpm if not available
```bash
# Setup
corepack enable && pnpm install
### Running Applications
- `pnpm run server:start` - Start development server (http://localhost:8080)
- `pnpm run server:start-prod` - Run server in production mode
# Run
pnpm server:start # Dev server at http://localhost:8080
pnpm desktop:start # Electron dev app
pnpm standalone:start # Standalone client dev
### Building
- `pnpm run client:build` - Build client application
- `pnpm run server:build` - Build server application
- `pnpm run electron:build` - Build desktop application
# Build
pnpm client:build # Frontend
pnpm server:build # Backend
pnpm desktop:build # Electron
### Testing
- `pnpm test:all` - Run all tests (parallel + sequential)
- `pnpm test:parallel` - Run tests that can run in parallel
- `pnpm test:sequential` - Run tests that must run sequentially (server, ckeditor5-mermaid, ckeditor5-math)
- `pnpm coverage` - Generate coverage reports
# Test
pnpm test:all # All tests (parallel + sequential)
pnpm test:parallel # Client + most package tests
pnpm test:sequential # Server, ckeditor5-mermaid, ckeditor5-math (shared DB)
pnpm --filter server test # Single package tests
pnpm coverage # Coverage reports
## Architecture Overview
# Lint & Format
pnpm dev:linter-check # ESLint check
pnpm dev:linter-fix # ESLint fix
pnpm dev:format-check # Format check (stricter stylistic rules)
pnpm dev:format-fix # Format fix
pnpm typecheck # TypeScript type check across all projects
```
### Monorepo Structure
- **apps/**: Runnable applications
- `client/` - Frontend application (shared by server and desktop)
- `server/` - Node.js server with web interface
- `desktop/` - Electron desktop application
- `web-clipper/` - Browser extension for saving web content
- Additional tools: `db-compare`, `dump-db`, `edit-docs`
**Running a single test file**: `pnpm --filter server test spec/etapi/search.spec.ts`
- **packages/**: Shared libraries
- `commons/` - Shared interfaces and utilities
- `ckeditor5/` - Custom rich text editor with Trilium-specific plugins
- `codemirror/` - Code editor customizations
- `highlightjs/` - Syntax highlighting
- Custom CKEditor plugins: `ckeditor5-admonition`, `ckeditor5-footnotes`, `ckeditor5-math`, `ckeditor5-mermaid`
## Main Applications
### Core Architecture Patterns
The four main apps share `packages/trilium-core/` for business logic but differ in runtime:
#### Three-Layer Cache System
- **Becca** (Backend Cache): Server-side entity cache (`apps/server/src/becca/`)
- **Froca** (Frontend Cache): Client-side mirror of backend data (`apps/client/src/services/froca.ts`)
- **Shaca** (Share Cache): Optimized cache for shared/published notes (`apps/server/src/share/`)
- **client** (`apps/client/`): Preact frontend with jQuery widget system. Shared UI layer used by both server and desktop.
- **server** (`apps/server/`): Node.js backend (Express, better-sqlite3). Serves the client and provides REST/WebSocket APIs.
- **desktop** (`apps/desktop/`): Electron wrapper around server + client, running both in a single process.
- **standalone** (`apps/client-standalone/` + `apps/standalone-desktop/`): Runs the entire stack in the browser — server logic compiled to WASM via sql.js, executed in a service worker. No Node.js dependency at runtime.
#### Entity System
Core entities are defined in `apps/server/src/becca/entities/`:
- `BNote` - Notes with content and metadata
- `BBranch` - Hierarchical relationships between notes (allows multiple parents)
- `BAttribute` - Key-value metadata attached to notes
- `BRevision` - Note version history
- `BOption` - Application configuration
## Monorepo Structure
#### Widget-Based UI
Frontend uses a widget system (`apps/client/src/widgets/`):
- `BasicWidget` - Base class for all UI components
- `NoteContextAwareWidget` - Widgets that respond to note changes
- `RightPanelWidget` - Widgets displayed in the right panel
```
apps/
client/ # Preact frontend (shared by server, desktop, standalone)
server/ # Node.js backend (Express, better-sqlite3)
desktop/ # Electron (bundles server + client)
client-standalone/ # Standalone client (WASM + service workers, no Node.js)
standalone-desktop/ # Standalone desktop variant
server-e2e/ # Playwright E2E tests for server
web-clipper/ # Browser extension
website/ # Project website
db-compare/, dump-db/, edit-docs/, build-docs/, icon-pack-builder/
packages/
trilium-core/ # Core business logic: entities, services, SQL, sync
commons/ # Shared interfaces and utilities
ckeditor5/ # Custom rich text editor bundle
codemirror/ # Code editor integration
highlightjs/ # Syntax highlighting
share-theme/ # Theme for shared/published notes
ckeditor5-admonition/, ckeditor5-footnotes/, ckeditor5-math/, ckeditor5-mermaid/
ckeditor5-keyboard-marker/, express-partial-content/, pdfjs-viewer/, splitjs/
turndown-plugin-gfm/
```
Use `pnpm --filter <package-name> <command>` to run commands in specific packages.
## Core Architecture
### Three-Layer Cache System
All data access goes through cache layers — never bypass with direct DB queries:
- **Becca** (`packages/trilium-core/src/becca/`): Server-side entity cache. Access via `becca.notes[noteId]`.
- **Froca** (`apps/client/src/services/froca.ts`): Client-side mirror synced via WebSocket. Access via `froca.getNote()`.
- **Shaca** (`apps/server/src/share/`): Optimized cache for shared/published notes.
**Critical**: Always use cache methods, not direct DB writes. Cache methods create `EntityChange` records needed for synchronization.
### Entity System
Core entities live in `packages/trilium-core/src/becca/entities/` (not `apps/server/`):
- `BNote` — Notes with content and metadata
- `BBranch` — Multi-parent tree relationships (cloning supported)
- `BAttribute` — Key-value metadata (labels and relations)
- `BRevision` — Version history
- `BOption` — Application configuration
- `BBlob` — Binary content storage
Entities extend `AbstractBeccaEntity<T>` with built-in change tracking, hash generation, and date management.
### Entity Change & Sync Protocol
Every entity modification creates an `EntityChange` record driving sync:
1. Login with HMAC authentication (document secret + timestamp)
2. Push changes → Pull changes → Push again (conflict resolution)
3. Content hash verification with retry loop
Sync services: `packages/trilium-core/src/services/sync.ts`, `syncMutexService`, `syncUpdateService`.
### Widget-Based UI
Frontend widgets in `apps/client/src/widgets/`:
- `BasicWidget` / `TypedBasicWidget` — Base classes (jQuery `this.$widget` for DOM)
- `NoteContextAwareWidget` — Responds to note changes
- `RightPanelWidget` — Sidebar widgets with position ordering
- Type-specific widgets in `type_widgets/` directory
#### API Architecture
- **Internal API**: REST endpoints in `apps/server/src/routes/api/`
- **ETAPI**: External API for third-party integrations (`apps/server/src/etapi/`)
- **WebSocket**: Real-time synchronization (`apps/server/src/services/ws.ts`)
**Widget lifecycle**: `doRenderBody()` for initial render, `refreshWithNote()` for note changes, `entitiesReloadedEvent({loadResults})` for entity updates. Uses jQuery — don't mix React patterns.
### Key Files for Understanding Architecture
#### Reusable Preact Components
Common UI components are available in `apps/client/src/widgets/react/` — prefer reusing these over creating custom implementations:
- `NoItems` - Empty state placeholder with icon and message (use for "no results", "too many items", error states)
- `ActionButton` - Consistent button styling with icon support
- `FormTextBox` - Text input with validation and controlled input handling
- `Slider` - Range slider with label
- `Checkbox`, `RadioButton` - Form controls
- `CollapsibleSection` - Expandable content sections
1. **Application Entry Points**:
- `apps/server/src/main.ts` - Server startup
- `apps/client/src/desktop.ts` - Client initialization
Fluent builder pattern: `.child()`, `.class()`, `.css()` chaining with position-based ordering.
2. **Core Services**:
- `apps/server/src/becca/becca.ts` - Backend data management
- `apps/client/src/services/froca.ts` - Frontend data synchronization
- `apps/server/src/services/backend_script_api.ts` - Scripting API
### API Architecture
3. **Database Schema**:
- `apps/server/src/assets/db/schema.sql` - Core database structure
- **Internal API** (`apps/server/src/routes/api/`): REST endpoints, trusts frontend
- **ETAPI** (`apps/server/src/etapi/`): External API with basic auth tokens — maintain backwards compatibility
- **WebSocket** (`apps/server/src/services/ws.ts`): Real-time sync
4. **Configuration**:
- `package.json` - Project dependencies and scripts
### Platform Abstraction
## Note Types and Features
`packages/trilium-core/src/services/platform.ts` defines `PlatformProvider` interface with implementations in `apps/desktop/`, `apps/server/`, and `apps/client-standalone/`. Singleton via `initPlatform()`/`getPlatform()`.
Trilium supports multiple note types, each with specialized widgets:
- **Text**: Rich text with CKEditor5 (markdown import/export)
- **Code**: Syntax-highlighted code editing with CodeMirror
- **File**: Binary file attachments
- **Image**: Image display with editing capabilities
- **Canvas**: Drawing/diagramming with Excalidraw
- **Mermaid**: Diagram generation
- **Relation Map**: Visual note relationship mapping
- **Web View**: Embedded web pages
- **Doc/Book**: Hierarchical documentation structure
**PlatformProvider** provides:
- `crash(message)` — Platform-specific fatal error handling
- `getEnv(key)` — Environment variable access (server/desktop use `process.env`, standalone maps URL query params like `?safeMode``TRILIUM_SAFE_MODE`)
- `isElectron`, `isMac`, `isWindows` — Platform detection flags
## Development Guidelines
**Critical rules for `trilium-core`**:
- **No `process.env` in core** — use `getPlatform().getEnv()` instead (not available in standalone/browser)
- **No `import path from "path"` in core** — Node's `path` module is externalized in browser builds. Use `packages/trilium-core/src/services/utils/path.ts` for `extname()`/`basename()` equivalents
- **No Node.js built-in modules in core** — core runs in both Node.js and the browser (standalone). Use platform-agnostic alternatives or platform providers
- **Platform detection via functions** — `isElectron()`, `isMac()`, `isWindows()` from `utils/index.ts` are functions (not constants) that call `getPlatform()`. They can only be called after `initializeCore()`, not at module top-level. If used in static definitions, wrap in a closure: `value: () => isWindows() ? "0.9" : "1.0"`
- **Barrel import caution** — `import { x } from "@triliumnext/core"` loads ALL core exports. Early-loading modules like `config.ts` should import specific subpaths (e.g. `@triliumnext/core/src/services/utils/index`) to avoid circular dependencies or initialization ordering issues
- **Electron IPC** — In desktop mode, client API calls use Electron IPC (not HTTP). The IPC handler in `apps/server/src/routes/electron.ts` must be registered via `utils.isElectron` from the **server's** utils (which correctly checks `process.versions["electron"]`), not from core's utils
### Binary Utilities
Use utilities from `packages/trilium-core/src/services/utils/binary.ts` for string/buffer conversions instead of manual `TextEncoder`/`TextDecoder` or `Buffer.from()` calls:
- **`wrapStringOrBuffer(input)`** — Converts `string` to `Uint8Array`, returns `Uint8Array` unchanged. Use when a function expects `Uint8Array` but receives `string | Uint8Array`.
- **`unwrapStringOrBuffer(input)`** — Converts `Uint8Array` to `string`, returns `string` unchanged. Use when a function expects `string` but receives `string | Uint8Array`.
- **`encodeBase64(input)`** / **`decodeBase64(input)`** — Base64 encoding/decoding that works in both Node.js and browser.
- **`encodeUtf8(string)`** / **`decodeUtf8(buffer)`** — UTF-8 encoding/decoding.
Import via `import { binary_utils } from "@triliumnext/core"` or directly from the module.
### Database
SQLite via `better-sqlite3`. SQL abstraction in `packages/trilium-core/src/services/sql/` with `DatabaseProvider` interface, prepared statement caching, and transaction support.
- Schema: `apps/server/src/assets/db/schema.sql`
- Migrations: `apps/server/src/migrations/YYMMDD_HHMM__description.sql`
### Testing Strategy
- Server tests run sequentially due to shared database
- Client tests can run in parallel
- E2E tests use Playwright for both server and desktop apps
- Build validation tests check artifact integrity
### Scripting System
Trilium provides powerful user scripting capabilities:
- Frontend scripts run in browser context
- Backend scripts run in Node.js context with full API access
- Script API documentation available in `docs/Script API/`
- **Write concise tests**: Group related assertions together in a single test case rather than creating many one-shot tests
- **Extract and test business logic**: When adding pure business logic (e.g., data transformations, migrations, validations), extract it as a separate function and always write unit tests for it
### Internationalization
- Translation files in `apps/client/src/translations/`
- Supported languages: English, German, Spanish, French, Romanian, Chinese
- **Only add new translation keys to `en/translation.json`** — translations for other languages are managed via Weblate and will be contributed by the community
- Third-party components (e.g., mind-map context menu) should use i18next `t()` for their labels, with the English strings added to `en/translation.json` under a dedicated namespace (e.g., `"mind-map"`)
- When a translated string contains **interpolated components** (e.g. links, note references) whose order may vary across languages, use `<Trans>` from `react-i18next` instead of `t()`. This lets translators reorder components freely (e.g. `"<Note/> in <Parent/>"` vs `"in <Parent/>, <Note/>"`)
- When adding a new locale, follow the step-by-step guide in `docs/Developer Guide/Developer Guide/Concepts/Internationalisation Translations/Adding a new locale.md`
- **Server-side translations** (e.g. hidden subtree titles) go in `apps/server/src/assets/translations/en/server.json`, not in the client `translation.json`
### Security Considerations
- Per-note encryption with granular protected sessions
- CSRF protection for API endpoints
- OpenID and TOTP authentication support
- Sanitization of user-generated content
#### Client vs Server Translation Usage
- **Client-side**: `import { t } from "../services/i18n"` with keys in `apps/client/src/translations/en/translation.json`
- **Server-side**: `import { t } from "i18next"` with keys in `apps/server/src/assets/translations/en/server.json`
- **Interpolation**: Use `{{variable}}` for normal interpolation; use `{{- variable}}` (with hyphen) for **unescaped** interpolation when the value contains special characters like quotes that shouldn't be HTML-escaped
## Common Development Tasks
### Electron Desktop App
- Desktop entry point: `apps/desktop/src/main.ts`, window management: `apps/server/src/services/window.ts`
- IPC communication: use `electron.ipcMain.on(channel, handler)` on server side, `electron.ipcRenderer.send(channel, data)` on client side
- Electron-only features should check `isElectron()` from `apps/client/src/services/utils.ts` (client) or `utils.isElectron` (server)
### Adding New Note Types
1. Create widget in `apps/client/src/widgets/type_widgets/`
2. Register in `apps/client/src/services/note_types.ts`
3. Add backend handling in `apps/server/src/services/notes.ts`
Three inheritance mechanisms:
1. **Standard**: `note.getInheritableAttributes()` walks parent tree
2. **Child prefix**: `child:label` on parent copies to children
3. **Template relation**: `#template=noteNoteId` includes template's inheritable attributes
### Extending Search
- Search expressions handled in `apps/server/src/services/search/`
- Add new search operators in search context files
### Attribute Inheritance
### Custom CKEditor Plugins
- Create new package in `packages/` following existing plugin structure
- Register in `packages/ckeditor5/src/plugins.ts`
Use `note.getOwnedAttribute()` for direct, `note.getAttribute()` for inherited.
### Client-Side API Restrictions
- **Do not use `crypto.randomUUID()`** or other Web Crypto APIs that require secure contexts - Trilium can run over HTTP, not just HTTPS
- Use `randomString()` from `apps/client/src/services/utils.ts` for generating IDs instead
### Storing User Preferences
- **Do not use `localStorage`** for user preferences — Trilium has a synced options system that persists across devices
- To add a new user preference:
1. Add the option type to `OptionDefinitions` in `packages/commons/src/lib/options_interface.ts`
2. Add a default value in `apps/server/src/services/options_init.ts` in the `defaultOptions` array
3. **Whitelist the option** in `apps/server/src/routes/api/options.ts` by adding it to `ALLOWED_OPTIONS` (required for client updates)
4. Use `useTriliumOption("optionName")` hook in React components to read/write the option
- Available hooks: `useTriliumOption` (string), `useTriliumOptionBool`, `useTriliumOptionInt`, `useTriliumOptionJson`
- See `docs/Developer Guide/Developer Guide/Concepts/Options/Creating a new option.md` for detailed documentation
### Shared Types Policy
- Types shared between client and server belong in `@triliumnext/commons` (`packages/commons/src/lib/`)
- Import shared types directly from `@triliumnext/commons` - do not re-export them from app-specific modules
- Keep app-specific types (e.g., `LlmProvider` for server, `StreamCallbacks` for client) in their respective apps
## Important Patterns
- **Protected notes**: Check `note.isContentAvailable()` before accessing content; use `note.getTitleOrProtected()` for safe title access
- **Long operations**: Use `TaskContext` for progress reporting via WebSocket
- **Event system** (`packages/trilium-core/src/services/events.ts`): Events emitted in order (notes → branches → attributes) during load for referential integrity
- **Search**: Expression-based, scoring happens in-memory — cannot add SQL-level LIMIT/OFFSET without losing scoring
- **Widget cleanup**: Unsubscribe from events in `cleanup()`/`doDestroy()` to prevent memory leaks
## Code Style
- 4-space indentation, semicolons always required
- Double quotes (enforced by format config)
- Max line length: 100 characters
- Unix line endings
- Import sorting via `eslint-plugin-simple-import-sort`
## Testing
- **Server tests** (`apps/server/spec/`): Vitest, must run sequentially (shared DB), forks pool, max 6 workers
- **Client tests** (`apps/client/src/`): Vitest with happy-dom environment, can run in parallel
- **E2E tests** (`apps/server-e2e/`): Playwright, Chromium, server started automatically on port 8082
- **ETAPI tests** (`apps/server/spec/etapi/`): External API contract tests
## Documentation
- `docs/Script API/` — Auto-generated, never edit directly
- `docs/User Guide/` — Edit via `pnpm edit-docs:edit-docs`, not manually
- `docs/Developer Guide/` and `docs/Release Notes/` — Safe for direct Markdown editing
## Key Entry Points
- `apps/server/src/main.ts` — Server startup
- `apps/client/src/desktop.ts` — Client initialization
- `packages/trilium-core/src/becca/becca.ts` — Backend data management
- `apps/client/src/services/froca.ts` — Frontend cache
- `apps/server/src/routes/routes.ts` — API route registration
- `packages/trilium-core/src/services/sql/sql.ts` — Database abstraction
### Adding Hidden System Notes
The hidden subtree (`_hidden`) contains system notes with predictable IDs (prefixed with `_`). Defined in `apps/server/src/services/hidden_subtree.ts` via the `HiddenSubtreeItem` interface from `@triliumnext/commons`.
1. Add the note definition to `buildHiddenSubtreeDefinition()` in `apps/server/src/services/hidden_subtree.ts`
2. Add a translation key for the title in `apps/server/src/assets/translations/en/server.json` under `"hidden-subtree"`
3. The note is auto-created on startup by `checkHiddenSubtree()` — uses deterministic IDs so all sync cluster instances generate the same structure
4. Key properties: `id` (must start with `_`), `title`, `type`, `icon` (format: `bx-icon-name` without `bx ` prefix), `attributes`, `children`, `content`
5. Use `enforceAttributes: true` to keep attributes in sync, `enforceBranches: true` for correct placement, `enforceDeleted: true` to remove deprecated notes
6. For launcher bar entries, see `hidden_subtree_launcherbar.ts`; for templates, see `hidden_subtree_templates.ts`
### Writing to Notes from Server Services
- `note.setContent()` requires a CLS (Continuation Local Storage) context — wrap calls in `cls.init(() => { ... })` (from `apps/server/src/services/cls.ts`)
- Operations called from Express routes already have CLS context; standalone services (schedulers, Electron IPC handlers) do not
### Adding New LLM Tools
Tools are defined using `defineTools()` in `apps/server/src/services/llm/tools/` and automatically registered for both the LLM chat and MCP server.
1. Add the tool definition in the appropriate module (`note_tools.ts`, `attribute_tools.ts`, `attachment_tools.ts`, `hierarchy_tools.ts`) or create a new module
2. Each tool needs: `description`, `inputSchema` (Zod), `execute` function, and optionally `mutates: true` for write operations
3. If creating a new module, wrap tools in `defineTools({...})` and add the registry to `allToolRegistries` in `tools/index.ts`
4. Add a client-side friendly name in `apps/client/src/translations/en/translation.json` under `llm.tools.<tool_name>` — use **imperative tense** (e.g. "Search notes", "Create note", "Get attributes"), not present continuous
5. Use ETAPI (`apps/server/src/etapi/`) as inspiration for what fields to expose, but **do not import ETAPI mappers** — inline the field mappings directly in the tool so the LLM layer stays decoupled from the API layer
### Updating PDF.js
1. Update `pdfjs-dist` version in `packages/pdfjs-viewer/package.json`
2. Run `npx tsx scripts/update-viewer.ts` from that directory
3. Run `pnpm build` to verify success
4. Commit all changes including updated viewer files
### Database Migrations
- Add migration scripts in `apps/server/src/migrations/`
- Update schema in `apps/server/src/assets/db/schema.sql`
### Server-Side Static Assets
- Static assets (templates, SQL, translations, etc.) go in `apps/server/src/assets/`
- Access them at runtime via `RESOURCE_DIR` from `apps/server/src/services/resource_dir.ts` (e.g. `path.join(RESOURCE_DIR, "llm", "skills", "file.md")`)
- **Do not use `import.meta.url`/`fileURLToPath`** to resolve file paths — the server is bundled into CJS for production, so `import.meta.url` will not point to the source directory
- **Do not use `__dirname` with relative paths** from source files — after bundling, `__dirname` points to the bundle output, not the original source tree
## MCP Server
- Trilium exposes an MCP (Model Context Protocol) server at `http://localhost:8080/mcp`, configured in `.mcp.json`
- The MCP server is **only available when the Trilium server is running** (`pnpm run server:start`)
- It provides tools for reading, searching, and modifying notes directly from the AI assistant
- Use it to interact with actual note data when developing or debugging note-related features
## Build System Notes
- Uses pnpm for monorepo management
- Vite for fast development builds
- ESBuild for production optimization
- pnpm workspaces for dependency management
- Docker support with multi-stage builds
- Docker support with multi-stage builds

View File

@@ -2,13 +2,87 @@
## Supported Versions
In the (still active) 0.X phase of the project only the latest stable minor release is getting bugfixes (including security ones).
Only the latest stable minor release receives security fixes.
So e.g. if the latest stable version is 0.42.3 and the latest beta version is 0.43.0-beta, then 0.42 line will still get security fixes but older versions (like 0.41.X) won't get any fixes.
For example, if the latest stable version is 0.92.3 and the latest beta is 0.93.0-beta, then only the 0.92.x line will receive security patches. Older versions (like 0.91.x) will not receive fixes.
Description above is a general rule and may be altered on case by case basis.
This policy may be altered on a case-by-case basis for critical vulnerabilities.
## Reporting a Vulnerability
* For low severity vulnerabilities, they can be reported as GitHub issues.
* For severe vulnerabilities, please report it using [GitHub Security Advisories](https://github.com/TriliumNext/Trilium/security/advisories).
**Please report all security vulnerabilities through [GitHub Security Advisories](https://github.com/TriliumNext/Notes/security/advisories/new).**
We do not accept security reports via email, public issues, or other channels. GitHub Security Advisories allows us to:
- Discuss and triage vulnerabilities privately
- Coordinate fixes before public disclosure
- Credit reporters appropriately
- Publish advisories with CVE identifiers
### What to Include
When reporting, please provide:
- A clear description of the vulnerability
- Steps to reproduce or proof-of-concept
- Affected versions (if known)
- Potential impact assessment
- Any suggested mitigations or fixes
### Response Timeline
- **Initial response**: Within 7 days
- **Triage decision**: Within 14 days
- **Fix timeline**: Depends on severity and complexity
## Scope
### In Scope
- Remote code execution
- Authentication/authorization bypass
- Cross-site scripting (XSS) that affects other users
- SQL injection
- Path traversal
- Sensitive data exposure
- Privilege escalation
### Out of Scope (Won't Fix)
The following are considered out of scope or accepted risks:
#### Self-XSS / Self-Injection
Trilium is a personal knowledge base where users have full control over their own data. Users can intentionally create notes containing scripts, HTML, or other executable content. This is by design - Trilium's scripting system allows users to extend functionality with custom JavaScript.
Vulnerabilities that require a user to inject malicious content into their own notes and then view it themselves are not considered security issues.
#### Electron Architecture (nodeIntegration)
Trilium's desktop application runs with `nodeIntegration: true` to enable its powerful scripting features. This is an intentional design decision, similar to VS Code extensions having full system access. We mitigate risks by:
- Sanitizing content at input boundaries
- Fixing specific XSS vectors as they're discovered
- Using Electron fuses to prevent external abuse
#### Authenticated User Actions
Actions that require valid authentication and only affect the authenticated user's own data are generally not vulnerabilities.
#### Denial of Service via Resource Exhaustion
Creating extremely large notes or performing many operations is expected user behavior in a note-taking application.
#### Missing Security Headers on Non-Sensitive Endpoints
We implement security headers where they provide meaningful protection, but may omit them on endpoints where they provide no practical benefit.
## Coordinated Disclosure
We follow a coordinated disclosure process:
1. **Report received** - We acknowledge receipt and begin triage
2. **Fix developed** - We develop and test a fix privately
3. **Release prepared** - Security release is prepared with vague changelog
4. **Users notified** - Release is published, users encouraged to upgrade
5. **Advisory published** - After reasonable upgrade window (typically 2-4 weeks), full advisory is published
We appreciate reporters allowing us time to fix issues before public disclosure. We aim to credit all reporters in published advisories unless they prefer to remain anonymous.
## Security Updates
Security fixes are released as patch versions (e.g., 0.92.1 → 0.92.2) to minimize upgrade friction. We recommend all users keep their installations up to date.
Subscribe to GitHub releases or watch the repository to receive notifications of new releases.

View File

@@ -14,15 +14,17 @@
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.32.0",
"packageManager": "pnpm@10.33.0",
"dependencies": {
"@triliumnext/core": "workspace:*",
"@triliumnext/server": "workspace:*"
},
"devDependencies": {
"@redocly/cli": "2.20.2",
"@redocly/cli": "2.25.4",
"archiver": "7.0.1",
"fs-extra": "11.3.4",
"js-yaml": "4.1.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"typedoc": "0.28.17",
"typedoc-plugin-missing-exports": "4.1.2"
"typedoc": "0.28.18",
"typedoc-plugin-missing-exports": "4.1.3"
}
}

View File

@@ -14,21 +14,18 @@
*/
export type {
default as AbstractBeccaEntity
} from "../../server/src/becca/entities/abstract_becca_entity.js";
export type {
default as BAttachment
} from "../../server/src/becca/entities/battachment.js";
export type { default as BAttribute } from "../../server/src/becca/entities/battribute.js";
export type { default as BBranch } from "../../server/src/becca/entities/bbranch.js";
export type { default as BEtapiToken } from "../../server/src/becca/entities/betapi_token.js";
export type { BNote };
export type { default as BOption } from "../../server/src/becca/entities/boption.js";
export type { default as BRecentNote } from "../../server/src/becca/entities/brecent_note.js";
export type { default as BRevision } from "../../server/src/becca/entities/brevision.js";
AbstractBeccaEntity,
BAttachment,
BAttribute,
BBranch,
BEtapiToken,
BNote,
BOption,
BRecentNote,
BRevision
} from "@triliumnext/core";
import BNote from "../../server/src/becca/entities/bnote.js";
import BackendScriptApi, { type Api } from "../../server/src/services/backend_script_api.js";
import { BNote, BackendScriptApi, type BackendScriptApiInterface as Api } from "@triliumnext/core";
export type { Api };

View File

@@ -5,10 +5,15 @@ if (!process.env.TRILIUM_RESOURCE_DIR) {
}
process.env.NODE_ENV = "development";
import cls from "@triliumnext/server/src/services/cls.js";
import { getContext, initializeCore } from "@triliumnext/core";
import ClsHookedExecutionContext from "@triliumnext/server/src/cls_provider.js";
import NodejsCryptoProvider from "@triliumnext/server/src/crypto_provider.js";
import ServerPlatformProvider from "@triliumnext/server/src/platform_provider.js";
import BetterSqlite3Provider from "@triliumnext/server/src/sql_provider.js";
import NodejsZipProvider from "@triliumnext/server/src/zip_provider.js";
import archiver from "archiver";
import { execSync } from "child_process";
import { WriteStream } from "fs";
import { readFileSync } from "fs";
import * as fs from "fs/promises";
import * as fsExtra from "fs-extra";
import yaml from "js-yaml";
@@ -16,6 +21,35 @@ import { dirname, join, resolve } from "path";
import BuildContext from "./context.js";
let initialized = false;
async function initializeBuildEnvironment() {
if (initialized) return;
initialized = true;
const dbProvider = new BetterSqlite3Provider();
dbProvider.loadFromMemory();
const { serverZipExportProviderFactory } = await import("@triliumnext/server/src/services/export/zip/factory.js");
await initializeCore({
dbConfig: {
provider: dbProvider,
isReadOnly: false,
onTransactionCommit: () => {},
onTransactionRollback: () => {}
},
crypto: new NodejsCryptoProvider(),
zip: new NodejsZipProvider(),
zipExportProviderFactory: serverZipExportProviderFactory,
executionContext: new ClsHookedExecutionContext(),
platform: new ServerPlatformProvider(),
schema: readFileSync(require.resolve("@triliumnext/core/src/assets/schema.sql"), "utf-8"),
translations: (await import("@triliumnext/server/src/services/i18n.js")).initializeTranslations,
getDemoArchive: async () => null
});
}
interface NoteMapping {
rootNoteId: string;
path: string;
@@ -72,9 +106,8 @@ async function exportDocs(
) {
const zipFilePath = `output-${noteId}.zip`;
try {
const { exportToZipFile } = (await import("@triliumnext/server/src/services/export/zip.js"))
.default;
await exportToZipFile(noteId, format, zipFilePath, {});
const { zipExportService } = await import("@triliumnext/core");
await zipExportService.exportToZipFile(noteId, format, zipFilePath, {});
const ignoredSet = ignoredFiles ? new Set(ignoredFiles) : undefined;
await extractZip(zipFilePath, outputPath, ignoredSet);
@@ -92,18 +125,12 @@ async function importAndExportDocs(sourcePath: string, outputSubDir: string) {
const zipName = outputSubDir || "user-guide";
const zipFilePath = `output-${zipName}.zip`;
try {
const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js"))
.default;
const branch = note.getParentBranches()[0];
const taskContext = new (await import("@triliumnext/server/src/services/task_context.js"))
.default(
"no-progress-reporting",
"export",
null
);
const fileOutputStream = fsExtra.createWriteStream(zipFilePath);
await exportToZip(taskContext, branch, "share", fileOutputStream);
const { zipExportService, TaskContext } = await import("@triliumnext/core");
const { waitForStreamToFinish } = await import("@triliumnext/server/src/services/utils.js");
const branch = note.getParentBranches()[0];
const taskContext = new TaskContext("no-progress-reporting", "export", null);
const fileOutputStream = fsExtra.createWriteStream(zipFilePath);
await zipExportService.exportToZip(taskContext, branch, "share", fileOutputStream);
await waitForStreamToFinish(fileOutputStream);
// Output to root directory if outputSubDir is empty, otherwise to subdirectory
@@ -117,15 +144,11 @@ async function importAndExportDocs(sourcePath: string, outputSubDir: string) {
}
async function buildDocsInner(config?: Config) {
const i18n = await import("@triliumnext/server/src/services/i18n.js");
await i18n.initializeTranslations();
const sqlInit = (await import("../../server/src/services/sql_init.js")).default;
await sqlInit.createInitialDatabase(true);
const { sql_init, becca_loader } = await import("@triliumnext/core");
await sql_init.createInitialDatabase(true);
// Wait for becca to be loaded before importing data
const beccaLoader = await import("../../server/src/becca/becca_loader.js");
await beccaLoader.beccaLoaded;
await becca_loader.beccaLoaded;
if (config) {
// Config-based build (reads from edit-docs-config.yaml)
@@ -176,16 +199,14 @@ async function buildDocsInner(config?: Config) {
export async function importData(path: string) {
const buffer = await createImportZip(path);
const importService = (await import("../../server/src/services/import/zip.js")).default;
const TaskContext = (await import("../../server/src/services/task_context.js")).default;
const { zipImportService, TaskContext, becca } = await import("@triliumnext/core");
const context = new TaskContext("no-progress-reporting", "importNotes", null);
const becca = (await import("../../server/src/becca/becca.js")).default;
const rootNote = becca.getRoot();
if (!rootNote) {
throw new Error("Missing root note for import.");
}
return await importService.importZip(context, buffer, rootNote, {
return await zipImportService.importZip(context, buffer, rootNote, {
preserveIds: true
});
}
@@ -218,20 +239,16 @@ export async function extractZip(
outputPath: string,
ignoredFiles?: Set<string>
) {
const { readZipFile, readContent } = (await import(
"@triliumnext/server/src/services/import/zip.js"
));
await readZipFile(await fs.readFile(zipFilePath), async (zip, entry) => {
const { getZipProvider } = await import("@triliumnext/core");
await getZipProvider().readZipFile(await fs.readFile(zipFilePath), async (entry, readContent) => {
// We ignore directories since they can appear out of order anyway.
if (!entry.fileName.endsWith("/") && !ignoredFiles?.has(entry.fileName)) {
const destPath = join(outputPath, entry.fileName);
const fileContent = await readContent(zip, entry);
const fileContent = await readContent();
await fsExtra.mkdirs(dirname(destPath));
await fs.writeFile(destPath, fileContent);
}
zip.readEntry();
});
}
@@ -246,9 +263,12 @@ export async function buildDocsFromConfig(configPath?: string, gitRootDir?: stri
});
}
// Initialize the build environment before using cls
await initializeBuildEnvironment();
// Trigger the actual build.
await new Promise((res, rej) => {
cls.init(() => {
getContext().init(() => {
buildDocsInner(config ?? undefined)
.catch(rej)
.then(res);
@@ -263,9 +283,12 @@ export default async function buildDocs({ gitRootDir }: BuildContext) {
cwd: gitRootDir
});
// Initialize the build environment before using cls
await initializeBuildEnvironment();
// Trigger the actual build.
await new Promise((res, rej) => {
cls.init(() => {
getContext().init(() => {
buildDocsInner()
.catch(rej)
.then(res);

View File

@@ -28,4 +28,13 @@ async function main() {
cpSync(join(context.baseDir, "user-guide/404.html"), join(context.baseDir, "404.html"));
}
main();
// Note: forcing process.exit() because importing notes via the core triggers
// fire-and-forget async work in `notes.ts#downloadImages` (a 5s setTimeout that
// re-schedules itself via `asyncPostProcessContent`), which keeps the libuv
// event loop alive forever even after main() completes.
main()
.then(() => process.exit(0))
.catch((error) => {
console.error("Error building documentation:", error);
process.exit(1);
});

View File

@@ -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

View File

@@ -0,0 +1 @@
VITE_CKEDITOR_ENABLE_INSPECTOR=false

View File

@@ -0,0 +1,92 @@
{
"name": "@triliumnext/client-standalone",
"version": "0.102.2",
"description": "Standalone client for TriliumNext with SQLite WASM backend",
"private": true,
"license": "AGPL-3.0-only",
"scripts": {
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build",
"dev": "vite dev",
"test": "vitest",
"start-prod": "pnpm build && pnpm vite preview --port 8888",
"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.1",
"@mind-elixir/node-menu": "5.0.1",
"@popperjs/core": "2.11.8",
"@preact/signals": "2.9.0",
"@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.7.0",
"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",
"fflate": "0.8.2",
"force-graph": "1.51.2",
"globals": "17.4.0",
"i18next": "26.0.3",
"i18next-http-backend": "3.0.4",
"aes-js": "3.1.2",
"jquery": "4.0.0",
"jquery.fancytree": "2.38.5",
"js-md5": "0.8.3",
"js-sha1": "0.7.0",
"js-sha256": "0.11.1",
"js-sha512": "0.9.0",
"scrypt-js": "3.0.1",
"jsplumb": "2.15.6",
"katex": "0.16.45",
"knockout": "3.5.1",
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "18.0.0",
"mermaid": "11.14.0",
"mind-elixir": "5.10.0",
"normalize.css": "8.0.1",
"panzoom": "9.4.4",
"preact": "10.29.1",
"react-i18next": "17.0.2",
"react-window": "2.2.7",
"reveal.js": "6.0.0",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.4.0",
"vanilla-js-wheel-zoom": "9.0.4"
},
"devDependencies": {
"@types/aes-js": "3.1.4",
"@ckeditor/ckeditor5-inspector": "5.0.0",
"@preact/preset-vite": "2.10.2",
"@types/bootstrap": "5.2.10",
"@types/jquery": "4.0.0",
"@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": "14.0.0",
"cross-env": "7.0.3",
"happy-dom": "20.8.9",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "4.0.1"
}
}

View File

@@ -0,0 +1,3 @@
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -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": "assets/icon.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@@ -0,0 +1,2 @@
// Re-export desktop from client
export * from "../../client/src/desktop";

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<title>Trilium Notes</title>
</head>
<body id="trilium-app">
<noscript>Trilium requires JavaScript to be enabled.</noscript>
<div id="context-menu-cover"></div>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
<!-- Required for match the PWA's top bar color with the theme -->
<!-- This works even when the user directly changes --root-background in CSS -->
<div id="background-color-tracker" style="position: absolute; visibility: hidden; color: var(--root-background); transition: color 1ms;"></div>
<!-- Bootstrap (request server for required information) -->
<script src="./main.ts" type="module"></script>
<!-- Required for correct loading of scripts in Electron -->
<script>
if (typeof module === 'object') {window.module = module; module = undefined;}
</script>
</body>
</html>

View File

@@ -0,0 +1,156 @@
import type { DatabaseBackup } from "@triliumnext/commons";
import { BackupOptionsService, BackupService, getSql } from "@triliumnext/core";
const BACKUP_DIR_NAME = "backups";
const BACKUP_FILE_PATTERN = /^backup-.*\.db$/;
/**
* Standalone backup service using OPFS (Origin Private File System).
* Stores database backups as serialized byte arrays in OPFS.
* Falls back to no-op behavior when OPFS is not available (e.g., in tests).
*/
export default class StandaloneBackupService extends BackupService {
private backupDir: FileSystemDirectoryHandle | null = null;
private opfsAvailable: boolean | null = null;
constructor(options: BackupOptionsService) {
super(options);
}
private isOpfsAvailable(): boolean {
if (this.opfsAvailable === null) {
this.opfsAvailable = typeof navigator !== "undefined"
&& navigator.storage
&& typeof navigator.storage.getDirectory === "function";
}
return this.opfsAvailable;
}
private async ensureBackupDirectory(): Promise<FileSystemDirectoryHandle | null> {
if (!this.isOpfsAvailable()) {
return null;
}
if (!this.backupDir) {
const root = await navigator.storage.getDirectory();
this.backupDir = await root.getDirectoryHandle(BACKUP_DIR_NAME, { create: true });
}
return this.backupDir;
}
override async backupNow(name: string): Promise<string> {
const fileName = `backup-${name}.db`;
// Check if OPFS is available
if (!this.isOpfsAvailable()) {
console.warn(`[Backup] OPFS not available, skipping backup: ${fileName}`);
return `/${BACKUP_DIR_NAME}/${fileName}`;
}
try {
const dir = await this.ensureBackupDirectory();
if (!dir) {
console.warn(`[Backup] Backup directory not available, skipping: ${fileName}`);
return `/${BACKUP_DIR_NAME}/${fileName}`;
}
// Serialize the database
const data = getSql().serialize();
// Write to OPFS
const fileHandle = await dir.getFileHandle(fileName, { create: true });
const writable = await fileHandle.createWritable();
await writable.write(data);
await writable.close();
console.log(`[Backup] Created backup: ${fileName} (${data.byteLength} bytes)`);
return `/${BACKUP_DIR_NAME}/${fileName}`;
} catch (error) {
console.error(`[Backup] Failed to create backup ${fileName}:`, error);
// Don't throw - backup failure shouldn't block operations
return `/${BACKUP_DIR_NAME}/${fileName}`;
}
}
override async getExistingBackups(): Promise<DatabaseBackup[]> {
if (!this.isOpfsAvailable()) {
return [];
}
try {
const dir = await this.ensureBackupDirectory();
if (!dir) {
return [];
}
const backups: DatabaseBackup[] = [];
for await (const [name, handle] of dir.entries()) {
if (handle.kind !== "file" || !BACKUP_FILE_PATTERN.test(name)) {
continue;
}
const file = await (handle as FileSystemFileHandle).getFile();
backups.push({
fileName: name,
filePath: `/${BACKUP_DIR_NAME}/${name}`,
mtime: new Date(file.lastModified)
});
}
// Sort by modification time, newest first
backups.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
return backups;
} catch (error) {
console.error("[Backup] Failed to list backups:", error);
return [];
}
}
/**
* Delete a backup by filename.
*/
async deleteBackup(fileName: string): Promise<void> {
if (!this.isOpfsAvailable()) {
return;
}
try {
const dir = await this.ensureBackupDirectory();
if (!dir) {
return;
}
await dir.removeEntry(fileName);
console.log(`[Backup] Deleted backup: ${fileName}`);
} catch (error) {
console.error(`[Backup] Failed to delete backup ${fileName}:`, error);
}
}
override async getBackupContent(filePath: string): Promise<Uint8Array | null> {
if (!this.isOpfsAvailable()) {
return null;
}
try {
const dir = await this.ensureBackupDirectory();
if (!dir) {
return null;
}
// Extract fileName from filePath (e.g., "/backups/backup-now.db" -> "backup-now.db")
const fileName = filePath.split("/").pop();
if (!fileName || !BACKUP_FILE_PATTERN.test(fileName)) {
return null;
}
const fileHandle = await dir.getFileHandle(fileName);
const file = await fileHandle.getFile();
const data = await file.arrayBuffer();
return new Uint8Array(data);
} catch (error) {
console.error(`[Backup] Failed to get backup content ${filePath}:`, error);
return null;
}
}
}

View File

@@ -0,0 +1,314 @@
/**
* Browser-compatible router that mimics Express routing patterns.
* Supports path parameters (e.g., /api/notes/:noteId) and query strings.
*/
import { getContext, routes } from "@triliumnext/core";
export interface UploadedFile {
originalname: string;
mimetype: string;
buffer: Uint8Array;
}
export interface BrowserRequest {
method: string;
url: string;
path: string;
params: Record<string, string>;
query: Record<string, string | undefined>;
headers?: Record<string, string>;
body?: unknown;
file?: UploadedFile;
}
export interface BrowserResponse {
status: number;
headers: Record<string, string>;
body: ArrayBuffer | null;
}
export type RouteHandler = (req: BrowserRequest) => unknown | Promise<unknown>;
interface Route {
method: string;
pattern: RegExp;
paramNames: string[];
handler: RouteHandler;
}
/**
* Symbol used to mark a result as an already-formatted response,
* so that formatResult passes it through without JSON-serializing.
* Must match the symbol exported from browser_routes.ts.
*/
const RAW_RESPONSE = Symbol.for('RAW_RESPONSE');
const encoder = new TextEncoder();
/**
* Convert an Express-style path pattern to a RegExp.
* Supports :param syntax for path parameters.
*
* Examples:
* /api/notes/:noteId -> /^\/api\/notes\/([^\/]+)$/
* /api/notes/:noteId/revisions -> /^\/api\/notes\/([^\/]+)\/revisions$/
*/
function pathToRegex(path: string): { pattern: RegExp; paramNames: string[] } {
const paramNames: string[] = [];
// Escape special regex characters except for :param patterns
const regexPattern = path
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape special chars
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => {
paramNames.push(paramName);
return '([^/]+)';
});
return {
pattern: new RegExp(`^${regexPattern}$`),
paramNames
};
}
/**
* Parse query string into an object.
*/
function parseQuery(search: string): Record<string, string | undefined> {
const query: Record<string, string | undefined> = {};
if (!search || search === '?') return query;
const params = new URLSearchParams(search);
for (const [key, value] of params) {
query[key] = value;
}
return query;
}
/**
* Convert a result to a JSON response.
*/
function jsonResponse(obj: unknown, status = 200, extraHeaders: Record<string, string> = {}): BrowserResponse {
const parsedObj = routes.convertEntitiesToPojo(obj);
const body = encoder.encode(JSON.stringify(parsedObj)).buffer as ArrayBuffer;
return {
status,
headers: { "content-type": "application/json; charset=utf-8", ...extraHeaders },
body
};
}
/**
* Convert a string to a text response.
*/
function textResponse(text: string, status = 200, extraHeaders: Record<string, string> = {}): BrowserResponse {
const body = encoder.encode(text).buffer as ArrayBuffer;
return {
status,
headers: { "content-type": "text/plain; charset=utf-8", ...extraHeaders },
body
};
}
/**
* Browser router class that handles route registration and dispatching.
*/
export class BrowserRouter {
private routes: Route[] = [];
/**
* Register a route handler.
*/
register(method: string, path: string, handler: RouteHandler): void {
const { pattern, paramNames } = pathToRegex(path);
this.routes.push({
method: method.toUpperCase(),
pattern,
paramNames,
handler
});
}
/**
* Convenience methods for common HTTP methods.
*/
get(path: string, handler: RouteHandler): void {
this.register('GET', path, handler);
}
post(path: string, handler: RouteHandler): void {
this.register('POST', path, handler);
}
put(path: string, handler: RouteHandler): void {
this.register('PUT', path, handler);
}
patch(path: string, handler: RouteHandler): void {
this.register('PATCH', path, handler);
}
delete(path: string, handler: RouteHandler): void {
this.register('DELETE', path, handler);
}
/**
* Dispatch a request to the appropriate handler.
*/
async dispatch(method: string, urlString: string, body?: unknown, headers?: Record<string, string>): Promise<BrowserResponse> {
const url = new URL(urlString);
const path = url.pathname;
const query = parseQuery(url.search);
const upperMethod = method.toUpperCase();
// Parse body based on content-type
let parsedBody = body;
let uploadedFile: UploadedFile | undefined;
if (body instanceof ArrayBuffer && headers) {
const contentType = headers['content-type'] || headers['Content-Type'] || '';
if (contentType.includes('application/json')) {
try {
const text = new TextDecoder().decode(body);
if (text.trim()) {
parsedBody = JSON.parse(text);
}
} catch (e) {
console.warn('[Router] Failed to parse JSON body:', e);
parsedBody = body;
}
} else if (contentType.includes('multipart/form-data')) {
try {
// Reconstruct a Response so we can use the native FormData parser
const response = new Response(body, { headers: { 'content-type': contentType } });
const formData = await response.formData();
const formFields: Record<string, string> = {};
for (const [key, value] of formData.entries()) {
if (typeof value === 'string') {
formFields[key] = value;
} else {
// File field (Blob) — multer uses the field name "upload"
const fileBuffer = new Uint8Array(await value.arrayBuffer());
uploadedFile = {
originalname: value.name,
mimetype: value.type || 'application/octet-stream',
buffer: fileBuffer
};
}
}
parsedBody = formFields;
} catch (e) {
console.warn('[Router] Failed to parse multipart body:', e);
}
}
}
// Find matching route
for (const route of this.routes) {
if (route.method !== upperMethod) continue;
const match = path.match(route.pattern);
if (!match) continue;
// Extract path parameters
const params: Record<string, string> = {};
for (let i = 0; i < route.paramNames.length; i++) {
params[route.paramNames[i]] = decodeURIComponent(match[i + 1]);
}
const request: BrowserRequest = {
method: upperMethod,
url: urlString,
path,
params,
query,
headers: headers ?? {},
body: parsedBody,
file: uploadedFile
};
try {
const result = await getContext().init(async () => await route.handler(request));
return this.formatResult(result);
} catch (error) {
return this.formatError(error, `Error handling ${method} ${path}`);
}
}
// No route matched
return textResponse(`Not found: ${method} ${path}`, 404);
}
/**
* Format a handler result into a response.
* Follows the same patterns as the server's apiResultHandler.
*/
private formatResult(result: unknown): BrowserResponse {
// Handle raw responses (e.g. from image routes that write directly to res)
if (result && typeof result === 'object' && RAW_RESPONSE in result) {
const raw = result as unknown as { status: number; headers: Record<string, string>; body: unknown };
let body: ArrayBuffer | null = null;
if (raw.body instanceof ArrayBuffer) {
body = raw.body;
} else if (raw.body instanceof Uint8Array) {
body = raw.body.buffer as ArrayBuffer;
} else if (typeof raw.body === 'string') {
body = encoder.encode(raw.body).buffer as ArrayBuffer;
}
return {
status: raw.status,
headers: raw.headers,
body
};
}
// Handle [statusCode, response] format
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
const [statusCode, response] = result;
return jsonResponse(response, statusCode);
}
// Handle undefined (no content) - 204 should have no body
if (result === undefined) {
return {
status: 204,
headers: {},
body: null
};
}
// Default: JSON response with 200
return jsonResponse(result, 200);
}
/**
* Format an error into a response.
*/
private formatError(error: unknown, context: string): BrowserResponse {
console.error('[Router] Handler error:', context, error);
// Check for known error types
if (error && typeof error === 'object') {
const err = error as { constructor?: { name?: string }; message?: string };
if (err.constructor?.name === 'NotFoundError') {
return jsonResponse({ message: err.message || 'Not found' }, 404);
}
if (err.constructor?.name === 'ValidationError') {
return jsonResponse({ message: err.message || 'Validation error' }, 400);
}
}
// Generic error
const message = error instanceof Error ? error.message : String(error);
return jsonResponse({ message }, 500);
}
}
/**
* Create a new router instance.
*/
export function createRouter(): BrowserRouter {
return new BrowserRouter();
}

View File

@@ -0,0 +1,340 @@
/**
* Browser route definitions.
* This integrates with the shared route builder from @triliumnext/core.
*/
import { BootstrapDefinition } from '@triliumnext/commons';
import { entity_changes, getContext, getPlatform, getSharedBootstrapItems, getSql, routes, sql_init } from '@triliumnext/core';
import packageJson from '../../package.json' with { type: 'json' };
import { type BrowserRequest, BrowserRouter } from './browser_router';
/** Minimal response object used by apiResultHandler to capture the processed result. */
interface ResultHandlerResponse {
headers: Record<string, string>;
result: unknown;
setHeader(name: string, value: string): void;
}
/**
* Symbol used to mark a result as an already-formatted BrowserResponse,
* so that BrowserRouter.formatResult passes it through without JSON-serializing.
* Uses Symbol.for() so the same symbol is shared across modules.
*/
const RAW_RESPONSE = Symbol.for('RAW_RESPONSE');
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
/**
* Creates an Express-like request object from a BrowserRequest.
*/
function toExpressLikeReq(req: BrowserRequest) {
return {
params: req.params,
query: req.query,
body: req.body,
headers: req.headers ?? {},
method: req.method,
file: req.file,
get originalUrl() { return req.url; }
};
}
/**
* Extracts context headers from the request and sets them in the execution context,
* mirroring what the server does in route_api.ts.
*/
function setContextFromHeaders(req: BrowserRequest) {
const headers = req.headers ?? {};
const ctx = getContext();
ctx.set("componentId", headers["trilium-component-id"]);
ctx.set("localNowDateTime", headers["trilium-local-now-datetime"]);
ctx.set("hoistedNoteId", headers["trilium-hoisted-note-id"] || "root");
}
/**
* Wraps a core route handler to work with the BrowserRouter.
* Core handlers expect an Express-like request object with params, query, and body.
* Each request is wrapped in an execution context (like cls.init() on the server)
* to ensure entity change tracking works correctly.
*/
function wrapHandler(handler: (req: any) => unknown, transactional: boolean) {
return (req: BrowserRequest) => {
return getContext().init(() => {
setContextFromHeaders(req);
const expressLikeReq = toExpressLikeReq(req);
if (transactional) {
return getSql().transactional(() => handler(expressLikeReq));
}
return handler(expressLikeReq);
});
};
}
/**
* Creates an apiRoute function compatible with buildSharedApiRoutes.
* This bridges the core's route registration to the BrowserRouter.
*/
function createApiRoute(router: BrowserRouter, transactional: boolean) {
return (method: HttpMethod, path: string, handler: (req: any) => unknown) => {
router.register(method, path, wrapHandler(handler, transactional));
};
}
/**
* Low-level route registration matching the server's `route()` signature:
* route(method, path, middleware[], handler, resultHandler)
*
* In standalone mode:
* - Middleware (e.g. checkApiAuth) is skipped — there's no authentication.
* - The resultHandler is applied to post-process the result (entity conversion, status codes).
*/
function createRoute(router: BrowserRouter) {
return (method: HttpMethod, path: string, _middleware: any[], handler: (req: any, res: any) => unknown, resultHandler?: ((req: any, res: any, result: unknown) => unknown) | null) => {
router.register(method, path, (req: BrowserRequest) => {
return getContext().init(() => {
setContextFromHeaders(req);
const expressLikeReq = toExpressLikeReq(req);
const mockRes = createMockExpressResponse();
const result = getSql().transactional(() => handler(expressLikeReq, mockRes));
// If the handler used the mock response (e.g. image routes that call res.send()),
// return it as a raw response so BrowserRouter doesn't JSON-serialize it.
if (mockRes._used) {
return {
[RAW_RESPONSE]: true as const,
status: mockRes._status,
headers: mockRes._headers,
body: mockRes._body
};
}
if (resultHandler) {
// Create a minimal response object that captures what apiResultHandler sets.
const res = createResultHandlerResponse();
resultHandler(expressLikeReq, res, result);
return res.result;
}
return result;
});
});
};
}
/**
* Async variant of createRoute for handlers that return Promises (e.g. import).
* Uses transactionalAsync (manual BEGIN/COMMIT/ROLLBACK) instead of the synchronous
* transactional() wrapper, which would commit an empty transaction immediately when
* passed an async callback.
*/
function createAsyncRoute(router: BrowserRouter) {
return (method: HttpMethod, path: string, _middleware: any[], handler: (req: any, res: any) => Promise<unknown>, resultHandler?: ((req: any, res: any, result: unknown) => unknown) | null) => {
router.register(method, path, (req: BrowserRequest) => {
return getContext().init(async () => {
setContextFromHeaders(req);
const expressLikeReq = toExpressLikeReq(req);
const mockRes = createMockExpressResponse();
const result = await getSql().transactionalAsync(() => handler(expressLikeReq, mockRes));
// If the handler used the mock response (e.g. image routes that call res.send()),
// return it as a raw response so BrowserRouter doesn't JSON-serialize it.
if (mockRes._used) {
return {
[RAW_RESPONSE]: true as const,
status: mockRes._status,
headers: mockRes._headers,
body: mockRes._body
};
}
if (resultHandler) {
// Create a minimal response object that captures what apiResultHandler sets.
const res = createResultHandlerResponse();
resultHandler(expressLikeReq, res, result);
return res.result;
}
return result;
});
});
};
}
/**
* Creates a mock Express response object that captures calls to set(), send(), sendStatus(), etc.
* Used for route handlers (like image routes) that write directly to the response.
*/
function createMockExpressResponse() {
const chunks: string[] = [];
const res = {
_used: false,
_status: 200,
_headers: {} as Record<string, string>,
_body: null as unknown,
set(name: string, value: string) {
res._headers[name] = value;
return res;
},
setHeader(name: string, value: string) {
res._headers[name] = value;
return res;
},
removeHeader(name: string) {
delete res._headers[name];
return res;
},
status(code: number) {
res._status = code;
return res;
},
send(body: unknown) {
res._used = true;
res._body = body;
return res;
},
sendStatus(code: number) {
res._used = true;
res._status = code;
return res;
},
write(chunk: string) {
chunks.push(chunk);
return true;
},
end() {
res._used = true;
res._body = chunks.join("");
return res;
}
};
return res;
}
/**
* Standalone apiResultHandler matching the server's behavior:
* - Converts Becca entities to POJOs
* - Handles [statusCode, response] tuple format
* - Sets trilium-max-entity-change-id (captured in response headers)
*/
function apiResultHandler(_req: any, res: ResultHandlerResponse, result: unknown) {
res.headers["trilium-max-entity-change-id"] = String(entity_changes.getMaxEntityChangeId());
result = routes.convertEntitiesToPojo(result);
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
const [_statusCode, response] = result;
res.result = response;
} else if (result === undefined) {
res.result = "";
} else {
res.result = result;
}
}
/**
* No-op middleware stubs for standalone mode.
*
* In a browser context there is no network authentication, rate limiting,
* or multi-user access, so all auth/rate-limit middleware is a no-op.
*
* `checkAppNotInitialized` still guards setup routes: if the database is
* already initialised the middleware throws so the route handler is never
* reached (mirrors the server behaviour).
*/
function noopMiddleware() {
// No-op.
}
function checkAppNotInitialized() {
if (sql_init.isDbInitialized()) {
throw new Error("App already initialized.");
}
}
/**
* Creates a minimal response-like object for the apiResultHandler.
*/
function createResultHandlerResponse(): ResultHandlerResponse {
return {
headers: {},
result: undefined,
setHeader(name: string, value: string) {
this.headers[name] = value;
}
};
}
/**
* Register all API routes on the browser router using the shared builder.
*
* @param router - The browser router instance
*/
export function registerRoutes(router: BrowserRouter): void {
const apiRoute = createApiRoute(router, true);
routes.buildSharedApiRoutes({
route: createRoute(router),
asyncRoute: createAsyncRoute(router),
apiRoute,
asyncApiRoute: createApiRoute(router, false),
apiResultHandler,
checkApiAuth: noopMiddleware,
checkApiAuthOrElectron: noopMiddleware,
checkAppNotInitialized,
checkCredentials: noopMiddleware,
loginRateLimiter: noopMiddleware,
uploadMiddlewareWithErrorHandling: noopMiddleware,
csrfMiddleware: noopMiddleware
});
apiRoute('get', '/bootstrap', bootstrapRoute);
// Dummy routes for compatibility.
apiRoute("get", "/api/script/widgets", () => []);
apiRoute("get", "/api/script/startup", () => []);
apiRoute("get", "/api/system-checks", () => ({ isCpuArchMismatch: false }));
}
function bootstrapRoute(): BootstrapDefinition {
const assetPath = ".";
const isDbInitialized = sql_init.isDbInitialized();
const commonItems = {
...getSharedBootstrapItems(assetPath, isDbInitialized),
isDev: import.meta.env.DEV,
isStandalone: true,
isMainWindow: true,
isElectron: false,
hasNativeTitleBar: false,
hasBackgroundEffects: false,
triliumVersion: packageJson.version,
device: false as const, // Let the client detect device type.
appPath: assetPath,
instanceName: "standalone",
TRILIUM_SAFE_MODE: !!getPlatform().getEnv("TRILIUM_SAFE_MODE")
};
if (!isDbInitialized) {
return {
...commonItems,
baseApiUrl: "../api/",
isProtectedSessionAvailable: false,
};
}
return {
...commonItems,
csrfToken: "dummy-csrf-token",
baseApiUrl: "../api/",
headingStyle: "plain",
layoutOrientation: "vertical",
platform: "web",
};
}
/**
* Create and configure a router with all routes registered.
*/
export function createConfiguredRouter(): BrowserRouter {
const router = new BrowserRouter();
registerRoutes(router);
return router;
}

View File

@@ -0,0 +1,77 @@
import { ExecutionContext } from "@triliumnext/core";
/**
* Browser execution context implementation.
*
* Handles per-request context isolation with support for fire-and-forget async operations
* using a context stack and grace-period cleanup to allow unawaited promises to complete.
*/
export default class BrowserExecutionContext implements ExecutionContext {
private contextStack: Map<string, any>[] = [];
private cleanupTimers = new WeakMap<Map<string, any>, ReturnType<typeof setTimeout>>();
private readonly CLEANUP_GRACE_PERIOD = 1000; // 1 second for fire-and-forget operations
private getCurrentContext(): Map<string, any> {
if (this.contextStack.length === 0) {
throw new Error("ExecutionContext not initialized");
}
return this.contextStack[this.contextStack.length - 1];
}
get<T = any>(key: string): T {
return this.getCurrentContext().get(key);
}
set(key: string, value: any): void {
this.getCurrentContext().set(key, value);
}
reset(): void {
this.contextStack = [];
}
init<T>(callback: () => T): T {
const context = new Map<string, any>();
this.contextStack.push(context);
// Cancel any pending cleanup timer for this context
const existingTimer = this.cleanupTimers.get(context);
if (existingTimer) {
clearTimeout(existingTimer);
this.cleanupTimers.delete(context);
}
try {
const result = callback();
// If the result is a Promise
if (result && typeof result === 'object' && 'then' in result && 'catch' in result) {
const promise = result as unknown as Promise<any>;
return promise.finally(() => {
this.scheduleContextCleanup(context);
}) as T;
} else {
// For synchronous results, schedule delayed cleanup to allow fire-and-forget operations
this.scheduleContextCleanup(context);
return result;
}
} catch (error) {
// Always clean up on error with grace period
this.scheduleContextCleanup(context);
throw error;
}
}
private scheduleContextCleanup(context: Map<string, any>): void {
const timer = setTimeout(() => {
// Remove from stack if still present
const index = this.contextStack.indexOf(context);
if (index !== -1) {
this.contextStack.splice(index, 1);
}
this.cleanupTimers.delete(context);
}, this.CLEANUP_GRACE_PERIOD);
this.cleanupTimers.set(context, timer);
}
}

View File

@@ -0,0 +1,175 @@
import type { Cipher, CryptoProvider, ScryptOptions } from "@triliumnext/core";
import { binary_utils } from "@triliumnext/core";
import { sha1 } from "js-sha1";
import { sha256 } from "js-sha256";
import { sha512 } from "js-sha512";
import { md5 } from "js-md5";
import { scrypt } from "scrypt-js";
import aesjs from "aes-js";
const CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
/**
* Crypto provider for browser environments using pure JavaScript crypto libraries.
* Uses aes-js for synchronous AES encryption (matching Node.js behavior).
*/
export default class BrowserCryptoProvider implements CryptoProvider {
createHash(algorithm: "md5" | "sha1" | "sha512", content: string | Uint8Array): Uint8Array {
const data = binary_utils.unwrapStringOrBuffer(content);
let hexHash: string;
if (algorithm === "md5") {
hexHash = md5(data);
} else if (algorithm === "sha1") {
hexHash = sha1(data);
} else {
hexHash = sha512(data);
}
// Convert hex string to Uint8Array
const bytes = new Uint8Array(hexHash.length / 2);
for (let i = 0; i < hexHash.length; i += 2) {
bytes[i / 2] = parseInt(hexHash.substr(i, 2), 16);
}
return bytes;
}
createCipheriv(algorithm: "aes-128-cbc", key: Uint8Array, iv: Uint8Array): Cipher {
return new AesJsCipher(algorithm, key, iv, "encrypt");
}
createDecipheriv(algorithm: "aes-128-cbc", key: Uint8Array, iv: Uint8Array): Cipher {
return new AesJsCipher(algorithm, key, iv, "decrypt");
}
randomBytes(size: number): Uint8Array {
const bytes = new Uint8Array(size);
crypto.getRandomValues(bytes);
return bytes;
}
randomString(length: number): string {
const bytes = this.randomBytes(length);
let result = "";
for (let i = 0; i < length; i++) {
result += CHARS[bytes[i] % CHARS.length];
}
return result;
}
hmac(secret: string | Uint8Array, value: string | Uint8Array): string {
const secretStr = binary_utils.unwrapStringOrBuffer(secret);
const valueStr = binary_utils.unwrapStringOrBuffer(value);
// sha256.hmac returns hex, convert to base64 to match Node's behavior
const hexHash = sha256.hmac(secretStr, valueStr);
const bytes = new Uint8Array(hexHash.length / 2);
for (let i = 0; i < hexHash.length; i += 2) {
bytes[i / 2] = parseInt(hexHash.substr(i, 2), 16);
}
return btoa(String.fromCharCode(...bytes));
}
async scrypt(
password: Uint8Array | string,
salt: Uint8Array | string,
keyLength: number,
options: ScryptOptions = {}
): Promise<Uint8Array> {
const { N = 16384, r = 8, p = 1 } = options;
const passwordBytes = binary_utils.wrapStringOrBuffer(password);
const saltBytes = binary_utils.wrapStringOrBuffer(salt);
return scrypt(passwordBytes, saltBytes, N, r, p, keyLength);
}
constantTimeCompare(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) {
return false;
}
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result === 0;
}
}
/**
* A synchronous cipher implementation using aes-js.
* Matches Node.js crypto behavior with update() and final() methods.
*/
class AesJsCipher implements Cipher {
private chunks: Uint8Array[] = [];
private key: Uint8Array;
private iv: Uint8Array;
private mode: "encrypt" | "decrypt";
private finalized = false;
constructor(
_algorithm: "aes-128-cbc",
key: Uint8Array,
iv: Uint8Array,
mode: "encrypt" | "decrypt"
) {
this.key = key;
this.iv = iv;
this.mode = mode;
}
update(data: Uint8Array): Uint8Array {
if (this.finalized) {
throw new Error("Cipher has already been finalized");
}
// Buffer the data - we process everything in final() to match streaming behavior
this.chunks.push(data);
// Return empty array since aes-js CBC doesn't support true streaming
return new Uint8Array(0);
}
final(): Uint8Array {
if (this.finalized) {
throw new Error("Cipher has already been finalized");
}
this.finalized = true;
// Concatenate all chunks
const totalLength = this.chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const data = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of this.chunks) {
data.set(chunk, offset);
offset += chunk.length;
}
if (this.mode === "encrypt") {
// PKCS7 padding for encryption
const blockSize = 16;
const paddingLength = blockSize - (data.length % blockSize);
const paddedData = new Uint8Array(data.length + paddingLength);
paddedData.set(data);
paddedData.fill(paddingLength, data.length);
const aesCbc = new aesjs.ModeOfOperation.cbc(
Array.from(this.key),
Array.from(this.iv)
);
return new Uint8Array(aesCbc.encrypt(paddedData));
} else {
// Decryption
const aesCbc = new aesjs.ModeOfOperation.cbc(
Array.from(this.key),
Array.from(this.iv)
);
const decrypted = new Uint8Array(aesCbc.decrypt(data));
// Remove PKCS7 padding
const paddingLength = decrypted[decrypted.length - 1];
if (paddingLength > 0 && paddingLength <= 16) {
return decrypted.slice(0, decrypted.length - paddingLength);
}
return decrypted;
}
}
}

View File

@@ -0,0 +1,168 @@
import { FileBasedLogService, type LogFileInfo } from "@triliumnext/core";
const LOG_DIR_NAME = "logs";
const LOG_FILE_PATTERN = /^trilium-\d{4}-\d{2}-\d{2}\.log$/;
const DEFAULT_RETENTION_DAYS = 7;
/**
* Standalone log service using OPFS (Origin Private File System).
* Uses synchronous access handles available in service worker context.
*/
export default class StandaloneLogService extends FileBasedLogService {
private logDir: FileSystemDirectoryHandle | null = null;
private currentFile: FileSystemSyncAccessHandle | null = null;
private currentFileName: string = "";
private textEncoder = new TextEncoder();
private textDecoder = new TextDecoder();
constructor() {
super();
}
// ==================== Abstract Method Implementations ====================
protected override get eol(): string {
return "\n";
}
protected override async ensureLogDirectory(): Promise<void> {
const root = await navigator.storage.getDirectory();
this.logDir = await root.getDirectoryHandle(LOG_DIR_NAME, { create: true });
}
protected override async openLogFile(fileName: string): Promise<void> {
if (!this.logDir) {
await this.ensureLogDirectory();
}
// Close existing file if open
if (this.currentFile) {
this.currentFile.close();
this.currentFile = null;
}
const fileHandle = await this.logDir!.getFileHandle(fileName, { create: true });
// Try to create sync access handle with retry logic for worker restarts
// Previous worker may have left handle open before being terminated
const maxRetries = 3;
const retryDelay = 100;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
this.currentFile = await fileHandle.createSyncAccessHandle();
break;
} catch (error) {
if (attempt === maxRetries - 1) {
// Last attempt failed - fall back to console-only logging
console.warn("[LogService] Could not open log file, using console-only logging:", error);
this.currentFile = null;
this.currentFileName = "";
return;
}
// Wait before retrying - previous handle may be released
await new Promise(resolve => setTimeout(resolve, retryDelay * (attempt + 1)));
}
}
this.currentFileName = fileName;
// Seek to end for appending
if (this.currentFile) {
const size = this.currentFile.getSize();
this.currentFile.truncate(size); // No-op, but ensures we're at the right position
}
}
protected override closeLogFile(): void {
if (this.currentFile) {
this.currentFile.close();
this.currentFile = null;
this.currentFileName = "";
}
}
protected override writeEntry(entry: string): void {
if (!this.currentFile) {
console.log(entry); // Fallback to console if file not ready
return;
}
const data = this.textEncoder.encode(entry);
const currentSize = this.currentFile.getSize();
this.currentFile.write(data, { at: currentSize });
this.currentFile.flush();
}
protected override readLogFile(fileName: string): string | null {
if (!this.logDir) {
return null;
}
try {
// For the current file, we need to read from the sync handle
if (fileName === this.currentFileName && this.currentFile) {
const size = this.currentFile.getSize();
const buffer = new ArrayBuffer(size);
const view = new DataView(buffer);
this.currentFile.read(view, { at: 0 });
return this.textDecoder.decode(buffer);
}
// For other files, we'd need async access - return null for now
// The current file is what's most commonly needed
return null;
} catch {
return null;
}
}
protected override async listLogFiles(): Promise<LogFileInfo[]> {
if (!this.logDir) {
return [];
}
const logFiles: LogFileInfo[] = [];
for await (const [name, handle] of this.logDir.entries()) {
if (handle.kind !== "file" || !LOG_FILE_PATTERN.test(name)) {
continue;
}
// OPFS doesn't provide mtime directly, so we parse from filename
const match = name.match(/trilium-(\d{4})-(\d{2})-(\d{2})\.log/);
if (match) {
const mtime = new Date(
parseInt(match[1]),
parseInt(match[2]) - 1,
parseInt(match[3])
);
logFiles.push({ name, mtime });
}
}
return logFiles;
}
protected override async deleteLogFile(fileName: string): Promise<void> {
if (!this.logDir) {
return;
}
// Don't delete the current file
if (fileName === this.currentFileName) {
return;
}
try {
await this.logDir.removeEntry(fileName);
} catch {
// File might not exist or be locked
}
}
protected override getRetentionDays(): number {
// Standalone doesn't have config system, use default
return DEFAULT_RETENTION_DAYS;
}
}

View File

@@ -0,0 +1,120 @@
import type { WebSocketMessage } from "@triliumnext/commons";
import type { ClientMessageHandler, MessageHandler,MessagingProvider } from "@triliumnext/core";
/**
* Messaging provider for browser Worker environments.
*
* This provider uses the Worker's postMessage API to communicate
* with the main thread. It's designed to be used inside a Web Worker
* that runs the core services.
*
* Message flow:
* - Outbound (worker → main): Uses self.postMessage() with type: "WS_MESSAGE"
* - Inbound (main → worker): Listens to onmessage for type: "WS_MESSAGE"
*/
export default class WorkerMessagingProvider implements MessagingProvider {
private messageHandlers: MessageHandler[] = [];
private clientMessageHandler?: ClientMessageHandler;
private isDisposed = false;
constructor() {
// Listen for incoming messages from the main thread
self.addEventListener("message", this.handleIncomingMessage);
}
private handleIncomingMessage = (event: MessageEvent) => {
if (this.isDisposed) return;
const { type, message } = event.data || {};
if (type === "WS_MESSAGE" && message) {
// Dispatch to the client message handler (used by ws.ts for log-error, log-info, ping)
if (this.clientMessageHandler) {
try {
this.clientMessageHandler("main-thread", message);
} catch (e) {
console.error("[WorkerMessagingProvider] Error in client message handler:", e);
}
}
// Dispatch to all registered handlers
for (const handler of this.messageHandlers) {
try {
handler(message as WebSocketMessage);
} catch (e) {
console.error("[WorkerMessagingProvider] Error in message handler:", e);
}
}
}
};
/**
* Send a message to all clients (in this case, the main thread).
* The main thread is responsible for further distribution if needed.
*/
sendMessageToAllClients(message: WebSocketMessage): void {
if (this.isDisposed) {
console.warn("[WorkerMessagingProvider] Cannot send message - provider is disposed");
return;
}
try {
self.postMessage({
type: "WS_MESSAGE",
message
});
} catch (e) {
console.error("[WorkerMessagingProvider] Error sending message:", e);
}
}
/**
* Send a message to a specific client.
* In worker context, there's only one client (the main thread), so clientId is ignored.
*/
sendMessageToClient(_clientId: string, message: WebSocketMessage): boolean {
if (this.isDisposed) {
return false;
}
this.sendMessageToAllClients(message);
return true;
}
/**
* Register a handler for incoming client messages.
*/
setClientMessageHandler(handler: ClientMessageHandler): void {
this.clientMessageHandler = handler;
}
/**
* Subscribe to incoming messages from the main thread.
*/
onMessage(handler: MessageHandler): () => void {
this.messageHandlers.push(handler);
return () => {
this.messageHandlers = this.messageHandlers.filter(h => h !== handler);
};
}
/**
* Get the number of connected "clients".
* In worker context, there's always exactly 1 client (the main thread).
*/
getClientCount(): number {
return this.isDisposed ? 0 : 1;
}
/**
* Clean up resources.
*/
dispose(): void {
if (this.isDisposed) return;
this.isDisposed = true;
self.removeEventListener("message", this.handleIncomingMessage);
this.messageHandlers = [];
}
}

View File

@@ -0,0 +1,36 @@
import type { PlatformProvider } from "@triliumnext/core";
/** Maps URL query parameter names to TRILIUM_ environment variable names. */
const QUERY_TO_ENV: Record<string, string> = {
"safeMode": "TRILIUM_SAFE_MODE",
"startNoteId": "TRILIUM_START_NOTE_ID",
};
export default class StandalonePlatformProvider implements PlatformProvider {
readonly isElectron = false;
readonly isMac = false;
readonly isWindows = false;
private envMap: Record<string, string> = {};
constructor(queryString: string) {
const params = new URLSearchParams(queryString);
for (const [queryKey, envKey] of Object.entries(QUERY_TO_ENV)) {
if (params.has(queryKey)) {
this.envMap[envKey] = params.get(queryKey) || "true";
}
}
}
crash(message: string): void {
console.error("[Standalone] FATAL:", message);
self.postMessage({
type: "FATAL_ERROR",
message
});
}
getEnv(key: string): string | undefined {
return this.envMap[key];
}
}

View File

@@ -0,0 +1,93 @@
import type { ExecOpts, RequestProvider } from "@triliumnext/core";
/**
* Fetch-based implementation of RequestProvider for browser environments.
*
* Uses the Fetch API instead of Node's http/https modules.
* Proxy support is not available in browsers, so the proxy option is ignored.
*/
export default class FetchRequestProvider implements RequestProvider {
async exec<T>(opts: ExecOpts): Promise<T> {
const paging = opts.paging || {
pageCount: 1,
pageIndex: 0,
requestId: "n/a"
};
const headers: Record<string, string> = {
"Content-Type": paging.pageCount === 1 ? "application/json" : "text/plain",
"pageCount": String(paging.pageCount),
"pageIndex": String(paging.pageIndex),
"requestId": paging.requestId
};
// Note: the Cookie header is a forbidden header in fetch —
// the browser manages cookies automatically via credentials: 'include'.
if (opts.auth?.password) {
headers["trilium-cred"] = btoa(`dummy:${opts.auth.password}`);
}
let body: string | undefined;
if (opts.body) {
body = typeof opts.body === "object" ? JSON.stringify(opts.body) : opts.body;
}
const controller = new AbortController();
const timeoutId = opts.timeout
? setTimeout(() => controller.abort(), opts.timeout)
: undefined;
try {
const response = await fetch(opts.url, {
method: opts.method,
headers,
body,
signal: controller.signal,
credentials: "include"
});
if ([200, 201, 204].includes(response.status)) {
const text = await response.text();
return text.trim() ? JSON.parse(text) : null;
}
const text = await response.text();
let errorMessage: string;
try {
const json = JSON.parse(text);
errorMessage = json?.message || "";
} catch {
errorMessage = text.substring(0, 100);
}
throw new Error(`${response.status} ${opts.method} ${opts.url}: ${errorMessage}`);
} catch (e: any) {
if (e.name === "AbortError") {
throw new Error(`${opts.method} ${opts.url} failed, error: timeout after ${opts.timeout}ms`);
}
if (e instanceof TypeError && e.message === "Failed to fetch") {
const isCrossOrigin = !opts.url.startsWith(location.origin);
if (isCrossOrigin) {
throw new Error(`Request to ${opts.url} was blocked. The server may not allow requests from this origin (CORS), or it may be unreachable.`);
}
throw new Error(`Request to ${opts.url} failed. The server may be unreachable.`);
}
throw e;
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
}
}
async getImage(imageUrl: string): Promise<ArrayBuffer> {
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error(`${response.status} GET ${imageUrl} failed`);
}
return await response.arrayBuffer();
}
}

View File

@@ -0,0 +1,628 @@
import { type BindableValue, default as sqlite3InitModule } from "@sqlite.org/sqlite-wasm";
import type { DatabaseProvider, RunResult, Statement, Transaction } from "@triliumnext/core";
// Type definitions for SQLite WASM (the library doesn't export these directly)
type Sqlite3Module = Awaited<ReturnType<typeof sqlite3InitModule>>;
type Sqlite3Database = InstanceType<Sqlite3Module["oo1"]["DB"]>;
type Sqlite3PreparedStatement = ReturnType<Sqlite3Database["prepare"]>;
/**
* Wraps an SQLite WASM PreparedStatement to match the Statement interface
* expected by trilium-core.
*/
class WasmStatement implements Statement {
private isRawMode = false;
private isPluckMode = false;
private isFinalized = false;
constructor(
private stmt: Sqlite3PreparedStatement,
private db: Sqlite3Database,
private sqlite3: Sqlite3Module,
private sql: string
) {}
run(...params: unknown[]): RunResult {
if (this.isFinalized) {
throw new Error("Cannot call run() on finalized statement");
}
this.bindParams(params);
try {
// Use step() and then reset instead of stepFinalize()
// 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: typeof lastInsertRowid === "bigint" ? Number(lastInsertRowid) : lastInsertRowid
};
} catch (e) {
// Reset on error to allow reuse
this.stmt.reset();
throw e;
}
}
get(params: unknown): unknown {
if (this.isFinalized) {
throw new Error("Cannot call get() on finalized statement");
}
this.bindParams(Array.isArray(params) ? params : params !== undefined ? [params] : []);
try {
if (this.stmt.step()) {
if (this.isPluckMode) {
// In pluck mode, return only the first column value
const row = this.stmt.get([]);
return Array.isArray(row) && row.length > 0 ? row[0] : undefined;
}
return this.isRawMode ? this.stmt.get([]) : this.stmt.get({});
}
return undefined;
} finally {
this.stmt.reset();
}
}
all(...params: unknown[]): unknown[] {
if (this.isFinalized) {
throw new Error("Cannot call all() on finalized statement");
}
this.bindParams(params);
const results: unknown[] = [];
try {
while (this.stmt.step()) {
if (this.isPluckMode) {
// In pluck mode, return only the first column value for each row
const row = this.stmt.get([]);
if (Array.isArray(row) && row.length > 0) {
results.push(row[0]);
}
} else {
results.push(this.isRawMode ? this.stmt.get([]) : this.stmt.get({}));
}
}
return results;
} finally {
this.stmt.reset();
}
}
iterate(...params: unknown[]): IterableIterator<unknown> {
if (this.isFinalized) {
throw new Error("Cannot call iterate() on finalized statement");
}
this.bindParams(params);
const stmt = this.stmt;
const isRaw = this.isRawMode;
const isPluck = this.isPluckMode;
return {
[Symbol.iterator]() {
return this;
},
next(): IteratorResult<unknown> {
if (stmt.step()) {
if (isPluck) {
const row = stmt.get([]);
const value = Array.isArray(row) && row.length > 0 ? row[0] : undefined;
return { value, done: false };
}
return { value: isRaw ? stmt.get([]) : stmt.get({}), done: false };
}
stmt.reset();
return { value: undefined, done: true };
}
};
}
raw(toggleState?: boolean): this {
// In raw mode, rows are returned as arrays instead of objects
// If toggleState is undefined, enable raw mode (better-sqlite3 behavior)
this.isRawMode = toggleState !== undefined ? toggleState : true;
return this;
}
pluck(toggleState?: boolean): this {
// In pluck mode, only the first column of each row is returned
// If toggleState is undefined, enable pluck mode (better-sqlite3 behavior)
this.isPluckMode = toggleState !== undefined ? toggleState : true;
return this;
}
/**
* Detect the prefix used for a parameter name in the SQL query.
* SQLite supports @name, :name, and $name parameter styles.
* Returns the prefix character, or ':' as default if not found.
*/
private detectParamPrefix(paramName: string): string {
// Search for the parameter with each possible prefix
for (const prefix of [':', '@', '$']) {
// Use word boundary to avoid partial matches
const pattern = new RegExp(`\\${prefix}${paramName}(?![a-zA-Z0-9_])`);
if (pattern.test(this.sql)) {
return prefix;
}
}
// Default to ':' if not found (most common in Trilium)
return ':';
}
private bindParams(params: unknown[]): void {
this.stmt.clearBindings();
if (params.length === 0) {
return;
}
// Handle single object with named parameters
if (params.length === 1 && typeof params[0] === "object" && params[0] !== null && !Array.isArray(params[0])) {
const inputBindings = params[0] as { [paramName: string]: BindableValue };
// SQLite WASM expects parameter names to include the prefix (@ : or $)
// We detect the prefix used in the SQL for each parameter
const bindings: { [paramName: string]: BindableValue } = {};
for (const [key, value] of Object.entries(inputBindings)) {
// If the key already has a prefix, use it as-is
if (key.startsWith('@') || key.startsWith(':') || key.startsWith('$')) {
bindings[key] = value;
} else {
// Detect the prefix used in the SQL and apply it
const prefix = this.detectParamPrefix(key);
bindings[`${prefix}${key}`] = value;
}
}
this.stmt.bind(bindings);
} else {
// Handle positional parameters - flatten and cast to BindableValue[]
const flatParams = params.flat() as BindableValue[];
if (flatParams.length > 0) {
this.stmt.bind(flatParams);
}
}
}
finalize(): void {
if (!this.isFinalized) {
try {
this.stmt.finalize();
} catch (e) {
console.warn("Error finalizing SQLite statement:", e);
} finally {
this.isFinalized = true;
}
}
}
}
/**
* SQLite database provider for browser environments using SQLite WASM.
*
* This provider wraps the official @sqlite.org/sqlite-wasm package to provide
* a DatabaseProvider implementation compatible with trilium-core.
*
* @example
* ```typescript
* const provider = new BrowserSqlProvider();
* await provider.initWasm(); // Initialize SQLite WASM module
* provider.loadFromMemory(); // Open an in-memory database
* // or
* provider.loadFromBuffer(existingDbBuffer); // Load from existing data
* ```
*/
export default class BrowserSqlProvider implements DatabaseProvider {
private db?: Sqlite3Database;
private sqlite3?: Sqlite3Module;
private _inTransaction = false;
private initPromise?: Promise<void>;
private initError?: Error;
private statementCache: Map<string, WasmStatement> = new Map();
// OPFS state tracking
private opfsDbPath?: string;
/**
* Get the SQLite WASM module version info.
* Returns undefined if the module hasn't been initialized yet.
*/
get version(): { libVersion: string; sourceId: string } | undefined {
return this.sqlite3?.version;
}
/**
* Initialize the SQLite WASM module.
* This must be called before using any database operations.
* Safe to call multiple times - subsequent calls return the same promise.
*
* @returns A promise that resolves when the module is initialized
* @throws Error if initialization fails
*/
async initWasm(): Promise<void> {
// Return existing promise if already initializing/initialized
if (this.initPromise) {
return this.initPromise;
}
// Fail fast if we already tried and failed
if (this.initError) {
throw this.initError;
}
this.initPromise = this.doInitWasm();
return this.initPromise;
}
private async doInitWasm(): Promise<void> {
try {
console.log("[BrowserSqlProvider] Initializing SQLite WASM...");
const startTime = performance.now();
this.sqlite3 = await sqlite3InitModule({
print: console.log,
printErr: console.error,
});
const initTime = performance.now() - startTime;
console.log(
`[BrowserSqlProvider] SQLite WASM initialized in ${initTime.toFixed(2)}ms:`,
this.sqlite3.version.libVersion
);
} catch (e) {
this.initError = e instanceof Error ? e : new Error(String(e));
console.error("[BrowserSqlProvider] SQLite WASM initialization failed:", this.initError);
throw this.initError;
}
}
/**
* Check if the SQLite WASM module has been initialized.
*/
get isInitialized(): boolean {
return this.sqlite3 !== undefined;
}
// ==================== OPFS Support ====================
/**
* Check if the OPFS VFS is available.
* This requires:
* - Running in a Worker context
* - Browser support for OPFS APIs
* - COOP/COEP headers sent by the server (for SharedArrayBuffer)
*
* @returns true if OPFS VFS is available for use
*/
isOpfsAvailable(): boolean {
this.ensureSqlite3();
// SQLite WASM automatically installs the OPFS VFS if the environment supports it
// We can check for its presence via sqlite3_vfs_find or the OpfsDb class
return this.sqlite3!.oo1.OpfsDb !== undefined;
}
/**
* Load or create a database stored in OPFS for persistent storage.
* The database will persist across browser sessions.
*
* Requires COOP/COEP headers to be set by the server:
* - Cross-Origin-Opener-Policy: same-origin
* - Cross-Origin-Embedder-Policy: require-corp
*
* @param path - The path for the database file in OPFS (e.g., "/trilium.db")
* Paths without a leading slash are treated as relative to OPFS root.
* Leading directories are created automatically.
* @param options - Additional options
* @throws Error if OPFS VFS is not available
*
* @example
* ```typescript
* const provider = new BrowserSqlProvider();
* await provider.initWasm();
* if (provider.isOpfsAvailable()) {
* provider.loadFromOpfs("/my-database.db");
* } else {
* console.warn("OPFS not available, using in-memory database");
* provider.loadFromMemory();
* }
* ```
*/
loadFromOpfs(path: string, options: { createIfNotExists?: boolean } = {}): void {
this.ensureSqlite3();
if (!this.isOpfsAvailable()) {
throw new Error(
"OPFS VFS is not available. This requires:\n" +
"1. Running in a Worker context\n" +
"2. Browser support for OPFS (Chrome 102+, Firefox 111+, Safari 17+)\n" +
"3. COOP/COEP headers from the server:\n" +
" Cross-Origin-Opener-Policy: same-origin\n" +
" Cross-Origin-Embedder-Policy: require-corp"
);
}
console.log(`[BrowserSqlProvider] Loading database from OPFS: ${path}`);
const startTime = performance.now();
try {
// OpfsDb automatically creates directories in the path
// Mode 'c' = create if not exists
const mode = options.createIfNotExists !== false ? 'c' : '';
this.db = new this.sqlite3!.oo1.OpfsDb(path, mode);
this.opfsDbPath = path;
// Configure the database for OPFS
// Note: WAL mode requires exclusive locking in OPFS environment
this.db.exec("PRAGMA journal_mode = DELETE");
this.db.exec("PRAGMA synchronous = NORMAL");
const loadTime = performance.now() - startTime;
console.log(`[BrowserSqlProvider] OPFS database loaded in ${loadTime.toFixed(2)}ms`);
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
console.error(`[BrowserSqlProvider] Failed to load OPFS database: ${error.message}`);
throw error;
}
}
/**
* Check if the currently open database is stored in OPFS.
*/
get isUsingOpfs(): boolean {
return this.opfsDbPath !== undefined;
}
/**
* Get the OPFS path of the currently open database.
* Returns undefined if not using OPFS.
*/
get currentOpfsPath(): string | undefined {
return this.opfsDbPath;
}
/**
* Check if the database has been initialized with a schema.
* This is a simple sanity check that looks for the existence of core tables.
*
* @returns true if the database appears to be initialized
*/
isDbInitialized(): boolean {
this.ensureDb();
// Check if the 'notes' table exists (a core table that must exist in an initialized DB)
const tableExists = this.db!.selectValue(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'notes'"
);
return tableExists !== undefined;
}
// ==================== End OPFS Support ====================
loadFromFile(_path: string, _isReadOnly: boolean): void {
// Browser environment doesn't have direct file system access.
// Use OPFS for persistent storage.
throw new Error(
"loadFromFile is not supported in browser environment. " +
"Use loadFromMemory() for temporary databases, loadFromBuffer() to load from data, " +
"or loadFromOpfs() for persistent storage."
);
}
/**
* Create an empty in-memory database.
* Data will be lost when the page is closed.
*
* For persistent storage, use loadFromOpfs() instead.
* To load demo data, call initializeDemoDatabase() after this.
*/
loadFromMemory(): void {
this.ensureSqlite3();
console.log("[BrowserSqlProvider] Creating in-memory database...");
const startTime = performance.now();
this.db = new this.sqlite3!.oo1.DB(":memory:", "c");
this.opfsDbPath = undefined; // Not using OPFS
this.db.exec("PRAGMA journal_mode = WAL");
const loadTime = performance.now() - startTime;
console.log(`[BrowserSqlProvider] In-memory database created in ${loadTime.toFixed(2)}ms`);
}
loadFromBuffer(buffer: Uint8Array): void {
this.ensureSqlite3();
// SQLite WASM's allocFromTypedArray rejects Node's Buffer (and other
// non-Uint8Array typed arrays) with "expecting 8/16/32/64". Normalize
// to a plain Uint8Array view over the same memory so callers can pass
// anything readFileSync returns.
const view = buffer instanceof Uint8Array && buffer.constructor === Uint8Array
? buffer
: new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
const p = this.sqlite3!.wasm.allocFromTypedArray(view);
try {
// Cached statements reference the previous DB and become invalid
// once we swap connections. Drop them so callers re-prepare.
this.clearStatementCache();
this.db = new this.sqlite3!.oo1.DB({ filename: ":memory:", flags: "c" });
this.opfsDbPath = undefined; // Not using OPFS
const rc = this.sqlite3!.capi.sqlite3_deserialize(
this.db.pointer!,
"main",
p,
view.byteLength,
view.byteLength,
this.sqlite3!.capi.SQLITE_DESERIALIZE_FREEONCLOSE |
this.sqlite3!.capi.SQLITE_DESERIALIZE_RESIZEABLE
);
if (rc !== 0) {
throw new Error(`Failed to deserialize database: ${rc}`);
}
} catch (e) {
this.sqlite3!.wasm.dealloc(p);
throw e;
}
}
backup(_destinationFile: string): void {
// In browser, we can serialize the database to a byte array
// For actual file backup, we'd need to use File System Access API or download
throw new Error(
"backup to file is not supported in browser environment. " +
"Use serialize() to get the database as a Uint8Array instead."
);
}
/**
* Serialize the database to a byte array.
* This can be used to save the database to IndexedDB, download it, etc.
*/
serialize(): Uint8Array {
this.ensureDb();
// Use the convenience wrapper which handles all the memory management
return this.sqlite3!.capi.sqlite3_js_db_export(this.db!);
}
prepare(query: string): Statement {
this.ensureDb();
// Check if we already have this statement cached
if (this.statementCache.has(query)) {
return this.statementCache.get(query)!;
}
// Create new statement and cache it
const stmt = this.db!.prepare(query);
const wasmStatement = new WasmStatement(stmt, this.db!, this.sqlite3!, query);
this.statementCache.set(query, wasmStatement);
return wasmStatement;
}
transaction<T>(func: (statement: Statement) => T): Transaction {
this.ensureDb();
const self = this;
let savepointCounter = 0;
// Helper function to execute within a transaction
const executeTransaction = (beginStatement: string, ...args: unknown[]): T => {
// If we're already in a transaction (either tracked via JS flag or via actual SQLite
// autocommit state), use SAVEPOINTs for nesting — this handles the case where a manual
// BEGIN was issued directly (e.g. transactionalAsync) without going through transaction().
const sqliteInTransaction = self.db?.pointer !== undefined
&& (self.sqlite3!.capi as any).sqlite3_get_autocommit(self.db!.pointer) === 0;
if (self._inTransaction || sqliteInTransaction) {
const savepointName = `sp_${++savepointCounter}_${Date.now()}`;
self.db!.exec(`SAVEPOINT ${savepointName}`);
try {
const result = func.apply(null, args as [Statement]);
self.db!.exec(`RELEASE SAVEPOINT ${savepointName}`);
return result;
} catch (e) {
self.db!.exec(`ROLLBACK TO SAVEPOINT ${savepointName}`);
throw e;
}
}
// Not in a transaction, start a new one
self._inTransaction = true;
self.db!.exec(beginStatement);
try {
const result = func.apply(null, args as [Statement]);
self.db!.exec("COMMIT");
return result;
} catch (e) {
self.db!.exec("ROLLBACK");
throw e;
} finally {
self._inTransaction = false;
}
};
// Create the transaction function that acts like better-sqlite3's Transaction interface
// In better-sqlite3, the transaction function is callable and has .deferred(), .immediate(), etc.
const transactionWrapper = Object.assign(
// Default call executes with BEGIN (same as immediate)
(...args: unknown[]): T => executeTransaction("BEGIN", ...args),
{
// Deferred transaction - locks acquired on first data access
deferred: (...args: unknown[]): T => executeTransaction("BEGIN DEFERRED", ...args),
// Immediate transaction - acquires write lock immediately
immediate: (...args: unknown[]): T => executeTransaction("BEGIN IMMEDIATE", ...args),
// Exclusive transaction - exclusive lock
exclusive: (...args: unknown[]): T => executeTransaction("BEGIN EXCLUSIVE", ...args),
// Default is same as calling directly
default: (...args: unknown[]): T => executeTransaction("BEGIN", ...args)
}
);
return transactionWrapper as unknown as Transaction;
}
get inTransaction(): boolean {
return this._inTransaction;
}
exec(query: string): void {
this.ensureDb();
this.db!.exec(query);
}
private clearStatementCache(): void {
for (const statement of this.statementCache.values()) {
try {
statement.finalize();
} catch (e) {
// Ignore errors during cleanup
console.warn("Error finalizing statement during cleanup:", e);
}
}
this.statementCache.clear();
}
close(): void {
this.clearStatementCache();
if (this.db) {
this.db.close();
this.db = undefined;
}
// Reset OPFS state
this.opfsDbPath = undefined;
}
/**
* Get the number of rows changed by the last INSERT, UPDATE, or DELETE statement.
*/
changes(): number {
this.ensureDb();
return this.db!.changes();
}
/**
* Check if the database is currently open.
*/
isOpen(): boolean {
return this.db !== undefined && this.db.isOpen();
}
private ensureSqlite3(): void {
if (!this.sqlite3) {
throw new Error(
"SQLite WASM module not initialized. Call initialize() first with the sqlite3 module."
);
}
}
private ensureDb(): void {
this.ensureSqlite3();
if (!this.db) {
throw new Error("Database not opened. Call loadFromMemory(), loadFromBuffer(), or loadFromOpfs() first.");
}
}
}

View File

@@ -0,0 +1,16 @@
import { LOCALE_IDS } from "@triliumnext/commons";
import type i18next from "i18next";
import I18NextHttpBackend from "i18next-http-backend";
export default async function translationProvider(i18nextInstance: typeof i18next, locale: LOCALE_IDS) {
await i18nextInstance.use(I18NextHttpBackend).init({
lng: locale,
fallbackLng: "en",
ns: "server",
backend: {
loadPath: `${import.meta.resolve("../server-assets/translations")}/{{lng}}/{{ns}}.json`
},
returnEmptyString: false,
debug: true
});
}

View File

@@ -0,0 +1,18 @@
import { type ExportFormat, type ZipExportProviderData, ZipExportProvider } from "@triliumnext/core";
import contentCss from "@triliumnext/ckeditor5/src/theme/ck-content.css?raw";
export async function standaloneZipExportProviderFactory(format: ExportFormat, data: ZipExportProviderData): Promise<ZipExportProvider> {
switch (format) {
case "html": {
const { default: HtmlExportProvider } = await import("@triliumnext/core/src/services/export/zip/html.js");
return new HtmlExportProvider(data, { contentCss });
}
case "markdown": {
const { default: MarkdownExportProvider } = await import("@triliumnext/core/src/services/export/zip/markdown.js");
return new MarkdownExportProvider(data);
}
default:
throw new Error(`Unsupported export format: '${format}'`);
}
}

View File

@@ -0,0 +1,101 @@
import type { FileStream, ZipArchive, ZipEntry, ZipProvider } from "@triliumnext/core/src/services/zip_provider.js";
import { strToU8, unzip, zipSync } from "fflate";
type ZipOutput = {
send?: (body: unknown) => unknown;
write?: (chunk: Uint8Array | string) => unknown;
end?: (chunk?: Uint8Array | string) => unknown;
};
class BrowserZipArchive implements ZipArchive {
readonly #entries: Record<string, Uint8Array> = {};
#destination: ZipOutput | null = null;
append(content: string | Uint8Array, options: { name: string }) {
this.#entries[options.name] = typeof content === "string" ? strToU8(content) : content;
}
pipe(destination: unknown) {
this.#destination = destination as ZipOutput;
}
async finalize(): Promise<void> {
if (!this.#destination) {
throw new Error("ZIP output destination not set.");
}
const content = zipSync(this.#entries, { level: 9 });
if (typeof this.#destination.send === "function") {
this.#destination.send(content);
return;
}
if (typeof this.#destination.end === "function") {
if (typeof this.#destination.write === "function") {
this.#destination.write(content);
this.#destination.end();
} else {
this.#destination.end(content);
}
return;
}
throw new Error("Unsupported ZIP output destination.");
}
}
export default class BrowserZipProvider implements ZipProvider {
createZipArchive(): ZipArchive {
return new BrowserZipArchive();
}
createFileStream(_filePath: string): FileStream {
throw new Error("File stream creation is not supported in the browser.");
}
readZipFile(
buffer: Uint8Array,
processEntry: (entry: ZipEntry, readContent: () => Promise<Uint8Array>) => Promise<void>
): Promise<void> {
return new Promise<void>((res, rej) => {
unzip(buffer, async (err, files) => {
if (err) { rej(err); return; }
try {
for (const [fileName, data] of Object.entries(files)) {
await processEntry(
{ fileName: decodeZipFileName(fileName) },
() => Promise.resolve(data)
);
}
res();
} catch (e) {
rej(e);
}
});
});
}
}
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
/**
* fflate decodes ZIP entry filenames as CP437/Latin-1 unless the language
* encoding flag (general purpose bit 11) is set, but many real-world archives
* (e.g. those produced by macOS / Linux unzip / Python's zipfile) write UTF-8
* filenames without setting that flag. Recover the original UTF-8 bytes from
* fflate's per-byte string and re-decode them; if the result isn't valid
* UTF-8 we fall back to the as-decoded name.
*/
function decodeZipFileName(name: string): string {
const bytes = new Uint8Array(name.length);
for (let i = 0; i < name.length; i++) {
bytes[i] = name.charCodeAt(i) & 0xff;
}
try {
return utf8Decoder.decode(bytes);
} catch {
return name;
}
}

View File

@@ -0,0 +1,110 @@
import LocalServerWorker from "./local-server-worker?worker";
let localWorker: Worker | null = null;
const pending = new Map();
function showFatalErrorDialog(message: string) {
alert(message);
}
export function startLocalServerWorker() {
if (localWorker) return localWorker;
localWorker = new LocalServerWorker();
localWorker.postMessage({ type: "INIT", queryString: location.search });
// Handle worker errors during initialization
localWorker.onerror = (event) => {
console.error("[LocalBridge] Worker error:", event);
// Reject all pending requests
for (const [, resolver] of pending) {
resolver.reject(new Error(`Worker error: ${event.message}`));
}
pending.clear();
};
localWorker.onmessage = (event) => {
const msg = event.data;
// Handle fatal platform crashes (shown as a dialog to the user)
if (msg?.type === "FATAL_ERROR") {
console.error("[LocalBridge] Fatal error:", msg.message);
showFatalErrorDialog(msg.message);
return;
}
// 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 [, resolver] of pending) {
resolver.reject(new Error(msg.error?.message || "Unknown worker error"));
}
pending.clear();
return;
}
// Handle WebSocket-like messages from the worker (for frontend updates)
if (msg?.type === "WS_MESSAGE" && msg.message) {
// Dispatch a custom event that ws.ts listens to in standalone mode
window.dispatchEvent(new CustomEvent("trilium:ws-message", {
detail: msg.message
}));
return;
}
if (!msg || msg.type !== "LOCAL_RESPONSE") return;
const { id, response, error } = msg;
const resolver = pending.get(id);
if (!resolver) return;
pending.delete(id);
if (error) resolver.reject(new Error(error));
else resolver.resolve(response);
};
return localWorker;
}
export function attachServiceWorkerBridge() {
navigator.serviceWorker.addEventListener("message", async (event) => {
const msg = event.data;
if (!msg || msg.type !== "LOCAL_FETCH") return;
const port = event.ports && event.ports[0];
if (!port) return;
try {
startLocalServerWorker();
const id = msg.id;
const req = msg.request;
const response = await new Promise<{ body?: ArrayBuffer }>((resolve, reject) => {
pending.set(id, { resolve, reject });
// Transfer body to worker for efficiency (if present)
localWorker!.postMessage({
type: "LOCAL_REQUEST",
id,
request: req
}, req.body ? [req.body] : []);
});
port.postMessage({
type: "LOCAL_FETCH_RESPONSE",
id,
response
}, response.body ? [response.body] : []);
} catch (e: unknown) {
const errorMessage = e instanceof Error ? e.message : String(e);
port.postMessage({
type: "LOCAL_FETCH_RESPONSE",
id: msg.id,
response: {
status: 500,
headers: { "content-type": "text/plain; charset=utf-8" },
body: new TextEncoder().encode(errorMessage).buffer
}
});
}
});
}

View File

@@ -0,0 +1,303 @@
// =============================================================================
// ERROR HANDLERS FIRST - No static imports above this!
// ES modules hoist static imports, so they execute BEFORE any code runs.
// We use dynamic imports below to ensure error handlers are registered first.
// =============================================================================
self.onerror = (message, source, lineno, colno, error) => {
const errorMsg = `[Worker] Uncaught error: ${message}\n at ${source}:${lineno}:${colno}`;
console.error(errorMsg, error);
try {
self.postMessage({
type: "WORKER_ERROR",
error: {
message: String(message),
source,
lineno,
colno,
stack: error?.stack || new Error().stack
}
});
} catch (e) {
console.error("[Worker] Failed to report error:", e);
}
return false;
};
self.onunhandledrejection = (event) => {
const reason = event.reason;
const errorMsg = `[Worker] Unhandled rejection: ${reason?.message || reason}`;
console.error(errorMsg, reason);
try {
self.postMessage({
type: "WORKER_ERROR",
error: {
message: String(reason?.message || reason),
stack: reason?.stack || new Error().stack
}
});
} catch (e) {
console.error("[Worker] Failed to report rejection:", e);
}
};
console.log("[Worker] Error handlers installed, loading modules...");
// =============================================================================
// TYPE-ONLY IMPORTS (erased at runtime, safe as static imports)
// =============================================================================
import type { BrowserRouter } from './lightweight/browser_router';
// =============================================================================
// MODULE STATE (populated by dynamic imports)
// =============================================================================
let BrowserSqlProvider: typeof import('./lightweight/sql_provider').default;
let WorkerMessagingProvider: typeof import('./lightweight/messaging_provider').default;
let BrowserExecutionContext: typeof import('./lightweight/cls_provider').default;
let BrowserCryptoProvider: typeof import('./lightweight/crypto_provider').default;
let BrowserZipProvider: typeof import('./lightweight/zip_provider').default;
let FetchRequestProvider: typeof import('./lightweight/request_provider').default;
let StandalonePlatformProvider: typeof import('./lightweight/platform_provider').default;
let StandaloneLogService: typeof import('./lightweight/log_provider').default;
let StandaloneBackupService: typeof import('./lightweight/backup_provider').default;
let translationProvider: typeof import('./lightweight/translation_provider').default;
let createConfiguredRouter: typeof import('./lightweight/browser_routes').createConfiguredRouter;
// Instance state
let sqlProvider: InstanceType<typeof BrowserSqlProvider> | null = null;
let messagingProvider: InstanceType<typeof WorkerMessagingProvider> | null = null;
// Core module, router, and initialization state
let coreModule: typeof import("@triliumnext/core") | null = null;
let router: BrowserRouter | null = null;
let initPromise: Promise<void> | null = null;
let initError: Error | null = null;
let queryString = "";
/**
* Load all required modules using dynamic imports.
* This allows errors to be caught by our error handlers.
*/
async function loadModules(): Promise<void> {
console.log("[Worker] Loading lightweight modules...");
const [
sqlModule,
messagingModule,
clsModule,
cryptoModule,
zipModule,
requestModule,
platformModule,
logModule,
backupModule,
translationModule,
routesModule
] = await Promise.all([
import('./lightweight/sql_provider.js'),
import('./lightweight/messaging_provider.js'),
import('./lightweight/cls_provider.js'),
import('./lightweight/crypto_provider.js'),
import('./lightweight/zip_provider.js'),
import('./lightweight/request_provider.js'),
import('./lightweight/platform_provider.js'),
import('./lightweight/log_provider.js'),
import('./lightweight/backup_provider.js'),
import('./lightweight/translation_provider.js'),
import('./lightweight/browser_routes.js')
]);
BrowserSqlProvider = sqlModule.default;
WorkerMessagingProvider = messagingModule.default;
BrowserExecutionContext = clsModule.default;
BrowserCryptoProvider = cryptoModule.default;
BrowserZipProvider = zipModule.default;
FetchRequestProvider = requestModule.default;
StandalonePlatformProvider = platformModule.default;
StandaloneLogService = logModule.default;
StandaloneBackupService = backupModule.default;
translationProvider = translationModule.default;
createConfiguredRouter = routesModule.createConfiguredRouter;
// Create instances
sqlProvider = new BrowserSqlProvider();
messagingProvider = new WorkerMessagingProvider();
console.log("[Worker] Lightweight modules loaded successfully");
}
/**
* Initialize SQLite WASM and load the core module.
* This happens once at worker startup.
*/
async function initialize(): Promise<void> {
if (initPromise) {
return initPromise; // Already initializing
}
if (initError) {
throw initError; // Failed before, don't retry
}
initPromise = (async () => {
try {
// First, load all modules dynamically
await loadModules();
console.log("[Worker] Initializing SQLite WASM...");
await sqlProvider!.initWasm();
// Try to use OPFS for persistent storage
if (sqlProvider!.isOpfsAvailable()) {
console.log("[Worker] OPFS available, loading persistent database...");
sqlProvider!.loadFromOpfs("/trilium.db");
} else {
// Fall back to in-memory database (non-persistent)
console.warn("[Worker] OPFS not available, using in-memory database (data will not persist)");
console.warn("[Worker] To enable persistence, ensure COOP/COEP headers are set by the server");
sqlProvider!.loadFromMemory();
}
console.log("[Worker] Database loaded");
console.log("[Worker] Loading @triliumnext/core...");
const schemaModule = await import("@triliumnext/core/src/assets/schema.sql?raw");
coreModule = await import("@triliumnext/core");
// Initialize log service with OPFS persistence
const logService = new StandaloneLogService();
await logService.initialize();
console.log("[Worker] Log service initialized with OPFS");
await coreModule.initializeCore({
executionContext: new BrowserExecutionContext(),
crypto: new BrowserCryptoProvider(),
zip: new BrowserZipProvider(),
zipExportProviderFactory: (await import("./lightweight/zip_export_provider_factory.js")).standaloneZipExportProviderFactory,
messaging: messagingProvider!,
request: new FetchRequestProvider(),
platform: new StandalonePlatformProvider(queryString),
log: logService,
backup: new StandaloneBackupService(coreModule!.options),
translations: translationProvider,
schema: schemaModule.default,
getDemoArchive: async () => {
const response = await fetch("/server-assets/db/demo.zip");
if (!response.ok) return null;
return new Uint8Array(await response.arrayBuffer());
},
image: (await import("./services/image_provider.js")).standaloneImageProvider,
dbConfig: {
provider: sqlProvider!,
isReadOnly: false,
onTransactionCommit: () => {
coreModule?.ws.sendTransactionEntityChangesToAllClients();
},
onTransactionRollback: () => {
// No-op for now
}
}
});
coreModule.ws.init();
console.log("[Worker] Supported routes", Object.keys(coreModule.routes));
// Create and configure the router
router = createConfiguredRouter();
console.log("[Worker] Router configured");
// initializeDb runs initDbConnection inside an execution context,
// which resolves dbReady — required before beccaLoaded can settle.
coreModule.sql_init.initializeDb();
if (coreModule.sql_init.isDbInitialized()) {
console.log("[Worker] Database already initialized, loading becca...");
await coreModule.becca_loader.beccaLoaded;
} else {
console.log("[Worker] Database not initialized, skipping becca load (will be loaded during DB initialization)");
}
coreModule.scheduler.startScheduler();
console.log("[Worker] Initialization complete");
} catch (error) {
initError = error instanceof Error ? error : new Error(String(error));
console.error("[Worker] Initialization failed:", initError);
throw initError;
}
})();
return initPromise;
}
/**
* Ensure the worker is initialized before processing requests.
* Returns the router if initialization was successful.
*/
async function ensureInitialized() {
await initialize();
if (!router) {
throw new Error("Router not initialized");
}
return router;
}
interface LocalRequest {
method: string;
url: string;
body?: unknown;
headers?: Record<string, string>;
}
// Main dispatch
async function dispatch(request: LocalRequest) {
// Ensure initialization is complete and get the router
const appRouter = await ensureInitialized();
// Dispatch to the router
return appRouter.dispatch(request.method, request.url, request.body, request.headers);
}
// Start initialization immediately when the worker loads
console.log("[Worker] Starting initialization...");
initialize().catch(err => {
console.error("[Worker] Initialization failed:", err);
// Post error to main thread
self.postMessage({
type: "WORKER_ERROR",
error: {
message: String(err?.message || err),
stack: err?.stack
}
});
});
self.onmessage = async (event) => {
const msg = event.data;
if (!msg) return;
if (msg.type === "INIT") {
queryString = msg.queryString || "";
return;
}
if (msg.type !== "LOCAL_REQUEST") return;
const { id, request } = msg;
try {
const response = await dispatch(request);
// Transfer body back (if any) - use options object for proper typing
(self as unknown as Worker).postMessage({
type: "LOCAL_RESPONSE",
id,
response
}, { transfer: response.body ? [response.body] : [] });
} catch (e) {
console.error("[Worker] Dispatch error:", e);
(self as unknown as Worker).postMessage({
type: "LOCAL_RESPONSE",
id,
error: String((e as Error)?.message || e)
});
}
};

View File

@@ -0,0 +1,84 @@
import { attachServiceWorkerBridge, startLocalServerWorker } from "./local-bridge.js";
async function waitForServiceWorkerControl(): Promise<void> {
if (!("serviceWorker" in navigator)) {
throw new Error("Service Worker not supported in this browser");
}
// If already controlling, we're good
if (navigator.serviceWorker.controller) {
console.log("[Bootstrap] Service worker already controlling");
return;
}
console.log("[Bootstrap] Waiting for service worker to take control...");
// Register service worker
await navigator.serviceWorker.register("./sw.js", { scope: "/" });
// Wait for it to be ready (installed + activated)
await navigator.serviceWorker.ready;
// Check if we're now controlling
if (navigator.serviceWorker.controller) {
console.log("[Bootstrap] Service worker now controlling");
return;
}
// If not controlling yet, we need to reload the page for SW to take control
// This is standard PWA behavior on first install
console.log("[Bootstrap] Service worker installed but not controlling yet - reloading page");
// Wait a tiny bit for SW to fully activate
await new Promise(resolve => setTimeout(resolve, 100));
// Reload to let SW take control
window.location.reload();
// Throw to stop execution (page will reload)
throw new Error("Reloading for service worker activation");
}
async function bootstrap() {
/* fixes https://github.com/webpack/webpack/issues/10035 */
window.global = globalThis;
try {
// 1) Start local worker ASAP (so /bootstrap is fast)
startLocalServerWorker();
// 2) Bridge SW -> local worker
attachServiceWorkerBridge();
// 3) Wait for service worker to control the page (may reload on first install)
await waitForServiceWorkerControl();
await loadScripts();
} catch (err) {
// If error is from reload, it will stop here (page reloads)
// Otherwise, show error to user
if (err instanceof Error && err.message.includes("Reloading")) {
// Page is reloading, do nothing
return;
}
console.error("[Bootstrap] Fatal error:", err);
document.body.innerHTML = `
<div style="padding: 40px; max-width: 600px; margin: 0 auto; font-family: system-ui, sans-serif;">
<h1 style="color: #d32f2f;">Failed to Initialize</h1>
<p>The application failed to start. Please check the browser console for details.</p>
<pre style="background: #f5f5f5; padding: 16px; border-radius: 4px; overflow: auto;">${err instanceof Error ? err.message : String(err)}</pre>
<button onclick="location.reload()" style="padding: 12px 24px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px;">
Reload Page
</button>
</div>
`;
document.body.style.display = "block";
}
}
async function loadScripts() {
await import("../../client/src/index.js");
}
bootstrap();

View File

@@ -0,0 +1,67 @@
import { describe, it, expect } from "vitest";
import { data_encryption } from "@triliumnext/core";
// Note: BrowserCryptoProvider is already initialized via test_setup.ts
describe("data_encryption with BrowserCryptoProvider", () => {
it("should encrypt and decrypt ASCII text correctly", () => {
const key = new Uint8Array(16).fill(42);
const plainText = "Hello, World!";
const encrypted = data_encryption.encrypt(key, plainText);
expect(typeof encrypted).toBe("string");
expect(encrypted.length).toBeGreaterThan(0);
const decrypted = data_encryption.decryptString(key, encrypted);
expect(decrypted).toBe(plainText);
});
it("should encrypt and decrypt UTF-8 text correctly", () => {
const key = new Uint8Array(16).fill(42);
const plainText = "Привет мир! 你好世界! 🎉";
const encrypted = data_encryption.encrypt(key, plainText);
const decrypted = data_encryption.decryptString(key, encrypted);
expect(decrypted).toBe(plainText);
});
it("should encrypt and decrypt empty string", () => {
const key = new Uint8Array(16).fill(42);
const plainText = "";
const encrypted = data_encryption.encrypt(key, plainText);
const decrypted = data_encryption.decryptString(key, encrypted);
expect(decrypted).toBe(plainText);
});
it("should encrypt and decrypt binary data", () => {
const key = new Uint8Array(16).fill(42);
const plainData = new Uint8Array([0, 1, 2, 255, 128, 64]);
const encrypted = data_encryption.encrypt(key, plainData);
const decrypted = data_encryption.decrypt(key, encrypted);
expect(decrypted).toBeInstanceOf(Uint8Array);
expect(Array.from(decrypted as Uint8Array)).toEqual(Array.from(plainData));
});
it("should fail decryption with wrong key", () => {
const key1 = new Uint8Array(16).fill(42);
const key2 = new Uint8Array(16).fill(43);
const plainText = "Secret message";
const encrypted = data_encryption.encrypt(key1, plainText);
// decrypt returns false when digest doesn't match
const result = data_encryption.decrypt(key2, encrypted);
expect(result).toBe(false);
});
it("should handle large content", () => {
const key = new Uint8Array(16).fill(42);
const plainText = "x".repeat(100000);
const encrypted = data_encryption.encrypt(key, plainText);
const decrypted = data_encryption.decryptString(key, encrypted);
expect(decrypted).toBe(plainText);
});
});

View File

@@ -0,0 +1,96 @@
/**
* Standalone image provider implementation.
* Uses pure JavaScript for format detection without compression.
* Images are saved as-is without resizing.
*/
import type { ImageProvider, ImageFormat, ProcessedImage } from "@triliumnext/core";
/**
* Detect image type from buffer using magic bytes.
*/
function getImageTypeFromBuffer(buffer: Uint8Array): ImageFormat | null {
if (buffer.length < 12) {
return null;
}
// Check for SVG (text-based)
if (isSvg(buffer)) {
return { ext: "svg", mime: "image/svg+xml" };
}
// JPEG: FF D8 FF
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
return { ext: "jpg", mime: "image/jpeg" };
}
// PNG: 89 50 4E 47 0D 0A 1A 0A
if (
buffer[0] === 0x89 &&
buffer[1] === 0x50 &&
buffer[2] === 0x4e &&
buffer[3] === 0x47 &&
buffer[4] === 0x0d &&
buffer[5] === 0x0a &&
buffer[6] === 0x1a &&
buffer[7] === 0x0a
) {
return { ext: "png", mime: "image/png" };
}
// GIF: "GIF"
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) {
return { ext: "gif", mime: "image/gif" };
}
// WebP: RIFF....WEBP
if (
buffer[0] === 0x52 &&
buffer[1] === 0x49 &&
buffer[2] === 0x46 &&
buffer[3] === 0x46 &&
buffer[8] === 0x57 &&
buffer[9] === 0x45 &&
buffer[10] === 0x42 &&
buffer[11] === 0x50
) {
return { ext: "webp", mime: "image/webp" };
}
// BMP: "BM"
if (buffer[0] === 0x42 && buffer[1] === 0x4d) {
return { ext: "bmp", mime: "image/bmp" };
}
return null;
}
/**
* Check if buffer contains SVG content.
*/
function isSvg(buffer: Uint8Array): boolean {
const maxBytes = Math.min(buffer.length, 1000);
let str = "";
for (let i = 0; i < maxBytes; i++) {
str += String.fromCharCode(buffer[i]);
}
const trimmed = str.trim().toLowerCase();
return trimmed.startsWith("<svg") || (trimmed.startsWith("<?xml") && trimmed.includes("<svg"));
}
export const standaloneImageProvider: ImageProvider = {
getImageType(buffer: Uint8Array): ImageFormat | null {
return getImageTypeFromBuffer(buffer);
},
async processImage(buffer: Uint8Array, _originalName: string, _shrink: boolean): Promise<ProcessedImage> {
// Standalone doesn't do compression - just detect format and return original
const format = getImageTypeFromBuffer(buffer) || { ext: "dat", mime: "application/octet-stream" };
return {
buffer,
format
};
}
};

View File

@@ -0,0 +1,196 @@
// public/sw.js
const VERSION = "localserver-v1.4";
const STATIC_CACHE = `static-${VERSION}`;
// Check if running in dev mode (passed via URL parameter)
const isDev = true;
if (isDev) {
console.log('[Service Worker] Running in DEV mode - caching disabled');
}
// Adjust these to your routes:
const LOCAL_FIRST_PREFIXES = [
"/bootstrap",
"/api/",
"/sync/",
"/search/"
];
// Optional: basic precache list (keep small; you can expand later)
const PRECACHE_URLS = [
// "/",
// "/index.html",
// "/manifest.webmanifest",
// "/favicon.ico",
];
self.addEventListener("install", (event) => {
event.waitUntil((async () => {
// Skip precaching in dev mode
if (!isDev) {
const cache = await caches.open(STATIC_CACHE);
await cache.addAll(PRECACHE_URLS);
}
self.skipWaiting();
})());
});
self.addEventListener("activate", (event) => {
event.waitUntil((async () => {
// Cleanup old caches
const keys = await caches.keys();
await Promise.all(keys.map((k) => (k === STATIC_CACHE ? Promise.resolve() : caches.delete(k))));
await self.clients.claim();
})());
});
function isLocalFirst(url) {
return LOCAL_FIRST_PREFIXES.some((p) => url.pathname.startsWith(p));
}
async function cacheFirst(request) {
// In dev mode, always bypass cache
if (isDev) {
return fetch(request);
}
const cache = await caches.open(STATIC_CACHE);
const cached = await cache.match(request);
if (cached) return cached;
const fresh = await fetch(request);
// Cache only successful GETs
if (request.method === "GET" && fresh.ok) cache.put(request, fresh.clone());
return fresh;
}
async function networkFirst(request) {
// In dev mode, always bypass cache
if (isDev) {
return fetch(request);
}
const cache = await caches.open(STATIC_CACHE);
try {
const fresh = await fetch(request);
// Cache only successful GETs
if (request.method === "GET" && fresh.ok) cache.put(request, fresh.clone());
return fresh;
} catch (error) {
// Fallback to cache if network fails
const cached = await cache.match(request);
if (cached) return cached;
throw error;
}
}
async function forwardToClientLocalServer(request, _clientId) {
// Find the main app window to handle the request
// We must route to the main app (which has the local bridge), not iframes like PDF.js viewer
// @ts-expect-error - self.clients is valid in service worker context
const all = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
// Find the main app window - it's the one NOT serving pdfjs or other embedded content
// The main app has the local bridge handler for LOCAL_FETCH messages
let client = all.find((c: { url: string }) => {
const url = new URL(c.url);
// Main app is at root or index.html, not in /pdfjs/ or other iframe paths
return !url.pathname.startsWith("/pdfjs/");
}) || null;
// If no main app window found, fall back to any available client
if (!client) {
client = all[0] || null;
}
// If no page is available, fall back to network
if (!client) return fetch(request);
const reqUrl = request.url;
const headersObj = {};
for (const [k, v] of request.headers.entries()) headersObj[k] = v;
const body = (request.method === "GET" || request.method === "HEAD")
? null
: await request.arrayBuffer();
const id = crypto.randomUUID();
const channel = new MessageChannel();
const responsePromise = new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Local server timeout"));
}, 30_000);
channel.port1.onmessage = (event) => {
clearTimeout(timeout);
resolve(event.data);
};
channel.port1.onmessageerror = () => {
clearTimeout(timeout);
reject(new Error("Local server message error"));
};
});
// Send to the client with a reply port
client.postMessage({
type: "LOCAL_FETCH",
id,
request: {
url: reqUrl,
method: request.method,
headers: headersObj,
body // ArrayBuffer or null
}
}, [channel.port2]);
const localResp = await responsePromise;
if (!localResp || localResp.type !== "LOCAL_FETCH_RESPONSE" || localResp.id !== id) {
// Protocol mismatch; fall back
return fetch(request);
}
// localResp.response: { status, headers, body }
const { status, headers, body: respBody } = localResp.response;
const respHeaders = new Headers();
if (headers) {
for (const [k, v] of Object.entries(headers)) respHeaders.set(k, String(v));
}
return new Response(respBody ? respBody : null, {
status: status || 200,
headers: respHeaders
});
}
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
// Only handle same-origin
if (url.origin !== self.location.origin) return;
// API-ish: local-first via bridge (must be checked before navigate handling,
// because export triggers a navigation to an /api/ URL)
if (isLocalFirst(url)) {
event.respondWith(forwardToClientLocalServer(event.request, event.clientId));
return;
}
// HTML files: network-first to ensure updates are reflected immediately
if (event.request.mode === "navigate" || url.pathname.endsWith(".html")) {
event.respondWith(networkFirst(event.request));
return;
}
// Static assets: cache-first for performance
if (event.request.method === "GET") {
event.respondWith(cacheFirst(event.request));
return;
}
// Default
event.respondWith(fetch(event.request));
});

View File

@@ -0,0 +1,142 @@
import { createRequire } from "node:module";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { initializeCore, options } from "@triliumnext/core";
import schemaSql from "@triliumnext/core/src/assets/schema.sql?raw";
import HappyDomHtmlParser from "happy-dom/lib/html-parser/HTMLParser.js";
import serverEnTranslations from "../../server/src/assets/translations/en/server.json";
import { beforeAll } from "vitest";
import StandaloneBackupService from "./lightweight/backup_provider.js";
import BrowserExecutionContext from "./lightweight/cls_provider.js";
import BrowserCryptoProvider from "./lightweight/crypto_provider.js";
import StandalonePlatformProvider from "./lightweight/platform_provider.js";
import BrowserSqlProvider from "./lightweight/sql_provider.js";
import BrowserZipProvider from "./lightweight/zip_provider.js";
// =============================================================================
// SQLite WASM compatibility shims
// =============================================================================
// The @sqlite.org/sqlite-wasm package loads its .wasm via fetch, and its
// bundled `instantiateWasm` hook overrides any user-supplied alternative.
// Two things go wrong under vitest + happy-dom:
// 1. happy-dom's `fetch()` refuses `file://` URLs.
// 2. happy-dom installs its own Response global, which Node's
// `WebAssembly.instantiateStreaming` rejects ("Received an instance of
// Response" — it wants undici's Response).
// We intercept fetch for file:// URLs ourselves and force instantiateStreaming
// to fall back to the ArrayBuffer path.
const fileFetchCache = new Map<string, ArrayBuffer>();
function readFileAsArrayBuffer(url: string): ArrayBuffer {
let cached = fileFetchCache.get(url);
if (!cached) {
const bytes = readFileSync(fileURLToPath(url));
cached = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
fileFetchCache.set(url, cached);
}
return cached;
}
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === "string"
? input
: input instanceof URL
? input.href
: input.url;
if (url.startsWith("file://")) {
const body = readFileAsArrayBuffer(url);
return new Response(body, {
status: 200,
headers: { "Content-Type": "application/wasm" }
});
}
return originalFetch(input as RequestInfo, init);
}) as typeof fetch;
WebAssembly.instantiateStreaming = (async (source, importObject) => {
const response = await source;
const bytes = await response.arrayBuffer();
return WebAssembly.instantiate(bytes, importObject);
}) as typeof WebAssembly.instantiateStreaming;
// =============================================================================
// happy-dom HTMLParser spec compliance patch
// =============================================================================
// Per HTML5 parsing spec, a single U+000A LINE FEED immediately after a <pre>,
// <listing>, or <textarea> start tag must be ignored ("newlines at the start
// of pre blocks are ignored as an authoring convenience"). Real browsers and
// domino (which the server runtime uses via turnish) both implement this;
// happy-dom (as of 20.8.9) does not — it keeps the LF as a text node.
//
// That difference makes turnish's markdown export produce different output
// under happy-dom vs. production, breaking markdown.spec.ts > "exports jQuery
// code in table properly". Patch HTMLParser.parse to pre-process the string.
const LEADING_LF_IN_PRE_RE = /(<(?:pre|listing|textarea)\b[^>]*>)(\r\n|\r|\n)/gi;
const originalHtmlParserParse = (HappyDomHtmlParser as unknown as {
prototype: { parse(html: string, rootNode?: unknown): unknown };
}).prototype.parse;
(HappyDomHtmlParser as unknown as {
prototype: { parse(html: string, rootNode?: unknown): unknown };
}).prototype.parse = function (html: string, rootNode?: unknown) {
const patched = typeof html === "string"
? html.replace(LEADING_LF_IN_PRE_RE, "$1")
: html;
return originalHtmlParserParse.call(this, patched, rootNode);
};
// =============================================================================
// Core initialization for standalone-flavored tests
// =============================================================================
// Mirror what apps/server/spec/setup.ts does: load the pre-seeded integration
// fixture DB into an in-memory sqlite-wasm instance, then initialize core
// against it with the standalone (browser) providers. Each vitest worker gets
// a fresh copy because tests run in forks (per the default pool).
const require = createRequire(import.meta.url);
const fixtureDb = readFileSync(
require.resolve("@triliumnext/core/src/test/fixtures/document.db")
);
beforeAll(async () => {
const sqlProvider = new BrowserSqlProvider();
await sqlProvider.initWasm();
sqlProvider.loadFromBuffer(fixtureDb);
await initializeCore({
executionContext: new BrowserExecutionContext(),
crypto: new BrowserCryptoProvider(),
zip: new BrowserZipProvider(),
zipExportProviderFactory: (
await import("./lightweight/zip_export_provider_factory.js")
).standaloneZipExportProviderFactory,
// i18next must be wired up — keyboard_actions.ts and other modules
// call `t()` and throw if translations are missing. Inline the
// en/server.json resources via vite's JSON import so we don't need a
// backend in tests.
translations: async (i18nextInstance, locale) => {
await i18nextInstance.init({
lng: locale,
fallbackLng: "en",
ns: "server",
defaultNS: "server",
resources: {
en: { server: serverEnTranslations }
}
});
},
platform: new StandalonePlatformProvider(""),
backup: new StandaloneBackupService(options),
schema: schemaSql,
dbConfig: {
provider: sqlProvider,
isReadOnly: false,
onTransactionCommit: () => {},
onTransactionRollback: () => {}
}
});
});

View File

@@ -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"
]
}

View File

@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.spec.json" }
]
}

View File

@@ -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"
]
}

View File

@@ -0,0 +1,277 @@
import fs from "fs";
import { join } from "path";
import prefresh from "@prefresh/vite";
import { defineConfig, type Plugin } from "vite";
import { viteStaticCopy } from "vite-plugin-static-copy";
const clientAssets = ["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 (adjusted for new root)
server.watcher.add("../../client/src/**/*");
server.watcher.on("change", (file: string) => {
if (file.includes("../../client/src/")) {
server.ws.send({
type: "full-reload"
});
}
});
}
}
});
// Serve PDF.js files directly in dev mode to bypass SPA fallback
const pdfjsServePlugin = (): Plugin => ({
name: "pdfjs-serve",
configureServer(server) {
const pdfjsRoot = join(__dirname, "../../packages/pdfjs-viewer/dist");
server.middlewares.use((req, res, next) => {
if (!req.url?.startsWith("/pdfjs/")) {
return next();
}
// Map /pdfjs/web/... to dist/web/...
// Map /pdfjs/build/... to dist/build/...
// Strip query string (e.g., ?v=0.102.2) before resolving path
const urlWithoutQuery = req.url.split("?")[0];
const relativePath = urlWithoutQuery.replace(/^\/pdfjs\//, "");
const filePath = join(pdfjsRoot, relativePath);
// Security: ensure we're still within pdfjsRoot
if (!filePath.startsWith(pdfjsRoot)) {
return next();
}
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
const ext = filePath.split(".").pop() || "";
const mimeTypes: Record<string, string> = {
html: "text/html",
css: "text/css",
js: "application/javascript",
mjs: "application/javascript",
wasm: "application/wasm",
png: "image/png",
svg: "image/svg+xml",
json: "application/json"
};
res.setHeader("Content-Type", mimeTypes[ext] || "application/octet-stream");
// Match isolation headers from main page for iframe compatibility
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
fs.createReadStream(filePath).pipe(res);
} else {
next();
}
});
}
});
// Always copy SQLite WASM files so they're available to the module
const sqliteWasmPlugin = viteStaticCopy({
targets: [
{
src: "../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm",
dest: "assets",
rename: { stripBase: true }
},
{
src: "../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-opfs-async-proxy.js",
dest: "assets",
rename: { stripBase: true }
}
]
});
let plugins: any = [
sqliteWasmPlugin, // Always include SQLite WASM files
viteStaticCopy({
targets: clientAssets.map((asset) => ({
src: `../../client/src/${asset}/**/*`,
dest: asset,
rename: { stripBase: 3 }
})),
// Enable watching in development
...(isDev && {
watch: {
reloadPageOnChange: true
}
})
}),
viteStaticCopy({
targets: [
{
src: "../../server/src/assets/**/*",
dest: "server-assets",
rename: { stripBase: 3 }
}
]
}),
// PDF.js viewer for PDF preview support
// stripBase: 4 removes packages/pdfjs-viewer/dist/web (or /build)
viteStaticCopy({
targets: [
{
src: "../../../packages/pdfjs-viewer/dist/web/**/*",
dest: "pdfjs/web",
rename: { stripBase: 4 }
},
{
src: "../../../packages/pdfjs-viewer/dist/build/**/*",
dest: "pdfjs/build",
rename: { stripBase: 4 }
}
]
}),
// Watch client files for changes in development
...(isDev ? [
prefresh(),
clientWatchPlugin(),
pdfjsServePlugin()
] : [])
];
if (!isDev) {
plugins = [
...plugins,
viteStaticCopy({
targets: [
{
src: "../../../node_modules/@excalidraw/excalidraw/dist/prod/fonts/**/*",
dest: "",
}
]
})
]
}
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,
esbuild: {
jsx: 'automatic',
jsxImportSource: 'preact',
jsxDev: isDev
},
css: {
transformer: 'lightningcss',
devSourcemap: isDev
},
publicDir: join(__dirname, 'public'),
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: {
// 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
// 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" as const
},
commonjsOptions: {
transformMixedEsModules: true,
},
build: {
target: "esnext",
outDir: join(__dirname, 'dist'),
emptyOutDir: true,
rollupOptions: {
input: {
main: join(__dirname, 'src', 'index.html'),
sw: join(__dirname, 'src', 'sw.ts'),
'local-bridge': join(__dirname, 'src', 'local-bridge.ts'),
},
output: {
entryFileNames: (chunkInfo) => {
// Service worker and other workers should be at root level
if (chunkInfo.name === 'sw') {
return '[name].js';
}
return 'src/[name].js';
},
chunkFileNames: "src/[name].js",
assetFileNames: "src/[name].[ext]"
}
}
},
test: {
environment: "happy-dom",
setupFiles: [join(__dirname, "src/test_setup.ts")],
dir: join(__dirname),
include: [
"src/**/*.{test,spec}.{ts,tsx}",
"../../packages/trilium-core/src/**/*.{test,spec}.{ts,tsx}"
],
server: {
deps: {
inline: ["@sqlite.org/sqlite-wasm"]
}
},
alias: {
// The package's `node.mjs` entry references a non-existent
// `sqlite3-node.mjs`. Force the browser-style entry which works
// under Node + happy-dom too.
"@sqlite.org/sqlite-wasm": join(
__dirname,
"../../node_modules/@sqlite.org/sqlite-wasm/index.mjs"
)
}
},
define: {
"process.env.IS_PREACT": JSON.stringify("true"),
}
}));

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/client",
"version": "0.102.1",
"version": "0.102.2",
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
"private": true,
"license": "AGPL-3.0-only",
@@ -27,51 +27,48 @@
"@maplibre/maplibre-gl-leaflet": "0.1.3",
"@mermaid-js/layout-elk": "0.2.1",
"@mind-elixir/node-menu": "5.0.1",
"@popperjs/core": "2.11.8",
"@preact/signals": "2.8.2",
"@preact/signals": "2.9.0",
"@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/codemirror": "workspace:*",
"@triliumnext/commons": "workspace:*",
"@triliumnext/highlightjs": "workspace:*",
"@triliumnext/share-theme": "workspace:*",
"@triliumnext/split.js": "workspace:*",
"@univerjs/preset-sheets-conditional-formatting": "0.16.1",
"@univerjs/preset-sheets-core": "0.16.1",
"@univerjs/preset-sheets-data-validation": "0.16.1",
"@univerjs/preset-sheets-filter": "0.16.1",
"@univerjs/preset-sheets-find-replace": "0.16.1",
"@univerjs/preset-sheets-note": "0.16.1",
"@univerjs/preset-sheets-sort": "0.16.1",
"@univerjs/presets": "0.16.1",
"@zumer/snapdom": "2.1.0",
"@univerjs/preset-sheets-conditional-formatting": "0.20.0",
"@univerjs/preset-sheets-core": "0.20.0",
"@univerjs/preset-sheets-data-validation": "0.20.0",
"@univerjs/preset-sheets-filter": "0.20.0",
"@univerjs/preset-sheets-find-replace": "0.20.0",
"@univerjs/preset-sheets-note": "0.20.0",
"@univerjs/preset-sheets-sort": "0.20.0",
"@univerjs/presets": "0.20.0",
"@zumer/snapdom": "2.7.0",
"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",
"dompurify": "3.3.3",
"draggabilly": "3.0.0",
"force-graph": "1.51.1",
"globals": "17.4.0",
"i18next": "25.8.17",
"i18next-http-backend": "3.0.2",
"force-graph": "1.51.2",
"i18next": "26.0.3",
"i18next-http-backend": "3.0.4",
"jquery": "4.0.0",
"jquery.fancytree": "2.38.5",
"jsplumb": "2.15.6",
"katex": "0.16.38",
"knockout": "3.5.1",
"katex": "0.16.45",
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "17.0.4",
"mermaid": "11.12.3",
"mind-elixir": "5.9.3",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.29.0",
"react-i18next": "16.5.6",
"marked": "18.0.0",
"mermaid": "11.14.0",
"mind-elixir": "5.10.0",
"panzoom": "9.4.4",
"preact": "10.29.1",
"react-i18next": "17.0.2",
"react-window": "2.2.7",
"reveal.js": "5.2.1",
"reveal.js": "6.0.0",
"rrule": "2.8.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.4.0",
@@ -79,18 +76,17 @@
},
"devDependencies": {
"@ckeditor/ckeditor5-inspector": "5.0.0",
"@prefresh/vite": "2.4.12",
"@prefresh/vite": "3.0.0",
"@types/bootstrap": "5.2.10",
"@types/jquery": "4.0.0",
"@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": "14.0.0",
"happy-dom": "20.8.3",
"happy-dom": "20.8.9",
"lightningcss": "1.32.0",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.2.0"
"vite-plugin-static-copy": "4.0.1"
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg enable-background="new 0 0 256 256" version="1.1" viewBox="0 0 256 256" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<title>Trilium Notes</title>
<style type="text/css">
.st0{fill:#95C980;}
.st1{fill:#72B755;}
.st2{fill:#4FA52B;}
.st3{fill:#EE8C89;}
.st4{fill:#E96562;}
.st5{fill:#E33F3B;}
.st6{fill:#EFB075;}
.st7{fill:#E99547;}
.st8{fill:#E47B19;}
</style>
<g>
<path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
<path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
<path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
<path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
<path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
<path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
<path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
<path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
<path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,10 +1,11 @@
import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
import { SqlExecuteResponse } from "@triliumnext/commons";
import { type LOCALE_IDS, SqlExecuteResponse } from "@triliumnext/commons";
import type { NativeImage, TouchBar } from "electron";
import { ColumnComponent } from "tabulator-tables";
import type { Attribute } from "../services/attribute_parser.js";
import bundleService from "../services/bundle.js";
import froca from "../services/froca.js";
import { initLocale, t } from "../services/i18n.js";
import keyboardActionsService from "../services/keyboard_actions.js";
@@ -302,6 +303,7 @@ export type CommandMappings = {
ninthTab: CommandData;
lastTab: CommandData;
showNoteSource: CommandData;
showNoteOCRText: CommandData;
showSQLConsole: CommandData;
showBackendLog: CommandData;
showCheatsheet: CommandData;
@@ -508,7 +510,7 @@ type EventMappings = {
contentSafeMarginChanged: {
top: number;
noteContext: NoteContext;
}
};
};
export type EventListener<T extends EventNames> = {
@@ -562,7 +564,7 @@ export class AppContext extends Component {
*/
async earlyInit() {
await options.initializedPromise;
await initLocale();
await initLocale((options.get("locale") || "en") as LOCALE_IDS);
}
setLayout(layout: Layout) {
@@ -577,7 +579,6 @@ export class AppContext extends Component {
this.tabManager.loadTabs();
const bundleService = (await import("../services/bundle.js")).default;
setTimeout(() => bundleService.executeStartupBundles(), 2000);
}

View File

@@ -148,6 +148,19 @@ export default class RootCommandExecutor extends Component {
}
}
async showNoteOCRTextCommand() {
const notePath = appContext.tabManager.getActiveContextNotePath();
if (notePath) {
await appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
activate: true,
viewScope: {
viewMode: "ocr"
}
});
}
}
async showAttachmentsCommand() {
const notePath = appContext.tabManager.getActiveContextNotePath();

View File

@@ -54,7 +54,7 @@ function initOnElectron() {
const currentWindow = electronRemote.getCurrentWindow();
const style = window.getComputedStyle(document.body);
initDarkOrLightMode(style);
initDarkOrLightMode();
initTransparencyEffects(style, currentWindow);
initFullScreenDetection(currentWindow);
@@ -119,11 +119,11 @@ function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Elec
*
* @param style the root CSS element to read variables from.
*/
function initDarkOrLightMode(style: CSSStyleDeclaration) {
function initDarkOrLightMode() {
let themeSource: typeof nativeTheme.themeSource = "system";
const themeStyle = style.getPropertyValue("--theme-style");
if (style.getPropertyValue("--theme-style-auto") !== "true" && (themeStyle === "light" || themeStyle === "dark")) {
const themeStyle = window.glob.getThemeStyle();
if (themeStyle !== "auto") {
themeSource = themeStyle;
}

View File

@@ -1,5 +1,6 @@
import { getNoteIcon } from "@triliumnext/commons";
import bundleService from "../services/bundle.js";
import cssClassManager from "../services/css_class_manager.js";
import type { Froca } from "../services/froca-interface.js";
import noteAttributeCache from "../services/note_attribute_cache.js";
@@ -18,7 +19,7 @@ const RELATION = "relation";
* end user. Those types should be used only for checking against, they are
* not for direct use.
*/
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "spreadsheet";
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "spreadsheet" | "llmChat";
export interface NotePathRecord {
isArchived: boolean;
@@ -235,6 +236,16 @@ export default class FNote {
return this.hasAttribute("label", "archived");
}
/**
* Returns true if the note's metadata (title, icon) should not be editable.
* This applies to system notes like options, help, and launch bar configuration.
*/
get isMetadataReadOnly() {
return utils.isLaunchBarConfig(this.noteId)
|| this.noteId.startsWith("_help_")
|| this.noteId.startsWith("_options");
}
getChildNoteIds() {
return this.children;
}
@@ -1014,7 +1025,6 @@ export default class FNote {
const env = this.getScriptEnv();
if (env === "frontend") {
const bundleService = (await import("../services/bundle.js")).default;
return await bundleService.getAndExecuteBundle(this.noteId);
} else if (env === "backend") {
await server.post(`script/run/${this.noteId}`);

View File

@@ -1,3 +1,5 @@
import { getThemeStyle } from "./services/theme";
async function bootstrap() {
showSplash();
await setupGlob();
@@ -36,8 +38,36 @@ async function setupGlob() {
window.global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.glob = {
...json,
activeDialog: null
activeDialog: null,
device: json.device || getDevice()
};
window.glob.getThemeStyle = getThemeStyle;
}
function getDevice() {
// Respect user's manual override via URL.
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has("print")) {
return "print";
} else if (urlParams.has("desktop")) {
return "desktop";
} else if (urlParams.has("mobile")) {
return "mobile";
}
const deviceCookie = document.cookie.split("; ").find(row => row.startsWith("trilium-device="))?.split("=")[1];
if (deviceCookie === "desktop" || deviceCookie === "mobile") return deviceCookie;
return isMobile() ? "mobile" : "desktop";
}
// https://stackoverflow.com/a/73731646/944162
function isMobile() {
const mQ = matchMedia?.("(pointer:coarse)");
if (mQ?.media === "(pointer:coarse)") return !!mQ.matches;
if ("orientation" in window) return true;
const userAgentsRegEx = /\b(Android|iPhone|iPad|iPod|Windows Phone|BlackBerry|webOS|IEMobile)\b/i;
return userAgentsRegEx.test(navigator.userAgent);
}
async function loadBootstrapCss() {
@@ -49,31 +79,65 @@ async function loadBootstrapCss() {
}
}
function loadStylesheets() {
const { device, assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
type StylesheetRef = {
href: string;
media?: string;
};
const cssToLoad: string[] = [];
if (device !== "print") {
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
cssToLoad.push(`api/fonts`);
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
if (themeCssUrl) {
cssToLoad.push(themeCssUrl);
}
if (themeUseNextAsBase === "next") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
} else if (themeUseNextAsBase === "next-dark") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
} else if (themeUseNextAsBase === "next-light") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
}
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
function getConfiguredThemeStylesheets(stylesheetsPath: string, theme: string, customThemeCssUrl?: string) {
if (theme === "auto") {
return [{ href: `${stylesheetsPath}/theme-dark.css`, media: "(prefers-color-scheme: dark)" }];
}
for (const href of cssToLoad) {
if (theme === "dark") {
return [{ href: `${stylesheetsPath}/theme-dark.css` }];
}
if (theme === "next") {
return [
{ href: `${stylesheetsPath}/theme-next-light.css` },
{ href: `${stylesheetsPath}/theme-next-dark.css`, media: "(prefers-color-scheme: dark)" }
];
}
if (theme === "next-light") {
return [{ href: `${stylesheetsPath}/theme-next-light.css` }];
}
if (theme === "next-dark") {
return [{ href: `${stylesheetsPath}/theme-next-dark.css` }];
}
if (theme !== "light" && customThemeCssUrl) {
return [{ href: customThemeCssUrl }];
}
return [];
}
function loadStylesheets() {
const { device, assetPath, theme, themeBase, customThemeCssUrl } = window.glob;
const stylesheetsPath = `${assetPath}/stylesheets`;
const cssToLoad: StylesheetRef[] = [];
if (device !== "print") {
cssToLoad.push({ href: `${stylesheetsPath}/ckeditor-theme.css` });
cssToLoad.push({ href: `api/fonts` });
cssToLoad.push({ href: `${stylesheetsPath}/theme-light.css` });
cssToLoad.push(...getConfiguredThemeStylesheets(stylesheetsPath, theme, customThemeCssUrl));
if (themeBase) {
cssToLoad.push(...getConfiguredThemeStylesheets(stylesheetsPath, themeBase));
}
cssToLoad.push({ href: `${stylesheetsPath}/style.css` });
}
for (const { href, media } of cssToLoad) {
const linkEl = document.createElement("link");
linkEl.href = href;
linkEl.rel = "stylesheet";
if (media) {
linkEl.media = media;
}
document.head.appendChild(linkEl);
}
}
@@ -85,6 +149,8 @@ function loadIcons() {
}
function setBodyAttributes() {
if (!glob.dbInitialized) return;
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob;
const classesToSet = [
device,
@@ -105,6 +171,11 @@ function setBodyAttributes() {
}
async function loadScripts() {
if (!glob.dbInitialized) {
await import("./setup.js");
return;
}
switch (glob.device) {
case "mobile":
await import("./mobile.js");

View File

@@ -30,6 +30,7 @@ import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
import StandaloneWarningBar from "../widgets/layout/StandaloneWarningBar.jsx";
import StatusBar from "../widgets/layout/StatusBar.jsx";
import NoteIconWidget from "../widgets/note_icon.jsx";
import NoteTitleWidget from "../widgets/note_title.jsx";
@@ -186,6 +187,7 @@ export default class DesktopLayout {
)
)
.optChild(launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
.optChild(glob.isStandalone, <StandaloneWarningBar />)
.child(<CloseZenModeButton />)
// Desktop-specific dialogs.

View File

@@ -13,6 +13,7 @@ import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
import StandaloneWarningBar from "../widgets/layout/StandaloneWarningBar";
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
@@ -55,6 +56,7 @@ export default class MobileLayout {
.child(
new SplitNoteContainer(() =>
new NoteWrapperWidget()
.optChild(glob.isStandalone, <StandaloneWarningBar />)
.child(
new FlexContainer("row")
.class("title-row note-split-title")

View File

@@ -1,12 +1,14 @@
import utils from "../services/utils.js";
import options from "../services/options.js";
import zoomService from "../components/zoom.js";
import contextMenu, { type MenuItem } from "./context_menu.js";
import { t } from "../services/i18n.js";
import server from "../services/server.js";
import * as clipboardExt from "../services/clipboard_ext.js";
import type { BrowserWindow } from "electron";
import type { CommandNames, AppContext } from "../components/app_context.js";
import type { CommandNames } from "../components/app_context.js";
import appContext from "../components/app_context.js";
import zoomService from "../components/zoom.js";
import * as clipboardExt from "../services/clipboard_ext.js";
import { t } from "../services/i18n.js";
import options from "../services/options.js";
import server from "../services/server.js";
import utils from "../services/utils.js";
import contextMenu, { type MenuItem } from "./context_menu.js";
function setupContextMenu() {
const electron = utils.dynamicRequire("electron");
@@ -15,8 +17,6 @@ function setupContextMenu() {
// FIXME: Remove typecast once Electron is properly integrated.
const { webContents } = remote.getCurrentWindow() as BrowserWindow;
let appContext: AppContext;
webContents.on("context-menu", (event, params) => {
const { editFlags } = params;
const hasText = params.selectionText.trim().length > 0;
@@ -38,7 +38,7 @@ function setupContextMenu() {
items.push({
title: t("electron_context_menu.add-term-to-dictionary", { term: params.misspelledWord }),
uiIcon: "bx bx-plus",
handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
handler: () => electron.ipcRenderer.send("add-word-to-dictionary", params.misspelledWord)
});
items.push({ kind: "separator" });
@@ -141,7 +141,7 @@ function setupContextMenu() {
}
// Replace the placeholder with the real search keyword.
let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
const searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
items.push({ kind: "separator" });
@@ -155,10 +155,6 @@ function setupContextMenu() {
title: t("electron_context_menu.search_in_trilium", { term: shortenedSelection }),
uiIcon: "bx bx-search",
handler: async () => {
if (!appContext) {
appContext = (await import("../components/app_context.js")).default;
}
await appContext.triggerCommand("searchNotes", {
searchString: params.selectionText
});

View File

@@ -4,6 +4,7 @@ import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
import FNote from "./entities/fnote";
import content_renderer from "./services/content_renderer";
import { applyInlineMermaid } from "./services/content_renderer_text";
import froca from "./services/froca";
import { dynamicRequire, isElectron } from "./services/utils";
import { CustomNoteList, useNoteViewType } from "./widgets/collections/NoteList";
@@ -30,7 +31,6 @@ async function main() {
if (!noteId) return;
await import("./print.css");
const froca = (await import("./services/froca")).default;
const note = await froca.getNote(noteId);
const bodyWrapper = document.createElement("div");

View File

@@ -6,10 +6,8 @@ import froca from "./froca";
import server from "./server.js";
// Spy on server methods to track calls
// @ts-expect-error the generic typing is causing issues here
server.put = vi.fn(async <T> (url: string, data?: T) => ({} as T));
// @ts-expect-error the generic typing is causing issues here
server.remove = vi.fn(async <T> (url: string) => ({} as T));
server.put = vi.fn(async () => ({})) as typeof server.put;
server.remove = vi.fn(async () => ({})) as typeof server.remove;
describe("Set boolean with inheritance", () => {
beforeEach(() => {

View File

@@ -120,7 +120,7 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
if (moveToParent) {
try {
await activateParentNotePath();
await activateParentNotePath(branchIdsToDelete);
} catch (e) {
console.error(e);
}
@@ -152,13 +152,28 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
return true;
}
async function activateParentNotePath() {
// this is not perfect, maybe we should find the next/previous sibling, but that's more complex
async function activateParentNotePath(branchIdsToDelete: string[]) {
const activeContext = appContext.tabManager.getActiveContext();
const parentNotePathArr = activeContext?.notePathArray.slice(0, -1);
const activeNotePath = activeContext?.notePathArray ?? [];
if (parentNotePathArr && parentNotePathArr.length > 0) {
activeContext?.setNote(parentNotePathArr.join("/"));
// Find the deleted branch that appears earliest in the active note's path
let earliestIndex = activeNotePath.length;
for (const branchId of branchIdsToDelete) {
const branch = froca.getBranch(branchId);
if (branch) {
const index = activeNotePath.indexOf(branch.noteId);
if (index !== -1 && index < earliestIndex) {
earliestIndex = index;
}
}
}
// Navigate to the parent of the highest deleted ancestor
if (earliestIndex < activeNotePath.length) {
const parentPath = activeNotePath.slice(0, earliestIndex);
if (parentPath.length > 0) {
await activeContext?.setNote(parentPath.join("/"));
}
}
}

View File

@@ -1,5 +1,7 @@
import { ScriptParams } from "@triliumnext/commons";
import { h, VNode } from "preact";
import FNote from "../entities/fnote.js";
import BasicWidget, { ReactWrappedWidget } from "../widgets/basic_widget.js";
import RightPanelWidget from "../widgets/right_panel_widget.js";
import type { Entity } from "./frontend_script_api.js";
@@ -26,7 +28,7 @@ type WithNoteId<T> = T & {
};
export type Widget = WithNoteId<(LegacyWidget | WidgetDefinitionWithType)>;
async function getAndExecuteBundle(noteId: string, originEntity = null, script = null, params = null) {
async function getAndExecuteBundle(noteId: string, originEntity: FNote | null = null, script: string | null = null, params: ScriptParams | null = null) {
const bundle = await server.post<Bundle>(`script/bundle/${noteId}`, {
script,
params

View File

@@ -1,3 +1,6 @@
import { t } from "./i18n.js";
import toast from "./toast.js";
export function copyText(text: string) {
if (!text) {
return;
@@ -6,29 +9,26 @@ export function copyText(text: string) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
return true;
} else {
// Fallback method: https://stackoverflow.com/a/72239825
const textArea = document.createElement("textarea");
textArea.value = text;
try {
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
return document.execCommand('copy');
} finally {
document.body.removeChild(textArea);
}
}
// Fallback method: https://stackoverflow.com/a/72239825
const textArea = document.createElement("textarea");
textArea.value = text;
try {
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
return document.execCommand('copy');
} finally {
document.body.removeChild(textArea);
}
} catch (e) {
console.warn(e);
return false;
}
}
export async function copyTextWithToast(text: string) {
const t = (await import("./i18n.js")).t;
const toast = (await import("./toast.js")).default;
export function copyTextWithToast(text: string) {
if (copyText(text)) {
toast.showMessage(t("clipboard.copy_success"));
} else {

View File

@@ -1,6 +1,6 @@
import "./content_renderer.css";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import { normalizeMimeTypeForCKEditor, type TextRepresentationResponse } from "@triliumnext/commons";
import { h, render } from "preact";
import WheelZoom from 'vanilla-js-wheel-zoom';
@@ -15,6 +15,7 @@ import openService from "./open.js";
import protectedSessionService from "./protected_session.js";
import protectedSessionHolder from "./protected_session_holder.js";
import renderService from "./render.js";
import server from "./server.js";
import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js";
import utils, { getErrorMessage } from "./utils.js";
@@ -32,6 +33,7 @@ export interface RenderOptions {
includeArchivedNotes?: boolean;
/** Set of note IDs that have already been seen during rendering to prevent infinite recursion. */
seenNoteIds?: Set<string>;
showTextRepresentation?: boolean;
}
const CODE_MIME_TYPES = new Set(["application/json"]);
@@ -55,9 +57,9 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
} else if (type === "code") {
await renderCode(entity, $renderedContent);
} else if (["image", "canvas", "mindMap", "spreadsheet"].includes(type)) {
renderImage(entity, $renderedContent, options);
await renderImage(entity, $renderedContent, options);
} else if (!options.tooltip && ["file", "pdf", "audio", "video"].includes(type)) {
await renderFile(entity, type, $renderedContent);
await renderFile(entity, type, $renderedContent, options);
} else if (type === "mermaid") {
await renderMermaid(entity, $renderedContent);
} else if (type === "render" && entity instanceof FNote) {
@@ -138,7 +140,7 @@ async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HT
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
}
function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
async function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
const encodedTitle = encodeURIComponent(entity.title);
let url;
@@ -146,13 +148,14 @@ function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLE
if (entity instanceof FNote) {
url = `api/images/${entity.noteId}/${encodedTitle}?${Math.random()}`;
} else if (entity instanceof FAttachment) {
url = `api/attachments/${entity.attachmentId}/image/${encodedTitle}?${entity.utcDateModified}">`;
url = `api/attachments/${entity.attachmentId}/image/${encodedTitle}?${entity.utcDateModified}`;
}
$renderedContent // styles needed for the zoom to work well
.css("display", "flex")
.css("align-items", "center")
.css("justify-content", "center");
.css("justify-content", "center")
.css("flex-direction", "column"); // OCR text is displayed below the image.
const $img = $("<img>")
.attr("src", url || "")
@@ -178,9 +181,35 @@ function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLE
}
imageContextMenuService.setupContextMenu($img);
if (entity instanceof FNote && options.showTextRepresentation) {
await addOCRTextIfAvailable(entity, $renderedContent);
}
}
async function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: JQuery<HTMLElement>) {
async function addOCRTextIfAvailable(note: FNote, $content: JQuery<HTMLElement>) {
try {
const data = await server.get<TextRepresentationResponse>(`ocr/notes/${note.noteId}/text`);
if (data.success && data.hasOcr && data.text) {
const $ocrSection = $(`
<div class="ocr-text-section">
<div class="ocr-header">
<span class="bx bx-text"></span> ${t("ocr.extracted_text")}
</div>
<div class="ocr-content"></div>
</div>
`);
$ocrSection.find('.ocr-content').text(data.text);
$content.append($ocrSection);
}
} catch (error) {
// Silently fail if OCR API is not available
console.debug('Failed to fetch OCR text:', error);
}
}
async function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
let entityType, entityId;
if (entity instanceof FNote) {
@@ -220,6 +249,10 @@ async function renderFile(entity: FNote | FAttachment, type: string, $renderedCo
$content.append($videoPreview);
}
if (entity instanceof FNote && options.showTextRepresentation) {
await addOCRTextIfAvailable(entity, $content);
}
if (entityType === "notes" && "noteId" in entity) {
// TODO: we should make this available also for attachments, but there's a problem with "Open externally" support
// in attachment list

View File

@@ -84,6 +84,55 @@ async function createSearchNote(opts = {}) {
return await froca.getNote(note.noteId);
}
async function createLlmChat() {
const note = await server.post<FNoteRow>("special-notes/llm-chat");
await ws.waitForMaxKnownEntityChangeId();
return await froca.getNote(note.noteId);
}
/**
* Gets the most recently modified LLM chat.
* Returns null if no chat exists.
*/
async function getMostRecentLlmChat() {
const note = await server.get<FNoteRow | null>("special-notes/most-recent-llm-chat");
if (!note) {
return null;
}
await ws.waitForMaxKnownEntityChangeId();
return await froca.getNote(note.noteId);
}
/**
* Gets the most recent LLM chat, or creates a new one if none exists.
* Used by sidebar chat for persistent conversations across page refreshes.
*/
async function getOrCreateLlmChat() {
const note = await server.get<FNoteRow>("special-notes/get-or-create-llm-chat");
await ws.waitForMaxKnownEntityChangeId();
return await froca.getNote(note.noteId);
}
export interface RecentLlmChat {
noteId: string;
title: string;
dateModified: string;
}
/**
* Gets a list of recent LLM chats for the history popup.
*/
async function getRecentLlmChats(limit: number = 10): Promise<RecentLlmChat[]> {
return await server.get<RecentLlmChat[]>(`special-notes/recent-llm-chats?limit=${limit}`);
}
export default {
getInboxNote,
getTodayNote,
@@ -94,5 +143,9 @@ export default {
getMonthNote,
getYearNote,
createSqlConsole,
createSearchNote
createSearchNote,
createLlmChat,
getMostRecentLlmChat,
getOrCreateLlmChat,
getRecentLlmChats
};

View File

@@ -1,9 +1,11 @@
import { Modal } from "bootstrap";
import appContext from "../components/app_context.js";
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions, MessageType } from "../widgets/dialogs/confirm.js";
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import { focusSavedElement, saveFocusedElement } from "./focus.js";
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
import keyboardActionsService from "./keyboard_actions.js";
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
if (closeActDialog) {
@@ -25,7 +27,6 @@ export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog =
}
});
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
keyboardActionsService.updateDisplayedShortcuts($dialog);
return $dialog;

View File

@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { isValidDocName } from "./doc_renderer.js";
describe("isValidDocName", () => {
it("accepts valid docNames", () => {
expect(isValidDocName("launchbar_intro")).toBe(true);
expect(isValidDocName("User Guide/Quick Start")).toBe(true);
expect(isValidDocName("User Guide/User Guide/Quick Start")).toBe(true);
expect(isValidDocName("Quick Start Guide")).toBe(true);
expect(isValidDocName("quick_start_guide")).toBe(true);
expect(isValidDocName("quick-start-guide")).toBe(true);
});
it("rejects path traversal attacks", () => {
expect(isValidDocName("..")).toBe(false);
expect(isValidDocName("../etc/passwd")).toBe(false);
expect(isValidDocName("foo/../bar")).toBe(false);
expect(isValidDocName("../../../../api/notes/_malicious/open")).toBe(false);
expect(isValidDocName("..\\etc\\passwd")).toBe(false);
expect(isValidDocName("foo\\bar")).toBe(false);
});
it("rejects URL manipulation attacks", () => {
expect(isValidDocName("../../../../api/notes/_malicious/open?x=")).toBe(false);
expect(isValidDocName("foo#bar")).toBe(false);
expect(isValidDocName("%2e%2e")).toBe(false);
expect(isValidDocName("%2e%2e%2f%2e%2e%2fapi")).toBe(false);
});
});

View File

@@ -3,22 +3,39 @@ import { applyReferenceLinks } from "../widgets/type_widgets/text/read_only_help
import { getCurrentLanguage } from "./i18n.js";
import { formatCodeBlocks } from "./syntax_highlight.js";
/**
* Validates a docName to prevent path traversal attacks.
* Allows forward slashes for subdirectories (e.g., "User Guide/Quick Start")
* but blocks traversal sequences and URL manipulation characters.
*/
export function isValidDocName(docName: string): boolean {
// Allow alphanumeric characters, spaces, underscores, hyphens, and forward slashes.
const validDocNameRegex = /^[a-zA-Z0-9_/\- ]+$/;
return validDocNameRegex.test(docName);
}
export default function renderDoc(note: FNote) {
return new Promise<JQuery<HTMLElement>>((resolve) => {
let docName = note.getLabelValue("docName");
const docName = note.getLabelValue("docName");
const $content = $("<div>");
if (docName) {
// find doc based on language
const url = getUrl(docName, getCurrentLanguage());
// find doc based on language
const url = getUrl(docName, getCurrentLanguage());
if (url) {
$content.load(url, async (response, status) => {
// fallback to english doc if no translation available
if (status === "error") {
const fallbackUrl = getUrl(docName, "en");
$content.load(fallbackUrl, async () => {
await processContent(fallbackUrl, $content)
if (fallbackUrl) {
$content.load(fallbackUrl, async () => {
await processContent(fallbackUrl, $content);
resolve($content);
});
} else {
resolve($content);
});
}
return;
}
@@ -28,8 +45,6 @@ export default function renderDoc(note: FNote) {
} else {
resolve($content);
}
return $content;
});
}
@@ -37,9 +52,9 @@ async function processContent(url: string, $content: JQuery<HTMLElement>) {
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);
@@ -48,10 +63,27 @@ async function processContent(url: string, $content: JQuery<HTMLElement>) {
await applyReferenceLinks($content[0]);
}
function getUrl(docNameValue: string, language: string) {
function getUrl(docNameValue: string | null, language: string) {
if (!docNameValue) return;
if (!isValidDocName(docNameValue)) {
console.error(`Invalid docName: ${docNameValue}`);
return null;
}
// 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`;
// 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`;
}
function getBasePath() {
if (window.glob.isStandalone) {
return `server-assets`;
}
if (window.glob.isDev) {
return `${window.glob.assetPath}/..`;
}
return window.glob.assetPath;
}

View File

@@ -1,6 +1,6 @@
import { t } from "./i18n";
import options from "./options";
import { isMobile } from "./utils";
import { isMobile, isStandalone } from "./utils";
export interface ExperimentalFeature {
id: string;
@@ -13,11 +13,21 @@ export const experimentalFeatures = [
id: "new-layout",
name: t("experimental_features.new_layout_name"),
description: t("experimental_features.new_layout_description"),
},
{
id: "llm",
name: t("experimental_features.llm_name"),
description: t("experimental_features.llm_description"),
}
] as const satisfies ExperimentalFeature[];
export type ExperimentalFeatureId = typeof experimentalFeatures[number]["id"];
/** Returns experimental features available for the current platform (excludes LLM in standalone mode). */
export function getAvailableExperimentalFeatures() {
return experimentalFeatures.filter(f => !(f.id === "llm" && isStandalone));
}
let enabledFeatures: Set<ExperimentalFeatureId> | null = null;
export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean {
@@ -25,14 +35,24 @@ export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId):
return (isMobile() || options.is("newLayout"));
}
// LLM features require server-side API calls that don't work in standalone mode
// due to CORS restrictions from LLM providers (OpenAI, Google don't allow browser requests)
if (featureId === "llm" && isStandalone) {
return false;
}
return getEnabledFeatures().has(featureId);
}
export function getEnabledExperimentalFeatureIds() {
const values = [ ...getEnabledFeatures().values() ];
let values = [ ...getEnabledFeatures().values() ];
if (isMobile() || options.is("newLayout")) {
values.push("new-layout");
}
// LLM is not available in standalone mode
if (isStandalone) {
values = values.filter(v => v !== "llm");
}
return values;
}

View File

@@ -1,11 +1,11 @@
import appContext from "../components/app_context.js";
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import FBlob, { type FBlobRow } from "../entities/fblob.js";
import FBranch, { type FBranchRow } from "../entities/fbranch.js";
import FNote, { type FNoteRow } from "../entities/fnote.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import server from "./server.js";
import appContext from "../components/app_context.js";
import FBlob, { type FBlobRow } from "../entities/fblob.js";
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
import type { Froca } from "./froca-interface.js";
import server from "./server.js";
interface SubtreeResponse {
notes: FNoteRow[];
@@ -44,8 +44,9 @@ class FrocaImpl implements Froca {
}
async loadInitialTree() {
const resp = await server.get<SubtreeResponse>("tree");
if (!glob.dbInitialized) return;
const resp = await server.get<SubtreeResponse>("tree");
// clear the cache only directly before adding new content which is important for e.g., switching to protected session
this.#clear();
this.addResp(resp);
@@ -77,7 +78,7 @@ class FrocaImpl implements Froca {
for (const noteRow of noteRows) {
const { noteId } = noteRow;
let note = this.notes[noteId];
const note = this.notes[noteId];
if (note) {
note.update(noteRow);
@@ -240,9 +241,8 @@ class FrocaImpl implements Froca {
console.trace(`Can't find note '${noteId}'`);
return null;
} else {
return this.notes[noteId];
}
return this.notes[noteId];
})
.filter((note) => !!note) as FNote[];
}
@@ -263,9 +263,8 @@ class FrocaImpl implements Froca {
console.trace(`Can't find note '${noteId}'`);
return null;
} else {
return this.notes[noteId];
}
return this.notes[noteId];
})
.filter((note) => !!note) as FNote[];
}
@@ -338,11 +337,10 @@ class FrocaImpl implements Froca {
attachmentRows = await server.getWithSilentNotFound<FAttachmentRow[]>(`attachments/${attachmentId}/all`);
} catch (e: any) {
if (silentNotFoundError) {
logInfo(`Attachment '${attachmentId}' not found, but silentNotFoundError is enabled: ` + e.message);
logInfo(`Attachment '${attachmentId}' not found, but silentNotFoundError is enabled: ${e.message}`);
return null;
} else {
throw e;
}
throw e;
}
const attachments = this.processAttachmentRows(attachmentRows);

View File

@@ -1,14 +1,16 @@
import LoadResults from "./load_results.js";
import froca from "./froca.js";
import utils from "./utils.js";
import options from "./options.js";
import noteAttributeCache from "./note_attribute_cache.js";
import FBranch, { type FBranchRow } from "../entities/fbranch.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import type { OptionNames } from "@triliumnext/commons";
import appContext from "../components/app_context.js";
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import FBranch, { type FBranchRow } from "../entities/fbranch.js";
import type { default as FNote, FNoteRow } from "../entities/fnote.js";
import type { EntityChange } from "../server_types.js";
import type { OptionNames } from "@triliumnext/commons";
import froca from "./froca.js";
import LoadResults from "./load_results.js";
import noteAttributeCache from "./note_attribute_cache.js";
import options from "./options.js";
import utils from "./utils.js";
async function processEntityChanges(entityChanges: EntityChange[]) {
const loadResults = new LoadResults(entityChanges);
@@ -63,7 +65,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
if (entityName === "branches" && !((entity as FBranchRow).parentNoteId in froca.notes)) {
missingNoteIds.push((entity as FBranchRow).parentNoteId);
} else if (entityName === "attributes") {
let attributeEntity = entity as FAttributeRow;
const attributeEntity = entity as FAttributeRow;
if (attributeEntity.type === "relation" && (attributeEntity.name === "template" || attributeEntity.name === "inherit") && !(attributeEntity.value in froca.notes)) {
missingNoteIds.push(attributeEntity.value);
}
@@ -79,7 +81,6 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
noteAttributeCache.invalidate();
}
const appContext = (await import("../components/app_context.js")).default;
await appContext.triggerEvent("entitiesReloaded", { loadResults });
}
}

View File

@@ -206,7 +206,7 @@ export interface Api {
* Instance name identifies particular Trilium instance. It can be useful for scripts
* if some action needs to happen on only one specific instance.
*/
getInstanceName(): string;
getInstanceName(): string | null;
/**
* @returns date in YYYY-MM-DD format

View File

@@ -1,4 +1,4 @@
import { Fragment, h, VNode } from "preact";
import { createContext, Fragment, h, VNode } from "preact";
import * as hooks from "preact/hooks";
import ActionButton from "../widgets/react/ActionButton";
@@ -47,6 +47,7 @@ export const preactAPI = Object.freeze({
// Core
h,
Fragment,
createContext,
/**
* Method that must be run for widget scripts that run on Preact, using JSX. The method just returns the same definition, reserved for future typechecking and perhaps validation purposes.

View File

@@ -1,21 +1,14 @@
import options from "./options.js";
import { LOCALE_IDS, LOCALES, setDayjsLocale } from "@triliumnext/commons";
import i18next from "i18next";
import i18nextHttpBackend from "i18next-http-backend";
import server from "./server.js";
import { LOCALE_IDS, setDayjsLocale, type Locale } from "@triliumnext/commons";
import { initReactI18next } from "react-i18next";
let locales: Locale[] | null;
/**
* A deferred promise that resolves when translations are initialized.
*/
export let translationsInitializedPromise = $.Deferred();
export const translationsInitializedPromise = $.Deferred();
export async function initLocale() {
const locale = ((options.get("locale") as string) || "en") as LOCALE_IDS;
locales = await server.get<Locale[]>("options/locales");
export async function initLocale(locale: LOCALE_IDS = "en") {
i18next.use(initReactI18next);
await i18next.use(i18nextHttpBackend).init({
@@ -24,8 +17,7 @@ export async function initLocale() {
backend: {
loadPath: `${window.glob.assetPath}/translations/{{lng}}/{{ns}}.json`
},
returnEmptyString: false,
showSupportNotice: false
returnEmptyString: false
});
await setDayjsLocale(locale);
@@ -33,11 +25,7 @@ export async function initLocale() {
}
export function getAvailableLocales() {
if (!locales) {
throw new Error("Tried to load list of locales, but localization is not yet initialized.")
}
return locales;
return LOCALES;
}
/**
@@ -48,7 +36,7 @@ export function getAvailableLocales() {
*/
export function getLocaleById(localeId: string | null | undefined) {
if (!localeId) return null;
return locales?.find((l) => l.id === localeId) ?? null;
return LOCALES.find((l) => l.id === localeId) ?? null;
}
export const t = i18next.t;

View File

@@ -19,7 +19,8 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
search: null,
text: null,
webView: null,
spreadsheet: null
spreadsheet: null,
llmChat: null
};
export const byBookType: Record<ViewTypeOptions, string | null> = {

View File

@@ -28,7 +28,7 @@ async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) {
return icon;
}
export type ViewMode = "default" | "source" | "attachments" | "contextual-help" | "note-map";
export type ViewMode = "default" | "source" | "attachments" | "contextual-help" | "note-map" | "ocr";
export interface ViewScope {
/**

View File

@@ -0,0 +1,116 @@
import type { LlmChatConfig, LlmCitation, LlmMessage, LlmModelInfo,LlmUsage } from "@triliumnext/commons";
import server from "./server.js";
/**
* Fetch available models from all configured providers.
*/
export async function getAvailableModels(): Promise<LlmModelInfo[]> {
const response = await server.get<{ models?: LlmModelInfo[] }>("llm-chat/models");
return response.models ?? [];
}
export interface StreamCallbacks {
onChunk: (text: string) => void;
onThinking?: (text: string) => void;
onToolUse?: (toolName: string, input: Record<string, unknown>) => void;
onToolResult?: (toolName: string, result: string, isError?: boolean) => void;
onCitation?: (citation: LlmCitation) => void;
onUsage?: (usage: LlmUsage) => void;
onError: (error: string) => void;
onDone: () => void;
}
/**
* Stream a chat completion from the LLM API using Server-Sent Events.
*/
export async function streamChatCompletion(
messages: LlmMessage[],
config: LlmChatConfig,
callbacks: StreamCallbacks,
abortSignal?: AbortSignal
): Promise<void> {
const headers = await server.getHeaders();
const response = await fetch(`${window.glob.baseApiUrl}llm-chat/stream`, {
method: "POST",
headers: {
...headers,
"Content-Type": "application/json"
} as HeadersInit,
body: JSON.stringify({ messages, config }),
signal: abortSignal
});
if (!response.ok) {
callbacks.onError(`HTTP ${response.status}: ${response.statusText}`);
return;
}
const reader = response.body?.getReader();
if (!reader) {
callbacks.onError("No response body");
return;
}
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
try {
const data = JSON.parse(line.slice(6));
switch (data.type) {
case "text":
callbacks.onChunk(data.content);
break;
case "thinking":
callbacks.onThinking?.(data.content);
break;
case "tool_use":
callbacks.onToolUse?.(data.toolName, data.toolInput);
// Yield to force Preact to commit the pending tool call
// state before we process the result.
await new Promise((r) => setTimeout(r, 1));
break;
case "tool_result":
callbacks.onToolResult?.(data.toolName, data.result, data.isError);
await new Promise((r) => setTimeout(r, 1));
break;
case "citation":
if (data.citation) {
callbacks.onCitation?.(data.citation);
}
break;
case "usage":
if (data.usage) {
callbacks.onUsage?.(data.usage);
}
break;
case "error":
callbacks.onError(data.error);
break;
case "done":
callbacks.onDone();
break;
}
} catch (e) {
console.error("Failed to parse SSE data line:", line, e);
}
}
}
}
} finally {
reader.releaseLock();
}
}

View File

@@ -68,7 +68,8 @@ async function autocompleteSourceForCKEditor(queryText: string) {
name: row.notePathTitle || "",
link: `#${row.notePath}`,
notePath: row.notePath,
highlightedNotePathTitle: row.highlightedNotePathTitle
highlightedNotePathTitle: row.highlightedNotePathTitle,
icon: row.icon
};
})
);

View File

@@ -67,14 +67,6 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
const parentNoteId = treeService.getNoteIdFromUrl(parentNotePath);
if (options.type === "mermaid" && !options.content && !options.templateNoteId) {
options.content = `graph TD;
A-->B;
A-->C;
B-->D;
C-->D;`;
}
const { note, branch } = await server.post<Response>(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId || ""}`, {
title: options.title,
content: options.content || "",

View File

@@ -1,6 +1,7 @@
import type { NoteType } from "../entities/fnote.js";
import type { MenuCommandItem, MenuItem, MenuItemBadge, MenuSeparatorItem } from "../menus/context_menu.js";
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
import { isExperimentalFeatureEnabled } from "./experimental_features.js";
import froca from "./froca.js";
import { t } from "./i18n.js";
import server from "./server.js";
@@ -41,6 +42,7 @@ export const NOTE_TYPES: NoteTypeMapping[] = [
{ type: "relationMap", mime: "application/json", title: t("note_types.relation-map"), icon: "bxs-network-chart" },
// Misc note types
{ type: "llmChat", mime: "application/json", title: t("note_types.llm-chat"), icon: "bx-message-square-dots", isBeta: true },
{ type: "render", mime: "", title: t("note_types.render-note"), icon: "bx-extension" },
{ type: "search", title: t("note_types.saved-search"), icon: "bx-file-find", static: true },
{ type: "webView", mime: "", title: t("note_types.web-view"), icon: "bx-globe-alt" },
@@ -92,6 +94,7 @@ async function getNoteTypeItems(command?: TreeCommandNames) {
function getBlankNoteTypes(command?: TreeCommandNames): MenuItem<TreeCommandNames>[] {
return NOTE_TYPES
.filter((nt) => !nt.reserved && nt.type !== "book")
.filter((nt) => nt.type !== "llmChat" || isExperimentalFeatureEnabled("llm"))
.map((nt) => {
const menuItem: MenuCommandItem<TreeCommandNames> = {
title: nt.title,

View File

@@ -1,14 +1,4 @@
export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url" | "color";
type Multiplicity = "single" | "multi";
export interface DefinitionObject {
isPromoted?: boolean;
labelType?: LabelType;
multiplicity?: Multiplicity;
numberPrecision?: number;
promotedAlias?: string;
inverseRelation?: string;
}
import { DefinitionObject, LabelType, Multiplicity } from "@triliumnext/commons";
function parse(value: string) {
const tokens = value.split(",").map((t) => t.trim());
@@ -17,7 +7,7 @@ function parse(value: string) {
for (const token of tokens) {
if (token === "promoted") {
defObj.isPromoted = true;
} else if (["text", "number", "boolean", "date", "datetime", "time", "url", "color"].includes(token)) {
} else if (["text", "textarea", "number", "boolean", "date", "datetime", "time", "url", "color"].includes(token)) {
defObj.labelType = token as LabelType;
} else if (["single", "multi"].includes(token)) {
defObj.multiplicity = token as Multiplicity;

View File

@@ -18,6 +18,10 @@ async function render(note: FNote, $el: JQuery<HTMLElement>, onError?: ErrorHand
for (const renderNoteId of renderNoteIds) {
const bundle = await server.postWithSilentInternalServerError<Bundle>(`script/bundle/${renderNoteId}`);
if (!bundle) {
throw new Error(`Script note '${renderNoteId}' could not be loaded. It may be protected and require an active protected session.`);
}
const $scriptContainer = $("<div>");
$el.append($scriptContainer);

View File

@@ -1,3 +1,4 @@
import { t } from "./i18n.js";
import utils, { isShare } from "./utils.js";
import ValidationError from "./validation_error.js";
@@ -32,8 +33,7 @@ async function getHeaders(headers?: Headers) {
return {};
}
const appContext = (await import("../components/app_context.js")).default;
const activeNoteContext = appContext.tabManager ? appContext.tabManager.getActiveContext() : null;
const activeNoteContext = glob.appContext?.tabManager ? glob.appContext.tabManager.getActiveContext() : null;
// headers need to be lowercase because node.js automatically converts them to lower case
// also avoiding using underscores instead of dashes since nginx filters them out by default
@@ -93,7 +93,7 @@ async function upload(url: string, fileToUpload: File, componentId?: string, met
const formData = new FormData();
formData.append("upload", fileToUpload);
return await $.ajax({
const doUpload = async () => $.ajax({
url: window.glob.baseApiUrl + url,
headers: await getHeaders(componentId ? {
"trilium-component-id": componentId
@@ -104,6 +104,18 @@ async function upload(url: string, fileToUpload: File, componentId?: string, met
contentType: false, // NEEDED, DON'T REMOVE THIS
processData: false // NEEDED, DON'T REMOVE THIS
});
try {
return await doUpload();
} catch (e: unknown) {
// jQuery rejects with the jqXHR object
const jqXhr = e as JQuery.jqXHR;
if (jqXhr?.status && isCsrfError(jqXhr.status, jqXhr.responseText)) {
await refreshCsrfToken();
return await doUpload();
}
throw e;
}
}
let idCounter = 1;
@@ -112,12 +124,55 @@ const idToRequestMap: Record<string, RequestData> = {};
let maxKnownEntityChangeId = 0;
let csrfRefreshInProgress: Promise<void> | null = null;
/**
* Re-fetches /bootstrap to obtain a fresh CSRF token. This is needed when the
* server session expires (e.g. mobile tab backgrounded for a long time) and the
* existing CSRF token is no longer valid.
*
* Coalesces concurrent calls so only one bootstrap request is in-flight at a time.
*/
async function refreshCsrfToken(): Promise<void> {
if (csrfRefreshInProgress) {
return csrfRefreshInProgress;
}
csrfRefreshInProgress = (async () => {
try {
const response = await fetch(`./bootstrap${window.location.search}`, { cache: "no-store" });
if (response.ok) {
const json = await response.json();
glob.csrfToken = json.csrfToken;
}
} finally {
csrfRefreshInProgress = null;
}
})();
return csrfRefreshInProgress;
}
function isCsrfError(status: number, responseText: string): boolean {
if (status !== 403) {
return false;
}
try {
const body = JSON.parse(responseText);
return body.message === "Invalid CSRF token";
} catch {
return false;
}
}
interface CallOptions {
data?: unknown;
silentNotFound?: boolean;
silentInternalServerError?: boolean;
// If `true`, the value will be returned as a string instead of a JavaScript object if JSON, XMLDocument if XML, etc.
raw?: boolean;
/** Used internally to prevent infinite retry loops on CSRF refresh. */
csrfRetried?: boolean;
}
async function call<T>(method: string, url: string, componentId?: string, options: CallOptions = {}) {
@@ -167,7 +222,7 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, opts
type: method,
headers,
timeout: 60000,
success: (body, textStatus, jqXhr) => {
success: (body, _textStatus, jqXhr) => {
const respHeaders: Headers = {};
jqXhr
@@ -192,12 +247,34 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, opts
// don't report requests that are rejected by the browser, usually when the user is refreshing or going to a different page.
rej("rejected by browser");
return;
} else if (opts.silentNotFound && jqXhr.status === 404) {
}
// If the CSRF token is stale (e.g. session expired while tab was backgrounded),
// refresh it and retry the request once.
if (!opts.csrfRetried && isCsrfError(jqXhr.status, jqXhr.responseText)) {
try {
await refreshCsrfToken();
// Rebuild headers so the fresh glob.csrfToken is picked up
const retryHeaders = await getHeaders({ "trilium-component-id": headers["trilium-component-id"] });
const retryResult = await ajax(url, method, data, retryHeaders, { ...opts, csrfRetried: true });
res(retryResult);
return;
} catch (retryErr) {
rej(retryErr);
return;
}
}
if (opts.silentNotFound && jqXhr.status === 404) {
// report nothing
} else if (opts.silentInternalServerError && jqXhr.status === 500) {
// report nothing
} else {
await reportError(method, url, jqXhr.status, jqXhr.responseText);
try {
await reportError(method, url, jqXhr.status, jqXhr.responseText);
} catch {
// reportError may throw (e.g. ValidationError); ensure rej() is still called below.
}
}
rej(jqXhr.responseText);
@@ -267,6 +344,7 @@ async function reportError(method: string, url: string, statusCode: number, resp
} catch (e) {}
}
// Dynamic import to avoid circular dependency (toast → app_context → options → server).
const toastService = (await import("./toast.js")).default;
const messageStr = (typeof message === "string" ? message : JSON.stringify(message)) || "-";
@@ -280,7 +358,6 @@ async function reportError(method: string, url: string, statusCode: number, resp
...response
});
} else {
const { t } = await import("./i18n.js");
if (statusCode === 400 && (url.includes("%23") || url.includes("%2F"))) {
toastService.showPersistent({
id: "trafik-blocked",
@@ -294,8 +371,7 @@ async function reportError(method: string, url: string, statusCode: number, resp
t("server.unknown_http_error_content", { statusCode, method, url, message: messageStr }),
15_000);
}
const { logError } = await import("./ws.js");
logError(`${statusCode} ${method} ${url} - ${message}`);
window.logError(`${statusCode} ${method} ${url} - ${message}`);
}
}

View File

@@ -0,0 +1,87 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import SpacedUpdate from "./spaced_update";
// Mock logError which is a global in Trilium
vi.stubGlobal("logError", vi.fn());
describe("SpacedUpdate", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("should only call updater once per interval even with multiple pending callbacks", async () => {
const updater = vi.fn(async () => {
// Simulate a slow network request - this is where the race condition occurs
await new Promise((resolve) => setTimeout(resolve, 100));
});
const spacedUpdate = new SpacedUpdate(updater, 50);
// Simulate rapid typing - each keystroke calls scheduleUpdate()
// This queues multiple setTimeout callbacks due to recursive scheduleUpdate() calls
for (let i = 0; i < 10; i++) {
spacedUpdate.scheduleUpdate();
// Small delay between keystrokes
await vi.advanceTimersByTimeAsync(5);
}
// Advance time past the update interval to trigger the update
await vi.advanceTimersByTimeAsync(100);
// Let the "network request" complete and any pending callbacks run
await vi.advanceTimersByTimeAsync(200);
// The updater should have been called only ONCE, not multiple times
// With the bug, multiple pending setTimeout callbacks would all pass the time check
// during the async updater call and trigger multiple concurrent requests
expect(updater).toHaveBeenCalledTimes(1);
});
it("should call updater again if changes occur during the update", async () => {
const updater = vi.fn(async () => {
await new Promise((resolve) => setTimeout(resolve, 50));
});
const spacedUpdate = new SpacedUpdate(updater, 30);
// First update
spacedUpdate.scheduleUpdate();
await vi.advanceTimersByTimeAsync(40);
// Schedule another update while the first one is in progress
spacedUpdate.scheduleUpdate();
// Let first update complete
await vi.advanceTimersByTimeAsync(60);
// Advance past the interval again for the second update
await vi.advanceTimersByTimeAsync(100);
// Should have been called twice - once for each distinct change period
expect(updater).toHaveBeenCalledTimes(2);
});
it("should restore changed flag on error so retry can happen", async () => {
const updater = vi.fn()
.mockRejectedValueOnce(new Error("Network error"))
.mockResolvedValue(undefined);
const spacedUpdate = new SpacedUpdate(updater, 50);
spacedUpdate.scheduleUpdate();
// Advance to trigger first update (which will fail)
await vi.advanceTimersByTimeAsync(60);
// The error should have restored the changed flag, so scheduling again should work
spacedUpdate.scheduleUpdate();
await vi.advanceTimersByTimeAsync(60);
expect(updater).toHaveBeenCalledTimes(2);
});
});

View File

@@ -77,16 +77,22 @@ export default class SpacedUpdate {
}
if (Date.now() - this.lastUpdated > this.updateInterval) {
// Update these BEFORE the async call to prevent race conditions.
// Multiple setTimeout callbacks may be pending from recursive scheduleUpdate() calls.
// Without this, they would all pass the time check during the await and trigger multiple requests.
this.lastUpdated = Date.now();
this.changed = false;
this.onStateChanged("saving");
try {
await this.updater();
this.onStateChanged("saved");
this.changed = false;
} catch (e) {
// Restore changed flag on error so a retry can happen
this.changed = true;
this.onStateChanged("error");
logError(getErrorMessage(e));
}
this.lastUpdated = Date.now();
} else {
// update isn't triggered but changes are still pending, so we need to schedule another check
this.scheduleUpdate();

View File

@@ -33,6 +33,14 @@ export async function formatCodeBlocks($container: JQuery<HTMLElement>) {
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);
}
}
// Add click-to-copy for inline code (code elements not inside pre)
if (glob.device !== "print") {
const inlineCodeElements = $container.find("code:not(pre code)");
for (const inlineCode of inlineCodeElements) {
applyInlineCodeCopy($(inlineCode));
}
}
}
export function applyCopyToClipboardButton($codeBlock: JQuery<HTMLElement>) {
@@ -51,6 +59,23 @@ export function applyCopyToClipboardButton($codeBlock: JQuery<HTMLElement>) {
$codeBlock.parent().append($copyButton);
}
export function applyInlineCodeCopy($inlineCode: JQuery<HTMLElement>) {
$inlineCode
.addClass("copyable-inline-code")
.attr("title", t("code_block.click_to_copy"))
.off("click")
.on("click", (e) => {
e.stopPropagation();
const text = $inlineCode.text();
if (!isShare) {
copyTextWithToast(text);
} else {
copyText(text);
}
});
}
/**
* Applies syntax highlight to the given code block (assumed to be <pre><code>), using highlight.js.
*/

View File

@@ -0,0 +1,35 @@
export function getThemeStyle(): "auto" | "light" | "dark" {
const configuredTheme = window.glob?.theme;
if (configuredTheme === "auto" || configuredTheme === "next") {
return "auto";
}
if (configuredTheme === "light" || configuredTheme === "dark") {
return configuredTheme;
}
if (configuredTheme === "next-light") {
return "light";
}
if (configuredTheme === "next-dark") {
return "dark";
}
const style = window.getComputedStyle(document.body);
const themeStyle = style.getPropertyValue("--theme-style");
if (style.getPropertyValue("--theme-style-auto") !== "true" && (themeStyle === "light" || themeStyle === "dark")) {
return themeStyle as "light" | "dark";
}
return "auto";
}
export function getEffectiveThemeStyle(): "light" | "dark" {
const themeStyle = getThemeStyle();
if (themeStyle === "auto") {
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
return themeStyle === "dark" ? "dark" : "light";
}

View File

@@ -135,6 +135,8 @@ export function isElectron() {
return !!(window && window.process && window.process.type);
}
export const isStandalone = window.glob.isStandalone;
/**
* Returns `true` if the client is running as a PWA, otherwise `false`.
*/
@@ -455,9 +457,7 @@ export function openInAppHelpFromUrl(inAppHelpPage: string) {
export async function openInReusableSplit(targetNoteId: string, targetViewMode: ViewMode, openOpts: {
hoistedNoteId?: string;
} = {}) {
// Dynamic import to avoid import issues in tests.
const appContext = (await import("../components/app_context.js")).default;
const activeContext = appContext.tabManager.getActiveContext();
const activeContext = glob.appContext?.tabManager?.getActiveContext();
if (!activeContext) {
return;
}
@@ -467,7 +467,7 @@ export async function openInReusableSplit(targetNoteId: string, targetViewMode:
if (!existingSubcontext) {
// The target split is not already open, open a new split with it.
const { ntxId } = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", {
glob.appContext?.triggerCommand("openNewNoteSplit", {
ntxId,
notePath: targetNoteId,
hoistedNoteId: openOpts.hoistedNoteId,
@@ -816,7 +816,7 @@ function compareVersions(v1: string, v2: string): number {
/**
* Compares two semantic version strings and returns `true` if the latest version is greater than the current version.
*/
function isUpdateAvailable(latestVersion: string | null | undefined, currentVersion: string): boolean {
export function isUpdateAvailable(latestVersion: string | null | undefined, currentVersion: string): boolean {
if (!latestVersion) {
return false;
}
@@ -903,6 +903,10 @@ export function getErrorMessage(e: unknown) {
}
export function replaceHtmlEscapedSlashes(str: string) {
return str.replace(/&#x2F;/g, "/");
}
/**
* Handles left or right placement of e.g. tooltips in case of right-to-left languages. If the current language is a RTL one, then left and right are swapped. Other directions are unaffected.
* @param placement a string optionally containing a "left" or "right" value.
@@ -922,6 +926,7 @@ export default {
parseDate,
formatDateISO,
formatDateTime,
formatTime,
formatTimeInterval,
formatSize,
localNowDateTime,

View File

@@ -1,21 +1,23 @@
import utils from "./utils.js";
import toastService from "./toast.js";
import server from "./server.js";
import options from "./options.js";
import frocaUpdater from "./froca_updater.js";
import appContext from "../components/app_context.js";
import { t } from "./i18n.js";
import type { EntityChange } from "../server_types.js";
import { WebSocketMessage } from "@triliumnext/commons";
import toast from "./toast.js";
import appContext from "../components/app_context.js";
import type { EntityChange } from "../server_types.js";
import bundleService from "./bundle.js";
import froca from "./froca.js";
import frocaUpdater from "./froca_updater.js";
import { t } from "./i18n.js";
import options from "./options.js";
import server from "./server.js";
import toastService from "./toast.js";
import utils from "./utils.js";
type MessageHandler = (message: WebSocketMessage) => void;
let messageHandlers: MessageHandler[] = [];
let ws: WebSocket;
let lastAcceptedEntityChangeId = window.glob.maxEntityChangeIdAtLoad;
let lastAcceptedEntityChangeSyncId = window.glob.maxEntityChangeSyncIdAtLoad;
let lastProcessedEntityChangeId = window.glob.maxEntityChangeIdAtLoad;
let lastAcceptedEntityChangeId = window.glob.maxEntityChangeIdAtLoad ?? 0;
let lastAcceptedEntityChangeSyncId = window.glob.maxEntityChangeSyncIdAtLoad ?? 0;
let lastProcessedEntityChangeId = window.glob.maxEntityChangeIdAtLoad ?? 0;
let lastPingTs: number;
let frontendUpdateDataQueue: EntityChange[] = [];
@@ -57,6 +59,43 @@ export function unsubscribeToMessage(messageHandler: MessageHandler) {
messageHandlers = messageHandlers.filter(handler => handler !== messageHandler);
}
/**
* Dispatch a message to all handlers and process it.
* This is the main entry point for incoming messages from any provider
* (WebSocket, Worker, etc.)
*/
export async function dispatchMessage(message: WebSocketMessage) {
// Notify all subscribers
for (const messageHandler of messageHandlers) {
messageHandler(message);
}
// Use string type for flexibility - server sends more message types than are typed
const messageType = message.type as string;
const msg = message as any;
// Process the message
if (messageType === "ping") {
lastPingTs = Date.now();
} else if (messageType === "reload-frontend") {
utils.reloadFrontendApp("received request from backend to reload frontend");
} else if (messageType === "frontend-update") {
await executeFrontendUpdate(msg.data.entityChanges);
} else if (messageType === "sync-hash-check-failed") {
toastService.showError(t("ws.sync-check-failed"), 60000);
} else if (messageType === "consistency-checks-failed") {
toastService.showError(t("ws.consistency-checks-failed"), 50 * 60000);
} else if (messageType === "api-log-messages") {
appContext.triggerEvent("apiLogMessages", { noteId: msg.noteId, messages: msg.messages });
} else if (messageType === "toast") {
toastService.showMessage(msg.message, msg.timeout);
} else if (messageType === "execute-script") {
const originEntity = msg.originEntityId ? await froca.getNote(msg.originEntityId) : null;
bundleService.getAndExecuteBundle(msg.currentNoteId, originEntity, msg.script, msg.params);
}
}
// used to serialize frontend update operations
let consumeQueuePromise: Promise<void> | null = null;
@@ -112,38 +151,13 @@ async function executeFrontendUpdate(entityChanges: EntityChange[]) {
}
}
async function handleMessage(event: MessageEvent<any>) {
const message = JSON.parse(event.data);
for (const messageHandler of messageHandlers) {
messageHandler(message);
}
if (message.type === "ping") {
lastPingTs = Date.now();
} else if (message.type === "reload-frontend") {
utils.reloadFrontendApp("received request from backend to reload frontend");
} else if (message.type === "frontend-update") {
await executeFrontendUpdate(message.data.entityChanges);
} else if (message.type === "sync-hash-check-failed") {
toastService.showError(t("ws.sync-check-failed"), 60000);
} else if (message.type === "consistency-checks-failed") {
toastService.showError(t("ws.consistency-checks-failed"), 50 * 60000);
} else if (message.type === "api-log-messages") {
appContext.triggerEvent("apiLogMessages", { noteId: message.noteId, messages: message.messages });
} else if (message.type === "toast") {
toastService.showMessage(message.message);
} else if (message.type === "execute-script") {
// TODO: Remove after porting the file
// @ts-ignore
const bundleService = (await import("./bundle.js")).default as any;
// TODO: Remove after porting the file
// @ts-ignore
const froca = (await import("./froca.js")).default as any;
const originEntity = message.originEntityId ? await froca.getNote(message.originEntityId) : null;
bundleService.getAndExecuteBundle(message.currentNoteId, originEntity, message.script, message.params);
}
/**
* WebSocket message handler - parses the event and dispatches to generic handler.
* This is only used in WebSocket mode (not standalone).
*/
async function handleWebSocketMessage(event: MessageEvent<string>) {
const message = JSON.parse(event.data) as WebSocketMessage;
await dispatchMessage(message);
}
let entityChangeIdReachedListeners: {
@@ -161,7 +175,7 @@ function waitForEntityChangeId(desiredEntityChangeId: number) {
return new Promise<void>((res, rej) => {
entityChangeIdReachedListeners.push({
desiredEntityChangeId: desiredEntityChangeId,
desiredEntityChangeId,
resolvePromise: res,
start: Date.now()
});
@@ -228,16 +242,21 @@ function connectWebSocket() {
// use wss for secure messaging
const ws = new WebSocket(webSocketUri);
ws.onopen = () => console.debug(utils.now(), `Connected to server ${webSocketUri} with WebSocket`);
ws.onmessage = handleMessage;
ws.onmessage = handleWebSocketMessage;
// we're not handling ws.onclose here because reconnection is done in sendPing()
return ws;
}
async function sendPing() {
if (!ws) {
// In standalone mode, there's no WebSocket — nothing to ping.
return;
}
if (Date.now() - lastPingTs > 30000) {
console.warn(utils.now(), "Lost websocket connection to the backend");
toast.showPersistent({
toastService.showPersistent({
id: "lost-websocket-connection",
title: t("ws.lost-websocket-connection-title"),
message: t("ws.lost-websocket-connection-message"),
@@ -246,7 +265,7 @@ async function sendPing() {
}
if (ws.readyState === ws.OPEN) {
toast.closePersistent("lost-websocket-connection");
toastService.closePersistent("lost-websocket-connection");
ws.send(
JSON.stringify({
type: "ping",
@@ -262,7 +281,18 @@ async function sendPing() {
setTimeout(() => {
if (glob.device === "print") return;
if (!glob.dbInitialized) return;
if (glob.isStandalone) {
// In standalone mode, listen for messages from the local worker via custom event
window.addEventListener("trilium:ws-message", ((event: CustomEvent<WebSocketMessage>) => {
dispatchMessage(event.detail);
}) as EventListener);
console.debug(utils.now(), "Standalone mode: listening for worker messages");
return;
}
// Normal mode: use WebSocket
ws = connectWebSocket();
lastPingTs = Date.now();

420
apps/client/src/setup.css Normal file
View File

@@ -0,0 +1,420 @@
html,
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
}
body.setup {
margin: 0;
padding: 0;
&>.setup-outer-wrapper {
width: 100dvw;
height: 100dvh;
body:not(.electron) & {
@media (min-width: 700px) {
background:
radial-gradient(ellipse at 20% 50%, rgba(99, 102, 241, 0.3) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, rgba(168, 85, 247, 0.25) 0%, transparent 50%),
radial-gradient(ellipse at 60% 80%, rgba(59, 130, 246, 0.25) 0%, transparent 50%),
var(--left-pane-background-color);
display: flex;
justify-content: center;
align-items: center;
padding: 2em;
}
}
.setup-container {
background-color: var(--main-background-color);
border-radius: 16px;
padding: 2em;
flex-direction: column;
gap: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
position: relative;
height: 100%;
@media (min-width: 700px) {
display: flex;
width: 750px;
height: 650px;
top: unset;
overflow: hidden;
}
.setup-options {
display: flex;
flex-direction: column;
justify-content: center;
gap: 1rem;
.setup-option-card {
padding: 1.5em;
cursor: pointer;
display: flex;
align-items: center;
gap: 1rem;
&.disabled {
cursor: not-allowed;
opacity: 0.5;
background-color: transparent;
border-color: var(--main-border-color)
}
&:not(.disabled):hover {
background-color: var(--card-background-hover-color);
filter: contrast(105%);
transition: background-color .2s ease-out;
}
.tn-icon {
font-size: 2.5em;
color: var(--muted-text-color);
}
h3 {
font-size: 1.5em;
font-weight: normal;
}
p:last-of-type {
margin-bottom: 0;
color: var(--muted-text-color);
}
}
}
}
.page {
display: flex;
flex-direction: column;
height: 100%;
padding: 2em;
overflow: auto;
>.back-button {
position: absolute;
top: 2em;
left: 2em;
color: var(--muted-text-color);
.tn-icon {
margin-right: 0.4em;
}
}
>main {
flex: 1;
display: flex;
flex-direction: column;
padding-top: 1em;
min-height: 0;
}
&.contentless {
justify-content: center;
align-items: center;
}
>footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
border-top: 1px solid var(--main-border-color);
padding-top: 1rem;
margin-inline: -2em;
padding-inline: 2em;
}
>.page-error {
position: absolute;
top: 0;
left: 0;
right: 0;
background: var(--admonition-caution-accent-color);
z-index: 1;
margin: 0;
border-radius: 0;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
padding-right: 2.5em;
button {
position: absolute;
top: 0.5em;
right: 0.5em;
}
}
}
form {
display: flex;
flex-direction: column;
gap: 1rem;
width: 80%;
margin-inline: auto;
.form-group {
margin-bottom: 0;
}
.admonition {
margin: 0;
}
}
.form-item-with-icon {
display: flex;
align-items: center;
gap: 0.5rem;
.tn-icon {
font-size: 1.5em;
color: var(--muted-text-color);
}
}
}
.sync-illustration {
display: flex;
justify-content: center;
margin-top: 1.5em;
margin-bottom: 1.5rem;
.tn-icon {
font-size: 3em;
color: var(--muted-text-color);
}
>div {
display: flex;
flex-direction: column;
text-align: center;
gap: 0.5rem;
line-height: 1;
font-size: 0.85rem;
}
.sync-illustration-arrows {
width: 60px;
height: 3em;
position: relative;
&::after {
content: "";
position: absolute;
border: 2px dashed var(--main-border-color);
top: 1.5em;
left: 0;
right: 0;
}
}
}
.illustration-icon {
font-size: 4em;
text-align: center;
color: var(--muted-text-color);
opacity: 0.6;
margin-block: 1rem;
}
.illustration-logo {
width: 96px;
height: 96px;
margin: auto;
}
h1 {
font-size: 1.4em;
text-align: center;
}
h1 + p {
text-align: center;
color: var(--muted-text-color);
margin-bottom: 0;
}
.tooltip {
z-index: 15 !important;
}
}
body.setup.background-effects,
body.setup.background-effects .setup-container {
background: transparent;
}
/* macOS: draggable title bar region and traffic light buttons */
body.setup.platform-darwin {
.drag-region {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 40px;
-webkit-app-region: drag;
z-index: 10;
}
.back-button {
-webkit-app-region: no-drag;
z-index: 11;
}
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Slide transitions */
.slide-page {
position: absolute;
inset: 0;
}
.slide-out-forward,
.slide-out-backward,
.slide-in-forward,
.slide-in-backward {
animation-duration: 0.35s;
animation-timing-function: ease-in-out;
animation-fill-mode: forwards;
}
.slide-out-forward {
animation-name: slide-out-left;
}
.slide-out-backward {
animation-name: slide-out-right;
}
.slide-in-forward {
animation-name: slide-in-right;
}
.slide-in-backward {
animation-name: slide-in-left;
}
.page.select-language {
.dropdownWrapper {
padding-bottom: 2em;
width: 80%;
margin: auto;
}
.dropdownWrapper,
.dropdown,
.dropdown-menu {
height: 100%;
}
.dropdown-menu {
box-sizing: border-box;
overflow: auto;
}
}
.page.sync-from-desktop {
.card-columns {
display: flex;
flex-direction: row;
gap: 1.5rem;
}
.sync-from-desktop-waiting {
margin-top: 2rem;
text-align: center;
.main {
font-size: 1.35em;
}
.subtle {
color: var(--muted-text-color);
}
}
.ip-addresses {
min-width: 250px;
user-select: text;
display: flex;
flex-direction: column;
font-family: var(--monospace-font-family);
font-size: 0.9em;
.tn-card-body {
overflow: auto;
padding-bottom: 0.5em;
> :first-child {
font-weight: bold;
}
}
}
}
.page.sync-in-progress {
.sync-progress {
margin-top: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
progress {
width: 100%;
height: 1rem;
border-radius: 0.5rem;
overflow: hidden;
appearance: none;
&::-webkit-progress-bar {
background-color: var(--main-border-color);
}
&::-webkit-progress-value {
background-color: var(--main-text-color);
transition: width 0.2s ease-out;
}
}
span {
font-size: 0.85rem;
color: var(--muted-text-color);
min-width: 2.5em;
text-align: right;
}
}
}
@keyframes slide-out-left {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(-100%); opacity: 0; }
}
@keyframes slide-out-right {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
@keyframes slide-in-right {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slide-in-left {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}

View File

@@ -1,128 +0,0 @@
import "jquery";
import utils from "./services/utils.js";
import ko from "knockout";
// TriliumNextTODO: properly make use of below types
// type SetupModelSetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
// type SetupModelStep = "sync-in-progress" | "setup-type" | "new-document-in-progress" | "sync-from-desktop";
class SetupModel {
syncInProgress: boolean;
step: ko.Observable<string>;
setupType: ko.Observable<string>;
setupNewDocument: ko.Observable<boolean>;
setupSyncFromDesktop: ko.Observable<boolean>;
setupSyncFromServer: ko.Observable<boolean>;
syncServerHost: ko.Observable<string | undefined>;
syncProxy: ko.Observable<string | undefined>;
password: ko.Observable<string | undefined>;
constructor(syncInProgress: boolean) {
this.syncInProgress = syncInProgress;
this.step = ko.observable(syncInProgress ? "sync-in-progress" : "setup-type");
this.setupType = ko.observable("");
this.setupNewDocument = ko.observable(false);
this.setupSyncFromDesktop = ko.observable(false);
this.setupSyncFromServer = ko.observable(false);
this.syncServerHost = ko.observable();
this.syncProxy = ko.observable();
this.password = ko.observable();
if (this.syncInProgress) {
setInterval(checkOutstandingSyncs, 1000);
}
const serverAddress = `${location.protocol}//${location.host}`;
$("#current-host").html(serverAddress);
}
// this is called in setup.ejs
setupTypeSelected() {
return !!this.setupType();
}
selectSetupType() {
if (this.setupType() === "new-document") {
this.step("new-document-in-progress");
$.post("api/setup/new-document").then(() => {
window.location.replace("./setup");
});
} else {
this.step(this.setupType());
}
}
back() {
this.step("setup-type");
this.setupType("");
}
async finish() {
const syncServerHost = this.syncServerHost();
const syncProxy = this.syncProxy();
const password = this.password();
if (!syncServerHost) {
showAlert("Trilium server address can't be empty");
return;
}
if (!password) {
showAlert("Password can't be empty");
return;
}
// not using server.js because it loads too many dependencies
const resp = await $.post("api/setup/sync-from-server", {
syncServerHost: syncServerHost,
syncProxy: syncProxy,
password: password
});
if (resp.result === "success") {
this.step("sync-in-progress");
setInterval(checkOutstandingSyncs, 1000);
hideAlert();
} else {
showAlert(`Sync setup failed: ${resp.error}`);
}
}
}
async function checkOutstandingSyncs() {
const { outstandingPullCount, initialized } = await $.get("api/sync/stats");
if (initialized) {
if (utils.isElectron()) {
const remote = utils.dynamicRequire("@electron/remote");
remote.app.relaunch();
remote.app.exit(0);
} else {
utils.reloadFrontendApp();
}
} else {
$("#outstanding-syncs").html(outstandingPullCount);
}
}
function showAlert(message: string) {
$("#alert").text(message);
$("#alert").show();
}
function hideAlert() {
$("#alert").hide();
}
function getSyncInProgress() {
const el = document.getElementById("syncInProgress");
if (!el || !(el instanceof HTMLMetaElement)) return false;
return !!parseInt(el.content);
}
addEventListener("DOMContentLoaded", (event) => {
ko.applyBindings(new SetupModel(getSyncInProgress()), document.getElementById("setup-dialog"));
$("#setup-dialog").show();
});

524
apps/client/src/setup.tsx Normal file
View File

@@ -0,0 +1,524 @@
import "./setup.css";
import { LOCALES, SetupSyncFromServerResponse } from "@triliumnext/commons";
import clsx from "clsx";
import { ComponentChildren, render } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { useTranslation } from "react-i18next";
import logo from "./assets/icon-color.svg?url";
import { initLocale, t } from "./services/i18n";
import server from "./services/server";
import { isElectron, replaceHtmlEscapedSlashes } from "./services/utils";
import ActionButton from "./widgets/react/ActionButton";
import Admonition from "./widgets/react/Admonition";
import Button from "./widgets/react/Button";
import { Card, CardFrame, CardSection } from "./widgets/react/Card";
import FormGroup from "./widgets/react/FormGroup";
import FormList, { FormListItem } from "./widgets/react/FormList";
import FormTextBox from "./widgets/react/FormTextBox";
import Icon from "./widgets/react/Icon";
async function main() {
await initLocale();
const bodyWrapper = document.createElement("div");
bodyWrapper.classList.add("setup-outer-wrapper");
document.body.classList.add("setup");
if (isElectron()) {
document.body.classList.add("electron", `platform-${window.process.platform}`, "background-effects");
}
render(<App />, bodyWrapper);
document.body.replaceChildren(bodyWrapper);
}
type State = "selectLanguage" | "firstOptions" | "createNewDocumentOptions" | "createNewDocumentWithDemo" | "createNewDocumentEmpty" | "syncFromDesktop" | "syncFromServer" | "syncFromServerInProgress" | "syncFromDesktopInProgress" | "syncFailed";
const STATE_ORDER: State[] = ["selectLanguage", "firstOptions", "createNewDocumentOptions", "createNewDocumentWithDemo", "createNewDocumentEmpty", "syncFromDesktop", "syncFromServer", "syncFromServerInProgress", "syncFromDesktopInProgress", "syncFailed"];
function renderState(state: State, setState: (state: State) => void) {
switch (state) {
case "selectLanguage": return <SelectLanguage setState={setState} />;
case "firstOptions": return <SetupOptions setState={setState} />;
case "createNewDocumentOptions": return <CreateNewDocumentOptions setState={setState} />;
case "createNewDocumentWithDemo": return <CreateNewDocumentInProgress withDemo />;
case "createNewDocumentEmpty": return <CreateNewDocumentInProgress />;
case "syncFromServer": return <SyncFromServer setState={setState} />;
case "syncFromDesktop": return <SyncFromDesktop setState={setState} />;
case "syncFromServerInProgress": return <SyncInProgress device="server" />;
case "syncFromDesktopInProgress": return <SyncInProgress device="desktop" />;
default: return null;
}
}
function App() {
const [state, setState] = useState<State>("selectLanguage");
const [prevState, setPrevState] = useState<State | null>(null);
const [transitioning, setTransitioning] = useState(false);
const prevStateRef = useRef<State>(state);
function handleSetState(newState: State) {
setPrevState(prevStateRef.current);
prevStateRef.current = newState;
setTransitioning(true);
setState(newState);
}
const direction = prevState !== null
? STATE_ORDER.indexOf(state) > STATE_ORDER.indexOf(prevState) ? "forward" : "backward"
: "forward";
return (
<div class="setup-container">
<div class="drag-region" />
{transitioning && prevState !== null && (
<div
class={`slide-page slide-out-${direction}`}
onAnimationEnd={() => {
setTransitioning(false);
setPrevState(null);
}}
>
{renderState(prevState, handleSetState)}
</div>
)}
<div class={`slide-page ${transitioning ? `slide-in-${direction}` : "slide-current"}`} key={state}>
{renderState(state, handleSetState)}
</div>
</div>
);
}
function SelectLanguage({ setState }: { setState: (state: State) => void }) {
const { t, i18n } = useTranslation();
const [ currentLocale, setCurrentLocale ] = useState(i18n.language);
const filteredLocales = useMemo(() => LOCALES.filter(l => !l.contentOnly), []);
return (
<SetupPage
title={t("setup.language")}
className="select-language"
illustration={<Icon icon="bx bx-globe" className="illustration-icon" />}
footer={<Button text={t("setup.continue")} kind="primary" onClick={() => setState("firstOptions")} />}
>
<FormList onSelect={async (id) => {
await i18n.changeLanguage(id);
setCurrentLocale(id);
}}>
{filteredLocales.map(locale => (
<FormListItem key={locale.id} value={locale.id} active={locale.id === currentLocale}>{locale.name}</FormListItem>
))}
</FormList>
</SetupPage>
);
}
function SetupOptions({ setState }: { setState: (state: State) => void }) {
return (
<SetupPage
title={t("setup.heading")}
className="setup-options-container"
illustration={<img src={logo} alt="Setup illustration" className="illustration-logo" />}
onBack={() => setState("selectLanguage")}
>
<div class="setup-options">
<SetupOptionCard
icon="bx bx-file-blank"
title={t("setup.new-document")}
description={t("setup.new-document-description")}
onClick={() => setState("createNewDocumentOptions")}
/>
<SetupOptionCard
icon="bx bx-server"
title={t("setup.sync-from-server")}
description={t("setup.sync-from-server-description")}
onClick={() => setState("syncFromServer")}
/>
<SetupOptionCard
icon="bx bx-desktop"
title={t("setup.sync-from-desktop")}
description={t("setup.sync-from-desktop-description")}
disabled={glob.isStandalone}
onClick={() => setState("syncFromDesktop")}
/>
</div>
</SetupPage>
);
}
type SyncStep = "connecting" | "syncing" | "finalizing";
function getSyncStep(stats: { outstandingPullCount: number; totalPullCount: number | null; initialized: boolean }): SyncStep {
if (stats.initialized) {
return "finalizing"; // will reload momentarily
}
if (stats.totalPullCount !== null && stats.outstandingPullCount > 0) {
return "syncing";
}
if (stats.totalPullCount !== null && stats.outstandingPullCount === 0) {
return "finalizing";
}
return "connecting";
}
function SyncInProgress({ device }: { device: "server" | "desktop" }) {
const stats = useOutstandingSyncInfo();
const step = getSyncStep(stats);
useEffect(() => {
if (stats.initialized) {
onSetupFinished();
}
}, [stats.initialized]);
const steps: { key: SyncStep; label: string }[] = [
{ key: "connecting", label: t("setup.sync-step-connecting") },
{ key: "syncing", label: t("setup.sync-step-syncing") },
{ key: "finalizing", label: t("setup.sync-step-finalizing") }
];
const currentIndex = steps.findIndex((s) => s.key === step);
const syncingDone = currentIndex > steps.findIndex((s) => s.key === "syncing");
let progress = 0;
if (syncingDone) {
progress = 100;
} else if (stats.totalPullCount) {
progress = Math.round(((stats.totalPullCount - stats.outstandingPullCount) / stats.totalPullCount) * 100);
}
return (
<SetupPage
className="sync-in-progress"
illustration={<SyncIllustration targetDevice={device} />}
title={t("setup.sync-in-progress-title")}
>
<Card className="sync-steps">
{steps.map((s, i) => (
<CardSection className={i < currentIndex ? "completed" : i === currentIndex ? "active" : ""} key={s.key}>
<Icon icon={i < currentIndex ? "bx bx-check-circle" : i === currentIndex ? "bx bx-loader-circle bx-spin" : "bx bx-circle"} />{" "}
{s.label}
{s.key === "syncing" && (
<div class="sync-progress">
<progress value={syncingDone ? 1 : stats.totalPullCount! - stats.outstandingPullCount} max={syncingDone ? 1 : stats.totalPullCount!} />
<span>{progress}%</span>
</div>
)}
</CardSection>
))}
</Card>
</SetupPage>
);
}
function useOutstandingSyncInfo() {
const [ outstandingPullCount, setOutstandingPullCount ] = useState(0);
const [ totalPullCount, setTotalPullCount ] = useState<number | null>(null);
const [ initialized, setInitialized ] = useState(false);
async function refresh() {
const resp = await server.get<{ outstandingPullCount: number; totalPullCount: number | null; initialized: boolean }>("sync/stats");
setOutstandingPullCount(resp.outstandingPullCount);
setTotalPullCount(resp.totalPullCount);
setInitialized(resp.initialized);
}
useEffect(() => {
const interval = setInterval(refresh, 1000);
refresh();
return () => clearInterval(interval);
}, []);
return { outstandingPullCount, totalPullCount, initialized };
}
function CreateNewDocumentOptions({ setState }: { setState: (state: State) => void }) {
return (
<SetupPage
className="create-new-document-options"
title={t("setup.create-new-document-options-title")}
illustration={<Icon icon="bx bx-star" className="illustration-icon" />}
onBack={() => setState("firstOptions")}
>
<div class="setup-options">
<SetupOptionCard icon="bx bx-book-open" title={t("setup.create-new-document-options-with-demo")} description={t("setup.create-new-document-options-with-demo-description")} onClick={() => setState("createNewDocumentWithDemo")} />
<SetupOptionCard icon="bx bx-file-blank" title={t("setup.create-new-document-options-empty")} description={t("setup.create-new-document-options-empty-description")} onClick={() => setState("createNewDocumentEmpty")} />
</div>
</SetupPage>
);
}
function CreateNewDocumentInProgress({ withDemo = false }: { withDemo?: boolean }) {
useEffect(() => {
server.post(`setup/new-document${withDemo ? "" : "?skipDemoDb"}`).then(onSetupFinished);
}, [ withDemo ]);
return (
<SetupPage
className="create-new-document"
title={t("setup.create-new-document-title")}
description={t("setup.create-new-document-description")}
illustration={<Icon icon="bx bx-loader-circle bx-spin" className="illustration-icon" />}
/>
);
}
function SyncFromServer({ setState }: { setState: (state: State) => void }) {
const [ syncServerHost, setSyncServerHost ] = useState("");
const [ password, setPassword ] = useState("");
const [ syncProxy, setSyncProxy ] = useState("");
const [ error, setError ] = useState<string | null>(null);
const [ errorId, setErrorId ] = useState(0);
const [ isWrongPassword, setIsWrongPassword ] = useState(false);
const isValid = syncServerHost.trim() !== "" && password !== "";
function raiseError(message: string) {
setError(message);
setErrorId(id => id + 1);
}
async function handleFinishSetup() {
try {
const resp = await server.post<SetupSyncFromServerResponse>("setup/sync-from-server", {
syncServerHost: syncServerHost.trim().replace(/\/+$/, ""),
syncProxy: syncProxy.trim(),
password
});
if (resp.result === "success") {
setState("syncFromServerInProgress");
} else if (resp.error.includes("Incorrect password")) {
setIsWrongPassword(true);
} else {
raiseError(t("setup.sync-failed", { message: resp.error }));
}
} catch (e) {
raiseError(e instanceof Error ? e.message : String(e));
}
}
return (
<SetupPage
className="sync-from-server top-aligned"
title={t("setup.sync-from-server")}
description={t("setup.sync-from-server-page-description")}
illustration={<SyncIllustration targetDevice="server" />}
error={error}
errorId={errorId}
onBack={() => setState("firstOptions")}
footer={<Button text={t("setup.button-finish-setup")} kind="primary" onClick={handleFinishSetup} disabled={!isValid} />}
>
<form>
<Card>
<CardSection>
<FormGroup label={t("setup.server-host")} name="serverHost">
<FormTextBox
placeholder={t("setup.server-host-placeholder")}
currentValue={syncServerHost} onChange={setSyncServerHost}
autocomplete="trilium-sync-server-host"
required
/>
</FormGroup>
</CardSection>
<CardSection>
<FormGroup
label={t("setup.server-password")} name="serverPassword"
error={isWrongPassword ? t("setup.wrong-password") : undefined}
>
<FormTextBox
type="password"
currentValue={password} onChange={setPassword}
autocomplete="trilium-sync-server-password"
required
/>
</FormGroup>
</CardSection>
</Card>
<Card heading={t("setup.advanced-options")}>
<CardSection>
<FormGroup
name="proxyServer"
label={t("setup.proxy-server")}
description={isElectron() ? t("setup.proxy-instruction") : undefined}
>
<FormTextBox placeholder={t("setup.proxy-server-placeholder")} currentValue={syncProxy} onChange={setSyncProxy} />
</FormGroup>
</CardSection>
</Card>
</form>
</SetupPage>
);
}
function SyncFromDesktop({ setState }: { setState: (state: State) => void }) {
const networkAddresses = getNetworkAddresses();
useEffect(() => {
const interval = setInterval(async () => {
const status = await server.get<{ schemaExists: boolean }>("setup/status");
if (status.schemaExists) {
setState("syncFromDesktopInProgress");
}
}, 1000);
return () => clearInterval(interval);
}, [setState]);
return (
<SetupPage
className="sync-from-desktop"
title={t("setup.sync-from-desktop")}
illustration={<SyncIllustration targetDevice="desktop" />}
onBack={() => setState("firstOptions")}
>
<div class="card-columns">
<Card heading="On the other device">
<CardSection>1. {t("setup.sync-from-desktop-step1")}</CardSection>
<CardSection>2. {t("setup.sync-from-desktop-step2")}</CardSection>
<CardSection>3. {t("setup.sync-from-desktop-step3")}</CardSection>
<CardSection>4. {t("setup.sync-from-desktop-step4")}</CardSection>
<CardSection>5. {t("setup.sync-from-desktop-step5")}</CardSection>
</Card>
{networkAddresses.length > 0 && (
<Card heading={t("setup.your-ip-addresses")} className="ip-addresses">
{networkAddresses.map((addr) => (
<CardSection key={addr}>{addr}</CardSection>
))}
</Card>
)}
</div>
<div class="sync-from-desktop-waiting">
<div class="main"><Icon icon="bx bx-loader-circle bx-spin" />{" "} {t("setup.sync-from-desktop-waiting")}</div>
<div class="subtle">{t("setup.sync-from-desktop-warning")}</div>
</div>
</SetupPage>
);
}
function SyncIllustration({ targetDevice }: { targetDevice: "desktop" | "server" }) {
return (
<div class="sync-illustration">
<div>
<Icon icon={isElectron() ? "bx bx-desktop" : "bx bx-globe"} />
{t("setup.sync-illustration-this-device")}
</div>
<div class="sync-illustration-arrows" />
<div>
<Icon icon={targetDevice === "desktop" ? "bx bx-desktop" : "bx bx-server"} />
{targetDevice === "desktop" ? t("setup.sync-illustration-desktop-app") : t("setup.sync-illustration-server")}
</div>
</div>
);
}
function SetupOptionCard({ title, description, icon, onClick, disabled }: { title: string; description: string, icon: string, onClick?: () => void, disabled?: boolean }) {
return (
<CardFrame
className={clsx("setup-option-card", { disabled })}
onClick={disabled ? undefined : onClick}
>
<Icon icon={icon} />
<div>
<h3>{title}</h3>
<p>{description}</p>
</div>
</CardFrame>
);
}
function SetupPage({ title, description, className, illustration, children, footer, error, errorId, onBack }: {
title: string;
description?: string;
error?: string | null;
errorId?: number;
className?: string;
illustration?: ComponentChildren;
children?: ComponentChildren;
footer?: ComponentChildren;
onBack?: () => void;
}) {
const [ showError, setShowError ] = useState(!!error);
useEffect(() => {
if (error) {
setShowError(true);
}
}, [ error, errorId ]);
return (
<div className={clsx("page", className, { "contentless": !children })}>
{onBack && (
<Button
className="back-button"
icon="bx bx-arrow-back"
text={t("setup.button-back")}
onClick={onBack}
kind="lowProfile"
/>
)}
{error && showError && (
<Admonition className="page-error" type="caution">
<ActionButton icon="bx bx-x" text={t("setup.dismiss-error")} onClick={() => setShowError(false)} />
{replaceHtmlEscapedSlashes(error)}
</Admonition>
)}
{illustration}
<h1>{title}</h1>
{description && <p class="page-description">{description}</p>}
{children && <main>
{children}
</main>}
{footer && <footer>{footer}</footer>}
</div>
);
}
function getNetworkAddresses(): string[] {
if (!isElectron()) {
return [`${location.protocol}//${location.host}`];
}
// eslint-disable-next-line @typescript-eslint/no-require-imports
const os = require("os") as typeof import("os");
const interfaces = os.networkInterfaces();
const addresses: string[] = [];
for (const nets of Object.values(interfaces)) {
if (!nets) continue;
for (const net of nets) {
if (net.internal) continue;
if (net.family === "IPv6" && net.scopeid !== 0) continue;
addresses.push(net.address);
}
}
// Sort by likelihood of being the local network address.
addresses.sort((a, b) => networkScore(a) - networkScore(b));
return addresses.map((addr) => `${location.protocol}//${addr}:${location.port}`);
}
function networkScore(addr: string): number {
if (addr.startsWith("192.168.")) return 0;
if (addr.startsWith("10.")) return 1;
if (/^172\.(1[6-9]|2\d|3[01])\./.test(addr)) return 2;
if (addr.includes(":")) return 4; // IPv6
return 3;
}
function onSetupFinished() {
if (isElectron()) {
// On Electron we need to use the setup route because it handles the closing of the setup window and opening the main app window.
location.href = "setup";
} else {
location.reload();
}
}
main();

View File

@@ -1230,6 +1230,43 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
width: 100%;
}
/* Expandable include note styles */
.include-note-title-row {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
}
.include-note-title-row .include-note-title {
margin: 0;
}
.include-note-toggle {
background: none;
border: none;
padding: 2px;
cursor: pointer;
font-size: 1.2em;
color: var(--main-text-color);
transition: transform 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.include-note-toggle:hover {
color: var(--main-link-color);
}
.include-note-toggle.expanded {
transform: rotate(90deg);
}
.include-note[data-box-size="expandable"] .include-note-content {
margin-top: 10px;
}
.alert {
padding: 8px 14px;
width: auto;
@@ -1612,11 +1649,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
}
body.mobile #launcher-container {
justify-content: center;
}
body.mobile #launcher-container button {
margin: 0 16px;
justify-content: space-evenly;
}
body.mobile .modal.show {
@@ -1754,10 +1787,13 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
justify-content: space-between;
align-items: baseline;
font-weight: bold;
text-transform: uppercase;
color: var(--muted-text-color) !important;
}
#right-pane .card-header-title {
text-transform: uppercase;
}
#right-pane .card-header-buttons {
display: flex;
transform: scale(0.9);
@@ -2642,3 +2678,26 @@ iframe.print-iframe {
min-height: 50px;
align-items: center;
}
.ocr-text-section {
padding: 10px;
background: var(--accented-background-color);
border-left: 3px solid var(--main-border-color);
text-align: left;
width: 100%;
}
.ocr-header {
font-weight: bold;
margin-bottom: 8px;
font-size: 0.9em;
color: var(--muted-text-color);
}
.ocr-content {
max-height: 150px;
overflow-y: auto;
font-size: 0.9em;
line-height: 1.4;
white-space: pre-wrap;
}

View File

@@ -1,11 +0,0 @@
/* Import the light color scheme.
* This is the base color scheme, always active and overridden by the dark
* color scheme stylesheet when necessary. */
@import url(./theme-next-light.css);
/* Import the dark color scheme when the system preference is set to dark mode */
@import url(./theme-next-dark.css) (prefers-color-scheme: dark);
:root {
--theme-style-auto: true;
}

Some files were not shown because too many files have changed in this diff Show More