Compare commits

...

3 Commits

Author SHA1 Message Date
perf3ct
ce298e477b feat(docs): try to fix meta.json 2025-08-21 16:32:10 +00:00
perf3ct
81c0e508ac yeet 2025-08-21 16:03:53 +00:00
perf3ct
065740eabc feat(docs): completely redo documentation 2025-08-21 15:55:44 +00:00
155 changed files with 58122 additions and 2966 deletions

View File

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

View File

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

View File

@@ -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;
}
});

View File

@@ -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))
);
}

View File

@@ -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[] = [];

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();
}
});

View File

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

View File

@@ -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 ?? "");

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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;
}
});

View File

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

View File

@@ -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);
}

View File

@@ -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();

View File

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

View File

@@ -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();

View File

@@ -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");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) */

View 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: &lt;your-token&gt;</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": "&lt;p&gt;This is the note content&lt;/p&gt;",
"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", "&lt;p&gt;Discussion points:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Item 1&lt;/li&gt;&lt;/ul&gt;")
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(
"&lt;p&gt;Today's meeting went well. Key decisions:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Item 1&lt;/li&gt;&lt;/ul&gt;",
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>

View 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: '&lt;p&gt;Note content&lt;/p&gt;',
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: '&lt;p&gt;Updated content&lt;/p&gt;',
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>

View 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 = $('&lt;button&gt;').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(`
&lt;h2&gt;📅 ${api.dayjs().format('dddd, MMMM D, YYYY')}&lt;/h2&gt;
&lt;h3&gt;☀️ Morning Routine&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Morning meditation&lt;/li&gt;
&lt;li&gt;[ ] Exercise&lt;/li&gt;
&lt;li&gt;[ ] Review daily goals&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;📋 Today's Tasks&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;📝 Notes&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;🌙 Evening Reflection&lt;/h3&gt;
&lt;p&gt;&lt;/p&gt;
`);
} 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 = `
&lt;h1&gt;📊 Note Statistics&lt;/h1&gt;
&lt;p&gt;Generated: ${api.dayjs().format('YYYY-MM-DD HH:mm:ss')}&lt;/p&gt;
&lt;h2&gt;Overview&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Total Notes: &lt;strong&gt;${stats.totalNotes}&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Note Types: ${Object.entries(stats.notesByType)
.map(([type, count]) => `${type} (${count})`)
.join(', ')}&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Largest Notes&lt;/h2&gt;
&lt;ol&gt;
${stats.largestNotes.map(n =>
`&lt;li&gt;&lt;a href="#root/${n.noteId}"&gt;${n.title}&lt;/a&gt; - ${(n.size / 1024).toFixed(1)} KB&lt;/li&gt;`
).join('')}
&lt;/ol&gt;
&lt;h2&gt;Top Tags&lt;/h2&gt;
&lt;div class="tag-cloud"&gt;
${Object.entries(stats.tagCloud)
.sort((a, b) => b[1] - a[1])
.slice(0, 20)
.map(([tag, count]) =>
`&lt;span style="font-size: ${Math.min(200, 100 + count * 5)}%"&gt;#${tag} (${count})&lt;/span&gt;`
).join(' ')}
&lt;/div&gt;
`;
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|\&lt;|$)/gi;
let match;
while ((match = checkboxRegex.exec(content)) !== null) {
const isCompleted = match[1] === 'x';
const taskText = match[2].replace(/&lt;[^&gt;]*&gt;/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 = `
&lt;h1&gt;📋 Task Dashboard&lt;/h1&gt;
&lt;p&gt;Last updated: ${api.dayjs().format('YYYY-MM-DD HH:mm:ss')}&lt;/p&gt;
&lt;h2&gt;⚠️ Overdue (${tasks.overdue.length})&lt;/h2&gt;
&lt;ul&gt;
${tasks.overdue.map(t =>
`&lt;li style="color: red;"&gt;
&lt;strong&gt;${t.text}&lt;/strong&gt;
(Due: ${api.dayjs(t.dueDate).format('MMM D')})
- &lt;a href="#root/${t.noteId}"&gt;${t.noteTitle}&lt;/a&gt;
&lt;/li&gt;`
).join('')}
&lt;/ul&gt;
&lt;h2&gt;📌 Pending (${tasks.pending.length})&lt;/h2&gt;
&lt;ul&gt;
${tasks.pending.slice(0, 20).map(t =>
`&lt;li&gt;
${t.text}
${t.dueDate ? `(Due: ${api.dayjs(t.dueDate).format('MMM D')})` : ''}
- &lt;a href="#root/${t.noteId}"&gt;${t.noteTitle}&lt;/a&gt;
&lt;/li&gt;`
).join('')}
&lt;/ul&gt;
${tasks.pending.length > 20 ? `&lt;p&gt;&lt;em&gt;...and ${tasks.pending.length - 20} more&lt;/em&gt;&lt;/p&gt;` : ''}
&lt;h2&gt;✅ Recently Completed (${tasks.completed.length})&lt;/h2&gt;
&lt;ul&gt;
${tasks.completed.slice(0, 10).map(t =>
`&lt;li style="text-decoration: line-through; opacity: 0.7;"&gt;
${t.text} - &lt;a href="#root/${t.noteId}"&gt;${t.noteTitle}&lt;/a&gt;
&lt;/li&gt;`
).join('')}
&lt;/ul&gt;
`;
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 = $(`
&lt;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;
"&gt;+&lt;/div&gt;
`);
// Create modal
const $modal = $(`
&lt;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;
"&gt;
&lt;h3&gt;Quick Note&lt;/h3&gt;
&lt;input type="text" id="quick-note-title" placeholder="Title..." style="
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
border-radius: 5px;
"&gt;
&lt;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;
"&gt;&lt;/textarea&gt;
&lt;div&gt;
&lt;button id="quick-note-save" style="
padding: 10px 20px;
background: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
"&gt;Save&lt;/button&gt;
&lt;button id="quick-note-cancel" style="
padding: 10px 20px;
background: #f44336;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
margin-left: 10px;
"&gt;Cancel&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
`);
// 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, `&lt;h2&gt;${title}&lt;/h2&gt;&lt;p&gt;${content}&lt;/p&gt;`]);
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 = $(`
&lt;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;
"&gt;
&lt;div id="preview-content"&gt;&lt;/div&gt;
&lt;/div&gt;
`);
// Create toggle button
const $toggleBtn = $(`
&lt;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;
"&gt;
&lt;i class="bx bx-show"&gt;&lt;/i&gt; Preview
&lt;/button&gt;
`);
// 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(/&lt;h1[^&gt;]*&gt;(.*?)&lt;\/h1&gt;/g, '# $1\n')
.replace(/&lt;h2[^&gt;]*&gt;(.*?)&lt;\/h2&gt;/g, '## $1\n')
.replace(/&lt;h3[^&gt;]*&gt;(.*?)&lt;\/h3&gt;/g, '### $1\n')
.replace(/&lt;p[^&gt;]*&gt;(.*?)&lt;\/p&gt;/g, '$1\n\n')
.replace(/&lt;strong[^&gt;]*&gt;(.*?)&lt;\/strong&gt;/g, '**$1**')
.replace(/&lt;em[^&gt;]*&gt;(.*?)&lt;\/em&gt;/g, '*$1*')
.replace(/&lt;code[^&gt;]*&gt;(.*?)&lt;\/code&gt;/g, '`$1`')
.replace(/&lt;ul[^&gt;]*&gt;/g, '')
.replace(/&lt;\/ul&gt;/g, '\n')
.replace(/&lt;li[^&gt;]*&gt;(.*?)&lt;\/li&gt;/g, '- $1\n')
.replace(/&lt;br[^&gt;]*&gt;/g, '\n')
.replace(/&lt;[^&gt;]+&gt;/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('&lt;i class="bx bx-hide"&gt;&lt;/i&gt; Hide');
updatePreview();
} else {
$previewPane.hide();
$('.note-detail-text .note-detail-editable').css('width', '100%');
$toggleBtn.html('&lt;i class="bx bx-show"&gt;&lt;/i&gt; 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, '&lt;h1&gt;$1&lt;/h1&gt;')
.replace(/^## (.+)$/gm, '&lt;h2&gt;$1&lt;/h2&gt;')
.replace(/^### (.+)$/gm, '&lt;h3&gt;$1&lt;/h3&gt;')
.replace(/\*\*(.*?)\*\*/g, '&lt;strong&gt;$1&lt;/strong&gt;')
.replace(/\*(.*?)\*/g, '&lt;em&gt;$1&lt;/em&gt;')
.replace(/`(.*?)`/g, '&lt;code&gt;$1&lt;/code&gt;')
.replace(/^- (.+)$/gm, '&lt;li&gt;$1&lt;/li&gt;')
.replace(/(&lt;li&gt;.*&lt;\/li&gt;)/s, '&lt;ul&gt;$1&lt;/ul&gt;')
.replace(/\n\n/g, '&lt;/p&gt;&lt;p&gt;')
.replace(/^(?!&lt;[h|u])(.+)$/gm, '&lt;p&gt;$1&lt;/p&gt;');
$('#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 = `
&lt;h1&gt;${issue.title}&lt;/h1&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;Issue #&lt;/th&gt;&lt;td&gt;${issue.number}&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;th&gt;State&lt;/th&gt;&lt;td&gt;${issue.state}&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;th&gt;Author&lt;/th&gt;&lt;td&gt;${issue.user.login}&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;th&gt;Created&lt;/th&gt;&lt;td&gt;${api.dayjs(issue.created_at).format('YYYY-MM-DD HH:mm')}&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;th&gt;Labels&lt;/th&gt;&lt;td&gt;${issue.labels.map(l => l.name).join(', ')}&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;h2&gt;Description&lt;/h2&gt;
&lt;div style="background: #f5f5f5; padding: 10px; border-radius: 5px;"&gt;
${issue.body || 'No description'}
&lt;/div&gt;
&lt;h2&gt;Links&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="${issue.html_url}"&gt;View on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="${issue.url}"&gt;API URL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
`;
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')}`,
`
&lt;h1&gt;Script Error&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Error:&lt;/strong&gt; ${error.message}&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;${error.stack}&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Script:&lt;/strong&gt; ${api.currentNote.title}&lt;/p&gt;
`
);
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 &lt; 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>

File diff suppressed because it is too large Load Diff

View 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 &lt;project&gt;: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>

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

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

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

View 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&lt;string, BNote&gt;;
branches: Record&lt;string, BBranch&gt;;
childParentToBranch: Record&lt;string, BBranch&gt;;
attributes: Record&lt;string, BAttribute&gt;;
attributeIndex: Record&lt;string, BAttribute[]&gt;;
options: Record&lt;string, BOption&gt;;
etapiTokens: Record&lt;string, BEtapiToken&gt;;
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&lt;string, FNote&gt;;
branches: Record&lt;string, FBranch&gt;;
attributes: Record&lt;string, FAttribute&gt;;
attachments: Record&lt;string, FAttachment&gt;;
blobPromises: Record&lt;string, Promise&lt;FBlob | null&gt; | null&gt;;
}</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&lt;string, SNote&gt;;
branches: Record&lt;string, SBranch&gt;;
childParentToBranch: Record&lt;string, SBranch&gt;;
attributes: Record&lt;string, SAttribute&gt;;
attachments: Record&lt;string, SAttachment&gt;;
aliasToNote: Record&lt;string, SNote&gt;;
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>

View 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 = $('&lt;div&gt;')
.addClass('my-widget')
.append($('&lt;h3&gt;').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 $('&lt;div class="info-widget"&gt;')
.append($('&lt;div class="created"&gt;'))
.append($('&lt;div class="modified"&gt;'))
.append($('&lt;div class="word-count"&gt;'));
}
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 = $('&lt;div class="task-list-widget"&gt;');
this.$list = $('&lt;ul&gt;').appendTo(this.$widget);
return this.$widget;
}
async refreshWithNote(note) {
const tasks = await this.loadTasks(note);
this.$list.empty();
for (const task of tasks) {
$('&lt;li&gt;')
.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(`
&lt;div class="alert alert-danger"&gt;
${message}
&lt;/div&gt;
`);
}
}</code></pre>
</body>
</html>

View File

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

View File

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

View File

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

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

View File

@@ -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.&nbsp;</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"

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

View 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>&lt; 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>&gt; 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 (&gt; 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>

View 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>&lt; 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>&gt; 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>

View 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&lt;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 &lt; 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 &lt;= TODAY+7 #dueDate >= TODAY</code></pre>
<p>Finds tasks due in the next week.</p>
<pre><code>note.dateCreated >= MONTH-2 note.dateCreated &lt; 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 &lt;= TODAY AND #endDate >= TODAY) OR #status=ongoing</code></pre>
<p>Finds current events or ongoing items.</p>
<pre><code>#reminderDate &lt;= 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 &lt;= 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 &lt; 100 AND
note.dateModified &lt; 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>

View 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 &lt; TODAY
#orderBy=dateCreated
#orderDirection=desc</code></pre>
<h4>Monthly Review Collection</h4>
<pre><code>#searchString=#reviewed=false note.dateCreated >= MONTH note.dateCreated &lt; MONTH+1
#orderBy=dateCreated</code></pre>
<h4>Upcoming Deadlines</h4>
<pre><code>#searchString=#dueDate >= TODAY #dueDate &lt;= 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 &lt; 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 &lt; TODAY-7 #status!=completed
`);
// Find projects with no recent activity
const staleProjects = api.searchForNotes(`
#project #status=active note.dateModified &lt; TODAY-30
`);
// Find notes with many attributes but no content
const overlabeledNotes = api.searchForNotes(`
note.attributeCount > 5 note.contentSize &lt; 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 &lt;= 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 &lt; "${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>

View 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>&gt;</code>, <code>&gt;=</code>, <code>&lt;</code>, <code>&lt;=</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 &lt;= 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>

View 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 &lt;= 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 &lt; 50 note.dateModified &lt; 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 &lt; 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 &lt;= 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 &lt;= TODAY-90 #status!=completed</code></pre>
<p>Find projects that started over 90 days ago but aren't completed.</p>
<pre><code>#milestone #targetDate &lt;= 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 &lt; 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 &lt; 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 &lt;= TODAY+60 #renewalDate >= TODAY</code></pre>
<p>Find contracts expiring in the next 60 days.</p>
<pre><code>#invoice #status=unpaid #dueDate &lt; 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 &lt; 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 &lt;= 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 &lt; 70 #course=mathematics</code></pre>
<p>Find students struggling in mathematics.</p>
<pre><code>#syllabus #course #lastUpdated &lt; 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 &lt;= 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 &lt;= 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 &lt;= 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 &lt;= TODAY+90</code></pre>
<p>Find deprecated API endpoints scheduled for removal soon.</p>
<pre><code>#architecture #component=database #lastReviewed &lt; 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 &lt;= 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 &lt; TODAY-10</code></pre>
<p>Find weekly reports that haven't been generated in 10 days.</p>
<pre><code>#dashboard #stakeholder=executive #lastUpdated &lt; 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 &lt; 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 &lt; 100 note.dateModified &lt; TODAY-30
# Monthly project review
#project #status=active note.dateModified &lt; TODAY-30
# Quarterly archive review
note.isArchived=false note.dateModified &lt; 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>

View 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>&gt;</code> - Greater than</li>
<li><code>&gt;=</code> - Greater than or equal</li>
<li><code>&lt;</code> - Less than</li>
<li><code>&lt;=</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>

View 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 &lt; 5 high-quality matches
if (highQualityResults.length &lt; 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) &lt; 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 &lt;= str1.length; i++) {
currentRow[0] = i;
let minInRow = i;
for (let j = 1; j &lt;= 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) &lt;= 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) &lt;= 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(/&nbsp;/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>

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

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

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

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

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

View File

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

View File

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

View File

@@ -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 });

View File

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

View File

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

View File

@@ -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");

View File

@@ -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(":");

View File

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

View File

@@ -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();

View File

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

View File

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

View File

@@ -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 = "";
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
});
}

View File

@@ -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`);
}

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View 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/)

View 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

View 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

View File

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

View File

@@ -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>&nbsp;</td><td>The HTML of the note.</td><td>&nbsp;</td></tr><tr><th><a href="https://github.com/zadam/trilium/wiki/Relation-map">Relation Map&nbsp;</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>&nbsp;</td><td>The HTML of the note.</td><td>&nbsp;</td></tr><tr><th><a href="https://github.com/TriliumNext/Trilium/wiki/Relation-map">Relation Map&nbsp;</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": {},

View File

@@ -29,5 +29,5 @@ module.exports = new MyWidget();
Reference:
* [https://trilium.rocks/X7pxYpiu0lgU](https://trilium.rocks/X7pxYpiu0lgU)
* [https://github.com/zadam/trilium/wiki/Widget-Basics](https://github.com/zadam/trilium/wiki/Widget-Basics)
* [https://github.com/zadam/trilium/wiki/Frontend-Basics](https://github.com/zadam/trilium/wiki/Frontend-Basics)
* [https://github.com/TriliumNext/Trilium/wiki/Widget-Basics](https://github.com/TriliumNext/Trilium/wiki/Widget-Basics)
* [https://github.com/TriliumNext/Trilium/wiki/Frontend-Basics](https://github.com/TriliumNext/Trilium/wiki/Frontend-Basics)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,737 @@
# Custom Widget Development Guide
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.
## Prerequisites
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
## Understanding Widget Architecture
### Widget Hierarchy
Trilium's widget system follows a hierarchical structure:
```
Component (base class)
└── BasicWidget
├── NoteContextAwareWidget
│ ├── TypeWidget (for note type widgets)
│ └── RightPanelWidget
└── Custom widgets (buttons, containers, etc.)
```
### Core Widget Classes
#### BasicWidget
The foundation class for all widgets. Provides basic rendering, positioning, and visibility management.
```typescript
import BasicWidget from "../widgets/basic_widget.js";
class MyCustomWidget extends BasicWidget {
doRender() {
this.$widget = $('<div class="my-widget">Hello Widget</div>');
}
}
```
#### NoteContextAwareWidget
Extends BasicWidget to respond to note changes. Use this when your widget needs to update based on the active note.
```typescript
import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js";
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 = $(`
<div class="note-info-widget">
<div class="note-title"></div>
<div class="note-type"></div>
</div>
`);
}
}
```
#### RightPanelWidget
Specialized widget for rendering panels in the right sidebar with a consistent card layout.
```typescript
import RightPanelWidget from "../widgets/right_panel_widget.js";
class StatisticsWidget extends RightPanelWidget {
get widgetTitle() {
return "Note Statistics";
}
async doRenderBody() {
this.$body.html(`
<div class="stats-container">
<div class="word-count">Words: <span>0</span></div>
<div class="char-count">Characters: <span>0</span></div>
</div>
`);
}
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);
}
}
```
## Widget Lifecycle
### Initialization Phase
1. **Constructor**: Set up initial state and child widgets
2. **render()**: Called to create the widget's DOM structure
3. **doRender()**: Override this to create your widget's HTML
### Update Phase
1. **refresh()**: Called when widget needs updating
2. **refreshWithNote()**: Called for NoteContextAwareWidget when note changes
3. **Event handlers**: Respond to various Trilium events
### Cleanup Phase
1. **cleanup()**: Override to clean up resources, event listeners, etc.
2. **remove()**: Removes widget from DOM
## Event Handling
### Subscribing to Events
Widgets can listen to Trilium's event system:
```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();
}
}
```
### Common Events
- `noteSwitched`: Active note changed
- `activeContextChanged`: Active tab/context changed
- `entitiesReloaded`: Notes, branches, or attributes reloaded
- `noteContentChanged`: Note content modified
- `noteTypeMimeChanged`: Note type or MIME changed
- `frocaReloaded`: Frontend cache reloaded
## State Management
### Local State
Store widget-specific state in instance properties:
```typescript
class StatefulWidget extends BasicWidget {
constructor() {
super();
this.isExpanded = false;
this.cachedData = null;
}
toggleExpanded() {
this.isExpanded = !this.isExpanded;
this.$widget.toggleClass('expanded', this.isExpanded);
}
}
```
### Persistent State
Use options or attributes for persistent state:
```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) : {};
}
}
```
## Accessing Trilium APIs
### Frontend Services
```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";
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?");
}
}
```
### Server Communication
```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(`notes/${this.noteId}`, {
title: 'Updated Title'
});
}
}
```
## Styling Widgets
### Inline Styles
```typescript
class StyledWidget extends BasicWidget {
doRender() {
this.$widget = $('<div>');
this.css('padding', '10px')
.css('background-color', '#f0f0f0')
.css('border-radius', '4px');
}
}
```
### CSS Classes
```typescript
class ClassedWidget extends BasicWidget {
doRender() {
this.$widget = $('<div>');
this.class('custom-widget')
.class('bordered');
}
}
```
### CSS Blocks
```typescript
class CSSBlockWidget extends BasicWidget {
doRender() {
this.$widget = $('<div class="my-widget">Content</div>');
this.cssBlock(`
.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);
}
`);
}
}
```
## Performance Optimization
### Lazy Loading
```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();
}
}
```
### Debouncing Updates
```typescript
import SpacedUpdate from "../services/spaced_update.js";
class DebouncedWidget extends NoteContextAwareWidget {
constructor() {
super();
this.spacedUpdate = new SpacedUpdate(async () => {
await this.performUpdate();
}, 500); // 500ms delay
}
async handleInput(value) {
await this.spacedUpdate.scheduleUpdate();
}
}
```
### Caching
```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();
}
}
```
## Debugging Widgets
### Console Logging
```typescript
class DebugWidget extends BasicWidget {
doRender() {
console.log('Widget rendering', this.componentId);
console.time('render');
this.$widget = $('<div>');
console.timeEnd('render');
}
}
```
### Error Handling
```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>');
}
}
}
```
### Development Tools
```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(`
<div class="debug-info">
Component ID: ${this.componentId}
Position: ${this.position}
</div>
`);
}
}
}
```
## Complete Example: Note Statistics Widget
Here's a complete example implementing a custom note statistics widget:
```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";
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(`
<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>
`);
this.cssBlock(`
.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;
}
`);
// 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(`${this.statistics.readingTime} min`);
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',
`Words,${this.statistics.words}`,
`Characters,${this.statistics.characters}`,
`Paragraphs,${this.statistics.paragraphs}`,
`Reading Time,${this.statistics.readingTime} minutes`,
`Links,${this.statistics.links}`,
`Images,${this.statistics.images}`
].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 = `statistics-${note.noteId}.csv`;
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;
}
}
export default NoteStatisticsWidget;
```
## Best Practices
### 1. Memory Management
- Clean up event listeners in `cleanup()`
- Clear caches and timers when widget is destroyed
- Avoid circular references
### 2. Performance
- Use debouncing for frequent updates
- Implement lazy loading for expensive operations
- Cache computed values when appropriate
### 3. Error Handling
- Always wrap async operations in try-catch
- Provide user feedback for errors
- Log errors for debugging
### 4. User Experience
- Show loading states for async operations
- Provide clear error messages
- Ensure widgets are responsive
### 5. Code Organization
- Keep widgets focused on a single responsibility
- Extract reusable logic into services
- Use composition over inheritance when possible
## Troubleshooting
### Widget Not Rendering
- Check `doRender()` creates `this.$widget`
- Verify widget is properly registered
- Check console for errors
### Events Not Firing
- Ensure event method name matches pattern: `${eventName}Event`
- Check event is being triggered
- Verify widget is active/visible
### State Not Persisting
- Use options or attributes for persistence
- Check save operations complete successfully
- Verify data serialization
### Performance Issues
- Profile with browser dev tools
- Implement caching and debouncing
- Optimize DOM operations
## Next Steps
- Explore existing widgets in `/apps/client/src/widgets/` for examples
- Review the Frontend Script API documentation
- Join the Trilium community for support and sharing widgets

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