mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-29 17:26:38 +01:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			feat/add-c
			...
			feat/impro
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | ce298e477b | ||
|  | 81c0e508ac | ||
|  | 065740eabc | ||
|  | 4ce9102f93 | 
| @@ -54,7 +54,7 @@ The original Trilium developer ([Zadam](https://github.com/zadam)) has graciousl | ||||
|  | ||||
| There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Trilium instance. Simply [install TriliumNext/Trilium](#-installation) as usual and it will use your existing database. | ||||
|  | ||||
| Versions up to and including [v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext/Trilium have their sync versions incremented which prevents direct migration. | ||||
| Versions up to and including [v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are compatible with the latest TriliumNext/Trilium version of [v0.63.7](https://github.com/TriliumNext/Trilium/releases/tag/v0.63.7). Any later versions of TriliumNext/Trilium have their sync versions incremented which prevents direct migration. | ||||
|  | ||||
| ## 📖 Documentation | ||||
|  | ||||
|   | ||||
| @@ -8,9 +8,9 @@ fi | ||||
| VERSION=$1 | ||||
| SERIES=${VERSION:0:4}-latest | ||||
|  | ||||
| docker push zadam/trilium:$VERSION | ||||
| docker push zadam/trilium:$SERIES | ||||
| docker push TriliumNext/Trilium:$VERSION | ||||
| docker push TriliumNext/Trilium:$SERIES | ||||
|  | ||||
| if [[ $1 != *"beta"* ]]; then | ||||
|   docker push zadam/trilium:latest | ||||
|   docker push TriliumNext/Trilium:latest | ||||
| fi | ||||
|   | ||||
| @@ -834,7 +834,7 @@ class FNote { | ||||
|             if (a.noteId === b.noteId) { | ||||
|                 return a.position < b.position ? -1 : 1; | ||||
|             } else { | ||||
|                 // inherited promoted attributes should stay grouped: https://github.com/zadam/trilium/issues/3761 | ||||
|                 // inherited promoted attributes should stay grouped: https://github.com/TriliumNext/Trilium/issues/3761 | ||||
|                 return a.noteId < b.noteId ? -1 : 1; | ||||
|             } | ||||
|         }); | ||||
|   | ||||
| @@ -78,7 +78,7 @@ export class WidgetsByParent { | ||||
|             this.byParent[parentName] | ||||
|                 // previously, custom widgets were provided as a single instance, but that has the disadvantage | ||||
|                 // for splits where we actually need multiple instaces and thus having a class to instantiate is better | ||||
|                 // https://github.com/zadam/trilium/issues/4274 | ||||
|                 // https://github.com/TriliumNext/Trilium/issues/4274 | ||||
|                 .map((w: any) => (w.prototype ? new w() : w)) | ||||
|         ); | ||||
|     } | ||||
|   | ||||
| @@ -78,7 +78,7 @@ async function copy(branchIds: string[]) { | ||||
|     clipboardMode = "copy"; | ||||
|  | ||||
|     if (utils.isElectron()) { | ||||
|         // https://github.com/zadam/trilium/issues/2401 | ||||
|         // https://github.com/TriliumNext/Trilium/issues/2401 | ||||
|         const { clipboard } = require("electron"); | ||||
|         const links: string[] = []; | ||||
|  | ||||
|   | ||||
| @@ -507,7 +507,7 @@ $(document).on("dblclick", "a", (e) => { | ||||
| $(document).on("mousedown", "a", (e) => { | ||||
|     if (e.which === 2) { | ||||
|         // prevent paste on middle click | ||||
|         // https://github.com/zadam/trilium/issues/2995 | ||||
|         // https://github.com/TriliumNext/Trilium/issues/2995 | ||||
|         // https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#preventing_default_actions | ||||
|         e.preventDefault(); | ||||
|         return false; | ||||
|   | ||||
| @@ -99,7 +99,7 @@ async function mouseEnterHandler(this: HTMLElement) { | ||||
|     if ($link.filter(":hover").length > 0) { | ||||
|         $link.tooltip({ | ||||
|             container: "body", | ||||
|             // https://github.com/zadam/trilium/issues/2794 https://github.com/zadam/trilium/issues/2988 | ||||
|             // https://github.com/TriliumNext/Trilium/issues/2794 https://github.com/TriliumNext/Trilium/issues/2988 | ||||
|             // with bottom this flickering happens a bit less | ||||
|             placement: "bottom", | ||||
|             trigger: "manual", | ||||
|   | ||||
| @@ -79,7 +79,7 @@ body { | ||||
|     height: unset !important; | ||||
|     overflow: visible; | ||||
|     position: unset; | ||||
|     /* https://github.com/zadam/trilium/issues/3202 */ | ||||
|     /* https://github.com/TriliumNext/Trilium/issues/3202 */ | ||||
|     color: black; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -40,7 +40,7 @@ | ||||
| } | ||||
|  | ||||
| html { | ||||
|     /* this fixes FF filter vs. position fixed bug: https://github.com/zadam/trilium/issues/233 */ | ||||
|     /* this fixes FF filter vs. position fixed bug: https://github.com/TriliumNext/Trilium/issues/233 */ | ||||
|     height: 100vh; | ||||
|     overscroll-behavior: none; | ||||
| } | ||||
| @@ -543,7 +543,7 @@ button.btn-sm { | ||||
|     transform: translateX(7px); | ||||
|     color: var(--muted-text-color); | ||||
|     background-color: var(--main-background-color); | ||||
|     /* Making this narrower because https://github.com/zadam/trilium/issues/502 (problem only in smaller font sizes) */ | ||||
|     /* Making this narrower because https://github.com/TriliumNext/Trilium/issues/502 (problem only in smaller font sizes) */ | ||||
|     min-width: 0; | ||||
|     padding: 0; | ||||
|     z-index: 1000; | ||||
| @@ -1117,7 +1117,7 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href | ||||
|     padding: 10px; | ||||
|     border-radius: 10px; | ||||
|     background-color: var(--accented-background-color); | ||||
|     display: flex; /* see https://github.com/zadam/trilium/issues/1590 */ | ||||
|     display: flex; /* see https://github.com/TriliumNext/Trilium/issues/1590 */ | ||||
| } | ||||
|  | ||||
| .include-note.ck-placeholder::before { | ||||
| @@ -1251,7 +1251,7 @@ body.desktop li.dropdown-submenu:hover > ul.dropdown-menu { | ||||
|     left: calc(100% - 2px); /* -2px, otherwise there's a small gap between menu and submenu where the hover can disappear */ | ||||
|     margin-top: -10px; | ||||
|     min-width: 15rem; | ||||
|     /* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */ | ||||
|     /* to make submenu scrollable https://github.com/TriliumNext/Trilium/issues/3136 */ | ||||
|     max-height: 600px; | ||||
|     overflow: auto; | ||||
| } | ||||
|   | ||||
| @@ -364,7 +364,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { | ||||
|         this.$inputName = this.$widget.find(".attr-input-name"); | ||||
|         this.$inputName.on("input", (ev) => { | ||||
|             if (!(ev.originalEvent as KeyboardEvent)?.isComposing) { | ||||
|                 // https://github.com/zadam/trilium/pull/3812 | ||||
|                 // https://github.com/TriliumNext/Trilium/pull/3812 | ||||
|                 this.userEditedAttribute(); | ||||
|             } | ||||
|         }); | ||||
| @@ -383,7 +383,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { | ||||
|         this.$inputValue = this.$widget.find(".attr-input-value"); | ||||
|         this.$inputValue.on("input", (ev) => { | ||||
|             if (!(ev.originalEvent as KeyboardEvent)?.isComposing) { | ||||
|                 // https://github.com/zadam/trilium/pull/3812 | ||||
|                 // https://github.com/TriliumNext/Trilium/pull/3812 | ||||
|                 this.userEditedAttribute(); | ||||
|             } | ||||
|         }); | ||||
| @@ -421,7 +421,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { | ||||
|         this.$inputInverseRelation = this.$widget.find(".attr-input-inverse-relation"); | ||||
|         this.$inputInverseRelation.on("input", (ev) => { | ||||
|             if (!(ev.originalEvent as KeyboardEvent)?.isComposing) { | ||||
|                 // https://github.com/zadam/trilium/pull/3812 | ||||
|                 // https://github.com/TriliumNext/Trilium/pull/3812 | ||||
|                 this.userEditedAttribute(); | ||||
|             } | ||||
|         }); | ||||
|   | ||||
| @@ -173,7 +173,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem | ||||
|             this.attributeDetailWidget.hide(); | ||||
|         }); | ||||
|  | ||||
|         this.$editor.on("blur", () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/zadam/trilium/issues/4160 | ||||
|         this.$editor.on("blur", () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/TriliumNext/Trilium/issues/4160 | ||||
|  | ||||
|         this.$addNewAttributeButton = this.$widget.find(".add-new-attribute-button"); | ||||
|         this.$addNewAttributeButton.on("click", (e) => this.addNewAttribute(e)); | ||||
| @@ -282,7 +282,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem | ||||
|  | ||||
|     async save() { | ||||
|         if (this.lastUpdatedNoteId !== this.noteId) { | ||||
|             // https://github.com/zadam/trilium/issues/3090 | ||||
|             // https://github.com/TriliumNext/Trilium/issues/3090 | ||||
|             console.warn("Ignoring blur event because a different note is loaded."); | ||||
|             return; | ||||
|         } | ||||
|   | ||||
| @@ -192,7 +192,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { | ||||
|      * sets full height of container that contains note content for a subset of note-types | ||||
|      */ | ||||
|     checkFullHeight() { | ||||
|         // https://github.com/zadam/trilium/issues/2522 | ||||
|         // https://github.com/TriliumNext/Trilium/issues/2522 | ||||
|         const isBackendNote = this.noteContext?.noteId === "_backendLog"; | ||||
|         const isSqlNote = this.mime === "text/x-sqlite;schema=trilium"; | ||||
|         const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "mermaid", "file"].includes(this.type ?? ""); | ||||
|   | ||||
| @@ -81,7 +81,7 @@ export default class NoteListWidget extends NoteContextAwareWidget { | ||||
|         ); | ||||
|  | ||||
|         // there seems to be a race condition on Firefox which triggers the observer only before the widget is visible | ||||
|         // (intersection is false). https://github.com/zadam/trilium/issues/4165 | ||||
|         // (intersection is false). https://github.com/TriliumNext/Trilium/issues/4165 | ||||
|         setTimeout(() => observer.observe(this.$widget[0]), 10); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -312,7 +312,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { | ||||
|     setupNoteTitleTooltip() { | ||||
|         // the following will dynamically set tree item's tooltip if the whole item's text is not currently visible | ||||
|         // if the whole text is visible then no tooltip is show since that's unnecessarily distracting | ||||
|         // see https://github.com/zadam/trilium/pull/1120 for discussion | ||||
|         // see https://github.com/TriliumNext/Trilium/pull/1120 for discussion | ||||
|  | ||||
|         // code inspired by https://gist.github.com/jtsternberg/c272d7de5b967cec2d3d | ||||
|         const isEnclosing = ($container: JQuery<HTMLElement>, $sub: JQuery<HTMLElement>) => { | ||||
| @@ -952,7 +952,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { | ||||
|  | ||||
|         await this.filterHoistedBranch(true); | ||||
|  | ||||
|         // don't activate the active note, see discussion in https://github.com/zadam/trilium/issues/3664 | ||||
|         // don't activate the active note, see discussion in https://github.com/TriliumNext/Trilium/issues/3664 | ||||
|     } | ||||
|  | ||||
|     async expandTree(node: Fancytree.FancytreeNode | null = null) { | ||||
| @@ -1181,7 +1181,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { | ||||
|             /* | ||||
|              * We're collapsing notes after a period of inactivity to "cleanup" the tree - users rarely | ||||
|              * collapse the notes and the tree becomes unusuably large. | ||||
|              * Some context: https://github.com/zadam/trilium/issues/1192 | ||||
|              * Some context: https://github.com/TriliumNext/Trilium/issues/1192 | ||||
|              */ | ||||
|  | ||||
|             const noteIdsToKeepExpanded = new Set( | ||||
| @@ -1429,7 +1429,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { | ||||
|         } | ||||
|  | ||||
|         if (activeNodeFocused) { | ||||
|             // needed by Firefox: https://github.com/zadam/trilium/issues/1865 | ||||
|             // needed by Firefox: https://github.com/TriliumNext/Trilium/issues/1865 | ||||
|             this.tree.$container.focus(); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -103,7 +103,7 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget { | ||||
|             if (a.noteId === b.noteId) { | ||||
|                 return a.position - b.position; | ||||
|             } else { | ||||
|                 // inherited attributes should stay grouped: https://github.com/zadam/trilium/issues/3761 | ||||
|                 // inherited attributes should stay grouped: https://github.com/TriliumNext/Trilium/issues/3761 | ||||
|                 return a.noteId < b.noteId ? -1 : 1; | ||||
|             } | ||||
|         }); | ||||
|   | ||||
| @@ -16,7 +16,7 @@ const TPL = /*html*/` | ||||
| <div class="promoted-attributes-widget"> | ||||
|     <style> | ||||
|     body.mobile .promoted-attributes-widget { | ||||
|         /* https://github.com/zadam/trilium/issues/4468 */ | ||||
|         /* https://github.com/TriliumNext/Trilium/issues/4468 */ | ||||
|         flex-shrink: 0.4; | ||||
|         overflow: auto; | ||||
|     } | ||||
|   | ||||
| @@ -48,7 +48,7 @@ export default class SharedInfoWidget extends NoteContextAwareWidget { | ||||
|             let host = location.host; | ||||
|             if (host.endsWith("/")) { | ||||
|                 // seems like IE has trailing slash | ||||
|                 // https://github.com/zadam/trilium/issues/3782 | ||||
|                 // https://github.com/TriliumNext/Trilium/issues/3782 | ||||
|                 host = host.substr(0, host.length - 1); | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -360,7 +360,7 @@ export default class TocWidget extends RightPanelWidget { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Reduce indent if a larger headings are not being used: https://github.com/zadam/trilium/issues/4363 | ||||
|      * Reduce indent if a larger headings are not being used: https://github.com/TriliumNext/Trilium/issues/4363 | ||||
|      */ | ||||
|     pullLeft($toc: JQuery<HTMLElement>) { | ||||
|         while (true) { | ||||
| @@ -390,7 +390,7 @@ export default class TocWidget extends RightPanelWidget { | ||||
|         // temporarily" (ie "edit this note" button) without any | ||||
|         // intervening events, do the readonly calculation at navigation | ||||
|         // time and not at outline creation time | ||||
|         // See https://github.com/zadam/trilium/issues/2828 | ||||
|         // See https://github.com/TriliumNext/Trilium/issues/2828 | ||||
|         const isDocNote = this.note.type === "doc"; | ||||
|         const isReadOnly = await this.noteContext.isReadOnly(); | ||||
|  | ||||
|   | ||||
| @@ -16,7 +16,7 @@ const TPL = /*html*/` | ||||
|         } | ||||
|  | ||||
|         /* Conflict between excalidraw and bootstrap classes keeps the menu hidden */ | ||||
|         /* https://github.com/zadam/trilium/issues/3780 */ | ||||
|         /* https://github.com/TriliumNext/Trilium/issues/3780 */ | ||||
|         /* https://github.com/excalidraw/excalidraw/issues/6567 */ | ||||
|         .excalidraw .dropdown-menu { | ||||
|             display: block; | ||||
|   | ||||
| @@ -99,7 +99,7 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget { | ||||
|     async doRefresh(note: FNote) { | ||||
|         // we load CKEditor also for read only notes because they contain content styles required for correct rendering of even read only notes | ||||
|         // we could load just ckeditor-content.css but that causes CSS conflicts when both build CSS and this content CSS is loaded at the same time | ||||
|         // (see https://github.com/zadam/trilium/issues/1590 for example of such conflict) | ||||
|         // (see https://github.com/TriliumNext/Trilium/issues/1590 for example of such conflict) | ||||
|         await import("@triliumnext/ckeditor5"); | ||||
|  | ||||
|         this.onLanguageChanged(); | ||||
|   | ||||
| @@ -22,7 +22,7 @@ async function main() { | ||||
|     electronDebug(); | ||||
|     electronDl({ saveAs: true }); | ||||
|  | ||||
|     // needed for excalidraw export https://github.com/zadam/trilium/issues/4271 | ||||
|     // needed for excalidraw export https://github.com/TriliumNext/Trilium/issues/4271 | ||||
|     electron.app.commandLine.appendSwitch("enable-experimental-web-platform-features"); | ||||
|     electron.app.commandLine.appendSwitch("lang", options.getOptionOrNull("formattingLocale") ?? "en"); | ||||
|  | ||||
|   | ||||
| @@ -27,7 +27,7 @@ function decrypt(key: any, cipherText: any) { | ||||
|  | ||||
|     try { | ||||
|         const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64"); | ||||
|         // old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/trilium/issues/3017 | ||||
|         // old encrypted data can have IV of length 13, see some details here: https://github.com/TriliumNext/Trilium/issues/3017 | ||||
|         const ivLength = cipherTextBufferWithIv.length % 16 === 0 ? 16 : 13; | ||||
|         const iv = cipherTextBufferWithIv.slice(0, ivLength); | ||||
|  | ||||
| @@ -48,7 +48,7 @@ function decrypt(key: any, cipherText: any) { | ||||
|  | ||||
|         return payload; | ||||
|     } catch (e: any) { | ||||
|         // recovery from https://github.com/zadam/trilium/issues/510 | ||||
|         // recovery from https://github.com/TriliumNext/Trilium/issues/510 | ||||
|         if (e.message?.includes("WRONG_FINAL_BLOCK_LENGTH") || e.message?.includes("wrong final block length")) { | ||||
|             console.log("Caught WRONG_FINAL_BLOCK_LENGTH, returning cipherText instead"); | ||||
|             return cipherText; | ||||
|   | ||||
| @@ -13,7 +13,7 @@ | ||||
|        <h1 data-trilium-h1>Journal</h1> | ||||
|  | ||||
|       <div class="ck-content"> | ||||
|         <p>You can read some explanation on how this journal works here: <a href="https://github.com/zadam/trilium/wiki/Day-notes">https://github.com/zadam/trilium/wiki/Day-notes</a> | ||||
|         <p>You can read some explanation on how this journal works here: <a href="https://github.com/TriliumNext/Trilium/wiki/Day-notes">https://github.com/TriliumNext/Trilium/wiki/Day-notes</a> | ||||
|  | ||||
|         </p> | ||||
|       </div> | ||||
|   | ||||
| @@ -6,7 +6,7 @@ return api.res.send(404); | ||||
|  * To test this, execute the following curl request: curl -X POST http://localhost:37740/custom/create-note -H "Content-Type: application/json" -d "{ \"secret\": \"secret-password\", \"title\": \"hello\", \"content\": \"world\" }" | ||||
|  * (host and port might have to be adjusted based on your setup) | ||||
|  * | ||||
|  * See https://github.com/zadam/trilium/wiki/Custom-request-handler for details. | ||||
|  * See https://github.com/TriliumNext/Trilium/wiki/Custom-request-handler for details. | ||||
|  */ | ||||
|  | ||||
| const {req, res} = api; | ||||
|   | ||||
| @@ -14,7 +14,7 @@ | ||||
|  | ||||
|       <div class="ck-content"> | ||||
|         <p>This is a simple TODO/Task manager. You can see some description and explanation | ||||
|           here: <a href="https://github.com/zadam/trilium/wiki/Task-manager">https://github.com/zadam/trilium/wiki/Task-manager</a> | ||||
|           here: <a href="https://github.com/TriliumNext/Trilium/wiki/Task-manager">https://github.com/TriliumNext/Trilium/wiki/Task-manager</a> | ||||
|  | ||||
|         </p> | ||||
|         <p>Please note that this is meant as scripting example only and feature/bug | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <div style="padding: 20px"> | ||||
|     <strong>See explanation <a href="https://github.com/zadam/trilium/wiki/Weight-tracker" target="_blank">here</a></strong>. | ||||
|     <strong>See explanation <a href="https://github.com/TriliumNext/Trilium/wiki/Weight-tracker" target="_blank">here</a></strong>. | ||||
|  | ||||
|     <canvas></canvas> | ||||
| </div> | ||||
| @@ -2,7 +2,7 @@ | ||||
|  * This is a demo of how you can create custom theme for Trilium. You can activate it by going | ||||
|  * into options in first tab "Appearance". | ||||
|  *  | ||||
|  * You can read some details on theming here: http://github.com/zadam/trilium/wiki/Themes | ||||
|  * You can read some details on theming here: http://github.com/TriliumNext/Trilium/wiki/Themes | ||||
|  */ | ||||
|  | ||||
| @font-face { /* This will be used as main UI font (see below) */ | ||||
|   | ||||
							
								
								
									
										1269
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/API Client Libraries.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1269
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/API Client Libraries.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										720
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/ETAPI Complete Guide.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										720
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/ETAPI Complete Guide.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,720 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|     <title>ETAPI Complete Guide</title> | ||||
|     <style> | ||||
|         body {  | ||||
|             font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;  | ||||
|             line-height: 1.6;  | ||||
|             max-width: 1200px;  | ||||
|             margin: 0 auto;  | ||||
|             padding: 20px;  | ||||
|         } | ||||
|         h1 { color: #2c3e50; border-bottom: 3px solid #e67e22; padding-bottom: 10px; } | ||||
|         h2 { color: #34495e; margin-top: 40px; border-bottom: 2px solid #ecf0f1; padding-bottom: 5px; } | ||||
|         h3 { color: #7f8c8d; margin-top: 30px; } | ||||
|         h4 { color: #95a5a6; margin-top: 25px; } | ||||
|         pre {  | ||||
|             background: #2c3e50;  | ||||
|             color: #ecf0f1;  | ||||
|             padding: 20px;  | ||||
|             border-radius: 8px;  | ||||
|             overflow-x: auto;  | ||||
|             border-left: 4px solid #e67e22; | ||||
|         } | ||||
|         code {  | ||||
|             background: #f4f4f4;  | ||||
|             padding: 3px 6px;  | ||||
|             border-radius: 4px;  | ||||
|             font-family: 'Courier New', Monaco, monospace;  | ||||
|             font-size: 0.9em; | ||||
|         } | ||||
|         pre code { | ||||
|             background: transparent; | ||||
|             padding: 0; | ||||
|             color: inherit; | ||||
|         } | ||||
|         .endpoint-box { | ||||
|             background: #f8f9fa; | ||||
|             border: 1px solid #e9ecef; | ||||
|             border-radius: 8px; | ||||
|             padding: 20px; | ||||
|             margin: 20px 0; | ||||
|             border-left: 4px solid #e67e22; | ||||
|         } | ||||
|         .method-get { border-left-color: #28a745; } | ||||
|         .method-post { border-left-color: #007bff; } | ||||
|         .method-put { border-left-color: #ffc107; } | ||||
|         .method-patch { border-left-color: #fd7e14; } | ||||
|         .method-delete { border-left-color: #dc3545; } | ||||
|         .example-box { | ||||
|             background: #f1f3f4; | ||||
|             border-radius: 8px; | ||||
|             padding: 15px; | ||||
|             margin: 15px 0; | ||||
|         } | ||||
|         table {  | ||||
|             border-collapse: collapse;  | ||||
|             width: 100%;  | ||||
|             margin: 20px 0;  | ||||
|             box-shadow: 0 2px 4px rgba(0,0,0,0.1); | ||||
|         } | ||||
|         th, td {  | ||||
|             border: 1px solid #ddd;  | ||||
|             padding: 12px;  | ||||
|             text-align: left;  | ||||
|         } | ||||
|         th {  | ||||
|             background: #34495e;  | ||||
|             color: white; | ||||
|             font-weight: 600; | ||||
|         } | ||||
|         tr:nth-child(even) { background: #f8f9fa; } | ||||
|         .warning { | ||||
|             background: #fff3cd; | ||||
|             border: 1px solid #ffeaa7; | ||||
|             border-radius: 6px; | ||||
|             padding: 15px; | ||||
|             margin: 20px 0; | ||||
|         } | ||||
|         .info { | ||||
|             background: #d1ecf1; | ||||
|             border: 1px solid #74c0fc; | ||||
|             border-radius: 6px; | ||||
|             padding: 15px; | ||||
|             margin: 20px 0; | ||||
|         } | ||||
|         .success { | ||||
|             background: #d4edda; | ||||
|             border: 1px solid #51cf66; | ||||
|             border-radius: 6px; | ||||
|             padding: 15px; | ||||
|             margin: 20px 0; | ||||
|         } | ||||
|         .toc { | ||||
|             background: #f8f9fa; | ||||
|             border-radius: 8px; | ||||
|             padding: 20px; | ||||
|             margin-bottom: 30px; | ||||
|         } | ||||
|         .toc ul { | ||||
|             list-style-type: none; | ||||
|             padding-left: 20px; | ||||
|         } | ||||
|         .toc a { | ||||
|             text-decoration: none; | ||||
|             color: #495057; | ||||
|         } | ||||
|         .toc a:hover { | ||||
|             color: #e67e22; | ||||
|         } | ||||
|         .auth-example { | ||||
|             background: #e8f5e8; | ||||
|             border-left: 4px solid #28a745; | ||||
|             padding: 15px; | ||||
|             margin: 15px 0; | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|     <h1>ETAPI Complete Guide</h1> | ||||
|      | ||||
|     <div class="toc"> | ||||
|         <h2>Table of Contents</h2> | ||||
|         <ul> | ||||
|             <li><a href="#introduction">Introduction</a></li> | ||||
|             <li><a href="#authentication-setup">Authentication Setup</a></li> | ||||
|             <li><a href="#api-endpoints">API Endpoints</a></li> | ||||
|             <li><a href="#common-use-cases">Common Use Cases</a></li> | ||||
|             <li><a href="#client-library-examples">Client Library Examples</a></li> | ||||
|             <li><a href="#rate-limiting-and-best-practices">Rate Limiting and Best Practices</a></li> | ||||
|             <li><a href="#migration-from-internal-api">Migration from Internal API</a></li> | ||||
|             <li><a href="#error-handling">Error Handling</a></li> | ||||
|             <li><a href="#performance-considerations">Performance Considerations</a></li> | ||||
|         </ul> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="introduction">Introduction</h2> | ||||
|      | ||||
|     <p>ETAPI (External Trilium API) is the recommended REST API for external integrations with Trilium Notes. It provides a secure, stable interface for programmatic access to notes, attributes, branches, and attachments.</p> | ||||
|  | ||||
|     <div class="success"> | ||||
|         <h4>Key Features</h4> | ||||
|         <ul> | ||||
|             <li>RESTful design with predictable endpoints</li> | ||||
|             <li>Token-based authentication</li> | ||||
|             <li>Comprehensive CRUD operations</li> | ||||
|             <li>Search functionality</li> | ||||
|             <li>Import/export capabilities</li> | ||||
|             <li>Calendar and special note access</li> | ||||
|         </ul> | ||||
|     </div> | ||||
|  | ||||
|     <div class="info"> | ||||
|         <strong>Base URL:</strong> <code>http://localhost:8080/etapi</code> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="authentication-setup">Authentication Setup</h2> | ||||
|      | ||||
|     <h3>Method 1: Token Authentication</h3> | ||||
|      | ||||
|     <div class="auth-example"> | ||||
|         <h4>Step 1: Generate ETAPI Token</h4> | ||||
|         <ol> | ||||
|             <li>Open Trilium Notes</li> | ||||
|             <li>Navigate to <strong>Options</strong> → <strong>ETAPI</strong></li> | ||||
|             <li>Click "Create new ETAPI token"</li> | ||||
|             <li>Copy the generated token</li> | ||||
|         </ol> | ||||
|     </div> | ||||
|  | ||||
|     <h4>Step 2: Use Token in Requests</h4> | ||||
|  | ||||
|     <div class="endpoint-box"> | ||||
|         <strong>HTTP Header:</strong> | ||||
|         <pre><code>Authorization: <your-token></code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <strong>cURL Example:</strong> | ||||
|         <pre><code>curl -X GET http://localhost:8080/etapi/notes/root \ | ||||
|   -H "Authorization: myEtapiToken123"</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <strong>Python Example:</strong> | ||||
|         <pre><code>import requests | ||||
|  | ||||
| headers = { | ||||
|     'Authorization': 'myEtapiToken123' | ||||
| } | ||||
|  | ||||
| response = requests.get('http://localhost:8080/etapi/notes/root', headers=headers) | ||||
| print(response.json())</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>Method 2: Basic Authentication</h3> | ||||
|      | ||||
|     <p>Use the ETAPI token as the password with any username:</p> | ||||
|      | ||||
|     <div class="example-box"> | ||||
|         <pre><code>curl -X GET http://localhost:8080/etapi/notes/root \ | ||||
|   -u "trilium:myEtapiToken123"</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>Method 3: Programmatic Login</h3> | ||||
|      | ||||
|     <div class="example-box"> | ||||
|         <pre><code>import requests | ||||
|  | ||||
| # Login to get token | ||||
| login_data = {'password': 'your-trilium-password'} | ||||
| response = requests.post('http://localhost:8080/etapi/auth/login', json=login_data) | ||||
| token = response.json()['authToken'] | ||||
|  | ||||
| # Use token for subsequent requests | ||||
| headers = {'Authorization': token} | ||||
| notes = requests.get('http://localhost:8080/etapi/notes/root', headers=headers)</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="api-endpoints">API Endpoints</h2> | ||||
|      | ||||
|     <h3>Notes</h3> | ||||
|      | ||||
|     <h4>Create Note</h4> | ||||
|      | ||||
|     <div class="endpoint-box method-post"> | ||||
|         <strong>POST</strong> <code>/etapi/create-note</code> | ||||
|         <p>Creates a new note and places it in the tree.</p> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <strong>Request Body:</strong> | ||||
|         <pre><code>{ | ||||
|   "parentNoteId": "root", | ||||
|   "title": "My New Note", | ||||
|   "type": "text", | ||||
|   "content": "<p>This is the note content</p>", | ||||
|   "notePosition": 10, | ||||
|   "prefix": "📝", | ||||
|   "isExpanded": true | ||||
| }</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <strong>Response (201 Created):</strong> | ||||
|         <pre><code>{ | ||||
|   "note": { | ||||
|     "noteId": "evnnmvHTCgIn", | ||||
|     "title": "My New Note", | ||||
|     "type": "text", | ||||
|     "mime": "text/html", | ||||
|     "isProtected": false, | ||||
|     "dateCreated": "2024-01-15 10:30:00.000+0100", | ||||
|     "dateModified": "2024-01-15 10:30:00.000+0100", | ||||
|     "utcDateCreated": "2024-01-15 09:30:00.000Z", | ||||
|     "utcDateModified": "2024-01-15 09:30:00.000Z" | ||||
|   }, | ||||
|   "branch": { | ||||
|     "branchId": "ibhg4WxTdULk", | ||||
|     "noteId": "evnnmvHTCgIn", | ||||
|     "parentNoteId": "root", | ||||
|     "prefix": "📝", | ||||
|     "notePosition": 10, | ||||
|     "isExpanded": true, | ||||
|     "utcDateModified": "2024-01-15 09:30:00.000Z" | ||||
|   } | ||||
| }</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <strong>Python Example:</strong> | ||||
|         <pre><code>import requests | ||||
| import json | ||||
|  | ||||
| def create_note(parent_id, title, content, note_type="text"): | ||||
|     url = "http://localhost:8080/etapi/create-note" | ||||
|     headers = { | ||||
|         'Authorization': 'your-token', | ||||
|         'Content-Type': 'application/json' | ||||
|     } | ||||
|      | ||||
|     data = { | ||||
|         "parentNoteId": parent_id, | ||||
|         "title": title, | ||||
|         "type": note_type, | ||||
|         "content": content | ||||
|     } | ||||
|      | ||||
|     response = requests.post(url, headers=headers, json=data) | ||||
|      | ||||
|     if response.status_code == 201: | ||||
|         return response.json() | ||||
|     else: | ||||
|         raise Exception(f"Failed to create note: {response.text}") | ||||
|  | ||||
| # Usage | ||||
| new_note = create_note("root", "Meeting Notes", "<p>Discussion points:</p><ul><li>Item 1</li></ul>") | ||||
| print(f"Created note with ID: {new_note['note']['noteId']}")</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h4>Get Note by ID</h4> | ||||
|      | ||||
|     <div class="endpoint-box method-get"> | ||||
|         <strong>GET</strong> <code>/etapi/notes/{noteId}</code> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <strong>cURL Example:</strong> | ||||
|         <pre><code>curl -X GET http://localhost:8080/etapi/notes/evnnmvHTCgIn \ | ||||
|   -H "Authorization: your-token"</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <strong>Response:</strong> | ||||
|         <pre><code>{ | ||||
|   "noteId": "evnnmvHTCgIn", | ||||
|   "title": "My Note", | ||||
|   "type": "text", | ||||
|   "mime": "text/html", | ||||
|   "isProtected": false, | ||||
|   "attributes": [ | ||||
|     { | ||||
|       "attributeId": "abc123", | ||||
|       "noteId": "evnnmvHTCgIn", | ||||
|       "type": "label", | ||||
|       "name": "todo", | ||||
|       "value": "", | ||||
|       "position": 10, | ||||
|       "isInheritable": false | ||||
|     } | ||||
|   ], | ||||
|   "parentNoteIds": ["root"], | ||||
|   "childNoteIds": ["child1", "child2"], | ||||
|   "dateCreated": "2024-01-15 10:30:00.000+0100", | ||||
|   "dateModified": "2024-01-15 14:20:00.000+0100", | ||||
|   "utcDateCreated": "2024-01-15 09:30:00.000Z", | ||||
|   "utcDateModified": "2024-01-15 13:20:00.000Z" | ||||
| }</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h4>Update Note</h4> | ||||
|      | ||||
|     <div class="endpoint-box method-patch"> | ||||
|         <strong>PATCH</strong> <code>/etapi/notes/{noteId}</code> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <strong>Request Body:</strong> | ||||
|         <pre><code>{ | ||||
|   "title": "Updated Title", | ||||
|   "type": "text", | ||||
|   "mime": "text/html" | ||||
| }</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <strong>JavaScript Example:</strong> | ||||
|         <pre><code>async function updateNote(noteId, updates) { | ||||
|     const response = await fetch(`http://localhost:8080/etapi/notes/${noteId}`, { | ||||
|         method: 'PATCH', | ||||
|         headers: { | ||||
|             'Authorization': 'your-token', | ||||
|             'Content-Type': 'application/json' | ||||
|         }, | ||||
|         body: JSON.stringify(updates) | ||||
|     }); | ||||
|      | ||||
|     if (!response.ok) { | ||||
|         throw new Error(`Failed to update note: ${response.statusText}`); | ||||
|     } | ||||
|      | ||||
|     return response.json(); | ||||
| } | ||||
|  | ||||
| // Usage | ||||
| updateNote('evnnmvHTCgIn', { title: 'New Title' }) | ||||
|     .then(note => console.log('Updated note:', note)) | ||||
|     .catch(err => console.error('Error:', err));</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>Search</h3> | ||||
|      | ||||
|     <h4>Search Notes</h4> | ||||
|      | ||||
|     <div class="endpoint-box method-get"> | ||||
|         <strong>GET</strong> <code>/etapi/notes</code> | ||||
|         <p>Search for notes using Trilium's search syntax.</p> | ||||
|     </div> | ||||
|  | ||||
|     <table> | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <th>Parameter</th> | ||||
|                 <th>Required</th> | ||||
|                 <th>Description</th> | ||||
|                 <th>Example</th> | ||||
|             </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|             <tr> | ||||
|                 <td><code>search</code></td> | ||||
|                 <td>Yes</td> | ||||
|                 <td>Search query string</td> | ||||
|                 <td>#todo, "exact phrase"</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><code>fastSearch</code></td> | ||||
|                 <td>No</td> | ||||
|                 <td>Enable fast search (default: false)</td> | ||||
|                 <td>true</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><code>includeArchivedNotes</code></td> | ||||
|                 <td>No</td> | ||||
|                 <td>Include archived notes (default: false)</td> | ||||
|                 <td>true</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><code>ancestorNoteId</code></td> | ||||
|                 <td>No</td> | ||||
|                 <td>Search within subtree</td> | ||||
|                 <td>root</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><code>orderBy</code></td> | ||||
|                 <td>No</td> | ||||
|                 <td>Property to order by</td> | ||||
|                 <td>dateModified</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><code>orderDirection</code></td> | ||||
|                 <td>No</td> | ||||
|                 <td>"asc" or "desc"</td> | ||||
|                 <td>desc</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><code>limit</code></td> | ||||
|                 <td>No</td> | ||||
|                 <td>Maximum number of results</td> | ||||
|                 <td>10</td> | ||||
|             </tr> | ||||
|         </tbody> | ||||
|     </table> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <strong>Python Examples:</strong> | ||||
|         <pre><code># Full-text search | ||||
| def search_notes(query, token, **kwargs): | ||||
|     url = "http://localhost:8080/etapi/notes" | ||||
|     headers = {'Authorization': token} | ||||
|     params = {'search': query, **kwargs} | ||||
|      | ||||
|     response = requests.get(url, headers=headers, params=params) | ||||
|     return response.json() | ||||
|  | ||||
| # Search for keyword | ||||
| results = search_notes("project management", token) | ||||
|  | ||||
| # Search with label | ||||
| results = search_notes("#todo", token) | ||||
|  | ||||
| # Search for exact phrase | ||||
| results = search_notes('"exact phrase"', token) | ||||
|  | ||||
| # Complex search with ordering and limit | ||||
| results = search_notes( | ||||
|     "type:text #important",  | ||||
|     token, | ||||
|     orderBy="dateModified", | ||||
|     orderDirection="desc", | ||||
|     limit=10 | ||||
| )</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="common-use-cases">Common Use Cases</h2> | ||||
|      | ||||
|     <h3>1. Daily Journal Entry</h3> | ||||
|      | ||||
|     <div class="example-box"> | ||||
|         <pre><code>from datetime import date | ||||
| import requests | ||||
|  | ||||
| class TriliumJournal: | ||||
|     def __init__(self, base_url, token): | ||||
|         self.base_url = base_url | ||||
|         self.headers = {'Authorization': token} | ||||
|      | ||||
|     def create_journal_entry(self, content, tags=[]): | ||||
|         # Get today's day note | ||||
|         today = date.today().strftime('%Y-%m-%d') | ||||
|         day_note_url = f"{self.base_url}/calendar/days/{today}" | ||||
|         day_note = requests.get(day_note_url, headers=self.headers).json() | ||||
|          | ||||
|         # Create entry | ||||
|         entry_data = { | ||||
|             "parentNoteId": day_note['noteId'], | ||||
|             "title": f"Entry - {date.today().strftime('%H:%M')}", | ||||
|             "type": "text", | ||||
|             "content": content | ||||
|         } | ||||
|          | ||||
|         response = requests.post( | ||||
|             f"{self.base_url}/create-note", | ||||
|             headers={**self.headers, 'Content-Type': 'application/json'}, | ||||
|             json=entry_data | ||||
|         ) | ||||
|          | ||||
|         entry = response.json() | ||||
|          | ||||
|         # Add tags | ||||
|         for tag in tags: | ||||
|             self.add_tag(entry['note']['noteId'], tag) | ||||
|          | ||||
|         return entry | ||||
|      | ||||
|     def add_tag(self, note_id, tag_name): | ||||
|         attr_data = { | ||||
|             "noteId": note_id, | ||||
|             "type": "label", | ||||
|             "name": tag_name, | ||||
|             "value": "" | ||||
|         } | ||||
|          | ||||
|         requests.post( | ||||
|             f"{self.base_url}/attributes", | ||||
|             headers={**self.headers, 'Content-Type': 'application/json'}, | ||||
|             json=attr_data | ||||
|         ) | ||||
|  | ||||
| # Usage | ||||
| journal = TriliumJournal("http://localhost:8080/etapi", "your-token") | ||||
| entry = journal.create_journal_entry( | ||||
|     "<p>Today's meeting went well. Key decisions:</p><ul><li>Item 1</li></ul>", | ||||
|     tags=["meeting", "important"] | ||||
| )</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="error-handling">Error Handling</h2> | ||||
|      | ||||
|     <h3>Common Error Codes</h3> | ||||
|      | ||||
|     <table> | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <th>Status</th> | ||||
|                 <th>Code</th> | ||||
|                 <th>Description</th> | ||||
|                 <th>Resolution</th> | ||||
|             </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|             <tr> | ||||
|                 <td>400</td> | ||||
|                 <td>BAD_REQUEST</td> | ||||
|                 <td>Invalid request format</td> | ||||
|                 <td>Check request body and parameters</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td>401</td> | ||||
|                 <td>UNAUTHORIZED</td> | ||||
|                 <td>Invalid or missing token</td> | ||||
|                 <td>Verify authentication token</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td>404</td> | ||||
|                 <td>NOTE_NOT_FOUND</td> | ||||
|                 <td>Note doesn't exist</td> | ||||
|                 <td>Check note ID</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td>400</td> | ||||
|                 <td>NOTE_IS_PROTECTED</td> | ||||
|                 <td>Cannot modify protected note</td> | ||||
|                 <td>Unlock protected session first</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td>429</td> | ||||
|                 <td>TOO_MANY_REQUESTS</td> | ||||
|                 <td>Rate limit exceeded</td> | ||||
|                 <td>Wait before retrying</td> | ||||
|             </tr> | ||||
|         </tbody> | ||||
|     </table> | ||||
|  | ||||
|     <h3>Error Response Format</h3> | ||||
|      | ||||
|     <div class="example-box"> | ||||
|         <pre><code>{ | ||||
|   "status": 400, | ||||
|   "code": "VALIDATION_ERROR", | ||||
|   "message": "Note title cannot be empty" | ||||
| }</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>Handling Errors in Code</h3> | ||||
|      | ||||
|     <div class="example-box"> | ||||
|         <pre><code>class ETAPIError(Exception): | ||||
|     def __init__(self, status, code, message): | ||||
|         self.status = status | ||||
|         self.code = code | ||||
|         self.message = message | ||||
|         super().__init__(f"{code}: {message}") | ||||
|  | ||||
| def handle_api_response(response): | ||||
|     if response.status_code >= 400: | ||||
|         try: | ||||
|             error = response.json() | ||||
|             raise ETAPIError( | ||||
|                 error.get('status'), | ||||
|                 error.get('code'), | ||||
|                 error.get('message') | ||||
|             ) | ||||
|         except json.JSONDecodeError: | ||||
|             raise ETAPIError( | ||||
|                 response.status_code, | ||||
|                 'UNKNOWN_ERROR', | ||||
|                 response.text | ||||
|             ) | ||||
|      | ||||
|     return response.json() if response.content else None | ||||
|  | ||||
| # Usage | ||||
| try: | ||||
|     response = requests.get( | ||||
|         'http://localhost:8080/etapi/notes/invalid', | ||||
|         headers={'Authorization': 'token'} | ||||
|     ) | ||||
|     note = handle_api_response(response) | ||||
| except ETAPIError as e: | ||||
|     if e.code == 'NOTE_NOT_FOUND': | ||||
|         print("Note doesn't exist") | ||||
|     else: | ||||
|         print(f"API Error: {e.message}")</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="rate-limiting-and-best-practices">Rate Limiting and Best Practices</h2> | ||||
|      | ||||
|     <div class="warning"> | ||||
|         <h4>Rate Limiting</h4> | ||||
|         <p>ETAPI implements rate limiting for authentication endpoints:</p> | ||||
|         <ul> | ||||
|             <li><strong>Login endpoint</strong>: Maximum 10 requests per IP per hour</li> | ||||
|             <li><strong>Other endpoints</strong>: No specific rate limits, but excessive requests may be throttled</li> | ||||
|         </ul> | ||||
|     </div> | ||||
|  | ||||
|     <h3>Best Practices</h3> | ||||
|      | ||||
|     <div class="success"> | ||||
|         <h4>1. Connection Pooling</h4> | ||||
|         <p>Reuse HTTP connections for better performance:</p> | ||||
|         <pre><code>import requests | ||||
| from requests.adapters import HTTPAdapter | ||||
| from requests.packages.urllib3.util.retry import Retry | ||||
|  | ||||
| session = requests.Session() | ||||
| retry = Retry( | ||||
|     total=3, | ||||
|     backoff_factor=0.3, | ||||
|     status_forcelist=[500, 502, 503, 504] | ||||
| ) | ||||
| adapter = HTTPAdapter(max_retries=retry) | ||||
| session.mount('http://', adapter) | ||||
| session.mount('https://', adapter)</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <div class="success"> | ||||
|         <h4>2. Error Handling</h4> | ||||
|         <p>Implement robust error handling with specific exception types for different error conditions.</p> | ||||
|     </div> | ||||
|  | ||||
|     <div class="success"> | ||||
|         <h4>3. Caching</h4> | ||||
|         <p>Cache frequently accessed data to reduce API calls and improve performance.</p> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="performance-considerations">Performance Considerations</h2> | ||||
|      | ||||
|     <h3>1. Minimize API Calls</h3> | ||||
|      | ||||
|     <div class="example-box"> | ||||
|         <pre><code># Bad: Multiple calls (N+1 problem) | ||||
| note = api.get_note(note_id) | ||||
| for child_id in note['childNoteIds']: | ||||
|     child = api.get_note(child_id)  # N+1 problem | ||||
|     process(child) | ||||
|  | ||||
| # Good: Batch processing | ||||
| note = api.get_note(note_id) | ||||
| children = api.search_notes( | ||||
|     f"note.parents.noteId={note_id}", | ||||
|     limit=1000 | ||||
| ) | ||||
| for child in children: | ||||
|     process(child)</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>2. Use Appropriate Search Depth</h3> | ||||
|      | ||||
|     <div class="example-box"> | ||||
|         <pre><code># Limit search depth for better performance | ||||
| results = api.search_notes( | ||||
|     "keyword", | ||||
|     ancestor_note_id="root", | ||||
|     ancestor_depth="lt3"  # Only search 3 levels deep | ||||
| )</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <div class="info"> | ||||
|         <h4>Additional Resources</h4> | ||||
|         <ul> | ||||
|             <li><a href="https://github.com/TriliumNext/Trilium">Trilium GitHub Repository</a></li> | ||||
|             <li><a href="/apps/server/src/assets/etapi.openapi.yaml">OpenAPI Specification</a></li> | ||||
|             <li><a href="https://triliumnext.github.io/Docs/Wiki/search.html">Trilium Search Documentation</a></li> | ||||
|         </ul> | ||||
|     </div> | ||||
|  | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										810
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/Internal API Reference.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										810
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/Internal API Reference.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,810 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|     <title>Internal API Reference</title> | ||||
|     <style> | ||||
|         body {  | ||||
|             font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;  | ||||
|             line-height: 1.6;  | ||||
|             max-width: 1200px;  | ||||
|             margin: 0 auto;  | ||||
|             padding: 20px;  | ||||
|         } | ||||
|         h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; } | ||||
|         h2 { color: #34495e; margin-top: 40px; border-bottom: 2px solid #ecf0f1; padding-bottom: 5px; } | ||||
|         h3 { color: #7f8c8d; margin-top: 30px; } | ||||
|         h4 { color: #95a5a6; margin-top: 25px; } | ||||
|         pre {  | ||||
|             background: #2c3e50;  | ||||
|             color: #ecf0f1;  | ||||
|             padding: 20px;  | ||||
|             border-radius: 8px;  | ||||
|             overflow-x: auto;  | ||||
|             border-left: 4px solid #3498db; | ||||
|         } | ||||
|         code {  | ||||
|             background: #f4f4f4;  | ||||
|             padding: 3px 6px;  | ||||
|             border-radius: 4px;  | ||||
|             font-family: 'Courier New', Monaco, monospace;  | ||||
|             font-size: 0.9em; | ||||
|         } | ||||
|         pre code { | ||||
|             background: transparent; | ||||
|             padding: 0; | ||||
|             color: inherit; | ||||
|         } | ||||
|         .endpoint-box { | ||||
|             background: #f8f9fa; | ||||
|             border: 1px solid #e9ecef; | ||||
|             border-radius: 8px; | ||||
|             padding: 20px; | ||||
|             margin: 20px 0; | ||||
|             border-left: 4px solid #3498db; | ||||
|         } | ||||
|         .method-get { border-left-color: #28a745; } | ||||
|         .method-post { border-left-color: #007bff; } | ||||
|         .method-put { border-left-color: #ffc107; } | ||||
|         .method-patch { border-left-color: #fd7e14; } | ||||
|         .method-delete { border-left-color: #dc3545; } | ||||
|         .example-box { | ||||
|             background: #f1f3f4; | ||||
|             border-radius: 8px; | ||||
|             padding: 15px; | ||||
|             margin: 15px 0; | ||||
|         } | ||||
|         table {  | ||||
|             border-collapse: collapse;  | ||||
|             width: 100%;  | ||||
|             margin: 20px 0;  | ||||
|             box-shadow: 0 2px 4px rgba(0,0,0,0.1); | ||||
|         } | ||||
|         th, td {  | ||||
|             border: 1px solid #ddd;  | ||||
|             padding: 12px;  | ||||
|             text-align: left;  | ||||
|         } | ||||
|         th {  | ||||
|             background: #34495e;  | ||||
|             color: white; | ||||
|             font-weight: 600; | ||||
|         } | ||||
|         tr:nth-child(even) { background: #f8f9fa; } | ||||
|         .warning { | ||||
|             background: #fff3cd; | ||||
|             border: 1px solid #ffeaa7; | ||||
|             border-radius: 6px; | ||||
|             padding: 15px; | ||||
|             margin: 20px 0; | ||||
|             border-left: 4px solid #f39c12; | ||||
|         } | ||||
|         .info { | ||||
|             background: #d1ecf1; | ||||
|             border: 1px solid #74c0fc; | ||||
|             border-radius: 6px; | ||||
|             padding: 15px; | ||||
|             margin: 20px 0; | ||||
|             border-left: 4px solid #3498db; | ||||
|         } | ||||
|         .danger { | ||||
|             background: #f8d7da; | ||||
|             border: 1px solid #f1aeb5; | ||||
|             border-radius: 6px; | ||||
|             padding: 15px; | ||||
|             margin: 20px 0; | ||||
|             border-left: 4px solid #dc3545; | ||||
|         } | ||||
|         .toc { | ||||
|             background: #f8f9fa; | ||||
|             border-radius: 8px; | ||||
|             padding: 20px; | ||||
|             margin-bottom: 30px; | ||||
|         } | ||||
|         .toc ul { | ||||
|             list-style-type: none; | ||||
|             padding-left: 20px; | ||||
|         } | ||||
|         .toc a { | ||||
|             text-decoration: none; | ||||
|             color: #495057; | ||||
|         } | ||||
|         .toc a:hover { | ||||
|             color: #3498db; | ||||
|         } | ||||
|         .websocket-box { | ||||
|             background: #e8f4f8; | ||||
|             border-left: 4px solid #9b59b6; | ||||
|             padding: 15px; | ||||
|             margin: 15px 0; | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|     <h1>Internal API Reference</h1> | ||||
|      | ||||
|     <div class="toc"> | ||||
|         <h2>Table of Contents</h2> | ||||
|         <ul> | ||||
|             <li><a href="#introduction">Introduction</a></li> | ||||
|             <li><a href="#authentication-and-session-management">Authentication and Session Management</a></li> | ||||
|             <li><a href="#core-api-endpoints">Core API Endpoints</a></li> | ||||
|             <li><a href="#websocket-real-time-updates">WebSocket Real-time Updates</a></li> | ||||
|             <li><a href="#file-operations">File Operations</a></li> | ||||
|             <li><a href="#import-export-operations">Import/Export Operations</a></li> | ||||
|             <li><a href="#when-to-use-internal-vs-etapi">When to Use Internal vs ETAPI</a></li> | ||||
|             <li><a href="#security-considerations">Security Considerations</a></li> | ||||
|         </ul> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="introduction">Introduction</h2> | ||||
|      | ||||
|     <p>The Internal API is the primary interface used by the Trilium Notes client application to communicate with the server. While powerful and feature-complete, this API is primarily designed for internal use.</p> | ||||
|  | ||||
|     <div class="danger"> | ||||
|         <h4>Important Notice</h4> | ||||
|         <p><strong>For external integrations, please use <a href="ETAPI%20Complete%20Guide.html">ETAPI</a> instead.</strong> The Internal API:</p> | ||||
|         <ul> | ||||
|             <li>May change between versions without notice</li> | ||||
|             <li>Requires session-based authentication with CSRF protection</li> | ||||
|             <li>Is tightly coupled with the frontend application</li> | ||||
|             <li>Has limited documentation and stability guarantees</li> | ||||
|         </ul> | ||||
|     </div> | ||||
|  | ||||
|     <div class="info"> | ||||
|         <strong>Base URL:</strong> <code>http://localhost:8080/api</code> | ||||
|     </div> | ||||
|  | ||||
|     <h3>Key Characteristics</h3> | ||||
|     <ul> | ||||
|         <li>Session-based authentication with cookies</li> | ||||
|         <li>CSRF token protection for state-changing operations</li> | ||||
|         <li>WebSocket support for real-time updates</li> | ||||
|         <li>Full feature parity with the Trilium UI</li> | ||||
|         <li>Complex request/response formats optimized for the client</li> | ||||
|     </ul> | ||||
|  | ||||
|     <h2 id="authentication-and-session-management">Authentication and Session Management</h2> | ||||
|      | ||||
|     <h3>Password Login</h3> | ||||
|      | ||||
|     <div class="endpoint-box method-post"> | ||||
|         <strong>POST</strong> <code>/api/login</code> | ||||
|         <p>Authenticates user with password and creates a session.</p> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <strong>Request:</strong> | ||||
|         <pre><code>const formData = new URLSearchParams(); | ||||
| formData.append('password', 'your-password'); | ||||
|  | ||||
| const response = await fetch('http://localhost:8080/api/login', { | ||||
|     method: 'POST', | ||||
|     headers: { | ||||
|         'Content-Type': 'application/x-www-form-urlencoded' | ||||
|     }, | ||||
|     body: formData, | ||||
|     credentials: 'include'  // Important for cookie handling | ||||
| });</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <strong>Response:</strong> | ||||
|         <pre><code>{ | ||||
|     "success": true, | ||||
|     "message": "Login successful" | ||||
| }</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <p>The server sets a session cookie (<code>trilium.sid</code>) that must be included in subsequent requests.</p> | ||||
|  | ||||
|     <h3>TOTP Authentication (2FA)</h3> | ||||
|      | ||||
|     <p>If 2FA is enabled, include the TOTP token:</p> | ||||
|      | ||||
|     <div class="example-box"> | ||||
|         <pre><code>formData.append('password', 'your-password'); | ||||
| formData.append('totpToken', '123456');</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>Token Authentication</h3> | ||||
|      | ||||
|     <div class="endpoint-box method-post"> | ||||
|         <strong>POST</strong> <code>/api/login/token</code> | ||||
|         <p>Generate an API token for programmatic access:</p> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <pre><code>const response = await fetch('http://localhost:8080/api/login/token', { | ||||
|     method: 'POST', | ||||
|     headers: { | ||||
|         'Content-Type': 'application/json' | ||||
|     }, | ||||
|     body: JSON.stringify({ | ||||
|         password: 'your-password', | ||||
|         tokenName: 'My Integration' | ||||
|     }) | ||||
| }); | ||||
|  | ||||
| const { authToken } = await response.json(); | ||||
| // Use this token in Authorization header for future requests</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>Protected Session</h3> | ||||
|      | ||||
|     <div class="endpoint-box method-post"> | ||||
|         <strong>POST</strong> <code>/api/login/protected</code> | ||||
|         <p>Enter protected session to access encrypted notes:</p> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <pre><code>await fetch('http://localhost:8080/api/login/protected', { | ||||
|     method: 'POST', | ||||
|     headers: { | ||||
|         'Content-Type': 'application/json', | ||||
|         'X-CSRF-Token': csrfToken | ||||
|     }, | ||||
|     body: JSON.stringify({ | ||||
|         password: 'your-password' | ||||
|     }), | ||||
|     credentials: 'include' | ||||
| });</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="core-api-endpoints">Core API Endpoints</h2> | ||||
|      | ||||
|     <h3>Notes</h3> | ||||
|      | ||||
|     <h4>Get Note</h4> | ||||
|      | ||||
|     <div class="endpoint-box method-get"> | ||||
|         <strong>GET</strong> <code>/api/notes/{noteId}</code> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <pre><code>const response = await fetch('http://localhost:8080/api/notes/root', { | ||||
|     credentials: 'include' | ||||
| }); | ||||
|  | ||||
| const note = await response.json();</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <strong>Response:</strong> | ||||
|         <pre><code>{ | ||||
|     "noteId": "root", | ||||
|     "title": "Trilium Notes", | ||||
|     "type": "text", | ||||
|     "mime": "text/html", | ||||
|     "isProtected": false, | ||||
|     "isDeleted": false, | ||||
|     "dateCreated": "2024-01-01 00:00:00.000+0000", | ||||
|     "dateModified": "2024-01-15 10:30:00.000+0000", | ||||
|     "utcDateCreated": "2024-01-01 00:00:00.000Z", | ||||
|     "utcDateModified": "2024-01-15 10:30:00.000Z", | ||||
|     "parentBranches": [ | ||||
|         { | ||||
|             "branchId": "root_root", | ||||
|             "parentNoteId": "none", | ||||
|             "prefix": null, | ||||
|             "notePosition": 10 | ||||
|         } | ||||
|     ], | ||||
|     "attributes": [], | ||||
|     "cssClass": "", | ||||
|     "iconClass": "bx bx-folder" | ||||
| }</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h4>Create Note</h4> | ||||
|      | ||||
|     <div class="endpoint-box method-post"> | ||||
|         <strong>POST</strong> <code>/api/notes/{parentNoteId}/children</code> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <pre><code>const response = await fetch('http://localhost:8080/api/notes/root/children', { | ||||
|     method: 'POST', | ||||
|     headers: { | ||||
|         'Content-Type': 'application/json', | ||||
|         'X-CSRF-Token': csrfToken | ||||
|     }, | ||||
|     body: JSON.stringify({ | ||||
|         title: 'New Note', | ||||
|         type: 'text', | ||||
|         content: '<p>Note content</p>', | ||||
|         isProtected: false | ||||
|     }), | ||||
|     credentials: 'include' | ||||
| }); | ||||
|  | ||||
| const { note, branch } = await response.json();</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h4>Update Note</h4> | ||||
|      | ||||
|     <div class="endpoint-box method-put"> | ||||
|         <strong>PUT</strong> <code>/api/notes/{noteId}</code> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <pre><code>await fetch(`http://localhost:8080/api/notes/${noteId}`, { | ||||
|     method: 'PUT', | ||||
|     headers: { | ||||
|         'Content-Type': 'application/json', | ||||
|         'X-CSRF-Token': csrfToken | ||||
|     }, | ||||
|     body: JSON.stringify({ | ||||
|         title: 'Updated Title', | ||||
|         type: 'text', | ||||
|         mime: 'text/html' | ||||
|     }), | ||||
|     credentials: 'include' | ||||
| });</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h4>Get Note Content</h4> | ||||
|      | ||||
|     <div class="endpoint-box method-get"> | ||||
|         <strong>GET</strong> <code>/api/notes/{noteId}/content</code> | ||||
|         <p>Returns the actual content of the note:</p> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <pre><code>const response = await fetch(`http://localhost:8080/api/notes/${noteId}/content`, { | ||||
|     credentials: 'include' | ||||
| }); | ||||
|  | ||||
| const content = await response.text();</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h4>Save Note Content</h4> | ||||
|      | ||||
|     <div class="endpoint-box method-put"> | ||||
|         <strong>PUT</strong> <code>/api/notes/{noteId}/content</code> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <pre><code>await fetch(`http://localhost:8080/api/notes/${noteId}/content`, { | ||||
|     method: 'PUT', | ||||
|     headers: { | ||||
|         'Content-Type': 'text/html', | ||||
|         'X-CSRF-Token': csrfToken | ||||
|     }, | ||||
|     body: '<p>Updated content</p>', | ||||
|     credentials: 'include' | ||||
| });</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>Tree Operations</h3> | ||||
|      | ||||
|     <h4>Move Note</h4> | ||||
|      | ||||
|     <div class="endpoint-box method-put"> | ||||
|         <strong>PUT</strong> <code>/api/branches/{branchId}/move</code> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <pre><code>await fetch(`http://localhost:8080/api/branches/${branchId}/move`, { | ||||
|     method: 'PUT', | ||||
|     headers: { | ||||
|         'Content-Type': 'application/json', | ||||
|         'X-CSRF-Token': csrfToken | ||||
|     }, | ||||
|     body: JSON.stringify({ | ||||
|         parentNoteId: 'newParentId', | ||||
|         beforeNoteId: 'siblingNoteId'  // optional, for positioning | ||||
|     }), | ||||
|     credentials: 'include' | ||||
| });</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h4>Clone Note</h4> | ||||
|      | ||||
|     <div class="endpoint-box method-post"> | ||||
|         <strong>POST</strong> <code>/api/notes/{noteId}/clone</code> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <pre><code>const response = await fetch(`http://localhost:8080/api/notes/${noteId}/clone`, { | ||||
|     method: 'POST', | ||||
|     headers: { | ||||
|         'Content-Type': 'application/json', | ||||
|         'X-CSRF-Token': csrfToken | ||||
|     }, | ||||
|     body: JSON.stringify({ | ||||
|         parentNoteId: 'targetParentId', | ||||
|         prefix: 'Copy of ' | ||||
|     }), | ||||
|     credentials: 'include' | ||||
| });</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>Search</h3> | ||||
|      | ||||
|     <h4>Search Notes</h4> | ||||
|      | ||||
|     <div class="endpoint-box method-get"> | ||||
|         <strong>GET</strong> <code>/api/search</code> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <pre><code>const params = new URLSearchParams({ | ||||
|     query: '#todo OR #task', | ||||
|     fastSearch: 'false', | ||||
|     includeArchivedNotes: 'false', | ||||
|     ancestorNoteId: 'root', | ||||
|     orderBy: 'relevancy', | ||||
|     orderDirection: 'desc', | ||||
|     limit: '50' | ||||
| }); | ||||
|  | ||||
| const response = await fetch(`http://localhost:8080/api/search?${params}`, { | ||||
|     credentials: 'include' | ||||
| }); | ||||
|  | ||||
| const { results } = await response.json();</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="websocket-real-time-updates">WebSocket Real-time Updates</h2> | ||||
|      | ||||
|     <div class="websocket-box"> | ||||
|         <p>The Internal API provides WebSocket connections for real-time synchronization and updates.</p> | ||||
|     </div> | ||||
|  | ||||
|     <h3>Connection Setup</h3> | ||||
|      | ||||
|     <div class="example-box"> | ||||
|         <pre><code>class TriliumWebSocket { | ||||
|     constructor() { | ||||
|         this.ws = null; | ||||
|         this.reconnectInterval = 5000; | ||||
|         this.shouldReconnect = true; | ||||
|     } | ||||
|      | ||||
|     connect() { | ||||
|         // WebSocket URL same as base URL but with ws:// protocol | ||||
|         const wsUrl = 'ws://localhost:8080'; | ||||
|          | ||||
|         this.ws = new WebSocket(wsUrl); | ||||
|          | ||||
|         this.ws.onopen = () => { | ||||
|             console.log('WebSocket connected'); | ||||
|             this.sendPing(); | ||||
|         }; | ||||
|          | ||||
|         this.ws.onmessage = (event) => { | ||||
|             const message = JSON.parse(event.data); | ||||
|             this.handleMessage(message); | ||||
|         }; | ||||
|          | ||||
|         this.ws.onerror = (error) => { | ||||
|             console.error('WebSocket error:', error); | ||||
|         }; | ||||
|          | ||||
|         this.ws.onclose = () => { | ||||
|             console.log('WebSocket disconnected'); | ||||
|             if (this.shouldReconnect) { | ||||
|                 setTimeout(() => this.connect(), this.reconnectInterval); | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|      | ||||
|     handleMessage(message) { | ||||
|         switch (message.type) { | ||||
|             case 'sync': | ||||
|                 this.handleSync(message.data); | ||||
|                 break; | ||||
|             case 'entity-changes': | ||||
|                 this.handleEntityChanges(message.data); | ||||
|                 break; | ||||
|             case 'refresh-tree': | ||||
|                 this.refreshTree(); | ||||
|                 break; | ||||
|             case 'create-note': | ||||
|                 this.handleNoteCreated(message.data); | ||||
|                 break; | ||||
|             case 'update-note': | ||||
|                 this.handleNoteUpdated(message.data); | ||||
|                 break; | ||||
|             case 'delete-note': | ||||
|                 this.handleNoteDeleted(message.data); | ||||
|                 break; | ||||
|             default: | ||||
|                 console.log('Unknown message type:', message.type); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     sendPing() { | ||||
|         if (this.ws.readyState === WebSocket.OPEN) { | ||||
|             this.ws.send(JSON.stringify({ type: 'ping' })); | ||||
|             setTimeout(() => this.sendPing(), 30000); // Ping every 30 seconds | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     send(type, data) { | ||||
|         if (this.ws.readyState === WebSocket.OPEN) { | ||||
|             this.ws.send(JSON.stringify({ type, data })); | ||||
|         } | ||||
|     } | ||||
| }</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>Message Types</h3> | ||||
|      | ||||
|     <table> | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <th>Type</th> | ||||
|                 <th>Direction</th> | ||||
|                 <th>Description</th> | ||||
|                 <th>Data Format</th> | ||||
|             </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|             <tr> | ||||
|                 <td><code>sync</code></td> | ||||
|                 <td>Incoming</td> | ||||
|                 <td>Synchronization data</td> | ||||
|                 <td><code>{ entityChanges: [], lastSyncedPush: number }</code></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><code>entity-changes</code></td> | ||||
|                 <td>Incoming</td> | ||||
|                 <td>Entity modifications</td> | ||||
|                 <td><code>[{ entityName, entityId, action }]</code></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><code>refresh-tree</code></td> | ||||
|                 <td>Incoming</td> | ||||
|                 <td>Tree structure changed</td> | ||||
|                 <td><code>None</code></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><code>ping</code></td> | ||||
|                 <td>Outgoing</td> | ||||
|                 <td>Keep connection alive</td> | ||||
|                 <td><code>None</code></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><code>log-error</code></td> | ||||
|                 <td>Outgoing</td> | ||||
|                 <td>Log client error</td> | ||||
|                 <td><code>{ error, stack }</code></td> | ||||
|             </tr> | ||||
|         </tbody> | ||||
|     </table> | ||||
|  | ||||
|     <h2 id="file-operations">File Operations</h2> | ||||
|      | ||||
|     <h3>Upload File</h3> | ||||
|      | ||||
|     <div class="endpoint-box method-post"> | ||||
|         <strong>POST</strong> <code>/api/notes/{noteId}/attachments/upload</code> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <pre><code>const formData = new FormData(); | ||||
| formData.append('file', fileInput.files[0]); | ||||
|  | ||||
| const response = await fetch(`/api/notes/${noteId}/attachments/upload`, { | ||||
|     method: 'POST', | ||||
|     headers: { | ||||
|         'X-CSRF-Token': csrfToken | ||||
|     }, | ||||
|     body: formData, | ||||
|     credentials: 'include' | ||||
| }); | ||||
|  | ||||
| const attachment = await response.json();</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>Download Attachment</h3> | ||||
|      | ||||
|     <div class="endpoint-box method-get"> | ||||
|         <strong>GET</strong> <code>/api/attachments/{attachmentId}/download</code> | ||||
|     </div> | ||||
|  | ||||
|     <div class="example-box"> | ||||
|         <pre><code>const response = await fetch(`/api/attachments/${attachmentId}/download`, { | ||||
|     credentials: 'include' | ||||
| }); | ||||
|  | ||||
| const blob = await response.blob(); | ||||
| const url = URL.createObjectURL(blob); | ||||
| const a = document.createElement('a'); | ||||
| a.href = url; | ||||
| a.download = 'attachment.pdf'; | ||||
| a.click();</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="when-to-use-internal-vs-etapi">When to Use Internal vs ETAPI</h2> | ||||
|      | ||||
|     <table> | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <th>Use Internal API When</th> | ||||
|                 <th>Use ETAPI When</th> | ||||
|             </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|             <tr> | ||||
|                 <td>Building custom Trilium clients</td> | ||||
|                 <td>Building external integrations</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td>Needing WebSocket real-time updates</td> | ||||
|                 <td>Creating automation scripts</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td>Requiring full feature parity with the UI</td> | ||||
|                 <td>Developing third-party applications</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td>Working within the Trilium frontend environment</td> | ||||
|                 <td>Needing stable, documented API</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td>Accessing advanced features not available in ETAPI</td> | ||||
|                 <td>Working with different programming languages</td> | ||||
|             </tr> | ||||
|         </tbody> | ||||
|     </table> | ||||
|  | ||||
|     <h3>Feature Comparison</h3> | ||||
|      | ||||
|     <table> | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <th>Feature</th> | ||||
|                 <th>Internal API</th> | ||||
|                 <th>ETAPI</th> | ||||
|             </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|             <tr> | ||||
|                 <td><strong>Authentication</strong></td> | ||||
|                 <td>Session/Cookie</td> | ||||
|                 <td>Token</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><strong>CSRF Protection</strong></td> | ||||
|                 <td>Required</td> | ||||
|                 <td>Not needed</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><strong>WebSocket</strong></td> | ||||
|                 <td>Yes</td> | ||||
|                 <td>No</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><strong>Stability</strong></td> | ||||
|                 <td>May change</td> | ||||
|                 <td>Stable</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><strong>Documentation</strong></td> | ||||
|                 <td>Limited</td> | ||||
|                 <td>Comprehensive</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><strong>Real-time updates</strong></td> | ||||
|                 <td>Yes</td> | ||||
|                 <td>No</td> | ||||
|             </tr> | ||||
|         </tbody> | ||||
|     </table> | ||||
|  | ||||
|     <h2 id="security-considerations">Security Considerations</h2> | ||||
|      | ||||
|     <h3>CSRF Protection</h3> | ||||
|      | ||||
|     <p>All state-changing operations require a CSRF token:</p> | ||||
|      | ||||
|     <div class="example-box"> | ||||
|         <pre><code>// Get CSRF token from meta tag or API | ||||
| async function getCsrfToken() { | ||||
|     const response = await fetch('/api/csrf-token', { | ||||
|         credentials: 'include' | ||||
|     }); | ||||
|     const { token } = await response.json(); | ||||
|     return token; | ||||
| } | ||||
|  | ||||
| // Use in requests | ||||
| const csrfToken = await getCsrfToken(); | ||||
|  | ||||
| await fetch('/api/notes', { | ||||
|     method: 'POST', | ||||
|     headers: { | ||||
|         'Content-Type': 'application/json', | ||||
|         'X-CSRF-Token': csrfToken | ||||
|     }, | ||||
|     body: JSON.stringify(data), | ||||
|     credentials: 'include' | ||||
| });</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>Session Management</h3> | ||||
|      | ||||
|     <div class="example-box"> | ||||
|         <pre><code>class TriliumSession { | ||||
|     constructor() { | ||||
|         this.isAuthenticated = false; | ||||
|         this.csrfToken = null; | ||||
|     } | ||||
|      | ||||
|     async login(password) { | ||||
|         const formData = new URLSearchParams(); | ||||
|         formData.append('password', password); | ||||
|          | ||||
|         const response = await fetch('/api/login', { | ||||
|             method: 'POST', | ||||
|             headers: { | ||||
|                 'Content-Type': 'application/x-www-form-urlencoded' | ||||
|             }, | ||||
|             body: formData, | ||||
|             credentials: 'include' | ||||
|         }); | ||||
|          | ||||
|         if (response.ok) { | ||||
|             this.isAuthenticated = true; | ||||
|             this.csrfToken = await this.getCsrfToken(); | ||||
|             return true; | ||||
|         } | ||||
|          | ||||
|         return false; | ||||
|     } | ||||
|      | ||||
|     async getCsrfToken() { | ||||
|         const response = await fetch('/api/csrf-token', { | ||||
|             credentials: 'include' | ||||
|         }); | ||||
|         const { token } = await response.json(); | ||||
|         return token; | ||||
|     } | ||||
|      | ||||
|     async request(url, options = {}) { | ||||
|         if (!this.isAuthenticated) { | ||||
|             throw new Error('Not authenticated'); | ||||
|         } | ||||
|          | ||||
|         const headers = { | ||||
|             ...options.headers | ||||
|         }; | ||||
|          | ||||
|         if (options.method && options.method !== 'GET') { | ||||
|             headers['X-CSRF-Token'] = this.csrfToken; | ||||
|         } | ||||
|          | ||||
|         return fetch(url, { | ||||
|             ...options, | ||||
|             headers, | ||||
|             credentials: 'include' | ||||
|         }); | ||||
|     } | ||||
| }</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <div class="warning"> | ||||
|         <h4>Important Security Notes</h4> | ||||
|         <ul> | ||||
|             <li>Always include <code>credentials: 'include'</code> for session-based requests</li> | ||||
|             <li>Use CSRF tokens for all state-changing operations</li> | ||||
|             <li>Handle protected notes with proper authentication</li> | ||||
|             <li>Validate all user input before sending to the API</li> | ||||
|             <li>Use HTTPS in production environments</li> | ||||
|         </ul> | ||||
|     </div> | ||||
|  | ||||
|     <div class="info"> | ||||
|         <h4>Related Documentation</h4> | ||||
|         <ul> | ||||
|             <li><a href="ETAPI%20Complete%20Guide.html">ETAPI Complete Guide</a></li> | ||||
|             <li><a href="WebSocket%20API.html">WebSocket API Documentation</a></li> | ||||
|             <li><a href="Script%20API%20Cookbook.html">Script API Cookbook</a></li> | ||||
|         </ul> | ||||
|     </div> | ||||
|  | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										937
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/Script API Cookbook.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										937
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/Script API Cookbook.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,937 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|     <title>Script API Cookbook</title> | ||||
|     <style> | ||||
|         body {  | ||||
|             font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;  | ||||
|             line-height: 1.6;  | ||||
|             max-width: 1200px;  | ||||
|             margin: 0 auto;  | ||||
|             padding: 20px;  | ||||
|         } | ||||
|         h1 { color: #2c3e50; border-bottom: 3px solid #8e44ad; padding-bottom: 10px; } | ||||
|         h2 { color: #34495e; margin-top: 40px; border-bottom: 2px solid #ecf0f1; padding-bottom: 5px; } | ||||
|         h3 { color: #7f8c8d; margin-top: 30px; } | ||||
|         h4 { color: #95a5a6; margin-top: 25px; } | ||||
|         pre {  | ||||
|             background: #2c3e50;  | ||||
|             color: #ecf0f1;  | ||||
|             padding: 20px;  | ||||
|             border-radius: 8px;  | ||||
|             overflow-x: auto;  | ||||
|             border-left: 4px solid #8e44ad; | ||||
|         } | ||||
|         code {  | ||||
|             background: #f4f4f4;  | ||||
|             padding: 3px 6px;  | ||||
|             border-radius: 4px;  | ||||
|             font-family: 'Courier New', Monaco, monospace;  | ||||
|             font-size: 0.9em; | ||||
|         } | ||||
|         pre code { | ||||
|             background: transparent; | ||||
|             padding: 0; | ||||
|             color: inherit; | ||||
|         } | ||||
|         .recipe-box { | ||||
|             background: #f8f9fa; | ||||
|             border: 1px solid #e9ecef; | ||||
|             border-radius: 8px; | ||||
|             padding: 20px; | ||||
|             margin: 20px 0; | ||||
|             border-left: 4px solid #8e44ad; | ||||
|         } | ||||
|         .backend-script { border-left-color: #e74c3c; } | ||||
|         .frontend-script { border-left-color: #3498db; } | ||||
|         .example-box { | ||||
|             background: #f1f3f4; | ||||
|             border-radius: 8px; | ||||
|             padding: 15px; | ||||
|             margin: 15px 0; | ||||
|         } | ||||
|         table {  | ||||
|             border-collapse: collapse;  | ||||
|             width: 100%;  | ||||
|             margin: 20px 0;  | ||||
|             box-shadow: 0 2px 4px rgba(0,0,0,0.1); | ||||
|         } | ||||
|         th, td {  | ||||
|             border: 1px solid #ddd;  | ||||
|             padding: 12px;  | ||||
|             text-align: left;  | ||||
|         } | ||||
|         th {  | ||||
|             background: #8e44ad;  | ||||
|             color: white; | ||||
|             font-weight: 600; | ||||
|         } | ||||
|         tr:nth-child(even) { background: #f8f9fa; } | ||||
|         .warning { | ||||
|             background: #fff3cd; | ||||
|             border: 1px solid #ffeaa7; | ||||
|             border-radius: 6px; | ||||
|             padding: 15px; | ||||
|             margin: 20px 0; | ||||
|             border-left: 4px solid #f39c12; | ||||
|         } | ||||
|         .info { | ||||
|             background: #d1ecf1; | ||||
|             border: 1px solid #74c0fc; | ||||
|             border-radius: 6px; | ||||
|             padding: 15px; | ||||
|             margin: 20px 0; | ||||
|             border-left: 4px solid #3498db; | ||||
|         } | ||||
|         .success { | ||||
|             background: #d4edda; | ||||
|             border: 1px solid #51cf66; | ||||
|             border-radius: 6px; | ||||
|             padding: 15px; | ||||
|             margin: 20px 0; | ||||
|             border-left: 4px solid #28a745; | ||||
|         } | ||||
|         .toc { | ||||
|             background: #f8f9fa; | ||||
|             border-radius: 8px; | ||||
|             padding: 20px; | ||||
|             margin-bottom: 30px; | ||||
|         } | ||||
|         .toc ul { | ||||
|             list-style-type: none; | ||||
|             padding-left: 20px; | ||||
|         } | ||||
|         .toc a { | ||||
|             text-decoration: none; | ||||
|             color: #495057; | ||||
|         } | ||||
|         .toc a:hover { | ||||
|             color: #8e44ad; | ||||
|         } | ||||
|         .use-case { | ||||
|             background: #f8f4ff; | ||||
|             border-left: 4px solid #8e44ad; | ||||
|             padding: 15px; | ||||
|             margin: 15px 0; | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|     <h1>Script API Cookbook</h1> | ||||
|      | ||||
|     <div class="toc"> | ||||
|         <h2>Table of Contents</h2> | ||||
|         <ul> | ||||
|             <li><a href="#introduction">Introduction</a></li> | ||||
|             <li><a href="#backend-script-recipes">Backend Script Recipes</a></li> | ||||
|             <li><a href="#frontend-script-recipes">Frontend Script Recipes</a></li> | ||||
|             <li><a href="#common-patterns">Common Patterns</a></li> | ||||
|             <li><a href="#note-manipulation">Note Manipulation</a></li> | ||||
|             <li><a href="#attribute-operations">Attribute Operations</a></li> | ||||
|             <li><a href="#search-and-filtering">Search and Filtering</a></li> | ||||
|             <li><a href="#automation-examples">Automation Examples</a></li> | ||||
|             <li><a href="#integration-with-external-services">Integration with External Services</a></li> | ||||
|             <li><a href="#best-practices">Best Practices</a></li> | ||||
|         </ul> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="introduction">Introduction</h2> | ||||
|      | ||||
|     <p>Trilium's Script API provides powerful automation capabilities through JavaScript code that runs either on the backend (Node.js) or frontend (browser). This cookbook contains practical recipes and patterns for common scripting tasks.</p> | ||||
|  | ||||
|     <h3>Script Types</h3> | ||||
|      | ||||
|     <table> | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <th>Type</th> | ||||
|                 <th>Environment</th> | ||||
|                 <th>Access</th> | ||||
|                 <th>Use Cases</th> | ||||
|             </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|             <tr> | ||||
|                 <td><strong>Backend Script</strong></td> | ||||
|                 <td>Node.js</td> | ||||
|                 <td>Full database, file system, network</td> | ||||
|                 <td>Automation, data processing, integrations</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><strong>Frontend Script</strong></td> | ||||
|                 <td>Browser</td> | ||||
|                 <td>UI manipulation, user interaction</td> | ||||
|                 <td>Custom widgets, UI enhancements</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><strong>Custom Widget</strong></td> | ||||
|                 <td>Browser</td> | ||||
|                 <td>Widget lifecycle, note context</td> | ||||
|                 <td>Interactive components, visualizations</td> | ||||
|             </tr> | ||||
|         </tbody> | ||||
|     </table> | ||||
|  | ||||
|     <h3>Basic Script Structure</h3> | ||||
|      | ||||
|     <div class="recipe-box backend-script"> | ||||
|         <h4>Backend Script:</h4> | ||||
|         <pre><code>// Access to api object is automatic | ||||
| const note = await api.getNoteWithLabel('todoList'); | ||||
| const children = await note.getChildNotes(); | ||||
|  | ||||
| // Return value becomes script output | ||||
| return { | ||||
|     noteTitle: note.title, | ||||
|     childCount: children.length | ||||
| };</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <div class="recipe-box frontend-script"> | ||||
|         <h4>Frontend Script:</h4> | ||||
|         <pre><code>// Access to api object is automatic | ||||
| api.showMessage('Script executed!'); | ||||
|  | ||||
| // Manipulate UI | ||||
| const $button = $('<button>').text('Click Me').click(() => { | ||||
|     api.showMessage('Button clicked!'); | ||||
| }); | ||||
|  | ||||
| $('body').append($button);</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="backend-script-recipes">Backend Script Recipes</h2> | ||||
|      | ||||
|     <h3>1. Daily Note Generator</h3> | ||||
|      | ||||
|     <div class="use-case"> | ||||
|         <strong>Use Case:</strong> Automatically create daily notes with template content | ||||
|     </div> | ||||
|  | ||||
|     <div class="recipe-box backend-script"> | ||||
|         <pre><code>// #run=hourly | ||||
|  | ||||
| async function createDailyNote() { | ||||
|     const today = api.dayjs().format('YYYY-MM-DD'); | ||||
|     const dayNote = await api.getDayNote(today); | ||||
|      | ||||
|     // Check if content already exists | ||||
|     const content = await dayNote.getContent(); | ||||
|     if (content && content.length > 100) { | ||||
|         return; // Already has content | ||||
|     } | ||||
|      | ||||
|     // Get template | ||||
|     const template = await api.getNoteWithLabel('dailyTemplate'); | ||||
|     if (!template) { | ||||
|         await dayNote.setContent(` | ||||
|             <h2>📅 ${api.dayjs().format('dddd, MMMM D, YYYY')}</h2> | ||||
|              | ||||
|             <h3>☀️ Morning Routine</h3> | ||||
|             <ul> | ||||
|                 <li>[ ] Morning meditation</li> | ||||
|                 <li>[ ] Exercise</li> | ||||
|                 <li>[ ] Review daily goals</li> | ||||
|             </ul> | ||||
|              | ||||
|             <h3>📋 Today's Tasks</h3> | ||||
|             <ul> | ||||
|                 <li></li> | ||||
|             </ul> | ||||
|              | ||||
|             <h3>📝 Notes</h3> | ||||
|             <p></p> | ||||
|              | ||||
|             <h3>🌙 Evening Reflection</h3> | ||||
|             <p></p> | ||||
|         `); | ||||
|     } else { | ||||
|         const templateContent = await template.getContent(); | ||||
|         await dayNote.setContent(templateContent); | ||||
|     } | ||||
|      | ||||
|     // Add metadata | ||||
|     await dayNote.setLabel('type', 'daily'); | ||||
|     await dayNote.setLabel('created', api.dayjs().format()); | ||||
|      | ||||
|     api.log(`Daily note created for ${today}`); | ||||
| } | ||||
|  | ||||
| await createDailyNote();</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>2. Note Statistics Collector</h3> | ||||
|      | ||||
|     <div class="use-case"> | ||||
|         <strong>Use Case:</strong> Collect and display statistics about your notes | ||||
|     </div> | ||||
|  | ||||
|     <div class="recipe-box backend-script"> | ||||
|         <pre><code>async function collectStatistics() { | ||||
|     const stats = { | ||||
|         totalNotes: 0, | ||||
|         notesByType: {}, | ||||
|         notesByMonth: {}, | ||||
|         largestNotes: [], | ||||
|         recentlyModified: [], | ||||
|         tagCloud: {} | ||||
|     }; | ||||
|      | ||||
|     // Get all notes | ||||
|     const notes = await api.searchForNotes(''); | ||||
|     stats.totalNotes = notes.length; | ||||
|      | ||||
|     for (const note of notes) { | ||||
|         // Count by type | ||||
|         stats.notesByType[note.type] = (stats.notesByType[note.type] || 0) + 1; | ||||
|          | ||||
|         // Count by creation month | ||||
|         const month = api.dayjs(note.utcDateCreated).format('YYYY-MM'); | ||||
|         stats.notesByMonth[month] = (stats.notesByMonth[month] || 0) + 1; | ||||
|          | ||||
|         // Track largest notes | ||||
|         const content = await note.getContent(); | ||||
|         if (content) { | ||||
|             stats.largestNotes.push({ | ||||
|                 noteId: note.noteId, | ||||
|                 title: note.title, | ||||
|                 size: content.length | ||||
|             }); | ||||
|         } | ||||
|          | ||||
|         // Collect labels for tag cloud | ||||
|         const labels = await note.getLabels(); | ||||
|         for (const label of labels) { | ||||
|             if (!label.name.startsWith('child:')) { | ||||
|                 stats.tagCloud[label.name] = (stats.tagCloud[label.name] || 0) + 1; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Sort largest notes | ||||
|     stats.largestNotes.sort((a, b) => b.size - a.size); | ||||
|     stats.largestNotes = stats.largestNotes.slice(0, 10); | ||||
|      | ||||
|     // Create or update statistics note | ||||
|     let statsNote = await api.getNoteWithLabel('statistics'); | ||||
|     if (!statsNote) { | ||||
|         statsNote = await api.createTextNote('root', 'Statistics', ''); | ||||
|         await statsNote.setLabel('statistics'); | ||||
|     } | ||||
|      | ||||
|     // Generate report | ||||
|     const report = ` | ||||
|         <h1>📊 Note Statistics</h1> | ||||
|         <p>Generated: ${api.dayjs().format('YYYY-MM-DD HH:mm:ss')}</p> | ||||
|          | ||||
|         <h2>Overview</h2> | ||||
|         <ul> | ||||
|             <li>Total Notes: <strong>${stats.totalNotes}</strong></li> | ||||
|             <li>Note Types: ${Object.entries(stats.notesByType) | ||||
|                 .map(([type, count]) => `${type} (${count})`) | ||||
|                 .join(', ')}</li> | ||||
|         </ul> | ||||
|          | ||||
|         <h2>Largest Notes</h2> | ||||
|         <ol> | ||||
|             ${stats.largestNotes.map(n =>  | ||||
|                 `<li><a href="#root/${n.noteId}">${n.title}</a> - ${(n.size / 1024).toFixed(1)} KB</li>` | ||||
|             ).join('')} | ||||
|         </ol> | ||||
|          | ||||
|         <h2>Top Tags</h2> | ||||
|         <div class="tag-cloud"> | ||||
|             ${Object.entries(stats.tagCloud) | ||||
|                 .sort((a, b) => b[1] - a[1]) | ||||
|                 .slice(0, 20) | ||||
|                 .map(([tag, count]) =>  | ||||
|                     `<span style="font-size: ${Math.min(200, 100 + count * 5)}%">#${tag} (${count})</span>` | ||||
|                 ).join(' ')} | ||||
|         </div> | ||||
|     `; | ||||
|      | ||||
|     await statsNote.setContent(report); | ||||
|      | ||||
|     return stats; | ||||
| } | ||||
|  | ||||
| return await collectStatistics();</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>3. Task Aggregator</h3> | ||||
|      | ||||
|     <div class="use-case"> | ||||
|         <strong>Use Case:</strong> Collect all tasks from different notes and create a dashboard | ||||
|     </div> | ||||
|  | ||||
|     <div class="recipe-box backend-script"> | ||||
|         <pre><code>async function aggregateTasks() { | ||||
|     // Find all notes with todos | ||||
|     const todoNotes = await api.searchForNotes('#todo OR #task OR content:"[ ]" OR content:"[x]"'); | ||||
|      | ||||
|     const tasks = { | ||||
|         pending: [], | ||||
|         completed: [], | ||||
|         overdue: [] | ||||
|     }; | ||||
|      | ||||
|     for (const note of todoNotes) { | ||||
|         const content = await note.getContent(); | ||||
|         if (!content) continue; | ||||
|          | ||||
|         // Parse checkbox tasks | ||||
|         const checkboxRegex = /\[([ x])\]\s*(.+?)(?=\n|\<|$)/gi; | ||||
|         let match; | ||||
|          | ||||
|         while ((match = checkboxRegex.exec(content)) !== null) { | ||||
|             const isCompleted = match[1] === 'x'; | ||||
|             const taskText = match[2].replace(/<[^>]*>/g, ''); // Strip HTML | ||||
|              | ||||
|             const task = { | ||||
|                 noteId: note.noteId, | ||||
|                 noteTitle: note.title, | ||||
|                 text: taskText, | ||||
|                 completed: isCompleted | ||||
|             }; | ||||
|              | ||||
|             // Check for due date | ||||
|             const dueDateLabel = await note.getLabel('dueDate'); | ||||
|             if (dueDateLabel) { | ||||
|                 task.dueDate = dueDateLabel.value; | ||||
|                 const dueDate = api.dayjs(dueDateLabel.value); | ||||
|                 if (!isCompleted && dueDate.isBefore(api.dayjs())) { | ||||
|                     tasks.overdue.push(task); | ||||
|                     continue; | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             if (isCompleted) { | ||||
|                 tasks.completed.push(task); | ||||
|             } else { | ||||
|                 tasks.pending.push(task); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Create or update task dashboard | ||||
|     let dashboard = await api.getNoteWithLabel('taskDashboard'); | ||||
|     if (!dashboard) { | ||||
|         dashboard = await api.createTextNote('root', '📋 Task Dashboard', ''); | ||||
|         await dashboard.setLabel('taskDashboard'); | ||||
|     } | ||||
|      | ||||
|     const dashboardContent = ` | ||||
|         <h1>📋 Task Dashboard</h1> | ||||
|         <p>Last updated: ${api.dayjs().format('YYYY-MM-DD HH:mm:ss')}</p> | ||||
|          | ||||
|         <h2>⚠️ Overdue (${tasks.overdue.length})</h2> | ||||
|         <ul> | ||||
|             ${tasks.overdue.map(t =>  | ||||
|                 `<li style="color: red;"> | ||||
|                     <strong>${t.text}</strong>  | ||||
|                     (Due: ${api.dayjs(t.dueDate).format('MMM D')})  | ||||
|                     - <a href="#root/${t.noteId}">${t.noteTitle}</a> | ||||
|                 </li>` | ||||
|             ).join('')} | ||||
|         </ul> | ||||
|          | ||||
|         <h2>📌 Pending (${tasks.pending.length})</h2> | ||||
|         <ul> | ||||
|             ${tasks.pending.slice(0, 20).map(t =>  | ||||
|                 `<li> | ||||
|                     ${t.text}  | ||||
|                     ${t.dueDate ? `(Due: ${api.dayjs(t.dueDate).format('MMM D')})` : ''}  | ||||
|                     - <a href="#root/${t.noteId}">${t.noteTitle}</a> | ||||
|                 </li>` | ||||
|             ).join('')} | ||||
|         </ul> | ||||
|         ${tasks.pending.length > 20 ? `<p><em>...and ${tasks.pending.length - 20} more</em></p>` : ''} | ||||
|          | ||||
|         <h2>✅ Recently Completed (${tasks.completed.length})</h2> | ||||
|         <ul> | ||||
|             ${tasks.completed.slice(0, 10).map(t =>  | ||||
|                 `<li style="text-decoration: line-through; opacity: 0.7;"> | ||||
|                     ${t.text} - <a href="#root/${t.noteId}">${t.noteTitle}</a> | ||||
|                 </li>` | ||||
|             ).join('')} | ||||
|         </ul> | ||||
|     `; | ||||
|      | ||||
|     await dashboard.setContent(dashboardContent); | ||||
|      | ||||
|     return tasks; | ||||
| } | ||||
|  | ||||
| return await aggregateTasks();</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="frontend-script-recipes">Frontend Script Recipes</h2> | ||||
|      | ||||
|     <h3>4. Quick Note Creator</h3> | ||||
|      | ||||
|     <div class="use-case"> | ||||
|         <strong>Use Case:</strong> Add a floating button to quickly create notes from anywhere in the UI | ||||
|     </div> | ||||
|  | ||||
|     <div class="recipe-box frontend-script"> | ||||
|         <pre><code>// Create floating button | ||||
| const $button = $(` | ||||
|     <div id="quick-note-btn" style=" | ||||
|         position: fixed; | ||||
|         bottom: 20px; | ||||
|         right: 20px; | ||||
|         width: 60px; | ||||
|         height: 60px; | ||||
|         background: #4CAF50; | ||||
|         border-radius: 50%; | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         justify-content: center; | ||||
|         cursor: pointer; | ||||
|         box-shadow: 0 4px 6px rgba(0,0,0,0.1); | ||||
|         z-index: 10000; | ||||
|         color: white; | ||||
|         font-size: 30px; | ||||
|     ">+</div> | ||||
| `); | ||||
|  | ||||
| // Create modal | ||||
| const $modal = $(` | ||||
|     <div id="quick-note-modal" style=" | ||||
|         display: none; | ||||
|         position: fixed; | ||||
|         top: 50%; | ||||
|         left: 50%; | ||||
|         transform: translate(-50%, -50%); | ||||
|         background: white; | ||||
|         padding: 20px; | ||||
|         border-radius: 10px; | ||||
|         box-shadow: 0 10px 25px rgba(0,0,0,0.2); | ||||
|         z-index: 10001; | ||||
|         min-width: 400px; | ||||
|     "> | ||||
|         <h3>Quick Note</h3> | ||||
|         <input type="text" id="quick-note-title" placeholder="Title..." style=" | ||||
|             width: 100%; | ||||
|             padding: 10px; | ||||
|             margin: 10px 0; | ||||
|             border: 1px solid #ddd; | ||||
|             border-radius: 5px; | ||||
|         "> | ||||
|         <textarea id="quick-note-content" placeholder="Content..." style=" | ||||
|             width: 100%; | ||||
|             height: 200px; | ||||
|             padding: 10px; | ||||
|             margin: 10px 0; | ||||
|             border: 1px solid #ddd; | ||||
|             border-radius: 5px; | ||||
|             resize: vertical; | ||||
|         "></textarea> | ||||
|         <div> | ||||
|             <button id="quick-note-save" style=" | ||||
|                 padding: 10px 20px; | ||||
|                 background: #4CAF50; | ||||
|                 color: white; | ||||
|                 border: none; | ||||
|                 border-radius: 5px; | ||||
|                 cursor: pointer; | ||||
|             ">Save</button> | ||||
|             <button id="quick-note-cancel" style=" | ||||
|                 padding: 10px 20px; | ||||
|                 background: #f44336; | ||||
|                 color: white; | ||||
|                 border: none; | ||||
|                 border-radius: 5px; | ||||
|                 cursor: pointer; | ||||
|                 margin-left: 10px; | ||||
|             ">Cancel</button> | ||||
|         </div> | ||||
|     </div> | ||||
| `); | ||||
|  | ||||
| // Add to page | ||||
| $('body').append($button, $modal); | ||||
|  | ||||
| // Handle button click | ||||
| $button.click(() => { | ||||
|     $modal.show(); | ||||
|     $('#quick-note-title').focus(); | ||||
| }); | ||||
|  | ||||
| // Handle save | ||||
| $('#quick-note-save').click(async () => { | ||||
|     const title = $('#quick-note-title').val() || 'Quick Note'; | ||||
|     const content = $('#quick-note-content').val() || ''; | ||||
|      | ||||
|     // Get current note or use inbox | ||||
|     const currentNote = api.getActiveContextNote(); | ||||
|     const parentNoteId = currentNote ? currentNote.noteId : (await api.getDayNote()).noteId; | ||||
|      | ||||
|     // Create note | ||||
|     const { note } = await api.runOnBackend(async (parentId, noteTitle, noteContent) => { | ||||
|         const parent = await api.getNote(parentId); | ||||
|         const newNote = await api.createNote(parent, noteTitle, noteContent, 'text'); | ||||
|         return { note: newNote.getPojo() }; | ||||
|     }, [parentNoteId, title, `<h2>${title}</h2><p>${content}</p>`]); | ||||
|      | ||||
|     api.showMessage(`Note "${title}" created!`); | ||||
|      | ||||
|     // Clear and close | ||||
|     $('#quick-note-title').val(''); | ||||
|     $('#quick-note-content').val(''); | ||||
|     $modal.hide(); | ||||
|      | ||||
|     // Navigate to new note | ||||
|     await api.activateNewNote(note.noteId); | ||||
| }); | ||||
|  | ||||
| // Handle cancel | ||||
| $('#quick-note-cancel').click(() => { | ||||
|     $modal.hide(); | ||||
| }); | ||||
|  | ||||
| // Keyboard shortcuts | ||||
| $(document).keydown((e) => { | ||||
|     // Ctrl+Shift+N to open quick note | ||||
|     if (e.ctrlKey && e.shiftKey && e.key === 'N') { | ||||
|         e.preventDefault(); | ||||
|         $button.click(); | ||||
|     } | ||||
|      | ||||
|     // Escape to close | ||||
|     if (e.key === 'Escape' && $modal.is(':visible')) { | ||||
|         $modal.hide(); | ||||
|     } | ||||
| });</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>5. Markdown Preview Toggle</h3> | ||||
|      | ||||
|     <div class="use-case"> | ||||
|         <strong>Use Case:</strong> Add live markdown preview for text notes | ||||
|     </div> | ||||
|  | ||||
|     <div class="recipe-box frontend-script"> | ||||
|         <pre><code>// Create preview pane | ||||
| const $previewPane = $(` | ||||
|     <div id="markdown-preview" style=" | ||||
|         display: none; | ||||
|         position: absolute; | ||||
|         top: 50px; | ||||
|         right: 10px; | ||||
|         width: 45%; | ||||
|         height: calc(100% - 60px); | ||||
|         background: white; | ||||
|         border: 1px solid #ddd; | ||||
|         border-radius: 5px; | ||||
|         padding: 20px; | ||||
|         overflow-y: auto; | ||||
|         box-shadow: 0 2px 10px rgba(0,0,0,0.1); | ||||
|         z-index: 100; | ||||
|     "> | ||||
|         <div id="preview-content"></div> | ||||
|     </div> | ||||
| `); | ||||
|  | ||||
| // Create toggle button | ||||
| const $toggleBtn = $(` | ||||
|     <button id="preview-toggle" style=" | ||||
|         position: absolute; | ||||
|         top: 10px; | ||||
|         right: 10px; | ||||
|         padding: 8px 15px; | ||||
|         background: #2196F3; | ||||
|         color: white; | ||||
|         border: none; | ||||
|         border-radius: 5px; | ||||
|         cursor: pointer; | ||||
|         z-index: 101; | ||||
|     "> | ||||
|         <i class="bx bx-show"></i> Preview | ||||
|     </button> | ||||
| `); | ||||
|  | ||||
| // Add to note detail | ||||
| $('.note-detail-text').css('position', 'relative').append($previewPane, $toggleBtn); | ||||
|  | ||||
| let previewVisible = false; | ||||
|  | ||||
| // Load markdown library (simplified conversion) | ||||
| const convertToMarkdown = (html) => { | ||||
|     return html | ||||
|         .replace(/<h1[^>]*>(.*?)<\/h1>/g, '# $1\n') | ||||
|         .replace(/<h2[^>]*>(.*?)<\/h2>/g, '## $1\n') | ||||
|         .replace(/<h3[^>]*>(.*?)<\/h3>/g, '### $1\n') | ||||
|         .replace(/<p[^>]*>(.*?)<\/p>/g, '$1\n\n') | ||||
|         .replace(/<strong[^>]*>(.*?)<\/strong>/g, '**$1**') | ||||
|         .replace(/<em[^>]*>(.*?)<\/em>/g, '*$1*') | ||||
|         .replace(/<code[^>]*>(.*?)<\/code>/g, '`$1`') | ||||
|         .replace(/<ul[^>]*>/g, '') | ||||
|         .replace(/<\/ul>/g, '\n') | ||||
|         .replace(/<li[^>]*>(.*?)<\/li>/g, '- $1\n') | ||||
|         .replace(/<br[^>]*>/g, '\n') | ||||
|         .replace(/<[^>]+>/g, ''); // Remove remaining HTML tags | ||||
| }; | ||||
|  | ||||
| // Toggle preview | ||||
| $toggleBtn.click(() => { | ||||
|     previewVisible = !previewVisible; | ||||
|      | ||||
|     if (previewVisible) { | ||||
|         $previewPane.show(); | ||||
|         $('.note-detail-text .note-detail-editable').css('width', '50%'); | ||||
|         $toggleBtn.html('<i class="bx bx-hide"></i> Hide'); | ||||
|         updatePreview(); | ||||
|     } else { | ||||
|         $previewPane.hide(); | ||||
|         $('.note-detail-text .note-detail-editable').css('width', '100%'); | ||||
|         $toggleBtn.html('<i class="bx bx-show"></i> Preview'); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| // Update preview function | ||||
| async function updatePreview() { | ||||
|     if (!previewVisible) return; | ||||
|      | ||||
|     const content = await api.getActiveContextTextEditor().getContent(); | ||||
|     const markdown = convertToMarkdown(content); | ||||
|      | ||||
|     // Simple markdown to HTML conversion | ||||
|     const html = markdown | ||||
|         .replace(/^# (.+)$/gm, '<h1>$1</h1>') | ||||
|         .replace(/^## (.+)$/gm, '<h2>$1</h2>') | ||||
|         .replace(/^### (.+)$/gm, '<h3>$1</h3>') | ||||
|         .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') | ||||
|         .replace(/\*(.*?)\*/g, '<em>$1</em>') | ||||
|         .replace(/`(.*?)`/g, '<code>$1</code>') | ||||
|         .replace(/^- (.+)$/gm, '<li>$1</li>') | ||||
|         .replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>') | ||||
|         .replace(/\n\n/g, '</p><p>') | ||||
|         .replace(/^(?!<[h|u])(.+)$/gm, '<p>$1</p>'); | ||||
|      | ||||
|     $('#preview-content').html(html); | ||||
| }</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="integration-with-external-services">Integration with External Services</h2> | ||||
|      | ||||
|     <h3>6. GitHub Integration</h3> | ||||
|      | ||||
|     <div class="use-case"> | ||||
|         <strong>Use Case:</strong> Sync GitHub issues with Trilium notes | ||||
|     </div> | ||||
|  | ||||
|     <div class="recipe-box backend-script"> | ||||
|         <pre><code>// Requires axios library | ||||
| const axios = require('axios'); | ||||
|  | ||||
| class GitHubSync { | ||||
|     constructor(token, repo) { | ||||
|         this.token = token; | ||||
|         this.repo = repo; // format: "owner/repo" | ||||
|         this.apiBase = 'https://api.github.com'; | ||||
|     } | ||||
|      | ||||
|     async getIssues(state = 'open') { | ||||
|         const response = await axios.get(`${this.apiBase}/repos/${this.repo}/issues`, { | ||||
|             headers: { | ||||
|                 'Authorization': `token ${this.token}`, | ||||
|                 'Accept': 'application/vnd.github.v3+json' | ||||
|             }, | ||||
|             params: { state } | ||||
|         }); | ||||
|          | ||||
|         return response.data; | ||||
|     } | ||||
|      | ||||
|     async syncIssuesToNotes() { | ||||
|         // Get or create GitHub folder | ||||
|         let githubFolder = await api.getNoteWithLabel('githubSync'); | ||||
|         if (!githubFolder) { | ||||
|             githubFolder = await api.createTextNote('root', 'GitHub Issues', ''); | ||||
|             await githubFolder.setLabel('githubSync'); | ||||
|         } | ||||
|          | ||||
|         const issues = await this.getIssues(); | ||||
|         const syncedNotes = []; | ||||
|          | ||||
|         for (const issue of issues) { | ||||
|             // Check if issue note already exists | ||||
|             let issueNote = await api.getNoteWithLabel(`github:issue:${issue.number}`); | ||||
|              | ||||
|             const content = ` | ||||
|                 <h1>${issue.title}</h1> | ||||
|                  | ||||
|                 <table> | ||||
|                     <tr><th>Issue #</th><td>${issue.number}</td></tr> | ||||
|                     <tr><th>State</th><td>${issue.state}</td></tr> | ||||
|                     <tr><th>Author</th><td>${issue.user.login}</td></tr> | ||||
|                     <tr><th>Created</th><td>${api.dayjs(issue.created_at).format('YYYY-MM-DD HH:mm')}</td></tr> | ||||
|                     <tr><th>Labels</th><td>${issue.labels.map(l => l.name).join(', ')}</td></tr> | ||||
|                 </table> | ||||
|                  | ||||
|                 <h2>Description</h2> | ||||
|                 <div style="background: #f5f5f5; padding: 10px; border-radius: 5px;"> | ||||
|                     ${issue.body || 'No description'} | ||||
|                 </div> | ||||
|                  | ||||
|                 <h2>Links</h2> | ||||
|                 <ul> | ||||
|                     <li><a href="${issue.html_url}">View on GitHub</a></li> | ||||
|                     <li><a href="${issue.url}">API URL</a></li> | ||||
|                 </ul> | ||||
|             `; | ||||
|              | ||||
|             if (!issueNote) { | ||||
|                 // Create new note | ||||
|                 issueNote = await api.createNote( | ||||
|                     githubFolder, | ||||
|                     `#${issue.number}: ${issue.title}`, | ||||
|                     content | ||||
|                 ); | ||||
|                 await issueNote.setLabel(`github:issue:${issue.number}`); | ||||
|             } else { | ||||
|                 // Update existing note | ||||
|                 await issueNote.setContent(content); | ||||
|             } | ||||
|              | ||||
|             // Set labels based on issue state and labels | ||||
|             await issueNote.setLabel('githubIssue'); | ||||
|             await issueNote.setLabel('state', issue.state); | ||||
|              | ||||
|             for (const label of issue.labels) { | ||||
|                 await issueNote.setLabel(`gh:${label.name}`); | ||||
|             } | ||||
|              | ||||
|             syncedNotes.push({ | ||||
|                 noteId: issueNote.noteId, | ||||
|                 issueNumber: issue.number, | ||||
|                 title: issue.title | ||||
|             }); | ||||
|         } | ||||
|          | ||||
|         api.log(`Synced ${syncedNotes.length} GitHub issues`); | ||||
|         return syncedNotes; | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Usage | ||||
| const github = new GitHubSync( | ||||
|     process.env.GITHUB_TOKEN || 'your-token', | ||||
|     'your-org/your-repo' | ||||
| ); | ||||
|  | ||||
| // Sync issues to notes | ||||
| const synced = await github.syncIssuesToNotes(); | ||||
| return synced;</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h2 id="best-practices">Best Practices</h2> | ||||
|      | ||||
|     <h3>Error Handling</h3> | ||||
|      | ||||
|     <div class="success"> | ||||
|         <p>Always wrap scripts in try-catch blocks:</p> | ||||
|         <pre><code>async function safeScriptExecution() { | ||||
|     try { | ||||
|         // Your script code here | ||||
|         const result = await riskyOperation(); | ||||
|          | ||||
|         return { | ||||
|             success: true, | ||||
|             data: result | ||||
|         }; | ||||
|     } catch (error) { | ||||
|         api.log(`Error in script: ${error.message}`, 'error'); | ||||
|          | ||||
|         // Create error report note | ||||
|         const errorNote = await api.createTextNote( | ||||
|             'root', | ||||
|             `Script Error - ${api.dayjs().format('YYYY-MM-DD HH:mm:ss')}`, | ||||
|             ` | ||||
|                 <h1>Script Error</h1> | ||||
|                 <p><strong>Error:</strong> ${error.message}</p> | ||||
|                 <p><strong>Stack:</strong></p> | ||||
|                 <pre>${error.stack}</pre> | ||||
|                 <p><strong>Script:</strong> ${api.currentNote.title}</p> | ||||
|             ` | ||||
|         ); | ||||
|          | ||||
|         await errorNote.setLabel('scriptError'); | ||||
|          | ||||
|         return { | ||||
|             success: false, | ||||
|             error: error.message | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
| return await safeScriptExecution();</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <h3>Performance Optimization</h3> | ||||
|      | ||||
|     <div class="success"> | ||||
|         <p>Use batch operations and caching:</p> | ||||
|         <pre><code>class OptimizedNoteProcessor { | ||||
|     constructor() { | ||||
|         this.cache = new Map(); | ||||
|     } | ||||
|      | ||||
|     async processNotes(noteIds) { | ||||
|         // Batch fetch notes | ||||
|         const notes = await Promise.all( | ||||
|             noteIds.map(id => this.getCachedNote(id)) | ||||
|         ); | ||||
|          | ||||
|         // Process in chunks to avoid memory issues | ||||
|         const chunkSize = 100; | ||||
|         const results = []; | ||||
|          | ||||
|         for (let i = 0; i < notes.length; i += chunkSize) { | ||||
|             const chunk = notes.slice(i, i + chunkSize); | ||||
|             const chunkResults = await Promise.all( | ||||
|                 chunk.map(note => this.processNote(note)) | ||||
|             ); | ||||
|             results.push(...chunkResults); | ||||
|              | ||||
|             // Allow other operations | ||||
|             await new Promise(resolve => setTimeout(resolve, 10)); | ||||
|         } | ||||
|          | ||||
|         return results; | ||||
|     } | ||||
|      | ||||
|     async getCachedNote(noteId) { | ||||
|         if (!this.cache.has(noteId)) { | ||||
|             const note = await api.getNote(noteId); | ||||
|             this.cache.set(noteId, note); | ||||
|         } | ||||
|         return this.cache.get(noteId); | ||||
|     } | ||||
| }</code></pre> | ||||
|     </div> | ||||
|  | ||||
|     <div class="warning"> | ||||
|         <h4>Key Takeaways</h4> | ||||
|         <ol> | ||||
|             <li><strong>Use Backend Scripts</strong> for data processing, automation, and integrations</li> | ||||
|             <li><strong>Use Frontend Scripts</strong> for UI enhancements and user interactions</li> | ||||
|             <li><strong>Always handle errors</strong> gracefully and provide meaningful feedback</li> | ||||
|             <li><strong>Optimize performance</strong> with caching and batch operations</li> | ||||
|             <li><strong>Organize complex scripts</strong> into modules for reusability</li> | ||||
|             <li><strong>Test your scripts</strong> to ensure reliability</li> | ||||
|         </ol> | ||||
|     </div> | ||||
|  | ||||
|     <div class="info"> | ||||
|         <h4>Related Documentation</h4> | ||||
|         <ul> | ||||
|             <li><a href="https://triliumnext.github.io/Docs/api/Backend_Script_API.html">Backend Script API Reference</a></li> | ||||
|             <li><a href="https://triliumnext.github.io/Docs/api/Frontend_Script_API.html">Frontend Script API Reference</a></li> | ||||
|             <li><a href="Custom%20Widget%20Development.html">Custom Widget Development</a></li> | ||||
|         </ul> | ||||
|     </div> | ||||
|  | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										1780
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/WebSocket API.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1780
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/WebSocket API.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										122
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Architecture.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Architecture.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|     <title>Trilium Architecture Documentation</title> | ||||
|     <style> | ||||
|         body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } | ||||
|         h1 { color: #2c3e50; } | ||||
|         h2 { color: #34495e; margin-top: 30px; } | ||||
|         ul { line-height: 1.8; } | ||||
|         a { color: #3498db; text-decoration: none; } | ||||
|         a:hover { text-decoration: underline; } | ||||
|         .overview { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; } | ||||
|         .quick-start { background: #e8f5e9; padding: 20px; border-radius: 8px; margin: 20px 0; } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|     <h1>Trilium Architecture Documentation</h1> | ||||
|      | ||||
|     <p>This comprehensive guide documents the architecture of Trilium Notes, providing developers with detailed information about the system's core components, data flow, and design patterns.</p> | ||||
|      | ||||
|     <h2>Table of Contents</h2> | ||||
|     <ol> | ||||
|         <li><a href="Three-Layer-Cache-System.html">Three-Layer Cache System</a></li> | ||||
|         <li><a href="Entity-System.html">Entity System</a></li> | ||||
|         <li><a href="Widget-Based-UI-Architecture.html">Widget-Based UI Architecture</a></li> | ||||
|         <li><a href="API-Architecture.html">API Architecture</a></li> | ||||
|         <li><a href="Monorepo-Structure.html">Monorepo Structure</a></li> | ||||
|     </ol> | ||||
|      | ||||
|     <div class="overview"> | ||||
|         <h2>Overview</h2> | ||||
|         <p>Trilium Notes is built as a TypeScript monorepo using NX, featuring a sophisticated architecture that balances performance, flexibility, and maintainability. The system is designed around several key architectural patterns:</p> | ||||
|          | ||||
|         <ul> | ||||
|             <li><strong>Three-layer caching system</strong> for optimal performance across backend, frontend, and shared content</li> | ||||
|             <li><strong>Entity-based data model</strong> supporting hierarchical note structures with multiple parent relationships</li> | ||||
|             <li><strong>Widget-based UI architecture</strong> enabling modular and extensible interface components</li> | ||||
|             <li><strong>Multiple API layers</strong> for internal operations, external integrations, and real-time synchronization</li> | ||||
|             <li><strong>Monorepo structure</strong> facilitating code sharing and consistent development patterns</li> | ||||
|         </ul> | ||||
|     </div> | ||||
|      | ||||
|     <div class="quick-start"> | ||||
|         <h2>Quick Start for Developers</h2> | ||||
|          | ||||
|         <p>If you're new to Trilium development, start with these sections:</p> | ||||
|          | ||||
|         <ol> | ||||
|             <li><a href="Monorepo-Structure.html">Monorepo Structure</a> - Understand the project organization</li> | ||||
|             <li><a href="Entity-System.html">Entity System</a> - Learn about the core data model</li> | ||||
|             <li><a href="Three-Layer-Cache-System.html">Three-Layer Cache System</a> - Understand data flow and caching</li> | ||||
|         </ol> | ||||
|          | ||||
|         <p>For UI development, refer to:</p> | ||||
|         <ul> | ||||
|             <li><a href="Widget-Based-UI-Architecture.html">Widget-Based UI Architecture</a></li> | ||||
|         </ul> | ||||
|          | ||||
|         <p>For API integration, see:</p> | ||||
|         <ul> | ||||
|             <li><a href="API-Architecture.html">API Architecture</a></li> | ||||
|         </ul> | ||||
|     </div> | ||||
|      | ||||
|     <h2>Architecture Principles</h2> | ||||
|      | ||||
|     <h3>Performance First</h3> | ||||
|     <ul> | ||||
|         <li>Lazy loading of note content</li> | ||||
|         <li>Efficient caching at multiple layers</li> | ||||
|         <li>Optimized database queries with prepared statements</li> | ||||
|     </ul> | ||||
|      | ||||
|     <h3>Flexibility</h3> | ||||
|     <ul> | ||||
|         <li>Support for multiple note types</li> | ||||
|         <li>Extensible through scripting</li> | ||||
|         <li>Plugin architecture for UI widgets</li> | ||||
|     </ul> | ||||
|      | ||||
|     <h3>Data Integrity</h3> | ||||
|     <ul> | ||||
|         <li>Transactional database operations</li> | ||||
|         <li>Revision history for all changes</li> | ||||
|         <li>Synchronization conflict resolution</li> | ||||
|     </ul> | ||||
|      | ||||
|     <h3>Security</h3> | ||||
|     <ul> | ||||
|         <li>Per-note encryption</li> | ||||
|         <li>Protected sessions</li> | ||||
|         <li>API authentication tokens</li> | ||||
|     </ul> | ||||
|      | ||||
|     <h2>Development Workflow</h2> | ||||
|      | ||||
|     <ol> | ||||
|         <li><strong>Setup Development Environment</strong> | ||||
|             <pre><code>pnpm install | ||||
| pnpm run server:start</code></pre> | ||||
|         </li> | ||||
|          | ||||
|         <li><strong>Make Changes</strong> | ||||
|             <ul> | ||||
|                 <li>Backend changes in <code>apps/server/src/</code></li> | ||||
|                 <li>Frontend changes in <code>apps/client/src/</code></li> | ||||
|                 <li>Shared code in <code>packages/</code></li> | ||||
|             </ul> | ||||
|         </li> | ||||
|          | ||||
|         <li><strong>Test Your Changes</strong> | ||||
|             <pre><code>pnpm test:all | ||||
| pnpm nx run <project>:lint</code></pre> | ||||
|         </li> | ||||
|          | ||||
|         <li><strong>Build for Production</strong> | ||||
|             <pre><code>pnpm nx build server | ||||
| pnpm nx build client</code></pre> | ||||
|         </li> | ||||
|     </ol> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										367
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/API-Architecture.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										367
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/API-Architecture.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,367 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|     <title>API Architecture</title> | ||||
|     <style> | ||||
|         body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; } | ||||
|         h1 { color: #2c3e50; } | ||||
|         h2 { color: #34495e; margin-top: 30px; } | ||||
|         h3 { color: #7f8c8d; } | ||||
|         pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; } | ||||
|         code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-family: 'Courier New', monospace; } | ||||
|         .api-layer { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; } | ||||
|         .internal-api { border-left: 4px solid #3498db; } | ||||
|         .etapi { border-left: 4px solid #e67e22; } | ||||
|         .websocket { border-left: 4px solid #9b59b6; } | ||||
|         table { border-collapse: collapse; width: 100%; margin: 20px 0; } | ||||
|         th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } | ||||
|         th { background: #f8f9fa; } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|     <h1>API Architecture</h1> | ||||
|      | ||||
|     <p>Trilium provides multiple API layers for different use cases: Internal API for frontend-backend communication, ETAPI for external integrations, and WebSocket for real-time synchronization.</p> | ||||
|      | ||||
|     <h2>API Layers Overview</h2> | ||||
|      | ||||
|     <table> | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <th>API Layer</th> | ||||
|                 <th>Purpose</th> | ||||
|                 <th>Authentication</th> | ||||
|                 <th>Primary Users</th> | ||||
|             </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|             <tr> | ||||
|                 <td>Internal API</td> | ||||
|                 <td>Frontend-backend communication</td> | ||||
|                 <td>Session-based</td> | ||||
|                 <td>Web/Desktop clients</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td>ETAPI</td> | ||||
|                 <td>External integrations</td> | ||||
|                 <td>Token-based</td> | ||||
|                 <td>Third-party apps, scripts</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td>WebSocket</td> | ||||
|                 <td>Real-time sync</td> | ||||
|                 <td>Session/Token</td> | ||||
|                 <td>All clients</td> | ||||
|             </tr> | ||||
|         </tbody> | ||||
|     </table> | ||||
|      | ||||
|     <div class="api-layer internal-api"> | ||||
|         <h2>Internal API</h2> | ||||
|         <p><strong>Location:</strong> <code>/apps/server/src/routes/api/</code></p> | ||||
|          | ||||
|         <p>The Internal API handles communication between Trilium's frontend and backend, providing full access to application functionality.</p> | ||||
|          | ||||
|         <h3>Key Endpoints</h3> | ||||
|          | ||||
|         <h4>Note Operations</h4> | ||||
|         <pre><code>// Get note with content | ||||
| GET /api/notes/:noteId | ||||
|  | ||||
| // Update note | ||||
| PUT /api/notes/:noteId | ||||
| Body: { | ||||
|     title?: string, | ||||
|     content?: string, | ||||
|     type?: string, | ||||
|     mime?: string | ||||
| } | ||||
|  | ||||
| // Create note | ||||
| POST /api/notes/:parentNoteId/children | ||||
| Body: { | ||||
|     title: string, | ||||
|     type: string, | ||||
|     content?: string, | ||||
|     position?: number | ||||
| } | ||||
|  | ||||
| // Delete note | ||||
| DELETE /api/notes/:noteId</code></pre> | ||||
|          | ||||
|         <h4>Tree Operations</h4> | ||||
|         <pre><code>// Get tree structure | ||||
| GET /api/tree | ||||
| Query: { | ||||
|     subTreeNoteId?: string, | ||||
|     includeAttributes?: boolean | ||||
| } | ||||
|  | ||||
| // Move branch | ||||
| PUT /api/branches/:branchId/move | ||||
| Body: { | ||||
|     parentNoteId: string, | ||||
|     position: number | ||||
| }</code></pre> | ||||
|          | ||||
|         <h4>Search Operations</h4> | ||||
|         <pre><code>// Execute search | ||||
| GET /api/search | ||||
| Query: { | ||||
|     query: string, | ||||
|     fastSearch?: boolean, | ||||
|     includeArchivedNotes?: boolean | ||||
| }</code></pre> | ||||
|     </div> | ||||
|      | ||||
|     <div class="api-layer etapi"> | ||||
|         <h2>ETAPI (External API)</h2> | ||||
|         <p><strong>Location:</strong> <code>/apps/server/src/etapi/</code></p> | ||||
|          | ||||
|         <p>ETAPI provides a stable, versioned API for external applications and scripts to interact with Trilium.</p> | ||||
|          | ||||
|         <h3>Authentication</h3> | ||||
|         <pre><code>// Creating ETAPI token | ||||
| POST /etapi/auth/login | ||||
| Body: { | ||||
|     username: string, | ||||
|     password: string | ||||
| } | ||||
| Response: { | ||||
|     authToken: string | ||||
| } | ||||
|  | ||||
| // Using token in requests | ||||
| GET /etapi/notes/:noteId | ||||
| Headers: { | ||||
|     Authorization: "authToken" | ||||
| }</code></pre> | ||||
|          | ||||
|         <h3>Key Endpoints</h3> | ||||
|          | ||||
|         <h4>Note CRUD Operations</h4> | ||||
|         <pre><code>// Create note | ||||
| POST /etapi/notes | ||||
| Body: { | ||||
|     parentNoteId: string, | ||||
|     title: string, | ||||
|     type: string, | ||||
|     content?: string | ||||
| } | ||||
|  | ||||
| // Get note | ||||
| GET /etapi/notes/:noteId | ||||
|  | ||||
| // Update note content | ||||
| PUT /etapi/notes/:noteId/content | ||||
| Body: string | Buffer | ||||
|  | ||||
| // Delete note | ||||
| DELETE /etapi/notes/:noteId</code></pre> | ||||
|          | ||||
|         <h4>Search</h4> | ||||
|         <pre><code>// Search notes | ||||
| GET /etapi/notes/search | ||||
| Query: { | ||||
|     search: string, | ||||
|     limit?: number, | ||||
|     orderBy?: string | ||||
| }</code></pre> | ||||
|          | ||||
|         <h3>Client Example (JavaScript)</h3> | ||||
|         <pre><code>class EtapiClient { | ||||
|     constructor(serverUrl, authToken) { | ||||
|         this.serverUrl = serverUrl; | ||||
|         this.authToken = authToken; | ||||
|     } | ||||
|      | ||||
|     async getNote(noteId) { | ||||
|         const response = await fetch( | ||||
|             `${this.serverUrl}/etapi/notes/${noteId}`, | ||||
|             { | ||||
|                 headers: { | ||||
|                     'Authorization': this.authToken | ||||
|                 } | ||||
|             } | ||||
|         ); | ||||
|         return response.json(); | ||||
|     } | ||||
|      | ||||
|     async createNote(parentNoteId, title, content) { | ||||
|         const response = await fetch( | ||||
|             `${this.serverUrl}/etapi/notes`, | ||||
|             { | ||||
|                 method: 'POST', | ||||
|                 headers: { | ||||
|                     'Authorization': this.authToken, | ||||
|                     'Content-Type': 'application/json' | ||||
|                 }, | ||||
|                 body: JSON.stringify({ | ||||
|                     parentNoteId, | ||||
|                     title, | ||||
|                     type: 'text', | ||||
|                     content | ||||
|                 }) | ||||
|             } | ||||
|         ); | ||||
|         return response.json(); | ||||
|     } | ||||
| }</code></pre> | ||||
|     </div> | ||||
|      | ||||
|     <div class="api-layer websocket"> | ||||
|         <h2>WebSocket Real-time Synchronization</h2> | ||||
|         <p><strong>Location:</strong> <code>/apps/server/src/services/ws.ts</code></p> | ||||
|          | ||||
|         <p>WebSocket connections provide real-time updates and synchronization between clients.</p> | ||||
|          | ||||
|         <h3>Message Types</h3> | ||||
|         <ul> | ||||
|             <li><code>entity-changes</code> - Entity updates</li> | ||||
|             <li><code>sync</code> - Sync events</li> | ||||
|             <li><code>note-content-change</code> - Content updates</li> | ||||
|             <li><code>refresh-tree</code> - Tree structure changes</li> | ||||
|             <li><code>options-changed</code> - Configuration updates</li> | ||||
|         </ul> | ||||
|          | ||||
|         <h3>Connection Example</h3> | ||||
|         <pre><code>// Client connection | ||||
| const ws = new WebSocket('wss://server/ws'); | ||||
|  | ||||
| ws.on('open', () => { | ||||
|     // Authenticate | ||||
|     ws.send(JSON.stringify({ | ||||
|         type: 'auth', | ||||
|         token: sessionToken | ||||
|     })); | ||||
| }); | ||||
|  | ||||
| ws.on('message', (data) => { | ||||
|     const message = JSON.parse(data); | ||||
|     handleWSMessage(message); | ||||
| }); | ||||
|  | ||||
| // Handle messages | ||||
| function handleWSMessage(message) { | ||||
|     switch (message.type) { | ||||
|         case 'entity-changes': | ||||
|             handleEntityChanges(message.data); | ||||
|             break; | ||||
|         case 'refresh-tree': | ||||
|             froca.loadInitialTree(); | ||||
|             break; | ||||
|         case 'note-content-change': | ||||
|             handleContentChange(message.data); | ||||
|             break; | ||||
|     } | ||||
| }</code></pre> | ||||
|     </div> | ||||
|      | ||||
|     <h2>API Security</h2> | ||||
|      | ||||
|     <h3>Authentication Methods</h3> | ||||
|     <table> | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <th>Method</th> | ||||
|                 <th>API</th> | ||||
|                 <th>Description</th> | ||||
|             </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|             <tr> | ||||
|                 <td>Session-based</td> | ||||
|                 <td>Internal API</td> | ||||
|                 <td>Cookie-based sessions for web clients</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td>Token-based</td> | ||||
|                 <td>ETAPI</td> | ||||
|                 <td>Bearer tokens for external apps</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td>WebSocket auth</td> | ||||
|                 <td>WebSocket</td> | ||||
|                 <td>Initial auth message after connection</td> | ||||
|             </tr> | ||||
|         </tbody> | ||||
|     </table> | ||||
|      | ||||
|     <h3>Rate Limiting</h3> | ||||
|     <pre><code>// Global rate limit | ||||
| const globalLimiter = rateLimit({ | ||||
|     windowMs: 15 * 60 * 1000, // 15 minutes | ||||
|     max: 1000 // limit each IP to 1000 requests | ||||
| }); | ||||
|  | ||||
| // Strict limit for authentication | ||||
| const authLimiter = rateLimit({ | ||||
|     windowMs: 15 * 60 * 1000, | ||||
|     max: 5, | ||||
|     message: 'Too many authentication attempts' | ||||
| });</code></pre> | ||||
|      | ||||
|     <h2>Performance Optimization</h2> | ||||
|      | ||||
|     <h3>Batch Operations</h3> | ||||
|     <pre><code>// Batch API endpoint | ||||
| router.post('/api/batch', async (req, res) => { | ||||
|     const operations = req.body.operations; | ||||
|     const results = []; | ||||
|      | ||||
|     await sql.transactional(async () => { | ||||
|         for (const op of operations) { | ||||
|             const result = await executeOperation(op); | ||||
|             results.push(result); | ||||
|         } | ||||
|     }); | ||||
|      | ||||
|     res.json({ results }); | ||||
| });</code></pre> | ||||
|      | ||||
|     <h3>Streaming Responses</h3> | ||||
|     <pre><code>// Stream large data | ||||
| router.get('/api/export', (req, res) => { | ||||
|     res.writeHead(200, { | ||||
|         'Content-Type': 'application/x-ndjson', | ||||
|         'Transfer-Encoding': 'chunked' | ||||
|     }); | ||||
|      | ||||
|     const noteStream = createNoteExportStream(); | ||||
|      | ||||
|     noteStream.on('data', (note) => { | ||||
|         res.write(JSON.stringify(note) + '\n'); | ||||
|     }); | ||||
|      | ||||
|     noteStream.on('end', () => { | ||||
|         res.end(); | ||||
|     }); | ||||
| });</code></pre> | ||||
|      | ||||
|     <h2>Best Practices</h2> | ||||
|      | ||||
|     <h3>API Design</h3> | ||||
|     <ol> | ||||
|         <li><strong>RESTful conventions:</strong> Use appropriate HTTP methods and status codes</li> | ||||
|         <li><strong>Consistent naming:</strong> Use camelCase for JSON properties</li> | ||||
|         <li><strong>Versioning:</strong> Version the API to maintain compatibility</li> | ||||
|         <li><strong>Documentation:</strong> Keep OpenAPI spec up to date</li> | ||||
|     </ol> | ||||
|      | ||||
|     <h3>Security</h3> | ||||
|     <ol> | ||||
|         <li><strong>Authentication:</strong> Always verify user identity</li> | ||||
|         <li><strong>Authorization:</strong> Check permissions for each operation</li> | ||||
|         <li><strong>Validation:</strong> Validate all input data</li> | ||||
|         <li><strong>Rate limiting:</strong> Prevent abuse with appropriate limits</li> | ||||
|     </ol> | ||||
|      | ||||
|     <h3>Performance</h3> | ||||
|     <ol> | ||||
|         <li><strong>Pagination:</strong> Limit response sizes with pagination</li> | ||||
|         <li><strong>Caching:</strong> Cache frequently accessed data</li> | ||||
|         <li><strong>Batch operations:</strong> Support bulk operations</li> | ||||
|         <li><strong>Async processing:</strong> Use queues for long-running tasks</li> | ||||
|     </ol> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										268
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Entity-System.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Entity-System.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,268 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|     <title>Entity System Architecture</title> | ||||
|     <style> | ||||
|         body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; } | ||||
|         h1 { color: #2c3e50; } | ||||
|         h2 { color: #34495e; margin-top: 30px; } | ||||
|         h3 { color: #7f8c8d; } | ||||
|         pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; } | ||||
|         code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-family: 'Courier New', monospace; } | ||||
|         .entity-box { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #3498db; } | ||||
|         table { border-collapse: collapse; width: 100%; margin: 20px 0; } | ||||
|         th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } | ||||
|         th { background: #f8f9fa; } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|     <h1>Entity System Architecture</h1> | ||||
|      | ||||
|     <p>The Entity System forms the core data model of Trilium Notes, providing a flexible and powerful structure for organizing information. This document details the entities, their relationships, and usage patterns.</p> | ||||
|      | ||||
|     <h2>Core Entities Overview</h2> | ||||
|      | ||||
|     <div class="entity-box"> | ||||
|         <h3>BNote - Notes with Content and Metadata</h3> | ||||
|         <p><strong>Location:</strong> <code>/apps/server/src/becca/entities/bnote.ts</code></p> | ||||
|          | ||||
|         <p>Notes are the fundamental unit of information in Trilium. Each note can contain different types of content and maintain relationships with other notes.</p> | ||||
|          | ||||
|         <h4>Properties</h4> | ||||
|         <ul> | ||||
|             <li><code>noteId</code> - Unique identifier</li> | ||||
|             <li><code>title</code> - Display title</li> | ||||
|             <li><code>type</code> - Content type (text, code, file, etc.)</li> | ||||
|             <li><code>mime</code> - MIME type for content</li> | ||||
|             <li><code>isProtected</code> - Encryption flag</li> | ||||
|             <li><code>dateCreated</code> - Creation timestamp</li> | ||||
|             <li><code>dateModified</code> - Last modification</li> | ||||
|         </ul> | ||||
|          | ||||
|         <h4>Note Types</h4> | ||||
|         <table> | ||||
|             <thead> | ||||
|                 <tr> | ||||
|                     <th>Type</th> | ||||
|                     <th>Description</th> | ||||
|                     <th>Use Case</th> | ||||
|                 </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|                 <tr> | ||||
|                     <td><code>text</code></td> | ||||
|                     <td>Rich text with HTML formatting</td> | ||||
|                     <td>General notes, documentation</td> | ||||
|                 </tr> | ||||
|                 <tr> | ||||
|                     <td><code>code</code></td> | ||||
|                     <td>Source code with syntax highlighting</td> | ||||
|                     <td>Code snippets, scripts</td> | ||||
|                 </tr> | ||||
|                 <tr> | ||||
|                     <td><code>file</code></td> | ||||
|                     <td>Binary file attachment</td> | ||||
|                     <td>PDFs, documents, archives</td> | ||||
|                 </tr> | ||||
|                 <tr> | ||||
|                     <td><code>image</code></td> | ||||
|                     <td>Image with preview</td> | ||||
|                     <td>Pictures, diagrams</td> | ||||
|                 </tr> | ||||
|                 <tr> | ||||
|                     <td><code>search</code></td> | ||||
|                     <td>Saved search query</td> | ||||
|                     <td>Dynamic note collections</td> | ||||
|                 </tr> | ||||
|                 <tr> | ||||
|                     <td><code>canvas</code></td> | ||||
|                     <td>Drawing canvas (Excalidraw)</td> | ||||
|                     <td>Diagrams, sketches</td> | ||||
|                 </tr> | ||||
|                 <tr> | ||||
|                     <td><code>mermaid</code></td> | ||||
|                     <td>Mermaid diagram</td> | ||||
|                     <td>Flowcharts, graphs</td> | ||||
|                 </tr> | ||||
|             </tbody> | ||||
|         </table> | ||||
|     </div> | ||||
|      | ||||
|     <div class="entity-box"> | ||||
|         <h3>BBranch - Hierarchical Relationships</h3> | ||||
|         <p><strong>Location:</strong> <code>/apps/server/src/becca/entities/bbranch.ts</code></p> | ||||
|          | ||||
|         <p>Branches define the parent-child relationships between notes, allowing a note to have multiple parents (cloning).</p> | ||||
|          | ||||
|         <h4>Key Features</h4> | ||||
|         <ul> | ||||
|             <li><strong>Multiple Parents:</strong> Notes can appear in multiple locations</li> | ||||
|             <li><strong>Ordering:</strong> Explicit positioning among siblings</li> | ||||
|             <li><strong>Prefixes:</strong> Optional labels for context (e.g., "Chapter 1:")</li> | ||||
|             <li><strong>UI State:</strong> Expansion state persisted per branch</li> | ||||
|         </ul> | ||||
|          | ||||
|         <h4>Usage Example</h4> | ||||
|         <pre><code>// Create parent-child relationship | ||||
| const branch = new BBranch({ | ||||
|     noteId: childNote.noteId, | ||||
|     parentNoteId: parentNote.noteId, | ||||
|     notePosition: 10 | ||||
| }); | ||||
| branch.save(); | ||||
|  | ||||
| // Clone note to another parent | ||||
| const cloneBranch = childNote.cloneTo(otherParent.noteId);</code></pre> | ||||
|     </div> | ||||
|      | ||||
|     <div class="entity-box"> | ||||
|         <h3>BAttribute - Key-Value Metadata</h3> | ||||
|         <p><strong>Location:</strong> <code>/apps/server/src/becca/entities/battribute.ts</code></p> | ||||
|          | ||||
|         <p>Attributes provide flexible metadata and relationships between notes.</p> | ||||
|          | ||||
|         <h4>Types</h4> | ||||
|         <ol> | ||||
|             <li><strong>Labels:</strong> Key-value pairs for metadata</li> | ||||
|             <li><strong>Relations:</strong> References to other notes</li> | ||||
|         </ol> | ||||
|          | ||||
|         <h4>Common Patterns</h4> | ||||
|         <pre><code>// Add label | ||||
| note.addLabel("status", "active"); | ||||
| note.addLabel("priority", "high"); | ||||
|  | ||||
| // Add relation | ||||
| note.addRelation("template", templateNoteId); | ||||
| note.addRelation("renderNote", renderNoteId); | ||||
|  | ||||
| // Query by attributes | ||||
| const todos = becca.findAttributes("label", "todoItem");</code></pre> | ||||
|          | ||||
|         <h4>System Attributes</h4> | ||||
|         <table> | ||||
|             <thead> | ||||
|                 <tr> | ||||
|                     <th>Attribute</th> | ||||
|                     <th>Type</th> | ||||
|                     <th>Purpose</th> | ||||
|                 </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|                 <tr> | ||||
|                     <td><code>#hidePromotedAttributes</code></td> | ||||
|                     <td>Label</td> | ||||
|                     <td>Hide promoted attributes in UI</td> | ||||
|                 </tr> | ||||
|                 <tr> | ||||
|                     <td><code>#readOnly</code></td> | ||||
|                     <td>Label</td> | ||||
|                     <td>Prevent note editing</td> | ||||
|                 </tr> | ||||
|                 <tr> | ||||
|                     <td><code>#workspace</code></td> | ||||
|                     <td>Label</td> | ||||
|                     <td>Workspace organization</td> | ||||
|                 </tr> | ||||
|                 <tr> | ||||
|                     <td><code>~template</code></td> | ||||
|                     <td>Relation</td> | ||||
|                     <td>Note template reference</td> | ||||
|                 </tr> | ||||
|                 <tr> | ||||
|                     <td><code>~renderNote</code></td> | ||||
|                     <td>Relation</td> | ||||
|                     <td>Custom rendering</td> | ||||
|                 </tr> | ||||
|             </tbody> | ||||
|         </table> | ||||
|     </div> | ||||
|      | ||||
|     <div class="entity-box"> | ||||
|         <h3>BRevision - Version History</h3> | ||||
|         <p><strong>Location:</strong> <code>/apps/server/src/becca/entities/brevision.ts</code></p> | ||||
|          | ||||
|         <p>Revisions provide version history and recovery capabilities.</p> | ||||
|          | ||||
|         <h4>Revision Strategy</h4> | ||||
|         <ul> | ||||
|             <li>Created automatically on significant changes</li> | ||||
|             <li>Configurable retention period</li> | ||||
|             <li>Day/week/month/year retention rules</li> | ||||
|             <li>Protected note revisions are encrypted</li> | ||||
|         </ul> | ||||
|     </div> | ||||
|      | ||||
|     <div class="entity-box"> | ||||
|         <h3>BOption - Application Configuration</h3> | ||||
|         <p><strong>Location:</strong> <code>/apps/server/src/becca/entities/boption.ts</code></p> | ||||
|          | ||||
|         <p>Options store application and user preferences.</p> | ||||
|          | ||||
|         <h4>Common Options</h4> | ||||
|         <pre><code>// Theme settings | ||||
| setOption("theme", "dark"); | ||||
|  | ||||
| // Protected session timeout | ||||
| setOption("protectedSessionTimeout", "600"); | ||||
|  | ||||
| // Sync settings | ||||
| setOption("syncServerHost", "https://sync.server");</code></pre> | ||||
|     </div> | ||||
|      | ||||
|     <h2>Entity Relationships</h2> | ||||
|      | ||||
|     <h3>Parent-Child Hierarchy</h3> | ||||
|     <pre><code>// Single parent | ||||
| childNote.setParent(parentNote.noteId); | ||||
|  | ||||
| // Multiple parents (cloning) | ||||
| childNote.cloneTo(parent1.noteId); | ||||
| childNote.cloneTo(parent2.noteId); | ||||
|  | ||||
| // Get parents | ||||
| const parents = childNote.getParentNotes(); | ||||
|  | ||||
| // Get children | ||||
| const children = parentNote.getChildNotes();</code></pre> | ||||
|      | ||||
|     <h3>Attribute Relationships</h3> | ||||
|     <pre><code>// Direct relations | ||||
| note.addRelation("author", authorNote.noteId); | ||||
|  | ||||
| // Bidirectional relations | ||||
| note1.addRelation("related", note2.noteId); | ||||
| note2.addRelation("related", note1.noteId); | ||||
|  | ||||
| // Get related notes | ||||
| const related = note.getRelations("related");</code></pre> | ||||
|      | ||||
|     <h2>Best Practices</h2> | ||||
|      | ||||
|     <h3>Entity Creation</h3> | ||||
|     <pre><code>// Always use transactions for multiple operations | ||||
| sql.transactional(() => { | ||||
|     const note = new BNote({...}); | ||||
|     note.save(); | ||||
|      | ||||
|     note.addLabel("status", "draft"); | ||||
|     note.addRelation("template", templateId); | ||||
| });</code></pre> | ||||
|      | ||||
|     <h3>Entity Updates</h3> | ||||
|     <pre><code>// Check existence before update | ||||
| const note = becca.getNote(noteId); | ||||
| if (note) { | ||||
|     note.title = "Updated"; | ||||
|     note.save(); | ||||
| }</code></pre> | ||||
|      | ||||
|     <h3>Querying</h3> | ||||
|     <pre><code>// Use indexed queries | ||||
| const attrs = becca.findAttributes("label", "task"); | ||||
|  | ||||
| // Avoid N+1 queries | ||||
| const noteIds = [...]; | ||||
| const notes = becca.getNotes(noteIds); // Single batch</code></pre> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										325
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Monorepo-Structure.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										325
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Monorepo-Structure.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,325 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|     <title>Monorepo Structure</title> | ||||
|     <style> | ||||
|         body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; } | ||||
|         h1 { color: #2c3e50; } | ||||
|         h2 { color: #34495e; margin-top: 30px; } | ||||
|         h3 { color: #7f8c8d; } | ||||
|         pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; } | ||||
|         code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-family: 'Courier New', monospace; } | ||||
|         .directory-tree { background: #2c3e50; color: #ecf0f1; padding: 20px; border-radius: 8px; margin: 20px 0; } | ||||
|         .app-section { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #3498db; } | ||||
|         .package-section { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #27ae60; } | ||||
|         table { border-collapse: collapse; width: 100%; margin: 20px 0; } | ||||
|         th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } | ||||
|         th { background: #f8f9fa; } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|     <h1>Monorepo Structure</h1> | ||||
|      | ||||
|     <p>Trilium is organized as a TypeScript monorepo using NX, facilitating code sharing, consistent tooling, and efficient build processes.</p> | ||||
|      | ||||
|     <h2>Project Organization</h2> | ||||
|      | ||||
|     <div class="directory-tree"> | ||||
|         <pre>TriliumNext/Trilium/ | ||||
| ├── apps/                      # Runnable applications | ||||
| │   ├── client/               # Frontend web application | ||||
| │   ├── server/               # Node.js backend server | ||||
| │   ├── desktop/              # Electron desktop application | ||||
| │   ├── web-clipper/          # Browser extension | ||||
| │   ├── db-compare/           # Database comparison tool | ||||
| │   ├── dump-db/              # Database dump utility | ||||
| │   └── edit-docs/            # Documentation editor | ||||
| ├── packages/                  # Shared libraries | ||||
| │   ├── commons/              # Shared interfaces and utilities | ||||
| │   ├── ckeditor5/            # Rich text editor | ||||
| │   ├── codemirror/           # Code editor | ||||
| │   ├── highlightjs/          # Syntax highlighting | ||||
| │   └── ckeditor5-*/          # CKEditor plugins | ||||
| ├── docs/                      # Documentation | ||||
| ├── nx.json                    # NX workspace configuration | ||||
| ├── package.json              # Root package configuration | ||||
| ├── pnpm-workspace.yaml       # PNPM workspace configuration | ||||
| └── tsconfig.base.json        # Base TypeScript configuration</pre> | ||||
|     </div> | ||||
|      | ||||
|     <h2>Applications</h2> | ||||
|      | ||||
|     <div class="app-section"> | ||||
|         <h3>Client (/apps/client)</h3> | ||||
|         <p>The frontend application shared by both server and desktop versions.</p> | ||||
|          | ||||
|         <h4>Structure</h4> | ||||
|         <pre>apps/client/ | ||||
| ├── src/ | ||||
| │   ├── components/         # Core UI components | ||||
| │   ├── entities/           # Frontend entities | ||||
| │   ├── services/           # Business logic | ||||
| │   ├── widgets/            # UI widgets system | ||||
| │   └── desktop.ts          # Entry point | ||||
| ├── package.json | ||||
| └── vite.config.ts          # Vite configuration</pre> | ||||
|          | ||||
|         <h4>Key Files</h4> | ||||
|         <ul> | ||||
|             <li><code>desktop.ts</code> - Main application initialization</li> | ||||
|             <li><code>services/froca.ts</code> - Frontend cache implementation</li> | ||||
|             <li><code>widgets/basic_widget.ts</code> - Base widget class</li> | ||||
|             <li><code>services/server.ts</code> - API communication layer</li> | ||||
|         </ul> | ||||
|     </div> | ||||
|      | ||||
|     <div class="app-section"> | ||||
|         <h3>Server (/apps/server)</h3> | ||||
|         <p>The Node.js backend providing API, database, and business logic.</p> | ||||
|          | ||||
|         <h4>Structure</h4> | ||||
|         <pre>apps/server/ | ||||
| ├── src/ | ||||
| │   ├── becca/              # Backend cache system | ||||
| │   ├── routes/             # Express routes | ||||
| │   ├── etapi/              # External API | ||||
| │   ├── services/           # Business services | ||||
| │   ├── share/              # Note sharing | ||||
| │   └── main.ts             # Server entry point | ||||
| ├── package.json | ||||
| └── webpack.config.js       # Webpack configuration</pre> | ||||
|          | ||||
|         <h4>Key Services</h4> | ||||
|         <ul> | ||||
|             <li><code>services/sql.ts</code> - Database access layer</li> | ||||
|             <li><code>services/sync.ts</code> - Synchronization logic</li> | ||||
|             <li><code>services/ws.ts</code> - WebSocket server</li> | ||||
|             <li><code>services/protected_session.ts</code> - Encryption handling</li> | ||||
|         </ul> | ||||
|     </div> | ||||
|      | ||||
|     <div class="app-section"> | ||||
|         <h3>Desktop (/apps/desktop)</h3> | ||||
|         <p>Electron wrapper for the desktop application.</p> | ||||
|          | ||||
|         <h4>Key Components</h4> | ||||
|         <ul> | ||||
|             <li><code>main.ts</code> - Electron main process</li> | ||||
|             <li><code>preload.ts</code> - Preload script</li> | ||||
|             <li><code>electron-builder.yml</code> - Build configuration</li> | ||||
|         </ul> | ||||
|     </div> | ||||
|      | ||||
|     <h2>Packages</h2> | ||||
|      | ||||
|     <div class="package-section"> | ||||
|         <h3>Commons (/packages/commons)</h3> | ||||
|         <p>Shared TypeScript interfaces and utilities used across applications.</p> | ||||
|          | ||||
|         <pre><code>// packages/commons/src/types.ts | ||||
| export interface NoteRow { | ||||
|     noteId: string; | ||||
|     title: string; | ||||
|     type: string; | ||||
|     mime: string; | ||||
|     isProtected: boolean; | ||||
|     dateCreated: string; | ||||
|     dateModified: string; | ||||
| }</code></pre> | ||||
|     </div> | ||||
|      | ||||
|     <div class="package-section"> | ||||
|         <h3>CKEditor5 (/packages/ckeditor5)</h3> | ||||
|         <p>Custom CKEditor5 build with Trilium-specific plugins.</p> | ||||
|          | ||||
|         <h4>Custom Plugins</h4> | ||||
|         <ul> | ||||
|             <li><strong>Admonition:</strong> Note boxes with icons</li> | ||||
|             <li><strong>Footnotes:</strong> Reference footnotes</li> | ||||
|             <li><strong>Math:</strong> LaTeX equation rendering</li> | ||||
|             <li><strong>Mermaid:</strong> Diagram integration</li> | ||||
|         </ul> | ||||
|     </div> | ||||
|      | ||||
|     <h2>Build System</h2> | ||||
|      | ||||
|     <h3>Development Commands</h3> | ||||
|     <table> | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <th>Command</th> | ||||
|                 <th>Description</th> | ||||
|             </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|             <tr> | ||||
|                 <td><code>pnpm install</code></td> | ||||
|                 <td>Install dependencies</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><code>pnpm run server:start</code></td> | ||||
|                 <td>Start development server</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><code>pnpm nx run desktop:serve</code></td> | ||||
|                 <td>Start desktop app</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><code>pnpm test:all</code></td> | ||||
|                 <td>Run all tests</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td><code>pnpm nx build server</code></td> | ||||
|                 <td>Build server</td> | ||||
|             </tr> | ||||
|         </tbody> | ||||
|     </table> | ||||
|      | ||||
|     <h3>Build Commands</h3> | ||||
|     <pre><code># Build specific project | ||||
| pnpm nx build server | ||||
| pnpm nx build client | ||||
|  | ||||
| # Build all projects | ||||
| pnpm nx run-many --target=build --all | ||||
|  | ||||
| # Production build | ||||
| pnpm nx build server --configuration=production | ||||
|  | ||||
| # Build only affected projects | ||||
| pnpm nx affected:build --base=main</code></pre> | ||||
|      | ||||
|     <h2>Development Workflow</h2> | ||||
|      | ||||
|     <h3>Initial Setup</h3> | ||||
|     <pre><code># Install dependencies | ||||
| pnpm install | ||||
|  | ||||
| # Enable corepack for pnpm | ||||
| corepack enable | ||||
|  | ||||
| # Build all packages | ||||
| pnpm nx run-many --target=build --all</code></pre> | ||||
|      | ||||
|     <h3>Testing</h3> | ||||
|     <pre><code># Run all tests | ||||
| pnpm test:all | ||||
|  | ||||
| # Run tests for specific project | ||||
| pnpm nx test server | ||||
|  | ||||
| # Run tests in watch mode | ||||
| pnpm nx test server --watch | ||||
|  | ||||
| # Generate coverage | ||||
| pnpm nx test server --coverage</code></pre> | ||||
|      | ||||
|     <h3>Linting and Type Checking</h3> | ||||
|     <pre><code># Lint specific project | ||||
| pnpm nx lint server | ||||
|  | ||||
| # Type check | ||||
| pnpm nx run server:typecheck | ||||
|  | ||||
| # Fix lint issues | ||||
| pnpm nx lint server --fix</code></pre> | ||||
|      | ||||
|     <h2>TypeScript Configuration</h2> | ||||
|      | ||||
|     <h3>Base Configuration</h3> | ||||
|     <pre><code>{ | ||||
|   "compilerOptions": { | ||||
|     "target": "ES2022", | ||||
|     "module": "ESNext", | ||||
|     "lib": ["ES2022", "dom"], | ||||
|     "moduleResolution": "node", | ||||
|     "baseUrl": ".", | ||||
|     "paths": { | ||||
|       "@triliumnext/commons": ["packages/commons/src/index.ts"] | ||||
|     } | ||||
|   } | ||||
| }</code></pre> | ||||
|      | ||||
|     <h2>Build Optimization</h2> | ||||
|      | ||||
|     <h3>NX Features</h3> | ||||
|     <ul> | ||||
|         <li><strong>Build Caching:</strong> Speeds up subsequent builds</li> | ||||
|         <li><strong>Affected Commands:</strong> Build/test only changed code</li> | ||||
|         <li><strong>Parallel Execution:</strong> Run tasks in parallel</li> | ||||
|         <li><strong>Dependency Graph:</strong> Visualize project dependencies</li> | ||||
|     </ul> | ||||
|      | ||||
|     <h3>Optimization Commands</h3> | ||||
|     <pre><code># Show project graph | ||||
| pnpm nx graph | ||||
|  | ||||
| # Clear cache | ||||
| pnpm nx reset | ||||
|  | ||||
| # Profile build performance | ||||
| pnpm nx build server --profile | ||||
|  | ||||
| # Run with cache disabled | ||||
| pnpm nx build server --skip-nx-cache</code></pre> | ||||
|      | ||||
|     <h2>Production Builds</h2> | ||||
|      | ||||
|     <h3>Docker Build</h3> | ||||
|     <pre><code>FROM node:20-alpine AS builder | ||||
| WORKDIR /app | ||||
| COPY package*.json pnpm-lock.yaml ./ | ||||
| RUN corepack enable && pnpm install --frozen-lockfile | ||||
|  | ||||
| COPY . . | ||||
| RUN pnpm nx build server --configuration=production | ||||
|  | ||||
| FROM node:20-alpine | ||||
| WORKDIR /app | ||||
| COPY --from=builder /app/dist/apps/server ./ | ||||
| COPY --from=builder /app/node_modules ./node_modules | ||||
| CMD ["node", "main.js"]</code></pre> | ||||
|      | ||||
|     <h2>Best Practices</h2> | ||||
|      | ||||
|     <h3>Project Structure</h3> | ||||
|     <ol> | ||||
|         <li><strong>Keep packages focused:</strong> Each package should have a single, clear purpose</li> | ||||
|         <li><strong>Minimize circular dependencies:</strong> Use dependency graph to identify issues</li> | ||||
|         <li><strong>Share common code:</strong> Extract shared logic to packages/commons</li> | ||||
|     </ol> | ||||
|      | ||||
|     <h3>Development</h3> | ||||
|     <ol> | ||||
|         <li><strong>Use NX generators:</strong> Generate consistent code structure</li> | ||||
|         <li><strong>Leverage caching:</strong> Don't skip-nx-cache unless debugging</li> | ||||
|         <li><strong>Run affected commands:</strong> Save time by only building/testing changed code</li> | ||||
|     </ol> | ||||
|      | ||||
|     <h2>Troubleshooting</h2> | ||||
|      | ||||
|     <h3>Common Issues</h3> | ||||
|      | ||||
|     <h4>Build Cache Issues</h4> | ||||
|     <pre><code># Clear NX cache | ||||
| pnpm nx reset | ||||
|  | ||||
| # Clear node_modules and reinstall | ||||
| rm -rf node_modules | ||||
| pnpm install</code></pre> | ||||
|      | ||||
|     <h4>Dependency Conflicts</h4> | ||||
|     <pre><code># Check for duplicate packages | ||||
| pnpm list --depth=0 | ||||
|  | ||||
| # Update all dependencies | ||||
| pnpm update --recursive</code></pre> | ||||
|      | ||||
|     <h4>Debug Commands</h4> | ||||
|     <pre><code># Verbose output | ||||
| pnpm nx build server --verbose | ||||
|  | ||||
| # Show affected projects | ||||
| pnpm nx print-affected --type=app --select=projects</code></pre> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										266
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Three-Layer-Cache-System.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										266
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Three-Layer-Cache-System.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,266 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|     <title>Three-Layer Cache System Architecture</title> | ||||
|     <style> | ||||
|         body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; } | ||||
|         h1 { color: #2c3e50; } | ||||
|         h2 { color: #34495e; margin-top: 30px; } | ||||
|         h3 { color: #7f8c8d; } | ||||
|         pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; } | ||||
|         code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-family: 'Courier New', monospace; } | ||||
|         .cache-layer { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; } | ||||
|         .becca { background: #e1f5fe; } | ||||
|         .froca { background: #fff3e0; } | ||||
|         .shaca { background: #f3e5f5; } | ||||
|         table { border-collapse: collapse; width: 100%; margin: 20px 0; } | ||||
|         th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } | ||||
|         th { background: #f8f9fa; } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|     <h1>Three-Layer Cache System Architecture</h1> | ||||
|      | ||||
|     <p>Trilium implements a sophisticated three-layer caching system to optimize performance and reduce database load. This architecture ensures fast access to frequently used data while maintaining consistency across different application contexts.</p> | ||||
|      | ||||
|     <h2>Overview</h2> | ||||
|      | ||||
|     <p>The three cache layers are:</p> | ||||
|      | ||||
|     <ol> | ||||
|         <li><strong>Becca</strong> (Backend Cache) - Server-side entity cache</li> | ||||
|         <li><strong>Froca</strong> (Frontend Cache) - Client-side mirror of backend data</li> | ||||
|         <li><strong>Shaca</strong> (Share Cache) - Optimized cache for shared/published notes</li> | ||||
|     </ol> | ||||
|      | ||||
|     <div class="cache-layer becca"> | ||||
|         <h2>Becca (Backend Cache)</h2> | ||||
|          | ||||
|         <p><strong>Location:</strong> <code>/apps/server/src/becca/</code></p> | ||||
|          | ||||
|         <p>Becca is the authoritative cache layer that maintains all notes, branches, attributes, and options in server memory.</p> | ||||
|          | ||||
|         <h3>Key Components</h3> | ||||
|          | ||||
|         <h4>Becca Interface</h4> | ||||
|         <pre><code>export default class Becca { | ||||
|     loaded: boolean; | ||||
|     notes: Record<string, BNote>; | ||||
|     branches: Record<string, BBranch>; | ||||
|     childParentToBranch: Record<string, BBranch>; | ||||
|     attributes: Record<string, BAttribute>; | ||||
|     attributeIndex: Record<string, BAttribute[]>; | ||||
|     options: Record<string, BOption>; | ||||
|     etapiTokens: Record<string, BEtapiToken>; | ||||
|     allNoteSetCache: NoteSet | null; | ||||
| }</code></pre> | ||||
|          | ||||
|         <h3>Features</h3> | ||||
|         <ul> | ||||
|             <li><strong>In-memory storage:</strong> All active entities are kept in memory for fast access</li> | ||||
|             <li><strong>Lazy loading:</strong> Related entities (revisions, attachments) loaded on demand</li> | ||||
|             <li><strong>Index structures:</strong> Optimized lookups via childParentToBranch and attributeIndex</li> | ||||
|             <li><strong>Cache invalidation:</strong> Automatic cache updates on entity changes</li> | ||||
|             <li><strong>Protected note decryption:</strong> On-demand decryption of encrypted content</li> | ||||
|         </ul> | ||||
|          | ||||
|         <h3>Usage Example</h3> | ||||
|         <pre><code>import becca from "./becca/becca.js"; | ||||
|  | ||||
| // Get a note | ||||
| const note = becca.getNote("noteId"); | ||||
|  | ||||
| // Find attributes by type and name | ||||
| const labels = becca.findAttributes("label", "todoItem"); | ||||
|  | ||||
| // Get branch relationships | ||||
| const branch = becca.getBranchFromChildAndParent(childId, parentId);</code></pre> | ||||
|     </div> | ||||
|      | ||||
|     <div class="cache-layer froca"> | ||||
|         <h2>Froca (Frontend Cache)</h2> | ||||
|          | ||||
|         <p><strong>Location:</strong> <code>/apps/client/src/services/froca.ts</code></p> | ||||
|          | ||||
|         <p>Froca is the frontend mirror of Becca, maintaining a subset of backend data for client-side operations.</p> | ||||
|          | ||||
|         <h3>Key Components</h3> | ||||
|          | ||||
|         <pre><code>class FrocaImpl implements Froca { | ||||
|     notes: Record<string, FNote>; | ||||
|     branches: Record<string, FBranch>; | ||||
|     attributes: Record<string, FAttribute>; | ||||
|     attachments: Record<string, FAttachment>; | ||||
|     blobPromises: Record<string, Promise<FBlob | null> | null>; | ||||
| }</code></pre> | ||||
|          | ||||
|         <h3>Features</h3> | ||||
|         <ul> | ||||
|             <li><strong>Lazy loading:</strong> Notes loaded on-demand with their immediate context</li> | ||||
|             <li><strong>Subtree loading:</strong> Efficient loading of note hierarchies</li> | ||||
|             <li><strong>Real-time updates:</strong> WebSocket synchronization with backend changes</li> | ||||
|             <li><strong>Search note support:</strong> Virtual branches for search results</li> | ||||
|             <li><strong>Promise-based blob loading:</strong> Asynchronous content loading</li> | ||||
|         </ul> | ||||
|          | ||||
|         <h3>Loading Strategy</h3> | ||||
|         <pre><code>// Initial load - loads root and immediate children | ||||
| await froca.loadInitialTree(); | ||||
|  | ||||
| // Load subtree on demand | ||||
| const note = await froca.loadSubTree(noteId); | ||||
|  | ||||
| // Reload specific notes | ||||
| await froca.reloadNotes([noteId1, noteId2]);</code></pre> | ||||
|     </div> | ||||
|      | ||||
|     <div class="cache-layer shaca"> | ||||
|         <h2>Shaca (Share Cache)</h2> | ||||
|          | ||||
|         <p><strong>Location:</strong> <code>/apps/server/src/share/shaca/</code></p> | ||||
|          | ||||
|         <p>Shaca is a specialized cache for publicly shared notes, optimized for read-only access.</p> | ||||
|          | ||||
|         <h3>Key Components</h3> | ||||
|          | ||||
|         <pre><code>export default class Shaca { | ||||
|     notes: Record<string, SNote>; | ||||
|     branches: Record<string, SBranch>; | ||||
|     childParentToBranch: Record<string, SBranch>; | ||||
|     attributes: Record<string, SAttribute>; | ||||
|     attachments: Record<string, SAttachment>; | ||||
|     aliasToNote: Record<string, SNote>; | ||||
|     shareRootNote: SNote | null; | ||||
|     shareIndexEnabled: boolean; | ||||
| }</code></pre> | ||||
|          | ||||
|         <h3>Features</h3> | ||||
|         <ul> | ||||
|             <li><strong>Read-only optimization:</strong> Streamlined for public access</li> | ||||
|             <li><strong>Alias support:</strong> URL-friendly note access via aliases</li> | ||||
|             <li><strong>Share index:</strong> Optional indexing of all shared subtrees</li> | ||||
|             <li><strong>Minimal memory footprint:</strong> Only shared content cached</li> | ||||
|             <li><strong>Security isolation:</strong> Separate from main application cache</li> | ||||
|         </ul> | ||||
|     </div> | ||||
|      | ||||
|     <h2>Cache Interaction and Data Flow</h2> | ||||
|      | ||||
|     <h3>Create/Update Flow</h3> | ||||
|     <ol> | ||||
|         <li>Client sends update request to API</li> | ||||
|         <li>API updates Becca cache</li> | ||||
|         <li>Becca persists change to database</li> | ||||
|         <li>API pushes update to Froca via WebSocket</li> | ||||
|         <li>Froca updates UI components</li> | ||||
|     </ol> | ||||
|      | ||||
|     <h3>Read Flow</h3> | ||||
|     <ol> | ||||
|         <li>Client requests note from Froca</li> | ||||
|         <li>If cached: Return immediately</li> | ||||
|         <li>If not cached: Fetch from API</li> | ||||
|         <li>API retrieves from Becca</li> | ||||
|         <li>Froca caches and returns data</li> | ||||
|     </ol> | ||||
|      | ||||
|     <h2>Performance Considerations</h2> | ||||
|      | ||||
|     <table> | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <th>Cache Layer</th> | ||||
|                 <th>Memory Usage</th> | ||||
|                 <th>Loading Strategy</th> | ||||
|                 <th>Use Case</th> | ||||
|             </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|             <tr> | ||||
|                 <td>Becca</td> | ||||
|                 <td>100-500MB typical</td> | ||||
|                 <td>Full load on startup</td> | ||||
|                 <td>Server operations</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td>Froca</td> | ||||
|                 <td>Variable (on-demand)</td> | ||||
|                 <td>Progressive loading</td> | ||||
|                 <td>Client UI</td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|                 <td>Shaca</td> | ||||
|                 <td>Minimal</td> | ||||
|                 <td>Lazy loading</td> | ||||
|                 <td>Public sharing</td> | ||||
|             </tr> | ||||
|         </tbody> | ||||
|     </table> | ||||
|      | ||||
|     <h2>Best Practices</h2> | ||||
|      | ||||
|     <h3>When to Use Each Cache</h3> | ||||
|      | ||||
|     <p><strong>Use Becca when:</strong></p> | ||||
|     <ul> | ||||
|         <li>Implementing server-side business logic</li> | ||||
|         <li>Performing bulk operations</li> | ||||
|         <li>Handling synchronization</li> | ||||
|         <li>Managing protected notes</li> | ||||
|     </ul> | ||||
|      | ||||
|     <p><strong>Use Froca when:</strong></p> | ||||
|     <ul> | ||||
|         <li>Building UI components</li> | ||||
|         <li>Handling user interactions</li> | ||||
|         <li>Displaying note content</li> | ||||
|         <li>Managing client state</li> | ||||
|     </ul> | ||||
|      | ||||
|     <p><strong>Use Shaca when:</strong></p> | ||||
|     <ul> | ||||
|         <li>Serving public content</li> | ||||
|         <li>Building share pages</li> | ||||
|         <li>Implementing read-only access</li> | ||||
|         <li>Creating public APIs</li> | ||||
|     </ul> | ||||
|      | ||||
|     <h3>Cache Invalidation</h3> | ||||
|     <pre><code>// Becca - automatic on entity save | ||||
| note.save(); // Cache updated automatically | ||||
|  | ||||
| // Froca - manual reload when needed | ||||
| await froca.reloadNotes([noteId]); | ||||
|  | ||||
| // Shaca - rebuild on share changes | ||||
| shaca.reset(); | ||||
| shaca.load();</code></pre> | ||||
|      | ||||
|     <h2>Troubleshooting</h2> | ||||
|      | ||||
|     <h3>Common Issues</h3> | ||||
|      | ||||
|     <ol> | ||||
|         <li><strong>Cache Inconsistency</strong> | ||||
|             <ul> | ||||
|                 <li>Symptom: UI shows outdated data</li> | ||||
|                 <li>Solution: Force reload with <code>froca.reloadNotes()</code></li> | ||||
|             </ul> | ||||
|         </li> | ||||
|          | ||||
|         <li><strong>Memory Growth</strong> | ||||
|             <ul> | ||||
|                 <li>Symptom: Server memory usage increases</li> | ||||
|                 <li>Solution: Check for memory leaks in custom scripts</li> | ||||
|             </ul> | ||||
|         </li> | ||||
|          | ||||
|         <li><strong>Slow Initial Load</strong> | ||||
|             <ul> | ||||
|                 <li>Symptom: Long startup time</li> | ||||
|                 <li>Solution: Optimize database queries, add indexes</li> | ||||
|             </ul> | ||||
|         </li> | ||||
|     </ol> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										286
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Widget-Based-UI-Architecture.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										286
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Widget-Based-UI-Architecture.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,286 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head> | ||||
|     <title>Widget-Based UI Architecture</title> | ||||
|     <style> | ||||
|         body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; } | ||||
|         h1 { color: #2c3e50; } | ||||
|         h2 { color: #34495e; margin-top: 30px; } | ||||
|         h3 { color: #7f8c8d; } | ||||
|         pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; } | ||||
|         code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-family: 'Courier New', monospace; } | ||||
|         .widget-class { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #27ae60; } | ||||
|         .example-box { background: #ecf0f1; padding: 15px; border-radius: 5px; margin: 15px 0; } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|     <h1>Widget-Based UI Architecture</h1> | ||||
|      | ||||
|     <p>Trilium's frontend is built on a modular widget system that provides flexibility, reusability, and maintainability. This architecture enables dynamic UI composition and extensibility through custom widgets.</p> | ||||
|      | ||||
|     <h2>Widget System Overview</h2> | ||||
|      | ||||
|     <p>The widget hierarchy follows an inheritance pattern where each level adds specific functionality:</p> | ||||
|      | ||||
|     <ol> | ||||
|         <li><strong>Component</strong> - Base class for all UI components</li> | ||||
|         <li><strong>BasicWidget</strong> - UI foundation with DOM manipulation</li> | ||||
|         <li><strong>NoteContextAwareWidget</strong> - Note-aware components</li> | ||||
|         <li><strong>RightPanelWidget</strong> - Side panel widgets</li> | ||||
|         <li><strong>TypeWidgets</strong> - Note type specific widgets</li> | ||||
|         <li><strong>CustomWidgets</strong> - User-created widgets</li> | ||||
|     </ol> | ||||
|      | ||||
|     <div class="widget-class"> | ||||
|         <h2>Core Widget Classes</h2> | ||||
|          | ||||
|         <h3>BasicWidget</h3> | ||||
|         <p><strong>Location:</strong> <code>/apps/client/src/widgets/basic_widget.ts</code></p> | ||||
|          | ||||
|         <p>Base class for all UI widgets, providing DOM manipulation and styling capabilities.</p> | ||||
|          | ||||
|         <h4>Key Methods</h4> | ||||
|         <ul> | ||||
|             <li><code>id(id: string)</code> - Set widget ID</li> | ||||
|             <li><code>class(className: string)</code> - Add CSS class</li> | ||||
|             <li><code>css(name: string, value: string)</code> - Set CSS property</li> | ||||
|             <li><code>child(...components)</code> - Add child widgets</li> | ||||
|             <li><code>doRender()</code> - Render widget HTML</li> | ||||
|         </ul> | ||||
|          | ||||
|         <div class="example-box"> | ||||
|             <h4>Usage Example</h4> | ||||
|             <pre><code>class MyWidget extends BasicWidget { | ||||
|     doRender() { | ||||
|         this.$widget = $('<div>') | ||||
|             .addClass('my-widget') | ||||
|             .append($('<h3>').text('Widget Title')); | ||||
|              | ||||
|         return this.$widget; | ||||
|     } | ||||
|      | ||||
|     async refreshWithNote(note) { | ||||
|         this.$widget.find('h3').text(note.title); | ||||
|     } | ||||
| }</code></pre> | ||||
|         </div> | ||||
|     </div> | ||||
|      | ||||
|     <div class="widget-class"> | ||||
|         <h3>NoteContextAwareWidget</h3> | ||||
|         <p><strong>Location:</strong> <code>/apps/client/src/widgets/note_context_aware_widget.ts</code></p> | ||||
|          | ||||
|         <p>Base class for widgets that respond to note context changes.</p> | ||||
|          | ||||
|         <h4>Lifecycle Methods</h4> | ||||
|         <ul> | ||||
|             <li><code>refreshWithNote(note)</code> - Called when note context changes</li> | ||||
|             <li><code>noteSwitched()</code> - Called when user switches notes</li> | ||||
|             <li><code>activeContextChanged()</code> - Called on context change</li> | ||||
|             <li><code>noteTypeMimeChanged()</code> - React to note type changes</li> | ||||
|         </ul> | ||||
|          | ||||
|         <div class="example-box"> | ||||
|             <h4>Context Management Example</h4> | ||||
|             <pre><code>class MyNoteWidget extends NoteContextAwareWidget { | ||||
|     async refreshWithNote(note) { | ||||
|         // Called when note context changes | ||||
|         this.$widget.find('.note-title').text(note.title); | ||||
|         this.$widget.find('.note-type').text(note.type); | ||||
|          | ||||
|         // Access note attributes | ||||
|         const labels = note.getLabels(); | ||||
|         const relations = note.getRelations(); | ||||
|     } | ||||
|      | ||||
|     async noteSwitched() { | ||||
|         // Called when user switches to different note | ||||
|         console.log(`Switched to note: ${this.noteId}`); | ||||
|     } | ||||
| }</code></pre> | ||||
|         </div> | ||||
|     </div> | ||||
|      | ||||
|     <div class="widget-class"> | ||||
|         <h3>RightPanelWidget</h3> | ||||
|         <p><strong>Location:</strong> <code>/apps/client/src/widgets/right_panel_widget.ts</code></p> | ||||
|          | ||||
|         <p>Base class for widgets displayed in the right sidebar panel.</p> | ||||
|          | ||||
|         <h4>Required Methods</h4> | ||||
|         <ul> | ||||
|             <li><code>getTitle()</code> - Widget title</li> | ||||
|             <li><code>getIcon()</code> - Widget icon</li> | ||||
|             <li><code>getPosition()</code> - Display order</li> | ||||
|             <li><code>doRenderBody()</code> - Render widget content</li> | ||||
|         </ul> | ||||
|          | ||||
|         <div class="example-box"> | ||||
|             <h4>Right Panel Widget Example</h4> | ||||
|             <pre><code>class InfoWidget extends RightPanelWidget { | ||||
|     getTitle() { return "Note Info"; } | ||||
|     getIcon() { return "info"; } | ||||
|     getPosition() { return 100; } | ||||
|      | ||||
|     async doRenderBody() { | ||||
|         return $('<div class="info-widget">') | ||||
|             .append($('<div class="created">')) | ||||
|             .append($('<div class="modified">')) | ||||
|             .append($('<div class="word-count">')); | ||||
|     } | ||||
|      | ||||
|     async refreshWithNote(note) { | ||||
|         this.$body.find('.created').text(`Created: ${note.dateCreated}`); | ||||
|         this.$body.find('.modified').text(`Modified: ${note.dateModified}`); | ||||
|     } | ||||
| }</code></pre> | ||||
|         </div> | ||||
|     </div> | ||||
|      | ||||
|     <h2>Type-Specific Widgets</h2> | ||||
|      | ||||
|     <p><strong>Location:</strong> <code>/apps/client/src/widgets/type_widgets/</code></p> | ||||
|      | ||||
|     <p>Each note type has a specialized widget for rendering and editing:</p> | ||||
|      | ||||
|     <ul> | ||||
|         <li><strong>TextTypeWidget</strong> - Rich text editor using CKEditor</li> | ||||
|         <li><strong>CodeTypeWidget</strong> - Code editor using CodeMirror</li> | ||||
|         <li><strong>FileTypeWidget</strong> - File attachment viewer</li> | ||||
|         <li><strong>ImageTypeWidget</strong> - Image viewer with editing</li> | ||||
|         <li><strong>CanvasTypeWidget</strong> - Excalidraw integration</li> | ||||
|         <li><strong>MermaidTypeWidget</strong> - Mermaid diagram renderer</li> | ||||
|     </ul> | ||||
|      | ||||
|     <h2>Widget Communication</h2> | ||||
|      | ||||
|     <h3>Event System</h3> | ||||
|     <pre><code>// Publishing events | ||||
| class PublisherWidget extends BasicWidget { | ||||
|     async handleClick() { | ||||
|         // Local event | ||||
|         this.trigger('itemSelected', { itemId: '123' }); | ||||
|          | ||||
|         // Global event | ||||
|         appContext.triggerEvent('noteChanged', { noteId: this.noteId }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Subscribing to events | ||||
| class SubscriberWidget extends BasicWidget { | ||||
|     constructor() { | ||||
|         super(); | ||||
|          | ||||
|         // Local event subscription | ||||
|         this.on('itemSelected', (event) => { | ||||
|             console.log('Item selected:', event.itemId); | ||||
|         }); | ||||
|          | ||||
|         // Global event subscription | ||||
|         appContext.addEventListener('noteChanged', (event) => { | ||||
|             this.handleNoteChange(event.noteId); | ||||
|         }); | ||||
|     } | ||||
| }</code></pre> | ||||
|      | ||||
|     <h2>Custom Widget Development</h2> | ||||
|      | ||||
|     <h3>Creating Custom Widgets</h3> | ||||
|     <pre><code>// 1. Define widget class | ||||
| class TaskListWidget extends NoteContextAwareWidget { | ||||
|     doRender() { | ||||
|         this.$widget = $('<div class="task-list-widget">'); | ||||
|         this.$list = $('<ul>').appendTo(this.$widget); | ||||
|         return this.$widget; | ||||
|     } | ||||
|      | ||||
|     async refreshWithNote(note) { | ||||
|         const tasks = await this.loadTasks(note); | ||||
|          | ||||
|         this.$list.empty(); | ||||
|         for (const task of tasks) { | ||||
|             $('<li>') | ||||
|                 .text(task.title) | ||||
|                 .toggleClass('completed', task.completed) | ||||
|                 .appendTo(this.$list); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private async loadTasks(note) { | ||||
|         // Load task data from note attributes | ||||
|         const taskLabels = note.getLabels('task'); | ||||
|         return taskLabels.map(label => JSON.parse(label.value)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // 2. Register widget | ||||
| api.addWidget(TaskListWidget);</code></pre> | ||||
|      | ||||
|     <h2>Performance Optimization</h2> | ||||
|      | ||||
|     <h3>Lazy Loading</h3> | ||||
|     <pre><code>class LazyWidget extends BasicWidget { | ||||
|     private contentLoaded = false; | ||||
|      | ||||
|     async becomeVisible() { | ||||
|         if (!this.contentLoaded) { | ||||
|             await this.loadContent(); | ||||
|             this.contentLoaded = true; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private async loadContent() { | ||||
|         // Heavy content loading | ||||
|         const data = await server.get('expensive-data'); | ||||
|         this.renderContent(data); | ||||
|     } | ||||
| }</code></pre> | ||||
|      | ||||
|     <h3>Debouncing Updates</h3> | ||||
|     <pre><code>class DebouncedWidget extends NoteContextAwareWidget { | ||||
|     private refreshDebounced = utils.debounce( | ||||
|         () => this.doRefresh(), | ||||
|         500 | ||||
|     ); | ||||
|      | ||||
|     async refreshWithNote(note) { | ||||
|         // Debounce rapid updates | ||||
|         this.refreshDebounced(); | ||||
|     } | ||||
|      | ||||
|     private async doRefresh() { | ||||
|         // Actual refresh logic | ||||
|     } | ||||
| }</code></pre> | ||||
|      | ||||
|     <h2>Best Practices</h2> | ||||
|      | ||||
|     <h3>Widget Design</h3> | ||||
|     <ol> | ||||
|         <li><strong>Single Responsibility:</strong> Each widget should have one clear purpose</li> | ||||
|         <li><strong>Composition over Inheritance:</strong> Use composition for complex UIs</li> | ||||
|         <li><strong>Lazy Initialization:</strong> Load resources only when needed</li> | ||||
|         <li><strong>Event Cleanup:</strong> Remove event listeners in cleanup()</li> | ||||
|     </ol> | ||||
|      | ||||
|     <h3>Error Handling</h3> | ||||
|     <pre><code>class ResilientWidget extends BasicWidget { | ||||
|     async refreshWithNote(note) { | ||||
|         try { | ||||
|             await this.loadData(note); | ||||
|         } catch (error) { | ||||
|             this.showError('Failed to load data'); | ||||
|             console.error('Widget error:', error); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private showError(message) { | ||||
|         this.$widget.html(` | ||||
|             <div class="alert alert-danger"> | ||||
|                 ${message} | ||||
|             </div> | ||||
|         `); | ||||
|     } | ||||
| }</code></pre> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										1504
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Backend Script Development.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1504
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Backend Script Development.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1718
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Custom Note Type Development.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1718
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Custom Note Type Development.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,828 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <title>Custom Widget Development Guide - Trilium Developer Guide</title> | ||||
|     <style> | ||||
|         body { | ||||
|             font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | ||||
|             line-height: 1.6; | ||||
|             color: #333; | ||||
|             max-width: 1200px; | ||||
|             margin: 0 auto; | ||||
|             padding: 20px; | ||||
|             background: #f5f5f5; | ||||
|         } | ||||
|         .content { | ||||
|             background: white; | ||||
|             padding: 40px; | ||||
|             border-radius: 8px; | ||||
|             box-shadow: 0 2px 10px rgba(0,0,0,0.1); | ||||
|         } | ||||
|         h1 { | ||||
|             color: #2c3e50; | ||||
|             border-bottom: 3px solid #3498db; | ||||
|             padding-bottom: 10px; | ||||
|         } | ||||
|         h2 { | ||||
|             color: #34495e; | ||||
|             margin-top: 30px; | ||||
|             border-bottom: 1px solid #ecf0f1; | ||||
|             padding-bottom: 8px; | ||||
|         } | ||||
|         h3 { | ||||
|             color: #555; | ||||
|             margin-top: 25px; | ||||
|         } | ||||
|         pre { | ||||
|             background: #f8f8f8; | ||||
|             border: 1px solid #e1e4e8; | ||||
|             border-radius: 6px; | ||||
|             padding: 16px; | ||||
|             overflow-x: auto; | ||||
|         } | ||||
|         code { | ||||
|             background: #f3f4f6; | ||||
|             padding: 2px 6px; | ||||
|             border-radius: 3px; | ||||
|             font-family: 'Monaco', 'Courier New', monospace; | ||||
|         } | ||||
|         pre code { | ||||
|             background: none; | ||||
|             padding: 0; | ||||
|         } | ||||
|         blockquote { | ||||
|             border-left: 4px solid #3498db; | ||||
|             margin: 0; | ||||
|             padding-left: 20px; | ||||
|             color: #666; | ||||
|             font-style: italic; | ||||
|         } | ||||
|         ul, ol { | ||||
|             margin: 16px 0; | ||||
|             padding-left: 30px; | ||||
|         } | ||||
|         li { | ||||
|             margin: 8px 0; | ||||
|         } | ||||
|         table { | ||||
|             border-collapse: collapse; | ||||
|             width: 100%; | ||||
|             margin: 20px 0; | ||||
|         } | ||||
|         th, td { | ||||
|             border: 1px solid #ddd; | ||||
|             padding: 12px; | ||||
|             text-align: left; | ||||
|         } | ||||
|         th { | ||||
|             background: #f8f9fa; | ||||
|             font-weight: 600; | ||||
|         } | ||||
|         a { | ||||
|             color: #3498db; | ||||
|             text-decoration: none; | ||||
|         } | ||||
|         a:hover { | ||||
|             text-decoration: underline; | ||||
|         } | ||||
|         .warning { | ||||
|             background: #fff3cd; | ||||
|             border: 1px solid #ffc107; | ||||
|             border-radius: 4px; | ||||
|             padding: 12px; | ||||
|             margin: 20px 0; | ||||
|         } | ||||
|         .info { | ||||
|             background: #d1ecf1; | ||||
|             border: 1px solid #17a2b8; | ||||
|             border-radius: 4px; | ||||
|             padding: 12px; | ||||
|             margin: 20px 0; | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|     <div class="content"> | ||||
|         <h1>Custom Widget Development Guide</h1> | ||||
|  | ||||
| <p>This guide provides comprehensive instructions for creating custom widgets in Trilium Notes. Widgets are fundamental UI components that enable you to extend Trilium's functionality with custom interfaces and behaviors.</p> | ||||
|  | ||||
| <h2>Prerequisites</h2> | ||||
|  | ||||
| <p>Before developing custom widgets, ensure you have: | ||||
| - Basic knowledge of TypeScript/JavaScript | ||||
| - Understanding of jQuery and DOM manipulation | ||||
| - Familiarity with Trilium's note structure | ||||
| - A development environment with Trilium running locally</p> | ||||
|  | ||||
| <h2>Understanding Widget Architecture</h2> | ||||
|  | ||||
| <h3>Widget Hierarchy</h3> | ||||
|  | ||||
| <p>Trilium's widget system follows a hierarchical structure:</p> | ||||
|  | ||||
| <pre><code>Component (base class) | ||||
|     └── BasicWidget | ||||
|         ├── NoteContextAwareWidget | ||||
|         │   ├── TypeWidget (for note type widgets) | ||||
|         │   └── RightPanelWidget | ||||
|         └── Custom widgets (buttons, containers, etc.) | ||||
| </code></pre> | ||||
|  | ||||
| <h3>Core Widget Classes</h3> | ||||
|  | ||||
| <p>#### BasicWidget | ||||
| The foundation class for all widgets. Provides basic rendering, positioning, and visibility management.</p> | ||||
|  | ||||
| <pre><code class="language-typescript">import BasicWidget from "../widgets/basic_widget.js"; | ||||
|  | ||||
| <p>class MyCustomWidget extends BasicWidget { | ||||
|     doRender() { | ||||
|         this.$widget = $('<div class="my-widget">Hello Widget</div>'); | ||||
|     } | ||||
| } | ||||
| </code></pre></p> | ||||
|  | ||||
| <p>#### NoteContextAwareWidget | ||||
| Extends BasicWidget to respond to note changes. Use this when your widget needs to update based on the active note.</p> | ||||
|  | ||||
| <pre><code class="language-typescript">import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js"; | ||||
|  | ||||
| <p>class NoteInfoWidget extends NoteContextAwareWidget { | ||||
|     async refreshWithNote(note) { | ||||
|         if (!note) return; | ||||
|          | ||||
|         this.$widget.find('.note-title').text(note.title); | ||||
|         this.$widget.find('.note-type').text(note.type); | ||||
|     } | ||||
|      | ||||
|     doRender() { | ||||
|         this.$widget = $(<code> | ||||
|             <div class="note-info-widget"> | ||||
|                 <div class="note-title"></div> | ||||
|                 <div class="note-type"></div> | ||||
|             </div> | ||||
|         </code>); | ||||
|     } | ||||
| } | ||||
| </code></pre></p> | ||||
|  | ||||
| <p>#### RightPanelWidget | ||||
| Specialized widget for rendering panels in the right sidebar with a consistent card layout.</p> | ||||
|  | ||||
| <pre><code class="language-typescript">import RightPanelWidget from "../widgets/right_panel_widget.js"; | ||||
|  | ||||
| <p>class StatisticsWidget extends RightPanelWidget { | ||||
|     get widgetTitle() {  | ||||
|         return "Note Statistics";  | ||||
|     } | ||||
|      | ||||
|     async doRenderBody() { | ||||
|         this.$body.html(<code> | ||||
|             <div class="stats-container"> | ||||
|                 <div class="word-count">Words: <span>0</span></div> | ||||
|                 <div class="char-count">Characters: <span>0</span></div> | ||||
|             </div> | ||||
|         </code>); | ||||
|     } | ||||
|      | ||||
|     async refreshWithNote(note) { | ||||
|         const content = await note.getContent(); | ||||
|         const wordCount = content.split(/\s+/).length; | ||||
|         const charCount = content.length; | ||||
|          | ||||
|         this.$body.find('.word-count span').text(wordCount); | ||||
|         this.$body.find('.char-count span').text(charCount); | ||||
|     } | ||||
| } | ||||
| </code></pre></p> | ||||
|  | ||||
| <h2>Widget Lifecycle</h2> | ||||
|  | ||||
| <h3>Initialization Phase</h3> | ||||
| <li><strong>Constructor</strong>: Set up initial state and child widgets</li> | ||||
| <li><strong>render()</strong>: Called to create the widget's DOM structure</li> | ||||
| <li><strong>doRender()</strong>: Override this to create your widget's HTML</li> | ||||
|  | ||||
| <h3>Update Phase</h3> | ||||
| <li><strong>refresh()</strong>: Called when widget needs updating</li> | ||||
| <li><strong>refreshWithNote()</strong>: Called for NoteContextAwareWidget when note changes</li> | ||||
| <li><strong>Event handlers</strong>: Respond to various Trilium events</li> | ||||
|  | ||||
| <h3>Cleanup Phase</h3> | ||||
| <li><strong>cleanup()</strong>: Override to clean up resources, event listeners, etc.</li> | ||||
| <li><strong>remove()</strong>: Removes widget from DOM</li> | ||||
|  | ||||
| <h2>Event Handling</h2> | ||||
|  | ||||
| <h3>Subscribing to Events</h3> | ||||
|  | ||||
| <p>Widgets can listen to Trilium's event system:</p> | ||||
|  | ||||
| <pre><code class="language-typescript">class EventAwareWidget extends NoteContextAwareWidget { | ||||
|     constructor() { | ||||
|         super(); | ||||
|         // Events are automatically subscribed based on method names | ||||
|     } | ||||
|      | ||||
|     // Called when entities are reloaded | ||||
|     async entitiesReloadedEvent({ loadResults }) { | ||||
|         console.log('Entities reloaded'); | ||||
|         await this.refresh(); | ||||
|     } | ||||
|      | ||||
|     // Called when note content changes | ||||
|     async noteContentChangedEvent({ noteId }) { | ||||
|         if (this.noteId === noteId) { | ||||
|             await this.refresh(); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // Called when active context changes | ||||
|     async activeContextChangedEvent({ noteContext }) { | ||||
|         this.noteContext = noteContext; | ||||
|         await this.refresh(); | ||||
|     } | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h3>Common Events</h3> | ||||
|  | ||||
| <p>- <code>noteSwitched</code>: Active note changed | ||||
| - <code>activeContextChanged</code>: Active tab/context changed | ||||
| - <code>entitiesReloaded</code>: Notes, branches, or attributes reloaded | ||||
| - <code>noteContentChanged</code>: Note content modified | ||||
| - <code>noteTypeMimeChanged</code>: Note type or MIME changed | ||||
| - <code>frocaReloaded</code>: Frontend cache reloaded</p> | ||||
|  | ||||
| <h2>State Management</h2> | ||||
|  | ||||
| <h3>Local State</h3> | ||||
| Store widget-specific state in instance properties: | ||||
|  | ||||
| <pre><code class="language-typescript">class StatefulWidget extends BasicWidget { | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.isExpanded = false; | ||||
|         this.cachedData = null; | ||||
|     } | ||||
|      | ||||
|     toggleExpanded() { | ||||
|         this.isExpanded = !this.isExpanded; | ||||
|         this.$widget.toggleClass('expanded', this.isExpanded); | ||||
|     } | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h3>Persistent State</h3> | ||||
| Use options or attributes for persistent state: | ||||
|  | ||||
| <pre><code class="language-typescript">class PersistentWidget extends NoteContextAwareWidget { | ||||
|     async saveState(state) { | ||||
|         await server.put('options', { | ||||
|             name: 'widgetState', | ||||
|             value: JSON.stringify(state) | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     async loadState() { | ||||
|         const option = await server.get('options/widgetState'); | ||||
|         return option ? JSON.parse(option.value) : {}; | ||||
|     } | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h2>Accessing Trilium APIs</h2> | ||||
|  | ||||
| <h3>Frontend Services</h3> | ||||
|  | ||||
| <pre><code class="language-typescript">import froca from "../services/froca.js"; | ||||
| import server from "../services/server.js"; | ||||
| import linkService from "../services/link.js"; | ||||
| import toastService from "../services/toast.js"; | ||||
| import dialogService from "../services/dialog.js"; | ||||
|  | ||||
| <p>class ApiWidget extends NoteContextAwareWidget { | ||||
|     async doRenderBody() { | ||||
|         // Access notes | ||||
|         const note = await froca.getNote(this.noteId); | ||||
|          | ||||
|         // Get attributes | ||||
|         const attributes = note.getAttributes(); | ||||
|          | ||||
|         // Create links | ||||
|         const $link = await linkService.createLink(note.noteId); | ||||
|          | ||||
|         // Show notifications | ||||
|         toastService.showMessage("Widget loaded"); | ||||
|          | ||||
|         // Open dialogs | ||||
|         const result = await dialogService.confirm("Continue?"); | ||||
|     } | ||||
| } | ||||
| </code></pre></p> | ||||
|  | ||||
| <h3>Server Communication</h3> | ||||
|  | ||||
| <pre><code class="language-typescript">class ServerWidget extends BasicWidget { | ||||
|     async loadData() { | ||||
|         // GET request | ||||
|         const data = await server.get('custom-api/data'); | ||||
|          | ||||
|         // POST request | ||||
|         const result = await server.post('custom-api/process', { | ||||
|             noteId: this.noteId, | ||||
|             action: 'analyze' | ||||
|         }); | ||||
|          | ||||
|         // PUT request | ||||
|         await server.put(<code>notes/${this.noteId}</code>, { | ||||
|             title: 'Updated Title' | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h2>Styling Widgets</h2> | ||||
|  | ||||
| <h3>Inline Styles</h3> | ||||
| <pre><code class="language-typescript">class StyledWidget extends BasicWidget { | ||||
|     doRender() { | ||||
|         this.$widget = $('<div>'); | ||||
|         this.css('padding', '10px') | ||||
|             .css('background-color', '#f0f0f0') | ||||
|             .css('border-radius', '4px'); | ||||
|     } | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h3>CSS Classes</h3> | ||||
| <pre><code class="language-typescript">class ClassedWidget extends BasicWidget { | ||||
|     doRender() { | ||||
|         this.$widget = $('<div>'); | ||||
|         this.class('custom-widget') | ||||
|             .class('bordered'); | ||||
|     } | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h3>CSS Blocks</h3> | ||||
| <pre><code class="language-typescript">class CSSBlockWidget extends BasicWidget { | ||||
|     doRender() { | ||||
|         this.$widget = $('<div class="my-widget">Content</div>'); | ||||
|          | ||||
|         this.cssBlock(<code> | ||||
|             .my-widget { | ||||
|                 padding: 15px; | ||||
|                 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | ||||
|                 color: white; | ||||
|                 border-radius: 8px; | ||||
|             } | ||||
|              | ||||
|             .my-widget:hover { | ||||
|                 transform: translateY(-2px); | ||||
|                 box-shadow: 0 4px 12px rgba(0,0,0,0.15); | ||||
|             } | ||||
|         </code>); | ||||
|     } | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h2>Performance Optimization</h2> | ||||
|  | ||||
| <h3>Lazy Loading</h3> | ||||
| <pre><code class="language-typescript">class LazyWidget extends NoteContextAwareWidget { | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.dataLoaded = false; | ||||
|     } | ||||
|      | ||||
|     async refreshWithNote(note) { | ||||
|         if (!this.isVisible()) { | ||||
|             return; // Don't load if not visible | ||||
|         } | ||||
|          | ||||
|         if (!this.dataLoaded) { | ||||
|             await this.loadExpensiveData(); | ||||
|             this.dataLoaded = true; | ||||
|         } | ||||
|          | ||||
|         this.updateDisplay(); | ||||
|     } | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h3>Debouncing Updates</h3> | ||||
| <pre><code class="language-typescript">import SpacedUpdate from "../services/spaced_update.js"; | ||||
|  | ||||
| <p>class DebouncedWidget extends NoteContextAwareWidget { | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.spacedUpdate = new SpacedUpdate(async () => { | ||||
|             await this.performUpdate(); | ||||
|         }, 500); // 500ms delay | ||||
|     } | ||||
|      | ||||
|     async handleInput(value) { | ||||
|         await this.spacedUpdate.scheduleUpdate(); | ||||
|     } | ||||
| } | ||||
| </code></pre></p> | ||||
|  | ||||
| <h3>Caching</h3> | ||||
| <pre><code class="language-typescript">class CachedWidget extends NoteContextAwareWidget { | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.cache = new Map(); | ||||
|     } | ||||
|      | ||||
|     async getProcessedData(noteId) { | ||||
|         if (!this.cache.has(noteId)) { | ||||
|             const data = await this.processExpensiveOperation(noteId); | ||||
|             this.cache.set(noteId, data); | ||||
|         } | ||||
|         return this.cache.get(noteId); | ||||
|     } | ||||
|      | ||||
|     cleanup() { | ||||
|         this.cache.clear(); | ||||
|     } | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h2>Debugging Widgets</h2> | ||||
|  | ||||
| <h3>Console Logging</h3> | ||||
| <pre><code class="language-typescript">class DebugWidget extends BasicWidget { | ||||
|     doRender() { | ||||
|         console.log('Widget rendering', this.componentId); | ||||
|         console.time('render'); | ||||
|          | ||||
|         this.$widget = $('<div>'); | ||||
|          | ||||
|         console.timeEnd('render'); | ||||
|     } | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h3>Error Handling</h3> | ||||
| <pre><code class="language-typescript">class SafeWidget extends NoteContextAwareWidget { | ||||
|     async refreshWithNote(note) { | ||||
|         try { | ||||
|             await this.riskyOperation(); | ||||
|         } catch (error) { | ||||
|             console.error('Widget error:', error); | ||||
|             this.logRenderingError(error); | ||||
|             this.$widget.html('<div class="error">Failed to load</div>'); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h3>Development Tools</h3> | ||||
| <pre><code class="language-typescript">class DevWidget extends BasicWidget { | ||||
|     doRender() { | ||||
|         this.$widget = $('<div>'); | ||||
|          | ||||
|         // Add debug information in development | ||||
|         if (window.glob.isDev) { | ||||
|             this.$widget.attr('data-debug', 'true'); | ||||
|             this.$widget.append(<code> | ||||
|                 <div class="debug-info"> | ||||
|                     Component ID: ${this.componentId} | ||||
|                     Position: ${this.position} | ||||
|                 </div> | ||||
|             </code>); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h2>Complete Example: Note Statistics Widget</h2> | ||||
|  | ||||
| <p>Here's a complete example implementing a custom note statistics widget:</p> | ||||
|  | ||||
| <pre><code class="language-typescript">import RightPanelWidget from "../widgets/right_panel_widget.js"; | ||||
| import server from "../services/server.js"; | ||||
| import froca from "../services/froca.js"; | ||||
| import toastService from "../services/toast.js"; | ||||
| import SpacedUpdate from "../services/spaced_update.js"; | ||||
|  | ||||
| <p>class NoteStatisticsWidget extends RightPanelWidget { | ||||
|     constructor() { | ||||
|         super(); | ||||
|          | ||||
|         // Initialize state | ||||
|         this.statistics = { | ||||
|             words: 0, | ||||
|             characters: 0, | ||||
|             paragraphs: 0, | ||||
|             readingTime: 0, | ||||
|             links: 0, | ||||
|             images: 0 | ||||
|         }; | ||||
|          | ||||
|         // Debounce updates for performance | ||||
|         this.spacedUpdate = new SpacedUpdate(async () => { | ||||
|             await this.calculateStatistics(); | ||||
|         }, 300); | ||||
|     } | ||||
|      | ||||
|     get widgetTitle() { | ||||
|         return "Note Statistics"; | ||||
|     } | ||||
|      | ||||
|     get help() { | ||||
|         return { | ||||
|             title: "Note Statistics", | ||||
|             text: "Displays various statistics about the current note including word count, reading time, and more." | ||||
|         }; | ||||
|     } | ||||
|      | ||||
|     async doRenderBody() { | ||||
|         this.$body.html(<code> | ||||
|             <div class="note-statistics"> | ||||
|                 <div class="stat-group"> | ||||
|                     <h5>Content</h5> | ||||
|                     <div class="stat-item"> | ||||
|                         <span class="stat-label">Words:</span> | ||||
|                         <span class="stat-value" data-stat="words">0</span> | ||||
|                     </div> | ||||
|                     <div class="stat-item"> | ||||
|                         <span class="stat-label">Characters:</span> | ||||
|                         <span class="stat-value" data-stat="characters">0</span> | ||||
|                     </div> | ||||
|                     <div class="stat-item"> | ||||
|                         <span class="stat-label">Paragraphs:</span> | ||||
|                         <span class="stat-value" data-stat="paragraphs">0</span> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                  | ||||
|                 <div class="stat-group"> | ||||
|                     <h5>Reading</h5> | ||||
|                     <div class="stat-item"> | ||||
|                         <span class="stat-label">Reading time:</span> | ||||
|                         <span class="stat-value" data-stat="readingTime">0 min</span> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                  | ||||
|                 <div class="stat-group"> | ||||
|                     <h5>Elements</h5> | ||||
|                     <div class="stat-item"> | ||||
|                         <span class="stat-label">Links:</span> | ||||
|                         <span class="stat-value" data-stat="links">0</span> | ||||
|                     </div> | ||||
|                     <div class="stat-item"> | ||||
|                         <span class="stat-label">Images:</span> | ||||
|                         <span class="stat-value" data-stat="images">0</span> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                  | ||||
|                 <div class="stat-actions"> | ||||
|                     <button class="btn btn-sm refresh-stats">Refresh</button> | ||||
|                     <button class="btn btn-sm export-stats">Export</button> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </code>); | ||||
|          | ||||
|         this.cssBlock(<code> | ||||
|             .note-statistics { | ||||
|                 padding: 10px; | ||||
|             } | ||||
|              | ||||
|             .stat-group { | ||||
|                 margin-bottom: 15px; | ||||
|                 padding-bottom: 15px; | ||||
|                 border-bottom: 1px solid var(--main-border-color); | ||||
|             } | ||||
|              | ||||
|             .stat-group:last-child { | ||||
|                 border-bottom: none; | ||||
|             } | ||||
|              | ||||
|             .stat-group h5 { | ||||
|                 margin: 0 0 10px 0; | ||||
|                 color: var(--muted-text-color); | ||||
|                 font-size: 12px; | ||||
|                 text-transform: uppercase; | ||||
|                 letter-spacing: 0.5px; | ||||
|             } | ||||
|              | ||||
|             .stat-item { | ||||
|                 display: flex; | ||||
|                 justify-content: space-between; | ||||
|                 padding: 5px 0; | ||||
|             } | ||||
|              | ||||
|             .stat-label { | ||||
|                 color: var(--main-text-color); | ||||
|             } | ||||
|              | ||||
|             .stat-value { | ||||
|                 font-weight: 600; | ||||
|                 color: var(--primary-color); | ||||
|             } | ||||
|              | ||||
|             .stat-actions { | ||||
|                 margin-top: 15px; | ||||
|                 display: flex; | ||||
|                 gap: 10px; | ||||
|             } | ||||
|              | ||||
|             .stat-actions .btn { | ||||
|                 flex: 1; | ||||
|             } | ||||
|         </code>); | ||||
|          | ||||
|         // Bind events | ||||
|         this.$body.on('click', '.refresh-stats', () => this.handleRefresh()); | ||||
|         this.$body.on('click', '.export-stats', () => this.handleExport()); | ||||
|     } | ||||
|      | ||||
|     async refreshWithNote(note) { | ||||
|         if (!note) { | ||||
|             this.clearStatistics(); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         // Schedule statistics calculation | ||||
|         await this.spacedUpdate.scheduleUpdate(); | ||||
|     } | ||||
|      | ||||
|     async calculateStatistics() { | ||||
|         try { | ||||
|             const note = this.note; | ||||
|             if (!note) return; | ||||
|              | ||||
|             const content = await note.getContent(); | ||||
|              | ||||
|             if (note.type === 'text') { | ||||
|                 // Parse HTML content | ||||
|                 const $content = $('<div>').html(content); | ||||
|                 const textContent = $content.text(); | ||||
|                  | ||||
|                 // Calculate statistics | ||||
|                 this.statistics.words = this.countWords(textContent); | ||||
|                 this.statistics.characters = textContent.length; | ||||
|                 this.statistics.paragraphs = $content.find('p').length; | ||||
|                 this.statistics.readingTime = Math.ceil(this.statistics.words / 200); | ||||
|                 this.statistics.links = $content.find('a').length; | ||||
|                 this.statistics.images = $content.find('img').length; | ||||
|             } else if (note.type === 'code') { | ||||
|                 // For code notes, count lines and characters | ||||
|                 const lines = content.split('\n'); | ||||
|                 this.statistics.words = lines.length; // Show lines instead of words | ||||
|                 this.statistics.characters = content.length; | ||||
|                 this.statistics.paragraphs = 0; | ||||
|                 this.statistics.readingTime = 0; | ||||
|                 this.statistics.links = 0; | ||||
|                 this.statistics.images = 0; | ||||
|             } | ||||
|              | ||||
|             this.updateDisplay(); | ||||
|              | ||||
|         } catch (error) { | ||||
|             console.error('Failed to calculate statistics:', error); | ||||
|             toastService.showError("Failed to calculate statistics"); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     countWords(text) { | ||||
|         const words = text.match(/\b\w+\b/g); | ||||
|         return words ? words.length : 0; | ||||
|     } | ||||
|      | ||||
|     clearStatistics() { | ||||
|         this.statistics = { | ||||
|             words: 0, | ||||
|             characters: 0, | ||||
|             paragraphs: 0, | ||||
|             readingTime: 0, | ||||
|             links: 0, | ||||
|             images: 0 | ||||
|         }; | ||||
|         this.updateDisplay(); | ||||
|     } | ||||
|      | ||||
|     updateDisplay() { | ||||
|         this.$body.find('[data-stat="words"]').text(this.statistics.words); | ||||
|         this.$body.find('[data-stat="characters"]').text(this.statistics.characters); | ||||
|         this.$body.find('[data-stat="paragraphs"]').text(this.statistics.paragraphs); | ||||
|         this.$body.find('[data-stat="readingTime"]').text(<code>${this.statistics.readingTime} min</code>); | ||||
|         this.$body.find('[data-stat="links"]').text(this.statistics.links); | ||||
|         this.$body.find('[data-stat="images"]').text(this.statistics.images); | ||||
|     } | ||||
|      | ||||
|     async handleRefresh() { | ||||
|         await this.calculateStatistics(); | ||||
|         toastService.showMessage("Statistics refreshed"); | ||||
|     } | ||||
|      | ||||
|     async handleExport() { | ||||
|         const note = this.note; | ||||
|         if (!note) return; | ||||
|          | ||||
|         const exportData = { | ||||
|             noteId: note.noteId, | ||||
|             title: note.title, | ||||
|             statistics: this.statistics, | ||||
|             timestamp: new Date().toISOString() | ||||
|         }; | ||||
|          | ||||
|         // Create a CSV | ||||
|         const csv = [ | ||||
|             'Metric,Value', | ||||
|             <code>Words,${this.statistics.words}</code>, | ||||
|             <code>Characters,${this.statistics.characters}</code>, | ||||
|             <code>Paragraphs,${this.statistics.paragraphs}</code>, | ||||
|             <code>Reading Time,${this.statistics.readingTime} minutes</code>, | ||||
|             <code>Links,${this.statistics.links}</code>, | ||||
|             <code>Images,${this.statistics.images}</code> | ||||
|         ].join('\n'); | ||||
|          | ||||
|         // Download CSV | ||||
|         const blob = new Blob([csv], { type: 'text/csv' }); | ||||
|         const url = URL.createObjectURL(blob); | ||||
|         const a = document.createElement('a'); | ||||
|         a.href = url; | ||||
|         a.download = <code>statistics-${note.noteId}.csv</code>; | ||||
|         a.click(); | ||||
|         URL.revokeObjectURL(url); | ||||
|          | ||||
|         toastService.showMessage("Statistics exported"); | ||||
|     } | ||||
|      | ||||
|     async noteContentChangedEvent({ noteId }) { | ||||
|         if (this.noteId === noteId) { | ||||
|             await this.spacedUpdate.scheduleUpdate(); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     cleanup() { | ||||
|         this.$body.off('click'); | ||||
|         this.spacedUpdate = null; | ||||
|     } | ||||
| }</p> | ||||
|  | ||||
| <p>export default NoteStatisticsWidget; | ||||
| </code></pre></p> | ||||
|  | ||||
| <h2>Best Practices</h2> | ||||
|  | ||||
| <h3>1. Memory Management</h3> | ||||
| - Clean up event listeners in <code>cleanup()</code> | ||||
| - Clear caches and timers when widget is destroyed | ||||
| - Avoid circular references | ||||
|  | ||||
| <h3>2. Performance</h3> | ||||
| - Use debouncing for frequent updates | ||||
| - Implement lazy loading for expensive operations | ||||
| - Cache computed values when appropriate | ||||
|  | ||||
| <h3>3. Error Handling</h3> | ||||
| - Always wrap async operations in try-catch | ||||
| - Provide user feedback for errors | ||||
| - Log errors for debugging | ||||
|  | ||||
| <h3>4. User Experience</h3> | ||||
| - Show loading states for async operations | ||||
| - Provide clear error messages | ||||
| - Ensure widgets are responsive | ||||
|  | ||||
| <h3>5. Code Organization</h3> | ||||
| - Keep widgets focused on a single responsibility | ||||
| - Extract reusable logic into services | ||||
| - Use composition over inheritance when possible | ||||
|  | ||||
| <h2>Troubleshooting</h2> | ||||
|  | ||||
| <h3>Widget Not Rendering</h3> | ||||
| - Check <code>doRender()</code> creates <code>this.$widget</code> | ||||
| - Verify widget is properly registered | ||||
| - Check console for errors | ||||
|  | ||||
| <h3>Events Not Firing</h3> | ||||
| - Ensure event method name matches pattern: <code>${eventName}Event</code> | ||||
| - Check event is being triggered | ||||
| - Verify widget is active/visible | ||||
|  | ||||
| <h3>State Not Persisting</h3> | ||||
| - Use options or attributes for persistence | ||||
| - Check save operations complete successfully | ||||
| - Verify data serialization | ||||
|  | ||||
| <h3>Performance Issues</h3> | ||||
| - Profile with browser dev tools | ||||
| - Implement caching and debouncing | ||||
| - Optimize DOM operations | ||||
|  | ||||
| <h2>Next Steps</h2> | ||||
|  | ||||
| <p>- Explore existing widgets in <code>/apps/client/src/widgets/</code> for examples | ||||
| - Review the Frontend Script API documentation | ||||
| - Join the Trilium community for support and sharing widgets</p> | ||||
|     </div> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										1125
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Frontend Script Development.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1125
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Frontend Script Development.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1266
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Theme Development Guide.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1266
									
								
								apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Theme Development Guide.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1,119 @@ | ||||
| <h1>Anthropic Configuration Guide</h1> | ||||
|  | ||||
| <h2>Overview</h2> | ||||
| <p>Anthropic provides access to the Claude 3 family of models, known for their strong analytical capabilities, safety features, and large context windows. This guide will help you configure Anthropic as your AI provider in Trilium Notes.</p> | ||||
|  | ||||
| <h2>Getting Started</h2> | ||||
|  | ||||
| <h3>Step 1: Create an Anthropic Account</h3> | ||||
| <ol> | ||||
|   <li>Visit <a href="https://console.anthropic.com/signup" target="_blank">Anthropic Console</a></li> | ||||
|   <li>Sign up with your email address</li> | ||||
|   <li>Verify your email</li> | ||||
|   <li>Complete account setup and billing information</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Step 2: Generate an API Key</h3> | ||||
| <ol> | ||||
|   <li>Log into the <a href="https://console.anthropic.com/" target="_blank">Anthropic Console</a></li> | ||||
|   <li>Navigate to <strong>API Keys</strong> section</li> | ||||
|   <li>Click <strong>"Create Key"</strong></li> | ||||
|   <li>Name your key (e.g., "Trilium Integration")</li> | ||||
|   <li><strong>Important:</strong> Copy and save the key immediately</li> | ||||
|   <li>Store securely - the key won't be shown again</li> | ||||
| </ol> | ||||
|  | ||||
| <div class="admonition warning"> | ||||
|   <p class="admonition-title">Security Notice</p> | ||||
|   <p>Your API key provides full access to your Anthropic account. Never share it publicly or commit it to version control.</p> | ||||
| </div> | ||||
|  | ||||
| <h3>Step 3: Configure in Trilium</h3> | ||||
| <ol> | ||||
|   <li>Open Trilium Notes</li> | ||||
|   <li>Go to <strong>Options <20> AI/LLM</strong></li> | ||||
|   <li>Enable AI features</li> | ||||
|   <li>Select <strong>Anthropic</strong> from the provider dropdown</li> | ||||
|   <li>Enter your configuration: | ||||
|     <ul> | ||||
|       <li><strong>API Key:</strong> Your Anthropic API key (sk-ant-...)</li> | ||||
|       <li><strong>Base URL:</strong> <code>https://api.anthropic.com</code> (default)</li> | ||||
|       <li><strong>Default Model:</strong> Choose from available Claude models</li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li>Click <strong>Test Connection</strong> to verify</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Available Models</h2> | ||||
|  | ||||
| <h3>Claude 3 Model Family</h3> | ||||
| <table class="table table-bordered"> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th>Model</th> | ||||
|       <th>Best For</th> | ||||
|       <th>Context Window</th> | ||||
|       <th>Speed</th> | ||||
|       <th>Cost (per 1M tokens)</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr> | ||||
|       <td><code>claude-3-opus-20240229</code></td> | ||||
|       <td>Most capable, complex analysis, research</td> | ||||
|       <td>200,000 tokens</td> | ||||
|       <td>Slower</td> | ||||
|       <td>$15 input / $75 output</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>claude-3-sonnet-20240229</code></td> | ||||
|       <td>Balanced performance, general use</td> | ||||
|       <td>200,000 tokens</td> | ||||
|       <td>Medium</td> | ||||
|       <td>$3 input / $15 output</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>claude-3-haiku-20240307</code></td> | ||||
|       <td>Fast responses, simple tasks</td> | ||||
|       <td>200,000 tokens</td> | ||||
|       <td>Fastest</td> | ||||
|       <td>$0.25 input / $1.25 output</td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <h2>Configuration Options</h2> | ||||
|  | ||||
| <h3>Model Parameters</h3> | ||||
| <ul> | ||||
|   <li><strong>Temperature (0.0-1.0):</strong> Controls response randomness | ||||
|     <ul> | ||||
|       <li>0.0-0.3: Precise, consistent responses</li> | ||||
|       <li>0.4-0.7: Balanced creativity and accuracy</li> | ||||
|       <li>0.8-1.0: Creative, varied outputs</li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li><strong>Max Tokens:</strong> Maximum response length | ||||
|     <ul> | ||||
|       <li>Default: 4096 tokens</li> | ||||
|       <li>Maximum: Model-dependent (up to 200K)</li> | ||||
|     </ul> | ||||
|   </li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Best Practices</h2> | ||||
|  | ||||
| <h3>Prompt Engineering for Claude</h3> | ||||
| <ul> | ||||
|   <li><strong>Be Direct:</strong> Claude responds well to clear, direct instructions</li> | ||||
|   <li><strong>Use XML Tags:</strong> Structure complex prompts with XML-like tags for better organization</li> | ||||
|   <li><strong>Provide Examples:</strong> Claude excels at following patterns from examples</li> | ||||
|   <li><strong>Think Step-by-Step:</strong> For complex tasks, ask Claude to reason through steps</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Additional Resources</h2> | ||||
| <ul> | ||||
|   <li><a href="https://docs.anthropic.com/" target="_blank">Anthropic API Documentation</a></li> | ||||
|   <li><a href="https://console.anthropic.com/" target="_blank">Anthropic Console</a></li> | ||||
|   <li><a href="https://www.anthropic.com/claude" target="_blank">Claude Model Information</a></li> | ||||
| </ul> | ||||
| @@ -0,0 +1,255 @@ | ||||
| <h1>OpenAI Configuration Guide</h1> | ||||
|  | ||||
| <h2>Overview</h2> | ||||
| <p>OpenAI provides access to GPT-4, GPT-3.5-turbo, and other advanced language models through their API. This guide will help you set up and configure OpenAI as your AI provider in Trilium Notes.</p> | ||||
|  | ||||
| <h2>Getting Started</h2> | ||||
|  | ||||
| <h3>Step 1: Create an OpenAI Account</h3> | ||||
| <ol> | ||||
|   <li>Visit <a href="https://platform.openai.com/signup" target="_blank">OpenAI Platform</a></li> | ||||
|   <li>Sign up with your email or Google/Microsoft account</li> | ||||
|   <li>Verify your email address</li> | ||||
|   <li>Complete your profile information</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Step 2: Obtain an API Key</h3> | ||||
| <ol> | ||||
|   <li>Navigate to <a href="https://platform.openai.com/api-keys" target="_blank">API Keys</a> in your OpenAI account</li> | ||||
|   <li>Click <strong>"Create new secret key"</strong></li> | ||||
|   <li>Give your key a descriptive name (e.g., "Trilium Notes")</li> | ||||
|   <li><strong>Important:</strong> Copy the key immediately - it won't be shown again!</li> | ||||
|   <li>Store the key securely (password manager recommended)</li> | ||||
| </ol> | ||||
|  | ||||
| <div class="admonition warning"> | ||||
|   <p class="admonition-title">Security Note</p> | ||||
|   <p>Never share your API key or commit it to version control. Treat it like a password.</p> | ||||
| </div> | ||||
|  | ||||
| <h3>Step 3: Configure in Trilium</h3> | ||||
| <ol> | ||||
|   <li>Open Trilium Notes</li> | ||||
|   <li>Navigate to <strong>Options <20> AI/LLM</strong></li> | ||||
|   <li>Enable AI features if not already enabled</li> | ||||
|   <li>Select <strong>OpenAI</strong> from the provider dropdown</li> | ||||
|   <li>Enter your configuration: | ||||
|     <ul> | ||||
|       <li><strong>API Key:</strong> Paste your OpenAI API key</li> | ||||
|       <li><strong>Base URL:</strong> <code>https://api.openai.com/v1</code> (default)</li> | ||||
|       <li><strong>Default Model:</strong> Select from available models (see below)</li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li>Click <strong>Test Connection</strong> to verify setup</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Available Models</h2> | ||||
|  | ||||
| <h3>Chat Models</h3> | ||||
| <table class="table table-bordered"> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th>Model</th> | ||||
|       <th>Best For</th> | ||||
|       <th>Context Window</th> | ||||
|       <th>Cost (per 1K tokens)</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr> | ||||
|       <td><code>gpt-4-turbo-preview</code></td> | ||||
|       <td>Complex reasoning, analysis, latest knowledge</td> | ||||
|       <td>128,000 tokens</td> | ||||
|       <td>$0.01 input / $0.03 output</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>gpt-4</code></td> | ||||
|       <td>High-quality responses, complex tasks</td> | ||||
|       <td>8,192 tokens</td> | ||||
|       <td>$0.03 input / $0.06 output</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>gpt-4-32k</code></td> | ||||
|       <td>Long documents, extensive context</td> | ||||
|       <td>32,768 tokens</td> | ||||
|       <td>$0.06 input / $0.12 output</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>gpt-3.5-turbo</code></td> | ||||
|       <td>Quick responses, general use, cost-effective</td> | ||||
|       <td>16,385 tokens</td> | ||||
|       <td>$0.0005 input / $0.0015 output</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>gpt-3.5-turbo-16k</code></td> | ||||
|       <td>Longer conversations, more context</td> | ||||
|       <td>16,385 tokens</td> | ||||
|       <td>$0.003 input / $0.004 output</td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <h3>Embedding Models</h3> | ||||
| <table class="table table-bordered"> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th>Model</th> | ||||
|       <th>Dimensions</th> | ||||
|       <th>Performance</th> | ||||
|       <th>Cost (per 1M tokens)</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr> | ||||
|       <td><code>text-embedding-3-small</code></td> | ||||
|       <td>1,536</td> | ||||
|       <td>Good, cost-effective</td> | ||||
|       <td>$0.02</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>text-embedding-3-large</code></td> | ||||
|       <td>3,072</td> | ||||
|       <td>Best quality</td> | ||||
|       <td>$0.13</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>text-embedding-ada-002</code></td> | ||||
|       <td>1,536</td> | ||||
|       <td>Legacy, still supported</td> | ||||
|       <td>$0.10</td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <h2>Configuration Options</h2> | ||||
|  | ||||
| <h3>Model Parameters</h3> | ||||
| <ul> | ||||
|   <li><strong>Temperature (0.0-2.0):</strong> Controls randomness. Lower = more focused, Higher = more creative | ||||
|     <ul> | ||||
|       <li>0.0-0.3: Factual, deterministic responses</li> | ||||
|       <li>0.4-0.7: Balanced (recommended)</li> | ||||
|       <li>0.8-1.0: Creative, varied responses</li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li><strong>Max Tokens:</strong> Maximum response length (1 token H 0.75 words) | ||||
|     <ul> | ||||
|       <li>Default: 4000 tokens</li> | ||||
|       <li>Adjust based on needs and cost considerations</li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li><strong>Top P (0.0-1.0):</strong> Nucleus sampling threshold | ||||
|     <ul> | ||||
|       <li>Default: 1.0 (consider all tokens)</li> | ||||
|       <li>Lower values = more focused responses</li> | ||||
|     </ul> | ||||
|   </li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Advanced Settings</h3> | ||||
| <h4>Custom Endpoints</h4> | ||||
| <p>For Azure OpenAI or OpenAI-compatible services:</p> | ||||
| <pre><code>Base URL: https://your-resource.openai.azure.com/ | ||||
| API Version: 2024-02-15-preview | ||||
| Deployment Name: your-deployment-name</code></pre> | ||||
|  | ||||
| <h4>Rate Limiting</h4> | ||||
| <p>Configure to avoid hitting API limits:</p> | ||||
| <ul> | ||||
|   <li>Tier 1: 60 requests/minute, 200,000 tokens/minute</li> | ||||
|   <li>Tier 2: 120 requests/minute, 400,000 tokens/minute</li> | ||||
|   <li>Higher tiers available upon request</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Cost Management</h2> | ||||
|  | ||||
| <h3>Estimating Costs</h3> | ||||
| <p>Typical usage patterns and estimated monthly costs:</p> | ||||
| <ul> | ||||
|   <li><strong>Light Use (Personal):</strong> ~$5-10/month | ||||
|     <ul> | ||||
|       <li>50 queries/day with GPT-3.5-turbo</li> | ||||
|       <li>Basic embeddings for 1000 notes</li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li><strong>Regular Use (Professional):</strong> ~$20-50/month | ||||
|     <ul> | ||||
|       <li>100 queries/day with mix of GPT-4 and GPT-3.5</li> | ||||
|       <li>Comprehensive embeddings for 5000 notes</li> | ||||
|     </ul> | ||||
|   </li> | ||||
|   <li><strong>Heavy Use (Research/Business):</strong> ~$100+/month | ||||
|     <ul> | ||||
|       <li>200+ queries/day primarily with GPT-4</li> | ||||
|       <li>Large-scale embeddings and regular updates</li> | ||||
|     </ul> | ||||
|   </li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Cost Optimization Tips</h3> | ||||
| <ol> | ||||
|   <li><strong>Use GPT-3.5-turbo for simple queries</strong> - 60x cheaper than GPT-4</li> | ||||
|   <li><strong>Enable response caching</strong> - Avoid repeated API calls</li> | ||||
|   <li><strong>Set token limits</strong> - Prevent unexpectedly long responses</li> | ||||
|   <li><strong>Use text-embedding-3-small</strong> - Good quality at low cost</li> | ||||
|   <li><strong>Monitor usage</strong> - Check OpenAI dashboard regularly</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Troubleshooting</h2> | ||||
|  | ||||
| <h3>Common Issues</h3> | ||||
|  | ||||
| <div class="admonition info"> | ||||
|   <p class="admonition-title">Invalid API Key</p> | ||||
|   <p><strong>Solution:</strong> Verify the key is copied correctly without spaces. Check it hasn't been revoked in your OpenAI dashboard.</p> | ||||
| </div> | ||||
|  | ||||
| <div class="admonition info"> | ||||
|   <p class="admonition-title">Rate Limit Exceeded</p> | ||||
|   <p><strong>Solution:</strong> Wait a few minutes and retry. Consider upgrading your API tier or implementing request throttling.</p> | ||||
| </div> | ||||
|  | ||||
| <div class="admonition info"> | ||||
|   <p class="admonition-title">Model Not Found</p> | ||||
|   <p><strong>Solution:</strong> Ensure you have access to the model. GPT-4 requires separate approval from OpenAI.</p> | ||||
| </div> | ||||
|  | ||||
| <div class="admonition info"> | ||||
|   <p class="admonition-title">Connection Timeout</p> | ||||
|   <p><strong>Solution:</strong> Check your internet connection and firewall settings. Ensure port 443 is open for HTTPS.</p> | ||||
| </div> | ||||
|  | ||||
| <h2>Best Practices</h2> | ||||
|  | ||||
| <h3>Security</h3> | ||||
| <ul> | ||||
|   <li>Rotate API keys monthly</li> | ||||
|   <li>Use separate keys for development and production</li> | ||||
|   <li>Monitor usage for unusual activity</li> | ||||
|   <li>Set spending limits in OpenAI dashboard</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Performance</h3> | ||||
| <ul> | ||||
|   <li>Start with smaller models and upgrade as needed</li> | ||||
|   <li>Use streaming for better perceived performance</li> | ||||
|   <li>Implement retry logic with exponential backoff</li> | ||||
|   <li>Cache frequently requested information</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Quality</h3> | ||||
| <ul> | ||||
|   <li>Provide clear, specific prompts</li> | ||||
|   <li>Use system prompts to set behavior</li> | ||||
|   <li>Include examples in prompts when needed</li> | ||||
|   <li>Test different temperature settings</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Additional Resources</h2> | ||||
| <ul> | ||||
|   <li><a href="https://platform.openai.com/docs" target="_blank">OpenAI API Documentation</a></li> | ||||
|   <li><a href="https://platform.openai.com/usage" target="_blank">Usage Dashboard</a></li> | ||||
|   <li><a href="https://openai.com/pricing" target="_blank">Pricing Calculator</a></li> | ||||
|   <li><a href="https://status.openai.com/" target="_blank">API Status Page</a></li> | ||||
|   <li><a href="https://cookbook.openai.com/" target="_blank">OpenAI Cookbook</a></li> | ||||
| </ul> | ||||
							
								
								
									
										274
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI/Features and Usage.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										274
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI/Features and Usage.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,274 @@ | ||||
| <h1>AI Features and Usage Guide</h1> | ||||
|  | ||||
| <h2>Overview</h2> | ||||
| <p>This guide explains how to effectively use AI features in Trilium Notes to enhance your knowledge management workflow.</p> | ||||
|  | ||||
| <h2>Chat with Notes</h2> | ||||
|  | ||||
| <h3>Getting Started</h3> | ||||
| <p>The Chat with Notes feature provides an interactive AI assistant that has access to your knowledge base.</p> | ||||
|  | ||||
| <h4>Accessing the Chat Interface</h4> | ||||
| <ol> | ||||
|   <li><strong>Via Launcher Button:</strong> Click the "Chat with Notes" button in the left sidebar</li> | ||||
|   <li><strong>Via Keyboard Shortcut:</strong> Press <code>Ctrl+Shift+C</code> (customizable)</li> | ||||
|   <li><strong>Via Command Palette:</strong> Press <code>Ctrl+P</code> and type "chat"</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Basic Chat Operations</h3> | ||||
|  | ||||
| <h4>Asking Questions</h4> | ||||
| <p>The AI can answer questions based on your notes:</p> | ||||
| <pre><code>User: What are the main points from my meeting notes this week? | ||||
| AI: Based on your meeting notes from this week, here are the main points: | ||||
| - Monday: Project timeline discussed, deadline set for March 30 | ||||
| - Wednesday: Budget approved for Q2 | ||||
| - Friday: New team member onboarding planned</code></pre> | ||||
|  | ||||
| <h4>Summarizing Content</h4> | ||||
| <pre><code>User: Summarize my research notes on machine learning | ||||
| AI: Your machine learning research covers: | ||||
| 1. Supervised learning algorithms (Random Forests, SVM) | ||||
| 2. Neural network architectures | ||||
| 3. Data preprocessing techniques | ||||
| 4. Model evaluation metrics</code></pre> | ||||
|  | ||||
| <h2>AI-Powered Tools</h2> | ||||
|  | ||||
| <h3>Available Tools</h3> | ||||
| <p>The AI can execute various tools to interact with your notes:</p> | ||||
|  | ||||
| <table class="table table-bordered"> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th>Tool</th> | ||||
|       <th>Function</th> | ||||
|       <th>Example Usage</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr> | ||||
|       <td><code>search_notes</code></td> | ||||
|       <td>Semantic search across notes</td> | ||||
|       <td>"Find all notes about project management"</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>keyword_search</code></td> | ||||
|       <td>Exact keyword matching</td> | ||||
|       <td>"Search for notes containing 'API documentation'"</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>read_note</code></td> | ||||
|       <td>Access full note content</td> | ||||
|       <td>"Show me the content of my budget proposal"</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>create_note</code></td> | ||||
|       <td>Generate new notes</td> | ||||
|       <td>"Create a note summarizing this discussion"</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>update_note</code></td> | ||||
|       <td>Modify existing notes</td> | ||||
|       <td>"Add these action items to my todo list"</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>manage_attributes</code></td> | ||||
|       <td>Handle note metadata</td> | ||||
|       <td>"Add #important label to my proposal"</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>calendar_integration</code></td> | ||||
|       <td>Work with date notes</td> | ||||
|       <td>"What's on my calendar tomorrow?"</td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <h3>Tool Execution Examples</h3> | ||||
|  | ||||
| <div class="admonition example"> | ||||
|   <p class="admonition-title">Research Assistant Example</p> | ||||
|   <pre><code>User: Help me research quantum computing for my presentation | ||||
|  | ||||
| AI: I'll help you research quantum computing. Let me search your notes. | ||||
| [Executing search_notes: "quantum computing"] | ||||
| Found 3 notes on quantum topics. | ||||
|  | ||||
| [Executing read_note: "Quantum Basics"] | ||||
| You have notes covering superposition and entanglement. | ||||
|  | ||||
| [Executing create_note: "Quantum Computing Presentation Outline"] | ||||
| Created outline with: | ||||
| 1. Introduction to quantum concepts | ||||
| 2. Current applications | ||||
| 3. Future possibilities</code></pre> | ||||
| </div> | ||||
|  | ||||
| <h2>Semantic Search</h2> | ||||
|  | ||||
| <h3>How It Works</h3> | ||||
| <p>Semantic search uses AI embeddings to find conceptually related content, even when exact keywords don't match.</p> | ||||
|  | ||||
| <h4>Benefits</h4> | ||||
| <ul> | ||||
|   <li>Find related concepts without exact keywords</li> | ||||
|   <li>Discover unexpected connections</li> | ||||
|   <li>More intuitive search results</li> | ||||
|   <li>Better context understanding</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Example Searches</h4> | ||||
| <pre><code>Query: "improving productivity" | ||||
| Finds: Time management, GTD methodology, focus strategies, tool optimization | ||||
|  | ||||
| Query: "customer satisfaction" | ||||
| Finds: User feedback, NPS scores, support tickets, product reviews</code></pre> | ||||
|  | ||||
| <h2>Content Generation</h2> | ||||
|  | ||||
| <h3>Note Creation</h3> | ||||
| <p>AI can help create various types of notes:</p> | ||||
|  | ||||
| <ul> | ||||
|   <li><strong>Meeting Summaries:</strong> "Create a summary of today's standup discussion"</li> | ||||
|   <li><strong>Research Outlines:</strong> "Generate an outline for my thesis on climate change"</li> | ||||
|   <li><strong>Documentation:</strong> "Create API documentation from these code snippets"</li> | ||||
|   <li><strong>Reports:</strong> "Generate a weekly progress report from my task notes"</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Content Enhancement</h3> | ||||
| <p>Improve existing notes with AI assistance:</p> | ||||
|  | ||||
| <ul> | ||||
|   <li><strong>Expansion:</strong> "Expand the bullet points in my ideas note"</li> | ||||
|   <li><strong>Clarification:</strong> "Rewrite this technical explanation for beginners"</li> | ||||
|   <li><strong>Formatting:</strong> "Convert my rough notes into a structured document"</li> | ||||
|   <li><strong>Translation:</strong> "Translate this note to Spanish"</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Workflow Integration</h2> | ||||
|  | ||||
| <h3>Daily Review Workflow</h3> | ||||
| <pre><code>User: Give me my daily review | ||||
|  | ||||
| AI: Daily Review for [Today's Date]: | ||||
| ✅ Completed Tasks: | ||||
| - Reviewed design mockups | ||||
| - Sent project update | ||||
| - Prepared agenda | ||||
|  | ||||
| 📝 Notes Created: | ||||
| - "Client Feedback" | ||||
| - "API Research" | ||||
| - "Team Notes" | ||||
|  | ||||
| 📅 Tomorrow: | ||||
| - Project review at 10 AM | ||||
| - Sprint planning at 2 PM</code></pre> | ||||
|  | ||||
| <h3>Research Workflow</h3> | ||||
| <ol> | ||||
|   <li><strong>Gather Information:</strong> "Find all notes about [topic]"</li> | ||||
|   <li><strong>Synthesize:</strong> "Create a summary of these research notes"</li> | ||||
|   <li><strong>Organize:</strong> "Suggest a structure for organizing this information"</li> | ||||
|   <li><strong>Generate:</strong> "Create a draft based on this research"</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Best Practices</h2> | ||||
|  | ||||
| <h3>Effective Prompting</h3> | ||||
|  | ||||
| <table class="table table-bordered"> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th>Instead of...</th> | ||||
|       <th>Try...</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr> | ||||
|       <td>"Tell me about my notes"</td> | ||||
|       <td>"Summarize my project management notes from Q1"</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td>"What should I do?"</td> | ||||
|       <td>"Based on my project timeline, what tasks should I prioritize?"</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td>"Give me information"</td> | ||||
|       <td>"Create a bullet-point summary of my research on AI"</td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <h3>Managing Conversations</h3> | ||||
| <ul> | ||||
|   <li><strong>Start fresh:</strong> Begin new chats for different topics</li> | ||||
|   <li><strong>Be specific:</strong> Reference note titles when possible</li> | ||||
|   <li><strong>Iterate:</strong> Use follow-up questions to refine responses</li> | ||||
|   <li><strong>Provide context:</strong> Include relevant background information</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Performance Tips</h3> | ||||
| <ul> | ||||
|   <li><strong>For speed:</strong> Use simpler models for basic tasks</li> | ||||
|   <li><strong>For quality:</strong> Provide examples of desired output</li> | ||||
|   <li><strong>For cost:</strong> Enable caching and batch similar queries</li> | ||||
|   <li><strong>For relevance:</strong> Keep your notes well-organized and labeled</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Advanced Usage Patterns</h2> | ||||
|  | ||||
| <h3>Creating Knowledge Graphs</h3> | ||||
| <pre><code>User: Create a knowledge graph of my programming notes | ||||
|  | ||||
| AI: Analyzing your programming notes... | ||||
| [Creates visual representation showing connections between: | ||||
| - Languages (Python, JavaScript, Java) | ||||
| - Frameworks (React, Django, Spring) | ||||
| - Concepts (OOP, Functional, Design Patterns) | ||||
| - Projects (Web App, API, Mobile)]</code></pre> | ||||
|  | ||||
| <h3>Automated Analysis</h3> | ||||
| <pre><code>User: Analyze sentiment in my journal entries this month | ||||
|  | ||||
| AI: Sentiment Analysis - Past 30 Days: | ||||
| 📈 Positive Trend: Mood improving over time | ||||
| 😊 Positive themes: achievements, learning, relationships | ||||
| 😟 Challenges: workload, deadlines | ||||
| 💡 Suggestion: Schedule breaks after intense work periods</code></pre> | ||||
|  | ||||
| <h2>Privacy and Control</h2> | ||||
|  | ||||
| <h3>Excluding Notes from AI</h3> | ||||
| <p>To prevent specific notes from being accessed by AI:</p> | ||||
| <ol> | ||||
|   <li>Add the <code>#excludeFromAI</code> label to sensitive notes</li> | ||||
|   <li>These notes won't be included in searches or context</li> | ||||
|   <li>Perfect for personal or confidential information</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Data Usage</h3> | ||||
| <ul> | ||||
|   <li>Only explicitly requested notes are sent to AI</li> | ||||
|   <li>No automatic uploads or background processing</li> | ||||
|   <li>You control what data the AI can access</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Troubleshooting Common Issues</h2> | ||||
|  | ||||
| <div class="admonition warning"> | ||||
|   <p class="admonition-title">AI Not Finding Relevant Notes</p> | ||||
|   <p><strong>Solution:</strong> Ensure embeddings are generated. Go to Settings → AI/LLM and click "Recreate All Embeddings".</p> | ||||
| </div> | ||||
|  | ||||
| <div class="admonition warning"> | ||||
|   <p class="admonition-title">Tools Not Executing</p> | ||||
|   <p><strong>Solution:</strong> Verify tool calling is enabled in your AI settings and your provider supports function calling.</p> | ||||
| </div> | ||||
|  | ||||
| <div class="admonition warning"> | ||||
|   <p class="admonition-title">Slow Responses</p> | ||||
|   <p><strong>Solution:</strong> Try using a faster model (GPT-3.5-turbo, Claude Haiku) or reduce the context size in settings.</p> | ||||
| </div> | ||||
							
								
								
									
										27
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI/Introduction.html
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										27
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI/Introduction.html
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -3,12 +3,27 @@ | ||||
|   height="1364"> | ||||
|   <figcaption>An example chat with an LLM</figcaption> | ||||
| </figure> | ||||
| <p>The AI / LLM features within Trilium Notes are designed to allow you to | ||||
|   interact with your Notes in a variety of ways, using as many of the major | ||||
|   providers as we can support. </p> | ||||
| <p>In addition to being able to send chats to LLM providers such as OpenAI, | ||||
|   Anthropic, and Ollama - we also support agentic tool calling, and embeddings.</p> | ||||
| <p>The quickest way to get started is to navigate to the “AI/LLM” settings:</p> | ||||
|  | ||||
| <h2>Overview</h2> | ||||
| <p>The AI / LLM features within Trilium Notes are designed to enhance your note-taking and knowledge management experience through intelligent search, content generation, and interactive assistance. These features integrate seamlessly with your personal knowledge base while maintaining complete control over your data.</p> | ||||
|  | ||||
| <h3>Key Capabilities</h3> | ||||
| <ul> | ||||
|   <li><strong>Chat with Notes</strong> - An interactive AI assistant that can answer questions based on your note content, provide summaries, and help discover connections</li> | ||||
|   <li><strong>Semantic Search</strong> - Find conceptually related notes even when exact keywords don't match</li> | ||||
|   <li><strong>Tool-Enabled Actions</strong> - AI can create, update, search, and manage your notes automatically</li> | ||||
|   <li><strong>Content Generation</strong> - Generate summaries, expand ideas, and assist with writing</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Supported Providers</h3> | ||||
| <p>We support multiple AI providers to give you flexibility in choosing between cloud and local options:</p> | ||||
| <ul> | ||||
|   <li><strong>OpenAI</strong> - GPT-4, GPT-3.5-turbo models with excellent general knowledge</li> | ||||
|   <li><strong>Anthropic</strong> - Claude 3 family with strong analytical capabilities</li> | ||||
|   <li><strong>Ollama</strong> - Run AI models locally for complete privacy and offline use</li> | ||||
| </ul> | ||||
|  | ||||
| <p>The quickest way to get started is to navigate to the "AI/LLM" settings:</p> | ||||
| <figure | ||||
| class="image image_resized" style="width:74.04%;"> | ||||
|   <img style="aspect-ratio:1916/1906;" src="5_Introduction_image.png" width="1916" | ||||
|   | ||||
							
								
								
									
										328
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI/Security and Privacy.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										328
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI/Security and Privacy.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,328 @@ | ||||
| <h1>Security and Privacy Guidelines</h1> | ||||
|  | ||||
| <h2>Overview</h2> | ||||
| <p>This document outlines important security considerations and privacy best practices for using AI features in Trilium Notes. Your privacy and data security are paramount.</p> | ||||
|  | ||||
| <h2>Data Privacy by Provider</h2> | ||||
|  | ||||
| <h3>Cloud Providers (OpenAI, Anthropic)</h3> | ||||
|  | ||||
| <div class="admonition info"> | ||||
|   <p class="admonition-title">What Gets Sent</p> | ||||
|   <ul> | ||||
|     <li>Selected note content based on your queries</li> | ||||
|     <li>Your questions and prompts</li> | ||||
|     <li>System instructions for AI behavior</li> | ||||
|     <li>Tool execution parameters</li> | ||||
|   </ul> | ||||
| </div> | ||||
|  | ||||
| <div class="admonition success"> | ||||
|   <p class="admonition-title">What Stays Private</p> | ||||
|   <ul> | ||||
|     <li>Notes marked with <code>#excludeFromAI</code> label</li> | ||||
|     <li>Encrypted note content (unless explicitly decrypted)</li> | ||||
|     <li>System metadata and file paths</li> | ||||
|     <li>Other users' data in multi-user setups</li> | ||||
|     <li>Your API keys and credentials</li> | ||||
|   </ul> | ||||
| </div> | ||||
|  | ||||
| <h4>Provider Data Policies</h4> | ||||
| <table class="table table-bordered"> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th>Provider</th> | ||||
|       <th>Data Usage</th> | ||||
|       <th>Retention</th> | ||||
|       <th>Compliance</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr> | ||||
|       <td>OpenAI</td> | ||||
|       <td>API data not used for training</td> | ||||
|       <td>30 days for abuse monitoring</td> | ||||
|       <td>SOC 2 Type II</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td>Anthropic</td> | ||||
|       <td>No training on API inputs</td> | ||||
|       <td>Limited retention period</td> | ||||
|       <td>SOC 2 Type II</td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <h3>Local Provider (Ollama)</h3> | ||||
|  | ||||
| <div class="admonition success"> | ||||
|   <p class="admonition-title">Complete Privacy with Ollama</p> | ||||
|   <ul> | ||||
|     <li>✅ No data leaves your machine</li> | ||||
|     <li>✅ No external API calls</li> | ||||
|     <li>✅ No usage tracking or telemetry</li> | ||||
|     <li>✅ Works completely offline</li> | ||||
|     <li>✅ You control all models and data</li> | ||||
|   </ul> | ||||
| </div> | ||||
|  | ||||
| <h2>Protecting Sensitive Information</h2> | ||||
|  | ||||
| <h3>Using the Exclusion System</h3> | ||||
|  | ||||
| <h4>Excluding Individual Notes</h4> | ||||
| <ol> | ||||
|   <li>Open the note you want to protect</li> | ||||
|   <li>Add the label: <code>#excludeFromAI</code></li> | ||||
|   <li>The note will be completely excluded from AI processing</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Bulk Exclusion Script</h4> | ||||
| <pre><code>// Script to exclude all notes in a folder | ||||
| const folder = api.getNoteWithLabel('confidential'); | ||||
| const descendants = folder.getDescendants(); | ||||
|  | ||||
| for (const note of descendants) { | ||||
|     note.addLabel('excludeFromAI'); | ||||
| } | ||||
|  | ||||
| api.showMessage(`Excluded ${descendants.length} notes from AI`);</code></pre> | ||||
|  | ||||
| <h4>Verifying Exclusions</h4> | ||||
| <p>To see which notes are excluded from AI:</p> | ||||
| <ol> | ||||
|   <li>Go to Search</li> | ||||
|   <li>Enter: <code>#excludeFromAI</code></li> | ||||
|   <li>Review the list of protected notes</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Content Filtering</h3> | ||||
|  | ||||
| <p>Trilium can automatically filter sensitive patterns:</p> | ||||
|  | ||||
| <table class="table table-bordered"> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th>Pattern Type</th> | ||||
|       <th>Example</th> | ||||
|       <th>Action</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr> | ||||
|       <td>Social Security Numbers</td> | ||||
|       <td>XXX-XX-XXXX</td> | ||||
|       <td>Automatically redacted</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td>Credit Card Numbers</td> | ||||
|       <td>16-digit numbers</td> | ||||
|       <td>Automatically redacted</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td>API Keys</td> | ||||
|       <td>Strings matching key patterns</td> | ||||
|       <td>Automatically redacted</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td>Passwords</td> | ||||
|       <td>password: fields</td> | ||||
|       <td>Automatically redacted</td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <h2>API Key Security</h2> | ||||
|  | ||||
| <h3>Best Practices</h3> | ||||
|  | ||||
| <div class="admonition warning"> | ||||
|   <p class="admonition-title">Never Do This</p> | ||||
|   <ul> | ||||
|     <li>❌ Share API keys in notes or messages</li> | ||||
|     <li>❌ Commit keys to version control</li> | ||||
|     <li>❌ Use the same key across multiple applications</li> | ||||
|     <li>❌ Store keys in plain text files</li> | ||||
|   </ul> | ||||
| </div> | ||||
|  | ||||
| <div class="admonition success"> | ||||
|   <p class="admonition-title">Always Do This</p> | ||||
|   <ul> | ||||
|     <li>✅ Store keys in Trilium's secure settings</li> | ||||
|     <li>✅ Rotate keys regularly (monthly recommended)</li> | ||||
|     <li>✅ Use separate keys for development/production</li> | ||||
|     <li>✅ Set spending limits in provider dashboards</li> | ||||
|     <li>✅ Monitor usage for unusual activity</li> | ||||
|   </ul> | ||||
| </div> | ||||
|  | ||||
| <h3>Key Rotation Schedule</h3> | ||||
| <ol> | ||||
|   <li><strong>Monthly:</strong> Rotate API keys</li> | ||||
|   <li><strong>Immediately:</strong> If key may be compromised</li> | ||||
|   <li><strong>Quarterly:</strong> Review and audit all keys</li> | ||||
|   <li><strong>Annually:</strong> Full security review</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Network Security</h2> | ||||
|  | ||||
| <h3>Secure Connections</h3> | ||||
| <ul> | ||||
|   <li>All API communications use HTTPS/TLS encryption</li> | ||||
|   <li>Certificate verification is enabled by default</li> | ||||
|   <li>Minimum TLS version 1.2 required</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Firewall Considerations</h3> | ||||
| <p>Required ports for AI providers:</p> | ||||
| <ul> | ||||
|   <li><strong>OpenAI/Anthropic:</strong> Port 443 (HTTPS)</li> | ||||
|   <li><strong>Ollama:</strong> Port 11434 (local only by default)</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Compliance and Regulations</h2> | ||||
|  | ||||
| <h3>GDPR Compliance</h3> | ||||
|  | ||||
| <h4>Your Rights</h4> | ||||
| <ul> | ||||
|   <li><strong>Right to Access:</strong> Export all AI-related data</li> | ||||
|   <li><strong>Right to Deletion:</strong> Remove AI chat history and embeddings</li> | ||||
|   <li><strong>Right to Portability:</strong> Export data in standard formats</li> | ||||
|   <li><strong>Right to Restriction:</strong> Limit AI processing of your data</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Data Minimization</h4> | ||||
| <ul> | ||||
|   <li>Send only necessary data to AI providers</li> | ||||
|   <li>Regularly clean up old chat sessions</li> | ||||
|   <li>Delete unused embeddings</li> | ||||
|   <li>Implement retention policies</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Healthcare Data (HIPAA)</h3> | ||||
|  | ||||
| <div class="admonition warning"> | ||||
|   <p class="admonition-title">For Healthcare Professionals</p> | ||||
|   <p>If handling protected health information (PHI):</p> | ||||
|   <ul> | ||||
|     <li>Use Ollama (local) exclusively - no cloud providers</li> | ||||
|     <li>Or ensure Business Associate Agreement (BAA) with provider</li> | ||||
|     <li>Enable maximum audit logging</li> | ||||
|     <li>Implement additional encryption</li> | ||||
|   </ul> | ||||
| </div> | ||||
|  | ||||
| <h2>Security Configurations by Use Case</h2> | ||||
|  | ||||
| <h3>Personal Use (Maximum Privacy)</h3> | ||||
| <pre><code>Provider: Ollama (local) | ||||
| Model: llama3 or mistral | ||||
| Embeddings: mxbai-embed-large | ||||
| Network: Localhost only | ||||
| Exclusions: Personal notes labeled</code></pre> | ||||
|  | ||||
| <h3>Professional Use (Balanced)</h3> | ||||
| <pre><code>Provider: OpenAI or Anthropic | ||||
| Model: GPT-4 or Claude Sonnet | ||||
| API Keys: Rotated monthly | ||||
| Exclusions: Confidential projects | ||||
| Audit: Logging enabled</code></pre> | ||||
|  | ||||
| <h3>Enterprise Use (Maximum Security)</h3> | ||||
| <pre><code>Provider: Azure OpenAI (private instance) | ||||
| Authentication: SSO + MFA | ||||
| Network: VPN required | ||||
| Audit: Full logging to SIEM | ||||
| DLP: Content scanning enabled</code></pre> | ||||
|  | ||||
| <h2>Incident Response</h2> | ||||
|  | ||||
| <h3>If API Key is Compromised</h3> | ||||
|  | ||||
| <div class="admonition danger"> | ||||
|   <p class="admonition-title">Immediate Actions Required</p> | ||||
|   <ol> | ||||
|     <li><strong>Revoke the key immediately</strong> in provider dashboard</li> | ||||
|     <li><strong>Generate new key</strong> and update in Trilium</li> | ||||
|     <li><strong>Review usage logs</strong> for unauthorized activity</li> | ||||
|     <li><strong>Check billing</strong> for unexpected charges</li> | ||||
|     <li><strong>Document the incident</strong> for future reference</li> | ||||
|   </ol> | ||||
| </div> | ||||
|  | ||||
| <h3>If Sensitive Data Was Sent</h3> | ||||
| <ol> | ||||
|   <li>Contact the AI provider's support team</li> | ||||
|   <li>Request data deletion if possible</li> | ||||
|   <li>Add affected notes to exclusion list</li> | ||||
|   <li>Review and update security practices</li> | ||||
|   <li>Consider switching to local AI (Ollama)</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Security Checklist</h2> | ||||
|  | ||||
| <h3>Initial Setup</h3> | ||||
| <ul class="checklist"> | ||||
|   <li>☐ Reviewed provider privacy policies</li> | ||||
|   <li>☐ Created strong, unique API keys</li> | ||||
|   <li>☐ Configured exclusion labels for sensitive notes</li> | ||||
|   <li>☐ Tested security configuration</li> | ||||
|   <li>☐ Set up spending limits</li> | ||||
|   <li>☐ Enabled audit logging</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Ongoing Maintenance</h3> | ||||
| <ul class="checklist"> | ||||
|   <li>☐ Rotate API keys monthly</li> | ||||
|   <li>☐ Review audit logs weekly</li> | ||||
|   <li>☐ Update exclusion lists as needed</li> | ||||
|   <li>☐ Monitor usage and costs</li> | ||||
|   <li>☐ Check for security updates</li> | ||||
|   <li>☐ Verify no sensitive data in logs</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Privacy-First Recommendations</h2> | ||||
|  | ||||
| <h3>For Maximum Privacy</h3> | ||||
| <p><strong>Use Ollama exclusively:</strong></p> | ||||
| <ul> | ||||
|   <li>Complete data control</li> | ||||
|   <li>No external dependencies</li> | ||||
|   <li>Works offline</li> | ||||
|   <li>No usage tracking</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>For Convenience with Privacy</h3> | ||||
| <p><strong>Hybrid approach:</strong></p> | ||||
| <ul> | ||||
|   <li>Ollama for sensitive content</li> | ||||
|   <li>Cloud providers for general queries</li> | ||||
|   <li>Strict exclusion labels</li> | ||||
|   <li>Regular key rotation</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>For Teams and Organizations</h3> | ||||
| <p><strong>Enterprise configuration:</strong></p> | ||||
| <ul> | ||||
|   <li>Private AI instances (Azure OpenAI)</li> | ||||
|   <li>Centralized key management</li> | ||||
|   <li>Audit logging to SIEM</li> | ||||
|   <li>DLP integration</li> | ||||
|   <li>Regular security training</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Additional Resources</h2> | ||||
| <ul> | ||||
|   <li><a href="https://openai.com/policies/privacy-policy" target="_blank">OpenAI Privacy Policy</a></li> | ||||
|   <li><a href="https://www.anthropic.com/privacy" target="_blank">Anthropic Privacy Policy</a></li> | ||||
|   <li><a href="https://gdpr.eu/" target="_blank">GDPR Information</a></li> | ||||
|   <li><a href="https://www.hhs.gov/hipaa/index.html" target="_blank">HIPAA Guidelines</a></li> | ||||
| </ul> | ||||
|  | ||||
| <div class="admonition tip"> | ||||
|   <p class="admonition-title">Remember</p> | ||||
|   <p>Security and privacy require ongoing attention. Start with the most restrictive settings and gradually relax them only as needed. When in doubt, prefer local processing with Ollama for sensitive data.</p> | ||||
| </div> | ||||
							
								
								
									
										264
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Bulk Operations.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Bulk Operations.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,264 @@ | ||||
| <h1>Bulk Operations</h1> | ||||
|  | ||||
| <p>Execute actions on multiple notes simultaneously to save time and ensure consistency across your note collection.</p> | ||||
|  | ||||
| <h2>Quick Start</h2> | ||||
|  | ||||
| <p>Access bulk operations through:</p> | ||||
| <ul> | ||||
|     <li>Search results menu → "Bulk Actions"</li> | ||||
|     <li>Select multiple notes → Right-click → "Bulk Operations"</li> | ||||
|     <li>Script API: <code>api.executeBulkActions(noteIds, actions)</code></li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Available Operations</h2> | ||||
|  | ||||
| <h3>Note Operations</h3> | ||||
|  | ||||
| <div class="operation-card"> | ||||
|     <h4>Move Notes</h4> | ||||
|     <p>Relocate multiple notes to a new parent location.</p> | ||||
|     <pre><code>{ | ||||
|   "name": "moveNote", | ||||
|   "targetParentNoteId": "target_note_id" | ||||
| }</code></pre> | ||||
|     <p class="note">Notes with multiple parents will be cloned rather than moved.</p> | ||||
| </div> | ||||
|  | ||||
| <div class="operation-card"> | ||||
|     <h4>Delete Notes</h4> | ||||
|     <p>Permanently remove multiple notes from the database.</p> | ||||
|     <pre><code>{ | ||||
|   "name": "deleteNote" | ||||
| }</code></pre> | ||||
|     <div class="warning"> | ||||
|         <strong>Warning:</strong> This operation cannot be undone. Ensure you have backups. | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <div class="operation-card"> | ||||
|     <h4>Rename Notes</h4> | ||||
|     <p>Update titles using dynamic patterns with variables.</p> | ||||
|     <pre><code>{ | ||||
|   "name": "renameNote", | ||||
|   "newTitle": "Project: ${note.title}" | ||||
| }</code></pre> | ||||
|     <p>Available variables: <code>${note.title}</code>, <code>${note.noteId}</code>, <code>${note.dateCreated}</code></p> | ||||
| </div> | ||||
|  | ||||
| <h3>Attribute Operations</h3> | ||||
|  | ||||
| <div class="operation-card"> | ||||
|     <h4>Add Label</h4> | ||||
|     <p>Attach labels to multiple notes at once.</p> | ||||
|     <pre><code>{ | ||||
|   "name": "addLabel", | ||||
|   "labelName": "reviewed", | ||||
|   "labelValue": "true" | ||||
| }</code></pre> | ||||
| </div> | ||||
|  | ||||
| <div class="operation-card"> | ||||
|     <h4>Update Label Value</h4> | ||||
|     <p>Modify existing label values across multiple notes.</p> | ||||
|     <pre><code>{ | ||||
|   "name": "updateLabelValue", | ||||
|   "labelName": "status", | ||||
|   "labelValue": "completed" | ||||
| }</code></pre> | ||||
| </div> | ||||
|  | ||||
| <div class="operation-card"> | ||||
|     <h4>Add Relation</h4> | ||||
|     <p>Create relationships between notes and a target.</p> | ||||
|     <pre><code>{ | ||||
|   "name": "addRelation", | ||||
|   "relationName": "references", | ||||
|   "targetNoteId": "bibliography_note" | ||||
| }</code></pre> | ||||
| </div> | ||||
|  | ||||
| <h3>Custom Script Execution</h3> | ||||
|  | ||||
| <div class="operation-card"> | ||||
|     <h4>Execute Script</h4> | ||||
|     <p>Run custom JavaScript code on each selected note.</p> | ||||
|     <pre><code>{ | ||||
|   "name": "executeScript", | ||||
|   "script": "note.setLabel('processed', new Date().toISOString());" | ||||
| }</code></pre> | ||||
|     <p>The <code>note</code> variable is available in the script context.</p> | ||||
| </div> | ||||
|  | ||||
| <h2>Including Descendants</h2> | ||||
|  | ||||
| <p>Apply operations to entire subtrees by enabling the "Include descendants" option:</p> | ||||
|  | ||||
| <pre><code>api.executeBulkActions(noteIds, actions, true);</code></pre> | ||||
|  | ||||
| <div class="info"> | ||||
|     <strong>Tip:</strong> This is useful for moving or deleting entire project hierarchies. | ||||
| </div> | ||||
|  | ||||
| <h2>Performance Guidelines</h2> | ||||
|  | ||||
| <table> | ||||
|     <thead> | ||||
|         <tr> | ||||
|             <th>Number of Notes</th> | ||||
|             <th>Expected Performance</th> | ||||
|             <th>Recommendations</th> | ||||
|         </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|         <tr> | ||||
|             <td>< 100</td> | ||||
|             <td>Instant</td> | ||||
|             <td>Execute directly</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td>100-1000</td> | ||||
|             <td>Few seconds</td> | ||||
|             <td>Show progress indicator</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td>> 1000</td> | ||||
|             <td>May take minutes</td> | ||||
|             <td>Run during low activity, consider batching</td> | ||||
|         </tr> | ||||
|     </tbody> | ||||
| </table> | ||||
|  | ||||
| <h2>Example: Project Archival</h2> | ||||
|  | ||||
| <p>Archive completed projects with metadata:</p> | ||||
|  | ||||
| <pre><code class="language-javascript">// Find completed projects | ||||
| const projectNotes = api.searchForNotes('#project #status=completed'); | ||||
|  | ||||
| // Define archive actions | ||||
| const archiveActions = [ | ||||
|   {  | ||||
|     name: 'moveNote',  | ||||
|     targetParentNoteId: 'archive_folder_id'  | ||||
|   }, | ||||
|   {  | ||||
|     name: 'addLabel',  | ||||
|     labelName: 'archivedDate',  | ||||
|     labelValue: new Date().toISOString()  | ||||
|   }, | ||||
|   {  | ||||
|     name: 'deleteLabel',  | ||||
|     labelName: 'active'  | ||||
|   } | ||||
| ]; | ||||
|  | ||||
| // Execute with descendants | ||||
| api.executeBulkActions(projectNotes, archiveActions, true);</code></pre> | ||||
|  | ||||
| <h2>Safety Considerations</h2> | ||||
|  | ||||
| <ul> | ||||
|     <li><strong>Always backup</strong> before large bulk operations</li> | ||||
|     <li><strong>Test first</strong> on a small subset of notes</li> | ||||
|     <li><strong>Review affected count</strong> before confirming</li> | ||||
|     <li><strong>Use transactions</strong> for related operations</li> | ||||
|     <li><strong>Monitor logs</strong> during execution</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Troubleshooting</h2> | ||||
|  | ||||
| <details> | ||||
|     <summary><strong>Operation appears to hang</strong></summary> | ||||
|     <p>For large operations (> 1000 notes), the process may take several minutes. Check server logs for progress. Consider breaking into smaller batches.</p> | ||||
| </details> | ||||
|  | ||||
| <details> | ||||
|     <summary><strong>Some notes not affected</strong></summary> | ||||
|     <p>Verify that:</p> | ||||
|     <ul> | ||||
|         <li>Notes exist and are accessible</li> | ||||
|         <li>Protected notes have unlocked session</li> | ||||
|         <li>No validation errors in logs</li> | ||||
|         <li>Target attributes don't have constraints</li> | ||||
|     </ul> | ||||
| </details> | ||||
|  | ||||
| <details> | ||||
|     <summary><strong>Memory errors on large operations</strong></summary> | ||||
|     <p>Increase Node.js heap size:</p> | ||||
|     <pre><code>NODE_OPTIONS="--max-old-space-size=4096" npm start</code></pre> | ||||
|     <p>Or process in smaller batches of 500 notes.</p> | ||||
| </details> | ||||
|  | ||||
| <style> | ||||
| .operation-card { | ||||
|     border: 1px solid var(--main-border-color); | ||||
|     border-radius: 4px; | ||||
|     padding: 15px; | ||||
|     margin: 15px 0; | ||||
|     background: var(--accented-background-color); | ||||
| } | ||||
|  | ||||
| .operation-card h4 { | ||||
|     margin-top: 0; | ||||
|     color: var(--main-text-color); | ||||
| } | ||||
|  | ||||
| .warning { | ||||
|     background: #fff3cd; | ||||
|     border: 1px solid #ffc107; | ||||
|     border-radius: 4px; | ||||
|     padding: 10px; | ||||
|     margin: 10px 0; | ||||
| } | ||||
|  | ||||
| .info { | ||||
|     background: #d1ecf1; | ||||
|     border: 1px solid #17a2b8; | ||||
|     border-radius: 4px; | ||||
|     padding: 10px; | ||||
|     margin: 10px 0; | ||||
| } | ||||
|  | ||||
| .note { | ||||
|     font-style: italic; | ||||
|     color: var(--muted-text-color); | ||||
| } | ||||
|  | ||||
| pre { | ||||
|     background: var(--code-background-color); | ||||
|     padding: 10px; | ||||
|     border-radius: 4px; | ||||
|     overflow-x: auto; | ||||
| } | ||||
|  | ||||
| details { | ||||
|     margin: 10px 0; | ||||
|     padding: 10px; | ||||
|     border: 1px solid var(--main-border-color); | ||||
|     border-radius: 4px; | ||||
| } | ||||
|  | ||||
| summary { | ||||
|     cursor: pointer; | ||||
|     font-weight: 500; | ||||
| } | ||||
|  | ||||
| table { | ||||
|     width: 100%; | ||||
|     border-collapse: collapse; | ||||
|     margin: 15px 0; | ||||
| } | ||||
|  | ||||
| th, td { | ||||
|     border: 1px solid var(--main-border-color); | ||||
|     padding: 8px; | ||||
|     text-align: left; | ||||
| } | ||||
|  | ||||
| th { | ||||
|     background: var(--accented-background-color); | ||||
|     font-weight: 600; | ||||
| } | ||||
| </style> | ||||
| @@ -1,27 +1,335 @@ | ||||
| <p>Trilium supports configuration via a file named <code>config.ini</code> and | ||||
|   environment variables. Please review the file named <a href="https://github.com/TriliumNext/Trilium/blob/main/apps/server/src/assets/config-sample.ini">config-sample.ini</a> in | ||||
|   the <a href="https://github.com/TriliumNext/Trilium">Trilium</a> repository | ||||
|   to see what values are supported.</p> | ||||
| <p>You can provide the same values via environment variables instead of the <code>config.ini</code> file, | ||||
|   and these environment variables use the following format:</p> | ||||
| <p>Trilium supports configuration via a file named <code>config.ini</code> and environment variables. This document provides a comprehensive reference for all configuration options.</p> | ||||
|  | ||||
| <h2>Configuration Precedence</h2> | ||||
| <p>Configuration values are loaded in the following order of precedence (highest to lowest):</p> | ||||
| <ol> | ||||
|   <li>Environment variables should be prefixed with <code>TRILIUM_</code> and | ||||
|     use underscores to represent the INI section structure.</li> | ||||
|   <li>The format is: <code>TRILIUM_<SECTION>_<KEY>=<VALUE></code> | ||||
|   </li> | ||||
|   <li>The environment variables will override any matching values from config.ini</li> | ||||
| </ol> | ||||
| <p>For example, if you have this in your config.ini:</p><pre><code class="language-text-x-trilium-auto">[Network] | ||||
| host=localhost | ||||
| port=8080</code></pre> | ||||
| <p>You can override these values using environment variables:</p><pre><code class="language-text-x-trilium-auto">TRILIUM_NETWORK_HOST=0.0.0.0 | ||||
| TRILIUM_NETWORK_PORT=9000</code></pre> | ||||
| <p>The code will:</p> | ||||
| <ol> | ||||
|   <li>First load the <code>config.ini</code> file as before</li> | ||||
|   <li>Then scan all environment variables for ones starting with <code>TRILIUM_</code> | ||||
|   </li> | ||||
|   <li>Parse these variables into section/key pairs</li> | ||||
|   <li>Merge them with the config from the file, with environment variables taking | ||||
|     precedence</li> | ||||
|   <li><strong>Environment variables</strong> (checked first)</li> | ||||
|   <li><strong>config.ini file values</strong></li> | ||||
|   <li><strong>Default values</strong></li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Environment Variable Patterns</h2> | ||||
| <p>Trilium supports multiple environment variable patterns for flexibility. The primary pattern is: <code>TRILIUM_[SECTION]_[KEY]</code></p> | ||||
| <p>Where:</p> | ||||
| <ul> | ||||
|   <li><code>SECTION</code> is the INI section name in UPPERCASE</li> | ||||
|   <li><code>KEY</code> is the camelCase configuration key converted to UPPERCASE (e.g., <code>instanceName</code> → <code>INSTANCENAME</code>)</li> | ||||
| </ul> | ||||
| <p>Additionally, shorter aliases are available for common configurations (see Alternative Variables section below).</p> | ||||
|  | ||||
| <h2>Environment Variable Reference</h2> | ||||
|  | ||||
| <h3>General Section</h3> | ||||
| <table> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th>Environment Variable</th> | ||||
|       <th>Type</th> | ||||
|       <th>Default</th> | ||||
|       <th>Description</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_GENERAL_INSTANCENAME</code></td> | ||||
|       <td>string</td> | ||||
|       <td>""</td> | ||||
|       <td>Instance name for API identification</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_GENERAL_NOAUTHENTICATION</code></td> | ||||
|       <td>boolean</td> | ||||
|       <td>false</td> | ||||
|       <td>Disable authentication (server only)</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_GENERAL_NOBACKUP</code></td> | ||||
|       <td>boolean</td> | ||||
|       <td>false</td> | ||||
|       <td>Disable automatic backups</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_GENERAL_NODESKTOPICON</code></td> | ||||
|       <td>boolean</td> | ||||
|       <td>false</td> | ||||
|       <td>Disable desktop icon creation</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_GENERAL_READONLY</code></td> | ||||
|       <td>boolean</td> | ||||
|       <td>false</td> | ||||
|       <td>Enable read-only mode</td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <h3>Network Section</h3> | ||||
| <table> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th>Environment Variable</th> | ||||
|       <th>Type</th> | ||||
|       <th>Default</th> | ||||
|       <th>Description</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_NETWORK_HOST</code></td> | ||||
|       <td>string</td> | ||||
|       <td>"0.0.0.0"</td> | ||||
|       <td>Server host binding</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_NETWORK_PORT</code></td> | ||||
|       <td>string</td> | ||||
|       <td>"3000"</td> | ||||
|       <td>Server port</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_NETWORK_HTTPS</code></td> | ||||
|       <td>boolean</td> | ||||
|       <td>false</td> | ||||
|       <td>Enable HTTPS</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_NETWORK_CERTPATH</code></td> | ||||
|       <td>string</td> | ||||
|       <td>""</td> | ||||
|       <td>SSL certificate path</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_NETWORK_KEYPATH</code></td> | ||||
|       <td>string</td> | ||||
|       <td>""</td> | ||||
|       <td>SSL key path</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_NETWORK_TRUSTEDREVERSEPROXY</code></td> | ||||
|       <td>boolean/string</td> | ||||
|       <td>false</td> | ||||
|       <td>Reverse proxy trust settings</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_NETWORK_CORSALLOWORIGIN</code></td> | ||||
|       <td>string</td> | ||||
|       <td>""</td> | ||||
|       <td>CORS allowed origins</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_NETWORK_CORSALLOWMETHODS</code></td> | ||||
|       <td>string</td> | ||||
|       <td>""</td> | ||||
|       <td>CORS allowed methods</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_NETWORK_CORSALLOWHEADERS</code></td> | ||||
|       <td>string</td> | ||||
|       <td>""</td> | ||||
|       <td>CORS allowed headers</td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <h3>Session Section</h3> | ||||
| <table> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th>Environment Variable</th> | ||||
|       <th>Type</th> | ||||
|       <th>Default</th> | ||||
|       <th>Description</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_SESSION_COOKIEMAXAGE</code></td> | ||||
|       <td>integer</td> | ||||
|       <td>1814400</td> | ||||
|       <td>Session cookie max age in seconds (21 days)</td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <h3>Sync Section</h3> | ||||
| <table> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th>Environment Variable</th> | ||||
|       <th>Type</th> | ||||
|       <th>Default</th> | ||||
|       <th>Description</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_SYNC_SYNCSERVERHOST</code></td> | ||||
|       <td>string</td> | ||||
|       <td>""</td> | ||||
|       <td>Sync server host URL</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_SYNC_SYNCSERVERTIMEOUT</code></td> | ||||
|       <td>string</td> | ||||
|       <td>"120000"</td> | ||||
|       <td>Sync server timeout in milliseconds</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_SYNC_SYNCPROXY</code></td> | ||||
|       <td>string</td> | ||||
|       <td>""</td> | ||||
|       <td>Sync proxy URL</td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <h3>MultiFactorAuthentication Section</h3> | ||||
| <table> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th>Environment Variable</th> | ||||
|       <th>Type</th> | ||||
|       <th>Default</th> | ||||
|       <th>Description</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code></td> | ||||
|       <td>string</td> | ||||
|       <td>""</td> | ||||
|       <td>OAuth/OpenID base URL</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code></td> | ||||
|       <td>string</td> | ||||
|       <td>""</td> | ||||
|       <td>OAuth client ID</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code></td> | ||||
|       <td>string</td> | ||||
|       <td>""</td> | ||||
|       <td>OAuth client secret</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code></td> | ||||
|       <td>string</td> | ||||
|       <td>"https://accounts.google.com"</td> | ||||
|       <td>OAuth issuer base URL</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code></td> | ||||
|       <td>string</td> | ||||
|       <td>"Google"</td> | ||||
|       <td>OAuth issuer display name</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code></td> | ||||
|       <td>string</td> | ||||
|       <td>""</td> | ||||
|       <td>OAuth issuer icon URL</td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <h3>Logging Section</h3> | ||||
| <table> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th>Environment Variable</th> | ||||
|       <th>Type</th> | ||||
|       <th>Default</th> | ||||
|       <th>Description</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr> | ||||
|       <td><code>TRILIUM_LOGGING_RETENTIONDAYS</code></td> | ||||
|       <td>integer</td> | ||||
|       <td>90</td> | ||||
|       <td>Number of days to retain log files</td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <h2>Alternative Environment Variables</h2> | ||||
| <p>The following alternative environment variable names are also supported and work identically to their longer counterparts:</p> | ||||
|  | ||||
| <h3>Network CORS Variables</h3> | ||||
| <ul> | ||||
|   <li><code>TRILIUM_NETWORK_CORS_ALLOW_ORIGIN</code> (alternative to <code>TRILIUM_NETWORK_CORSALLOWORIGIN</code>)</li> | ||||
|   <li><code>TRILIUM_NETWORK_CORS_ALLOW_METHODS</code> (alternative to <code>TRILIUM_NETWORK_CORSALLOWMETHODS</code>)</li> | ||||
|   <li><code>TRILIUM_NETWORK_CORS_ALLOW_HEADERS</code> (alternative to <code>TRILIUM_NETWORK_CORSALLOWHEADERS</code>)</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Sync Variables</h3> | ||||
| <ul> | ||||
|   <li><code>TRILIUM_SYNC_SERVER_HOST</code> (alternative to <code>TRILIUM_SYNC_SYNCSERVERHOST</code>)</li> | ||||
|   <li><code>TRILIUM_SYNC_SERVER_TIMEOUT</code> (alternative to <code>TRILIUM_SYNC_SYNCSERVERTIMEOUT</code>)</li> | ||||
|   <li><code>TRILIUM_SYNC_SERVER_PROXY</code> (alternative to <code>TRILIUM_SYNC_SYNCPROXY</code>)</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>OAuth/MFA Variables</h3> | ||||
| <ul> | ||||
|   <li><code>TRILIUM_OAUTH_BASE_URL</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>)</li> | ||||
|   <li><code>TRILIUM_OAUTH_CLIENT_ID</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>)</li> | ||||
|   <li><code>TRILIUM_OAUTH_CLIENT_SECRET</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code>)</li> | ||||
|   <li><code>TRILIUM_OAUTH_ISSUER_BASE_URL</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>)</li> | ||||
|   <li><code>TRILIUM_OAUTH_ISSUER_NAME</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>)</li> | ||||
|   <li><code>TRILIUM_OAUTH_ISSUER_ICON</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code>)</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Logging Variables</h3> | ||||
| <ul> | ||||
|   <li><code>TRILIUM_LOGGING_RETENTION_DAYS</code> (alternative to <code>TRILIUM_LOGGING_RETENTIONDAYS</code>)</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Boolean Values</h2> | ||||
| <p>Boolean environment variables accept the following values:</p> | ||||
| <ul> | ||||
|   <li><strong>True</strong>: <code>"true"</code>, <code>"1"</code>, <code>1</code></li> | ||||
|   <li><strong>False</strong>: <code>"false"</code>, <code>"0"</code>, <code>0</code></li> | ||||
|   <li>Any other value defaults to <code>false</code></li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Using Environment Variables</h2> | ||||
| <p>Both naming patterns are fully supported and can be used interchangeably:</p> | ||||
| <ul> | ||||
|   <li>The longer format follows the section/key pattern for consistency with the INI file structure</li> | ||||
|   <li>The shorter alternatives provide convenience for common configurations</li> | ||||
|   <li>You can use whichever format you prefer - both are equally valid</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Examples</h2> | ||||
|  | ||||
| <h3>Docker Compose Example</h3> | ||||
| <pre><code class="language-yaml">services: | ||||
|   trilium: | ||||
|     image: triliumnext/notes | ||||
|     environment: | ||||
|       # Using full format | ||||
|       TRILIUM_GENERAL_INSTANCENAME: "My Trilium Instance" | ||||
|       TRILIUM_NETWORK_PORT: "8080" | ||||
|       TRILIUM_NETWORK_CORSALLOWORIGIN: "https://myapp.com" | ||||
|       TRILIUM_SYNC_SYNCSERVERHOST: "https://sync.example.com" | ||||
|       TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL: "https://auth.example.com" | ||||
|        | ||||
|       # Or using shorter alternatives (equally valid) | ||||
|       # TRILIUM_NETWORK_CORS_ALLOW_ORIGIN: "https://myapp.com" | ||||
|       # TRILIUM_SYNC_SERVER_HOST: "https://sync.example.com" | ||||
|       # TRILIUM_OAUTH_BASE_URL: "https://auth.example.com"</code></pre> | ||||
|  | ||||
| <h3>Shell Export Example</h3> | ||||
| <pre><code class="language-bash"># Using either format | ||||
| export TRILIUM_GENERAL_NOAUTHENTICATION=false | ||||
| export TRILIUM_NETWORK_HTTPS=true | ||||
| export TRILIUM_NETWORK_CERTPATH=/path/to/cert.pem | ||||
| export TRILIUM_NETWORK_KEYPATH=/path/to/key.pem | ||||
| export TRILIUM_LOGGING_RETENTIONDAYS=30 | ||||
|  | ||||
| # Start Trilium | ||||
| npm start</code></pre> | ||||
|  | ||||
| <h2>config.ini Reference</h2> | ||||
| <p>For the complete list of configuration options and their INI file format, please review the <a href="https://github.com/TriliumNext/Trilium/blob/main/apps/server/src/assets/config-sample.ini">config-sample.ini</a> file in the Trilium repository.</p> | ||||
							
								
								
									
										378
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Note Revisions.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										378
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Note Revisions.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,378 @@ | ||||
| <h1>Note Revisions</h1> | ||||
|  | ||||
| <p>Track and restore previous versions of your notes with Trilium's comprehensive revision system.</p> | ||||
|  | ||||
| <h2>Understanding Revisions</h2> | ||||
|  | ||||
| <p>Revisions are automatic snapshots of your note content captured at specific intervals. Each revision preserves:</p> | ||||
| <ul> | ||||
|     <li>Complete note content</li> | ||||
|     <li>Note title at revision time</li> | ||||
|     <li>Type and MIME information</li> | ||||
|     <li>Creation and modification timestamps</li> | ||||
|     <li>Protection status</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Accessing Revision History</h2> | ||||
|  | ||||
| <div class="access-methods"> | ||||
|     <div class="method"> | ||||
|         <h4>Via Note Menu</h4> | ||||
|         <p>Click note menu (⋮) → "Revisions"</p> | ||||
|     </div> | ||||
|     <div class="method"> | ||||
|         <h4>Keyboard Shortcut</h4> | ||||
|         <p>Press <kbd>Alt</kbd> + <kbd>R</kbd></p> | ||||
|     </div> | ||||
|     <div class="method"> | ||||
|         <h4>Script API</h4> | ||||
|         <p><code>note.getRevisions()</code></p> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <h2>Automatic Creation Rules</h2> | ||||
|  | ||||
| <div class="info-box"> | ||||
|     <h3>Default Triggers</h3> | ||||
|     <ul> | ||||
|         <li>After 5 minutes of continuous editing</li> | ||||
|         <li>When switching to a different note</li> | ||||
|         <li>Before major operations (delete, move)</li> | ||||
|         <li>Content size changes > 20%</li> | ||||
|     </ul> | ||||
| </div> | ||||
|  | ||||
| <h2>Retention Policies</h2> | ||||
|  | ||||
| <table class="retention-table"> | ||||
|     <thead> | ||||
|         <tr> | ||||
|             <th>Age</th> | ||||
|             <th>Retention</th> | ||||
|             <th>Example</th> | ||||
|         </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|         <tr> | ||||
|             <td>< 1 day</td> | ||||
|             <td>Keep all</td> | ||||
|             <td>Every revision saved</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td>1-7 days</td> | ||||
|             <td>Daily</td> | ||||
|             <td>One per day</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td>7-30 days</td> | ||||
|             <td>Weekly</td> | ||||
|             <td>One per week</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td>> 30 days</td> | ||||
|             <td>Monthly</td> | ||||
|             <td>One per month</td> | ||||
|         </tr> | ||||
|     </tbody> | ||||
| </table> | ||||
|  | ||||
| <h2>Custom Retention Settings</h2> | ||||
|  | ||||
| <p>Configure retention per note using attributes:</p> | ||||
|  | ||||
| <div class="code-example"> | ||||
|     <h4>Keep All Revisions</h4> | ||||
|     <pre><code>#revisionRetention=all</code></pre> | ||||
| </div> | ||||
|  | ||||
| <div class="code-example"> | ||||
|     <h4>Keep for Specific Duration</h4> | ||||
|     <pre><code>#revisionRetention=90d</code></pre> | ||||
| </div> | ||||
|  | ||||
| <div class="code-example"> | ||||
|     <h4>Disable Revisions</h4> | ||||
|     <pre><code>#disableRevisions</code></pre> | ||||
| </div> | ||||
|  | ||||
| <h2>Comparing Revisions</h2> | ||||
|  | ||||
| <div class="feature-box"> | ||||
|     <h3>Visual Comparison</h3> | ||||
|     <ol> | ||||
|         <li>Open revision history</li> | ||||
|         <li>Select two revisions</li> | ||||
|         <li>Click "Compare"</li> | ||||
|         <li>View side-by-side differences</li> | ||||
|     </ol> | ||||
|     <p class="highlight">Added content shown in <span style="color: green;">green</span>, removed in <span style="color: red;">red</span>.</p> | ||||
| </div> | ||||
|  | ||||
| <h2>Restoring Revisions</h2> | ||||
|  | ||||
| <div class="steps"> | ||||
|     <h3>Manual Restoration</h3> | ||||
|     <ol> | ||||
|         <li>Open revision history (<kbd>Alt</kbd> + <kbd>R</kbd>)</li> | ||||
|         <li>Browse revisions by date/time</li> | ||||
|         <li>Preview revision content</li> | ||||
|         <li>Click "Restore this version"</li> | ||||
|         <li>Confirm replacement</li> | ||||
|     </ol> | ||||
| </div> | ||||
|  | ||||
| <div class="warning"> | ||||
|     <strong>Important:</strong> Restoring a revision replaces current content. Consider creating a manual snapshot first. | ||||
| </div> | ||||
|  | ||||
| <h2>Creating Manual Snapshots</h2> | ||||
|  | ||||
| <p>Force revision creation for important milestones:</p> | ||||
|  | ||||
| <pre><code class="language-javascript">// Via Script API | ||||
| api.createRevision(note.noteId, { | ||||
|   title: note.title, | ||||
|   content: note.getContent(), | ||||
|   reason: 'Before major refactoring' | ||||
| });</code></pre> | ||||
|  | ||||
| <h2>Protected Note Revisions</h2> | ||||
|  | ||||
| <div class="security-box"> | ||||
|     <h3>Encryption Behavior</h3> | ||||
|     <ul> | ||||
|         <li>Revisions inherit note's protection status</li> | ||||
|         <li>Title and content encrypted separately</li> | ||||
|         <li>Requires unlocked session to view</li> | ||||
|         <li>Protection changes apply to all revisions</li> | ||||
|     </ul> | ||||
| </div> | ||||
|  | ||||
| <h2>Storage Management</h2> | ||||
|  | ||||
| <div class="metrics"> | ||||
|     <h3>Typical Storage Usage</h3> | ||||
|     <table> | ||||
|         <tr> | ||||
|             <td>Text notes</td> | ||||
|             <td>~2KB per revision</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td>Code notes</td> | ||||
|             <td>~5KB per revision</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td>Image notes</td> | ||||
|             <td>Deduplicated via blobs</td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td>File notes</td> | ||||
|             <td>Full file size</td> | ||||
|         </tr> | ||||
|     </table> | ||||
| </div> | ||||
|  | ||||
| <h2>Cleanup Operations</h2> | ||||
|  | ||||
| <div class="code-example"> | ||||
|     <h4>Automatic Cleanup</h4> | ||||
|     <pre><code class="language-javascript">// Configure in options | ||||
| api.setOption('revisionCleanupDays', 90); | ||||
| api.setOption('revisionCleanupEnabled', true);</code></pre> | ||||
| </div> | ||||
|  | ||||
| <div class="code-example"> | ||||
|     <h4>Manual Cleanup</h4> | ||||
|     <pre><code class="language-javascript">// Delete revisions older than 90 days | ||||
| const cutoffDate = new Date(); | ||||
| cutoffDate.setDate(cutoffDate.getDate() - 90); | ||||
|  | ||||
| for (const note of api.getAllNotes()) { | ||||
|   const revisions = note.getRevisions(); | ||||
|   for (const revision of revisions) { | ||||
|     if (revision.dateCreated < cutoffDate) { | ||||
|       revision.delete(); | ||||
|     } | ||||
|   } | ||||
| }</code></pre> | ||||
| </div> | ||||
|  | ||||
| <h2>Performance Tips</h2> | ||||
|  | ||||
| <ul class="tips"> | ||||
|     <li><strong>Large files:</strong> Consider disabling revisions for binary attachments</li> | ||||
|     <li><strong>Frequent edits:</strong> Increase revision interval to reduce storage</li> | ||||
|     <li><strong>Cleanup:</strong> Run cleanup during low-activity periods</li> | ||||
|     <li><strong>Monitoring:</strong> Check database size regularly</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Troubleshooting</h2> | ||||
|  | ||||
| <details> | ||||
|     <summary><strong>Revisions not appearing</strong></summary> | ||||
|     <ul> | ||||
|         <li>Check if <code>#disableRevisions</code> is set</li> | ||||
|         <li>Verify revision creation interval in options</li> | ||||
|         <li>Ensure sufficient disk space</li> | ||||
|         <li>Check database write permissions</li> | ||||
|     </ul> | ||||
| </details> | ||||
|  | ||||
| <details> | ||||
|     <summary><strong>Cannot view revision content</strong></summary> | ||||
|     <ul> | ||||
|         <li>For protected notes, unlock protected session</li> | ||||
|         <li>Verify blob storage integrity</li> | ||||
|         <li>Check database consistency</li> | ||||
|         <li>Review error logs for details</li> | ||||
|     </ul> | ||||
| </details> | ||||
|  | ||||
| <details> | ||||
|     <summary><strong>Excessive storage usage</strong></summary> | ||||
|     <ul> | ||||
|         <li>Implement aggressive cleanup policy</li> | ||||
|         <li>Exclude binary notes from tracking</li> | ||||
|         <li>Archive old revisions externally</li> | ||||
|         <li>Consider compression options</li> | ||||
|     </ul> | ||||
| </details> | ||||
|  | ||||
| <style> | ||||
| .access-methods { | ||||
|     display: flex; | ||||
|     gap: 15px; | ||||
|     margin: 20px 0; | ||||
| } | ||||
|  | ||||
| .method { | ||||
|     flex: 1; | ||||
|     padding: 15px; | ||||
|     background: var(--accented-background-color); | ||||
|     border-radius: 4px; | ||||
|     border: 1px solid var(--main-border-color); | ||||
| } | ||||
|  | ||||
| .method h4 { | ||||
|     margin-top: 0; | ||||
|     color: var(--main-text-color); | ||||
| } | ||||
|  | ||||
| .info-box, .feature-box, .security-box { | ||||
|     background: #e8f4f8; | ||||
|     border-left: 4px solid #17a2b8; | ||||
|     padding: 15px; | ||||
|     margin: 20px 0; | ||||
|     border-radius: 4px; | ||||
| } | ||||
|  | ||||
| .warning { | ||||
|     background: #fff3cd; | ||||
|     border-left: 4px solid #ffc107; | ||||
|     padding: 15px; | ||||
|     margin: 20px 0; | ||||
|     border-radius: 4px; | ||||
| } | ||||
|  | ||||
| .code-example { | ||||
|     margin: 15px 0; | ||||
| } | ||||
|  | ||||
| .code-example h4 { | ||||
|     margin-bottom: 5px; | ||||
|     color: var(--main-text-color); | ||||
| } | ||||
|  | ||||
| .retention-table { | ||||
|     width: 100%; | ||||
|     border-collapse: collapse; | ||||
|     margin: 20px 0; | ||||
| } | ||||
|  | ||||
| .retention-table th, | ||||
| .retention-table td { | ||||
|     border: 1px solid var(--main-border-color); | ||||
|     padding: 10px; | ||||
|     text-align: left; | ||||
| } | ||||
|  | ||||
| .retention-table th { | ||||
|     background: var(--accented-background-color); | ||||
| } | ||||
|  | ||||
| .metrics table { | ||||
|     width: 100%; | ||||
|     margin: 10px 0; | ||||
| } | ||||
|  | ||||
| .metrics td { | ||||
|     padding: 8px; | ||||
|     border-bottom: 1px solid var(--main-border-color); | ||||
| } | ||||
|  | ||||
| .steps { | ||||
|     background: var(--accented-background-color); | ||||
|     padding: 20px; | ||||
|     border-radius: 4px; | ||||
|     margin: 20px 0; | ||||
| } | ||||
|  | ||||
| .steps ol { | ||||
|     margin: 10px 0; | ||||
| } | ||||
|  | ||||
| .tips { | ||||
|     list-style: none; | ||||
|     padding: 0; | ||||
| } | ||||
|  | ||||
| .tips li { | ||||
|     padding: 10px 0; | ||||
|     border-bottom: 1px solid var(--main-border-color); | ||||
| } | ||||
|  | ||||
| .tips li:last-child { | ||||
|     border-bottom: none; | ||||
| } | ||||
|  | ||||
| .highlight { | ||||
|     font-weight: 500; | ||||
| } | ||||
|  | ||||
| kbd { | ||||
|     background: #f4f4f4; | ||||
|     border: 1px solid #ccc; | ||||
|     border-radius: 3px; | ||||
|     padding: 2px 6px; | ||||
|     font-family: monospace; | ||||
|     font-size: 0.9em; | ||||
| } | ||||
|  | ||||
| details { | ||||
|     margin: 15px 0; | ||||
|     padding: 15px; | ||||
|     background: var(--accented-background-color); | ||||
|     border-radius: 4px; | ||||
|     border: 1px solid var(--main-border-color); | ||||
| } | ||||
|  | ||||
| summary { | ||||
|     cursor: pointer; | ||||
|     font-weight: 500; | ||||
|     margin-bottom: 10px; | ||||
| } | ||||
|  | ||||
| pre { | ||||
|     background: var(--code-background-color); | ||||
|     padding: 15px; | ||||
|     border-radius: 4px; | ||||
|     overflow-x: auto; | ||||
| } | ||||
|  | ||||
| code { | ||||
|     background: var(--code-background-color); | ||||
|     padding: 2px 4px; | ||||
|     border-radius: 3px; | ||||
|     font-family: monospace; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										232
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Advanced-Search-Expressions.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Advanced-Search-Expressions.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,232 @@ | ||||
| <h1>Advanced Search Expressions</h1> | ||||
| <p>This guide covers complex search expressions that combine multiple criteria, use advanced operators, and leverage Trilium's relationship system for sophisticated queries.</p> | ||||
|  | ||||
| <h2>Complex Query Construction</h2> | ||||
|  | ||||
| <h3>Boolean Logic with Parentheses</h3> | ||||
| <p>Use parentheses to group expressions and control evaluation order:</p> | ||||
| <pre><code>(#book OR #article) AND #author=Tolkien</code></pre> | ||||
| <p>Finds notes that are either books or articles, written by Tolkien.</p> | ||||
| <pre><code>#project AND (#status=active OR #status=pending)</code></pre> | ||||
| <p>Finds active or pending projects.</p> | ||||
| <pre><code>meeting AND (#priority=high OR #urgent) AND note.dateCreated >= TODAY-7</code></pre> | ||||
| <p>Finds recent high-priority or urgent meetings.</p> | ||||
|  | ||||
| <h3>Negation Patterns</h3> | ||||
| <p>Use <code>NOT</code> or the <code>not()</code> function to exclude certain criteria:</p> | ||||
| <pre><code>#book AND not(#genre=fiction)</code></pre> | ||||
| <p>Finds non-fiction books.</p> | ||||
| <pre><code>project AND not(note.isArchived=true)</code></pre> | ||||
| <p>Finds non-archived notes containing "project".</p> | ||||
| <pre><code>#!completed</code></pre> | ||||
| <p>Short syntax for notes without the "completed" label.</p> | ||||
|  | ||||
| <h3>Mixed Search Types</h3> | ||||
| <p>Combine full-text, attribute, and property searches:</p> | ||||
| <pre><code>development #category=work note.type=text note.dateModified >= TODAY-30</code></pre> | ||||
| <p>Finds text notes about development, categorized as work, modified in the last 30 days.</p> | ||||
|  | ||||
| <h2>Advanced Attribute Searches</h2> | ||||
|  | ||||
| <h3>Fuzzy Attribute Matching</h3> | ||||
| <p>When fuzzy attribute search is enabled, you can use partial matches:</p> | ||||
| <pre><code>#lang</code></pre> | ||||
| <p>Matches labels like "language", "languages", "programming-lang", etc.</p> | ||||
| <pre><code>#category=prog</code></pre> | ||||
| <p>Matches categories like "programming", "progress", "program", etc.</p> | ||||
|  | ||||
| <h3>Multiple Attribute Conditions</h3> | ||||
| <pre><code>#book #author=Tolkien #publicationYear>=1950 #publicationYear<1960</code></pre> | ||||
| <p>Finds Tolkien's books published in the 1950s.</p> | ||||
| <pre><code>#task #priority=high #status!=completed</code></pre> | ||||
| <p>Finds high-priority incomplete tasks.</p> | ||||
|  | ||||
| <h3>Complex Label Value Patterns</h3> | ||||
| <p>Use various operators for sophisticated label matching:</p> | ||||
| <pre><code>#isbn %= '978-[0-9-]+'</code></pre> | ||||
| <p>Finds notes with ISBN labels matching the pattern (regex).</p> | ||||
| <pre><code>#email *=* @company.com</code></pre> | ||||
| <p>Finds notes with email labels containing "@company.com".</p> | ||||
| <pre><code>#version >= 2.0</code></pre> | ||||
| <p>Finds notes with version labels of 2.0 or higher (numeric comparison).</p> | ||||
|  | ||||
| <h2>Relationship Traversal</h2> | ||||
|  | ||||
| <h3>Basic Relation Queries</h3> | ||||
| <pre><code>~author.title *=* Tolkien</code></pre> | ||||
| <p>Finds notes with an "author" relation to notes containing "Tolkien" in the title.</p> | ||||
| <pre><code>~project.labels.status = active</code></pre> | ||||
| <p>Finds notes related to projects with active status.</p> | ||||
|  | ||||
| <h3>Multi-Level Relationships</h3> | ||||
| <pre><code>~author.relations.publisher.title = "Penguin Books"</code></pre> | ||||
| <p>Finds notes authored by someone published by Penguin Books.</p> | ||||
| <pre><code>~project.children.title *=* documentation</code></pre> | ||||
| <p>Finds notes related to projects that have child notes about documentation.</p> | ||||
|  | ||||
| <h3>Relationship Direction</h3> | ||||
| <pre><code>note.children.title = "Chapter 1"</code></pre> | ||||
| <p>Finds parent notes that have a child titled "Chapter 1".</p> | ||||
| <pre><code>note.parents.labels.category = book</code></pre> | ||||
| <p>Finds notes whose parents are categorized as books.</p> | ||||
| <pre><code>note.ancestors.title = "Literature"</code></pre> | ||||
| <p>Finds notes with "Literature" anywhere in their ancestor chain.</p> | ||||
|  | ||||
| <h2>Property-Based Searches</h2> | ||||
|  | ||||
| <h3>Note Metadata Queries</h3> | ||||
| <pre><code>note.type=code note.mime=text/javascript note.dateCreated >= MONTH</code></pre> | ||||
| <p>Finds JavaScript code notes created this month.</p> | ||||
| <pre><code>note.isProtected=true note.contentSize > 1000</code></pre> | ||||
| <p>Finds large protected notes.</p> | ||||
| <pre><code>note.childrenCount >= 10 note.type=text</code></pre> | ||||
| <p>Finds text notes with many children.</p> | ||||
|  | ||||
| <h3>Advanced Property Combinations</h3> | ||||
| <pre><code>note.parentCount > 1 #template</code></pre> | ||||
| <p>Finds template notes that are cloned in multiple places.</p> | ||||
| <pre><code>note.attributeCount > 5 note.type=text note.contentSize < 500</code></pre> | ||||
| <p>Finds small text notes with many attributes (heavily tagged short notes).</p> | ||||
| <pre><code>note.revisionCount > 10 note.dateModified >= TODAY-7</code></pre> | ||||
| <p>Finds frequently edited notes modified recently.</p> | ||||
|  | ||||
| <h2>Date and Time Expressions</h2> | ||||
|  | ||||
| <h3>Relative Date Calculations</h3> | ||||
| <pre><code>#dueDate <= TODAY+7 #dueDate >= TODAY</code></pre> | ||||
| <p>Finds tasks due in the next week.</p> | ||||
| <pre><code>note.dateCreated >= MONTH-2 note.dateCreated < MONTH</code></pre> | ||||
| <p>Finds notes created in the past two months.</p> | ||||
| <pre><code>#eventDate = YEAR note.dateCreated >= YEAR-1</code></pre> | ||||
| <p>Finds events scheduled for this year that were planned last year.</p> | ||||
|  | ||||
| <h3>Complex Date Logic</h3> | ||||
| <pre><code>(#startDate <= TODAY AND #endDate >= TODAY) OR #status=ongoing</code></pre> | ||||
| <p>Finds current events or ongoing items.</p> | ||||
| <pre><code>#reminderDate <= NOW+3600 #reminderDate > NOW</code></pre> | ||||
| <p>Finds reminders due in the next hour (using seconds offset).</p> | ||||
|  | ||||
| <h2>Fuzzy Search Techniques</h2> | ||||
|  | ||||
| <h3>Fuzzy Exact Matching</h3> | ||||
| <pre><code>#title ~= managment</code></pre> | ||||
| <p>Finds notes with titles like "management" even with typos.</p> | ||||
| <pre><code>~category.title ~= progaming</code></pre> | ||||
| <p>Finds notes related to categories like "programming" with misspellings.</p> | ||||
|  | ||||
| <h3>Fuzzy Contains Matching</h3> | ||||
| <pre><code>note.content ~* algoritm</code></pre> | ||||
| <p>Finds notes containing words like "algorithm" with spelling variations.</p> | ||||
| <pre><code>#description ~* recieve</code></pre> | ||||
| <p>Finds notes with descriptions containing "receive" despite the common misspelling.</p> | ||||
|  | ||||
| <h3>Progressive Fuzzy Strategy</h3> | ||||
| <p>By default, Trilium uses exact matching first, then fuzzy as fallback:</p> | ||||
| <pre><code>development project</code></pre> | ||||
| <p>First finds exact matches for "development" and "project", then adds fuzzy matches if needed.</p> | ||||
|  | ||||
| <p>To force fuzzy behavior:</p> | ||||
| <pre><code>#title ~= development #category ~= projet</code></pre> | ||||
|  | ||||
| <h2>Ordering and Limiting</h2> | ||||
|  | ||||
| <h3>Multiple Sort Criteria</h3> | ||||
| <pre><code>#book orderBy #publicationYear desc, note.title asc limit 20</code></pre> | ||||
| <p>Orders books by publication year (newest first), then by title alphabetically, limited to 20 results.</p> | ||||
| <pre><code>#task orderBy #priority desc, #dueDate asc</code></pre> | ||||
| <p>Orders tasks by priority (high first), then by due date (earliest first).</p> | ||||
|  | ||||
| <h3>Dynamic Ordering</h3> | ||||
| <pre><code>#meeting note.dateCreated >= TODAY-30 orderBy note.dateModified desc</code></pre> | ||||
| <p>Finds recent meetings ordered by last modification.</p> | ||||
| <pre><code>#project #status=active orderBy note.childrenCount desc limit 10</code></pre> | ||||
| <p>Finds the 10 most complex active projects (by number of sub-notes).</p> | ||||
|  | ||||
| <h2>Performance Optimization Patterns</h2> | ||||
|  | ||||
| <h3>Efficient Query Structure</h3> | ||||
| <p>Start with the most selective criteria:</p> | ||||
| <pre><code>#book #author=Tolkien note.dateCreated >= 1950-01-01</code></pre> | ||||
| <p>Better than:</p> | ||||
| <pre><code>note.dateCreated >= 1950-01-01 #book #author=Tolkien</code></pre> | ||||
|  | ||||
| <h3>Fast Search for Large Datasets</h3> | ||||
| <pre><code>#category=project #status=active</code></pre> | ||||
| <p>With fast search enabled, this searches only attributes, not content.</p> | ||||
|  | ||||
| <h3>Limiting Expensive Operations</h3> | ||||
| <pre><code>note.content *=* "complex search term" limit 50</code></pre> | ||||
| <p>Limits content search to prevent performance issues.</p> | ||||
|  | ||||
| <h2>Error Handling and Debugging</h2> | ||||
|  | ||||
| <h3>Syntax Validation</h3> | ||||
| <p>Invalid syntax produces helpful error messages:</p> | ||||
| <pre><code>#book AND OR #author=Tolkien</code></pre> | ||||
| <p>Error: "Mixed usage of AND/OR - always use parentheses to group AND/OR expressions."</p> | ||||
|  | ||||
| <h3>Debug Mode</h3> | ||||
| <p>Enable debug mode to see how queries are parsed:</p> | ||||
| <pre><code>#book #author=Tolkien</code></pre> | ||||
| <p>With debug enabled, shows the internal expression tree structure.</p> | ||||
|  | ||||
| <h3>Common Pitfalls</h3> | ||||
| <ul> | ||||
|   <li>Unescaped special characters: Use quotes or backslashes</li> | ||||
|   <li>Missing parentheses in complex boolean expressions</li> | ||||
|   <li>Incorrect property names: Use <code>note.title</code> not <code>title</code></li> | ||||
|   <li>Case sensitivity assumptions: All searches are case-insensitive</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Expression Shortcuts</h2> | ||||
|  | ||||
| <h3>Label Shortcuts</h3> | ||||
| <p>Full syntax:</p> | ||||
| <pre><code>note.labels.category = book</code></pre> | ||||
| <p>Shortcut:</p> | ||||
| <pre><code>#category = book</code></pre> | ||||
|  | ||||
| <h3>Relation Shortcuts</h3> | ||||
| <p>Full syntax:</p> | ||||
| <pre><code>note.relations.author.title *=* Tolkien</code></pre> | ||||
| <p>Shortcut:</p> | ||||
| <pre><code>~author.title *=* Tolkien</code></pre> | ||||
|  | ||||
| <h3>Property Shortcuts</h3> | ||||
| <p>Some properties have convenient shortcuts:</p> | ||||
| <pre><code>note.text *=* content</code></pre> | ||||
| <p>Searches both title and content for "content".</p> | ||||
|  | ||||
| <h2>Real-World Complex Examples</h2> | ||||
|  | ||||
| <h3>Project Management</h3> | ||||
| <pre><code>(#project OR #task) AND #status!=completed AND  | ||||
| (#priority=high OR #dueDate <= TODAY+7) AND  | ||||
| not(note.isArchived=true)  | ||||
| orderBy #priority desc, #dueDate asc</code></pre> | ||||
|  | ||||
| <h3>Research Organization</h3> | ||||
| <pre><code>(#paper OR #article OR #book) AND  | ||||
| ~author.title *=* smith AND  | ||||
| #topic *=* "machine learning" AND  | ||||
| note.dateCreated >= YEAR-2  | ||||
| orderBy #citationCount desc limit 25</code></pre> | ||||
|  | ||||
| <h3>Content Management</h3> | ||||
| <pre><code>note.type=text AND note.contentSize > 5000 AND  | ||||
| #category=documentation AND note.childrenCount >= 3 AND  | ||||
| note.dateModified >= MONTH-1  | ||||
| orderBy note.dateModified desc</code></pre> | ||||
|  | ||||
| <h3>Knowledge Base Maintenance</h3> | ||||
| <pre><code>note.attributeCount = 0 AND note.childrenCount = 0 AND  | ||||
| note.parentCount = 1 AND note.contentSize < 100 AND  | ||||
| note.dateModified < TODAY-90</code></pre> | ||||
| <p>Finds potential cleanup candidates: small, untagged, isolated notes not modified in 90 days.</p> | ||||
|  | ||||
| <h2>Next Steps</h2> | ||||
| <ul> | ||||
|   <li><a class="reference-link" href="#root/_help_search_examples">Search Examples and Use Cases</a> - Practical applications</li> | ||||
|   <li><a class="reference-link" href="#root/_help_search_saved">Saved Searches</a> - Creating reusable search configurations</li> | ||||
|   <li><a class="reference-link" href="#root/_help_search_technical">Technical Search Details</a> - Implementation details and performance tuning</li> | ||||
| </ul> | ||||
							
								
								
									
										360
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Saved-Searches.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										360
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Saved-Searches.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,360 @@ | ||||
| <h1>Saved Searches</h1> | ||||
| <p>Saved searches in Trilium allow you to create dynamic collections of notes that automatically update based on search criteria. They appear as special notes in your tree and provide a powerful way to organize and access related content.</p> | ||||
|  | ||||
| <h2>Understanding Saved Searches</h2> | ||||
| <p>A saved search is a special note type that:</p> | ||||
| <ul> | ||||
|   <li>Stores search criteria and configuration</li> | ||||
|   <li>Dynamically displays matching notes as children</li> | ||||
|   <li>Updates automatically when notes change</li> | ||||
|   <li>Can be bookmarked and accessed like any other note</li> | ||||
|   <li>Supports all search features including ordering and limits</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Creating Saved Searches</h2> | ||||
|  | ||||
| <h3>From Search Dialog</h3> | ||||
| <ol> | ||||
|   <li>Open the search dialog (Ctrl+S or search icon)</li> | ||||
|   <li>Configure your search criteria and options</li> | ||||
|   <li>Click "Save to note" button</li> | ||||
|   <li>Choose a name and location for the saved search</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Manual Creation</h3> | ||||
| <ol> | ||||
|   <li>Create a new note and set its type to "Saved Search"</li> | ||||
|   <li>Configure the search using labels: | ||||
|     <ul> | ||||
|       <li><code>#searchString</code> - The search query</li> | ||||
|       <li><code>#fastSearch</code> - Enable fast search mode</li> | ||||
|       <li><code>#includeArchivedNotes</code> - Include archived notes</li> | ||||
|       <li><code>#orderBy</code> - Sort field</li> | ||||
|       <li><code>#orderDirection</code> - "asc" or "desc"</li> | ||||
|       <li><code>#limit</code> - Maximum number of results</li> | ||||
|     </ul> | ||||
|   </li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Using Search Scripts</h3> | ||||
| <p>For complex logic, create a JavaScript note and link it:</p> | ||||
| <ul> | ||||
|   <li><code>~searchScript</code> - Relation pointing to a backend script note</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Basic Saved Search Examples</h2> | ||||
|  | ||||
| <h3>Simple Text Search</h3> | ||||
| <pre><code>#searchString=project management</code></pre> | ||||
| <p>Finds all notes containing "project management".</p> | ||||
|  | ||||
| <h3>Tag-Based Collection</h3> | ||||
| <pre><code>#searchString=#book #author=Tolkien | ||||
| #orderBy=publicationYear | ||||
| #orderDirection=desc</code></pre> | ||||
| <p>Creates a collection of Tolkien's books ordered by publication year.</p> | ||||
|  | ||||
| <h3>Task Dashboard</h3> | ||||
| <pre><code>#searchString=#task #status!=completed #assignee=me | ||||
| #orderBy=priority | ||||
| #orderDirection=desc | ||||
| #limit=20</code></pre> | ||||
| <p>Shows your top 20 incomplete tasks by priority.</p> | ||||
|  | ||||
| <h3>Recent Activity</h3> | ||||
| <pre><code>#searchString=note.dateModified >= TODAY-7 | ||||
| #orderBy=dateModified | ||||
| #orderDirection=desc | ||||
| #limit=50</code></pre> | ||||
| <p>Shows the 50 most recently modified notes from the last week.</p> | ||||
|  | ||||
| <h2>Advanced Saved Search Patterns</h2> | ||||
|  | ||||
| <h3>Dynamic Date-Based Collections</h3> | ||||
|  | ||||
| <h4>This Week's Content</h4> | ||||
| <pre><code>#searchString=note.dateCreated >= TODAY-7 note.dateCreated < TODAY | ||||
| #orderBy=dateCreated | ||||
| #orderDirection=desc</code></pre> | ||||
|  | ||||
| <h4>Monthly Review Collection</h4> | ||||
| <pre><code>#searchString=#reviewed=false note.dateCreated >= MONTH note.dateCreated < MONTH+1 | ||||
| #orderBy=dateCreated</code></pre> | ||||
|  | ||||
| <h4>Upcoming Deadlines</h4> | ||||
| <pre><code>#searchString=#dueDate >= TODAY #dueDate <= TODAY+14 #status!=completed | ||||
| #orderBy=dueDate | ||||
| #orderDirection=asc</code></pre> | ||||
|  | ||||
| <h3>Project-Specific Collections</h3> | ||||
|  | ||||
| <h4>Project Dashboard</h4> | ||||
| <pre><code>#searchString=#project=alpha (#task OR #milestone OR #document) | ||||
| #orderBy=priority | ||||
| #orderDirection=desc</code></pre> | ||||
|  | ||||
| <h4>Project Health Monitor</h4> | ||||
| <pre><code>#searchString=#project=alpha #status=blocked OR (#dueDate < TODAY #status!=completed) | ||||
| #orderBy=dueDate | ||||
| #orderDirection=asc</code></pre> | ||||
|  | ||||
| <h3>Content Type Collections</h3> | ||||
|  | ||||
| <h4>Documentation Hub</h4> | ||||
| <pre><code>#searchString=(#documentation OR #guide OR #manual) #product=api | ||||
| #orderBy=dateModified | ||||
| #orderDirection=desc</code></pre> | ||||
|  | ||||
| <h4>Learning Path</h4> | ||||
| <pre><code>#searchString=#course #level=beginner #topic=programming | ||||
| #orderBy=difficulty | ||||
| #orderDirection=asc</code></pre> | ||||
|  | ||||
| <h2>Search Script Examples</h2> | ||||
| <p>For complex logic that can't be expressed in search strings, use JavaScript:</p> | ||||
|  | ||||
| <h3>Custom Business Logic</h3> | ||||
| <pre><code>// Find notes that need attention based on complex criteria | ||||
| const api = require('api'); | ||||
|  | ||||
| const cutoffDate = new Date(); | ||||
| cutoffDate.setDate(cutoffDate.getDate() - 30); | ||||
|  | ||||
| const results = []; | ||||
|  | ||||
| // Find high-priority tasks overdue by more than a week | ||||
| const overdueTasks = api.searchForNotes(` | ||||
|     #task #priority=high #dueDate < TODAY-7 #status!=completed | ||||
| `); | ||||
|  | ||||
| // Find projects with no recent activity | ||||
| const staleProjects = api.searchForNotes(` | ||||
|     #project #status=active note.dateModified < TODAY-30 | ||||
| `); | ||||
|  | ||||
| // Find notes with many attributes but no content | ||||
| const overlabeledNotes = api.searchForNotes(` | ||||
|     note.attributeCount > 5 note.contentSize < 100 | ||||
| `); | ||||
|  | ||||
| return [...overdueTasks, ...staleProjects, ...overlabeledNotes] | ||||
|     .map(note => note.noteId);</code></pre> | ||||
|  | ||||
| <h3>Dynamic Tag-Based Grouping</h3> | ||||
| <pre><code>// Group notes by quarter based on creation date | ||||
| const api = require('api'); | ||||
|  | ||||
| const currentYear = new Date().getFullYear(); | ||||
| const results = []; | ||||
|  | ||||
| for (let quarter = 1; quarter <= 4; quarter++) { | ||||
|     const startMonth = (quarter - 1) * 3 + 1; | ||||
|     const endMonth = quarter * 3; | ||||
|      | ||||
|     const quarterNotes = api.searchForNotes(` | ||||
|         note.dateCreated >= "${currentYear}-${String(startMonth).padStart(2, '0')}-01" | ||||
|         note.dateCreated < "${currentYear}-${String(endMonth + 1).padStart(2, '0')}-01" | ||||
|         #project | ||||
|     `); | ||||
|      | ||||
|     results.push(...quarterNotes.map(note => note.noteId)); | ||||
| } | ||||
|  | ||||
| return results;</code></pre> | ||||
|  | ||||
| <h3>Conditional Search Logic</h3> | ||||
| <pre><code>// Smart dashboard that changes based on day of week | ||||
| const api = require('api'); | ||||
|  | ||||
| const today = new Date(); | ||||
| const dayOfWeek = today.getDay(); // 0 = Sunday, 1 = Monday, etc. | ||||
|  | ||||
| let searchQuery; | ||||
|  | ||||
| if (dayOfWeek === 1) { // Monday - weekly planning | ||||
|     searchQuery = '#task #status=planned #week=' + getWeekNumber(today); | ||||
| } else if (dayOfWeek === 5) { // Friday - weekly review | ||||
|     searchQuery = '#task #completed=true #week=' + getWeekNumber(today); | ||||
| } else { // Regular days - focus on today's work | ||||
|     searchQuery = '#task #dueDate=TODAY #status!=completed'; | ||||
| } | ||||
|  | ||||
| const notes = api.searchForNotes(searchQuery); | ||||
| return notes.map(note => note.noteId); | ||||
|  | ||||
| function getWeekNumber(date) { | ||||
|     const firstDay = new Date(date.getFullYear(), 0, 1); | ||||
|     const pastDays = Math.floor((date - firstDay) / 86400000); | ||||
|     return Math.ceil((pastDays + firstDay.getDay() + 1) / 7); | ||||
| }</code></pre> | ||||
|  | ||||
| <h2>Performance Optimization</h2> | ||||
|  | ||||
| <h3>Fast Search for Large Collections</h3> | ||||
| <p>For collections that don't need content search:</p> | ||||
| <pre><code>#searchString=#category=reference #type=article | ||||
| #fastSearch=true | ||||
| #limit=100</code></pre> | ||||
|  | ||||
| <h3>Efficient Ordering</h3> | ||||
| <p>Use indexed properties for better performance:</p> | ||||
| <pre><code>#orderBy=dateCreated | ||||
| #orderBy=title | ||||
| #orderBy=noteId</code></pre> | ||||
| <p>Avoid complex calculated orderings in large collections.</p> | ||||
|  | ||||
| <h3>Result Limiting</h3> | ||||
| <p>Always set reasonable limits for large collections:</p> | ||||
| <pre><code>#limit=50</code></pre> | ||||
| <p>For very large result sets, consider breaking into multiple saved searches.</p> | ||||
|  | ||||
| <h2>Saved Search Organization</h2> | ||||
|  | ||||
| <h3>Hierarchical Organization</h3> | ||||
| <p>Create a folder structure for saved searches:</p> | ||||
| <pre><code>📁 Searches | ||||
| ├── 📁 Projects | ||||
| │   ├── 🔍 Active Projects | ||||
| │   ├── 🔍 Overdue Tasks   | ||||
| │   └── 🔍 Project Archive | ||||
| ├── 📁 Content | ||||
| │   ├── 🔍 Recent Drafts | ||||
| │   ├── 🔍 Published Articles | ||||
| │   └── 🔍 Review Queue | ||||
| └── 📁 Maintenance | ||||
|     ├── 🔍 Untagged Notes | ||||
|     ├── 🔍 Cleanup Candidates | ||||
|     └── 🔍 Orphaned Notes</code></pre> | ||||
|  | ||||
| <h3>Search Naming Conventions</h3> | ||||
| <p>Use clear, descriptive names:</p> | ||||
| <ul> | ||||
|   <li>"Active High-Priority Tasks"</li> | ||||
|   <li>"This Month's Meeting Notes"</li> | ||||
|   <li>"Unprocessed Inbox Items"</li> | ||||
|   <li>"Literature Review Papers"</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Search Labels</h3> | ||||
| <p>Tag saved searches for organization:</p> | ||||
| <pre><code>#searchType=dashboard | ||||
| #searchType=maintenance   | ||||
| #searchType=archive | ||||
| #frequency=daily | ||||
| #frequency=weekly</code></pre> | ||||
|  | ||||
| <h2>Dashboard Creation</h2> | ||||
|  | ||||
| <h3>Personal Dashboard</h3> | ||||
| <p>Combine multiple saved searches in a parent note:</p> | ||||
| <pre><code>📋 My Dashboard | ||||
| ├── 🔍 Today's Tasks | ||||
| ├── 🔍 Urgent Items | ||||
| ├── 🔍 Recent Notes | ||||
| ├── 🔍 Upcoming Deadlines | ||||
| └── 🔍 Weekly Review Items</code></pre> | ||||
|  | ||||
| <h3>Project Dashboard</h3> | ||||
| <pre><code>📋 Project Alpha Dashboard   | ||||
| ├── 🔍 Active Tasks | ||||
| ├── 🔍 Blocked Items | ||||
| ├── 🔍 Recent Updates | ||||
| ├── 🔍 Milestones | ||||
| └── 🔍 Team Notes</code></pre> | ||||
|  | ||||
| <h3>Content Dashboard</h3> | ||||
| <pre><code>📋 Content Management | ||||
| ├── 🔍 Draft Articles | ||||
| ├── 🔍 Review Queue | ||||
| ├── 🔍 Published This Month | ||||
| ├── 🔍 High-Engagement Posts | ||||
| └── 🔍 Content Ideas</code></pre> | ||||
|  | ||||
| <h2>Maintenance and Updates</h2> | ||||
|  | ||||
| <h3>Regular Review</h3> | ||||
| <p>Periodically review saved searches for:</p> | ||||
| <ul> | ||||
|   <li>Outdated search criteria</li> | ||||
|   <li>Performance issues</li> | ||||
|   <li>Unused collections</li> | ||||
|   <li>Scope creep</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Search Evolution</h3> | ||||
| <p>As your note-taking evolves, update searches:</p> | ||||
| <ul> | ||||
|   <li>Add new tags to existing searches</li> | ||||
|   <li>Refine criteria based on usage patterns</li> | ||||
|   <li>Split large collections into smaller ones</li> | ||||
|   <li>Merge rarely-used collections</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Performance Monitoring</h3> | ||||
| <p>Watch for performance issues:</p> | ||||
| <ul> | ||||
|   <li>Slow-loading saved searches</li> | ||||
|   <li>Memory usage with large result sets</li> | ||||
|   <li>Search timeout errors</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Troubleshooting</h2> | ||||
|  | ||||
| <h3>Common Issues</h3> | ||||
|  | ||||
| <h4>Empty Results</h4> | ||||
| <ul> | ||||
|   <li>Check search syntax</li> | ||||
|   <li>Verify tag spellings</li> | ||||
|   <li>Ensure notes have required attributes</li> | ||||
|   <li>Test search components individually</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Performance Problems</h4> | ||||
| <ul> | ||||
|   <li>Add <code>#fastSearch=true</code> for attribute-only searches</li> | ||||
|   <li>Reduce result limits</li> | ||||
|   <li>Simplify complex criteria</li> | ||||
|   <li>Use indexed properties for ordering</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Unexpected Results</h4> | ||||
| <ul> | ||||
|   <li>Enable debug mode to see query parsing</li> | ||||
|   <li>Test search in search dialog first</li> | ||||
|   <li>Check for case sensitivity issues</li> | ||||
|   <li>Verify date formats and ranges</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Best Practices</h3> | ||||
|  | ||||
| <h4>Search Design</h4> | ||||
| <ul> | ||||
|   <li>Start simple and add complexity gradually</li> | ||||
|   <li>Test searches thoroughly before saving</li> | ||||
|   <li>Document complex search logic</li> | ||||
|   <li>Use meaningful names and descriptions</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Performance</h4> | ||||
| <ul> | ||||
|   <li>Set appropriate limits</li> | ||||
|   <li>Use fast search when possible</li> | ||||
|   <li>Avoid overly complex expressions</li> | ||||
|   <li>Monitor search execution time</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Organization</h4> | ||||
| <ul> | ||||
|   <li>Group related searches</li> | ||||
|   <li>Use consistent naming conventions</li> | ||||
|   <li>Archive unused searches</li> | ||||
|   <li>Regular cleanup and maintenance</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Next Steps</h2> | ||||
| <ul> | ||||
|   <li><a class="reference-link" href="#root/_help_search_technical">Technical Search Details</a> - Understanding search performance and implementation</li> | ||||
|   <li><a class="reference-link" href="#root/_help_search_examples">Search Examples and Use Cases</a> - More practical examples</li> | ||||
|   <li><a class="reference-link" href="#root/_help_search_advanced">Advanced Search Expressions</a> - Complex query construction</li> | ||||
| </ul> | ||||
							
								
								
									
										160
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Documentation.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Documentation.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,160 @@ | ||||
| <h1>Trilium Search Documentation</h1> | ||||
| <p>Welcome to the comprehensive guide for Trilium's powerful search capabilities. This documentation covers everything from basic text searches to advanced query expressions and performance optimization.</p> | ||||
|  | ||||
| <h2>Quick Start</h2> | ||||
| <p>New to Trilium search? Start here:</p> | ||||
| <ul> | ||||
|   <li><strong><a class="reference-link" href="#root/_help_search_fundamentals">Search Fundamentals</a></strong> - Basic concepts, syntax, and operators</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Documentation Sections</h2> | ||||
|  | ||||
| <h3>Core Search Features</h3> | ||||
| <ul> | ||||
|   <li><strong><a class="reference-link" href="#root/_help_search_fundamentals">Search Fundamentals</a></strong> - Basic search syntax, operators, and concepts</li> | ||||
|   <li><strong><a class="reference-link" href="#root/_help_search_advanced">Advanced Search Expressions</a></strong> - Complex queries, boolean logic, and relationship traversal</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Practical Applications</h3> | ||||
| <ul> | ||||
|   <li><strong><a class="reference-link" href="#root/_help_search_examples">Search Examples and Use Cases</a></strong> - Real-world examples for common workflows</li> | ||||
|   <li><strong><a class="reference-link" href="#root/_help_search_saved">Saved Searches</a></strong> - Creating dynamic collections and dashboards</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Technical Reference</h3> | ||||
| <ul> | ||||
|   <li><strong><a class="reference-link" href="#root/_help_search_technical">Technical Search Details</a></strong> - Performance, implementation, and optimization</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Key Search Capabilities</h2> | ||||
|  | ||||
| <h3>Full-Text Search</h3> | ||||
| <ul> | ||||
|   <li>Search note titles and content</li> | ||||
|   <li>Exact phrase matching with quotes</li> | ||||
|   <li>Case-insensitive with diacritic normalization</li> | ||||
|   <li>Support for multiple note types (text, code, mermaid, canvas)</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Attribute-Based Search</h3> | ||||
| <ul> | ||||
|   <li>Label searches: <code>#tag</code>, <code>#category=book</code></li> | ||||
|   <li>Relation searches: <code>~author</code>, <code>~author.title=Tolkien</code></li> | ||||
|   <li>Complex attribute combinations</li> | ||||
|   <li>Fuzzy attribute matching</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Property Search</h3> | ||||
| <ul> | ||||
|   <li>Note metadata: <code>note.type=text</code>, <code>note.dateCreated >= TODAY-7</code></li> | ||||
|   <li>Hierarchical queries: <code>note.parents.title=Books</code></li> | ||||
|   <li>Relationship traversal: <code>note.children.labels.status=active</code></li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Advanced Features</h3> | ||||
| <ul> | ||||
|   <li><strong>Progressive Search</strong>: Exact matching first, fuzzy fallback when needed</li> | ||||
|   <li><strong>Fuzzy Search</strong>: Typo tolerance and spelling variations</li> | ||||
|   <li><strong>Boolean Logic</strong>: Complex AND/OR/NOT combinations</li> | ||||
|   <li><strong>Date Arithmetic</strong>: Dynamic date calculations (TODAY-30, YEAR+1)</li> | ||||
|   <li><strong>Regular Expressions</strong>: Pattern matching with <code>%=</code> operator</li> | ||||
|   <li><strong>Ordering and Limiting</strong>: Custom sort orders and result limits</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Search Operators Quick Reference</h2> | ||||
|  | ||||
| <h3>Text Operators</h3> | ||||
| <ul> | ||||
|   <li><code>=</code> - Exact match</li> | ||||
|   <li><code>!=</code> - Not equal</li> | ||||
|   <li><code>*=*</code> - Contains</li> | ||||
|   <li><code>=*</code> - Starts with</li> | ||||
|   <li><code>*=</code> - Ends with</li> | ||||
|   <li><code>%=</code> - Regular expression</li> | ||||
|   <li><code>~=</code> - Fuzzy exact match</li> | ||||
|   <li><code>~*</code> - Fuzzy contains match</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Numeric Operators</h3> | ||||
| <ul> | ||||
|   <li><code>=</code>, <code>!=</code>, <code>></code>, <code>>=</code>, <code><</code>, <code><=</code></li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Boolean Operators</h3> | ||||
| <ul> | ||||
|   <li><code>AND</code>, <code>OR</code>, <code>NOT</code></li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Special Syntax</h3> | ||||
| <ul> | ||||
|   <li><code>#labelName</code> - Label exists</li> | ||||
|   <li><code>#labelName=value</code> - Label equals value</li> | ||||
|   <li><code>~relationName</code> - Relation exists</li> | ||||
|   <li><code>~relationName.property</code> - Relation target property</li> | ||||
|   <li><code>note.property</code> - Note property access</li> | ||||
|   <li><code>"exact phrase"</code> - Quoted phrase search</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Common Search Patterns</h2> | ||||
|  | ||||
| <h3>Simple Searches</h3> | ||||
| <pre><code>hello world          # Find notes containing both words | ||||
| "project management" # Find exact phrase | ||||
| #task               # Find notes with "task" label | ||||
| ~author             # Find notes with "author" relation</code></pre> | ||||
|  | ||||
| <h3>Attribute Searches</h3> | ||||
| <pre><code>#book #author=Tolkien           # Books by Tolkien | ||||
| #task #priority=high #status!=completed  # High-priority incomplete tasks | ||||
| ~project.title *=* alpha        # Notes related to projects with "alpha" in title</code></pre> | ||||
|  | ||||
| <h3>Date-Based Searches</h3> | ||||
| <pre><code>note.dateCreated >= TODAY-7     # Notes created in last week | ||||
| #dueDate <= TODAY+30            # Items due in next 30 days | ||||
| #eventDate = YEAR               # Events scheduled for this year</code></pre> | ||||
|  | ||||
| <h3>Complex Queries</h3> | ||||
| <pre><code>(#book OR #article) AND #topic=programming AND note.dateModified >= MONTH | ||||
| #project AND (#status=active OR #status=pending) AND not(note.isArchived=true)</code></pre> | ||||
|  | ||||
| <h2>Getting Started Checklist</h2> | ||||
| <ol> | ||||
|   <li><strong>Learn Basic Syntax</strong> - Start with simple text and tag searches</li> | ||||
|   <li><strong>Understand Operators</strong> - Master the core operators (<code>=</code>, <code>*=*</code>, etc.)</li> | ||||
|   <li><strong>Practice Attributes</strong> - Use <code>#</code> for labels and <code>~</code> for relations</li> | ||||
|   <li><strong>Try Boolean Logic</strong> - Combine searches with AND/OR/NOT</li> | ||||
|   <li><strong>Explore Properties</strong> - Use <code>note.</code> prefix for metadata searches</li> | ||||
|   <li><strong>Create Saved Searches</strong> - Turn useful queries into dynamic collections</li> | ||||
|   <li><strong>Optimize Performance</strong> - Learn about fast search and limits</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Performance Tips</h2> | ||||
| <ul> | ||||
|   <li><strong>Use Fast Search</strong> for attribute-only queries</li> | ||||
|   <li><strong>Set Reasonable Limits</strong> to prevent large result sets</li> | ||||
|   <li><strong>Start Specific</strong> with the most selective criteria first</li> | ||||
|   <li><strong>Leverage Attributes</strong> instead of content search when possible</li> | ||||
|   <li><strong>Cache Common Queries</strong> as saved searches</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Need Help?</h2> | ||||
| <ul> | ||||
|   <li><strong>Examples</strong>: Check <a class="reference-link" href="#root/_help_search_examples">Search Examples and Use Cases</a> for practical patterns</li> | ||||
|   <li><strong>Complex Queries</strong>: See <a class="reference-link" href="#root/_help_search_advanced">Advanced Search Expressions</a> for sophisticated techniques</li> | ||||
|   <li><strong>Performance Issues</strong>: Review <a class="reference-link" href="#root/_help_search_technical">Technical Search Details</a> for optimization</li> | ||||
|   <li><strong>Dynamic Collections</strong>: Learn about <a class="reference-link" href="#root/_help_search_saved">Saved Searches</a> for automated organization</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Search Workflow Integration</h2> | ||||
| <p>Trilium's search integrates seamlessly with your note-taking workflow:</p> | ||||
| <ul> | ||||
|   <li><strong>Quick Search</strong> (Ctrl+S) for instant access</li> | ||||
|   <li><strong>Saved Searches</strong> for dynamic organization</li> | ||||
|   <li><strong>Search from Subtree</strong> for focused queries</li> | ||||
|   <li><strong>Auto-complete</strong> suggestions in search dialogs</li> | ||||
|   <li><strong>URL-triggered searches</strong> for bookmarkable queries</li> | ||||
| </ul> | ||||
|  | ||||
| <p>Start with the fundamentals and gradually explore advanced features as your needs grow. Trilium's search system is designed to scale from simple text queries to sophisticated knowledge management systems.</p> | ||||
|  | ||||
| <p>Happy searching! 🔍</p> | ||||
							
								
								
									
										245
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Examples-and-Use-Cases.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Examples-and-Use-Cases.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,245 @@ | ||||
| <h1>Search Examples and Use Cases</h1> | ||||
| <p>This guide provides practical examples of how to use Trilium's search capabilities for common organizational patterns and workflows.</p> | ||||
|  | ||||
| <h2>Personal Knowledge Management</h2> | ||||
|  | ||||
| <h3>Research and Learning</h3> | ||||
| <p>Track your learning progress and find related materials:</p> | ||||
| <pre><code>#topic=javascript #status=learning</code></pre> | ||||
| <p>Find all JavaScript materials you're currently learning.</p> | ||||
| <pre><code>#course #completed=false note.dateCreated >= MONTH-1</code></pre> | ||||
| <p>Find courses started in the last month that aren't completed.</p> | ||||
| <pre><code>#book #topic *=* programming #rating >= 4</code></pre> | ||||
| <p>Find highly-rated programming books.</p> | ||||
| <pre><code>#paper ~author.title *=* "Andrew Ng" #field=machine-learning</code></pre> | ||||
| <p>Find machine learning papers by Andrew Ng.</p> | ||||
|  | ||||
| <h3>Meeting and Event Management</h3> | ||||
| <p>Organize meetings, notes, and follow-ups:</p> | ||||
| <pre><code>#meeting note.dateCreated >= TODAY-7 #attendee *=* smith</code></pre> | ||||
| <p>Find this week's meetings with Smith.</p> | ||||
| <pre><code>#meeting #actionItems #status!=completed</code></pre> | ||||
| <p>Find meetings with outstanding action items.</p> | ||||
| <pre><code>#event #date >= TODAY #date <= TODAY+30</code></pre> | ||||
| <p>Find upcoming events in the next 30 days.</p> | ||||
| <pre><code>#meeting #project=alpha note.dateCreated >= MONTH</code></pre> | ||||
| <p>Find this month's meetings about project alpha.</p> | ||||
|  | ||||
| <h3>Note Organization and Cleanup</h3> | ||||
| <p>Maintain and organize your note structure:</p> | ||||
| <pre><code>note.childrenCount = 0 note.parentCount = 1 note.contentSize < 50 note.dateModified < TODAY-180</code></pre> | ||||
| <p>Find small, isolated notes not modified in 6 months (cleanup candidates).</p> | ||||
| <pre><code>note.attributeCount = 0 note.type=text note.contentSize > 1000</code></pre> | ||||
| <p>Find large text notes without any labels (might need categorization).</p> | ||||
| <pre><code>#draft note.dateCreated < TODAY-30</code></pre> | ||||
| <p>Find old draft notes that might need attention.</p> | ||||
| <pre><code>note.parentCount > 3 note.type=text</code></pre> | ||||
| <p>Find notes that are heavily cloned (might indicate important content).</p> | ||||
|  | ||||
| <h2>Project Management</h2> | ||||
|  | ||||
| <h3>Task Tracking</h3> | ||||
| <p>Manage tasks and project progress:</p> | ||||
| <pre><code>#task #priority=high #status!=completed #assignee=me</code></pre> | ||||
| <p>Find your high-priority incomplete tasks.</p> | ||||
| <pre><code>#task #dueDate <= TODAY+3 #dueDate >= TODAY #status!=completed</code></pre> | ||||
| <p>Find tasks due in the next 3 days.</p> | ||||
| <pre><code>#project=website #task #status=blocked</code></pre> | ||||
| <p>Find blocked tasks in the website project.</p> | ||||
| <pre><code>#task #estimatedHours > 0 #actualHours > 0 orderBy note.dateModified desc</code></pre> | ||||
| <p>Find tasks with time tracking data, sorted by recent updates.</p> | ||||
|  | ||||
| <h3>Project Oversight</h3> | ||||
| <p>Monitor project health and progress:</p> | ||||
| <pre><code>#project #status=active note.children.labels.status = blocked</code></pre> | ||||
| <p>Find active projects with blocked tasks.</p> | ||||
| <pre><code>#project #startDate <= TODAY-90 #status!=completed</code></pre> | ||||
| <p>Find projects that started over 90 days ago but aren't completed.</p> | ||||
| <pre><code>#milestone #targetDate <= TODAY #status!=achieved</code></pre> | ||||
| <p>Find overdue milestones.</p> | ||||
| <pre><code>#project orderBy note.childrenCount desc limit 10</code></pre> | ||||
| <p>Find the 10 largest projects by number of sub-notes.</p> | ||||
|  | ||||
| <h3>Resource Planning</h3> | ||||
| <p>Track resources and dependencies:</p> | ||||
| <pre><code>#resource #type=person #availability < 50</code></pre> | ||||
| <p>Find people with low availability.</p> | ||||
| <pre><code>#dependency #status=pending #project=mobile-app</code></pre> | ||||
| <p>Find pending dependencies for the mobile app project.</p> | ||||
| <pre><code>#budget #project #spent > #allocated</code></pre> | ||||
| <p>Find projects over budget.</p> | ||||
|  | ||||
| <h2>Content Creation and Writing</h2> | ||||
|  | ||||
| <h3>Writing Projects</h3> | ||||
| <p>Manage articles, books, and documentation:</p> | ||||
| <pre><code>#article #status=draft #wordCount >= 1000</code></pre> | ||||
| <p>Find substantial draft articles.</p> | ||||
| <pre><code>#chapter #book=novel #status=outline</code></pre> | ||||
| <p>Find novel chapters still in outline stage.</p> | ||||
| <pre><code>#blog-post #published=false #topic=technology</code></pre> | ||||
| <p>Find unpublished technology blog posts.</p> | ||||
| <pre><code>#documentation #lastReviewed < TODAY-90 #product=api</code></pre> | ||||
| <p>Find API documentation not reviewed in 90 days.</p> | ||||
|  | ||||
| <h3>Editorial Workflow</h3> | ||||
| <p>Track editing and publication status:</p> | ||||
| <pre><code>#article #editor=jane #status=review</code></pre> | ||||
| <p>Find articles assigned to Jane for review.</p> | ||||
| <pre><code>#manuscript #submissionDate >= TODAY-30 #status=pending</code></pre> | ||||
| <p>Find manuscripts submitted in the last 30 days still pending.</p> | ||||
| <pre><code>#publication #acceptanceDate >= YEAR #status=accepted</code></pre> | ||||
| <p>Find accepted publications this year.</p> | ||||
|  | ||||
| <h3>Content Research</h3> | ||||
| <p>Organize research materials and sources:</p> | ||||
| <pre><code>#source #reliability >= 8 #topic *=* climate</code></pre> | ||||
| <p>Find reliable sources about climate topics.</p> | ||||
| <pre><code>#quote #author *=* Einstein #verified=true</code></pre> | ||||
| <p>Find verified Einstein quotes.</p> | ||||
| <pre><code>#citation #used=false #relevance=high</code></pre> | ||||
| <p>Find high-relevance citations not yet used.</p> | ||||
|  | ||||
| <h2>Business and Professional Use</h2> | ||||
|  | ||||
| <h3>Client Management</h3> | ||||
| <p>Track client relationships and projects:</p> | ||||
| <pre><code>#client=acme #project #status=active</code></pre> | ||||
| <p>Find active projects for ACME client.</p> | ||||
| <pre><code>#meeting #client #date >= MONTH #followUp=required</code></pre> | ||||
| <p>Find client meetings this month requiring follow-up.</p> | ||||
| <pre><code>#contract #renewalDate <= TODAY+60 #renewalDate >= TODAY</code></pre> | ||||
| <p>Find contracts expiring in the next 60 days.</p> | ||||
| <pre><code>#invoice #status=unpaid #dueDate < TODAY</code></pre> | ||||
| <p>Find overdue unpaid invoices.</p> | ||||
|  | ||||
| <h3>Process Documentation</h3> | ||||
| <p>Maintain procedures and workflows:</p> | ||||
| <pre><code>#procedure #department=engineering #lastUpdated < TODAY-365</code></pre> | ||||
| <p>Find engineering procedures not updated in a year.</p> | ||||
| <pre><code>#workflow #status=active #automation=possible</code></pre> | ||||
| <p>Find active workflows that could be automated.</p> | ||||
| <pre><code>#checklist #process=onboarding #role=developer</code></pre> | ||||
| <p>Find onboarding checklists for developers.</p> | ||||
|  | ||||
| <h3>Compliance and Auditing</h3> | ||||
| <p>Track compliance requirements and audits:</p> | ||||
| <pre><code>#compliance #standard=sox #nextReview <= TODAY+30</code></pre> | ||||
| <p>Find SOX compliance items due for review soon.</p> | ||||
| <pre><code>#audit #finding #severity=high #status!=resolved</code></pre> | ||||
| <p>Find unresolved high-severity audit findings.</p> | ||||
| <pre><code>#policy #department=hr #effectiveDate >= YEAR</code></pre> | ||||
| <p>Find HR policies that became effective this year.</p> | ||||
|  | ||||
| <h2>Academic and Educational Use</h2> | ||||
|  | ||||
| <h3>Course Management</h3> | ||||
| <p>Organize courses and educational content:</p> | ||||
| <pre><code>#course #semester=fall-2024 #assignment #dueDate >= TODAY</code></pre> | ||||
| <p>Find upcoming assignments for fall 2024 courses.</p> | ||||
| <pre><code>#lecture #course=physics #topic *=* quantum</code></pre> | ||||
| <p>Find physics lectures about quantum topics.</p> | ||||
| <pre><code>#student #grade < 70 #course=mathematics</code></pre> | ||||
| <p>Find students struggling in mathematics.</p> | ||||
| <pre><code>#syllabus #course #lastUpdated < TODAY-180</code></pre> | ||||
| <p>Find syllabi not updated in 6 months.</p> | ||||
|  | ||||
| <h3>Research Management</h3> | ||||
| <p>Track research projects and publications:</p> | ||||
| <pre><code>#experiment #status=running #endDate <= TODAY+7</code></pre> | ||||
| <p>Find experiments ending in the next week.</p> | ||||
| <pre><code>#dataset #size > 1000000 #cleaned=true #public=false</code></pre> | ||||
| <p>Find large, cleaned, private datasets.</p> | ||||
| <pre><code>#hypothesis #tested=false #priority=high</code></pre> | ||||
| <p>Find high-priority untested hypotheses.</p> | ||||
| <pre><code>#collaboration #institution *=* stanford #status=active</code></pre> | ||||
| <p>Find active collaborations with Stanford.</p> | ||||
|  | ||||
| <h3>Grant and Funding</h3> | ||||
| <p>Manage funding applications and requirements:</p> | ||||
| <pre><code>#grant #deadline <= TODAY+30 #deadline >= TODAY #status=in-progress</code></pre> | ||||
| <p>Find grant applications due in the next 30 days.</p> | ||||
| <pre><code>#funding #amount >= 100000 #status=awarded #startDate >= YEAR</code></pre> | ||||
| <p>Find large grants awarded this year.</p> | ||||
| <pre><code>#report #funding #dueDate <= TODAY+14 #status!=submitted</code></pre> | ||||
| <p>Find funding reports due in 2 weeks.</p> | ||||
|  | ||||
| <h2>Technical Documentation</h2> | ||||
|  | ||||
| <h3>Code and Development</h3> | ||||
| <p>Track code-related notes and documentation:</p> | ||||
| <pre><code>#bug #severity=critical #status!=fixed #product=webapp</code></pre> | ||||
| <p>Find critical unfixed bugs in the web app.</p> | ||||
| <pre><code>#feature #version=2.0 #status=implemented #tested=false</code></pre> | ||||
| <p>Find version 2.0 features that are implemented but not tested.</p> | ||||
| <pre><code>#api #endpoint #deprecated=true #removalDate <= TODAY+90</code></pre> | ||||
| <p>Find deprecated API endpoints scheduled for removal soon.</p> | ||||
| <pre><code>#architecture #component=database #lastReviewed < TODAY-180</code></pre> | ||||
| <p>Find database architecture documentation not reviewed in 6 months.</p> | ||||
|  | ||||
| <h3>System Administration</h3> | ||||
| <p>Manage infrastructure and operations:</p> | ||||
| <pre><code>#server #status=maintenance #scheduledDate >= TODAY #scheduledDate <= TODAY+7</code></pre> | ||||
| <p>Find servers scheduled for maintenance this week.</p> | ||||
| <pre><code>#backup #status=failed #date >= TODAY-7</code></pre> | ||||
| <p>Find backup failures in the last week.</p> | ||||
| <pre><code>#security #vulnerability #severity=high #patched=false</code></pre> | ||||
| <p>Find unpatched high-severity vulnerabilities.</p> | ||||
| <pre><code>#monitoring #alert #frequency > 10 #period=week</code></pre> | ||||
| <p>Find alerts triggering more than 10 times per week.</p> | ||||
|  | ||||
| <h2>Data Analysis and Reporting</h2> | ||||
|  | ||||
| <h3>Performance Tracking</h3> | ||||
| <p>Monitor metrics and KPIs:</p> | ||||
| <pre><code>#metric #kpi=true #trend=declining #period=month</code></pre> | ||||
| <p>Find declining monthly KPIs.</p> | ||||
| <pre><code>#report #frequency=weekly #lastGenerated < TODAY-10</code></pre> | ||||
| <p>Find weekly reports that haven't been generated in 10 days.</p> | ||||
| <pre><code>#dashboard #stakeholder=executive #lastUpdated < TODAY-7</code></pre> | ||||
| <p>Find executive dashboards not updated this week.</p> | ||||
|  | ||||
| <h3>Trend Analysis</h3> | ||||
| <p>Track patterns and changes over time:</p> | ||||
| <pre><code>#data #source=sales #period=quarter #analyzed=false</code></pre> | ||||
| <p>Find unanalyzed quarterly sales data.</p> | ||||
| <pre><code>#trend #direction=up #significance=high #period=month</code></pre> | ||||
| <p>Find significant positive monthly trends.</p> | ||||
| <pre><code>#forecast #accuracy < 80 #model=linear #period=quarter</code></pre> | ||||
| <p>Find inaccurate quarterly linear forecasts.</p> | ||||
|  | ||||
| <h2>Search Strategy Tips</h2> | ||||
|  | ||||
| <h3>Building Effective Queries</h3> | ||||
| <ol> | ||||
|   <li><strong>Start Specific</strong>: Begin with the most selective criteria</li> | ||||
|   <li><strong>Add Gradually</strong>: Build complexity incrementally</li> | ||||
|   <li><strong>Test Components</strong>: Verify each part of complex queries</li> | ||||
|   <li><strong>Use Shortcuts</strong>: Leverage <code>#</code> and <code>~</code> shortcuts for efficiency</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Performance Optimization</h3> | ||||
| <ol> | ||||
|   <li><strong>Use Fast Search</strong>: For large databases, enable fast search when content isn't needed</li> | ||||
|   <li><strong>Limit Results</strong>: Add limits to prevent overwhelming result sets</li> | ||||
|   <li><strong>Order Strategically</strong>: Put the most useful results first</li> | ||||
|   <li><strong>Cache Common Queries</strong>: Save frequently used searches</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Maintenance Patterns</h3> | ||||
| <p>Regular queries for note maintenance:</p> | ||||
| <pre><code># Weekly cleanup check | ||||
| note.attributeCount = 0 note.type=text note.contentSize < 100 note.dateModified < TODAY-30 | ||||
|  | ||||
| # Monthly project review   | ||||
| #project #status=active note.dateModified < TODAY-30 | ||||
|  | ||||
| # Quarterly archive review | ||||
| note.isArchived=false note.dateModified < TODAY-90 note.childrenCount = 0</code></pre> | ||||
|  | ||||
| <h2>Next Steps</h2> | ||||
| <ul> | ||||
|   <li><a class="reference-link" href="#root/_help_search_saved">Saved Searches</a> - Convert these examples into reusable saved searches</li> | ||||
|   <li><a class="reference-link" href="#root/_help_search_technical">Technical Search Details</a> - Understanding performance and implementation</li> | ||||
|   <li><a class="reference-link" href="#root/_help_search_fundamentals">Search Fundamentals</a> - Review basic concepts and syntax</li> | ||||
| </ul> | ||||
							
								
								
									
										181
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Fundamentals.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Fundamentals.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | ||||
| <h1>Search Fundamentals</h1> | ||||
| <p>Trilium's search system is a powerful tool for finding and organizing notes. It supports multiple search modes, from simple text queries to complex expressions using attributes, relationships, and note properties.</p> | ||||
|  | ||||
| <h2>Search Types Overview</h2> | ||||
| <p>Trilium provides three main search approaches:</p> | ||||
| <ol> | ||||
|   <li><strong>Full-text Search</strong> - Searches within note titles and content</li> | ||||
|   <li><strong>Attribute Search</strong> - Searches based on labels and relations attached to notes</li> | ||||
|   <li><strong>Property Search</strong> - Searches based on note metadata (type, creation date, etc.)</li> | ||||
| </ol> | ||||
| <p>These can be combined in powerful ways to create precise queries.</p> | ||||
|  | ||||
| <h2>Basic Search Syntax</h2> | ||||
|  | ||||
| <h3>Simple Text Search</h3> | ||||
| <pre><code>hello world</code></pre> | ||||
| <p>Finds notes containing both "hello" and "world" anywhere in the title or content.</p> | ||||
|  | ||||
| <h3>Quoted Text Search</h3> | ||||
| <pre><code>"hello world"</code></pre> | ||||
| <p>Finds notes containing the exact phrase "hello world".</p> | ||||
|  | ||||
| <h3>Attribute Search</h3> | ||||
| <pre><code>#tag</code></pre> | ||||
| <p>Finds notes with the label "tag".</p> | ||||
| <pre><code>#category=book</code></pre> | ||||
| <p>Finds notes with label "category" set to "book".</p> | ||||
|  | ||||
| <h3>Relation Search</h3> | ||||
| <pre><code>~author</code></pre> | ||||
| <p>Finds notes with a relation named "author".</p> | ||||
| <pre><code>~author.title=Tolkien</code></pre> | ||||
| <p>Finds notes with an "author" relation pointing to a note titled "Tolkien".</p> | ||||
|  | ||||
| <h2>Search Operators</h2> | ||||
|  | ||||
| <h3>Text Operators</h3> | ||||
| <ul> | ||||
|   <li><code>=</code> - Exact match</li> | ||||
|   <li><code>!=</code> - Not equal</li> | ||||
|   <li><code>*=*</code> - Contains (substring)</li> | ||||
|   <li><code>=*</code> - Starts with</li> | ||||
|   <li><code>*=</code> - Ends with</li> | ||||
|   <li><code>%=</code> - Regular expression match</li> | ||||
|   <li><code>~=</code> - Fuzzy exact match</li> | ||||
|   <li><code>~*</code> - Fuzzy contains match</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Numeric Operators</h3> | ||||
| <ul> | ||||
|   <li><code>=</code> - Equal</li> | ||||
|   <li><code>!=</code> - Not equal</li> | ||||
|   <li><code>></code> - Greater than</li> | ||||
|   <li><code>>=</code> - Greater than or equal</li> | ||||
|   <li><code><</code> - Less than</li> | ||||
|   <li><code><=</code> - Less than or equal</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Boolean Operators</h3> | ||||
| <ul> | ||||
|   <li><code>AND</code> - Both conditions must be true</li> | ||||
|   <li><code>OR</code> - Either condition must be true</li> | ||||
|   <li><code>NOT</code> or <code>not()</code> - Condition must be false</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Search Context and Scope</h2> | ||||
|  | ||||
| <h3>Search Scope</h3> | ||||
| <p>By default, search covers:</p> | ||||
| <ul> | ||||
|   <li>Note titles</li> | ||||
|   <li>Note content (for text-based note types)</li> | ||||
|   <li>Label names and values</li> | ||||
|   <li>Relation names</li> | ||||
|   <li>Note properties</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Fast Search Mode</h3> | ||||
| <p>When enabled, fast search:</p> | ||||
| <ul> | ||||
|   <li>Searches only titles and attributes</li> | ||||
|   <li>Skips note content</li> | ||||
|   <li>Provides faster results for large databases</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Archived Notes</h3> | ||||
| <ul> | ||||
|   <li>Excluded by default</li> | ||||
|   <li>Can be included with "Include archived" option</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Case Sensitivity and Normalization</h2> | ||||
| <ul> | ||||
|   <li>All searches are case-insensitive</li> | ||||
|   <li>Diacritics are normalized ("café" matches "cafe")</li> | ||||
|   <li>Unicode characters are properly handled</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Performance Considerations</h2> | ||||
|  | ||||
| <h3>Content Size Limits</h3> | ||||
| <ul> | ||||
|   <li>Note content is limited to 10MB for search processing</li> | ||||
|   <li>Larger notes are still searchable by title and attributes</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Progressive Search Strategy</h3> | ||||
| <ol> | ||||
|   <li><strong>Exact Search Phase</strong>: Fast exact matching (handles 90%+ of searches)</li> | ||||
|   <li><strong>Fuzzy Search Phase</strong>: Activated when exact search returns fewer than 5 high-quality results</li> | ||||
|   <li><strong>Result Ordering</strong>: Exact matches always appear before fuzzy matches</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Search Optimization Tips</h3> | ||||
| <ul> | ||||
|   <li>Use specific terms rather than very common words</li> | ||||
|   <li>Combine full-text with attribute searches for precision</li> | ||||
|   <li>Use fast search for large databases when content search isn't needed</li> | ||||
|   <li>Limit results when dealing with very large result sets</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Special Characters and Escaping</h2> | ||||
|  | ||||
| <h3>Reserved Characters</h3> | ||||
| <p>These characters have special meaning in search queries:</p> | ||||
| <ul> | ||||
|   <li><code>#</code> - Label indicator</li> | ||||
|   <li><code>~</code> - Relation indicator</li> | ||||
|   <li><code>()</code> - Grouping</li> | ||||
|   <li><code>"</code> <code>'</code> <code>`</code> - Quotes for exact phrases</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Escaping Special Characters</h3> | ||||
| <p>Use backslash to search for literal special characters:</p> | ||||
| <pre><code>\#hashtag</code></pre> | ||||
| <p>Searches for the literal text "#hashtag" instead of a label.</p> | ||||
|  | ||||
| <p>Use quotes to include special characters in phrases:</p> | ||||
| <pre><code>"note.txt file"</code></pre> | ||||
| <p>Searches for the exact phrase including the dot.</p> | ||||
|  | ||||
| <h2>Date and Time Values</h2> | ||||
|  | ||||
| <h3>Special Date Keywords</h3> | ||||
| <ul> | ||||
|   <li><code>TODAY</code> - Current date</li> | ||||
|   <li><code>NOW</code> - Current date and time</li> | ||||
|   <li><code>MONTH</code> - Current month</li> | ||||
|   <li><code>YEAR</code> - Current year</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Date Arithmetic</h3> | ||||
| <pre><code>#dateCreated >= TODAY-30</code></pre> | ||||
| <p>Finds notes created in the last 30 days.</p> | ||||
| <pre><code>#eventDate = YEAR+1</code></pre> | ||||
| <p>Finds notes with eventDate set to next year.</p> | ||||
|  | ||||
| <h2>Search Results and Scoring</h2> | ||||
|  | ||||
| <h3>Result Ranking</h3> | ||||
| <p>Results are ordered by:</p> | ||||
| <ol> | ||||
|   <li>Relevance score (based on term frequency and position)</li> | ||||
|   <li>Note depth (closer to root ranks higher)</li> | ||||
|   <li>Alphabetical order for ties</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Progressive Search Behavior</h3> | ||||
| <ul> | ||||
|   <li>Exact matches always rank before fuzzy matches</li> | ||||
|   <li>High-quality exact matches prevent fuzzy search activation</li> | ||||
|   <li>Fuzzy matches help find content with typos or variations</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Next Steps</h2> | ||||
| <ul> | ||||
|   <li><a class="reference-link" href="#root/_help_search_advanced">Advanced Search Expressions</a> - Complex queries and combinations</li> | ||||
|   <li><a class="reference-link" href="#root/_help_search_examples">Search Examples and Use Cases</a> - Practical applications</li> | ||||
|   <li><a class="reference-link" href="#root/_help_search_saved">Saved Searches</a> - Creating dynamic collections</li> | ||||
|   <li><a class="reference-link" href="#root/_help_search_technical">Technical Search Details</a> - Under-the-hood implementation</li> | ||||
| </ul> | ||||
							
								
								
									
										499
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Technical-Search-Details.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										499
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Technical-Search-Details.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,499 @@ | ||||
| <h1>Technical Search Details</h1> | ||||
| <p>This guide provides technical information about Trilium's search implementation, performance characteristics, and optimization strategies for power users and administrators.</p> | ||||
|  | ||||
| <h2>Search Architecture Overview</h2> | ||||
|  | ||||
| <h3>Three-Layer Search System</h3> | ||||
| <p>Trilium's search operates across three cache layers:</p> | ||||
| <ol> | ||||
|   <li><strong>Becca (Backend Cache)</strong>: Server-side entity cache containing notes, attributes, and relationships</li> | ||||
|   <li><strong>Froca (Frontend Cache)</strong>: Client-side mirror providing fast UI updates</li> | ||||
|   <li><strong>Database Layer</strong>: SQLite database with FTS (Full-Text Search) support</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Search Processing Pipeline</h3> | ||||
| <ol> | ||||
|   <li><strong>Lexical Analysis</strong>: Query parsing and tokenization</li> | ||||
|   <li><strong>Expression Building</strong>: Converting tokens to executable expressions</li> | ||||
|   <li><strong>Progressive Execution</strong>: Exact search followed by optional fuzzy search</li> | ||||
|   <li><strong>Result Scoring</strong>: Relevance calculation and ranking</li> | ||||
|   <li><strong>Result Presentation</strong>: Formatting and highlighting</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Query Processing Details</h2> | ||||
|  | ||||
| <h3>Lexical Analysis (Lex)</h3> | ||||
| <p>The lexer breaks down search queries into components:</p> | ||||
| <pre><code>// Input: 'project #status=active note.dateCreated >= TODAY-7' | ||||
| // Output: | ||||
| { | ||||
|   fulltextTokens: ['project'], | ||||
|   expressionTokens: ['#status', '=', 'active', 'note', '.', 'dateCreated', '>=', 'TODAY-7'] | ||||
| }</code></pre> | ||||
|  | ||||
| <h4>Token Types</h4> | ||||
| <ul> | ||||
|   <li><strong>Fulltext Tokens</strong>: Regular search terms</li> | ||||
|   <li><strong>Expression Tokens</strong>: Attributes, operators, and property references</li> | ||||
|   <li><strong>Quoted Strings</strong>: Exact phrase matches</li> | ||||
|   <li><strong>Escaped Characters</strong>: Literal special characters</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Expression Building (Parse)</h3> | ||||
| <p>Tokens are converted into executable expression trees:</p> | ||||
| <pre><code>// Expression tree for: #book AND #author=Tolkien | ||||
| AndExp([ | ||||
|   AttributeExistsExp('label', 'book'), | ||||
|   LabelComparisonExp('label', 'author', equals('tolkien')) | ||||
| ])</code></pre> | ||||
|  | ||||
| <h4>Expression Types</h4> | ||||
| <ul> | ||||
|   <li><code>AndExp</code>, <code>OrExp</code>, <code>NotExp</code>: Boolean logic</li> | ||||
|   <li><code>AttributeExistsExp</code>: Label/relation existence</li> | ||||
|   <li><code>LabelComparisonExp</code>: Label value comparison</li> | ||||
|   <li><code>RelationWhereExp</code>: Relation target queries</li> | ||||
|   <li><code>PropertyComparisonExp</code>: Note property filtering</li> | ||||
|   <li><code>NoteContentFulltextExp</code>: Content search</li> | ||||
|   <li><code>OrderByAndLimitExp</code>: Result ordering and limiting</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Progressive Search Strategy</h3> | ||||
|  | ||||
| <h4>Phase 1: Exact Search</h4> | ||||
| <pre><code>// Fast exact matching | ||||
| const exactResults = performSearch(expression, searchContext, false);</code></pre> | ||||
| <p>Characteristics:</p> | ||||
| <ul> | ||||
|   <li>Substring matching for text</li> | ||||
|   <li>Exact attribute matching</li> | ||||
|   <li>Property-based filtering</li> | ||||
|   <li>Handles 90%+ of searches</li> | ||||
|   <li>Sub-second response time</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Phase 2: Fuzzy Fallback</h4> | ||||
| <pre><code>// Activated when exact results < 5 high-quality matches | ||||
| if (highQualityResults.length < 5) { | ||||
|   const fuzzyResults = performSearch(expression, searchContext, true); | ||||
|   return mergeExactAndFuzzyResults(exactResults, fuzzyResults); | ||||
| }</code></pre> | ||||
| <p>Characteristics:</p> | ||||
| <ul> | ||||
|   <li>Edit distance calculations</li> | ||||
|   <li>Phrase proximity matching</li> | ||||
|   <li>Typo tolerance</li> | ||||
|   <li>Performance safeguards</li> | ||||
|   <li>Exact matches always rank first</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Performance Characteristics</h2> | ||||
|  | ||||
| <h3>Search Limits and Thresholds</h3> | ||||
| <table> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th>Parameter</th> | ||||
|       <th>Value</th> | ||||
|       <th>Purpose</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
|     <tr> | ||||
|       <td><code>MAX_SEARCH_CONTENT_SIZE</code></td> | ||||
|       <td>2MB</td> | ||||
|       <td>Database-level content filtering</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>MIN_FUZZY_TOKEN_LENGTH</code></td> | ||||
|       <td>3 chars</td> | ||||
|       <td>Minimum length for fuzzy matching</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>MAX_EDIT_DISTANCE</code></td> | ||||
|       <td>2 chars</td> | ||||
|       <td>Maximum character changes for fuzzy</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>MAX_PHRASE_PROXIMITY</code></td> | ||||
|       <td>10 words</td> | ||||
|       <td>Maximum distance for phrase matching</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>RESULT_SUFFICIENCY_THRESHOLD</code></td> | ||||
|       <td>5 results</td> | ||||
|       <td>Threshold for fuzzy activation</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>ABSOLUTE_MAX_CONTENT_SIZE</code></td> | ||||
|       <td>100MB</td> | ||||
|       <td>Hard limit to prevent system crash</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|       <td><code>ABSOLUTE_MAX_WORD_COUNT</code></td> | ||||
|       <td>2M words</td> | ||||
|       <td>Hard limit for word processing</td> | ||||
|     </tr> | ||||
|   </tbody> | ||||
| </table> | ||||
|  | ||||
| <h3>Performance Optimization</h3> | ||||
|  | ||||
| <h4>Database-Level Optimizations</h4> | ||||
| <pre><code>-- Content size filtering at database level | ||||
| SELECT noteId, type, mime, content, isProtected | ||||
| FROM notes JOIN blobs USING (blobId) | ||||
| WHERE type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')  | ||||
|   AND isDeleted = 0  | ||||
|   AND LENGTH(content) < 2097152  -- 2MB limit</code></pre> | ||||
|  | ||||
| <h4>Memory Management</h4> | ||||
| <ul> | ||||
|   <li>Single-array edit distance calculation</li> | ||||
|   <li>Early termination for distant matches</li> | ||||
|   <li>Progressive content processing</li> | ||||
|   <li>Cached regular expressions</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Search Context Optimization</h4> | ||||
| <pre><code>// Efficient search context configuration | ||||
| const searchContext = new SearchContext({ | ||||
|   fastSearch: true,           // Skip content search | ||||
|   limit: 50,                 // Reasonable result limit | ||||
|   orderBy: 'dateCreated',    // Use indexed property | ||||
|   includeArchivedNotes: false // Reduce search space | ||||
| });</code></pre> | ||||
|  | ||||
| <h2>Fuzzy Search Implementation</h2> | ||||
|  | ||||
| <h3>Edit Distance Algorithm</h3> | ||||
| <p>Trilium uses an optimized Levenshtein distance calculation:</p> | ||||
| <pre><code>// Optimized single-array implementation | ||||
| function calculateOptimizedEditDistance(str1, str2, maxDistance) { | ||||
|   // Early termination checks | ||||
|   if (Math.abs(str1.length - str2.length) > maxDistance) { | ||||
|     return maxDistance + 1; | ||||
|   } | ||||
|    | ||||
|   // Single array optimization | ||||
|   let previousRow = Array.from({ length: str2.length + 1 }, (_, i) => i); | ||||
|   let currentRow = new Array(str2.length + 1); | ||||
|    | ||||
|   for (let i = 1; i <= str1.length; i++) { | ||||
|     currentRow[0] = i; | ||||
|     let minInRow = i; | ||||
|      | ||||
|     for (let j = 1; j <= str2.length; j++) { | ||||
|       const cost = str1[i - 1] === str2[j - 1] ? 0 : 1; | ||||
|       currentRow[j] = Math.min( | ||||
|         previousRow[j] + 1,        // deletion | ||||
|         currentRow[j - 1] + 1,     // insertion | ||||
|         previousRow[j - 1] + cost  // substitution | ||||
|       ); | ||||
|       minInRow = Math.min(minInRow, currentRow[j]); | ||||
|     } | ||||
|      | ||||
|     // Early termination if row minimum exceeds threshold | ||||
|     if (minInRow > maxDistance) return maxDistance + 1; | ||||
|      | ||||
|     [previousRow, currentRow] = [currentRow, previousRow]; | ||||
|   } | ||||
|    | ||||
|   return previousRow[str2.length]; | ||||
| }</code></pre> | ||||
|  | ||||
| <h3>Phrase Proximity Matching</h3> | ||||
| <p>For multi-token fuzzy searches:</p> | ||||
| <pre><code>// Check if tokens appear within reasonable proximity | ||||
| function hasProximityMatch(tokenPositions, maxDistance = 10) { | ||||
|   // For 2 tokens, simple distance check | ||||
|   if (tokenPositions.length === 2) { | ||||
|     const [pos1, pos2] = tokenPositions; | ||||
|     return pos1.some(p1 => pos2.some(p2 => Math.abs(p1 - p2) <= maxDistance)); | ||||
|   } | ||||
|    | ||||
|   // For multiple tokens, find sequence within range | ||||
|   const findSequence = (remaining, currentPos) => { | ||||
|     if (remaining.length === 0) return true; | ||||
|     const [nextPositions, ...rest] = remaining; | ||||
|     return nextPositions.some(pos =>  | ||||
|       Math.abs(pos - currentPos) <= maxDistance &&  | ||||
|       findSequence(rest, pos) | ||||
|     ); | ||||
|   }; | ||||
|    | ||||
|   const [firstPositions, ...rest] = tokenPositions; | ||||
|   return firstPositions.some(startPos => findSequence(rest, startPos)); | ||||
| }</code></pre> | ||||
|  | ||||
| <h2>Indexing and Storage</h2> | ||||
|  | ||||
| <h3>Database Schema Optimization</h3> | ||||
| <pre><code>-- Relevant indexes for search performance | ||||
| CREATE INDEX idx_notes_type ON notes(type); | ||||
| CREATE INDEX idx_notes_isDeleted ON notes(isDeleted); | ||||
| CREATE INDEX idx_notes_dateCreated ON notes(dateCreated); | ||||
| CREATE INDEX idx_notes_dateModified ON notes(dateModified); | ||||
| CREATE INDEX idx_attributes_name ON attributes(name); | ||||
| CREATE INDEX idx_attributes_type ON attributes(type); | ||||
| CREATE INDEX idx_attributes_value ON attributes(value);</code></pre> | ||||
|  | ||||
| <h3>Content Processing</h3> | ||||
| <p>Notes are processed differently based on type:</p> | ||||
| <pre><code>// Content preprocessing by note type | ||||
| function preprocessContent(content, type, mime) { | ||||
|   content = normalize(content.toString()); | ||||
|    | ||||
|   if (type === "text" && mime === "text/html") { | ||||
|     content = stripTags(content); | ||||
|     content = content.replace(/ /g, " "); | ||||
|   } else if (type === "mindMap" && mime === "application/json") { | ||||
|     content = processMindmapContent(content); | ||||
|   } else if (type === "canvas" && mime === "application/json") { | ||||
|     const canvasData = JSON.parse(content); | ||||
|     const textElements = canvasData.elements | ||||
|       .filter(el => el.type === "text" && el.text) | ||||
|       .map(el => el.text); | ||||
|     content = normalize(textElements.join(" ")); | ||||
|   } | ||||
|    | ||||
|   return content.trim(); | ||||
| }</code></pre> | ||||
|  | ||||
| <h2>Search Result Processing</h2> | ||||
|  | ||||
| <h3>Scoring Algorithm</h3> | ||||
| <p>Results are scored based on multiple factors:</p> | ||||
| <pre><code>function computeScore(fulltextQuery, highlightedTokens, enableFuzzyMatching) { | ||||
|   let score = 0; | ||||
|    | ||||
|   // Title matches get higher score | ||||
|   if (this.noteTitle.toLowerCase().includes(fulltextQuery.toLowerCase())) { | ||||
|     score += 10; | ||||
|   } | ||||
|    | ||||
|   // Path matches (hierarchical context) | ||||
|   const pathMatch = this.notePathArray.some(pathNote =>  | ||||
|     pathNote.title.toLowerCase().includes(fulltextQuery.toLowerCase()) | ||||
|   ); | ||||
|   if (pathMatch) score += 5; | ||||
|    | ||||
|   // Attribute matches | ||||
|   score += this.attributeMatches * 3; | ||||
|    | ||||
|   // Content snippet quality | ||||
|   if (this.contentSnippet && this.contentSnippet.length > 0) { | ||||
|     score += 2; | ||||
|   } | ||||
|    | ||||
|   // Fuzzy match penalty | ||||
|   if (enableFuzzyMatching && this.isFuzzyMatch) { | ||||
|     score *= 0.8; // 20% penalty for fuzzy matches | ||||
|   } | ||||
|    | ||||
|   return score; | ||||
| }</code></pre> | ||||
|  | ||||
| <h3>Result Merging</h3> | ||||
| <p>Exact and fuzzy results are carefully merged:</p> | ||||
| <pre><code>function mergeExactAndFuzzyResults(exactResults, fuzzyResults) { | ||||
|   // Deduplicate - exact results take precedence | ||||
|   const exactNoteIds = new Set(exactResults.map(r => r.noteId)); | ||||
|   const additionalFuzzyResults = fuzzyResults.filter(r =>  | ||||
|     !exactNoteIds.has(r.noteId) | ||||
|   ); | ||||
|    | ||||
|   // Sort within each category | ||||
|   exactResults.sort(byScoreAndDepth); | ||||
|   additionalFuzzyResults.sort(byScoreAndDepth); | ||||
|    | ||||
|   // CRITICAL: Exact matches always come first | ||||
|   return [...exactResults, ...additionalFuzzyResults]; | ||||
| }</code></pre> | ||||
|  | ||||
| <h2>Performance Monitoring</h2> | ||||
|  | ||||
| <h3>Search Metrics</h3> | ||||
| <p>Monitor these performance indicators:</p> | ||||
| <pre><code>// Performance tracking | ||||
| const searchMetrics = { | ||||
|   totalQueries: 0, | ||||
|   exactSearchTime: 0, | ||||
|   fuzzySearchTime: 0, | ||||
|   resultCount: 0, | ||||
|   cacheHitRate: 0, | ||||
|   slowQueries: [] // queries taking > 1 second | ||||
| };</code></pre> | ||||
|  | ||||
| <h3>Memory Usage</h3> | ||||
| <p>Track memory consumption:</p> | ||||
| <pre><code>// Memory monitoring | ||||
| const memoryMetrics = { | ||||
|   searchCacheSize: 0, | ||||
|   activeSearchContexts: 0, | ||||
|   largeContentNotes: 0, // notes > 1MB | ||||
|   indexSize: 0 | ||||
| };</code></pre> | ||||
|  | ||||
| <h3>Query Complexity Analysis</h3> | ||||
| <p>Identify expensive queries:</p> | ||||
| <pre><code>// Query complexity factors | ||||
| const complexityFactors = { | ||||
|   tokenCount: query.split(' ').length, | ||||
|   hasRegex: query.includes('%='), | ||||
|   hasFuzzy: query.includes('~=') || query.includes('~*'), | ||||
|   hasRelationTraversal: query.includes('.relations.'), | ||||
|   hasNestedProperties: (query.match(/\./g) || []).length > 2, | ||||
|   hasOrderBy: query.includes('orderBy'), | ||||
|   estimatedResultSize: 'unknown' | ||||
| };</code></pre> | ||||
|  | ||||
| <h2>Troubleshooting Performance Issues</h2> | ||||
|  | ||||
| <h3>Common Performance Problems</h3> | ||||
|  | ||||
| <h4>Slow Full-Text Search</h4> | ||||
| <p><strong>Diagnosis:</strong></p> | ||||
| <ul> | ||||
|   <li>Check note content sizes</li> | ||||
|   <li>Verify content type filtering</li> | ||||
|   <li>Monitor regex usage</li> | ||||
|   <li>Review fuzzy search activation</li> | ||||
| </ul> | ||||
| <p><strong>Solutions:</strong></p> | ||||
| <ul> | ||||
|   <li>Enable fast search for attribute-only queries</li> | ||||
|   <li>Add content size limits</li> | ||||
|   <li>Optimize regex patterns</li> | ||||
|   <li>Tune fuzzy search thresholds</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Memory Issues</h4> | ||||
| <p><strong>Diagnosis:</strong></p> | ||||
| <ul> | ||||
|   <li>Monitor result set sizes</li> | ||||
|   <li>Check for large content processing</li> | ||||
|   <li>Review search context caching</li> | ||||
|   <li>Identify memory leaks</li> | ||||
| </ul> | ||||
| <p><strong>Solutions:</strong></p> | ||||
| <ul> | ||||
|   <li>Add result limits</li> | ||||
|   <li>Implement progressive loading</li> | ||||
|   <li>Clear unused search contexts</li> | ||||
|   <li>Optimize content preprocessing</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>High CPU Usage</h4> | ||||
| <p><strong>Diagnosis:</strong></p> | ||||
| <ul> | ||||
|   <li>Profile fuzzy search operations</li> | ||||
|   <li>Check edit distance calculations</li> | ||||
|   <li>Monitor regex compilation</li> | ||||
|   <li>Review phrase proximity matching</li> | ||||
| </ul> | ||||
| <p><strong>Solutions:</strong></p> | ||||
| <ul> | ||||
|   <li>Increase minimum fuzzy token length</li> | ||||
|   <li>Reduce maximum edit distance</li> | ||||
|   <li>Cache compiled regexes</li> | ||||
|   <li>Limit phrase proximity distance</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Debugging Tools</h3> | ||||
|  | ||||
| <h4>Debug Mode</h4> | ||||
| <p>Enable search debugging:</p> | ||||
| <pre><code>// Search context with debugging | ||||
| const searchContext = new SearchContext({ | ||||
|   debug: true // Logs expression parsing and execution | ||||
| });</code></pre> | ||||
| <p>Output includes:</p> | ||||
| <ul> | ||||
|   <li>Token parsing results</li> | ||||
|   <li>Expression tree structure</li> | ||||
|   <li>Execution timing</li> | ||||
|   <li>Result scoring details</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Performance Profiling</h4> | ||||
| <pre><code>// Manual performance measurement | ||||
| const startTime = Date.now(); | ||||
| const results = searchService.findResultsWithQuery(query, searchContext); | ||||
| const endTime = Date.now(); | ||||
| console.log(`Search took ${endTime - startTime}ms for ${results.length} results`);</code></pre> | ||||
|  | ||||
| <h4>Query Analysis</h4> | ||||
| <pre><code>// Analyze query complexity | ||||
| function analyzeQuery(query) { | ||||
|   return { | ||||
|     tokenCount: query.split(/\s+/).length, | ||||
|     hasAttributes: /#|\~/.test(query), | ||||
|     hasProperties: /note\./.test(query), | ||||
|     hasRegex: /%=/.test(query), | ||||
|     hasFuzzy: /~[=*]/.test(query), | ||||
|     complexity: calculateComplexityScore(query) | ||||
|   }; | ||||
| }</code></pre> | ||||
|  | ||||
| <h2>Configuration and Tuning</h2> | ||||
|  | ||||
| <h3>Server Configuration</h3> | ||||
| <p>Relevant settings in <code>config.ini</code>:</p> | ||||
| <pre><code>[Search] | ||||
| maxContentSize=2097152          # 2MB content limit | ||||
| minFuzzyTokenLength=3          # Minimum chars for fuzzy | ||||
| maxEditDistance=2              # Edit distance limit | ||||
| resultSufficiencyThreshold=5   # Fuzzy activation threshold | ||||
| enableProgressiveSearch=true   # Enable progressive strategy | ||||
| cacheSearchResults=true        # Cache frequent searches | ||||
|  | ||||
| [Performance] | ||||
| searchTimeoutMs=30000         # 30 second search timeout | ||||
| maxSearchResults=1000         # Hard limit on results | ||||
| enableSearchProfiling=false   # Performance logging</code></pre> | ||||
|  | ||||
| <h3>Runtime Tuning</h3> | ||||
| <p>Adjust search behavior programmatically:</p> | ||||
| <pre><code>// Dynamic configuration | ||||
| const searchConfig = { | ||||
|   maxContentSize: 1024 * 1024,  // 1MB for faster processing | ||||
|   enableFuzzySearch: false,      // Exact only for speed | ||||
|   resultLimit: 50,               // Smaller result sets | ||||
|   useIndexedPropertiesOnly: true // Skip expensive calculations | ||||
| };</code></pre> | ||||
|  | ||||
| <h2>Best Practices for Performance</h2> | ||||
|  | ||||
| <h3>Query Design</h3> | ||||
| <ol> | ||||
|   <li><strong>Start Specific</strong>: Use selective criteria first</li> | ||||
|   <li><strong>Limit Results</strong>: Always set reasonable limits</li> | ||||
|   <li><strong>Use Indexes</strong>: Prefer indexed properties for ordering</li> | ||||
|   <li><strong>Avoid Regex</strong>: Use simple operators when possible</li> | ||||
|   <li><strong>Cache Common Queries</strong>: Save frequently used searches</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>System Administration</h3> | ||||
| <ol> | ||||
|   <li><strong>Monitor Performance</strong>: Track slow queries and memory usage</li> | ||||
|   <li><strong>Regular Maintenance</strong>: Clean up unused notes and attributes</li> | ||||
|   <li><strong>Index Optimization</strong>: Ensure database indexes are current</li> | ||||
|   <li><strong>Content Management</strong>: Archive or compress large content</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Development Guidelines</h3> | ||||
| <ol> | ||||
|   <li><strong>Test Performance</strong>: Benchmark complex queries</li> | ||||
|   <li><strong>Profile Regularly</strong>: Identify performance regressions</li> | ||||
|   <li><strong>Optimize Incrementally</strong>: Make small, measured improvements</li> | ||||
|   <li><strong>Document Complexity</strong>: Note expensive operations</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Next Steps</h2> | ||||
| <ul> | ||||
|   <li><a class="reference-link" href="#root/_help_search_fundamentals">Search Fundamentals</a> - Basic concepts and syntax</li> | ||||
|   <li><a class="reference-link" href="#root/_help_search_advanced">Advanced Search Expressions</a> - Complex query construction</li> | ||||
|   <li><a class="reference-link" href="#root/_help_search_examples">Search Examples and Use Cases</a> - Practical applications</li> | ||||
|   <li><a class="reference-link" href="#root/_help_search_saved">Saved Searches</a> - Creating dynamic collections</li> | ||||
| </ul> | ||||
							
								
								
									
										600
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Advanced-Protection-Setup.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										600
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Advanced-Protection-Setup.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,600 @@ | ||||
| <div class="note-content"> | ||||
|  | ||||
| <h1>Advanced Protection Setup Guide</h1> | ||||
|  | ||||
| <p>This guide provides step-by-step instructions for implementing advanced security features in Trilium, including enterprise-level protection measures and compliance configurations.</p> | ||||
|  | ||||
| <div class="alert alert-info"> | ||||
|     <strong>Target Audience:</strong> System administrators, security professionals, and advanced users implementing Trilium in production environments. | ||||
| </div> | ||||
|  | ||||
| <h2>Table of Contents</h2> | ||||
|  | ||||
| <ol> | ||||
|     <li><a href="#mfa-setup">Multi-Factor Authentication Setup</a></li> | ||||
|     <li><a href="#enterprise-auth">Enterprise Authentication</a></li> | ||||
|     <li><a href="#advanced-encryption">Advanced Encryption Configuration</a></li> | ||||
|     <li><a href="#security-monitoring">Security Monitoring Setup</a></li> | ||||
|     <li><a href="#compliance-config">Compliance Configuration</a></li> | ||||
|     <li><a href="#backup-security">Secure Backup Implementation</a></li> | ||||
| </ol> | ||||
|  | ||||
| <h2 id="mfa-setup">Multi-Factor Authentication Setup</h2> | ||||
|  | ||||
| <h3>Prerequisites</h3> | ||||
| <ul> | ||||
|     <li>Trilium instance with password authentication configured</li> | ||||
|     <li>Authenticator app (Google Authenticator, Authy, etc.)</li> | ||||
|     <li>Secure storage for recovery codes</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Step-by-Step MFA Configuration</h3> | ||||
|  | ||||
| <h4>Step 1: Enable MFA</h4> | ||||
| <ol> | ||||
|     <li>Navigate to <strong>Options → Security → Multi-Factor Authentication</strong></li> | ||||
|     <li>Click <strong>"Enable Multi-Factor Authentication"</strong></li> | ||||
|     <li>Confirm your current password when prompted</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Step 2: Generate TOTP Secret</h4> | ||||
| <ol> | ||||
|     <li>Click <strong>"Generate New Secret"</strong></li> | ||||
|     <li>A QR code will be displayed along with the secret key</li> | ||||
|     <li>Copy the secret key to a secure location (backup)</li> | ||||
| </ol> | ||||
|  | ||||
| <div class="alert alert-warning"> | ||||
|     <strong>Security Note:</strong> The secret key is your backup method for setting up the authenticator on new devices. Store it securely. | ||||
| </div> | ||||
|  | ||||
| <h4>Step 3: Configure Authenticator App</h4> | ||||
|  | ||||
| <p><strong>Option A: QR Code (Recommended)</strong></p> | ||||
| <ol> | ||||
|     <li>Open your authenticator app</li> | ||||
|     <li>Tap "Add Account" or "+"</li> | ||||
|     <li>Select "Scan QR Code"</li> | ||||
|     <li>Point camera at the QR code displayed in Trilium</li> | ||||
|     <li>Verify the account is added with name "Trilium Notes"</li> | ||||
| </ol> | ||||
|  | ||||
| <p><strong>Option B: Manual Entry</strong></p> | ||||
| <ol> | ||||
|     <li>Open your authenticator app</li> | ||||
|     <li>Tap "Add Account" or "+"</li> | ||||
|     <li>Select "Enter Key Manually"</li> | ||||
|     <li>Enter the following details: | ||||
|         <ul> | ||||
|             <li><strong>Account:</strong> Your email or username</li> | ||||
|             <li><strong>Key:</strong> The secret key from Trilium</li> | ||||
|             <li><strong>Type:</strong> Time-based</li> | ||||
|         </ul> | ||||
|     </li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Step 4: Verify TOTP Setup</h4> | ||||
| <ol> | ||||
|     <li>Wait for the authenticator to generate a 6-digit code</li> | ||||
|     <li>Enter the code in the "Verification Code" field</li> | ||||
|     <li>Click <strong>"Verify and Enable MFA"</strong></li> | ||||
|     <li>If successful, you'll see a confirmation message</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Step 5: Save Recovery Codes</h4> | ||||
| <ol> | ||||
|     <li>Click <strong>"Generate Recovery Codes"</strong></li> | ||||
|     <li>Save the 10 recovery codes in a secure location: | ||||
|         <ul> | ||||
|             <li>Password manager</li> | ||||
|             <li>Encrypted file</li> | ||||
|             <li>Physical secure storage</li> | ||||
|         </ul> | ||||
|     </li> | ||||
|     <li>Click <strong>"I have saved my recovery codes"</strong></li> | ||||
| </ol> | ||||
|  | ||||
| <div class="alert alert-danger"> | ||||
|     <strong>Critical:</strong> Recovery codes are your only way to access Trilium if you lose access to your authenticator. Store them securely and treat them like passwords. | ||||
| </div> | ||||
|  | ||||
| <h3>MFA Login Process</h3> | ||||
|  | ||||
| <p>After enabling MFA, the login process becomes:</p> | ||||
| <ol> | ||||
|     <li>Enter your username and password</li> | ||||
|     <li>Click "Login"</li> | ||||
|     <li>Enter the 6-digit TOTP code from your authenticator</li> | ||||
|     <li>Alternatively, use a recovery code if TOTP is unavailable</li> | ||||
|     <li>Click "Verify" to complete login</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Troubleshooting MFA</h3> | ||||
|  | ||||
| <h4>Common Issues</h4> | ||||
|  | ||||
| <div class="alert alert-info"> | ||||
|     <strong>Issue:</strong> TOTP codes are rejected<br> | ||||
|     <strong>Solutions:</strong> | ||||
|     <ul> | ||||
|         <li>Check that your device time is synchronized (most common cause)</li> | ||||
|         <li>Ensure you're entering the current 6-digit code</li> | ||||
|         <li>Try the next generated code if the current one expires</li> | ||||
|         <li>Verify the secret was entered correctly in your authenticator</li> | ||||
|     </ul> | ||||
| </div> | ||||
|  | ||||
| <div class="alert alert-warning"> | ||||
|     <strong>Issue:</strong> Authenticator app lost or unavailable<br> | ||||
|     <strong>Solutions:</strong> | ||||
|     <ul> | ||||
|         <li>Use one of your saved recovery codes</li> | ||||
|         <li>Each recovery code can only be used once</li> | ||||
|         <li>Generate new recovery codes after using several</li> | ||||
|         <li>Re-setup MFA if all recovery codes are used</li> | ||||
|     </ul> | ||||
| </div> | ||||
|  | ||||
| <h2 id="enterprise-auth">Enterprise Authentication</h2> | ||||
|  | ||||
| <h3>Single Sign-On (SSO) Configuration</h3> | ||||
|  | ||||
| <h4>OpenID Connect Setup</h4> | ||||
|  | ||||
| <p>Configure Trilium to integrate with enterprise identity providers:</p> | ||||
|  | ||||
| <h5>Configuration File Setup</h5> | ||||
| <pre><code># config.ini | ||||
| [OpenID] | ||||
| enabled=true | ||||
| issuer=https://your-provider.com | ||||
| client_id=your-client-id | ||||
| client_secret=your-client-secret | ||||
| redirect_uri=https://your-trilium.com/auth/callback | ||||
| scope=openid email profile</code></pre> | ||||
|  | ||||
| <h5>Common Provider Configurations</h5> | ||||
|  | ||||
| <p><strong>Google Workspace:</strong></p> | ||||
| <pre><code>[OpenID] | ||||
| enabled=true | ||||
| issuer=https://accounts.google.com | ||||
| client_id=your-google-client-id.apps.googleusercontent.com | ||||
| client_secret=your-google-client-secret | ||||
| redirect_uri=https://your-trilium.com/auth/callback | ||||
| scope=openid email profile</code></pre> | ||||
|  | ||||
| <p><strong>Microsoft Azure AD:</strong></p> | ||||
| <pre><code>[OpenID] | ||||
| enabled=true | ||||
| issuer=https://login.microsoftonline.com/your-tenant-id/v2.0 | ||||
| client_id=your-azure-client-id | ||||
| client_secret=your-azure-client-secret | ||||
| redirect_uri=https://your-trilium.com/auth/callback | ||||
| scope=openid email profile</code></pre> | ||||
|  | ||||
| <h4>Environment Variable Configuration</h4> | ||||
|  | ||||
| <p>For containerized deployments:</p> | ||||
| <pre><code># Docker environment variables | ||||
| TRILIUM_OPENID_ENABLED=true | ||||
| TRILIUM_OPENID_ISSUER=https://your-provider.com | ||||
| TRILIUM_OPENID_CLIENT_ID=your-client-id | ||||
| TRILIUM_OPENID_CLIENT_SECRET=your-client-secret | ||||
| TRILIUM_OPENID_REDIRECT_URI=https://your-trilium.com/auth/callback</code></pre> | ||||
|  | ||||
| <h2 id="advanced-encryption">Advanced Encryption Configuration</h2> | ||||
|  | ||||
| <h3>Custom Encryption Parameters</h3> | ||||
|  | ||||
| <div class="alert alert-danger"> | ||||
|     <strong>Warning:</strong> Modifying encryption parameters requires expert knowledge and may break compatibility with future versions. Only proceed if you understand the implications. | ||||
| </div> | ||||
|  | ||||
| <h4>Scrypt Parameter Tuning</h4> | ||||
|  | ||||
| <p>For high-security environments, you can increase scrypt parameters:</p> | ||||
|  | ||||
| <pre><code>// Location: apps/server/src/services/encryption/my_scrypt.ts | ||||
| const customScryptParams = { | ||||
|     N: 32768,    // Higher CPU/memory cost (default: 16384) | ||||
|     r: 8,        // Block size (default: 8) | ||||
|     p: 2         // Parallelization (default: 1) | ||||
| }; | ||||
| </code></pre> | ||||
|  | ||||
| <p><strong>Impact Assessment:</strong></p> | ||||
| <ul> | ||||
|     <li><strong>Security:</strong> Higher parameters increase resistance to brute force</li> | ||||
|     <li><strong>Performance:</strong> Significantly slower password verification</li> | ||||
|     <li><strong>Compatibility:</strong> May break with future updates</li> | ||||
|     <li><strong>Hardware:</strong> Requires more RAM and CPU</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Database-Level Encryption</h3> | ||||
|  | ||||
| <h4>SQLite Encryption Extension</h4> | ||||
|  | ||||
| <p>For additional database encryption (enterprise feature):</p> | ||||
|  | ||||
| <ol> | ||||
|     <li>Install SQLCipher extension</li> | ||||
|     <li>Configure database encryption key</li> | ||||
|     <li>Modify connection string to use encryption</li> | ||||
| </ol> | ||||
|  | ||||
| <pre><code># Database connection with encryption | ||||
| PRAGMA key = 'your-database-encryption-key'; | ||||
| PRAGMA cipher_compatibility = 4;</code></pre> | ||||
|  | ||||
| <h2 id="security-monitoring">Security Monitoring Setup</h2> | ||||
|  | ||||
| <h3>Log Configuration</h3> | ||||
|  | ||||
| <h4>Comprehensive Logging Setup</h4> | ||||
|  | ||||
| <ol> | ||||
|     <li><strong>Create Log Directory</strong> | ||||
|         <pre><code>sudo mkdir -p /var/log/trilium | ||||
| sudo chown trilium:adm /var/log/trilium | ||||
| sudo chmod 750 /var/log/trilium</code></pre> | ||||
|     </li> | ||||
|      | ||||
|     <li><strong>Configure Log Rotation</strong> | ||||
|         <pre><code># /etc/logrotate.d/trilium | ||||
| /var/log/trilium/*.log { | ||||
|     daily | ||||
|     missingok | ||||
|     rotate 90 | ||||
|     compress | ||||
|     delaycompress | ||||
|     notifempty | ||||
|     create 640 trilium adm | ||||
|     sharedscripts | ||||
|     postrotate | ||||
|         systemctl reload trilium | ||||
|     endscript | ||||
| }</code></pre> | ||||
|     </li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Security Event Monitoring</h3> | ||||
|  | ||||
| <h4>Real-time Monitoring Script</h4> | ||||
|  | ||||
| <p>Create automated monitoring for security events:</p> | ||||
|  | ||||
| <pre><code>#!/bin/bash | ||||
| # /usr/local/bin/trilium-security-monitor.sh | ||||
|  | ||||
| LOG_FILE="/var/log/trilium/security.log" | ||||
| ALERT_EMAIL="admin@yourdomain.com" | ||||
|  | ||||
| # Monitor failed login attempts | ||||
| check_failed_logins() { | ||||
|     local count=$(grep -c "Failed login" "$LOG_FILE" 2>/dev/null || echo 0) | ||||
|     if [ "$count" -gt 5 ]; then | ||||
|         echo "ALERT: $count failed login attempts" | \ | ||||
|         mail -s "Trilium Security Alert" "$ALERT_EMAIL" | ||||
|     fi | ||||
| } | ||||
|  | ||||
| # Monitor CSRF violations | ||||
| check_csrf_violations() { | ||||
|     local count=$(grep -c "CSRF violation" "$LOG_FILE" 2>/dev/null || echo 0) | ||||
|     if [ "$count" -gt 0 ]; then | ||||
|         echo "ALERT: $count CSRF violations detected" | \ | ||||
|         mail -s "Trilium Security Alert" "$ALERT_EMAIL" | ||||
|     fi | ||||
| } | ||||
|  | ||||
| # Run checks | ||||
| check_failed_logins | ||||
| check_csrf_violations</code></pre> | ||||
|  | ||||
| <h4>Automated Monitoring with Cron</h4> | ||||
|  | ||||
| <pre><code># Add to crontab | ||||
| */15 * * * * /usr/local/bin/trilium-security-monitor.sh</code></pre> | ||||
|  | ||||
| <h2 id="compliance-config">Compliance Configuration</h2> | ||||
|  | ||||
| <h3>GDPR Compliance</h3> | ||||
|  | ||||
| <h4>Data Protection Settings</h4> | ||||
|  | ||||
| <ol> | ||||
|     <li><strong>Enable Enhanced Logging</strong> | ||||
|         <pre><code># config.ini | ||||
| [Security] | ||||
| auditLogging=true | ||||
| dataAccessLogging=true | ||||
| retentionPeriod=2557</code></pre> | ||||
|     </li> | ||||
|      | ||||
|     <li><strong>Configure Data Export</strong> | ||||
|         <ul> | ||||
|             <li>Regular automated exports for data portability</li> | ||||
|             <li>User-accessible export functionality</li> | ||||
|             <li>Structured data format (JSON/XML)</li> | ||||
|         </ul> | ||||
|     </li> | ||||
|      | ||||
|     <li><strong>Right to Erasure Implementation</strong> | ||||
|         <ul> | ||||
|             <li>Secure deletion procedures</li> | ||||
|             <li>Encryption key destruction</li> | ||||
|             <li>Backup purging processes</li> | ||||
|         </ul> | ||||
|     </li> | ||||
| </ol> | ||||
|  | ||||
| <h3>HIPAA Compliance</h3> | ||||
|  | ||||
| <h4>Healthcare Data Protection</h4> | ||||
|  | ||||
| <ol> | ||||
|     <li><strong>Access Controls</strong> | ||||
|         <ul> | ||||
|             <li>Strong authentication (password + MFA)</li> | ||||
|             <li>Session timeout configuration (max 10 minutes)</li> | ||||
|             <li>Automatic logout on inactivity</li> | ||||
|         </ul> | ||||
|     </li> | ||||
|      | ||||
|     <li><strong>Audit Requirements</strong> | ||||
|         <pre><code># Enhanced audit logging | ||||
| [HIPAA] | ||||
| auditAllAccess=true | ||||
| logUserActions=true | ||||
| retainLogs=2555  # 7 years in days | ||||
| encryptAuditLogs=true</code></pre> | ||||
|     </li> | ||||
|      | ||||
|     <li><strong>Encryption Standards</strong> | ||||
|         <ul> | ||||
|             <li>AES-128 minimum (meets HIPAA requirements)</li> | ||||
|             <li>Key management procedures</li> | ||||
|             <li>Encrypted backups and transmission</li> | ||||
|         </ul> | ||||
|     </li> | ||||
| </ol> | ||||
|  | ||||
| <h2 id="backup-security">Secure Backup Implementation</h2> | ||||
|  | ||||
| <h3>Automated Encrypted Backups</h3> | ||||
|  | ||||
| <h4>Backup Script Configuration</h4> | ||||
|  | ||||
| <pre><code>#!/bin/bash | ||||
| # /usr/local/bin/trilium-secure-backup.sh | ||||
|  | ||||
| BACKUP_DIR="/opt/trilium/backups" | ||||
| GPG_RECIPIENT="trilium-backup@yourdomain.com" | ||||
| RETENTION_DAYS=90 | ||||
|  | ||||
| # Create encrypted backup | ||||
| create_backup() { | ||||
|     local timestamp=$(date +%Y%m%d_%H%M%S) | ||||
|     local backup_name="trilium_backup_$timestamp" | ||||
|      | ||||
|     # Stop Trilium for consistent backup | ||||
|     systemctl stop trilium | ||||
|      | ||||
|     # Create backup | ||||
|     tar czf - -C /opt/trilium/data . | \ | ||||
|     gpg --trust-model always --encrypt -r "$GPG_RECIPIENT" \ | ||||
|     > "$BACKUP_DIR/${backup_name}.tar.gz.gpg" | ||||
|      | ||||
|     # Start Trilium | ||||
|     systemctl start trilium | ||||
|      | ||||
|     # Verify backup | ||||
|     if gpg --decrypt "$BACKUP_DIR/${backup_name}.tar.gz.gpg" | tar tz > /dev/null; then | ||||
|         echo "Backup verification successful: $backup_name" | ||||
|     else | ||||
|         echo "Backup verification failed!" >&2 | ||||
|         exit 1 | ||||
|     fi | ||||
|      | ||||
|     # Set permissions | ||||
|     chown trilium:trilium "$BACKUP_DIR/${backup_name}.tar.gz.gpg" | ||||
|     chmod 600 "$BACKUP_DIR/${backup_name}.tar.gz.gpg" | ||||
| } | ||||
|  | ||||
| # Cleanup old backups | ||||
| cleanup_backups() { | ||||
|     find "$BACKUP_DIR" -name "*.tar.gz.gpg" -mtime +$RETENTION_DAYS -delete | ||||
| } | ||||
|  | ||||
| # Execute backup | ||||
| create_backup | ||||
| cleanup_backups</code></pre> | ||||
|  | ||||
| <h4>Automated Backup Schedule</h4> | ||||
|  | ||||
| <pre><code># Add to crontab for daily backups at 2 AM | ||||
| 0 2 * * * /usr/local/bin/trilium-secure-backup.sh</code></pre> | ||||
|  | ||||
| <h3>Backup Verification</h3> | ||||
|  | ||||
| <h4>Regular Backup Testing</h4> | ||||
|  | ||||
| <pre><code>#!/bin/bash | ||||
| # /usr/local/bin/trilium-backup-test.sh | ||||
|  | ||||
| BACKUP_DIR="/opt/trilium/backups" | ||||
| TEST_DIR="/tmp/trilium-backup-test" | ||||
|  | ||||
| # Test latest backup | ||||
| test_latest_backup() { | ||||
|     local latest_backup=$(ls -t "$BACKUP_DIR"/*.tar.gz.gpg | head -1) | ||||
|      | ||||
|     if [ -z "$latest_backup" ]; then | ||||
|         echo "No backups found!" | ||||
|         exit 1 | ||||
|     fi | ||||
|      | ||||
|     echo "Testing backup: $latest_backup" | ||||
|      | ||||
|     # Create test directory | ||||
|     mkdir -p "$TEST_DIR" | ||||
|      | ||||
|     # Decrypt and extract | ||||
|     if gpg --decrypt "$latest_backup" | tar xzf - -C "$TEST_DIR"; then | ||||
|         echo "Backup extraction successful" | ||||
|          | ||||
|         # Test database integrity | ||||
|         if sqlite3 "$TEST_DIR/document.db" "PRAGMA integrity_check;" | grep -q "ok"; then | ||||
|             echo "Database integrity check passed" | ||||
|         else | ||||
|             echo "Database integrity check failed!" | ||||
|             exit 1 | ||||
|         fi | ||||
|     else | ||||
|         echo "Backup extraction failed!" | ||||
|         exit 1 | ||||
|     fi | ||||
|      | ||||
|     # Cleanup | ||||
|     rm -rf "$TEST_DIR" | ||||
|      | ||||
|     echo "Backup test completed successfully" | ||||
| } | ||||
|  | ||||
| test_latest_backup</code></pre> | ||||
|  | ||||
| <h3>Off-site Backup Synchronization</h3> | ||||
|  | ||||
| <h4>Secure Remote Sync</h4> | ||||
|  | ||||
| <pre><code>#!/bin/bash | ||||
| # /usr/local/bin/trilium-remote-sync.sh | ||||
|  | ||||
| REMOTE_HOST="backup.yourdomain.com" | ||||
| REMOTE_USER="trilium-backup" | ||||
| REMOTE_PATH="/backups/trilium" | ||||
| LOCAL_BACKUP_DIR="/opt/trilium/backups" | ||||
|  | ||||
| # Sync to remote location | ||||
| sync_to_remote() { | ||||
|     rsync -avz --progress --delete \ | ||||
|         -e "ssh -i /home/trilium/.ssh/backup_key" \ | ||||
|         "$LOCAL_BACKUP_DIR/" \ | ||||
|         "$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/" | ||||
|      | ||||
|     if [ $? -eq 0 ]; then | ||||
|         echo "Remote sync completed successfully" | ||||
|     else | ||||
|         echo "Remote sync failed!" | ||||
|         exit 1 | ||||
|     fi | ||||
| } | ||||
|  | ||||
| sync_to_remote</code></pre> | ||||
|  | ||||
| <h2>Security Assessment Tools</h2> | ||||
|  | ||||
| <h3>Security Scanner</h3> | ||||
|  | ||||
| <h4>Automated Security Assessment</h4> | ||||
|  | ||||
| <pre><code>#!/bin/bash | ||||
| # /usr/local/bin/trilium-security-scan.sh | ||||
|  | ||||
| SCORE=0 | ||||
| MAX_SCORE=0 | ||||
|  | ||||
| print_check() { | ||||
|     local status="$1" | ||||
|     local message="$2" | ||||
|     local points="$3" | ||||
|      | ||||
|     case "$status" in | ||||
|         "PASS") | ||||
|             echo "✓ $message (+$points points)" | ||||
|             SCORE=$((SCORE + points)) | ||||
|             ;; | ||||
|         "FAIL") | ||||
|             echo "✗ $message" | ||||
|             ;; | ||||
|         "WARN") | ||||
|             echo "! $message" | ||||
|             ;; | ||||
|     esac | ||||
|      | ||||
|     MAX_SCORE=$((MAX_SCORE + points)) | ||||
| } | ||||
|  | ||||
| # Check HTTPS configuration | ||||
| check_https() { | ||||
|     if curl -s -I https://localhost:8080 2>/dev/null | grep -q "HTTP/"; then | ||||
|         print_check "PASS" "HTTPS is configured" 20 | ||||
|     else | ||||
|         print_check "FAIL" "HTTPS is not configured" 20 | ||||
|     fi | ||||
| } | ||||
|  | ||||
| # Check MFA status | ||||
| check_mfa() { | ||||
|     local mfa_enabled=$(sqlite3 /opt/trilium/data/document.db \ | ||||
|         "SELECT value FROM options WHERE name = 'mfaEnabled';" 2>/dev/null) | ||||
|     if [ "$mfa_enabled" = "true" ]; then | ||||
|         print_check "PASS" "MFA is enabled" 20 | ||||
|     else | ||||
|         print_check "WARN" "MFA is not enabled" 20 | ||||
|     fi | ||||
| } | ||||
|  | ||||
| # Check file permissions | ||||
| check_permissions() { | ||||
|     local db_perms=$(stat -c "%a" /opt/trilium/data/document.db 2>/dev/null) | ||||
|     if [ "$db_perms" = "600" ]; then | ||||
|         print_check "PASS" "Database permissions are secure" 10 | ||||
|     else | ||||
|         print_check "FAIL" "Database permissions are too permissive" 10 | ||||
|     fi | ||||
| } | ||||
|  | ||||
| # Run all checks | ||||
| echo "=== Trilium Security Assessment ===" | ||||
| check_https | ||||
| check_mfa | ||||
| check_permissions | ||||
|  | ||||
| echo "=== Summary ===" | ||||
| echo "Score: $SCORE / $MAX_SCORE ($(($SCORE * 100 / $MAX_SCORE))%)" | ||||
|  | ||||
| if [ $SCORE -eq $MAX_SCORE ]; then | ||||
|     echo "✓ Excellent security configuration!" | ||||
| elif [ $SCORE -ge $((MAX_SCORE * 80 / 100)) ]; then | ||||
|     echo "✓ Good security with minor improvements needed" | ||||
| else | ||||
|     echo "⚠ Security improvements required" | ||||
| fi</code></pre> | ||||
|  | ||||
| <h2>Support and Resources</h2> | ||||
|  | ||||
| <h3>Getting Help</h3> | ||||
|  | ||||
| <ul> | ||||
|     <li><strong>Documentation:</strong> Comprehensive guides in help system</li> | ||||
|     <li><strong>Community:</strong> GitHub discussions and issues</li> | ||||
|     <li><strong>Security Issues:</strong> Report to security team</li> | ||||
|     <li><strong>Professional Support:</strong> Enterprise support options</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Additional Resources</h3> | ||||
|  | ||||
| <ul> | ||||
|     <li><a href="#comprehensive-security-guide">Comprehensive Security Guide</a></li> | ||||
|     <li><a href="#protected-notes-encryption">Protected Notes and Encryption</a></li> | ||||
|     <li><a href="#authentication-session-management">Authentication and Session Management</a></li> | ||||
|     <li><a href="#security-best-practices">Security Best Practices</a></li> | ||||
| </ul> | ||||
|  | ||||
| <div class="alert alert-success"> | ||||
|     <strong>Security is a Journey:</strong> Implementing these advanced protection measures is just the beginning. Regular security assessments, updates, and monitoring are essential for maintaining a secure Trilium environment. | ||||
| </div> | ||||
|  | ||||
| </div> | ||||
							
								
								
									
										412
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Authentication and Session Management.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										412
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Authentication and Session Management.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,412 @@ | ||||
| <div class="note-content"> | ||||
|  | ||||
| <h1>Authentication and Session Management</h1> | ||||
|  | ||||
| <p>Trilium provides multiple authentication methods and robust session management to secure access to your notes while maintaining usability.</p> | ||||
|  | ||||
| <h2>Authentication Methods</h2> | ||||
|  | ||||
| <h3>Password Authentication</h3> | ||||
|  | ||||
| <p>The primary authentication method uses a master password to secure your Trilium instance.</p> | ||||
|  | ||||
| <h4>Password Setup</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Initial Setup</strong>: Set during first launch or server installation</li> | ||||
| <li><strong>Password Requirements</strong>: Configurable strength requirements</li> | ||||
| <li><strong>Verification</strong>: Scrypt-based password hashing for security</li> | ||||
| <li><strong>Storage</strong>: Hashed using scrypt with random salt</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Password Security</h4> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>Hashing Algorithm</strong>: Scrypt with parameters N=16384, r=8, p=1</li> | ||||
| <li><strong>Salt</strong>: Unique random salt generated per installation</li> | ||||
| <li><strong>Verification Hash</strong>: Stored separately from encryption keys</li> | ||||
| <li><strong>Timing Attack Protection</strong>: Constant-time comparison</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Multi-Factor Authentication (TOTP)</h3> | ||||
|  | ||||
| <p>Trilium supports Time-based One-Time Password (TOTP) authentication for enhanced security.</p> | ||||
|  | ||||
| <h4>Setup Process</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Enable MFA</strong>: Navigate to Options → Multi-Factor Authentication</li> | ||||
| <li><strong>Generate Secret</strong>: Click "Generate New Secret"</li> | ||||
| <li><strong>Add to Authenticator</strong>: Scan QR code or enter secret manually</li> | ||||
| <li><strong>Verify Setup</strong>: Enter TOTP code to confirm configuration</li> | ||||
| <li><strong>Save Recovery Codes</strong>: Store backup codes securely</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Supported Authenticators</h4> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>Google Authenticator</strong>: Mobile app for Android/iOS</li> | ||||
| <li><strong>Authy</strong>: Cross-platform authenticator with cloud sync</li> | ||||
| <li><strong>Microsoft Authenticator</strong>: Integrated with Microsoft accounts</li> | ||||
| <li><strong>1Password</strong>: Built-in TOTP support</li> | ||||
| <li><strong>Any RFC 6238 Compatible App</strong>: Standard TOTP implementation</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>TOTP Configuration</h4> | ||||
|  | ||||
| <pre><code class="language-typescript">// TOTP settings in options | ||||
| { | ||||
|   mfaEnabled: "true",           // Enable/disable MFA | ||||
|   mfaMethod: "totp",           // Authentication method | ||||
|   totpEncryptedSecret: "...",  // Encrypted TOTP secret | ||||
|   totpVerificationHash: "..."  // Secret verification hash | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h3>Recovery Codes</h3> | ||||
|  | ||||
| <p>Recovery codes provide backup access when TOTP is unavailable.</p> | ||||
|  | ||||
| <h4>Code Generation</h4> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>Format</strong>: Base64-encoded 24-character strings ending in "=="</li> | ||||
| <li><strong>Quantity</strong>: Multiple codes generated during setup</li> | ||||
| <li><strong>Encryption</strong>: AES-256-CBC encrypted storage</li> | ||||
| <li><strong>One-time Use</strong>: Each code invalidated after use</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Usage Guidelines</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Secure Storage</strong>: Keep codes in password manager or secure location</li> | ||||
| <li><strong>Limited Use</strong>: Only use when primary authentication unavailable</li> | ||||
| <li><strong>Regeneration</strong>: Generate new codes if compromised</li> | ||||
| <li><strong>Expiration</strong>: Codes replaced with timestamp when used</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Single Sign-On (SSO)</h3> | ||||
|  | ||||
| <p>Trilium supports OpenID Connect for enterprise authentication.</p> | ||||
|  | ||||
| <h4>Supported Providers</h4> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>Google</strong>: Google Workspace accounts</li> | ||||
| <li><strong>Microsoft</strong>: Azure AD integration</li> | ||||
| <li><strong>GitHub</strong>: Developer account authentication</li> | ||||
| <li><strong>Custom OIDC</strong>: Any OpenID Connect provider</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Configuration</h4> | ||||
|  | ||||
| <p>Set environment variables or config.ini:</p> | ||||
|  | ||||
| <pre><code class="language-ini">[OpenID] | ||||
| enabled=true | ||||
| issuer=https://accounts.google.com | ||||
| client_id=your-client-id | ||||
| client_secret=your-client-secret | ||||
| redirect_uri=https://your-trilium.example.com/auth/callback | ||||
| </code></pre> | ||||
|  | ||||
| <h2>Session Management</h2> | ||||
|  | ||||
| <h3>Session Security</h3> | ||||
|  | ||||
| <p>Trilium implements secure session management with multiple protection layers.</p> | ||||
|  | ||||
| <h4>Session Storage</h4> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>Database Storage</strong>: Sessions stored in SQLite database</li> | ||||
| <li><strong>Secure Secrets</strong>: Cryptographically secure session secrets</li> | ||||
| <li><strong>Expiration Tracking</strong>: Automatic cleanup of expired sessions</li> | ||||
| <li><strong>Multiple Sessions</strong>: Support for concurrent user sessions</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Session Configuration</h4> | ||||
|  | ||||
| <pre><code class="language-typescript">// Session settings | ||||
| { | ||||
|   secret: sessionSecret,           // Cryptographic secret | ||||
|   resave: false,                  // Don't save unchanged sessions | ||||
|   saveUninitialized: false,       // Don't save empty sessions | ||||
|   rolling: true,                  // Reset expiration on activity | ||||
|   cookie: { | ||||
|     httpOnly: true,               // Prevent XSS attacks | ||||
|     secure: false,                // HTTPS-only in production | ||||
|     maxAge: 24 * 60 * 60 * 1000  // 24-hour expiration | ||||
|   } | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h3>Session Lifecycle</h3> | ||||
|  | ||||
| <h4>Session Creation</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Authentication</strong>: User provides valid credentials</li> | ||||
| <li><strong>Session ID</strong>: Generate cryptographically secure session ID</li> | ||||
| <li><strong>Database Storage</strong>: Store session data with expiration</li> | ||||
| <li><strong>Cookie Setting</strong>: Send session cookie to client</li> | ||||
| <li><strong>State Tracking</strong>: Monitor authentication state changes</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Session Maintenance</h4> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>Activity Tracking</strong>: Update session expiration on each request</li> | ||||
| <li><strong>State Validation</strong>: Verify session integrity on each access</li> | ||||
| <li><strong>Timeout Management</strong>: Automatic logout after inactivity</li> | ||||
| <li><strong>Cross-tab Sync</strong>: Session state synchronized across browser tabs</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Session Termination</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Manual Logout</strong>: User-initiated session termination</li> | ||||
| <li><strong>Timeout Expiration</strong>: Automatic logout after inactivity</li> | ||||
| <li><strong>Security Events</strong>: Forced logout on security state changes</li> | ||||
| <li><strong>Cleanup</strong>: Remove session data from database</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>CSRF Protection</h3> | ||||
|  | ||||
| <p>Trilium implements double-submit cookie CSRF protection.</p> | ||||
|  | ||||
| <h4>Protection Mechanism</h4> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>Token Generation</strong>: Cryptographically secure CSRF tokens</li> | ||||
| <li><strong>Cookie Storage</strong>: Token stored in httpOnly cookie</li> | ||||
| <li><strong>Header Validation</strong>: Token required in request headers</li> | ||||
| <li><strong>Double Submit</strong>: Cookie and header values must match</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Configuration</h4> | ||||
|  | ||||
| <pre><code class="language-typescript">// CSRF protection settings | ||||
| { | ||||
|   cookieOptions: { | ||||
|     path: "/", | ||||
|     secure: false,        // HTTPS-only in production | ||||
|     sameSite: "strict",   // Strict same-site policy | ||||
|     httpOnly: true        // Prevent JavaScript access | ||||
|   }, | ||||
|   cookieName: "_csrf"     // Cookie name for CSRF token | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h3>Session Security Headers</h3> | ||||
|  | ||||
| <p>Trilium sets security headers to protect against common attacks.</p> | ||||
|  | ||||
| <h4>Standard Headers</h4> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>X-Frame-Options</strong>: Prevent clickjacking attacks</li> | ||||
| <li><strong>X-Content-Type-Options</strong>: Prevent MIME sniffing</li> | ||||
| <li><strong>X-XSS-Protection</strong>: Enable browser XSS protection</li> | ||||
| <li><strong>Strict-Transport-Security</strong>: Enforce HTTPS connections</li> | ||||
| <li><strong>Content-Security-Policy</strong>: Control resource loading</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Authentication Flow</h2> | ||||
|  | ||||
| <h3>Standard Login Process</h3> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Initial Request</strong>: User accesses protected resource</li> | ||||
| <li><strong>Redirect</strong>: System redirects to login page</li> | ||||
| <li><strong>Credential Entry</strong>: User enters username/password</li> | ||||
| <li><strong>Verification</strong>: System validates credentials</li> | ||||
| <li><strong>MFA Challenge</strong>: TOTP prompt if MFA enabled</li> | ||||
| <li><strong>Session Creation</strong>: Generate and store session</li> | ||||
| <li><strong>Redirect</strong>: Send user to requested resource</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>MFA Login Process</h3> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Primary Authentication</strong>: Password verification succeeds</li> | ||||
| <li><strong>MFA Challenge</strong>: Display TOTP input form</li> | ||||
| <li><strong>Code Verification</strong>: Validate TOTP code</li> | ||||
| <li><strong>Recovery Option</strong>: Allow recovery code if TOTP fails</li> | ||||
| <li><strong>Session Creation</strong>: Create authenticated session</li> | ||||
| <li><strong>State Tracking</strong>: Update last authentication state</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>SSO Login Process</h3> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Provider Redirect</strong>: Redirect to OpenID provider</li> | ||||
| <li><strong>Provider Authentication</strong>: User authenticates with provider</li> | ||||
| <li><strong>Authorization Code</strong>: Provider returns authorization code</li> | ||||
| <li><strong>Token Exchange</strong>: Exchange code for access token</li> | ||||
| <li><strong>User Info</strong>: Retrieve user information from provider</li> | ||||
| <li><strong>Local Session</strong>: Create local session for user</li> | ||||
| <li><strong>Access Grant</strong>: Allow access to protected resources</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Security Best Practices</h2> | ||||
|  | ||||
| <h3>Password Security</h3> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Strong Passwords</strong>: Require complex passwords</li> | ||||
| <li><strong>Regular Updates</strong>: Encourage periodic password changes</li> | ||||
| <li><strong>Unique Passwords</strong>: Don't reuse passwords from other services</li> | ||||
| <li><strong>Secure Storage</strong>: Use password managers</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Session Security</h3> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>HTTPS Only</strong>: Always use HTTPS in production</li> | ||||
| <li><strong>Secure Cookies</strong>: Enable secure flag for session cookies</li> | ||||
| <li><strong>Short Timeouts</strong>: Configure appropriate session timeouts</li> | ||||
| <li><strong>Regular Cleanup</strong>: Automatically clean expired sessions</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Multi-Factor Authentication</h3> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Enable MFA</strong>: Always enable MFA for sensitive installations</li> | ||||
| <li><strong>Secure Recovery</strong>: Store recovery codes securely</li> | ||||
| <li><strong>Regular Review</strong>: Periodically review MFA configuration</li> | ||||
| <li><strong>Backup Methods</strong>: Maintain multiple authentication methods</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Troubleshooting</h2> | ||||
|  | ||||
| <h3>Common Authentication Issues</h3> | ||||
|  | ||||
| <h4>Login Failures</h4> | ||||
|  | ||||
| <p><strong>Symptoms</strong>: Cannot login with correct credentials</p> | ||||
|  | ||||
| <p><strong>Possible Causes</strong>:</p> | ||||
| <ul> | ||||
| <li>Incorrect password</li> | ||||
| <li>Database connectivity issues</li> | ||||
| <li>Session storage problems</li> | ||||
| <li>Browser cookie issues</li> | ||||
| </ul> | ||||
|  | ||||
| <p><strong>Solutions</strong>:</p> | ||||
| <ol> | ||||
| <li>Verify password accuracy (check caps lock)</li> | ||||
| <li>Clear browser cookies and cache</li> | ||||
| <li>Check database connectivity</li> | ||||
| <li>Review server logs for errors</li> | ||||
| <li>Restart application if needed</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>MFA Issues</h4> | ||||
|  | ||||
| <p><strong>Symptoms</strong>: TOTP codes rejected or recovery codes fail</p> | ||||
|  | ||||
| <p><strong>Possible Causes</strong>:</p> | ||||
| <ul> | ||||
| <li>Clock synchronization issues</li> | ||||
| <li>Corrupted TOTP secret</li> | ||||
| <li>Used recovery codes</li> | ||||
| <li>Configuration problems</li> | ||||
| </ul> | ||||
|  | ||||
| <p><strong>Solutions</strong>:</p> | ||||
| <ol> | ||||
| <li>Synchronize device time</li> | ||||
| <li>Regenerate TOTP secret</li> | ||||
| <li>Use fresh recovery codes</li> | ||||
| <li>Check MFA configuration</li> | ||||
| <li>Contact administrator if needed</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Session Problems</h4> | ||||
|  | ||||
| <p><strong>Symptoms</strong>: Frequent logouts or session errors</p> | ||||
|  | ||||
| <p><strong>Possible Causes</strong>:</p> | ||||
| <ul> | ||||
| <li>Short session timeout</li> | ||||
| <li>Database session storage issues</li> | ||||
| <li>Browser cookie problems</li> | ||||
| <li>Network connectivity issues</li> | ||||
| </ul> | ||||
|  | ||||
| <p><strong>Solutions</strong>:</p> | ||||
| <ol> | ||||
| <li>Increase session timeout</li> | ||||
| <li>Check database permissions</li> | ||||
| <li>Enable browser cookies</li> | ||||
| <li>Verify network stability</li> | ||||
| <li>Review session configuration</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Security Monitoring</h3> | ||||
|  | ||||
| <h4>Log Analysis</h4> | ||||
|  | ||||
| <p>Monitor authentication logs for:</p> | ||||
| <ul> | ||||
| <li>Failed login attempts</li> | ||||
| <li>MFA failures</li> | ||||
| <li>Session anomalies</li> | ||||
| <li>Unusual access patterns</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Alert Configuration</h4> | ||||
|  | ||||
| <p>Set up alerts for:</p> | ||||
| <ul> | ||||
| <li>Multiple failed logins</li> | ||||
| <li>MFA bypass attempts</li> | ||||
| <li>Session manipulation</li> | ||||
| <li>Account lockouts</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Regular Audits</h4> | ||||
|  | ||||
| <p>Perform regular security audits:</p> | ||||
| <ul> | ||||
| <li>Review authentication logs</li> | ||||
| <li>Check session configurations</li> | ||||
| <li>Validate MFA setup</li> | ||||
| <li>Test recovery procedures</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Configuration Reference</h2> | ||||
|  | ||||
| <h3>Environment Variables</h3> | ||||
|  | ||||
| <pre><code class="language-bash"># Authentication settings | ||||
| TRILIUM_NO_AUTHENTICATION=false | ||||
| TRILIUM_PASSWORD_MIN_LENGTH=8 | ||||
| TRILIUM_SESSION_TIMEOUT=86400 | ||||
|  | ||||
| # MFA settings   | ||||
| TRILIUM_MFA_ENABLED=true | ||||
| TRILIUM_MFA_METHOD=totp | ||||
|  | ||||
| # OpenID settings | ||||
| TRILIUM_OPENID_ENABLED=false | ||||
| TRILIUM_OPENID_ISSUER=https://provider.example.com | ||||
| TRILIUM_OPENID_CLIENT_ID=your-client-id | ||||
| TRILIUM_OPENID_CLIENT_SECRET=your-client-secret | ||||
| </code></pre> | ||||
|  | ||||
| <h3>Database Options</h3> | ||||
|  | ||||
| <pre><code class="language-sql">-- Authentication options | ||||
| INSERT INTO options (name, value) VALUES  | ||||
| ('passwordMinLength', '8'), | ||||
| ('sessionTimeout', '86400'), | ||||
| ('mfaEnabled', 'true'), | ||||
| ('mfaMethod', 'totp'); | ||||
| </code></pre> | ||||
|  | ||||
| <p><strong>Remember</strong>: Strong authentication and session management are critical for protecting your notes. Always use HTTPS in production and enable MFA for enhanced security.</p> | ||||
|  | ||||
| </div> | ||||
							
								
								
									
										476
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Comprehensive-Security-Guide.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										476
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Comprehensive-Security-Guide.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,476 @@ | ||||
| <div class="note-content"> | ||||
|  | ||||
| <h1>Trilium Comprehensive Security Guide</h1> | ||||
|  | ||||
| <p>This comprehensive guide covers all aspects of Trilium security, from protected notes to enterprise deployment security practices.</p> | ||||
|  | ||||
| <div class="alert alert-info"> | ||||
|     <strong>Note:</strong> This guide contains advanced security configurations. Always test changes in a non-production environment first. | ||||
| </div> | ||||
|  | ||||
| <h2>Table of Contents</h2> | ||||
|  | ||||
| <ol> | ||||
|     <li><a href="#protected-notes">Protected Notes and Encryption</a></li> | ||||
|     <li><a href="#authentication">Authentication and Access Control</a></li> | ||||
|     <li><a href="#deployment">Secure Deployment</a></li> | ||||
|     <li><a href="#best-practices">Security Best Practices</a></li> | ||||
|     <li><a href="#monitoring">Security Monitoring</a></li> | ||||
|     <li><a href="#incident-response">Incident Response</a></li> | ||||
| </ol> | ||||
|  | ||||
| <h2 id="protected-notes">Protected Notes and Encryption</h2> | ||||
|  | ||||
| <h3>Overview</h3> | ||||
|  | ||||
| <p>Trilium's Protected Notes system provides robust encryption for sensitive content using industry-standard AES-128-CBC encryption with scrypt-based key derivation.</p> | ||||
|  | ||||
| <h4>Key Features</h4> | ||||
| <ul> | ||||
|     <li><strong>Selective Encryption:</strong> Only notes marked as protected are encrypted</li> | ||||
|     <li><strong>Strong Encryption:</strong> AES-128-CBC with scrypt key derivation</li> | ||||
|     <li><strong>Session-based Access:</strong> Encrypted content accessible during protected sessions</li> | ||||
|     <li><strong>Zero-knowledge:</strong> Server never stores unencrypted protected content</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Setting Up Protected Notes</h3> | ||||
|  | ||||
| <h4>Initial Configuration</h4> | ||||
|  | ||||
| <ol> | ||||
|     <li><strong>Set Master Password</strong> | ||||
|         <ul> | ||||
|             <li>Go to <em>Options → Security → Password</em></li> | ||||
|             <li>Choose a strong password (minimum 8 characters, recommended 12+)</li> | ||||
|             <li>Use a unique password not used elsewhere</li> | ||||
|         </ul> | ||||
|     </li> | ||||
|      | ||||
|     <li><strong>Configure Protected Session</strong> | ||||
|         <ul> | ||||
|             <li>Set session timeout (default: 10 minutes)</li> | ||||
|             <li>Configure auto-logout preferences</li> | ||||
|             <li>Enable session notifications</li> | ||||
|         </ul> | ||||
|     </li> | ||||
|      | ||||
|     <li><strong>Create Your First Protected Note</strong> | ||||
|         <ul> | ||||
|             <li>Right-click any note → "Toggle Protected Status"</li> | ||||
|             <li>Or use keyboard shortcut: <kbd>Ctrl+Shift+U</kbd></li> | ||||
|             <li>Or click Actions menu → "Protect this note"</li> | ||||
|         </ul> | ||||
|     </li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Managing Protected Sessions</h4> | ||||
|  | ||||
| <p><strong>Entering Protected Session:</strong></p> | ||||
| <ul> | ||||
|     <li>Click the shield icon in the toolbar</li> | ||||
|     <li>Use keyboard shortcut: <kbd>Ctrl+Shift+P</kbd></li> | ||||
|     <li>Automatic prompt when accessing protected content</li> | ||||
| </ul> | ||||
|  | ||||
| <p><strong>Session Security:</strong></p> | ||||
| <ul> | ||||
|     <li>Sessions timeout automatically after inactivity</li> | ||||
|     <li>Green shield indicates active protected session</li> | ||||
|     <li>Manual logout available via shield menu</li> | ||||
|     <li>Independent sessions per browser/client</li> | ||||
| </ul> | ||||
|  | ||||
| <div class="alert alert-warning"> | ||||
|     <strong>Security Note:</strong> Protected sessions store encryption keys in memory. Always log out when finished working with protected content. | ||||
| </div> | ||||
|  | ||||
| <h3>Encryption Technical Details</h3> | ||||
|  | ||||
| <h4>Encryption Process</h4> | ||||
| <pre><code>1. Generate random 16-byte IV | ||||
| 2. Compute SHA-1 digest of plaintext (integrity check)   | ||||
| 3. Prepend digest (4 bytes) to plaintext | ||||
| 4. Encrypt with AES-128-CBC using data key and IV | ||||
| 5. Prepend IV to encrypted data | ||||
| 6. Encode result as Base64</code></pre> | ||||
|  | ||||
| <h4>Key Management</h4> | ||||
| <ul> | ||||
|     <li><strong>Master Password:</strong> User-provided secret</li> | ||||
|     <li><strong>Password-Derived Key:</strong> Generated using scrypt (N=16384, r=8, p=1)</li> | ||||
|     <li><strong>Data Key:</strong> 32-byte random key, encrypted with password-derived key</li> | ||||
|     <li><strong>Session Key:</strong> Data key loaded into memory during protected session</li> | ||||
| </ul> | ||||
|  | ||||
| <h2 id="authentication">Authentication and Access Control</h2> | ||||
|  | ||||
| <h3>Password Authentication</h3> | ||||
|  | ||||
| <h4>Security Features</h4> | ||||
| <ul> | ||||
|     <li><strong>Scrypt Hashing:</strong> CPU-intensive hashing prevents brute force attacks</li> | ||||
|     <li><strong>Salt:</strong> Unique random salt per installation</li> | ||||
|     <li><strong>Timing Attack Protection:</strong> Constant-time comparison</li> | ||||
|     <li><strong>Separation:</strong> Authentication separate from encryption keys</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Password Management</h4> | ||||
| <ul> | ||||
|     <li><strong>Strong Passwords:</strong> Use complex, unique passwords</li> | ||||
|     <li><strong>Regular Updates:</strong> Change passwords periodically</li> | ||||
|     <li><strong>Secure Storage:</strong> Consider using a password manager</li> | ||||
|     <li><strong>Recovery:</strong> No built-in password recovery - keep backup access</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Multi-Factor Authentication (MFA)</h3> | ||||
|  | ||||
| <h4>TOTP Setup</h4> | ||||
|  | ||||
| <ol> | ||||
|     <li>Go to <em>Options → Security → Multi-Factor Authentication</em></li> | ||||
|     <li>Click "Generate New Secret"</li> | ||||
|     <li>Scan QR code with authenticator app</li> | ||||
|     <li>Enter TOTP code to verify setup</li> | ||||
|     <li>Save recovery codes securely</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Supported Authenticators</h4> | ||||
| <ul> | ||||
|     <li>Google Authenticator</li> | ||||
|     <li>Authy</li> | ||||
|     <li>Microsoft Authenticator</li> | ||||
|     <li>1Password</li> | ||||
|     <li>Any RFC 6238 compatible app</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Recovery Codes</h4> | ||||
| <ul> | ||||
|     <li><strong>Format:</strong> Base64-encoded 24-character strings</li> | ||||
|     <li><strong>Storage:</strong> AES-256-CBC encrypted</li> | ||||
|     <li><strong>Usage:</strong> One-time use only</li> | ||||
|     <li><strong>Security:</strong> Store in secure location (password manager)</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Session Management</h3> | ||||
|  | ||||
| <h4>Session Security</h4> | ||||
| <ul> | ||||
|     <li><strong>Secure Storage:</strong> Sessions stored in encrypted database</li> | ||||
|     <li><strong>Automatic Cleanup:</strong> Expired sessions removed periodically</li> | ||||
|     <li><strong>CSRF Protection:</strong> Double-submit cookie pattern</li> | ||||
|     <li><strong>Secure Cookies:</strong> HTTPOnly, Secure, SameSite attributes</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Configuration Options</h4> | ||||
| <ul> | ||||
|     <li><strong>Session Timeout:</strong> Configurable timeout period</li> | ||||
|     <li><strong>Remember Me:</strong> Extended session duration option</li> | ||||
|     <li><strong>Auto Logout:</strong> Automatic logout on browser close</li> | ||||
|     <li><strong>Multi-client:</strong> Independent sessions per device</li> | ||||
| </ul> | ||||
|  | ||||
| <h2 id="deployment">Secure Deployment</h2> | ||||
|  | ||||
| <h3>HTTPS Configuration</h3> | ||||
|  | ||||
| <h4>SSL/TLS Requirements</h4> | ||||
| <ul> | ||||
|     <li><strong>Mandatory for Production:</strong> Always use HTTPS in production</li> | ||||
|     <li><strong>TLS Version:</strong> Use TLS 1.2 or higher</li> | ||||
|     <li><strong>Certificates:</strong> Valid SSL certificates (Let's Encrypt recommended)</li> | ||||
|     <li><strong>HSTS:</strong> Enable HTTP Strict Transport Security</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Security Headers</h4> | ||||
| <pre><code># Essential security headers | ||||
| X-Frame-Options: DENY | ||||
| X-Content-Type-Options: nosniff   | ||||
| X-XSS-Protection: 1; mode=block | ||||
| Strict-Transport-Security: max-age=31536000; includeSubDomains | ||||
| Content-Security-Policy: default-src 'self'; ...</code></pre> | ||||
|  | ||||
| <h3>Network Security</h3> | ||||
|  | ||||
| <h4>Firewall Configuration</h4> | ||||
| <ul> | ||||
|     <li><strong>Restrict Ports:</strong> Only allow necessary ports (22, 443)</li> | ||||
|     <li><strong>Block Direct Access:</strong> Block direct access to Trilium port (8080)</li> | ||||
|     <li><strong>IP Restrictions:</strong> Limit access to trusted IP ranges</li> | ||||
|     <li><strong>Rate Limiting:</strong> Implement connection rate limiting</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Reverse Proxy</h4> | ||||
| <ul> | ||||
|     <li><strong>Nginx/Apache:</strong> Use reverse proxy for SSL termination</li> | ||||
|     <li><strong>Load Balancing:</strong> Distribute traffic across instances</li> | ||||
|     <li><strong>Caching:</strong> Cache static content</li> | ||||
|     <li><strong>Compression:</strong> Enable content compression</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Database Security</h3> | ||||
|  | ||||
| <h4>File Permissions</h4> | ||||
| <ul> | ||||
|     <li><strong>Database:</strong> 600 (owner read/write only)</li> | ||||
|     <li><strong>Data Directory:</strong> 700 (owner access only)</li> | ||||
|     <li><strong>Configuration:</strong> 600 (owner read/write only)</li> | ||||
|     <li><strong>Ownership:</strong> Dedicated trilium user</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Backup Security</h4> | ||||
| <ul> | ||||
|     <li><strong>Encryption:</strong> Always encrypt backups</li> | ||||
|     <li><strong>Storage:</strong> Secure off-site storage</li> | ||||
|     <li><strong>Access Control:</strong> Limit backup access</li> | ||||
|     <li><strong>Testing:</strong> Regular restoration testing</li> | ||||
| </ul> | ||||
|  | ||||
| <h2 id="best-practices">Security Best Practices</h2> | ||||
|  | ||||
| <h3>Password Security</h3> | ||||
|  | ||||
| <h4>Password Requirements</h4> | ||||
| <ul> | ||||
|     <li><strong>Length:</strong> Minimum 12 characters</li> | ||||
|     <li><strong>Complexity:</strong> Mix of letters, numbers, symbols</li> | ||||
|     <li><strong>Uniqueness:</strong> Don't reuse passwords</li> | ||||
|     <li><strong>Passphrases:</strong> Consider using memorable passphrases</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Password Management</h4> | ||||
| <ul> | ||||
|     <li><strong>Password Manager:</strong> Use reputable password manager</li> | ||||
|     <li><strong>Regular Updates:</strong> Change passwords periodically</li> | ||||
|     <li><strong>Secure Recovery:</strong> Store recovery information safely</li> | ||||
|     <li><strong>Team Coordination:</strong> Coordinate password changes in shared environments</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Access Control</h3> | ||||
|  | ||||
| <h4>User Management</h4> | ||||
| <ul> | ||||
|     <li><strong>Single User Model:</strong> Trilium designed for single-user access</li> | ||||
|     <li><strong>Shared Access:</strong> Use with caution in shared environments</li> | ||||
|     <li><strong>Guest Access:</strong> Disable unless specifically needed</li> | ||||
|     <li><strong>Admin Privileges:</strong> Run with minimal necessary privileges</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Session Management</h4> | ||||
| <ul> | ||||
|     <li><strong>Timeout Configuration:</strong> Set appropriate timeouts for usage pattern</li> | ||||
|     <li><strong>Device Security:</strong> Lock workstation when away</li> | ||||
|     <li><strong>Shared Computers:</strong> Always log out completely</li> | ||||
|     <li><strong>Browser Security:</strong> Use up-to-date browsers</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Data Protection</h3> | ||||
|  | ||||
| <h4>Backup Strategy</h4> | ||||
| <ul> | ||||
|     <li><strong>Regular Backups:</strong> Automated daily backups</li> | ||||
|     <li><strong>Encryption:</strong> All backups encrypted with strong keys</li> | ||||
|     <li><strong>Multiple Locations:</strong> Store backups in multiple secure locations</li> | ||||
|     <li><strong>Version Control:</strong> Maintain multiple backup versions</li> | ||||
|     <li><strong>Testing:</strong> Regular restoration testing</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Data Classification</h4> | ||||
| <ul> | ||||
|     <li><strong>Sensitive Data:</strong> Always use protected notes for sensitive content</li> | ||||
|     <li><strong>Public Data:</strong> Separate public and private information</li> | ||||
|     <li><strong>Compliance:</strong> Follow industry-specific requirements</li> | ||||
|     <li><strong>Retention:</strong> Implement data retention policies</li> | ||||
| </ul> | ||||
|  | ||||
| <h2 id="monitoring">Security Monitoring</h2> | ||||
|  | ||||
| <h3>Log Monitoring</h3> | ||||
|  | ||||
| <h4>Security Events</h4> | ||||
| <ul> | ||||
|     <li><strong>Authentication:</strong> Monitor login attempts and failures</li> | ||||
|     <li><strong>Authorization:</strong> Track access to protected resources</li> | ||||
|     <li><strong>Sessions:</strong> Monitor session creation and termination</li> | ||||
|     <li><strong>Data Access:</strong> Log protected note access</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Alerting</h4> | ||||
| <ul> | ||||
|     <li><strong>Failed Logins:</strong> Alert on multiple failed attempts</li> | ||||
|     <li><strong>MFA Failures:</strong> Monitor MFA bypass attempts</li> | ||||
|     <li><strong>Session Anomalies:</strong> Detect unusual session patterns</li> | ||||
|     <li><strong>Data Changes:</strong> Monitor unexpected data modifications</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Intrusion Detection</h3> | ||||
|  | ||||
| <h4>Behavioral Analysis</h4> | ||||
| <ul> | ||||
|     <li><strong>Login Patterns:</strong> Detect unusual login times/locations</li> | ||||
|     <li><strong>Access Patterns:</strong> Monitor unusual data access</li> | ||||
|     <li><strong>Session Behavior:</strong> Identify suspicious session activity</li> | ||||
|     <li><strong>Network Activity:</strong> Monitor network connection patterns</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Automated Response</h4> | ||||
| <ul> | ||||
|     <li><strong>Account Lockout:</strong> Temporary suspension of suspicious accounts</li> | ||||
|     <li><strong>IP Blocking:</strong> Block suspicious IP addresses</li> | ||||
|     <li><strong>Rate Limiting:</strong> Dynamic rate limit adjustment</li> | ||||
|     <li><strong>Alert Generation:</strong> Immediate notification of threats</li> | ||||
| </ul> | ||||
|  | ||||
| <h2 id="incident-response">Incident Response</h2> | ||||
|  | ||||
| <h3>Preparation</h3> | ||||
|  | ||||
| <h4>Response Plan</h4> | ||||
| <ul> | ||||
|     <li><strong>Procedures:</strong> Document step-by-step response procedures</li> | ||||
|     <li><strong>Contacts:</strong> Maintain emergency contact information</li> | ||||
|     <li><strong>Tools:</strong> Prepare incident response tools and scripts</li> | ||||
|     <li><strong>Communication:</strong> Plan user notification procedures</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Training</h4> | ||||
| <ul> | ||||
|     <li><strong>Team Training:</strong> Regular incident response training</li> | ||||
|     <li><strong>Simulations:</strong> Practice incident response scenarios</li> | ||||
|     <li><strong>Documentation:</strong> Keep response procedures updated</li> | ||||
|     <li><strong>Lessons Learned:</strong> Update procedures based on incidents</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Detection and Response</h3> | ||||
|  | ||||
| <h4>Immediate Actions</h4> | ||||
| <ol> | ||||
|     <li><strong>Identify:</strong> Quickly identify the type and scope of incident</li> | ||||
|     <li><strong>Contain:</strong> Isolate affected systems to prevent spread</li> | ||||
|     <li><strong>Preserve:</strong> Preserve evidence for forensic analysis</li> | ||||
|     <li><strong>Notify:</strong> Inform relevant stakeholders</li> | ||||
|     <li><strong>Assess:</strong> Evaluate impact and required response</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Recovery Procedures</h4> | ||||
| <ul> | ||||
|     <li><strong>System Isolation:</strong> Temporarily isolate compromised systems</li> | ||||
|     <li><strong>Forensic Backup:</strong> Create forensic copies for analysis</li> | ||||
|     <li><strong>Restore from Backup:</strong> Restore from known good backups</li> | ||||
|     <li><strong>Verify Integrity:</strong> Confirm system and data integrity</li> | ||||
|     <li><strong>Resume Operations:</strong> Safely resume normal operations</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Post-Incident</h3> | ||||
|  | ||||
| <h4>Documentation</h4> | ||||
| <ul> | ||||
|     <li><strong>Incident Report:</strong> Complete incident documentation</li> | ||||
|     <li><strong>Timeline:</strong> Detailed timeline of events and responses</li> | ||||
|     <li><strong>Impact Assessment:</strong> Evaluation of damage and losses</li> | ||||
|     <li><strong>Lessons Learned:</strong> Identify improvements for future</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Improvement</h4> | ||||
| <ul> | ||||
|     <li><strong>Process Updates:</strong> Update response procedures</li> | ||||
|     <li><strong>Security Enhancements:</strong> Implement additional security controls</li> | ||||
|     <li><strong>Training Updates:</strong> Update training based on lessons learned</li> | ||||
|     <li><strong>Regular Reviews:</strong> Periodic review of incident response capability</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Troubleshooting</h2> | ||||
|  | ||||
| <h3>Common Issues</h3> | ||||
|  | ||||
| <h4>Authentication Problems</h4> | ||||
| <div class="alert alert-info"> | ||||
|     <strong>Problem:</strong> Cannot login with correct credentials<br> | ||||
|     <strong>Solutions:</strong> | ||||
|     <ul> | ||||
|         <li>Check password spelling and case sensitivity</li> | ||||
|         <li>Clear browser cookies and cache</li> | ||||
|         <li>Verify database connectivity</li> | ||||
|         <li>Check server logs for errors</li> | ||||
|         <li>Restart application if needed</li> | ||||
|     </ul> | ||||
| </div> | ||||
|  | ||||
| <h4>Protected Notes Issues</h4> | ||||
| <div class="alert alert-warning"> | ||||
|     <strong>Problem:</strong> "Could not decrypt string" error<br> | ||||
|     <strong>Solutions:</strong> | ||||
|     <ul> | ||||
|         <li>Verify correct password entry</li> | ||||
|         <li>Check for active protected session</li> | ||||
|         <li>Restart application and retry</li> | ||||
|         <li>Restore from backup if corruption suspected</li> | ||||
|     </ul> | ||||
| </div> | ||||
|  | ||||
| <h4>MFA Problems</h4> | ||||
| <div class="alert alert-info"> | ||||
|     <strong>Problem:</strong> TOTP codes rejected<br> | ||||
|     <strong>Solutions:</strong> | ||||
|     <ul> | ||||
|         <li>Synchronize device time</li> | ||||
|         <li>Try recovery codes</li> | ||||
|         <li>Regenerate TOTP secret</li> | ||||
|         <li>Check authenticator app configuration</li> | ||||
|     </ul> | ||||
| </div> | ||||
|  | ||||
| <h3>Emergency Procedures</h3> | ||||
|  | ||||
| <h4>Password Recovery</h4> | ||||
| <div class="alert alert-danger"> | ||||
|     <strong>Important:</strong> Trilium cannot recover forgotten passwords. Options include: | ||||
|     <ul> | ||||
|         <li>Restore from backup with known password</li> | ||||
|         <li>Export unprotected content before password reset</li> | ||||
|         <li>Complete reset (loses all protected content)</li> | ||||
|     </ul> | ||||
| </div> | ||||
|  | ||||
| <h4>Data Recovery</h4> | ||||
| <ul> | ||||
|     <li><strong>Backup Restoration:</strong> Use recent encrypted backups</li> | ||||
|     <li><strong>Database Repair:</strong> Use SQLite repair tools if needed</li> | ||||
|     <li><strong>Partial Recovery:</strong> Export accessible content</li> | ||||
|     <li><strong>Professional Help:</strong> Contact data recovery services for critical data</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Compliance and Standards</h2> | ||||
|  | ||||
| <h3>Regulatory Compliance</h3> | ||||
|  | ||||
| <h4>GDPR Compliance</h4> | ||||
| <ul> | ||||
|     <li><strong>Data Protection:</strong> AES encryption provides technical safeguards</li> | ||||
|     <li><strong>Right to Erasure:</strong> Secure deletion of encryption keys</li> | ||||
|     <li><strong>Data Portability:</strong> Export capabilities for protected content</li> | ||||
|     <li><strong>Privacy by Design:</strong> Encryption built into architecture</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Industry Standards</h4> | ||||
| <ul> | ||||
|     <li><strong>ISO 27001:</strong> Information security management compliance</li> | ||||
|     <li><strong>SOC 2:</strong> Security and availability controls</li> | ||||
|     <li><strong>HIPAA:</strong> Healthcare data protection requirements</li> | ||||
|     <li><strong>PCI DSS:</strong> Payment card industry standards</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Encryption Standards</h3> | ||||
|  | ||||
| <h4>Algorithm Compliance</h4> | ||||
| <ul> | ||||
|     <li><strong>AES-128:</strong> NIST approved, FIPS 140-2 Level 1</li> | ||||
|     <li><strong>Scrypt:</strong> RFC 7914 standard</li> | ||||
|     <li><strong>SHA-1:</strong> NIST standard (integrity verification only)</li> | ||||
|     <li><strong>Random Generation:</strong> Cryptographically secure sources</li> | ||||
| </ul> | ||||
|  | ||||
| <div class="alert alert-success"> | ||||
|     <strong>Remember:</strong> Security is an ongoing process, not a one-time configuration. Regularly review and update your security posture to address evolving threats and requirements. | ||||
| </div> | ||||
|  | ||||
| </div> | ||||
							
								
								
									
										326
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Protected Notes and Encryption.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										326
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Protected Notes and Encryption.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,326 @@ | ||||
| <div class="note-content"> | ||||
|  | ||||
| <h1>Protected Notes and Encryption</h1> | ||||
|  | ||||
| <p>Trilium provides robust encryption capabilities through its Protected Notes system, ensuring your sensitive information remains secure even if your database is compromised.</p> | ||||
|  | ||||
| <h2>Overview</h2> | ||||
|  | ||||
| <p>Protected notes in Trilium use <strong>AES-128-CBC encryption</strong> with scrypt-based key derivation to protect sensitive content. The encryption is designed to be:</p> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>Secure</strong>: Uses industry-standard AES encryption with strong key derivation</li> | ||||
| <li><strong>Selective</strong>: Only notes marked as protected are encrypted</li> | ||||
| <li><strong>Session-based</strong>: Decrypted content remains accessible during a protected session</li> | ||||
| <li><strong>Zero-knowledge</strong>: The server never stores unencrypted protected content</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>How Encryption Works</h2> | ||||
|  | ||||
| <h3>Encryption Algorithm</h3> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>Cipher</strong>: AES-128-CBC (Advanced Encryption Standard in Cipher Block Chaining mode)</li> | ||||
| <li><strong>Key Derivation</strong>: Scrypt with configurable parameters (N=16384, r=8, p=1)</li> | ||||
| <li><strong>Initialization Vector</strong>: 16-byte random IV generated for each encryption operation</li> | ||||
| <li><strong>Integrity Protection</strong>: SHA-1 digest (first 4 bytes) prepended to plaintext for tamper detection</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Key Management</h3> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Master Password</strong>: User-provided password used for key derivation</li> | ||||
| <li><strong>Data Key</strong>: 32-byte random key generated during setup, encrypted with password-derived key</li> | ||||
| <li><strong>Password-Derived Key</strong>: Generated using scrypt from master password and salt</li> | ||||
| <li><strong>Session Key</strong>: Data key loaded into memory during protected session</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Encryption Process</h3> | ||||
|  | ||||
| <pre><code>1. Generate random 16-byte IV | ||||
| 2. Compute SHA-1 digest of plaintext (use first 4 bytes) | ||||
| 3. Prepend digest to plaintext | ||||
| 4. Encrypt (digest + plaintext) using AES-128-CBC | ||||
| 5. Prepend IV to encrypted data | ||||
| 6. Encode result as Base64 | ||||
| </code></pre> | ||||
|  | ||||
| <h3>Decryption Process</h3> | ||||
|  | ||||
| <pre><code>1. Decode Base64 ciphertext | ||||
| 2. Extract IV (first 16 bytes) and encrypted data | ||||
| 3. Decrypt using AES-128-CBC with data key and IV | ||||
| 4. Extract digest (first 4 bytes) and plaintext | ||||
| 5. Verify integrity by comparing computed vs. stored digest | ||||
| 6. Return plaintext if verification succeeds | ||||
| </code></pre> | ||||
|  | ||||
| <h2>Setting Up Protected Notes</h2> | ||||
|  | ||||
| <h3>Initial Setup</h3> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Set Master Password</strong>: Configure a strong password during initial setup</li> | ||||
| <li><strong>Create Protected Note</strong>: Right-click a note and select "Toggle Protected Status"</li> | ||||
| <li><strong>Enter Protected Session</strong>: Click the shield icon or use Ctrl+Shift+P</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Password Requirements</h3> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>Minimum Length</strong>: 8 characters (recommended: 12+ characters)</li> | ||||
| <li><strong>Complexity</strong>: Use a mix of uppercase, lowercase, numbers, and symbols</li> | ||||
| <li><strong>Uniqueness</strong>: Don't reuse passwords from other services</li> | ||||
| <li><strong>Storage</strong>: Consider using a password manager for complex passwords</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Best Practices</h3> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Strong Passwords</strong>: Use passphrases or generated passwords</li> | ||||
| <li><strong>Regular Changes</strong>: Update passwords periodically</li> | ||||
| <li><strong>Secure Storage</strong>: Store password recovery information securely</li> | ||||
| <li><strong>Backup Strategy</strong>: Ensure encrypted backups are properly secured</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Protected Sessions</h2> | ||||
|  | ||||
| <h3>Session Management</h3> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>Automatic Timeout</strong>: Sessions expire after configurable timeout (default: 10 minutes)</li> | ||||
| <li><strong>Manual Control</strong>: Explicitly enter/exit protected sessions</li> | ||||
| <li><strong>Activity Tracking</strong>: Session timeout resets with each protected note access</li> | ||||
| <li><strong>Multi-client</strong>: Each client maintains its own protected session</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Session Lifecycle</h3> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Enter Session</strong>: User enters master password</li> | ||||
| <li><strong>Key Derivation</strong>: System derives data key from password</li> | ||||
| <li><strong>Session Active</strong>: Protected content accessible in plaintext</li> | ||||
| <li><strong>Timeout/Logout</strong>: Data key removed from memory</li> | ||||
| <li><strong>Protection Restored</strong>: Content returns to encrypted state</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Configuration Options</h3> | ||||
|  | ||||
| <p>Access via Options → Protected Session:</p> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>Session Timeout</strong>: Duration before automatic logout (seconds)</li> | ||||
| <li><strong>Password Verification</strong>: Enable/disable password strength requirements</li> | ||||
| <li><strong>Recovery Options</strong>: Configure password recovery mechanisms</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Performance Considerations</h2> | ||||
|  | ||||
| <h3>Encryption Overhead</h3> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>CPU Impact</strong>: Scrypt key derivation is intentionally CPU-intensive</li> | ||||
| <li><strong>Memory Usage</strong>: Minimal additional memory for encrypted content</li> | ||||
| <li><strong>Storage Size</strong>: Encrypted content is slightly larger due to Base64 encoding</li> | ||||
| <li><strong>Network Transfer</strong>: Encrypted notes transfer as Base64 strings</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Optimization Tips</h3> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Selective Protection</strong>: Only encrypt truly sensitive notes</li> | ||||
| <li><strong>Session Management</strong>: Keep sessions active during intensive work</li> | ||||
| <li><strong>Hardware Acceleration</strong>: Modern CPUs provide AES acceleration</li> | ||||
| <li><strong>Batch Operations</strong>: Group protected note operations when possible</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Security Considerations</h2> | ||||
|  | ||||
| <h3>Threat Model</h3> | ||||
|  | ||||
| <p><strong>Protected Against</strong>:</p> | ||||
| <ul> | ||||
| <li>Database theft or unauthorized access</li> | ||||
| <li>Network interception (data at rest)</li> | ||||
| <li>Server-side data breaches</li> | ||||
| <li>Backup file compromise</li> | ||||
| </ul> | ||||
|  | ||||
| <p><strong>Not Protected Against</strong>:</p> | ||||
| <ul> | ||||
| <li>Keyloggers or screen capture malware</li> | ||||
| <li>Physical access to unlocked device</li> | ||||
| <li>Memory dumps during active session</li> | ||||
| <li>Social engineering attacks</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Limitations</h3> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Note Titles</strong>: Currently encrypted, may leak structural information</li> | ||||
| <li><strong>Metadata</strong>: Creation dates, modification times remain unencrypted</li> | ||||
| <li><strong>Search Indexing</strong>: Protected notes excluded from full-text search</li> | ||||
| <li><strong>Sync Conflicts</strong>: May be harder to resolve for protected content</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Troubleshooting</h2> | ||||
|  | ||||
| <h3>Common Issues</h3> | ||||
|  | ||||
| <h4>"Could not decrypt string" Error</h4> | ||||
|  | ||||
| <p><strong>Causes</strong>:</p> | ||||
| <ul> | ||||
| <li>Incorrect password entered</li> | ||||
| <li>Corrupted encrypted data</li> | ||||
| <li>Database migration issues</li> | ||||
| </ul> | ||||
|  | ||||
| <p><strong>Solutions</strong>:</p> | ||||
| <ol> | ||||
| <li>Verify password spelling and case sensitivity</li> | ||||
| <li>Check for active protected session</li> | ||||
| <li>Restart application and retry</li> | ||||
| <li>Restore from backup if corruption suspected</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Protected Session Won't Start</h4> | ||||
|  | ||||
| <p><strong>Causes</strong>:</p> | ||||
| <ul> | ||||
| <li>Password verification hash mismatch</li> | ||||
| <li>Missing encryption salt</li> | ||||
| <li>Database schema issues</li> | ||||
| </ul> | ||||
|  | ||||
| <p><strong>Solutions</strong>:</p> | ||||
| <ol> | ||||
| <li>Check error logs for specific error messages</li> | ||||
| <li>Verify database integrity</li> | ||||
| <li>Restore from known good backup</li> | ||||
| <li>Contact support with error details</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Performance Issues</h4> | ||||
|  | ||||
| <p><strong>Symptoms</strong>:</p> | ||||
| <ul> | ||||
| <li>Slow password verification</li> | ||||
| <li>Long delays entering protected session</li> | ||||
| <li>High CPU usage during encryption</li> | ||||
| </ul> | ||||
|  | ||||
| <p><strong>Solutions</strong>:</p> | ||||
| <ol> | ||||
| <li>Reduce scrypt parameters (advanced users only)</li> | ||||
| <li>Limit number of protected notes</li> | ||||
| <li>Upgrade hardware (more RAM/faster CPU)</li> | ||||
| <li>Close other resource-intensive applications</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Recovery Procedures</h3> | ||||
|  | ||||
| <h4>Password Recovery</h4> | ||||
|  | ||||
| <p>If you forget your master password:</p> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>No Built-in Recovery</strong>: Trilium cannot recover forgotten passwords</li> | ||||
| <li><strong>Backup Restoration</strong>: Restore from backup with known password</li> | ||||
| <li><strong>Data Export</strong>: Export unprotected content before password change</li> | ||||
| <li><strong>Complete Reset</strong>: Last resort - lose all protected content</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Data Recovery</h4> | ||||
|  | ||||
| <p>For corrupted protected notes:</p> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Verify Backup</strong>: Check if backups contain uncorrupted data</li> | ||||
| <li><strong>Export/Import</strong>: Try exporting and re-importing the note</li> | ||||
| <li><strong>Database Repair</strong>: Use database repair tools if available</li> | ||||
| <li><strong>Professional Help</strong>: Contact data recovery services for critical data</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Advanced Configuration</h2> | ||||
|  | ||||
| <h3>Custom Encryption Parameters</h3> | ||||
|  | ||||
| <p><strong>Warning</strong>: Modifying encryption parameters requires advanced knowledge and may break compatibility.</p> | ||||
|  | ||||
| <p>For expert users, encryption parameters can be modified in the source code:</p> | ||||
|  | ||||
| <pre><code class="language-typescript">// In my_scrypt.ts | ||||
| const scryptParams = { | ||||
|     N: 16384,  // CPU/memory cost parameter | ||||
|     r: 8,      // Block size parameter   | ||||
|     p: 1       // Parallelization parameter | ||||
| }; | ||||
| </code></pre> | ||||
|  | ||||
| <h3>Integration with External Tools</h3> | ||||
|  | ||||
| <p>Protected notes can be accessed programmatically:</p> | ||||
|  | ||||
| <pre><code class="language-javascript">// Backend script example | ||||
| const protectedNote = api.getNote('noteId'); | ||||
| if (protectedNote.isProtected) { | ||||
|     // Content will be encrypted unless in protected session | ||||
|     const content = protectedNote.getContent(); | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h2>Compliance and Auditing</h2> | ||||
|  | ||||
| <h3>Encryption Standards</h3> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>Algorithm</strong>: AES-128-CBC (FIPS 140-2 approved)</li> | ||||
| <li><strong>Key Derivation</strong>: Scrypt (RFC 7914)</li> | ||||
| <li><strong>Random Generation</strong>: Node.js crypto.randomBytes() (OS entropy)</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Audit Trail</h3> | ||||
|  | ||||
| <ul> | ||||
| <li>Protected session entry/exit events logged</li> | ||||
| <li>Encryption/decryption operations tracked</li> | ||||
| <li>Password verification attempts recorded</li> | ||||
| <li>Key derivation operations monitored</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Compliance Considerations</h3> | ||||
|  | ||||
| <ul> | ||||
| <li><strong>GDPR</strong>: Encryption provides data protection safeguards</li> | ||||
| <li><strong>HIPAA</strong>: AES encryption meets security requirements</li> | ||||
| <li><strong>SOX</strong>: Audit trails support compliance requirements</li> | ||||
| <li><strong>PCI DSS</strong>: Strong encryption protects sensitive data</li> | ||||
| </ul> | ||||
|  | ||||
| <h2>Migration and Backup</h2> | ||||
|  | ||||
| <h3>Backup Strategies</h3> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Encrypted Backups</strong>: Regular backups preserve encrypted state</li> | ||||
| <li><strong>Unencrypted Exports</strong>: Export protected content during session</li> | ||||
| <li><strong>Key Management</strong>: Securely store password recovery information</li> | ||||
| <li><strong>Testing</strong>: Regularly test backup restoration procedures</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Migration Procedures</h3> | ||||
|  | ||||
| <p>When moving to new installation:</p> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Export Data</strong>: Export all notes including protected content</li> | ||||
| <li><strong>Backup Database</strong>: Create complete database backup</li> | ||||
| <li><strong>Transfer Files</strong>: Move exported files to new installation</li> | ||||
| <li><strong>Import Data</strong>: Import using same master password</li> | ||||
| <li><strong>Verify</strong>: Confirm all protected content accessible</li> | ||||
| </ol> | ||||
|  | ||||
| <p><strong>Remember</strong>: The security of protected notes ultimately depends on choosing a strong master password and following security best practices for your overall system.</p> | ||||
|  | ||||
| </div> | ||||
							
								
								
									
										457
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Security Best Practices.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										457
									
								
								apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Security Best Practices.html
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,457 @@ | ||||
| <div class="note-content"> | ||||
|  | ||||
| <h1>Security Best Practices</h1> | ||||
|  | ||||
| <p>This guide provides comprehensive security recommendations for deploying and maintaining a secure Trilium installation.</p> | ||||
|  | ||||
| <h2>Deployment Security</h2> | ||||
|  | ||||
| <h3>Server Configuration</h3> | ||||
|  | ||||
| <h4>HTTPS Deployment</h4> | ||||
|  | ||||
| <p><strong>Always use HTTPS in production environments:</strong></p> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>TLS Configuration</strong>: Use TLS 1.2 or higher</li> | ||||
| <li><strong>Certificate Management</strong>: Use valid SSL certificates (Let's Encrypt recommended)</li> | ||||
| <li><strong>HSTS Headers</strong>: Enable HTTP Strict Transport Security</li> | ||||
| <li><strong>Secure Redirects</strong>: Redirect all HTTP traffic to HTTPS</li> | ||||
| </ol> | ||||
|  | ||||
| <p>Example Nginx configuration:</p> | ||||
| <pre><code class="language-nginx">server { | ||||
|     listen 443 ssl http2; | ||||
|     server_name your-trilium.example.com; | ||||
|      | ||||
|     ssl_certificate /path/to/cert.pem; | ||||
|     ssl_certificate_key /path/to/private.key; | ||||
|     ssl_protocols TLSv1.2 TLSv1.3; | ||||
|     ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384; | ||||
|      | ||||
|     add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; | ||||
|     add_header X-Frame-Options DENY; | ||||
|     add_header X-Content-Type-Options nosniff; | ||||
|      | ||||
|     location / { | ||||
|         proxy_pass http://localhost:8080; | ||||
|         proxy_set_header Host $host; | ||||
|         proxy_set_header X-Real-IP $remote_addr; | ||||
|         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; | ||||
|         proxy_set_header X-Forwarded-Proto $scheme; | ||||
|     } | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h4>Network Security</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Firewall Configuration</strong>: Restrict access to necessary ports only</li> | ||||
| <li><strong>Port Security</strong>: Use non-standard ports if required</li> | ||||
| <li><strong>IP Restrictions</strong>: Limit access to trusted IP ranges</li> | ||||
| <li><strong>VPN Access</strong>: Consider VPN for remote access</li> | ||||
| </ol> | ||||
|  | ||||
| <p>Example firewall rules:</p> | ||||
| <pre><code class="language-bash"># Allow only HTTPS and SSH | ||||
| ufw allow 22/tcp | ||||
| ufw allow 443/tcp | ||||
| ufw deny 8080/tcp  # Block direct access to Trilium | ||||
| ufw enable | ||||
| </code></pre> | ||||
|  | ||||
| <h3>Access Control</h3> | ||||
|  | ||||
| <h4>User Management</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Single User Model</strong>: Trilium is designed for single-user access</li> | ||||
| <li><strong>Shared Access</strong>: Use shared hosting or family sharing with caution</li> | ||||
| <li><strong>Guest Access</strong>: Disable guest access unless specifically needed</li> | ||||
| <li><strong>Admin Privileges</strong>: Run Trilium with minimal necessary privileges</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Authentication Hardening</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Strong Passwords</strong>: Enforce complex password requirements</li> | ||||
| <li><strong>Multi-Factor Authentication</strong>: Always enable MFA for production</li> | ||||
| <li><strong>Password Rotation</strong>: Regular password updates</li> | ||||
| <li><strong>Account Lockout</strong>: Monitor for brute force attempts</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Data Protection</h3> | ||||
|  | ||||
| <h4>Backup Security</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Encrypted Backups</strong>: Ensure backups are encrypted at rest</li> | ||||
| <li><strong>Secure Storage</strong>: Store backups in secure locations</li> | ||||
| <li><strong>Access Control</strong>: Limit backup access to authorized personnel</li> | ||||
| <li><strong>Regular Testing</strong>: Verify backup integrity regularly</li> | ||||
| </ol> | ||||
|  | ||||
| <p>Backup encryption example:</p> | ||||
| <pre><code class="language-bash"># Create encrypted backup | ||||
| tar czf - trilium-data/ | gpg --cipher-algo AES256 --compress-algo 1 --symmetric --output trilium-backup-$(date +%Y%m%d).tar.gz.gpg | ||||
|  | ||||
| # Restore encrypted backup | ||||
| gpg --decrypt trilium-backup-20240101.tar.gz.gpg | tar xzf - | ||||
| </code></pre> | ||||
|  | ||||
| <h4>Database Security</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>File Permissions</strong>: Restrict database file access (600 or 640)</li> | ||||
| <li><strong>Directory Security</strong>: Secure data directory permissions</li> | ||||
| <li><strong>Regular Monitoring</strong>: Monitor for unauthorized access attempts</li> | ||||
| <li><strong>Integrity Checks</strong>: Verify database integrity regularly</li> | ||||
| </ol> | ||||
|  | ||||
| <pre><code class="language-bash"># Secure file permissions | ||||
| chmod 600 /path/to/trilium/data/document.db | ||||
| chmod 700 /path/to/trilium/data/ | ||||
| chown trilium:trilium /path/to/trilium/data/ -R | ||||
| </code></pre> | ||||
|  | ||||
| <h2>Application Security</h2> | ||||
|  | ||||
| <h3>Configuration Hardening</h3> | ||||
|  | ||||
| <h4>Security Headers</h4> | ||||
|  | ||||
| <p>Configure security headers for web protection:</p> | ||||
|  | ||||
| <pre><code class="language-typescript">// Security headers configuration | ||||
| app.use((req, res, next) => { | ||||
|     res.setHeader('X-Frame-Options', 'DENY'); | ||||
|     res.setHeader('X-Content-Type-Options', 'nosniff'); | ||||
|     res.setHeader('X-XSS-Protection', '1; mode=block'); | ||||
|     res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); | ||||
|     res.setHeader('Content-Security-Policy',  | ||||
|         "default-src 'self'; " + | ||||
|         "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " + | ||||
|         "style-src 'self' 'unsafe-inline';" | ||||
|     ); | ||||
|     next(); | ||||
| }); | ||||
| </code></pre> | ||||
|  | ||||
| <h4>Session Security</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Session Timeout</strong>: Configure appropriate timeout values</li> | ||||
| <li><strong>Secure Cookies</strong>: Enable secure flag for all cookies</li> | ||||
| <li><strong>Session Regeneration</strong>: Regenerate session IDs after login</li> | ||||
| <li><strong>CSRF Protection</strong>: Enable and properly configure CSRF protection</li> | ||||
| </ol> | ||||
|  | ||||
| <p>Example session configuration:</p> | ||||
| <pre><code class="language-javascript">// Secure session configuration | ||||
| { | ||||
|     cookie: { | ||||
|         secure: true,         // HTTPS only | ||||
|         httpOnly: true,       // Prevent XSS | ||||
|         maxAge: 30 * 60 * 1000, // 30 minutes | ||||
|         sameSite: 'strict'    // CSRF protection | ||||
|     }, | ||||
|     rolling: true,            // Reset timeout on activity | ||||
|     resave: false,           // Don't save unchanged sessions | ||||
|     saveUninitialized: false // Don't save empty sessions | ||||
| } | ||||
| </code></pre> | ||||
|  | ||||
| <h3>Input Validation</h3> | ||||
|  | ||||
| <h4>Content Security</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>HTML Sanitization</strong>: Properly sanitize user-generated content</li> | ||||
| <li><strong>File Upload Security</strong>: Validate file types and sizes</li> | ||||
| <li><strong>Script Execution</strong>: Control custom script execution</li> | ||||
| <li><strong>SQL Injection Prevention</strong>: Use parameterized queries</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>API Security</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Rate Limiting</strong>: Implement API rate limiting</li> | ||||
| <li><strong>Input Validation</strong>: Validate all API inputs</li> | ||||
| <li><strong>Authentication</strong>: Require authentication for sensitive operations</li> | ||||
| <li><strong>Authorization</strong>: Implement proper access controls</li> | ||||
| </ol> | ||||
|  | ||||
| <p>Example rate limiting:</p> | ||||
| <pre><code class="language-typescript">import rateLimit from 'express-rate-limit'; | ||||
|  | ||||
| const limiter = rateLimit({ | ||||
|     windowMs: 15 * 60 * 1000, // 15 minutes | ||||
|     max: 100, // limit each IP to 100 requests per windowMs | ||||
|     message: 'Too many requests, please try again later.' | ||||
| }); | ||||
|  | ||||
| app.use('/api/', limiter); | ||||
| </code></pre> | ||||
|  | ||||
| <h2>Operational Security</h2> | ||||
|  | ||||
| <h3>Monitoring and Logging</h3> | ||||
|  | ||||
| <h4>Security Logging</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Authentication Events</strong>: Log all login attempts and failures</li> | ||||
| <li><strong>Authorization Events</strong>: Track access to protected resources</li> | ||||
| <li><strong>Data Access</strong>: Monitor sensitive data access patterns</li> | ||||
| <li><strong>System Events</strong>: Log system-level security events</li> | ||||
| </ol> | ||||
|  | ||||
| <p>Example log monitoring:</p> | ||||
| <pre><code class="language-bash"># Monitor failed login attempts | ||||
| tail -f /var/log/trilium/security.log | grep "Failed login" | ||||
|  | ||||
| # Alert on multiple failures | ||||
| tail -f /var/log/trilium/security.log | awk '/Failed login/ {count++} count>=5 {print "Alert: Multiple failed logins"; count=0}' | ||||
| </code></pre> | ||||
|  | ||||
| <h4>Security Metrics</h4> | ||||
|  | ||||
| <p>Monitor key security metrics:</p> | ||||
| <ul> | ||||
| <li>Failed authentication attempts</li> | ||||
| <li>Session anomalies</li> | ||||
| <li>Unusual access patterns</li> | ||||
| <li>Data export activities</li> | ||||
| <li>Configuration changes</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Incident Response</h3> | ||||
|  | ||||
| <h4>Preparation</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Incident Response Plan</strong>: Develop and document procedures</li> | ||||
| <li><strong>Contact Lists</strong>: Maintain emergency contact information</li> | ||||
| <li><strong>Backup Procedures</strong>: Ensure rapid recovery capabilities</li> | ||||
| <li><strong>Communication Plans</strong>: Prepare user notification procedures</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Detection and Response</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Automated Monitoring</strong>: Implement automated threat detection</li> | ||||
| <li><strong>Alert Systems</strong>: Configure appropriate alerting thresholds</li> | ||||
| <li><strong>Response Procedures</strong>: Define step-by-step response actions</li> | ||||
| <li><strong>Forensic Preparation</strong>: Preserve evidence for analysis</li> | ||||
| </ol> | ||||
|  | ||||
| <p>Example incident response checklist:</p> | ||||
| <pre><code>□ Identify and isolate affected systems | ||||
| □ Preserve logs and evidence | ||||
| □ Assess scope and impact | ||||
| □ Notify relevant stakeholders | ||||
| □ Implement containment measures | ||||
| □ Begin recovery procedures | ||||
| □ Document lessons learned | ||||
| □ Update security controls | ||||
| </code></pre> | ||||
|  | ||||
| <h3>Regular Maintenance</h3> | ||||
|  | ||||
| <h4>Security Updates</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Application Updates</strong>: Keep Trilium updated to latest version</li> | ||||
| <li><strong>Dependency Updates</strong>: Regularly update dependencies</li> | ||||
| <li><strong>System Updates</strong>: Maintain OS and security patches</li> | ||||
| <li><strong>Certificate Renewal</strong>: Monitor and renew SSL certificates</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Security Audits</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Regular Reviews</strong>: Conduct periodic security assessments</li> | ||||
| <li><strong>Penetration Testing</strong>: Perform authorized security testing</li> | ||||
| <li><strong>Configuration Audits</strong>: Review security configurations</li> | ||||
| <li><strong>Access Reviews</strong>: Audit user access and permissions</li> | ||||
| </ol> | ||||
|  | ||||
| <p>Automated update checking:</p> | ||||
| <pre><code class="language-bash">#!/bin/bash | ||||
| # Check for Trilium updates | ||||
| CURRENT_VERSION=$(curl -s https://api.github.com/repos/TriliumNext/Trilium/releases/latest | grep tag_name | cut -d'"' -f4) | ||||
| INSTALLED_VERSION=$(grep version /opt/trilium/package.json | cut -d'"' -f4) | ||||
|  | ||||
| if [ "$CURRENT_VERSION" != "v$INSTALLED_VERSION" ]; then | ||||
|     echo "Update available: $CURRENT_VERSION (current: $INSTALLED_VERSION)" | ||||
|     # Add notification logic here | ||||
| fi | ||||
| </code></pre> | ||||
|  | ||||
| <h2>Threat Mitigation</h2> | ||||
|  | ||||
| <h3>Common Attack Vectors</h3> | ||||
|  | ||||
| <h4>Web Application Attacks</h4> | ||||
|  | ||||
| <p><strong>Cross-Site Scripting (XSS)</strong>:</p> | ||||
| <ul> | ||||
| <li>Content Security Policy headers</li> | ||||
| <li>Input sanitization</li> | ||||
| <li>Output encoding</li> | ||||
| <li>Secure cookie flags</li> | ||||
| </ul> | ||||
|  | ||||
| <p><strong>Cross-Site Request Forgery (CSRF)</strong>:</p> | ||||
| <ul> | ||||
| <li>CSRF token validation</li> | ||||
| <li>SameSite cookie attributes</li> | ||||
| <li>Referrer validation</li> | ||||
| <li>Double-submit cookies</li> | ||||
| </ul> | ||||
|  | ||||
| <p><strong>Session Hijacking</strong>:</p> | ||||
| <ul> | ||||
| <li>Secure session management</li> | ||||
| <li>HTTPS enforcement</li> | ||||
| <li>Session timeout controls</li> | ||||
| <li>Session regeneration</li> | ||||
| </ul> | ||||
|  | ||||
| <h4>Infrastructure Attacks</h4> | ||||
|  | ||||
| <p><strong>Denial of Service (DoS)</strong>:</p> | ||||
| <ul> | ||||
| <li>Rate limiting</li> | ||||
| <li>Request size limits</li> | ||||
| <li>Connection throttling</li> | ||||
| <li>Resource monitoring</li> | ||||
| </ul> | ||||
|  | ||||
| <p><strong>Data Breaches</strong>:</p> | ||||
| <ul> | ||||
| <li>Encryption at rest</li> | ||||
| <li>Access controls</li> | ||||
| <li>Audit logging</li> | ||||
| <li>Regular backups</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Security Controls Implementation</h3> | ||||
|  | ||||
| <h4>Preventive Controls</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Authentication</strong>: Strong password policies and MFA</li> | ||||
| <li><strong>Authorization</strong>: Proper access controls and permissions</li> | ||||
| <li><strong>Encryption</strong>: Data encryption at rest and in transit</li> | ||||
| <li><strong>Input Validation</strong>: Comprehensive input sanitization</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Detective Controls</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Logging</strong>: Comprehensive security logging</li> | ||||
| <li><strong>Monitoring</strong>: Real-time security monitoring</li> | ||||
| <li><strong>Alerting</strong>: Automated threat detection</li> | ||||
| <li><strong>Auditing</strong>: Regular security audits</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>Responsive Controls</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Incident Response</strong>: Documented response procedures</li> | ||||
| <li><strong>Backup and Recovery</strong>: Reliable backup systems</li> | ||||
| <li><strong>Isolation</strong>: Network segmentation capabilities</li> | ||||
| <li><strong>Communication</strong>: Stakeholder notification systems</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Compliance Considerations</h2> | ||||
|  | ||||
| <h3>Data Protection Regulations</h3> | ||||
|  | ||||
| <h4>GDPR Compliance</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Data Minimization</strong>: Only collect necessary data</li> | ||||
| <li><strong>Consent Management</strong>: Obtain proper user consent</li> | ||||
| <li><strong>Right to Erasure</strong>: Implement data deletion capabilities</li> | ||||
| <li><strong>Data Portability</strong>: Enable data export functionality</li> | ||||
| <li><strong>Privacy by Design</strong>: Build privacy into system design</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>HIPAA Compliance (Healthcare)</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Access Controls</strong>: Implement user authentication and authorization</li> | ||||
| <li><strong>Audit Logs</strong>: Maintain comprehensive audit trails</li> | ||||
| <li><strong>Encryption</strong>: Encrypt data at rest and in transit</li> | ||||
| <li><strong>Risk Assessment</strong>: Conduct regular risk assessments</li> | ||||
| <li><strong>Business Associate Agreements</strong>: Ensure proper agreements</li> | ||||
| </ol> | ||||
|  | ||||
| <h3>Industry Standards</h3> | ||||
|  | ||||
| <h4>ISO 27001</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Information Security Management</strong>: Implement ISMS</li> | ||||
| <li><strong>Risk Management</strong>: Conduct regular risk assessments</li> | ||||
| <li><strong>Security Controls</strong>: Implement appropriate controls</li> | ||||
| <li><strong>Continuous Improvement</strong>: Regular reviews and updates</li> | ||||
| </ol> | ||||
|  | ||||
| <h4>SOC 2</h4> | ||||
|  | ||||
| <ol> | ||||
| <li><strong>Security</strong>: Implement comprehensive security controls</li> | ||||
| <li><strong>Availability</strong>: Ensure system availability and reliability</li> | ||||
| <li><strong>Processing Integrity</strong>: Maintain data processing integrity</li> | ||||
| <li><strong>Confidentiality</strong>: Protect sensitive information</li> | ||||
| <li><strong>Privacy</strong>: Implement privacy protection measures</li> | ||||
| </ol> | ||||
|  | ||||
| <h2>Security Assessment Checklist</h2> | ||||
|  | ||||
| <h3>Infrastructure Security</h3> | ||||
| <ul> | ||||
| <li>☐ HTTPS configured with valid certificates</li> | ||||
| <li>☐ Firewall rules properly configured</li> | ||||
| <li>☐ Network access controls implemented</li> | ||||
| <li>☐ System updates current</li> | ||||
| <li>☐ Backup procedures tested</li> | ||||
| <li>☐ Monitoring systems active</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Application Security</h3> | ||||
| <ul> | ||||
| <li>☐ Strong authentication configured</li> | ||||
| <li>☐ Multi-factor authentication enabled</li> | ||||
| <li>☐ Session security properly configured</li> | ||||
| <li>☐ CSRF protection enabled</li> | ||||
| <li>☐ Security headers configured</li> | ||||
| <li>☐ Input validation implemented</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Data Security</h3> | ||||
| <ul> | ||||
| <li>☐ Database properly secured</li> | ||||
| <li>☐ File permissions configured</li> | ||||
| <li>☐ Encryption properly implemented</li> | ||||
| <li>☐ Backup encryption verified</li> | ||||
| <li>☐ Access controls tested</li> | ||||
| <li>☐ Data retention policies defined</li> | ||||
| </ul> | ||||
|  | ||||
| <h3>Operational Security</h3> | ||||
| <ul> | ||||
| <li>☐ Security logging enabled</li> | ||||
| <li>☐ Monitoring systems configured</li> | ||||
| <li>☐ Incident response plan documented</li> | ||||
| <li>☐ Security training completed</li> | ||||
| <li>☐ Regular audits scheduled</li> | ||||
| <li>☐ Update procedures documented</li> | ||||
| </ul> | ||||
|  | ||||
| <p><strong>Remember</strong>: Security is an ongoing process, not a one-time configuration. Regularly review and update your security posture to address evolving threats and requirements.</p> | ||||
|  | ||||
| </div> | ||||
| @@ -134,6 +134,7 @@ docker run -d --name trilium -p 8080:8080 --user $(id -u):$(id -g) -v ~/trilium- | ||||
|   <li><code>TRILIUM_DATA_DIR</code>: Path to the data directory inside the container | ||||
|     (default: <code>/home/node/trilium-data</code>)</li> | ||||
| </ul> | ||||
| <p>For a complete list of configuration environment variables (network settings, authentication, sync, etc.), see <a class="reference-link" href="#root/_help_Gzjqa934BdH4">Configuration (config.ini or environment variables)</a>.</p> | ||||
| <h3>Volume Permissions</h3> | ||||
| <p>If you encounter permission issues with the data volume, ensure that:</p> | ||||
| <ol> | ||||
|   | ||||
| @@ -49,7 +49,12 @@ class="admonition warning"> | ||||
|       the <code>config.ini</code> file (check <a class="reference-link" href="#root/_help_Gzjqa934BdH4">Configuration (config.ini or environment variables)</a> for | ||||
|       more information). | ||||
|       <ol> | ||||
|         <li>You can also setup through environment variables (<code>TRILIUM_OAUTH_BASE_URL</code>, <code>TRILIUM_OAUTH_CLIENT_ID</code> and <code>TRILIUM_OAUTH_CLIENT_SECRET</code>).</li> | ||||
|         <li>You can also setup through environment variables: | ||||
|           <ul> | ||||
|             <li>Standard: <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code></li> | ||||
|             <li>Legacy (still supported): <code>TRILIUM_OAUTH_BASE_URL</code>, <code>TRILIUM_OAUTH_CLIENT_ID</code>, <code>TRILIUM_OAUTH_CLIENT_SECRET</code></li> | ||||
|           </ul> | ||||
|         </li> | ||||
|         <li><code>oauthBaseUrl</code> should be the link of your Trilium instance server, | ||||
|           for example, <code>https://<your-trilium-domain></code>.</li> | ||||
|       </ol> | ||||
| @@ -64,8 +69,12 @@ class="admonition warning"> | ||||
|     <p>The default OAuth issuer is Google. To use other services such as Authentik | ||||
|       or Auth0, you can configure the settings via <code>oauthIssuerBaseUrl</code>, <code>oauthIssuerName</code>, | ||||
|       and <code>oauthIssuerIcon</code> in the <code>config.ini</code> file. Alternatively, | ||||
|       these values can be set using environment variables: <code>TRILIUM_OAUTH_ISSUER_BASE_URL</code>, <code>TRILIUM_OAUTH_ISSUER_NAME</code>, | ||||
|       and <code>TRILIUM_OAUTH_ISSUER_ICON</code>. <code>oauthIssuerName</code> and <code>oauthIssuerIcon</code> are | ||||
|       these values can be set using environment variables: | ||||
|       <ul> | ||||
|         <li>Standard: <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code></li> | ||||
|         <li>Legacy (still supported): <code>TRILIUM_OAUTH_ISSUER_BASE_URL</code>, <code>TRILIUM_OAUTH_ISSUER_NAME</code>, <code>TRILIUM_OAUTH_ISSUER_ICON</code></li> | ||||
|       </ul> | ||||
|       <code>oauthIssuerName</code> and <code>oauthIssuerIcon</code> are | ||||
|       required for displaying correct issuer information at the Login page.</p> | ||||
|   </aside> | ||||
|   <h4>Authentik</h4> | ||||
|   | ||||
| @@ -26,7 +26,10 @@ https=true | ||||
| certPath=/[username]/.acme.sh/[hostname]/fullchain.cer | ||||
| keyPath=/[username]/.acme.sh/[hostname]/example.com.key</code></pre> | ||||
| <p>You can also review the <a href="#root/_help_Gzjqa934BdH4">configuration</a> file | ||||
|   to provide all <code>config.ini</code> values as environment variables instead.</p> | ||||
|   to provide all <code>config.ini</code> values as environment variables instead. For example, you can configure TLS using environment variables:</p> | ||||
| <pre><code class="language-bash">export TRILIUM_NETWORK_HTTPS=true | ||||
| export TRILIUM_NETWORK_CERTPATH=/path/to/cert.pem | ||||
| export TRILIUM_NETWORK_KEYPATH=/path/to/key.pem</code></pre> | ||||
| <p>The above example shows how this is set up in an environment where the | ||||
|   certificate was generated using Let's Encrypt's ACME utility. Your paths | ||||
|   may differ. For Docker installations, ensure these paths are within a volume | ||||
|   | ||||
| @@ -6,7 +6,7 @@ info: | ||||
|     contact: | ||||
|         name: zadam | ||||
|         email: zadam.apps@gmail.com | ||||
|         url: https://github.com/zadam/trilium | ||||
|         url: https://github.com/TriliumNext/Trilium | ||||
|     license: | ||||
|         name: Apache 2.0 | ||||
|         url: https://www.apache.org/licenses/LICENSE-2.0.html | ||||
|   | ||||
| @@ -50,7 +50,7 @@ function filterUrlValue(value: string) { | ||||
| } | ||||
|  | ||||
| function buildRewardMap(note: BNote) { | ||||
|     // Need to use Map instead of object: https://github.com/zadam/trilium/issues/1895 | ||||
|     // Need to use Map instead of object: https://github.com/TriliumNext/Trilium/issues/1895 | ||||
|     const map = new Map(); | ||||
|  | ||||
|     function addToRewardMap(text: string | undefined | null, rewardFactor: number) { | ||||
| @@ -188,7 +188,7 @@ function buildDateLimits(baseNote: BNote): DateLimits { | ||||
|     }; | ||||
| } | ||||
|  | ||||
| // Need to use Map instead of object: https://github.com/zadam/trilium/issues/1895 | ||||
| // Need to use Map instead of object: https://github.com/TriliumNext/Trilium/issues/1895 | ||||
| const wordCache = new Map(); | ||||
|  | ||||
| const WORD_BLACKLIST = [ | ||||
|   | ||||
| @@ -17,7 +17,7 @@ async function getBackendLog() { | ||||
|     } catch (e) { | ||||
|         const isErrorInstance = e instanceof Error; | ||||
|  | ||||
|         // most probably the log file does not exist yet - https://github.com/zadam/trilium/issues/1977 | ||||
|         // most probably the log file does not exist yet - https://github.com/TriliumNext/Trilium/issues/1977 | ||||
|         if (isErrorInstance && "code" in e && e.code === "ENOENT") { | ||||
|             log.error(e); | ||||
|             return t("backend_log.log-does-not-exist", { fileName }); | ||||
|   | ||||
| @@ -39,7 +39,7 @@ function processContent(content: Buffer | string | null, isProtected: boolean, i | ||||
|     if (isStringContent) { | ||||
|         return content === null ? "" : content.toString("utf-8"); | ||||
|     } else { | ||||
|         // see https://github.com/zadam/trilium/issues/3523 | ||||
|         // see https://github.com/TriliumNext/Trilium/issues/3523 | ||||
|         // IIRC a zero-sized buffer can be returned as null from the database | ||||
|         if (content === null) { | ||||
|             // this will force de/encryption | ||||
|   | ||||
| @@ -515,7 +515,7 @@ class ConsistencyChecks { | ||||
|         ); | ||||
|  | ||||
|         if (sqlInit.getDbSize() < 500000) { | ||||
|             // querying for "content IS NULL" is expensive since content is not indexed. See e.g. https://github.com/zadam/trilium/issues/2887 | ||||
|             // querying for "content IS NULL" is expensive since content is not indexed. See e.g. https://github.com/TriliumNext/Trilium/issues/2887 | ||||
|  | ||||
|             this.findAndFixIssues( | ||||
|                 ` | ||||
|   | ||||
| @@ -60,7 +60,7 @@ function decrypt(key: Buffer, cipherText: string | Buffer): Buffer | false | nul | ||||
|     try { | ||||
|         const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64"); | ||||
|  | ||||
|         // old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/trilium/issues/3017 | ||||
|         // old encrypted data can have IV of length 13, see some details here: https://github.com/TriliumNext/Trilium/issues/3017 | ||||
|         const ivLength = cipherTextBufferWithIv.length % 16 === 0 ? 16 : 13; | ||||
|  | ||||
|         const iv = cipherTextBufferWithIv.slice(0, ivLength); | ||||
| @@ -82,7 +82,7 @@ function decrypt(key: Buffer, cipherText: string | Buffer): Buffer | false | nul | ||||
|  | ||||
|         return payload; | ||||
|     } catch (e: any) { | ||||
|         // recovery from https://github.com/zadam/trilium/issues/510 | ||||
|         // recovery from https://github.com/TriliumNext/Trilium/issues/510 | ||||
|         if (e.message?.includes("WRONG_FINAL_BLOCK_LENGTH") || e.message?.includes("wrong final block length")) { | ||||
|             log.info("Caught WRONG_FINAL_BLOCK_LENGTH, returning cipherText instead"); | ||||
|  | ||||
|   | ||||
| @@ -33,7 +33,7 @@ function parseAuthToken(auth: string | undefined) { | ||||
|     if (auth.startsWith("Basic ")) { | ||||
|         // allow also basic auth format for systems which allow this type of authentication | ||||
|         // expect ETAPI token in the password field, require "etapi" username | ||||
|         // https://github.com/zadam/trilium/issues/3181 | ||||
|         // https://github.com/TriliumNext/Trilium/issues/3181 | ||||
|         const basicAuthStr = fromBase64(auth.substring(6)).toString("utf-8"); | ||||
|         const basicAuthChunks = basicAuthStr.split(":"); | ||||
|  | ||||
|   | ||||
| @@ -332,7 +332,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h | ||||
|                 const cssUrl = `${"../".repeat(noteMeta.notePath.length - 1)}style.css`; | ||||
|                 const htmlTitle = escapeHtml(title); | ||||
|  | ||||
|                 // <base> element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809 | ||||
|                 // <base> element will make sure external links are openable - https://github.com/TriliumNext/Trilium/issues/1289#issuecomment-704066809 | ||||
|                 content = `<html> | ||||
| <head> | ||||
|     <meta charset="utf-8"> | ||||
|   | ||||
| @@ -22,7 +22,7 @@ function sanitize(dirtyHtml: string) { | ||||
|         return dirtyHtml; | ||||
|     } | ||||
|  | ||||
|     // avoid H1 per https://github.com/zadam/trilium/issues/1552 | ||||
|     // avoid H1 per https://github.com/TriliumNext/Trilium/issues/1552 | ||||
|     // demote H1, and if that conflicts with existing H2, demote that, etc | ||||
|     const transformTags: Record<string, string> = {}; | ||||
|     const lowercasedHtml = dirtyHtml.toLowerCase(); | ||||
|   | ||||
| @@ -86,7 +86,7 @@ function saveImage(parentNoteId: string, uploadBuffer: Buffer, originalName: str | ||||
|     log.info(`Saving image ${originalName} into parent ${parentNoteId}`); | ||||
|  | ||||
|     if (trimFilename && originalName.length > 40) { | ||||
|         // https://github.com/zadam/trilium/issues/2307 | ||||
|         // https://github.com/TriliumNext/Trilium/issues/2307 | ||||
|         originalName = "image"; | ||||
|     } | ||||
|  | ||||
| @@ -135,7 +135,7 @@ function saveImageToAttachment(noteId: string, uploadBuffer: Buffer, originalNam | ||||
|     log.info(`Saving image '${originalName}' as attachment into note '${noteId}'`); | ||||
|  | ||||
|     if (trimFilename && originalName.length > 40) { | ||||
|         // https://github.com/zadam/trilium/issues/2307 | ||||
|         // https://github.com/TriliumNext/Trilium/issues/2307 | ||||
|         originalName = "image"; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -182,7 +182,7 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr | ||||
|             if (currentTag === "data") { | ||||
|                 text = text.replace(/\s/g, ""); | ||||
|  | ||||
|                 // resource can be chunked into multiple events: https://github.com/zadam/trilium/issues/3424 | ||||
|                 // resource can be chunked into multiple events: https://github.com/TriliumNext/Trilium/issues/3424 | ||||
|                 // it would probably make sense to do this in a more global way since it can in theory affect any field, | ||||
|                 // not just data | ||||
|                 resource.content = (resource.content || "") + text; | ||||
|   | ||||
| @@ -53,7 +53,7 @@ async function importOpml(taskContext: TaskContext, fileBuffer: string | Buffer, | ||||
|             content = toHtml(outline.$.text); | ||||
|  | ||||
|             if (!title || !title.trim()) { | ||||
|                 // https://github.com/zadam/trilium/issues/1862 | ||||
|                 // https://github.com/TriliumNext/Trilium/issues/1862 | ||||
|                 title = outline.$.text; | ||||
|                 content = ""; | ||||
|             } | ||||
|   | ||||
| @@ -477,7 +477,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo | ||||
|  | ||||
|         if (note) { | ||||
|             // only skeleton was created because of altered order of cloned notes in ZIP, we need to update | ||||
|             // https://github.com/zadam/trilium/issues/2440 | ||||
|             // https://github.com/TriliumNext/Trilium/issues/2440 | ||||
|             if (note.type === undefined) { | ||||
|                 note.type = type; | ||||
|                 note.mime = mime; | ||||
|   | ||||
| @@ -19,7 +19,7 @@ function getDefaultKeyboardActions() { | ||||
|             actionName: "backInNoteHistory", | ||||
|             friendlyName: t("keyboard_action_names.back-in-note-history"), | ||||
|             iconClass: "bx bxs-chevron-left", | ||||
|             // Mac has a different history navigation shortcuts - https://github.com/zadam/trilium/issues/376 | ||||
|             // Mac has a different history navigation shortcuts - https://github.com/TriliumNext/Trilium/issues/376 | ||||
|             defaultShortcuts: isMac ? ["CommandOrControl+Left"] : ["Alt+Left"], | ||||
|             description: t("keyboard_actions.back-in-note-history"), | ||||
|             scope: "window" | ||||
| @@ -28,7 +28,7 @@ function getDefaultKeyboardActions() { | ||||
|             actionName: "forwardInNoteHistory", | ||||
|             friendlyName: t("keyboard_action_names.forward-in-note-history"), | ||||
|             iconClass: "bx bxs-chevron-right", | ||||
|             // Mac has a different history navigation shortcuts - https://github.com/zadam/trilium/issues/376 | ||||
|             // Mac has a different history navigation shortcuts - https://github.com/TriliumNext/Trilium/issues/376 | ||||
|             defaultShortcuts: isMac ? ["CommandOrControl+Right"] : ["Alt+Right"], | ||||
|             description: t("keyboard_actions.forward-in-note-history"), | ||||
|             scope: "window" | ||||
|   | ||||
| @@ -330,7 +330,7 @@ class NoteContentFulltextExp extends Expression { | ||||
|  | ||||
|  | ||||
|     stripTags(content: string) { | ||||
|         // we want to allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412 | ||||
|         // we want to allow link to preserve URLs: https://github.com/TriliumNext/Trilium/issues/2412 | ||||
|         // we want to insert space in place of block tags (because they imply text separation) | ||||
|         // but we don't want to insert text for typical formatting inline tags which can occur within one word | ||||
|         const linkTag = "a"; | ||||
|   | ||||
| @@ -185,7 +185,7 @@ async function pullChanges(syncContext: SyncContext) { | ||||
|             break; | ||||
|         } else { | ||||
|             try { | ||||
|                 // https://github.com/zadam/trilium/issues/4310 | ||||
|                 // https://github.com/TriliumNext/Trilium/issues/4310 | ||||
|                 const sizeInKb = Math.round(JSON.stringify(resp).length / 1024); | ||||
|  | ||||
|                 log.info( | ||||
|   | ||||
| @@ -101,7 +101,7 @@ Stack: ${message.stack}`); | ||||
|     }); | ||||
|  | ||||
|     webSocketServer.on("error", (error) => { | ||||
|         // https://github.com/zadam/trilium/issues/3374#issuecomment-1341053765 | ||||
|         // https://github.com/TriliumNext/Trilium/issues/3374#issuecomment-1341053765 | ||||
|         console.log(error); | ||||
|     }); | ||||
| } | ||||
|   | ||||
| @@ -67,7 +67,7 @@ export default async function startTriliumServer() { | ||||
|     // for perf. issues it's good to know the rough configuration | ||||
|     const cpuInfos = (await import("os")).cpus(); | ||||
|     if (cpuInfos && cpuInfos[0] !== undefined) { | ||||
|         // https://github.com/zadam/trilium/pull/3957 | ||||
|         // https://github.com/TriliumNext/Trilium/pull/3957 | ||||
|         const cpuModel = (cpuInfos[0].model || "").trimEnd(); | ||||
|         log.info(`CPU model: ${cpuModel}, logical cores: ${cpuInfos.length}, freq: ${cpuInfos[0].speed} Mhz`); | ||||
|     } | ||||
|   | ||||
| @@ -4,9 +4,9 @@ | ||||
|  | ||||
| **Trilium is in maintenance mode and Web Clipper is not likely to get new releases.** | ||||
|  | ||||
| Trilium Web Clipper is a web browser extension which allows user to clip text, screenshots, whole pages and short notes and save them directly to [Trilium Notes](https://github.com/zadam/trilium).  | ||||
| Trilium Web Clipper is a web browser extension which allows user to clip text, screenshots, whole pages and short notes and save them directly to [Trilium Notes](https://github.com/TriliumNext/Trilium).  | ||||
|  | ||||
| For more details, see the [wiki page](https://github.com/zadam/trilium/wiki/Web-clipper). | ||||
| For more details, see the [wiki page](https://github.com/TriliumNext/Trilium/wiki/Web-clipper). | ||||
|  | ||||
| ## Keyboard shortcuts | ||||
| Keyboard shortcuts are available for most functions:   | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|   "name": "Trilium Web Clipper (dev)", | ||||
|   "version": "1.0.1", | ||||
|   "description": "Save web clippings to Trilium Notes.", | ||||
|   "homepage_url": "https://github.com/zadam/trilium-web-clipper", | ||||
|   "homepage_url": "https://github.com/TriliumNext/Trilium-web-clipper", | ||||
|   "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", | ||||
|   "icons": { | ||||
|     "32": "icons/32.png", | ||||
|   | ||||
| @@ -98,7 +98,7 @@ async function saveLinkWithNote() { | ||||
| $("#save-button").on("click", saveLinkWithNote); | ||||
|  | ||||
| $("#show-help-button").on("click", () => { | ||||
|     window.open("https://github.com/zadam/trilium/wiki/Web-clipper", '_blank'); | ||||
|     window.open("https://github.com/TriliumNext/Trilium/wiki/Web-clipper", '_blank'); | ||||
| }); | ||||
|  | ||||
| function escapeHtml(string) { | ||||
|   | ||||
							
								
								
									
										505
									
								
								docs/Developer Guide/!!!meta.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										505
									
								
								docs/Developer Guide/!!!meta.json
									
									
									
									
										vendored
									
									
								
							| @@ -45,6 +45,144 @@ | ||||
|           "dataFileName": "Environment Setup.md", | ||||
|           "attachments": [] | ||||
|         }, | ||||
|         { | ||||
|           "isClone": false, | ||||
|           "noteId": "gkVL91NKqxnq", | ||||
|           "notePath": [ | ||||
|             "jdjRLhLV3TtI", | ||||
|             "gkVL91NKqxnq" | ||||
|           ], | ||||
|           "title": "Architecture", | ||||
|           "notePosition": 100, | ||||
|           "prefix": null, | ||||
|           "isExpanded": false, | ||||
|           "type": "book", | ||||
|           "mime": "text/html", | ||||
|           "attributes": [ | ||||
|             { | ||||
|               "type": "label", | ||||
|               "name": "iconClass", | ||||
|               "value": "bx bx-buildings", | ||||
|               "isInheritable": false, | ||||
|               "position": 10 | ||||
|             } | ||||
|           ], | ||||
|           "format": "markdown", | ||||
|           "dataFileName": "README.md", | ||||
|           "dirFileName": "Architecture", | ||||
|           "children": [ | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "PBaX6rKcl3vx", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "gkVL91NKqxnq", | ||||
|                 "hvzZeXSdAKwX" | ||||
|               ], | ||||
|               "title": "Three-Layer Cache System", | ||||
|               "notePosition": 10, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-layer" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "Three-Layer-Cache-System.md" | ||||
|             }, | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "aEwg5w3srvQ", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "gkVL91NKqxnq", | ||||
|                 "P36wxMnIOBCa" | ||||
|               ], | ||||
|               "title": "Entity System", | ||||
|               "notePosition": 20, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-data" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "Entity-System.md" | ||||
|             }, | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "X4qy757zccQ", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "gkVL91NKqxnq", | ||||
|                 "zx4VwLF0W1r" | ||||
|               ], | ||||
|               "title": "Widget-Based UI Architecture", | ||||
|               "notePosition": 30, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-widget" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "Widget-Based-UI-Architecture.md" | ||||
|             }, | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "7CHwgLFjjO3y", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "gkVL91NKqxnq", | ||||
|                 "vXyl84mD18rX" | ||||
|               ], | ||||
|               "title": "API Architecture", | ||||
|               "notePosition": 40, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-transfer" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "API-Architecture.md" | ||||
|             }, | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "5lB37KgMY4h", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "gkVL91NKqxnq", | ||||
|                 "sIzwYe8xw5f" | ||||
|               ], | ||||
|               "title": "Monorepo Structure", | ||||
|               "notePosition": 50, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-folder" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "Monorepo-Structure.md" | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         { | ||||
|           "isClone": false, | ||||
|           "noteId": "cxfTSHIUQtt2", | ||||
| @@ -178,6 +316,143 @@ | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         { | ||||
|           "isClone": false, | ||||
|           "noteId": "YMe5uSDrCX5E", | ||||
|           "notePath": [ | ||||
|             "jdjRLhLV3TtI", | ||||
|             "YMe5uSDrCX5E" | ||||
|           ], | ||||
|           "title": "API Documentation", | ||||
|           "notePosition": 200, | ||||
|           "prefix": null, | ||||
|           "isExpanded": false, | ||||
|           "type": "book", | ||||
|           "mime": "text/html", | ||||
|           "attributes": [ | ||||
|             { | ||||
|               "type": "label", | ||||
|               "name": "iconClass", | ||||
|               "value": "bx bx-code-alt", | ||||
|               "isInheritable": false, | ||||
|               "position": 10 | ||||
|             } | ||||
|           ], | ||||
|           "format": "markdown", | ||||
|           "dirFileName": "API Documentation", | ||||
|           "children": [ | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "SeeRTwU9vV2", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "YMe5uSDrCX5E", | ||||
|                 "Gm5WtDJZumeI" | ||||
|               ], | ||||
|               "title": "ETAPI Complete Guide", | ||||
|               "notePosition": 10, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-api" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "ETAPI Complete Guide.md" | ||||
|             }, | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "sKWoNAfP4JsF", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "YMe5uSDrCX5E", | ||||
|                 "4ZbLI9nBMOW0" | ||||
|               ], | ||||
|               "title": "Internal API Reference", | ||||
|               "notePosition": 20, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-link" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "Internal API Reference.md" | ||||
|             }, | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "YkVmL89UpvpV", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "YMe5uSDrCX5E", | ||||
|                 "MOTM71beQAmX" | ||||
|               ], | ||||
|               "title": "Script API Cookbook", | ||||
|               "notePosition": 30, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-book" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "Script API Cookbook.md" | ||||
|             }, | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "xBUMjiuEGjkl", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "YMe5uSDrCX5E", | ||||
|                 "K1UgOZuURB5N" | ||||
|               ], | ||||
|               "title": "API Client Libraries", | ||||
|               "notePosition": 40, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-library" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "API Client Libraries.md" | ||||
|             }, | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "1zrZof0in", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "YMe5uSDrCX5E", | ||||
|                 "6mW7aog55js" | ||||
|               ], | ||||
|               "title": "WebSocket API", | ||||
|               "notePosition": 50, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-broadcast" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "WebSocket API.md" | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         { | ||||
|           "isClone": false, | ||||
|           "noteId": "wbVIolLKDhe2", | ||||
| @@ -2860,6 +3135,236 @@ | ||||
|               ] | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         { | ||||
|           "isClone": false, | ||||
|           "noteId": "GMPutLgCRFQG", | ||||
|           "notePath": [ | ||||
|             "jdjRLhLV3TtI", | ||||
|             "GMPutLgCRFQG" | ||||
|           ], | ||||
|           "title": "Plugin Development", | ||||
|           "notePosition": 300, | ||||
|           "prefix": null, | ||||
|           "isExpanded": false, | ||||
|           "type": "book", | ||||
|           "mime": "text/html", | ||||
|           "attributes": [ | ||||
|             { | ||||
|               "type": "label", | ||||
|               "name": "iconClass", | ||||
|               "value": "bx bx-plug", | ||||
|               "isInheritable": false, | ||||
|               "position": 10 | ||||
|             } | ||||
|           ], | ||||
|           "format": "markdown", | ||||
|           "dirFileName": "Plugin Development", | ||||
|           "children": [ | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "4F9CLkmimfs", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "GMPutLgCRFQG", | ||||
|                 "8sYWCGobZrU" | ||||
|               ], | ||||
|               "title": "Custom Widget Development Guide", | ||||
|               "notePosition": 10, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-widget" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "Custom Widget Development Guide.md" | ||||
|             }, | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "59eSlFWdt8AI", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "GMPutLgCRFQG", | ||||
|                 "lM79kNxBFbo" | ||||
|               ], | ||||
|               "title": "Frontend Script Development", | ||||
|               "notePosition": 20, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-code" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "Frontend Script Development.md" | ||||
|             }, | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "Ytg6EE8R6k5", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "GMPutLgCRFQG", | ||||
|                 "nlInGherV5" | ||||
|               ], | ||||
|               "title": "Backend Script Development", | ||||
|               "notePosition": 30, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-server" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "Backend Script Development.md" | ||||
|             }, | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "XxhCTQsHintT", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "GMPutLgCRFQG", | ||||
|                 "XThlPHWxrTkI" | ||||
|               ], | ||||
|               "title": "Custom Note Type Development", | ||||
|               "notePosition": 40, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-note" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "Custom Note Type Development.md" | ||||
|             }, | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "JWXUvnJELypk", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "GMPutLgCRFQG", | ||||
|                 "AkZ822kMHYTf" | ||||
|               ], | ||||
|               "title": "Theme Development Guide", | ||||
|               "notePosition": 50, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-palette" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "Theme Development Guide.md" | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|         { | ||||
|           "isClone": false, | ||||
|           "noteId": "iOxdMT23GwP", | ||||
|           "notePath": [ | ||||
|             "jdjRLhLV3TtI", | ||||
|             "iOxdMT23GwP" | ||||
|           ], | ||||
|           "title": "Security", | ||||
|           "notePosition": 400, | ||||
|           "prefix": null, | ||||
|           "isExpanded": false, | ||||
|           "type": "book", | ||||
|           "mime": "text/html", | ||||
|           "attributes": [ | ||||
|             { | ||||
|               "type": "label", | ||||
|               "name": "iconClass", | ||||
|               "value": "bx bx-lock", | ||||
|               "isInheritable": false, | ||||
|               "position": 10 | ||||
|             } | ||||
|           ], | ||||
|           "format": "markdown", | ||||
|           "dirFileName": "Security", | ||||
|           "children": [ | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "ZYmEhzvLBRQW", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "iOxdMT23GwP", | ||||
|                 "LNdHcJJxbVc2" | ||||
|               ], | ||||
|               "title": "Security Architecture", | ||||
|               "notePosition": 10, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-shield" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "Security Architecture.md" | ||||
|             }, | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "GdnoDyIwEL8B", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "iOxdMT23GwP", | ||||
|                 "zrncAvm9gD5" | ||||
|               ], | ||||
|               "title": "Secure Development Guidelines", | ||||
|               "notePosition": 20, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-check-shield" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "Secure Development Guidelines.md" | ||||
|             }, | ||||
|             { | ||||
|               "isClone": false, | ||||
|               "noteId": "5IWiTW0EfzT", | ||||
|               "notePath": [ | ||||
|                 "jdjRLhLV3TtI", | ||||
|                 "iOxdMT23GwP", | ||||
|                 "iWhKQldlDLr" | ||||
|               ], | ||||
|               "title": "Security Testing Guide", | ||||
|               "notePosition": 30, | ||||
|               "type": "text", | ||||
|               "mime": "text/html", | ||||
|               "attributes": [ | ||||
|                 { | ||||
|                   "type": "label", | ||||
|                   "name": "iconClass", | ||||
|                   "value": "bx bx-test-tube" | ||||
|                 } | ||||
|               ], | ||||
|               "format": "markdown", | ||||
|               "dataFileName": "Security Testing Guide.md" | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   | ||||
							
								
								
									
										2275
									
								
								docs/Developer Guide/API Documentation/API Client Libraries.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2275
									
								
								docs/Developer Guide/API Documentation/API Client Libraries.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1898
									
								
								docs/Developer Guide/API Documentation/ETAPI Complete Guide.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1898
									
								
								docs/Developer Guide/API Documentation/ETAPI Complete Guide.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1867
									
								
								docs/Developer Guide/API Documentation/Internal API Reference.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1867
									
								
								docs/Developer Guide/API Documentation/Internal API Reference.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1842
									
								
								docs/Developer Guide/API Documentation/Script API Cookbook.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1842
									
								
								docs/Developer Guide/API Documentation/Script API Cookbook.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1792
									
								
								docs/Developer Guide/API Documentation/WebSocket API.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1792
									
								
								docs/Developer Guide/API Documentation/WebSocket API.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										899
									
								
								docs/Developer Guide/Architecture/API-Architecture.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										899
									
								
								docs/Developer Guide/Architecture/API-Architecture.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,899 @@ | ||||
| # API Architecture | ||||
|  | ||||
| Trilium provides multiple API layers for different use cases: Internal API for frontend-backend communication, ETAPI for external integrations, and WebSocket for real-time synchronization. This document details each API layer's design, usage, and best practices. | ||||
|  | ||||
| ## API Layers Overview | ||||
|  | ||||
| ```mermaid | ||||
| graph TB | ||||
|     subgraph "Client Applications" | ||||
|         WebApp[Web Application] | ||||
|         Desktop[Desktop App] | ||||
|         Mobile[Mobile App] | ||||
|         External[External Apps] | ||||
|         Scripts[User Scripts] | ||||
|     end | ||||
|      | ||||
|     subgraph "API Layers" | ||||
|         Internal[Internal API<br/>REST + WebSocket] | ||||
|         ETAPI[ETAPI<br/>External API] | ||||
|         WS[WebSocket<br/>Real-time Sync] | ||||
|     end | ||||
|      | ||||
|     subgraph "Backend Services" | ||||
|         Routes[Route Handlers] | ||||
|         Services[Business Logic] | ||||
|         Becca[Becca Cache] | ||||
|         DB[(Database)] | ||||
|     end | ||||
|      | ||||
|     WebApp --> Internal | ||||
|     Desktop --> Internal | ||||
|     Mobile --> Internal | ||||
|     External --> ETAPI | ||||
|     Scripts --> ETAPI | ||||
|      | ||||
|     Internal --> Routes | ||||
|     ETAPI --> Routes | ||||
|     WS --> Services | ||||
|      | ||||
|     Routes --> Services | ||||
|     Services --> Becca | ||||
|     Becca --> DB | ||||
|      | ||||
|     style Internal fill:#e3f2fd | ||||
|     style ETAPI fill:#fff3e0 | ||||
|     style WS fill:#f3e5f5 | ||||
| ``` | ||||
|  | ||||
| ## Internal API | ||||
|  | ||||
| **Location**: `/apps/server/src/routes/api/` | ||||
|  | ||||
| The Internal API handles communication between Trilium's frontend and backend, providing full access to application functionality. | ||||
|  | ||||
| ### Architecture | ||||
|  | ||||
| ```typescript | ||||
| // Route structure | ||||
| /api/ | ||||
| ├── notes.ts           // Note operations | ||||
| ├── branches.ts        // Branch management | ||||
| ├── attributes.ts      // Attribute operations | ||||
| ├── tree.ts           // Tree structure | ||||
| ├── search.ts         // Search functionality | ||||
| ├── sync.ts           // Synchronization | ||||
| ├── options.ts        // Configuration | ||||
| └── special.ts        // Special operations | ||||
| ``` | ||||
|  | ||||
| ### Request/Response Pattern | ||||
|  | ||||
| ```typescript | ||||
| // Typical API endpoint structure | ||||
| router.get('/notes/:noteId', (req, res) => { | ||||
|     const note = becca.getNote(req.params.noteId); | ||||
|      | ||||
|     if (!note) { | ||||
|         return res.status(404).json({  | ||||
|             error: 'Note not found'  | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     res.json(note.getPojo()); | ||||
| }); | ||||
|  | ||||
| router.put('/notes/:noteId', (req, res) => { | ||||
|     const note = becca.getNoteOrThrow(req.params.noteId); | ||||
|      | ||||
|     note.title = req.body.title; | ||||
|     note.content = req.body.content; | ||||
|     note.save(); | ||||
|      | ||||
|     res.json({ success: true }); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Key Endpoints | ||||
|  | ||||
| #### Note Operations | ||||
|  | ||||
| ```typescript | ||||
| // Get note with content | ||||
| GET /api/notes/:noteId | ||||
| Response: { | ||||
|     noteId: string, | ||||
|     title: string, | ||||
|     type: string, | ||||
|     content: string, | ||||
|     dateCreated: string, | ||||
|     dateModified: string | ||||
| } | ||||
|  | ||||
| // Update note | ||||
| PUT /api/notes/:noteId | ||||
| Body: { | ||||
|     title?: string, | ||||
|     content?: string, | ||||
|     type?: string, | ||||
|     mime?: string | ||||
| } | ||||
|  | ||||
| // Create note | ||||
| POST /api/notes/:parentNoteId/children | ||||
| Body: { | ||||
|     title: string, | ||||
|     type: string, | ||||
|     content?: string, | ||||
|     position?: number | ||||
| } | ||||
|  | ||||
| // Delete note | ||||
| DELETE /api/notes/:noteId | ||||
| ``` | ||||
|  | ||||
| #### Tree Operations | ||||
|  | ||||
| ```typescript | ||||
| // Get tree structure | ||||
| GET /api/tree | ||||
| Query: { | ||||
|     subTreeNoteId?: string, | ||||
|     includeAttributes?: boolean | ||||
| } | ||||
| Response: { | ||||
|     notes: FNoteRow[], | ||||
|     branches: FBranchRow[], | ||||
|     attributes: FAttributeRow[] | ||||
| } | ||||
|  | ||||
| // Move branch | ||||
| PUT /api/branches/:branchId/move | ||||
| Body: { | ||||
|     parentNoteId: string, | ||||
|     position: number | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Search Operations | ||||
|  | ||||
| ```typescript | ||||
| // Execute search | ||||
| GET /api/search | ||||
| Query: { | ||||
|     query: string, | ||||
|     fastSearch?: boolean, | ||||
|     includeArchivedNotes?: boolean, | ||||
|     ancestorNoteId?: string | ||||
| } | ||||
| Response: { | ||||
|     results: Array<{ | ||||
|         noteId: string, | ||||
|         title: string, | ||||
|         path: string, | ||||
|         score: number | ||||
|     }> | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Authentication & Security | ||||
|  | ||||
| ```typescript | ||||
| // CSRF protection | ||||
| app.use(csrfMiddleware); | ||||
|  | ||||
| // Session authentication | ||||
| router.use((req, res, next) => { | ||||
|     if (!req.session.loggedIn) { | ||||
|         return res.status(401).json({  | ||||
|             error: 'Not authenticated'  | ||||
|         }); | ||||
|     } | ||||
|     next(); | ||||
| }); | ||||
|  | ||||
| // Protected note access | ||||
| router.get('/notes/:noteId', (req, res) => { | ||||
|     const note = becca.getNote(req.params.noteId); | ||||
|      | ||||
|     if (note.isProtected && !protectedSessionService.isProtectedSessionAvailable()) { | ||||
|         return res.status(403).json({  | ||||
|             error: 'Protected session required'  | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     res.json(note.getPojo()); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ## ETAPI (External API) | ||||
|  | ||||
| **Location**: `/apps/server/src/etapi/` | ||||
|  | ||||
| ETAPI provides a stable, versioned API for external applications and scripts to interact with Trilium. | ||||
|  | ||||
| ### Architecture | ||||
|  | ||||
| ```typescript | ||||
| // ETAPI structure | ||||
| /etapi/ | ||||
| ├── etapi.openapi.yaml    // OpenAPI specification | ||||
| ├── auth.ts               // Authentication | ||||
| ├── notes.ts              // Note endpoints | ||||
| ├── branches.ts           // Branch endpoints | ||||
| ├── attributes.ts         // Attribute endpoints | ||||
| ├── attachments.ts        // Attachment endpoints | ||||
| └── special_notes.ts      // Special note operations | ||||
| ``` | ||||
|  | ||||
| ### Authentication | ||||
|  | ||||
| ETAPI uses token-based authentication: | ||||
|  | ||||
| ```typescript | ||||
| // Creating ETAPI token | ||||
| POST /etapi/auth/login | ||||
| Body: { | ||||
|     username: string, | ||||
|     password: string | ||||
| } | ||||
| Response: { | ||||
|     authToken: string | ||||
| } | ||||
|  | ||||
| // Using token in requests | ||||
| GET /etapi/notes/:noteId | ||||
| Headers: { | ||||
|     Authorization: "authToken" | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Key Endpoints | ||||
|  | ||||
| #### Note CRUD Operations | ||||
|  | ||||
| ```typescript | ||||
| // Create note | ||||
| POST /etapi/notes | ||||
| Body: { | ||||
|     noteId?: string, | ||||
|     parentNoteId: string, | ||||
|     title: string, | ||||
|     type: string, | ||||
|     content?: string, | ||||
|     position?: number | ||||
| } | ||||
|  | ||||
| // Get note | ||||
| GET /etapi/notes/:noteId | ||||
| Response: { | ||||
|     noteId: string, | ||||
|     title: string, | ||||
|     type: string, | ||||
|     mime: string, | ||||
|     isProtected: boolean, | ||||
|     attributes: Array<{ | ||||
|         attributeId: string, | ||||
|         type: string, | ||||
|         name: string, | ||||
|         value: string | ||||
|     }>, | ||||
|     parentNoteIds: string[], | ||||
|     childNoteIds: string[], | ||||
|     dateCreated: string, | ||||
|     dateModified: string | ||||
| } | ||||
|  | ||||
| // Update note content | ||||
| PUT /etapi/notes/:noteId/content | ||||
| Body: string | Buffer | ||||
| Headers: { | ||||
|     "Content-Type": mime-type | ||||
| } | ||||
|  | ||||
| // Delete note | ||||
| DELETE /etapi/notes/:noteId | ||||
| ``` | ||||
|  | ||||
| #### Attribute Management | ||||
|  | ||||
| ```typescript | ||||
| // Create attribute | ||||
| POST /etapi/attributes | ||||
| Body: { | ||||
|     noteId: string, | ||||
|     type: 'label' | 'relation', | ||||
|     name: string, | ||||
|     value: string, | ||||
|     isInheritable?: boolean | ||||
| } | ||||
|  | ||||
| // Update attribute | ||||
| PATCH /etapi/attributes/:attributeId | ||||
| Body: { | ||||
|     value?: string, | ||||
|     isInheritable?: boolean | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Search | ||||
|  | ||||
| ```typescript | ||||
| // Search notes | ||||
| GET /etapi/notes/search | ||||
| Query: { | ||||
|     search: string, | ||||
|     limit?: number, | ||||
|     orderBy?: string, | ||||
|     orderDirection?: 'asc' | 'desc' | ||||
| } | ||||
| Response: { | ||||
|     results: Array<{ | ||||
|         noteId: string, | ||||
|         title: string, | ||||
|         // Other note properties | ||||
|     }> | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Client Libraries | ||||
|  | ||||
| ```javascript | ||||
| // JavaScript client example | ||||
| class EtapiClient { | ||||
|     constructor(serverUrl, authToken) { | ||||
|         this.serverUrl = serverUrl; | ||||
|         this.authToken = authToken; | ||||
|     } | ||||
|      | ||||
|     async getNote(noteId) { | ||||
|         const response = await fetch( | ||||
|             `${this.serverUrl}/etapi/notes/${noteId}`, | ||||
|             { | ||||
|                 headers: { | ||||
|                     'Authorization': this.authToken | ||||
|                 } | ||||
|             } | ||||
|         ); | ||||
|         return response.json(); | ||||
|     } | ||||
|      | ||||
|     async createNote(parentNoteId, title, content) { | ||||
|         const response = await fetch( | ||||
|             `${this.serverUrl}/etapi/notes`, | ||||
|             { | ||||
|                 method: 'POST', | ||||
|                 headers: { | ||||
|                     'Authorization': this.authToken, | ||||
|                     'Content-Type': 'application/json' | ||||
|                 }, | ||||
|                 body: JSON.stringify({ | ||||
|                     parentNoteId, | ||||
|                     title, | ||||
|                     type: 'text', | ||||
|                     content | ||||
|                 }) | ||||
|             } | ||||
|         ); | ||||
|         return response.json(); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Python Client Example | ||||
|  | ||||
| ```python | ||||
| import requests | ||||
|  | ||||
| class TriliumETAPI: | ||||
|     def __init__(self, server_url, auth_token): | ||||
|         self.server_url = server_url | ||||
|         self.auth_token = auth_token | ||||
|         self.headers = {'Authorization': auth_token} | ||||
|      | ||||
|     def get_note(self, note_id): | ||||
|         response = requests.get( | ||||
|             f"{self.server_url}/etapi/notes/{note_id}", | ||||
|             headers=self.headers | ||||
|         ) | ||||
|         return response.json() | ||||
|      | ||||
|     def create_note(self, parent_note_id, title, content=""): | ||||
|         response = requests.post( | ||||
|             f"{self.server_url}/etapi/notes", | ||||
|             headers=self.headers, | ||||
|             json={ | ||||
|                 'parentNoteId': parent_note_id, | ||||
|                 'title': title, | ||||
|                 'type': 'text', | ||||
|                 'content': content | ||||
|             } | ||||
|         ) | ||||
|         return response.json() | ||||
|      | ||||
|     def search_notes(self, query): | ||||
|         response = requests.get( | ||||
|             f"{self.server_url}/etapi/notes/search", | ||||
|             headers=self.headers, | ||||
|             params={'search': query} | ||||
|         ) | ||||
|         return response.json() | ||||
| ``` | ||||
|  | ||||
| ## WebSocket Real-time Synchronization | ||||
|  | ||||
| **Location**: `/apps/server/src/services/ws.ts` | ||||
|  | ||||
| WebSocket connections provide real-time updates and synchronization between clients. | ||||
|  | ||||
| ### Architecture | ||||
|  | ||||
| ```typescript | ||||
| // WebSocket message types | ||||
| interface WSMessage { | ||||
|     type: string; | ||||
|     data: any; | ||||
| } | ||||
|  | ||||
| // Common message types | ||||
| type MessageType =  | ||||
|     | 'entity-changes'      // Entity updates | ||||
|     | 'sync'               // Sync events | ||||
|     | 'note-content-change' // Content updates | ||||
|     | 'refresh-tree'       // Tree structure changes | ||||
|     | 'options-changed'    // Configuration updates | ||||
| ``` | ||||
|  | ||||
| ### Connection Management | ||||
|  | ||||
| ```typescript | ||||
| // Client connection | ||||
| const ws = new WebSocket('wss://server/ws'); | ||||
|  | ||||
| ws.on('open', () => { | ||||
|     // Authenticate | ||||
|     ws.send(JSON.stringify({ | ||||
|         type: 'auth', | ||||
|         token: sessionToken | ||||
|     })); | ||||
| }); | ||||
|  | ||||
| ws.on('message', (data) => { | ||||
|     const message = JSON.parse(data); | ||||
|     handleWSMessage(message); | ||||
| }); | ||||
|  | ||||
| // Server-side handling | ||||
| import WebSocket from 'ws'; | ||||
|  | ||||
| const wss = new WebSocket.Server({ server }); | ||||
|  | ||||
| wss.on('connection', (ws, req) => { | ||||
|     const session = parseSession(req); | ||||
|      | ||||
|     if (!session.authenticated) { | ||||
|         ws.close(1008, 'Not authenticated'); | ||||
|         return; | ||||
|     } | ||||
|      | ||||
|     clients.add(ws); | ||||
|      | ||||
|     ws.on('message', (message) => { | ||||
|         handleClientMessage(ws, message); | ||||
|     }); | ||||
|      | ||||
|     ws.on('close', () => { | ||||
|         clients.delete(ws); | ||||
|     }); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Message Broadcasting | ||||
|  | ||||
| ```typescript | ||||
| // Broadcast entity changes | ||||
| function broadcastEntityChanges(changes: EntityChange[]) { | ||||
|     const message = { | ||||
|         type: 'entity-changes', | ||||
|         data: changes | ||||
|     }; | ||||
|      | ||||
|     for (const client of clients) { | ||||
|         if (client.readyState === WebSocket.OPEN) { | ||||
|             client.send(JSON.stringify(message)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Targeted messages | ||||
| function sendToClient(clientId: string, message: WSMessage) { | ||||
|     const client = clients.get(clientId); | ||||
|     if (client?.readyState === WebSocket.OPEN) { | ||||
|         client.send(JSON.stringify(message)); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Real-time Sync Protocol | ||||
|  | ||||
| ```typescript | ||||
| // Entity change notification | ||||
| { | ||||
|     type: 'entity-changes', | ||||
|     data: [ | ||||
|         { | ||||
|             entityName: 'notes', | ||||
|             entityId: 'noteId123', | ||||
|             action: 'update', | ||||
|             entity: { /* note data */ } | ||||
|         } | ||||
|     ] | ||||
| } | ||||
|  | ||||
| // Sync pull request | ||||
| { | ||||
|     type: 'sync-pull', | ||||
|     data: { | ||||
|         lastSyncId: 12345 | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Sync push | ||||
| { | ||||
|     type: 'sync-push', | ||||
|     data: { | ||||
|         entities: [ /* changed entities */ ] | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Client-side Handling | ||||
|  | ||||
| ```typescript | ||||
| // Froca WebSocket integration | ||||
| class WSClient { | ||||
|     constructor() { | ||||
|         this.ws = null; | ||||
|         this.reconnectTimeout = null; | ||||
|         this.connect(); | ||||
|     } | ||||
|      | ||||
|     connect() { | ||||
|         this.ws = new WebSocket(this.getWSUrl()); | ||||
|          | ||||
|         this.ws.onmessage = (event) => { | ||||
|             const message = JSON.parse(event.data); | ||||
|             this.handleMessage(message); | ||||
|         }; | ||||
|          | ||||
|         this.ws.onclose = () => { | ||||
|             // Reconnect with exponential backoff | ||||
|             this.scheduleReconnect(); | ||||
|         }; | ||||
|     } | ||||
|      | ||||
|     handleMessage(message: WSMessage) { | ||||
|         switch (message.type) { | ||||
|             case 'entity-changes': | ||||
|                 this.handleEntityChanges(message.data); | ||||
|                 break; | ||||
|             case 'refresh-tree': | ||||
|                 froca.loadInitialTree(); | ||||
|                 break; | ||||
|             case 'note-content-change': | ||||
|                 this.handleContentChange(message.data); | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     handleEntityChanges(changes: EntityChange[]) { | ||||
|         for (const change of changes) { | ||||
|             if (change.entityName === 'notes') { | ||||
|                 froca.reloadNotes([change.entityId]); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## API Security | ||||
|  | ||||
| ### Authentication Methods | ||||
|  | ||||
| ```typescript | ||||
| // 1. Session-based (Internal API) | ||||
| app.use(session({ | ||||
|     secret: config.sessionSecret, | ||||
|     resave: false, | ||||
|     saveUninitialized: false | ||||
| })); | ||||
|  | ||||
| // 2. Token-based (ETAPI) | ||||
| router.use('/etapi', (req, res, next) => { | ||||
|     const token = req.headers.authorization; | ||||
|      | ||||
|     const etapiToken = becca.getEtapiToken(token); | ||||
|     if (!etapiToken || etapiToken.isExpired()) { | ||||
|         return res.status(401).json({  | ||||
|             error: 'Invalid or expired token'  | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     req.etapiToken = etapiToken; | ||||
|     next(); | ||||
| }); | ||||
|  | ||||
| // 3. WebSocket authentication | ||||
| ws.on('connection', (socket) => { | ||||
|     socket.on('auth', (token) => { | ||||
|         if (!validateToken(token)) { | ||||
|             socket.close(1008, 'Invalid token'); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Rate Limiting | ||||
|  | ||||
| ```typescript | ||||
| import rateLimit from 'express-rate-limit'; | ||||
|  | ||||
| // Global rate limit | ||||
| const globalLimiter = rateLimit({ | ||||
|     windowMs: 15 * 60 * 1000, // 15 minutes | ||||
|     max: 1000 // limit each IP to 1000 requests per windowMs | ||||
| }); | ||||
|  | ||||
| // Strict limit for authentication | ||||
| const authLimiter = rateLimit({ | ||||
|     windowMs: 15 * 60 * 1000, | ||||
|     max: 5, | ||||
|     message: 'Too many authentication attempts' | ||||
| }); | ||||
|  | ||||
| app.use('/api', globalLimiter); | ||||
| app.use('/api/auth', authLimiter); | ||||
| ``` | ||||
|  | ||||
| ### Input Validation | ||||
|  | ||||
| ```typescript | ||||
| import { body, validationResult } from 'express-validator'; | ||||
|  | ||||
| router.post('/api/notes', | ||||
|     body('title').isString().isLength({ min: 1, max: 1000 }), | ||||
|     body('type').isIn(['text', 'code', 'file', 'image']), | ||||
|     body('content').optional().isString(), | ||||
|     (req, res) => { | ||||
|         const errors = validationResult(req); | ||||
|         if (!errors.isEmpty()) { | ||||
|             return res.status(400).json({  | ||||
|                 errors: errors.array()  | ||||
|             }); | ||||
|         } | ||||
|          | ||||
|         // Process valid input | ||||
|     } | ||||
| ); | ||||
| ``` | ||||
|  | ||||
| ## Performance Optimization | ||||
|  | ||||
| ### Caching Strategies | ||||
|  | ||||
| ```typescript | ||||
| // Response caching | ||||
| const cache = new Map(); | ||||
|  | ||||
| router.get('/api/notes/:noteId', (req, res) => { | ||||
|     const cacheKey = `note:${req.params.noteId}`; | ||||
|     const cached = cache.get(cacheKey); | ||||
|      | ||||
|     if (cached && cached.expires > Date.now()) { | ||||
|         return res.json(cached.data); | ||||
|     } | ||||
|      | ||||
|     const note = becca.getNote(req.params.noteId); | ||||
|     const data = note.getPojo(); | ||||
|      | ||||
|     cache.set(cacheKey, { | ||||
|         data, | ||||
|         expires: Date.now() + 60000 // 1 minute | ||||
|     }); | ||||
|      | ||||
|     res.json(data); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Batch Operations | ||||
|  | ||||
| ```typescript | ||||
| // Batch API endpoint | ||||
| router.post('/api/batch', async (req, res) => { | ||||
|     const operations = req.body.operations; | ||||
|     const results = []; | ||||
|      | ||||
|     await sql.transactional(async () => { | ||||
|         for (const op of operations) { | ||||
|             const result = await executeOperation(op); | ||||
|             results.push(result); | ||||
|         } | ||||
|     }); | ||||
|      | ||||
|     res.json({ results }); | ||||
| }); | ||||
|  | ||||
| // Client batch usage | ||||
| const batch = [ | ||||
|     { method: 'PUT', path: '/notes/1', body: { title: 'Note 1' }}, | ||||
|     { method: 'PUT', path: '/notes/2', body: { title: 'Note 2' }}, | ||||
|     { method: 'POST', path: '/notes/3/attributes', body: { type: 'label', name: 'todo' }} | ||||
| ]; | ||||
|  | ||||
| await api.post('/batch', { operations: batch }); | ||||
| ``` | ||||
|  | ||||
| ### Streaming Responses | ||||
|  | ||||
| ```typescript | ||||
| // Stream large data | ||||
| router.get('/api/export', (req, res) => { | ||||
|     res.writeHead(200, { | ||||
|         'Content-Type': 'application/x-ndjson', | ||||
|         'Transfer-Encoding': 'chunked' | ||||
|     }); | ||||
|      | ||||
|     const noteStream = createNoteExportStream(); | ||||
|      | ||||
|     noteStream.on('data', (note) => { | ||||
|         res.write(JSON.stringify(note) + '\n'); | ||||
|     }); | ||||
|      | ||||
|     noteStream.on('end', () => { | ||||
|         res.end(); | ||||
|     }); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ## Error Handling | ||||
|  | ||||
| ### Standard Error Responses | ||||
|  | ||||
| ```typescript | ||||
| // Error response format | ||||
| interface ErrorResponse { | ||||
|     error: string; | ||||
|     code?: string; | ||||
|     details?: any; | ||||
| } | ||||
|  | ||||
| // Error handling middleware | ||||
| app.use((err: Error, req: Request, res: Response, next: NextFunction) => { | ||||
|     console.error('API Error:', err); | ||||
|      | ||||
|     if (err instanceof NotFoundError) { | ||||
|         return res.status(404).json({ | ||||
|             error: err.message, | ||||
|             code: 'NOT_FOUND' | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     if (err instanceof ValidationError) { | ||||
|         return res.status(400).json({ | ||||
|             error: err.message, | ||||
|             code: 'VALIDATION_ERROR', | ||||
|             details: err.details | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     // Generic error | ||||
|     res.status(500).json({ | ||||
|         error: 'Internal server error', | ||||
|         code: 'INTERNAL_ERROR' | ||||
|     }); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ## API Documentation | ||||
|  | ||||
| ### OpenAPI/Swagger | ||||
|  | ||||
| ```yaml | ||||
| # etapi.openapi.yaml | ||||
| openapi: 3.0.0 | ||||
| info: | ||||
|   title: Trilium ETAPI | ||||
|   version: 1.0.0 | ||||
|   description: External API for Trilium Notes | ||||
|  | ||||
| paths: | ||||
|   /etapi/notes/{noteId}: | ||||
|     get: | ||||
|       summary: Get note by ID | ||||
|       parameters: | ||||
|         - name: noteId | ||||
|           in: path | ||||
|           required: true | ||||
|           schema: | ||||
|             type: string | ||||
|       responses: | ||||
|         200: | ||||
|           description: Note found | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: '#/components/schemas/Note' | ||||
|         404: | ||||
|           description: Note not found | ||||
|  | ||||
| components: | ||||
|   schemas: | ||||
|     Note: | ||||
|       type: object | ||||
|       properties: | ||||
|         noteId: | ||||
|           type: string | ||||
|         title: | ||||
|           type: string | ||||
|         type: | ||||
|           type: string | ||||
|           enum: [text, code, file, image] | ||||
| ``` | ||||
|  | ||||
| ### API Testing | ||||
|  | ||||
| ```typescript | ||||
| // API test example | ||||
| describe('Notes API', () => { | ||||
|     it('should create a note', async () => { | ||||
|         const response = await request(app) | ||||
|             .post('/api/notes/root/children') | ||||
|             .send({ | ||||
|                 title: 'Test Note', | ||||
|                 type: 'text', | ||||
|                 content: 'Test content' | ||||
|             }) | ||||
|             .expect(200); | ||||
|              | ||||
|         expect(response.body).toHaveProperty('noteId'); | ||||
|         expect(response.body.title).toBe('Test Note'); | ||||
|     }); | ||||
|      | ||||
|     it('should handle errors', async () => { | ||||
|         const response = await request(app) | ||||
|             .get('/api/notes/invalid') | ||||
|             .expect(404); | ||||
|              | ||||
|         expect(response.body).toHaveProperty('error'); | ||||
|     }); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ## Best Practices | ||||
|  | ||||
| ### API Design | ||||
|  | ||||
| 1. **RESTful conventions**: Use appropriate HTTP methods and status codes | ||||
| 2. **Consistent naming**: Use camelCase for JSON properties | ||||
| 3. **Versioning**: Version the API to maintain compatibility | ||||
| 4. **Documentation**: Keep OpenAPI spec up to date | ||||
|  | ||||
| ### Security | ||||
|  | ||||
| 1. **Authentication**: Always verify user identity | ||||
| 2. **Authorization**: Check permissions for each operation | ||||
| 3. **Validation**: Validate all input data | ||||
| 4. **Rate limiting**: Prevent abuse with appropriate limits | ||||
|  | ||||
| ### Performance | ||||
|  | ||||
| 1. **Pagination**: Limit response sizes with pagination | ||||
| 2. **Caching**: Cache frequently accessed data | ||||
| 3. **Batch operations**: Support bulk operations | ||||
| 4. **Async processing**: Use queues for long-running tasks | ||||
|  | ||||
| ## Related Documentation | ||||
|  | ||||
| - [Three-Layer Cache System](Three-Layer-Cache-System.md) - Cache architecture | ||||
| - [Entity System](Entity-System.md) - Data model | ||||
| - [ETAPI Reference](/apps/server/src/etapi/etapi.openapi.yaml) - OpenAPI specification | ||||
							
								
								
									
										612
									
								
								docs/Developer Guide/Architecture/Entity-System.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										612
									
								
								docs/Developer Guide/Architecture/Entity-System.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,612 @@ | ||||
| # Entity System Architecture | ||||
|  | ||||
| The Entity System forms the core data model of Trilium Notes, providing a flexible and powerful structure for organizing information. This document details the entities, their relationships, and usage patterns. | ||||
|  | ||||
| ## Core Entities Overview | ||||
|  | ||||
| ```mermaid | ||||
| erDiagram | ||||
|     Note ||--o{ Branch : "parent-child" | ||||
|     Note ||--o{ Attribute : "has" | ||||
|     Note ||--o{ Revision : "history" | ||||
|     Note ||--o{ Attachment : "contains" | ||||
|     Attachment ||--|| Blob : "stores in" | ||||
|     Revision ||--|| Blob : "stores in" | ||||
|     Note }o--o{ Note : "relates via Attribute" | ||||
|      | ||||
|     Note { | ||||
|         string noteId PK | ||||
|         string title | ||||
|         string type | ||||
|         string content | ||||
|         boolean isProtected | ||||
|         string dateCreated | ||||
|         string dateModified | ||||
|     } | ||||
|      | ||||
|     Branch { | ||||
|         string branchId PK | ||||
|         string noteId FK | ||||
|         string parentNoteId FK | ||||
|         integer notePosition | ||||
|         string prefix | ||||
|         boolean isExpanded | ||||
|     } | ||||
|      | ||||
|     Attribute { | ||||
|         string attributeId PK | ||||
|         string noteId FK | ||||
|         string type "label or relation" | ||||
|         string name | ||||
|         string value | ||||
|         integer position | ||||
|         boolean isInheritable | ||||
|     } | ||||
|      | ||||
|     Revision { | ||||
|         string revisionId PK | ||||
|         string noteId FK | ||||
|         string title | ||||
|         string type | ||||
|         boolean isProtected | ||||
|         string dateCreated | ||||
|     } | ||||
|      | ||||
|     Attachment { | ||||
|         string attachmentId PK | ||||
|         string ownerId FK | ||||
|         string role | ||||
|         string mime | ||||
|         string title | ||||
|         string blobId FK | ||||
|     } | ||||
|      | ||||
|     Blob { | ||||
|         string blobId PK | ||||
|         binary content | ||||
|         string dateModified | ||||
|     } | ||||
|      | ||||
|     Option { | ||||
|         string name PK | ||||
|         string value | ||||
|         boolean isSynced | ||||
|     } | ||||
| ``` | ||||
|  | ||||
| ## Entity Definitions | ||||
|  | ||||
| ### BNote - Notes with Content and Metadata | ||||
|  | ||||
| **Location**: `/apps/server/src/becca/entities/bnote.ts` | ||||
|  | ||||
| Notes are the fundamental unit of information in Trilium. Each note can contain different types of content and maintain relationships with other notes. | ||||
|  | ||||
| #### Properties | ||||
|  | ||||
| ```typescript | ||||
| class BNote { | ||||
|     noteId: string;           // Unique identifier | ||||
|     title: string;            // Display title | ||||
|     type: string;             // Content type (text, code, file, etc.) | ||||
|     mime: string;             // MIME type for content | ||||
|     isProtected: boolean;     // Encryption flag | ||||
|     dateCreated: string;      // Creation timestamp | ||||
|     dateModified: string;     // Last modification | ||||
|     utcDateCreated: string;   // UTC creation | ||||
|     utcDateModified: string;  // UTC modification | ||||
|      | ||||
|     // Relationships | ||||
|     parentBranches: BBranch[];  // Parent connections | ||||
|     children: BBranch[];        // Child connections | ||||
|     attributes: BAttribute[];   // Metadata | ||||
|      | ||||
|     // Content | ||||
|     content?: string | Buffer;  // Note content (lazy loaded) | ||||
|      | ||||
|     // Computed | ||||
|     isDecrypted: boolean;       // Decryption status | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Note Types | ||||
|  | ||||
| - **text**: Rich text content with HTML formatting | ||||
| - **code**: Source code with syntax highlighting | ||||
| - **file**: Binary file attachment | ||||
| - **image**: Image with preview capabilities | ||||
| - **search**: Saved search query | ||||
| - **book**: Container for hierarchical documentation | ||||
| - **relationMap**: Visual relationship diagram | ||||
| - **canvas**: Drawing canvas (Excalidraw) | ||||
| - **mermaid**: Mermaid diagram | ||||
| - **mindMap**: Mind mapping visualization | ||||
| - **webView**: Embedded web content | ||||
| - **noteMap**: Tree visualization | ||||
|  | ||||
| #### Usage Examples | ||||
|  | ||||
| ```typescript | ||||
| // Create a new note | ||||
| const note = new BNote({ | ||||
|     noteId: generateNoteId(), | ||||
|     title: "My Note", | ||||
|     type: "text", | ||||
|     mime: "text/html", | ||||
|     content: "<p>Note content</p>" | ||||
| }); | ||||
| note.save(); | ||||
|  | ||||
| // Get note with content | ||||
| const note = becca.getNote(noteId); | ||||
| await note.loadContent(); | ||||
|  | ||||
| // Update note | ||||
| note.title = "Updated Title"; | ||||
| note.save(); | ||||
|  | ||||
| // Protect note | ||||
| note.isProtected = true; | ||||
| note.encrypt(); | ||||
| note.save(); | ||||
| ``` | ||||
|  | ||||
| ### BBranch - Hierarchical Relationships | ||||
|  | ||||
| **Location**: `/apps/server/src/becca/entities/bbranch.ts` | ||||
|  | ||||
| Branches define the parent-child relationships between notes, allowing a note to have multiple parents (cloning). | ||||
|  | ||||
| #### Properties | ||||
|  | ||||
| ```typescript | ||||
| class BBranch { | ||||
|     branchId: string;        // Unique identifier | ||||
|     noteId: string;          // Child note ID | ||||
|     parentNoteId: string;    // Parent note ID | ||||
|     notePosition: number;    // Order among siblings | ||||
|     prefix: string;          // Optional prefix label | ||||
|     isExpanded: boolean;     // Tree UI state | ||||
|      | ||||
|     // Computed | ||||
|     childNote: BNote;        // Reference to child | ||||
|     parentNote: BNote;       // Reference to parent | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Key Features | ||||
|  | ||||
| - **Multiple Parents**: Notes can appear in multiple locations | ||||
| - **Ordering**: Explicit positioning among siblings | ||||
| - **Prefixes**: Optional labels for context (e.g., "Chapter 1:") | ||||
| - **UI State**: Expansion state persisted per branch | ||||
|  | ||||
| #### Usage Examples | ||||
|  | ||||
| ```typescript | ||||
| // Create parent-child relationship | ||||
| const branch = new BBranch({ | ||||
|     noteId: childNote.noteId, | ||||
|     parentNoteId: parentNote.noteId, | ||||
|     notePosition: 10 | ||||
| }); | ||||
| branch.save(); | ||||
|  | ||||
| // Clone note to another parent | ||||
| const cloneBranch = childNote.cloneTo(otherParent.noteId); | ||||
|  | ||||
| // Reorder children | ||||
| parentNote.sortChildren((a, b) =>  | ||||
|     a.title.localeCompare(b.title) | ||||
| ); | ||||
|  | ||||
| // Add prefix | ||||
| branch.prefix = "Important: "; | ||||
| branch.save(); | ||||
| ``` | ||||
|  | ||||
| ### BAttribute - Key-Value Metadata | ||||
|  | ||||
| **Location**: `/apps/server/src/becca/entities/battribute.ts` | ||||
|  | ||||
| Attributes provide flexible metadata and relationships between notes. | ||||
|  | ||||
| #### Types | ||||
|  | ||||
| 1. **Labels**: Key-value pairs for metadata | ||||
| 2. **Relations**: References to other notes | ||||
|  | ||||
| #### Properties | ||||
|  | ||||
| ```typescript | ||||
| class BAttribute { | ||||
|     attributeId: string;     // Unique identifier | ||||
|     noteId: string;          // Owning note | ||||
|     type: 'label' | 'relation'; | ||||
|     name: string;            // Attribute name | ||||
|     value: string;           // Value or target noteId | ||||
|     position: number;        // Display order | ||||
|     isInheritable: boolean;  // Inherited by children | ||||
|      | ||||
|     // Computed | ||||
|     note: BNote;             // Owner note | ||||
|     targetNote?: BNote;      // For relations | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Common Patterns | ||||
|  | ||||
| ```typescript | ||||
| // Add label | ||||
| note.addLabel("status", "active"); | ||||
| note.addLabel("priority", "high"); | ||||
|  | ||||
| // Add relation | ||||
| note.addRelation("template", templateNoteId); | ||||
| note.addRelation("renderNote", renderNoteId); | ||||
|  | ||||
| // Query by attributes | ||||
| const todos = becca.findAttributes("label", "todoItem"); | ||||
| const templates = becca.findAttributes("label", "template"); | ||||
|  | ||||
| // Inheritable attributes | ||||
| note.addLabel("workspace", "project", true); // Children inherit | ||||
| ``` | ||||
|  | ||||
| #### System Attributes | ||||
|  | ||||
| Special attributes with system behavior: | ||||
|  | ||||
| - `#hidePromotedAttributes`: Hide promoted attributes in UI | ||||
| - `#readOnly`: Prevent note editing | ||||
| - `#autoReadOnlyDisabled`: Disable auto read-only | ||||
| - `#hideChildrenOverview`: Hide children count | ||||
| - `~template`: Note template relation | ||||
| - `~renderNote`: Custom rendering relation | ||||
|  | ||||
| ### BRevision - Version History | ||||
|  | ||||
| **Location**: `/apps/server/src/becca/entities/brevision.ts` | ||||
|  | ||||
| Revisions provide version history and recovery capabilities. | ||||
|  | ||||
| #### Properties | ||||
|  | ||||
| ```typescript | ||||
| class BRevision { | ||||
|     revisionId: string;      // Unique identifier | ||||
|     noteId: string;          // Parent note | ||||
|     type: string;            // Content type | ||||
|     mime: string;            // MIME type | ||||
|     title: string;           // Historical title | ||||
|     isProtected: boolean;    // Encryption flag | ||||
|     dateCreated: string;     // Creation time | ||||
|     utcDateCreated: string;  // UTC time | ||||
|     dateModified: string;    // Content modification | ||||
|     blobId: string;          // Content storage | ||||
|      | ||||
|     // Methods | ||||
|     getContent(): string | Buffer; | ||||
|     restore(): void; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Revision Strategy | ||||
|  | ||||
| - Created automatically on significant changes | ||||
| - Configurable retention period | ||||
| - Day/week/month/year retention rules | ||||
| - Protected note revisions are encrypted | ||||
|  | ||||
| #### Usage Examples | ||||
|  | ||||
| ```typescript | ||||
| // Get note revisions | ||||
| const revisions = note.getRevisions(); | ||||
|  | ||||
| // Restore revision | ||||
| const revision = becca.getRevision(revisionId); | ||||
| revision.restore(); | ||||
|  | ||||
| // Manual revision creation | ||||
| note.saveRevision(); | ||||
|  | ||||
| // Compare revisions | ||||
| const diff = revision1.getContent() !== revision2.getContent(); | ||||
| ``` | ||||
|  | ||||
| ### BOption - Application Configuration | ||||
|  | ||||
| **Location**: `/apps/server/src/becca/entities/boption.ts` | ||||
|  | ||||
| Options store application and user preferences. | ||||
|  | ||||
| #### Properties | ||||
|  | ||||
| ```typescript | ||||
| class BOption { | ||||
|     name: string;            // Option key | ||||
|     value: string;           // Option value | ||||
|     isSynced: boolean;       // Sync across instances | ||||
|     utcDateModified: string; // Last change | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Common Options | ||||
|  | ||||
| ```typescript | ||||
| // Theme settings | ||||
| setOption("theme", "dark"); | ||||
|  | ||||
| // Protected session timeout | ||||
| setOption("protectedSessionTimeout", "600"); | ||||
|  | ||||
| // Sync settings | ||||
| setOption("syncServerHost", "https://sync.server"); | ||||
|  | ||||
| // Note settings | ||||
| setOption("defaultNoteType", "text"); | ||||
| ``` | ||||
|  | ||||
| ### BAttachment - File Attachments | ||||
|  | ||||
| **Location**: `/apps/server/src/becca/entities/battachment.ts` | ||||
|  | ||||
| Attachments link binary content to notes. | ||||
|  | ||||
| #### Properties | ||||
|  | ||||
| ```typescript | ||||
| class BAttachment { | ||||
|     attachmentId: string;    // Unique identifier | ||||
|     ownerId: string;         // Parent note ID | ||||
|     role: string;            // Attachment role | ||||
|     mime: string;            // MIME type | ||||
|     title: string;           // Display title | ||||
|     blobId: string;          // Content reference | ||||
|     utcDateScheduledForDeletion: string; | ||||
|      | ||||
|     // Methods | ||||
|     getContent(): Buffer; | ||||
|     getBlob(): BBlob; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Usage Patterns | ||||
|  | ||||
| ```typescript | ||||
| // Add attachment to note | ||||
| const attachment = note.addAttachment({ | ||||
|     role: "file", | ||||
|     mime: "application/pdf", | ||||
|     title: "document.pdf", | ||||
|     content: buffer | ||||
| }); | ||||
|  | ||||
| // Get attachments | ||||
| const attachments = note.getAttachments(); | ||||
|  | ||||
| // Download attachment | ||||
| const content = attachment.getContent(); | ||||
| ``` | ||||
|  | ||||
| ## Entity Relationships | ||||
|  | ||||
| ### Parent-Child Hierarchy | ||||
|  | ||||
| ```typescript | ||||
| // Single parent | ||||
| childNote.setParent(parentNote.noteId); | ||||
|  | ||||
| // Multiple parents (cloning) | ||||
| childNote.cloneTo(parent1.noteId); | ||||
| childNote.cloneTo(parent2.noteId); | ||||
|  | ||||
| // Get parents | ||||
| const parents = childNote.getParentNotes(); | ||||
|  | ||||
| // Get children | ||||
| const children = parentNote.getChildNotes(); | ||||
|  | ||||
| // Get subtree | ||||
| const subtree = parentNote.getSubtreeNotes(); | ||||
| ``` | ||||
|  | ||||
| ### Attribute Relationships | ||||
|  | ||||
| ```typescript | ||||
| // Direct relations | ||||
| note.addRelation("author", authorNote.noteId); | ||||
|  | ||||
| // Bidirectional relations | ||||
| note1.addRelation("related", note2.noteId); | ||||
| note2.addRelation("related", note1.noteId); | ||||
|  | ||||
| // Get related notes | ||||
| const related = note.getRelations("related"); | ||||
|  | ||||
| // Get notes relating to this one | ||||
| const targetRelations = note.getTargetRelations(); | ||||
| ``` | ||||
|  | ||||
| ## Entity Lifecycle | ||||
|  | ||||
| ### Creation | ||||
|  | ||||
| ```typescript | ||||
| // Note creation | ||||
| const note = new BNote({ | ||||
|     noteId: generateNoteId(), | ||||
|     title: "New Note", | ||||
|     type: "text" | ||||
| }); | ||||
| note.save(); | ||||
|  | ||||
| // With parent | ||||
| const child = parentNote.addChild({ | ||||
|     title: "Child Note", | ||||
|     type: "text", | ||||
|     content: "Content" | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Updates | ||||
|  | ||||
| ```typescript | ||||
| // Atomic updates | ||||
| note.title = "New Title"; | ||||
| note.save(); | ||||
|  | ||||
| // Batch updates | ||||
| sql.transactional(() => { | ||||
|     note1.title = "Title 1"; | ||||
|     note1.save(); | ||||
|      | ||||
|     note2.content = "Content 2"; | ||||
|     note2.save(); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Deletion | ||||
|  | ||||
| ```typescript | ||||
| // Soft delete (move to trash) | ||||
| note.deleteNote(); | ||||
|  | ||||
| // Mark for deletion | ||||
| note.isDeleted = true; | ||||
| note.save(); | ||||
|  | ||||
| // Permanent deletion (after grace period) | ||||
| note.eraseNote(); | ||||
| ``` | ||||
|  | ||||
| ## Performance Considerations | ||||
|  | ||||
| ### Lazy Loading | ||||
|  | ||||
| ```typescript | ||||
| // Note content loaded on demand | ||||
| const note = becca.getNote(noteId); // Metadata only | ||||
| await note.loadContent(); // Load content when needed | ||||
|  | ||||
| // Revisions loaded on demand | ||||
| const revisions = note.getRevisions(); // Database query | ||||
| ``` | ||||
|  | ||||
| ### Batch Operations | ||||
|  | ||||
| ```typescript | ||||
| // Efficient bulk loading | ||||
| const notes = becca.getNotes(noteIds); | ||||
|  | ||||
| // Batch attribute queries | ||||
| const attributes = sql.getRows(` | ||||
|     SELECT * FROM attributes  | ||||
|     WHERE noteId IN (???)  | ||||
|     AND name = ? | ||||
| `, [noteIds, 'label']); | ||||
| ``` | ||||
|  | ||||
| ### Indexing | ||||
|  | ||||
| ```typescript | ||||
| // Attribute index for fast lookups | ||||
| const labels = becca.findAttributes("label", "important"); | ||||
|  | ||||
| // Branch index for relationship queries | ||||
| const branch = becca.getBranchFromChildAndParent(childId, parentId); | ||||
| ``` | ||||
|  | ||||
| ## Best Practices | ||||
|  | ||||
| ### Entity Creation | ||||
|  | ||||
| ```typescript | ||||
| // Always use transactions for multiple operations | ||||
| sql.transactional(() => { | ||||
|     const note = new BNote({...}); | ||||
|     note.save(); | ||||
|      | ||||
|     note.addLabel("status", "draft"); | ||||
|     note.addRelation("template", templateId); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ### Entity Updates | ||||
|  | ||||
| ```typescript | ||||
| // Check existence before update | ||||
| const note = becca.getNote(noteId); | ||||
| if (note) { | ||||
|     note.title = "Updated"; | ||||
|     note.save(); | ||||
| } | ||||
|  | ||||
| // Use proper error handling | ||||
| try { | ||||
|     const note = becca.getNoteOrThrow(noteId); | ||||
|     note.save(); | ||||
| } catch (e) { | ||||
|     log.error(`Note ${noteId} not found`); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Querying | ||||
|  | ||||
| ```typescript | ||||
| // Use indexed queries | ||||
| const attrs = becca.findAttributes("label", "task"); | ||||
|  | ||||
| // Avoid N+1 queries | ||||
| const noteIds = [...]; | ||||
| const notes = becca.getNotes(noteIds); // Single batch | ||||
|  | ||||
| // Use SQL for complex queries | ||||
| const results = sql.getRows(` | ||||
|     SELECT n.noteId, n.title, COUNT(b.branchId) as childCount | ||||
|     FROM notes n | ||||
|     LEFT JOIN branches b ON b.parentNoteId = n.noteId | ||||
|     GROUP BY n.noteId | ||||
| `); | ||||
| ``` | ||||
|  | ||||
| ## Troubleshooting | ||||
|  | ||||
| ### Common Issues | ||||
|  | ||||
| 1. **Circular References** | ||||
|    ```typescript | ||||
|    // Detect cycles before creating branches | ||||
|    if (!parentNote.hasAncestor(childNote.noteId)) { | ||||
|        childNote.setParent(parentNote.noteId); | ||||
|    } | ||||
|    ``` | ||||
|  | ||||
| 2. **Orphaned Entities** | ||||
|    ```typescript | ||||
|    // Find orphaned notes | ||||
|    const orphans = sql.getRows(` | ||||
|        SELECT noteId FROM notes | ||||
|        WHERE noteId != 'root' | ||||
|        AND noteId NOT IN (SELECT noteId FROM branches) | ||||
|    `); | ||||
|    ``` | ||||
|  | ||||
| 3. **Attribute Conflicts** | ||||
|    ```typescript | ||||
|    // Handle duplicate attributes | ||||
|    const existing = note.getAttribute("label", "status"); | ||||
|    if (existing) { | ||||
|        existing.value = "new value"; | ||||
|        existing.save(); | ||||
|    } else { | ||||
|        note.addLabel("status", "new value"); | ||||
|    } | ||||
|    ``` | ||||
|  | ||||
| ## Related Documentation | ||||
|  | ||||
| - [Three-Layer Cache System](Three-Layer-Cache-System.md) - Cache architecture | ||||
| - [Database Schema](../Development%20and%20architecture/Database/notes.md) - Database structure | ||||
| - [Script API](../../Script%20API/) - Entity API for scripts | ||||
							
								
								
									
										610
									
								
								docs/Developer Guide/Architecture/Monorepo-Structure.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										610
									
								
								docs/Developer Guide/Architecture/Monorepo-Structure.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,610 @@ | ||||
| # Monorepo Structure | ||||
|  | ||||
| Trilium is organized as a TypeScript monorepo using NX, facilitating code sharing, consistent tooling, and efficient build processes. This document provides a comprehensive overview of the project structure, build system, and development workflow. | ||||
|  | ||||
| ## Project Organization | ||||
|  | ||||
| ``` | ||||
| TriliumNext/Trilium/ | ||||
| ├── apps/                      # Runnable applications | ||||
| │   ├── client/               # Frontend web application | ||||
| │   ├── server/               # Node.js backend server | ||||
| │   ├── desktop/              # Electron desktop application | ||||
| │   ├── web-clipper/          # Browser extension | ||||
| │   ├── db-compare/           # Database comparison tool | ||||
| │   ├── dump-db/              # Database dump utility | ||||
| │   └── edit-docs/            # Documentation editor | ||||
| ├── packages/                  # Shared libraries | ||||
| │   ├── commons/              # Shared interfaces and utilities | ||||
| │   ├── ckeditor5/            # Rich text editor | ||||
| │   ├── codemirror/           # Code editor | ||||
| │   ├── highlightjs/          # Syntax highlighting | ||||
| │   ├── ckeditor5-admonition/ # CKEditor plugin | ||||
| │   ├── ckeditor5-footnotes/  # CKEditor plugin | ||||
| │   ├── ckeditor5-math/       # CKEditor plugin | ||||
| │   └── ckeditor5-mermaid/    # CKEditor plugin | ||||
| ├── docs/                      # Documentation | ||||
| ├── nx.json                    # NX workspace configuration | ||||
| ├── package.json              # Root package configuration | ||||
| ├── pnpm-workspace.yaml       # PNPM workspace configuration | ||||
| └── tsconfig.base.json        # Base TypeScript configuration | ||||
| ``` | ||||
|  | ||||
| ## Applications | ||||
|  | ||||
| ### Client (`/apps/client`) | ||||
|  | ||||
| The frontend application shared by both server and desktop versions. | ||||
|  | ||||
| ``` | ||||
| apps/client/ | ||||
| ├── src/ | ||||
| │   ├── components/         # Core UI components | ||||
| │   ├── entities/           # Frontend entities (FNote, FBranch, etc.) | ||||
| │   ├── services/           # Business logic and API calls | ||||
| │   ├── widgets/            # UI widgets system | ||||
| │   │   ├── type_widgets/   # Note type specific widgets | ||||
| │   │   ├── dialogs/        # Dialog components | ||||
| │   │   └── panels/         # Panel widgets | ||||
| │   ├── public/ | ||||
| │   │   ├── fonts/          # Font assets | ||||
| │   │   ├── images/         # Image assets | ||||
| │   │   └── libraries/      # Third-party libraries | ||||
| │   └── desktop.ts          # Desktop entry point | ||||
| ├── package.json | ||||
| ├── project.json            # NX project configuration | ||||
| └── vite.config.ts          # Vite build configuration | ||||
| ``` | ||||
|  | ||||
| #### Key Files | ||||
|  | ||||
| - `desktop.ts` - Main application initialization | ||||
| - `services/froca.ts` - Frontend cache implementation | ||||
| - `widgets/basic_widget.ts` - Base widget class | ||||
| - `services/server.ts` - API communication layer | ||||
|  | ||||
| ### Server (`/apps/server`) | ||||
|  | ||||
| The Node.js backend providing API, database, and business logic. | ||||
|  | ||||
| ``` | ||||
| apps/server/ | ||||
| ├── src/ | ||||
| │   ├── becca/              # Backend cache system | ||||
| │   │   ├── entities/       # Core entities (BNote, BBranch, etc.) | ||||
| │   │   └── becca.ts        # Cache interface | ||||
| │   ├── routes/             # Express routes | ||||
| │   │   ├── api/            # Internal API endpoints | ||||
| │   │   └── pages/          # HTML page routes | ||||
| │   ├── etapi/              # External API | ||||
| │   ├── services/           # Business services | ||||
| │   ├── share/              # Note sharing functionality | ||||
| │   │   └── shaca/          # Share cache | ||||
| │   ├── migrations/         # Database migrations | ||||
| │   ├── assets/ | ||||
| │   │   ├── db/             # Database schema | ||||
| │   │   └── doc_notes/      # Documentation notes | ||||
| │   └── main.ts             # Server entry point | ||||
| ├── package.json | ||||
| ├── project.json | ||||
| └── webpack.config.js       # Webpack configuration | ||||
| ``` | ||||
|  | ||||
| #### Key Services | ||||
|  | ||||
| - `services/sql.ts` - Database access layer | ||||
| - `services/sync.ts` - Synchronization logic | ||||
| - `services/ws.ts` - WebSocket server | ||||
| - `services/protected_session.ts` - Encryption handling | ||||
|  | ||||
| ### Desktop (`/apps/desktop`) | ||||
|  | ||||
| Electron wrapper for the desktop application. | ||||
|  | ||||
| ``` | ||||
| apps/desktop/ | ||||
| ├── src/ | ||||
| │   ├── main.ts             # Electron main process | ||||
| │   ├── preload.ts          # Preload script | ||||
| │   ├── services/           # Desktop-specific services | ||||
| │   └── utils/              # Desktop utilities | ||||
| ├── resources/              # Desktop resources (icons, etc.) | ||||
| ├── package.json | ||||
| └── electron-builder.yml    # Electron Builder configuration | ||||
| ``` | ||||
|  | ||||
| ### Web Clipper (`/apps/web-clipper`) | ||||
|  | ||||
| Browser extension for saving web content to Trilium. | ||||
|  | ||||
| ``` | ||||
| apps/web-clipper/ | ||||
| ├── src/ | ||||
| │   ├── background.js       # Background script | ||||
| │   ├── content.js          # Content script | ||||
| │   ├── popup/              # Extension popup | ||||
| │   └── options/            # Extension options | ||||
| ├── manifest.json           # Extension manifest | ||||
| └── package.json | ||||
| ``` | ||||
|  | ||||
| ## Packages | ||||
|  | ||||
| ### Commons (`/packages/commons`) | ||||
|  | ||||
| Shared TypeScript interfaces and utilities used across applications. | ||||
|  | ||||
| ```typescript | ||||
| // packages/commons/src/types.ts | ||||
| export interface NoteRow { | ||||
|     noteId: string; | ||||
|     title: string; | ||||
|     type: string; | ||||
|     mime: string; | ||||
|     isProtected: boolean; | ||||
|     dateCreated: string; | ||||
|     dateModified: string; | ||||
| } | ||||
|  | ||||
| export interface BranchRow { | ||||
|     branchId: string; | ||||
|     noteId: string; | ||||
|     parentNoteId: string; | ||||
|     notePosition: number; | ||||
|     prefix: string; | ||||
|     isExpanded: boolean; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### CKEditor5 (`/packages/ckeditor5`) | ||||
|  | ||||
| Custom CKEditor5 build with Trilium-specific plugins. | ||||
|  | ||||
| ``` | ||||
| packages/ckeditor5/ | ||||
| ├── src/ | ||||
| │   ├── ckeditor.ts         # Editor configuration | ||||
| │   ├── plugins.ts          # Plugin registration | ||||
| │   └── trilium/            # Custom plugins | ||||
| ├── theme/                  # Editor themes | ||||
| └── package.json | ||||
| ``` | ||||
|  | ||||
| #### Custom Plugins | ||||
|  | ||||
| - **Admonition**: Note boxes with icons | ||||
| - **Footnotes**: Reference footnotes | ||||
| - **Math**: LaTeX equation rendering | ||||
| - **Mermaid**: Diagram integration | ||||
|  | ||||
| ### CodeMirror (`/packages/codemirror`) | ||||
|  | ||||
| Code editor customizations for the code note type. | ||||
|  | ||||
| ```typescript | ||||
| // packages/codemirror/src/index.ts | ||||
| export function createCodeMirror(element: HTMLElement, options: CodeMirrorOptions) { | ||||
|     return CodeMirror(element, { | ||||
|         ...defaultOptions, | ||||
|         ...options, | ||||
|         // Trilium-specific customizations | ||||
|     }); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Build System | ||||
|  | ||||
| ### NX Configuration | ||||
|  | ||||
| **`nx.json`** | ||||
|  | ||||
| ```json | ||||
| { | ||||
|   "tasksRunnerOptions": { | ||||
|     "default": { | ||||
|       "runner": "nx/tasks-runners/default", | ||||
|       "options": { | ||||
|         "cacheableOperations": ["build", "test", "lint"], | ||||
|         "parallel": 3 | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "targetDefaults": { | ||||
|     "build": { | ||||
|       "dependsOn": ["^build"], | ||||
|       "cache": true | ||||
|     }, | ||||
|     "test": { | ||||
|       "cache": true | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Project Configuration | ||||
|  | ||||
| Each application and package has a `project.json` defining its targets: | ||||
|  | ||||
| ```json | ||||
| { | ||||
|   "name": "server", | ||||
|   "targets": { | ||||
|     "build": { | ||||
|       "executor": "@nx/webpack:webpack", | ||||
|       "options": { | ||||
|         "outputPath": "dist/apps/server", | ||||
|         "main": "apps/server/src/main.ts", | ||||
|         "tsConfig": "apps/server/tsconfig.app.json" | ||||
|       } | ||||
|     }, | ||||
|     "serve": { | ||||
|       "executor": "@nx/node:node", | ||||
|       "options": { | ||||
|         "buildTarget": "server:build" | ||||
|       } | ||||
|     }, | ||||
|     "test": { | ||||
|       "executor": "@nx/jest:jest", | ||||
|       "options": { | ||||
|         "jestConfig": "apps/server/jest.config.ts" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Build Commands | ||||
|  | ||||
| ```bash | ||||
| # Build specific project | ||||
| pnpm nx build server | ||||
| pnpm nx build client | ||||
|  | ||||
| # Build all projects | ||||
| pnpm nx run-many --target=build --all | ||||
|  | ||||
| # Build with dependencies | ||||
| pnpm nx build server --with-deps | ||||
|  | ||||
| # Production build | ||||
| pnpm nx build server --configuration=production | ||||
| ``` | ||||
|  | ||||
| ## Development Workflow | ||||
|  | ||||
| ### Initial Setup | ||||
|  | ||||
| ```bash | ||||
| # Install dependencies | ||||
| pnpm install | ||||
|  | ||||
| # Enable corepack for pnpm | ||||
| corepack enable | ||||
|  | ||||
| # Build all packages | ||||
| pnpm nx run-many --target=build --all | ||||
| ``` | ||||
|  | ||||
| ### Development Commands | ||||
|  | ||||
| ```bash | ||||
| # Start development server | ||||
| pnpm run server:start | ||||
| # or | ||||
| pnpm nx run server:serve | ||||
|  | ||||
| # Start desktop app | ||||
| pnpm nx run desktop:serve | ||||
|  | ||||
| # Run client dev server | ||||
| pnpm nx run client:serve | ||||
|  | ||||
| # Watch mode for packages | ||||
| pnpm nx run commons:build --watch | ||||
| ``` | ||||
|  | ||||
| ### Testing | ||||
|  | ||||
| ```bash | ||||
| # Run all tests | ||||
| pnpm test:all | ||||
|  | ||||
| # Run tests for specific project | ||||
| pnpm nx test server | ||||
| pnpm nx test client | ||||
|  | ||||
| # Run tests in watch mode | ||||
| pnpm nx test server --watch | ||||
|  | ||||
| # Generate coverage | ||||
| pnpm nx test server --coverage | ||||
| ``` | ||||
|  | ||||
| ### Linting and Type Checking | ||||
|  | ||||
| ```bash | ||||
| # Lint specific project | ||||
| pnpm nx lint server | ||||
|  | ||||
| # Type check | ||||
| pnpm nx run server:typecheck | ||||
|  | ||||
| # Lint all projects | ||||
| pnpm nx run-many --target=lint --all | ||||
|  | ||||
| # Fix lint issues | ||||
| pnpm nx lint server --fix | ||||
| ``` | ||||
|  | ||||
| ## Dependency Management | ||||
|  | ||||
| ### Package Dependencies | ||||
|  | ||||
| Dependencies are managed at both root and project levels: | ||||
|  | ||||
| ```json | ||||
| // Root package.json - shared dev dependencies | ||||
| { | ||||
|   "devDependencies": { | ||||
|     "@nx/workspace": "^17.0.0", | ||||
|     "typescript": "^5.0.0", | ||||
|     "eslint": "^8.0.0" | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Project package.json - project-specific dependencies | ||||
| { | ||||
|   "dependencies": { | ||||
|     "express": "^4.18.0", | ||||
|     "better-sqlite3": "^9.0.0" | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Adding Dependencies | ||||
|  | ||||
| ```bash | ||||
| # Add to root | ||||
| pnpm add -D typescript | ||||
|  | ||||
| # Add to specific project | ||||
| pnpm add express --filter server | ||||
|  | ||||
| # Add to multiple projects | ||||
| pnpm add lodash --filter server --filter client | ||||
| ``` | ||||
|  | ||||
| ### Workspace References | ||||
|  | ||||
| Internal packages are referenced using workspace protocol: | ||||
|  | ||||
| ```json | ||||
| { | ||||
|   "dependencies": { | ||||
|     "@triliumnext/commons": "workspace:*" | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## TypeScript Configuration | ||||
|  | ||||
| ### Base Configuration | ||||
|  | ||||
| **`tsconfig.base.json`** | ||||
|  | ||||
| ```json | ||||
| { | ||||
|   "compilerOptions": { | ||||
|     "rootDir": ".", | ||||
|     "sourceMap": true, | ||||
|     "declaration": false, | ||||
|     "moduleResolution": "node", | ||||
|     "emitDecoratorMetadata": true, | ||||
|     "experimentalDecorators": true, | ||||
|     "importHelpers": true, | ||||
|     "target": "ES2022", | ||||
|     "module": "ESNext", | ||||
|     "lib": ["ES2022", "dom"], | ||||
|     "skipLibCheck": true, | ||||
|     "skipDefaultLibCheck": true, | ||||
|     "baseUrl": ".", | ||||
|     "paths": { | ||||
|       "@triliumnext/commons": ["packages/commons/src/index.ts"] | ||||
|     } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Project-Specific Configuration | ||||
|  | ||||
| ```json | ||||
| // apps/server/tsconfig.json | ||||
| { | ||||
|   "extends": "../../tsconfig.base.json", | ||||
|   "compilerOptions": { | ||||
|     "module": "commonjs", | ||||
|     "types": ["node"] | ||||
|   }, | ||||
|   "include": ["src/**/*"], | ||||
|   "exclude": ["**/*.spec.ts"] | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Build Optimization | ||||
|  | ||||
| ### NX Cloud | ||||
|  | ||||
| ```bash | ||||
| # Enable NX Cloud for distributed caching | ||||
| pnpm nx connect-to-nx-cloud | ||||
| ``` | ||||
|  | ||||
| ### Affected Commands | ||||
|  | ||||
| ```bash | ||||
| # Build only affected projects | ||||
| pnpm nx affected:build --base=main | ||||
|  | ||||
| # Test only affected projects | ||||
| pnpm nx affected:test --base=main | ||||
|  | ||||
| # Lint only affected projects | ||||
| pnpm nx affected:lint --base=main | ||||
| ``` | ||||
|  | ||||
| ### Build Caching | ||||
|  | ||||
| NX caches build outputs to speed up subsequent builds: | ||||
|  | ||||
| ```bash | ||||
| # Clear cache | ||||
| pnpm nx reset | ||||
|  | ||||
| # Run with cache disabled | ||||
| pnpm nx build server --skip-nx-cache | ||||
|  | ||||
| # See cache statistics | ||||
| pnpm nx report | ||||
| ``` | ||||
|  | ||||
| ## Production Builds | ||||
|  | ||||
| ### Building for Production | ||||
|  | ||||
| ```bash | ||||
| # Build server for production | ||||
| pnpm nx build server --configuration=production | ||||
|  | ||||
| # Build client with optimization | ||||
| pnpm nx build client --configuration=production | ||||
|  | ||||
| # Build desktop app | ||||
| pnpm nx build desktop --configuration=production | ||||
| pnpm electron:build  # Creates distributables | ||||
| ``` | ||||
|  | ||||
| ### Docker Build | ||||
|  | ||||
| ```dockerfile | ||||
| # Multi-stage build | ||||
| FROM node:20-alpine AS builder | ||||
|  | ||||
| WORKDIR /app | ||||
| COPY package*.json pnpm-lock.yaml ./ | ||||
| RUN corepack enable && pnpm install --frozen-lockfile | ||||
|  | ||||
| COPY . . | ||||
| RUN pnpm nx build server --configuration=production | ||||
|  | ||||
| FROM node:20-alpine | ||||
| WORKDIR /app | ||||
| COPY --from=builder /app/dist/apps/server ./ | ||||
| COPY --from=builder /app/node_modules ./node_modules | ||||
|  | ||||
| CMD ["node", "main.js"] | ||||
| ``` | ||||
|  | ||||
| ## Continuous Integration | ||||
|  | ||||
| ### GitHub Actions Example | ||||
|  | ||||
| ```yaml | ||||
| name: CI | ||||
|  | ||||
| on: [push, pull_request] | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
|      | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|        | ||||
|       - uses: pnpm/action-setup@v2 | ||||
|         with: | ||||
|           version: 8 | ||||
|            | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: 20 | ||||
|           cache: 'pnpm' | ||||
|            | ||||
|       - run: pnpm install --frozen-lockfile | ||||
|        | ||||
|       - run: pnpm nx affected:lint --base=origin/main | ||||
|        | ||||
|       - run: pnpm nx affected:test --base=origin/main | ||||
|        | ||||
|       - run: pnpm nx affected:build --base=origin/main | ||||
| ``` | ||||
|  | ||||
| ## Troubleshooting | ||||
|  | ||||
| ### Common Issues | ||||
|  | ||||
| 1. **Build Cache Issues** | ||||
|    ```bash | ||||
|    # Clear NX cache | ||||
|    pnpm nx reset | ||||
|     | ||||
|    # Clear node_modules and reinstall | ||||
|    rm -rf node_modules | ||||
|    pnpm install | ||||
|    ``` | ||||
|  | ||||
| 2. **Dependency Version Conflicts** | ||||
|    ```bash | ||||
|    # Check for duplicate packages | ||||
|    pnpm list --depth=0 | ||||
|     | ||||
|    # Update all dependencies | ||||
|    pnpm update --recursive | ||||
|    ``` | ||||
|  | ||||
| 3. **TypeScript Path Resolution** | ||||
|    ```bash | ||||
|    # Verify TypeScript paths | ||||
|    pnpm nx run server:typecheck --traceResolution | ||||
|    ``` | ||||
|  | ||||
| ### Debug Commands | ||||
|  | ||||
| ```bash | ||||
| # Show project graph | ||||
| pnpm nx graph | ||||
|  | ||||
| # Show project dependencies | ||||
| pnpm nx print-affected --type=app --select=projects | ||||
|  | ||||
| # Verbose output | ||||
| pnpm nx build server --verbose | ||||
|  | ||||
| # Profile build performance | ||||
| pnpm nx build server --profile | ||||
| ``` | ||||
|  | ||||
| ## Best Practices | ||||
|  | ||||
| ### Project Structure | ||||
|  | ||||
| 1. **Keep packages focused**: Each package should have a single, clear purpose | ||||
| 2. **Minimize circular dependencies**: Use dependency graph to identify issues | ||||
| 3. **Share common code**: Extract shared logic to packages/commons | ||||
|  | ||||
| ### Development | ||||
|  | ||||
| 1. **Use NX generators**: Generate consistent code structure | ||||
| 2. **Leverage caching**: Don't skip-nx-cache unless debugging | ||||
| 3. **Run affected commands**: Save time by only building/testing changed code | ||||
|  | ||||
| ### Testing | ||||
|  | ||||
| 1. **Colocate tests**: Keep test files next to source files | ||||
| 2. **Use workspace scripts**: Define common scripts in root package.json | ||||
| 3. **Parallel execution**: Use `--parallel` flag for faster execution | ||||
|  | ||||
| ## Related Documentation | ||||
|  | ||||
| - [Environment Setup](../Environment%20Setup.md) - Development environment setup | ||||
| - [Project Structure](../Project%20Structure.md) - Detailed project structure | ||||
| - [Build Information](../Development%20and%20architecture/Build%20information.md) - Build details | ||||
							
								
								
									
										89
									
								
								docs/Developer Guide/Architecture/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								docs/Developer Guide/Architecture/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| # Trilium Architecture Documentation | ||||
|  | ||||
| This comprehensive guide documents the architecture of Trilium Notes, providing developers with detailed information about the system's core components, data flow, and design patterns. | ||||
|  | ||||
| ## Table of Contents | ||||
|  | ||||
| 1. [Three-Layer Cache System](Three-Layer-Cache-System.md) | ||||
| 2. [Entity System](Entity-System.md)  | ||||
| 3. [Widget-Based UI Architecture](Widget-Based-UI-Architecture.md) | ||||
| 4. [API Architecture](API-Architecture.md) | ||||
| 5. [Monorepo Structure](Monorepo-Structure.md) | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| Trilium Notes is built as a TypeScript monorepo using NX, featuring a sophisticated architecture that balances performance, flexibility, and maintainability. The system is designed around several key architectural patterns: | ||||
|  | ||||
| - **Three-layer caching system** for optimal performance across backend, frontend, and shared content | ||||
| - **Entity-based data model** supporting hierarchical note structures with multiple parent relationships | ||||
| - **Widget-based UI architecture** enabling modular and extensible interface components | ||||
| - **Multiple API layers** for internal operations, external integrations, and real-time synchronization | ||||
| - **Monorepo structure** facilitating code sharing and consistent development patterns | ||||
|  | ||||
| ## Quick Start for Developers | ||||
|  | ||||
| If you're new to Trilium development, start with these sections: | ||||
|  | ||||
| 1. [Monorepo Structure](Monorepo-Structure.md) - Understand the project organization | ||||
| 2. [Entity System](Entity-System.md) - Learn about the core data model | ||||
| 3. [Three-Layer Cache System](Three-Layer-Cache-System.md) - Understand data flow and caching | ||||
|  | ||||
| For UI development, refer to: | ||||
| - [Widget-Based UI Architecture](Widget-Based-UI-Architecture.md) | ||||
|  | ||||
| For API integration, see: | ||||
| - [API Architecture](API-Architecture.md) | ||||
|  | ||||
| ## Architecture Principles | ||||
|  | ||||
| ### Performance First | ||||
| - Lazy loading of note content | ||||
| - Efficient caching at multiple layers | ||||
| - Optimized database queries with prepared statements | ||||
|  | ||||
| ### Flexibility | ||||
| - Support for multiple note types | ||||
| - Extensible through scripting | ||||
| - Plugin architecture for UI widgets | ||||
|  | ||||
| ### Data Integrity | ||||
| - Transactional database operations | ||||
| - Revision history for all changes | ||||
| - Synchronization conflict resolution | ||||
|  | ||||
| ### Security | ||||
| - Per-note encryption | ||||
| - Protected sessions | ||||
| - API authentication tokens | ||||
|  | ||||
| ## Development Workflow | ||||
|  | ||||
| 1. **Setup Development Environment** | ||||
|    ```bash | ||||
|    pnpm install | ||||
|    pnpm run server:start | ||||
|    ``` | ||||
|  | ||||
| 2. **Make Changes** | ||||
|    - Backend changes in `apps/server/src/` | ||||
|    - Frontend changes in `apps/client/src/` | ||||
|    - Shared code in `packages/` | ||||
|  | ||||
| 3. **Test Your Changes** | ||||
|    ```bash | ||||
|    pnpm test:all | ||||
|    pnpm nx run <project>:lint | ||||
|    ``` | ||||
|  | ||||
| 4. **Build for Production** | ||||
|    ```bash | ||||
|    pnpm nx build server | ||||
|    pnpm nx build client | ||||
|    ``` | ||||
|  | ||||
| ## Further Reading | ||||
|  | ||||
| - [Development Environment Setup](../Environment%20Setup.md) | ||||
| - [Adding a New Note Type](../Development%20and%20architecture/Adding%20a%20new%20note%20type/First%20steps.md) | ||||
| - [Database Schema](../Development%20and%20architecture/Database/notes.md) | ||||
| - [Script API Documentation](../../Script%20API/) | ||||
							
								
								
									
										369
									
								
								docs/Developer Guide/Architecture/Three-Layer-Cache-System.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										369
									
								
								docs/Developer Guide/Architecture/Three-Layer-Cache-System.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,369 @@ | ||||
| # Three-Layer Cache System Architecture | ||||
|  | ||||
| Trilium implements a sophisticated three-layer caching system to optimize performance and reduce database load. This architecture ensures fast access to frequently used data while maintaining consistency across different application contexts. | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| The three cache layers are: | ||||
|  | ||||
| 1. **Becca** (Backend Cache) - Server-side entity cache | ||||
| 2. **Froca** (Frontend Cache) - Client-side mirror of backend data | ||||
| 3. **Shaca** (Share Cache) - Optimized cache for shared/published notes | ||||
|  | ||||
| ```mermaid | ||||
| graph TB | ||||
|     subgraph "Database Layer" | ||||
|         DB[(SQLite Database)] | ||||
|     end | ||||
|      | ||||
|     subgraph "Backend Layer" | ||||
|         Becca[Becca Cache<br/>Backend Cache] | ||||
|         API[API Layer] | ||||
|     end | ||||
|      | ||||
|     subgraph "Frontend Layer" | ||||
|         Froca[Froca Cache<br/>Frontend Cache] | ||||
|         UI[UI Components] | ||||
|     end | ||||
|      | ||||
|     subgraph "Share Layer" | ||||
|         Shaca[Shaca Cache<br/>Share Cache] | ||||
|         Share[Public Share Interface] | ||||
|     end | ||||
|      | ||||
|     DB <--> Becca | ||||
|     Becca <--> API | ||||
|     API <--> Froca | ||||
|     Froca <--> UI | ||||
|     DB <--> Shaca | ||||
|     Shaca <--> Share | ||||
|      | ||||
|     style Becca fill:#e1f5fe | ||||
|     style Froca fill:#fff3e0 | ||||
|     style Shaca fill:#f3e5f5 | ||||
| ``` | ||||
|  | ||||
| ## Becca (Backend Cache) | ||||
|  | ||||
| **Location**: `/apps/server/src/becca/` | ||||
|  | ||||
| Becca is the authoritative cache layer that maintains all notes, branches, attributes, and options in server memory. | ||||
|  | ||||
| ### Key Components | ||||
|  | ||||
| #### Becca Interface (`becca-interface.ts`) | ||||
|  | ||||
| ```typescript | ||||
| export default class Becca { | ||||
|     loaded: boolean; | ||||
|     notes: Record<string, BNote>; | ||||
|     branches: Record<string, BBranch>; | ||||
|     childParentToBranch: Record<string, BBranch>; | ||||
|     attributes: Record<string, BAttribute>; | ||||
|     attributeIndex: Record<string, BAttribute[]>; | ||||
|     options: Record<string, BOption>; | ||||
|     etapiTokens: Record<string, BEtapiToken>; | ||||
|     allNoteSetCache: NoteSet | null; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| - **In-memory storage**: All active entities are kept in memory for fast access | ||||
| - **Lazy loading**: Related entities (revisions, attachments) loaded on demand | ||||
| - **Index structures**: Optimized lookups via `childParentToBranch` and `attributeIndex` | ||||
| - **Cache invalidation**: Automatic cache updates on entity changes | ||||
| - **Protected note decryption**: On-demand decryption of encrypted content | ||||
|  | ||||
| ### Usage Example | ||||
|  | ||||
| ```typescript | ||||
| import becca from "./becca/becca.js"; | ||||
|  | ||||
| // Get a note | ||||
| const note = becca.getNote("noteId"); | ||||
|  | ||||
| // Find attributes by type and name | ||||
| const labels = becca.findAttributes("label", "todoItem"); | ||||
|  | ||||
| // Get branch relationships | ||||
| const branch = becca.getBranchFromChildAndParent(childId, parentId); | ||||
| ``` | ||||
|  | ||||
| ### Data Flow | ||||
|  | ||||
| 1. **Initialization**: Load all notes, branches, and attributes from database | ||||
| 2. **Access**: Direct memory access for cached entities | ||||
| 3. **Updates**: Write-through cache with immediate database persistence | ||||
| 4. **Invalidation**: Automatic cache refresh on entity changes | ||||
|  | ||||
| ## Froca (Frontend Cache) | ||||
|  | ||||
| **Location**: `/apps/client/src/services/froca.ts` | ||||
|  | ||||
| Froca is the frontend mirror of Becca, maintaining a subset of backend data for client-side operations. | ||||
|  | ||||
| ### Key Components | ||||
|  | ||||
| #### Froca Implementation (`froca.ts`) | ||||
|  | ||||
| ```typescript | ||||
| class FrocaImpl implements Froca { | ||||
|     notes: Record<string, FNote>; | ||||
|     branches: Record<string, FBranch>; | ||||
|     attributes: Record<string, FAttribute>; | ||||
|     attachments: Record<string, FAttachment>; | ||||
|     blobPromises: Record<string, Promise<FBlob | null> | null>; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| - **Lazy loading**: Notes loaded on-demand with their immediate context | ||||
| - **Subtree loading**: Efficient loading of note hierarchies | ||||
| - **Real-time updates**: WebSocket synchronization with backend changes | ||||
| - **Search note support**: Virtual branches for search results | ||||
| - **Promise-based blob loading**: Asynchronous content loading | ||||
|  | ||||
| ### Loading Strategy | ||||
|  | ||||
| ```typescript | ||||
| // Initial load - loads root and immediate children | ||||
| await froca.loadInitialTree(); | ||||
|  | ||||
| // Load subtree on demand | ||||
| const note = await froca.loadSubTree(noteId); | ||||
|  | ||||
| // Reload specific notes | ||||
| await froca.reloadNotes([noteId1, noteId2]); | ||||
| ``` | ||||
|  | ||||
| ### Synchronization | ||||
|  | ||||
| Froca maintains consistency with Becca through: | ||||
|  | ||||
| 1. **Initial sync**: Load essential tree structure on startup | ||||
| 2. **On-demand loading**: Fetch notes as needed | ||||
| 3. **WebSocket updates**: Real-time push of changes from backend | ||||
| 4. **Batch reloading**: Efficient refresh of multiple notes | ||||
|  | ||||
| ## Shaca (Share Cache) | ||||
|  | ||||
| **Location**: `/apps/server/src/share/shaca/` | ||||
|  | ||||
| Shaca is a specialized cache for publicly shared notes, optimized for read-only access. | ||||
|  | ||||
| ### Key Components | ||||
|  | ||||
| #### Shaca Interface (`shaca-interface.ts`) | ||||
|  | ||||
| ```typescript | ||||
| export default class Shaca { | ||||
|     notes: Record<string, SNote>; | ||||
|     branches: Record<string, SBranch>; | ||||
|     childParentToBranch: Record<string, SBranch>; | ||||
|     attributes: Record<string, SAttribute>; | ||||
|     attachments: Record<string, SAttachment>; | ||||
|     aliasToNote: Record<string, SNote>; | ||||
|     shareRootNote: SNote | null; | ||||
|     shareIndexEnabled: boolean; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| - **Read-only optimization**: Streamlined for public access | ||||
| - **Alias support**: URL-friendly note access via aliases | ||||
| - **Share index**: Optional indexing of all shared subtrees | ||||
| - **Minimal memory footprint**: Only shared content cached | ||||
| - **Security isolation**: Separate from main application cache | ||||
|  | ||||
| ### Usage Patterns | ||||
|  | ||||
| ```typescript | ||||
| // Get shared note by ID | ||||
| const note = shaca.getNote(noteId); | ||||
|  | ||||
| // Access via alias | ||||
| const aliasedNote = shaca.aliasToNote[alias]; | ||||
|  | ||||
| // Check if note is shared | ||||
| const isShared = shaca.hasNote(noteId); | ||||
| ``` | ||||
|  | ||||
| ## Cache Interaction and Data Flow | ||||
|  | ||||
| ### 1. Create/Update Flow | ||||
|  | ||||
| ```mermaid | ||||
| sequenceDiagram | ||||
|     participant Client | ||||
|     participant Froca | ||||
|     participant API | ||||
|     participant Becca | ||||
|     participant DB | ||||
|      | ||||
|     Client->>API: Update Note | ||||
|     API->>Becca: Update Cache | ||||
|     Becca->>DB: Persist Change | ||||
|     Becca->>API: Confirm | ||||
|     API->>Froca: Push Update (WebSocket) | ||||
|     Froca->>Client: Update UI | ||||
| ``` | ||||
|  | ||||
| ### 2. Read Flow | ||||
|  | ||||
| ```mermaid | ||||
| sequenceDiagram | ||||
|     participant Client | ||||
|     participant Froca | ||||
|     participant API | ||||
|     participant Becca | ||||
|      | ||||
|     Client->>Froca: Request Note | ||||
|     alt Note in Cache | ||||
|         Froca->>Client: Return Cached Note | ||||
|     else Note not in Cache | ||||
|         Froca->>API: Fetch Note | ||||
|         API->>Becca: Get Note | ||||
|         Becca->>API: Return Note | ||||
|         API->>Froca: Send Note Data | ||||
|         Froca->>Froca: Cache Note | ||||
|         Froca->>Client: Return Note | ||||
|     end | ||||
| ``` | ||||
|  | ||||
| ### 3. Share Access Flow | ||||
|  | ||||
| ```mermaid | ||||
| sequenceDiagram | ||||
|     participant Browser | ||||
|     participant ShareUI | ||||
|     participant Shaca | ||||
|     participant DB | ||||
|      | ||||
|     Browser->>ShareUI: Access Shared URL | ||||
|     ShareUI->>Shaca: Get Shared Note | ||||
|     alt Note in Cache | ||||
|         Shaca->>ShareUI: Return Cached | ||||
|     else Not in Cache | ||||
|         Shaca->>DB: Load Shared Tree | ||||
|         DB->>Shaca: Return Data | ||||
|         Shaca->>Shaca: Build Cache | ||||
|         Shaca->>ShareUI: Return Note | ||||
|     end | ||||
|     ShareUI->>Browser: Render Content | ||||
| ``` | ||||
|  | ||||
| ## Performance Considerations | ||||
|  | ||||
| ### Memory Management | ||||
|  | ||||
| - **Becca**: Keeps entire note tree in memory (~100-500MB for typical use) | ||||
| - **Froca**: Loads notes on-demand, automatic cleanup of unused notes | ||||
| - **Shaca**: Minimal footprint, only shared content | ||||
|  | ||||
| ### Cache Warming | ||||
|  | ||||
| - **Becca**: Full load on server startup | ||||
| - **Froca**: Progressive loading based on user navigation | ||||
| - **Shaca**: Lazy loading with configurable index | ||||
|  | ||||
| ### Optimization Strategies | ||||
|  | ||||
| 1. **Attribute Indexing**: Pre-built indexes for fast attribute queries | ||||
| 2. **Batch Operations**: Group updates to minimize round trips | ||||
| 3. **Partial Loading**: Load only required fields for lists | ||||
| 4. **WebSocket Compression**: Compressed real-time updates | ||||
|  | ||||
| ## Best Practices | ||||
|  | ||||
| ### When to Use Each Cache | ||||
|  | ||||
| **Use Becca when**: | ||||
| - Implementing server-side business logic | ||||
| - Performing bulk operations | ||||
| - Handling synchronization | ||||
| - Managing protected notes | ||||
|  | ||||
| **Use Froca when**: | ||||
| - Building UI components | ||||
| - Handling user interactions | ||||
| - Displaying note content | ||||
| - Managing client state | ||||
|  | ||||
| **Use Shaca when**: | ||||
| - Serving public content | ||||
| - Building share pages | ||||
| - Implementing read-only access | ||||
| - Creating public APIs | ||||
|  | ||||
| ### Cache Invalidation | ||||
|  | ||||
| ```typescript | ||||
| // Becca - automatic on entity save | ||||
| note.save(); // Cache updated automatically | ||||
|  | ||||
| // Froca - manual reload when needed | ||||
| await froca.reloadNotes([noteId]); | ||||
|  | ||||
| // Shaca - rebuild on share changes | ||||
| shaca.reset(); | ||||
| shaca.load(); | ||||
| ``` | ||||
|  | ||||
| ### Error Handling | ||||
|  | ||||
| ```typescript | ||||
| // Becca - throw on missing required entities | ||||
| const note = becca.getNoteOrThrow(noteId); // throws NotFoundError | ||||
|  | ||||
| // Froca - graceful degradation | ||||
| const note = await froca.getNote(noteId); | ||||
| if (!note) { | ||||
|     // Handle missing note | ||||
| } | ||||
|  | ||||
| // Shaca - check existence first | ||||
| if (shaca.hasNote(noteId)) { | ||||
|     const note = shaca.getNote(noteId); | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Troubleshooting | ||||
|  | ||||
| ### Common Issues | ||||
|  | ||||
| 1. **Cache Inconsistency** | ||||
|    - Symptom: UI shows outdated data | ||||
|    - Solution: Force reload with `froca.reloadNotes()` | ||||
|  | ||||
| 2. **Memory Growth** | ||||
|    - Symptom: Server memory usage increases | ||||
|    - Solution: Check for memory leaks in custom scripts | ||||
|  | ||||
| 3. **Slow Initial Load** | ||||
|    - Symptom: Long startup time | ||||
|    - Solution: Optimize database queries, add indexes | ||||
|  | ||||
| ### Debug Commands | ||||
|  | ||||
| ```javascript | ||||
| // Check cache sizes | ||||
| console.log('Becca notes:', Object.keys(becca.notes).length); | ||||
| console.log('Froca notes:', Object.keys(froca.notes).length); | ||||
| console.log('Shaca notes:', Object.keys(shaca.notes).length); | ||||
|  | ||||
| // Force cache refresh | ||||
| await froca.loadInitialTree(); | ||||
|  | ||||
| // Clear and reload Shaca | ||||
| shaca.reset(); | ||||
| await shaca.load(); | ||||
| ``` | ||||
|  | ||||
| ## Related Documentation | ||||
|  | ||||
| - [Entity System](Entity-System.md) - Detailed entity documentation | ||||
| - [Database Schema](../Development%20and%20architecture/Database/notes.md) - Database structure | ||||
| - [WebSocket Synchronization](API-Architecture.md#websocket-real-time-synchronization) - Real-time updates | ||||
							
								
								
									
										635
									
								
								docs/Developer Guide/Architecture/Widget-Based-UI-Architecture.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										635
									
								
								docs/Developer Guide/Architecture/Widget-Based-UI-Architecture.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,635 @@ | ||||
| # Widget-Based UI Architecture | ||||
|  | ||||
| Trilium's frontend is built on a modular widget system that provides flexibility, reusability, and maintainability. This architecture enables dynamic UI composition and extensibility through custom widgets. | ||||
|  | ||||
| ## Widget System Overview | ||||
|  | ||||
| ```mermaid | ||||
| graph TB | ||||
|     subgraph "Widget Hierarchy" | ||||
|         Component[Component<br/>Base Class] | ||||
|         BasicWidget[BasicWidget<br/>UI Foundation] | ||||
|         NoteContextAware[NoteContextAwareWidget<br/>Note-Aware] | ||||
|         RightPanel[RightPanelWidget<br/>Side Panel] | ||||
|         TypeWidgets[Type Widgets<br/>Note Type Specific] | ||||
|         CustomWidgets[Custom Widgets<br/>User Scripts] | ||||
|     end | ||||
|      | ||||
|     Component --> BasicWidget | ||||
|     BasicWidget --> NoteContextAware | ||||
|     NoteContextAware --> RightPanel | ||||
|     NoteContextAware --> TypeWidgets | ||||
|     NoteContextAware --> CustomWidgets | ||||
|      | ||||
|     style Component fill:#e8f5e9 | ||||
|     style BasicWidget fill:#c8e6c9 | ||||
|     style NoteContextAware fill:#a5d6a7 | ||||
| ``` | ||||
|  | ||||
| ## Core Widget Classes | ||||
|  | ||||
| ### Component (Base Class) | ||||
|  | ||||
| **Location**: `/apps/client/src/components/component.js` | ||||
|  | ||||
| The foundational class for all UI components in Trilium. | ||||
|  | ||||
| ```typescript | ||||
| class Component { | ||||
|     componentId: string;      // Unique identifier | ||||
|     children: Component[];    // Child components | ||||
|     parent: Component | null; // Parent reference | ||||
|      | ||||
|     async refresh(): Promise<void>; | ||||
|     child(...components: Component[]): this; | ||||
|     handleEvent(name: string, data: any): void; | ||||
|     trigger(name: string, data?: any): void; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### BasicWidget | ||||
|  | ||||
| **Location**: `/apps/client/src/widgets/basic_widget.ts` | ||||
|  | ||||
| Base class for all UI widgets, providing DOM manipulation and styling capabilities. | ||||
|  | ||||
| ```typescript | ||||
| export class BasicWidget extends Component { | ||||
|     protected $widget: JQuery; | ||||
|     private attrs: Record<string, string>; | ||||
|     private classes: string[]; | ||||
|      | ||||
|     // Chaining methods for declarative UI | ||||
|     id(id: string): this; | ||||
|     class(className: string): this; | ||||
|     css(name: string, value: string): this; | ||||
|     contentSized(): this; | ||||
|     collapsible(): this; | ||||
|     filling(): this; | ||||
|      | ||||
|     // Conditional rendering | ||||
|     optChild(condition: boolean, ...components: Component[]): this; | ||||
|     optCss(condition: boolean, name: string, value: string): this; | ||||
|      | ||||
|     // Rendering | ||||
|     doRender(): JQuery; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Usage Example | ||||
|  | ||||
| ```typescript | ||||
| class MyWidget extends BasicWidget { | ||||
|     doRender() { | ||||
|         this.$widget = $('<div>') | ||||
|             .addClass('my-widget') | ||||
|             .append($('<h3>').text('Widget Title')); | ||||
|              | ||||
|         return this.$widget; | ||||
|     } | ||||
|      | ||||
|     async refreshWithNote(note: FNote) { | ||||
|         this.$widget.find('h3').text(note.title); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Composing widgets | ||||
| const container = new FlexContainer('column') | ||||
|     .id('main-container') | ||||
|     .css('padding', '10px') | ||||
|     .filling() | ||||
|     .child( | ||||
|         new MyWidget(), | ||||
|         new ButtonWidget() | ||||
|             .title('Click Me') | ||||
|             .onClick(() => console.log('Clicked')) | ||||
|     ); | ||||
| ``` | ||||
|  | ||||
| ### NoteContextAwareWidget | ||||
|  | ||||
| **Location**: `/apps/client/src/widgets/note_context_aware_widget.ts` | ||||
|  | ||||
| Base class for widgets that respond to note context changes. | ||||
|  | ||||
| ```typescript | ||||
| class NoteContextAwareWidget extends BasicWidget { | ||||
|     noteContext: NoteContext | null; | ||||
|     note: FNote | null; | ||||
|     noteId: string | null; | ||||
|     notePath: string | null; | ||||
|      | ||||
|     // Lifecycle methods | ||||
|     async refresh(): Promise<void>; | ||||
|     async refreshWithNote(note: FNote): Promise<void>; | ||||
|     async noteSwitched(): Promise<void>; | ||||
|     async activeContextChanged(): Promise<void>; | ||||
|      | ||||
|     // Event handlers | ||||
|     async noteTypeMimeChanged(): Promise<void>; | ||||
|     async frocaReloaded(): Promise<void>; | ||||
|      | ||||
|     // Utility methods | ||||
|     isNote(noteId: string): boolean; | ||||
|     get isEnabled(): boolean; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Context Management | ||||
|  | ||||
| ```typescript | ||||
| class MyNoteWidget extends NoteContextAwareWidget { | ||||
|     async refreshWithNote(note: FNote) { | ||||
|         // Called when note context changes | ||||
|         this.$widget.find('.note-title').text(note.title); | ||||
|         this.$widget.find('.note-type').text(note.type); | ||||
|          | ||||
|         // Access note attributes | ||||
|         const labels = note.getLabels(); | ||||
|         const relations = note.getRelations(); | ||||
|     } | ||||
|      | ||||
|     async noteSwitched() { | ||||
|         // Called when user switches to different note | ||||
|         console.log(`Switched to note: ${this.noteId}`); | ||||
|     } | ||||
|      | ||||
|     async noteTypeMimeChanged() { | ||||
|         // React to note type changes | ||||
|         if (this.note?.type === 'code') { | ||||
|             this.setupCodeHighlighting(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### RightPanelWidget | ||||
|  | ||||
| **Location**: `/apps/client/src/widgets/right_panel_widget.ts` | ||||
|  | ||||
| Base class for widgets displayed in the right sidebar panel. | ||||
|  | ||||
| ```typescript | ||||
| abstract class RightPanelWidget extends NoteContextAwareWidget { | ||||
|     async doRenderBody(): Promise<JQuery>; | ||||
|     getTitle(): string; | ||||
|     getIcon(): string; | ||||
|     getPosition(): number; | ||||
|      | ||||
|     async isEnabled(): Promise<boolean> { | ||||
|         // Override to control visibility | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Creating Right Panel Widgets | ||||
|  | ||||
| ```typescript | ||||
| class InfoWidget extends RightPanelWidget { | ||||
|     getTitle() { return "Note Info"; } | ||||
|     getIcon() { return "info"; } | ||||
|     getPosition() { return 100; } | ||||
|      | ||||
|     async doRenderBody() { | ||||
|         return $('<div class="info-widget">') | ||||
|             .append($('<div class="created">')) | ||||
|             .append($('<div class="modified">')) | ||||
|             .append($('<div class="word-count">')); | ||||
|     } | ||||
|      | ||||
|     async refreshWithNote(note: FNote) { | ||||
|         this.$body.find('.created').text(`Created: ${note.dateCreated}`); | ||||
|         this.$body.find('.modified').text(`Modified: ${note.dateModified}`); | ||||
|          | ||||
|         const wordCount = this.calculateWordCount(await note.getContent()); | ||||
|         this.$body.find('.word-count').text(`Words: ${wordCount}`); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Type-Specific Widgets | ||||
|  | ||||
| **Location**: `/apps/client/src/widgets/type_widgets/` | ||||
|  | ||||
| Each note type has a specialized widget for rendering and editing. | ||||
|  | ||||
| ### TypeWidget Interface | ||||
|  | ||||
| ```typescript | ||||
| abstract class TypeWidget extends NoteContextAwareWidget { | ||||
|     abstract static getType(): string; | ||||
|      | ||||
|     // Content management | ||||
|     async getContent(): Promise<string>; | ||||
|     async saveContent(content: string): Promise<void>; | ||||
|      | ||||
|     // Focus management | ||||
|     async focus(): Promise<void>; | ||||
|     async blur(): Promise<void>; | ||||
|      | ||||
|     // Cleanup | ||||
|     async cleanup(): Promise<void>; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Common Type Widgets | ||||
|  | ||||
| #### TextTypeWidget | ||||
|  | ||||
| ```typescript | ||||
| class TextTypeWidget extends TypeWidget { | ||||
|     static getType() { return 'text'; } | ||||
|      | ||||
|     private textEditor: TextEditor; | ||||
|      | ||||
|     async doRender() { | ||||
|         const $editor = $('<div class="ck-editor">'); | ||||
|         this.textEditor = await TextEditor.create($editor[0], { | ||||
|             noteId: this.noteId, | ||||
|             content: await this.note.getContent() | ||||
|         }); | ||||
|          | ||||
|         return $editor; | ||||
|     } | ||||
|      | ||||
|     async getContent() { | ||||
|         return this.textEditor.getData(); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### CodeTypeWidget | ||||
|  | ||||
| ```typescript | ||||
| class CodeTypeWidget extends TypeWidget { | ||||
|     static getType() { return 'code'; } | ||||
|      | ||||
|     private codeMirror: CodeMirror; | ||||
|      | ||||
|     async doRender() { | ||||
|         const $container = $('<div class="code-editor">'); | ||||
|          | ||||
|         this.codeMirror = CodeMirror($container[0], { | ||||
|             value: await this.note.getContent(), | ||||
|             mode: this.note.mime, | ||||
|             theme: 'default', | ||||
|             lineNumbers: true | ||||
|         }); | ||||
|          | ||||
|         return $container; | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Widget Composition | ||||
|  | ||||
| ### Container Widgets | ||||
|  | ||||
| ```typescript | ||||
| // Flexible container layouts | ||||
| class FlexContainer extends BasicWidget { | ||||
|     constructor(private direction: 'row' | 'column') { | ||||
|         super(); | ||||
|     } | ||||
|      | ||||
|     doRender() { | ||||
|         this.$widget = $('<div class="flex-container">') | ||||
|             .css('display', 'flex') | ||||
|             .css('flex-direction', this.direction); | ||||
|              | ||||
|         for (const child of this.children) { | ||||
|             this.$widget.append(child.render()); | ||||
|         } | ||||
|          | ||||
|         return this.$widget; | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Tab container | ||||
| class TabContainer extends BasicWidget { | ||||
|     private tabs: Array<{title: string, widget: BasicWidget}> = []; | ||||
|      | ||||
|     addTab(title: string, widget: BasicWidget) { | ||||
|         this.tabs.push({title, widget}); | ||||
|         this.child(widget); | ||||
|         return this; | ||||
|     } | ||||
|      | ||||
|     doRender() { | ||||
|         // Render tab headers and content panels | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Composite Widgets | ||||
|  | ||||
| ```typescript | ||||
| class NoteEditorWidget extends NoteContextAwareWidget { | ||||
|     private typeWidget: TypeWidget; | ||||
|     private titleWidget: NoteTitleWidget; | ||||
|     private toolbarWidget: NoteToolbarWidget; | ||||
|      | ||||
|     constructor() { | ||||
|         super(); | ||||
|          | ||||
|         this.child( | ||||
|             this.toolbarWidget = new NoteToolbarWidget(), | ||||
|             this.titleWidget = new NoteTitleWidget(), | ||||
|             // Type widget added dynamically | ||||
|         ); | ||||
|     } | ||||
|      | ||||
|     async refreshWithNote(note: FNote) { | ||||
|         // Remove old type widget | ||||
|         if (this.typeWidget) { | ||||
|             this.typeWidget.remove(); | ||||
|         } | ||||
|          | ||||
|         // Add appropriate type widget | ||||
|         const WidgetClass = typeWidgetService.getWidgetClass(note.type); | ||||
|         this.typeWidget = new WidgetClass(); | ||||
|         this.child(this.typeWidget); | ||||
|          | ||||
|         await this.typeWidget.refresh(); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Widget Communication | ||||
|  | ||||
| ### Event System | ||||
|  | ||||
| ```typescript | ||||
| // Publishing events | ||||
| class PublisherWidget extends BasicWidget { | ||||
|     async handleClick() { | ||||
|         // Local event | ||||
|         this.trigger('itemSelected', { itemId: '123' }); | ||||
|          | ||||
|         // Global event | ||||
|         appContext.triggerEvent('noteChanged', { noteId: this.noteId }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Subscribing to events | ||||
| class SubscriberWidget extends BasicWidget { | ||||
|     constructor() { | ||||
|         super(); | ||||
|          | ||||
|         // Local event subscription | ||||
|         this.on('itemSelected', (event) => { | ||||
|             console.log('Item selected:', event.itemId); | ||||
|         }); | ||||
|          | ||||
|         // Global event subscription | ||||
|         appContext.addEventListener('noteChanged', (event) => { | ||||
|             this.handleNoteChange(event.noteId); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Command System | ||||
|  | ||||
| ```typescript | ||||
| // Registering commands | ||||
| class CommandWidget extends BasicWidget { | ||||
|     constructor() { | ||||
|         super(); | ||||
|          | ||||
|         this.bindCommand('saveNote', () => this.saveNote()); | ||||
|         this.bindCommand('deleteNote', () => this.deleteNote()); | ||||
|     } | ||||
|      | ||||
|     getCommands() { | ||||
|         return [ | ||||
|             { | ||||
|                 command: 'myWidget:doAction', | ||||
|                 handler: () => this.doAction(), | ||||
|                 hotkey: 'ctrl+shift+a' | ||||
|             } | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Custom Widget Development | ||||
|  | ||||
| ### Creating Custom Widgets | ||||
|  | ||||
| ```typescript | ||||
| // 1. Define widget class | ||||
| class TaskListWidget extends NoteContextAwareWidget { | ||||
|     doRender() { | ||||
|         this.$widget = $('<div class="task-list-widget">'); | ||||
|         this.$list = $('<ul>').appendTo(this.$widget); | ||||
|         return this.$widget; | ||||
|     } | ||||
|      | ||||
|     async refreshWithNote(note: FNote) { | ||||
|         const tasks = await this.loadTasks(note); | ||||
|          | ||||
|         this.$list.empty(); | ||||
|         for (const task of tasks) { | ||||
|             $('<li>') | ||||
|                 .text(task.title) | ||||
|                 .toggleClass('completed', task.completed) | ||||
|                 .appendTo(this.$list); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private async loadTasks(note: FNote) { | ||||
|         // Load task data from note attributes | ||||
|         const taskLabels = note.getLabels('task'); | ||||
|         return taskLabels.map(label => JSON.parse(label.value)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // 2. Register widget | ||||
| api.addWidget(TaskListWidget); | ||||
| ``` | ||||
|  | ||||
| ### Widget Lifecycle | ||||
|  | ||||
| ```typescript | ||||
| class LifecycleWidget extends NoteContextAwareWidget { | ||||
|     // 1. Construction | ||||
|     constructor() { | ||||
|         super(); | ||||
|         console.log('Widget constructed'); | ||||
|     } | ||||
|      | ||||
|     // 2. Initial render | ||||
|     doRender() { | ||||
|         console.log('Initial render'); | ||||
|         return $('<div>'); | ||||
|     } | ||||
|      | ||||
|     // 3. Context initialization | ||||
|     async refresh() { | ||||
|         console.log('Context refresh'); | ||||
|         await super.refresh(); | ||||
|     } | ||||
|      | ||||
|     // 4. Note updates | ||||
|     async refreshWithNote(note: FNote) { | ||||
|         console.log('Note refresh:', note.noteId); | ||||
|     } | ||||
|      | ||||
|     // 5. Cleanup | ||||
|     async cleanup() { | ||||
|         console.log('Widget cleanup'); | ||||
|         // Release resources | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Performance Optimization | ||||
|  | ||||
| ### Lazy Loading | ||||
|  | ||||
| ```typescript | ||||
| class LazyWidget extends BasicWidget { | ||||
|     private contentLoaded = false; | ||||
|      | ||||
|     async becomeVisible() { | ||||
|         if (!this.contentLoaded) { | ||||
|             await this.loadContent(); | ||||
|             this.contentLoaded = true; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private async loadContent() { | ||||
|         // Heavy content loading | ||||
|         const data = await server.get('expensive-data'); | ||||
|         this.renderContent(data); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Debouncing Updates | ||||
|  | ||||
| ```typescript | ||||
| class DebouncedWidget extends NoteContextAwareWidget { | ||||
|     private refreshDebounced = utils.debounce( | ||||
|         () => this.doRefresh(), | ||||
|         500 | ||||
|     ); | ||||
|      | ||||
|     async refreshWithNote(note: FNote) { | ||||
|         // Debounce rapid updates | ||||
|         this.refreshDebounced(); | ||||
|     } | ||||
|      | ||||
|     private async doRefresh() { | ||||
|         // Actual refresh logic | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Virtual Scrolling | ||||
|  | ||||
| ```typescript | ||||
| class VirtualListWidget extends BasicWidget { | ||||
|     private visibleItems: any[] = []; | ||||
|      | ||||
|     renderVisibleItems(scrollTop: number) { | ||||
|         const itemHeight = 30; | ||||
|         const containerHeight = this.$widget.height(); | ||||
|          | ||||
|         const startIndex = Math.floor(scrollTop / itemHeight); | ||||
|         const endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight); | ||||
|          | ||||
|         this.visibleItems = this.allItems.slice(startIndex, endIndex); | ||||
|         this.renderItems(); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Best Practices | ||||
|  | ||||
| ### Widget Design | ||||
|  | ||||
| 1. **Single Responsibility**: Each widget should have one clear purpose | ||||
| 2. **Composition over Inheritance**: Use composition for complex UIs | ||||
| 3. **Lazy Initialization**: Load resources only when needed | ||||
| 4. **Event Cleanup**: Remove event listeners in cleanup() | ||||
|  | ||||
| ### State Management | ||||
|  | ||||
| ```typescript | ||||
| class StatefulWidget extends NoteContextAwareWidget { | ||||
|     private state = { | ||||
|         isExpanded: false, | ||||
|         selectedItems: new Set<string>() | ||||
|     }; | ||||
|      | ||||
|     setState(updates: Partial<typeof this.state>) { | ||||
|         Object.assign(this.state, updates); | ||||
|         this.renderState(); | ||||
|     } | ||||
|      | ||||
|     private renderState() { | ||||
|         this.$widget.toggleClass('expanded', this.state.isExpanded); | ||||
|         // Update DOM based on state | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ### Error Handling | ||||
|  | ||||
| ```typescript | ||||
| class ResilientWidget extends BasicWidget { | ||||
|     async refreshWithNote(note: FNote) { | ||||
|         try { | ||||
|             await this.loadData(note); | ||||
|         } catch (error) { | ||||
|             this.showError('Failed to load data'); | ||||
|             console.error('Widget error:', error); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private showError(message: string) { | ||||
|         this.$widget.html(` | ||||
|             <div class="alert alert-danger"> | ||||
|                 ${message} | ||||
|             </div> | ||||
|         `); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Testing Widgets | ||||
|  | ||||
| ```typescript | ||||
| // Widget test example | ||||
| describe('TaskListWidget', () => { | ||||
|     let widget: TaskListWidget; | ||||
|     let note: FNote; | ||||
|      | ||||
|     beforeEach(() => { | ||||
|         widget = new TaskListWidget(); | ||||
|         note = createMockNote({ | ||||
|             noteId: 'test123', | ||||
|             attributes: [ | ||||
|                 { type: 'label', name: 'task', value: '{"title":"Task 1"}' } | ||||
|             ] | ||||
|         }); | ||||
|     }); | ||||
|      | ||||
|     it('should render tasks', async () => { | ||||
|         await widget.refreshWithNote(note); | ||||
|          | ||||
|         const tasks = widget.$widget.find('li'); | ||||
|         expect(tasks.length).toBe(1); | ||||
|         expect(tasks.text()).toBe('Task 1'); | ||||
|     }); | ||||
| }); | ||||
| ``` | ||||
|  | ||||
| ## Related Documentation | ||||
|  | ||||
| - [Frontend Basics](../../Scripting/Frontend%20Basics.html) - Frontend scripting guide | ||||
| - [Custom Widgets](../../Scripting/Custom%20Widgets.html) - Creating custom widgets | ||||
| - [Script API](../../Script%20API/) - Widget API reference | ||||
| @@ -14,5 +14,5 @@ sudo docker run -p 8081:8080 triliumnext/trilium:v0.90.6-beta | ||||
| To enter a shell in the Docker container: | ||||
|  | ||||
| ``` | ||||
| sudo docker run -it --entrypoint=/bin/sh zadam/trilium:0.63-latest | ||||
| sudo docker run -it --entrypoint=/bin/sh TriliumNext/Trilium:0.63-latest | ||||
| ``` | ||||
| @@ -3,7 +3,7 @@ The note type is defined by the `type` column in <a class="reference-link" href | ||||
|  | ||||
| Possible types: | ||||
|  | ||||
| <table class="ck-table-resized"><colgroup><col> <col> <col> <col> <col></colgroup><thead><tr><th>Note Type</th><th><code>type</code> value</th><th>Corresponding MIME type</th><th>Content of the note's blob</th><th>Relevant attributes</th></tr></thead><tbody><tr><th>Text</th><td><code>text</code></td><td> </td><td>The HTML of the note.</td><td> </td></tr><tr><th><a href="https://github.com/zadam/trilium/wiki/Relation-map">Relation Map </a></th><td><code>relationMap</code></td><td><code>application/json</code></td><td><p>A JSON describing the note:</p><pre><code class="language-text-plain">{ | ||||
| <table class="ck-table-resized"><colgroup><col> <col> <col> <col> <col></colgroup><thead><tr><th>Note Type</th><th><code>type</code> value</th><th>Corresponding MIME type</th><th>Content of the note's blob</th><th>Relevant attributes</th></tr></thead><tbody><tr><th>Text</th><td><code>text</code></td><td> </td><td>The HTML of the note.</td><td> </td></tr><tr><th><a href="https://github.com/TriliumNext/Trilium/wiki/Relation-map">Relation Map </a></th><td><code>relationMap</code></td><td><code>application/json</code></td><td><p>A JSON describing the note:</p><pre><code class="language-text-plain">{ | ||||
|     "notes": [ | ||||
|         { | ||||
|             "noteId": "gFQDL11KEm9G", | ||||
| @@ -21,7 +21,7 @@ Possible types: | ||||
|         "x": 480.29766098682165, | ||||
|         "y": 116.83892021963081 | ||||
|     } | ||||
| }</code></pre></td><td>None</td></tr><tr><th><a href="https://github.com/zadam/trilium/wiki/Scripts">Render Note</a></th><td><code>render</code></td><td><code>text/html</code> or blank.</td><td>An empty blob.</td><td><code>~renderNote</code> pointing to the HTML note to render.</td></tr><tr><th>Canvas</th><td><code>canvas</code></td><td><code>application/json</code></td><td><pre><code class="language-text-plain">{ | ||||
| }</code></pre></td><td>None</td></tr><tr><th><a href="https://github.com/TriliumNext/Trilium/wiki/Scripts">Render Note</a></th><td><code>render</code></td><td><code>text/html</code> or blank.</td><td>An empty blob.</td><td><code>~renderNote</code> pointing to the HTML note to render.</td></tr><tr><th>Canvas</th><td><code>canvas</code></td><td><code>application/json</code></td><td><pre><code class="language-text-plain">{ | ||||
| 	"appState": {}, | ||||
| 	"elemenets": {}, | ||||
| 	"files": {}, | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user