mirror of
https://github.com/zadam/trilium.git
synced 2025-10-26 15:56:29 +01:00
Compare commits
4 Commits
feat/ui-op
...
feat/impro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce298e477b | ||
|
|
81c0e508ac | ||
|
|
065740eabc | ||
|
|
4ce9102f93 |
@@ -54,7 +54,7 @@ The original Trilium developer ([Zadam](https://github.com/zadam)) has graciousl
|
||||
|
||||
There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Trilium instance. Simply [install TriliumNext/Trilium](#-installation) as usual and it will use your existing database.
|
||||
|
||||
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext/Trilium have their sync versions incremented which prevents direct migration.
|
||||
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are compatible with the latest TriliumNext/Trilium version of [v0.63.7](https://github.com/TriliumNext/Trilium/releases/tag/v0.63.7). Any later versions of TriliumNext/Trilium have their sync versions incremented which prevents direct migration.
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ fi
|
||||
VERSION=$1
|
||||
SERIES=${VERSION:0:4}-latest
|
||||
|
||||
docker push zadam/trilium:$VERSION
|
||||
docker push zadam/trilium:$SERIES
|
||||
docker push TriliumNext/Trilium:$VERSION
|
||||
docker push TriliumNext/Trilium:$SERIES
|
||||
|
||||
if [[ $1 != *"beta"* ]]; then
|
||||
docker push zadam/trilium:latest
|
||||
docker push TriliumNext/Trilium:latest
|
||||
fi
|
||||
|
||||
@@ -834,7 +834,7 @@ class FNote {
|
||||
if (a.noteId === b.noteId) {
|
||||
return a.position < b.position ? -1 : 1;
|
||||
} else {
|
||||
// inherited promoted attributes should stay grouped: https://github.com/zadam/trilium/issues/3761
|
||||
// inherited promoted attributes should stay grouped: https://github.com/TriliumNext/Trilium/issues/3761
|
||||
return a.noteId < b.noteId ? -1 : 1;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@ export class WidgetsByParent {
|
||||
this.byParent[parentName]
|
||||
// previously, custom widgets were provided as a single instance, but that has the disadvantage
|
||||
// for splits where we actually need multiple instaces and thus having a class to instantiate is better
|
||||
// https://github.com/zadam/trilium/issues/4274
|
||||
// https://github.com/TriliumNext/Trilium/issues/4274
|
||||
.map((w: any) => (w.prototype ? new w() : w))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ async function copy(branchIds: string[]) {
|
||||
clipboardMode = "copy";
|
||||
|
||||
if (utils.isElectron()) {
|
||||
// https://github.com/zadam/trilium/issues/2401
|
||||
// https://github.com/TriliumNext/Trilium/issues/2401
|
||||
const { clipboard } = require("electron");
|
||||
const links: string[] = [];
|
||||
|
||||
|
||||
@@ -507,7 +507,7 @@ $(document).on("dblclick", "a", (e) => {
|
||||
$(document).on("mousedown", "a", (e) => {
|
||||
if (e.which === 2) {
|
||||
// prevent paste on middle click
|
||||
// https://github.com/zadam/trilium/issues/2995
|
||||
// https://github.com/TriliumNext/Trilium/issues/2995
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#preventing_default_actions
|
||||
e.preventDefault();
|
||||
return false;
|
||||
|
||||
@@ -99,7 +99,7 @@ async function mouseEnterHandler(this: HTMLElement) {
|
||||
if ($link.filter(":hover").length > 0) {
|
||||
$link.tooltip({
|
||||
container: "body",
|
||||
// https://github.com/zadam/trilium/issues/2794 https://github.com/zadam/trilium/issues/2988
|
||||
// https://github.com/TriliumNext/Trilium/issues/2794 https://github.com/TriliumNext/Trilium/issues/2988
|
||||
// with bottom this flickering happens a bit less
|
||||
placement: "bottom",
|
||||
trigger: "manual",
|
||||
|
||||
@@ -79,7 +79,7 @@ body {
|
||||
height: unset !important;
|
||||
overflow: visible;
|
||||
position: unset;
|
||||
/* https://github.com/zadam/trilium/issues/3202 */
|
||||
/* https://github.com/TriliumNext/Trilium/issues/3202 */
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
}
|
||||
|
||||
html {
|
||||
/* this fixes FF filter vs. position fixed bug: https://github.com/zadam/trilium/issues/233 */
|
||||
/* this fixes FF filter vs. position fixed bug: https://github.com/TriliumNext/Trilium/issues/233 */
|
||||
height: 100vh;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
@@ -543,7 +543,7 @@ button.btn-sm {
|
||||
transform: translateX(7px);
|
||||
color: var(--muted-text-color);
|
||||
background-color: var(--main-background-color);
|
||||
/* Making this narrower because https://github.com/zadam/trilium/issues/502 (problem only in smaller font sizes) */
|
||||
/* Making this narrower because https://github.com/TriliumNext/Trilium/issues/502 (problem only in smaller font sizes) */
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
z-index: 1000;
|
||||
@@ -1117,7 +1117,7 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--accented-background-color);
|
||||
display: flex; /* see https://github.com/zadam/trilium/issues/1590 */
|
||||
display: flex; /* see https://github.com/TriliumNext/Trilium/issues/1590 */
|
||||
}
|
||||
|
||||
.include-note.ck-placeholder::before {
|
||||
@@ -1251,7 +1251,7 @@ body.desktop li.dropdown-submenu:hover > ul.dropdown-menu {
|
||||
left: calc(100% - 2px); /* -2px, otherwise there's a small gap between menu and submenu where the hover can disappear */
|
||||
margin-top: -10px;
|
||||
min-width: 15rem;
|
||||
/* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */
|
||||
/* to make submenu scrollable https://github.com/TriliumNext/Trilium/issues/3136 */
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
this.$inputName = this.$widget.find(".attr-input-name");
|
||||
this.$inputName.on("input", (ev) => {
|
||||
if (!(ev.originalEvent as KeyboardEvent)?.isComposing) {
|
||||
// https://github.com/zadam/trilium/pull/3812
|
||||
// https://github.com/TriliumNext/Trilium/pull/3812
|
||||
this.userEditedAttribute();
|
||||
}
|
||||
});
|
||||
@@ -383,7 +383,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
this.$inputValue = this.$widget.find(".attr-input-value");
|
||||
this.$inputValue.on("input", (ev) => {
|
||||
if (!(ev.originalEvent as KeyboardEvent)?.isComposing) {
|
||||
// https://github.com/zadam/trilium/pull/3812
|
||||
// https://github.com/TriliumNext/Trilium/pull/3812
|
||||
this.userEditedAttribute();
|
||||
}
|
||||
});
|
||||
@@ -421,7 +421,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
this.$inputInverseRelation = this.$widget.find(".attr-input-inverse-relation");
|
||||
this.$inputInverseRelation.on("input", (ev) => {
|
||||
if (!(ev.originalEvent as KeyboardEvent)?.isComposing) {
|
||||
// https://github.com/zadam/trilium/pull/3812
|
||||
// https://github.com/TriliumNext/Trilium/pull/3812
|
||||
this.userEditedAttribute();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -173,7 +173,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
|
||||
this.attributeDetailWidget.hide();
|
||||
});
|
||||
|
||||
this.$editor.on("blur", () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/zadam/trilium/issues/4160
|
||||
this.$editor.on("blur", () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/TriliumNext/Trilium/issues/4160
|
||||
|
||||
this.$addNewAttributeButton = this.$widget.find(".add-new-attribute-button");
|
||||
this.$addNewAttributeButton.on("click", (e) => this.addNewAttribute(e));
|
||||
@@ -282,7 +282,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
|
||||
|
||||
async save() {
|
||||
if (this.lastUpdatedNoteId !== this.noteId) {
|
||||
// https://github.com/zadam/trilium/issues/3090
|
||||
// https://github.com/TriliumNext/Trilium/issues/3090
|
||||
console.warn("Ignoring blur event because a different note is loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
* sets full height of container that contains note content for a subset of note-types
|
||||
*/
|
||||
checkFullHeight() {
|
||||
// https://github.com/zadam/trilium/issues/2522
|
||||
// https://github.com/TriliumNext/Trilium/issues/2522
|
||||
const isBackendNote = this.noteContext?.noteId === "_backendLog";
|
||||
const isSqlNote = this.mime === "text/x-sqlite;schema=trilium";
|
||||
const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "mermaid", "file"].includes(this.type ?? "");
|
||||
|
||||
@@ -81,7 +81,7 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
);
|
||||
|
||||
// there seems to be a race condition on Firefox which triggers the observer only before the widget is visible
|
||||
// (intersection is false). https://github.com/zadam/trilium/issues/4165
|
||||
// (intersection is false). https://github.com/TriliumNext/Trilium/issues/4165
|
||||
setTimeout(() => observer.observe(this.$widget[0]), 10);
|
||||
}
|
||||
|
||||
|
||||
@@ -312,7 +312,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
setupNoteTitleTooltip() {
|
||||
// the following will dynamically set tree item's tooltip if the whole item's text is not currently visible
|
||||
// if the whole text is visible then no tooltip is show since that's unnecessarily distracting
|
||||
// see https://github.com/zadam/trilium/pull/1120 for discussion
|
||||
// see https://github.com/TriliumNext/Trilium/pull/1120 for discussion
|
||||
|
||||
// code inspired by https://gist.github.com/jtsternberg/c272d7de5b967cec2d3d
|
||||
const isEnclosing = ($container: JQuery<HTMLElement>, $sub: JQuery<HTMLElement>) => {
|
||||
@@ -952,7 +952,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
|
||||
await this.filterHoistedBranch(true);
|
||||
|
||||
// don't activate the active note, see discussion in https://github.com/zadam/trilium/issues/3664
|
||||
// don't activate the active note, see discussion in https://github.com/TriliumNext/Trilium/issues/3664
|
||||
}
|
||||
|
||||
async expandTree(node: Fancytree.FancytreeNode | null = null) {
|
||||
@@ -1181,7 +1181,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
/*
|
||||
* We're collapsing notes after a period of inactivity to "cleanup" the tree - users rarely
|
||||
* collapse the notes and the tree becomes unusuably large.
|
||||
* Some context: https://github.com/zadam/trilium/issues/1192
|
||||
* Some context: https://github.com/TriliumNext/Trilium/issues/1192
|
||||
*/
|
||||
|
||||
const noteIdsToKeepExpanded = new Set(
|
||||
@@ -1429,7 +1429,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
if (activeNodeFocused) {
|
||||
// needed by Firefox: https://github.com/zadam/trilium/issues/1865
|
||||
// needed by Firefox: https://github.com/TriliumNext/Trilium/issues/1865
|
||||
this.tree.$container.focus();
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
||||
if (a.noteId === b.noteId) {
|
||||
return a.position - b.position;
|
||||
} else {
|
||||
// inherited attributes should stay grouped: https://github.com/zadam/trilium/issues/3761
|
||||
// inherited attributes should stay grouped: https://github.com/TriliumNext/Trilium/issues/3761
|
||||
return a.noteId < b.noteId ? -1 : 1;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ const TPL = /*html*/`
|
||||
<div class="promoted-attributes-widget">
|
||||
<style>
|
||||
body.mobile .promoted-attributes-widget {
|
||||
/* https://github.com/zadam/trilium/issues/4468 */
|
||||
/* https://github.com/TriliumNext/Trilium/issues/4468 */
|
||||
flex-shrink: 0.4;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default class SharedInfoWidget extends NoteContextAwareWidget {
|
||||
let host = location.host;
|
||||
if (host.endsWith("/")) {
|
||||
// seems like IE has trailing slash
|
||||
// https://github.com/zadam/trilium/issues/3782
|
||||
// https://github.com/TriliumNext/Trilium/issues/3782
|
||||
host = host.substr(0, host.length - 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -360,7 +360,7 @@ export default class TocWidget extends RightPanelWidget {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce indent if a larger headings are not being used: https://github.com/zadam/trilium/issues/4363
|
||||
* Reduce indent if a larger headings are not being used: https://github.com/TriliumNext/Trilium/issues/4363
|
||||
*/
|
||||
pullLeft($toc: JQuery<HTMLElement>) {
|
||||
while (true) {
|
||||
@@ -390,7 +390,7 @@ export default class TocWidget extends RightPanelWidget {
|
||||
// temporarily" (ie "edit this note" button) without any
|
||||
// intervening events, do the readonly calculation at navigation
|
||||
// time and not at outline creation time
|
||||
// See https://github.com/zadam/trilium/issues/2828
|
||||
// See https://github.com/TriliumNext/Trilium/issues/2828
|
||||
const isDocNote = this.note.type === "doc";
|
||||
const isReadOnly = await this.noteContext.isReadOnly();
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const TPL = /*html*/`
|
||||
}
|
||||
|
||||
/* Conflict between excalidraw and bootstrap classes keeps the menu hidden */
|
||||
/* https://github.com/zadam/trilium/issues/3780 */
|
||||
/* https://github.com/TriliumNext/Trilium/issues/3780 */
|
||||
/* https://github.com/excalidraw/excalidraw/issues/6567 */
|
||||
.excalidraw .dropdown-menu {
|
||||
display: block;
|
||||
|
||||
@@ -99,7 +99,7 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
|
||||
async doRefresh(note: FNote) {
|
||||
// we load CKEditor also for read only notes because they contain content styles required for correct rendering of even read only notes
|
||||
// we could load just ckeditor-content.css but that causes CSS conflicts when both build CSS and this content CSS is loaded at the same time
|
||||
// (see https://github.com/zadam/trilium/issues/1590 for example of such conflict)
|
||||
// (see https://github.com/TriliumNext/Trilium/issues/1590 for example of such conflict)
|
||||
await import("@triliumnext/ckeditor5");
|
||||
|
||||
this.onLanguageChanged();
|
||||
|
||||
@@ -22,7 +22,7 @@ async function main() {
|
||||
electronDebug();
|
||||
electronDl({ saveAs: true });
|
||||
|
||||
// needed for excalidraw export https://github.com/zadam/trilium/issues/4271
|
||||
// needed for excalidraw export https://github.com/TriliumNext/Trilium/issues/4271
|
||||
electron.app.commandLine.appendSwitch("enable-experimental-web-platform-features");
|
||||
electron.app.commandLine.appendSwitch("lang", options.getOptionOrNull("formattingLocale") ?? "en");
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ function decrypt(key: any, cipherText: any) {
|
||||
|
||||
try {
|
||||
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64");
|
||||
// old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/trilium/issues/3017
|
||||
// old encrypted data can have IV of length 13, see some details here: https://github.com/TriliumNext/Trilium/issues/3017
|
||||
const ivLength = cipherTextBufferWithIv.length % 16 === 0 ? 16 : 13;
|
||||
const iv = cipherTextBufferWithIv.slice(0, ivLength);
|
||||
|
||||
@@ -48,7 +48,7 @@ function decrypt(key: any, cipherText: any) {
|
||||
|
||||
return payload;
|
||||
} catch (e: any) {
|
||||
// recovery from https://github.com/zadam/trilium/issues/510
|
||||
// recovery from https://github.com/TriliumNext/Trilium/issues/510
|
||||
if (e.message?.includes("WRONG_FINAL_BLOCK_LENGTH") || e.message?.includes("wrong final block length")) {
|
||||
console.log("Caught WRONG_FINAL_BLOCK_LENGTH, returning cipherText instead");
|
||||
return cipherText;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<h1 data-trilium-h1>Journal</h1>
|
||||
|
||||
<div class="ck-content">
|
||||
<p>You can read some explanation on how this journal works here: <a href="https://github.com/zadam/trilium/wiki/Day-notes">https://github.com/zadam/trilium/wiki/Day-notes</a>
|
||||
<p>You can read some explanation on how this journal works here: <a href="https://github.com/TriliumNext/Trilium/wiki/Day-notes">https://github.com/TriliumNext/Trilium/wiki/Day-notes</a>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ return api.res.send(404);
|
||||
* To test this, execute the following curl request: curl -X POST http://localhost:37740/custom/create-note -H "Content-Type: application/json" -d "{ \"secret\": \"secret-password\", \"title\": \"hello\", \"content\": \"world\" }"
|
||||
* (host and port might have to be adjusted based on your setup)
|
||||
*
|
||||
* See https://github.com/zadam/trilium/wiki/Custom-request-handler for details.
|
||||
* See https://github.com/TriliumNext/Trilium/wiki/Custom-request-handler for details.
|
||||
*/
|
||||
|
||||
const {req, res} = api;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<div class="ck-content">
|
||||
<p>This is a simple TODO/Task manager. You can see some description and explanation
|
||||
here: <a href="https://github.com/zadam/trilium/wiki/Task-manager">https://github.com/zadam/trilium/wiki/Task-manager</a>
|
||||
here: <a href="https://github.com/TriliumNext/Trilium/wiki/Task-manager">https://github.com/TriliumNext/Trilium/wiki/Task-manager</a>
|
||||
|
||||
</p>
|
||||
<p>Please note that this is meant as scripting example only and feature/bug
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div style="padding: 20px">
|
||||
<strong>See explanation <a href="https://github.com/zadam/trilium/wiki/Weight-tracker" target="_blank">here</a></strong>.
|
||||
<strong>See explanation <a href="https://github.com/TriliumNext/Trilium/wiki/Weight-tracker" target="_blank">here</a></strong>.
|
||||
|
||||
<canvas></canvas>
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
* This is a demo of how you can create custom theme for Trilium. You can activate it by going
|
||||
* into options in first tab "Appearance".
|
||||
*
|
||||
* You can read some details on theming here: http://github.com/zadam/trilium/wiki/Themes
|
||||
* You can read some details on theming here: http://github.com/TriliumNext/Trilium/wiki/Themes
|
||||
*/
|
||||
|
||||
@font-face { /* This will be used as main UI font (see below) */
|
||||
|
||||
1269
apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/API Client Libraries.html
vendored
Normal file
1269
apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/API Client Libraries.html
vendored
Normal file
File diff suppressed because it is too large
Load Diff
720
apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/ETAPI Complete Guide.html
vendored
Normal file
720
apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/ETAPI Complete Guide.html
vendored
Normal file
@@ -0,0 +1,720 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ETAPI Complete Guide</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { color: #2c3e50; border-bottom: 3px solid #e67e22; padding-bottom: 10px; }
|
||||
h2 { color: #34495e; margin-top: 40px; border-bottom: 2px solid #ecf0f1; padding-bottom: 5px; }
|
||||
h3 { color: #7f8c8d; margin-top: 30px; }
|
||||
h4 { color: #95a5a6; margin-top: 25px; }
|
||||
pre {
|
||||
background: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
border-left: 4px solid #e67e22;
|
||||
}
|
||||
code {
|
||||
background: #f4f4f4;
|
||||
padding: 3px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', Monaco, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
.endpoint-box {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #e67e22;
|
||||
}
|
||||
.method-get { border-left-color: #28a745; }
|
||||
.method-post { border-left-color: #007bff; }
|
||||
.method-put { border-left-color: #ffc107; }
|
||||
.method-patch { border-left-color: #fd7e14; }
|
||||
.method-delete { border-left-color: #dc3545; }
|
||||
.example-box {
|
||||
background: #f1f3f4;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: #34495e;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
tr:nth-child(even) { background: #f8f9fa; }
|
||||
.warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.info {
|
||||
background: #d1ecf1;
|
||||
border: 1px solid #74c0fc;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.success {
|
||||
background: #d4edda;
|
||||
border: 1px solid #51cf66;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.toc {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.toc ul {
|
||||
list-style-type: none;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.toc a {
|
||||
text-decoration: none;
|
||||
color: #495057;
|
||||
}
|
||||
.toc a:hover {
|
||||
color: #e67e22;
|
||||
}
|
||||
.auth-example {
|
||||
background: #e8f5e8;
|
||||
border-left: 4px solid #28a745;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ETAPI Complete Guide</h1>
|
||||
|
||||
<div class="toc">
|
||||
<h2>Table of Contents</h2>
|
||||
<ul>
|
||||
<li><a href="#introduction">Introduction</a></li>
|
||||
<li><a href="#authentication-setup">Authentication Setup</a></li>
|
||||
<li><a href="#api-endpoints">API Endpoints</a></li>
|
||||
<li><a href="#common-use-cases">Common Use Cases</a></li>
|
||||
<li><a href="#client-library-examples">Client Library Examples</a></li>
|
||||
<li><a href="#rate-limiting-and-best-practices">Rate Limiting and Best Practices</a></li>
|
||||
<li><a href="#migration-from-internal-api">Migration from Internal API</a></li>
|
||||
<li><a href="#error-handling">Error Handling</a></li>
|
||||
<li><a href="#performance-considerations">Performance Considerations</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 id="introduction">Introduction</h2>
|
||||
|
||||
<p>ETAPI (External Trilium API) is the recommended REST API for external integrations with Trilium Notes. It provides a secure, stable interface for programmatic access to notes, attributes, branches, and attachments.</p>
|
||||
|
||||
<div class="success">
|
||||
<h4>Key Features</h4>
|
||||
<ul>
|
||||
<li>RESTful design with predictable endpoints</li>
|
||||
<li>Token-based authentication</li>
|
||||
<li>Comprehensive CRUD operations</li>
|
||||
<li>Search functionality</li>
|
||||
<li>Import/export capabilities</li>
|
||||
<li>Calendar and special note access</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<strong>Base URL:</strong> <code>http://localhost:8080/etapi</code>
|
||||
</div>
|
||||
|
||||
<h2 id="authentication-setup">Authentication Setup</h2>
|
||||
|
||||
<h3>Method 1: Token Authentication</h3>
|
||||
|
||||
<div class="auth-example">
|
||||
<h4>Step 1: Generate ETAPI Token</h4>
|
||||
<ol>
|
||||
<li>Open Trilium Notes</li>
|
||||
<li>Navigate to <strong>Options</strong> → <strong>ETAPI</strong></li>
|
||||
<li>Click "Create new ETAPI token"</li>
|
||||
<li>Copy the generated token</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<h4>Step 2: Use Token in Requests</h4>
|
||||
|
||||
<div class="endpoint-box">
|
||||
<strong>HTTP Header:</strong>
|
||||
<pre><code>Authorization: <your-token></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<strong>cURL Example:</strong>
|
||||
<pre><code>curl -X GET http://localhost:8080/etapi/notes/root \
|
||||
-H "Authorization: myEtapiToken123"</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<strong>Python Example:</strong>
|
||||
<pre><code>import requests
|
||||
|
||||
headers = {
|
||||
'Authorization': 'myEtapiToken123'
|
||||
}
|
||||
|
||||
response = requests.get('http://localhost:8080/etapi/notes/root', headers=headers)
|
||||
print(response.json())</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Method 2: Basic Authentication</h3>
|
||||
|
||||
<p>Use the ETAPI token as the password with any username:</p>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>curl -X GET http://localhost:8080/etapi/notes/root \
|
||||
-u "trilium:myEtapiToken123"</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Method 3: Programmatic Login</h3>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>import requests
|
||||
|
||||
# Login to get token
|
||||
login_data = {'password': 'your-trilium-password'}
|
||||
response = requests.post('http://localhost:8080/etapi/auth/login', json=login_data)
|
||||
token = response.json()['authToken']
|
||||
|
||||
# Use token for subsequent requests
|
||||
headers = {'Authorization': token}
|
||||
notes = requests.get('http://localhost:8080/etapi/notes/root', headers=headers)</code></pre>
|
||||
</div>
|
||||
|
||||
<h2 id="api-endpoints">API Endpoints</h2>
|
||||
|
||||
<h3>Notes</h3>
|
||||
|
||||
<h4>Create Note</h4>
|
||||
|
||||
<div class="endpoint-box method-post">
|
||||
<strong>POST</strong> <code>/etapi/create-note</code>
|
||||
<p>Creates a new note and places it in the tree.</p>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<strong>Request Body:</strong>
|
||||
<pre><code>{
|
||||
"parentNoteId": "root",
|
||||
"title": "My New Note",
|
||||
"type": "text",
|
||||
"content": "<p>This is the note content</p>",
|
||||
"notePosition": 10,
|
||||
"prefix": "📝",
|
||||
"isExpanded": true
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<strong>Response (201 Created):</strong>
|
||||
<pre><code>{
|
||||
"note": {
|
||||
"noteId": "evnnmvHTCgIn",
|
||||
"title": "My New Note",
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"isProtected": false,
|
||||
"dateCreated": "2024-01-15 10:30:00.000+0100",
|
||||
"dateModified": "2024-01-15 10:30:00.000+0100",
|
||||
"utcDateCreated": "2024-01-15 09:30:00.000Z",
|
||||
"utcDateModified": "2024-01-15 09:30:00.000Z"
|
||||
},
|
||||
"branch": {
|
||||
"branchId": "ibhg4WxTdULk",
|
||||
"noteId": "evnnmvHTCgIn",
|
||||
"parentNoteId": "root",
|
||||
"prefix": "📝",
|
||||
"notePosition": 10,
|
||||
"isExpanded": true,
|
||||
"utcDateModified": "2024-01-15 09:30:00.000Z"
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<strong>Python Example:</strong>
|
||||
<pre><code>import requests
|
||||
import json
|
||||
|
||||
def create_note(parent_id, title, content, note_type="text"):
|
||||
url = "http://localhost:8080/etapi/create-note"
|
||||
headers = {
|
||||
'Authorization': 'your-token',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
data = {
|
||||
"parentNoteId": parent_id,
|
||||
"title": title,
|
||||
"type": note_type,
|
||||
"content": content
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=data)
|
||||
|
||||
if response.status_code == 201:
|
||||
return response.json()
|
||||
else:
|
||||
raise Exception(f"Failed to create note: {response.text}")
|
||||
|
||||
# Usage
|
||||
new_note = create_note("root", "Meeting Notes", "<p>Discussion points:</p><ul><li>Item 1</li></ul>")
|
||||
print(f"Created note with ID: {new_note['note']['noteId']}")</code></pre>
|
||||
</div>
|
||||
|
||||
<h4>Get Note by ID</h4>
|
||||
|
||||
<div class="endpoint-box method-get">
|
||||
<strong>GET</strong> <code>/etapi/notes/{noteId}</code>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<strong>cURL Example:</strong>
|
||||
<pre><code>curl -X GET http://localhost:8080/etapi/notes/evnnmvHTCgIn \
|
||||
-H "Authorization: your-token"</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<strong>Response:</strong>
|
||||
<pre><code>{
|
||||
"noteId": "evnnmvHTCgIn",
|
||||
"title": "My Note",
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"isProtected": false,
|
||||
"attributes": [
|
||||
{
|
||||
"attributeId": "abc123",
|
||||
"noteId": "evnnmvHTCgIn",
|
||||
"type": "label",
|
||||
"name": "todo",
|
||||
"value": "",
|
||||
"position": 10,
|
||||
"isInheritable": false
|
||||
}
|
||||
],
|
||||
"parentNoteIds": ["root"],
|
||||
"childNoteIds": ["child1", "child2"],
|
||||
"dateCreated": "2024-01-15 10:30:00.000+0100",
|
||||
"dateModified": "2024-01-15 14:20:00.000+0100",
|
||||
"utcDateCreated": "2024-01-15 09:30:00.000Z",
|
||||
"utcDateModified": "2024-01-15 13:20:00.000Z"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h4>Update Note</h4>
|
||||
|
||||
<div class="endpoint-box method-patch">
|
||||
<strong>PATCH</strong> <code>/etapi/notes/{noteId}</code>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<strong>Request Body:</strong>
|
||||
<pre><code>{
|
||||
"title": "Updated Title",
|
||||
"type": "text",
|
||||
"mime": "text/html"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<strong>JavaScript Example:</strong>
|
||||
<pre><code>async function updateNote(noteId, updates) {
|
||||
const response = await fetch(`http://localhost:8080/etapi/notes/${noteId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': 'your-token',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(updates)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update note: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Usage
|
||||
updateNote('evnnmvHTCgIn', { title: 'New Title' })
|
||||
.then(note => console.log('Updated note:', note))
|
||||
.catch(err => console.error('Error:', err));</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Search</h3>
|
||||
|
||||
<h4>Search Notes</h4>
|
||||
|
||||
<div class="endpoint-box method-get">
|
||||
<strong>GET</strong> <code>/etapi/notes</code>
|
||||
<p>Search for notes using Trilium's search syntax.</p>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>search</code></td>
|
||||
<td>Yes</td>
|
||||
<td>Search query string</td>
|
||||
<td>#todo, "exact phrase"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>fastSearch</code></td>
|
||||
<td>No</td>
|
||||
<td>Enable fast search (default: false)</td>
|
||||
<td>true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>includeArchivedNotes</code></td>
|
||||
<td>No</td>
|
||||
<td>Include archived notes (default: false)</td>
|
||||
<td>true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ancestorNoteId</code></td>
|
||||
<td>No</td>
|
||||
<td>Search within subtree</td>
|
||||
<td>root</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>orderBy</code></td>
|
||||
<td>No</td>
|
||||
<td>Property to order by</td>
|
||||
<td>dateModified</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>orderDirection</code></td>
|
||||
<td>No</td>
|
||||
<td>"asc" or "desc"</td>
|
||||
<td>desc</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit</code></td>
|
||||
<td>No</td>
|
||||
<td>Maximum number of results</td>
|
||||
<td>10</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="example-box">
|
||||
<strong>Python Examples:</strong>
|
||||
<pre><code># Full-text search
|
||||
def search_notes(query, token, **kwargs):
|
||||
url = "http://localhost:8080/etapi/notes"
|
||||
headers = {'Authorization': token}
|
||||
params = {'search': query, **kwargs}
|
||||
|
||||
response = requests.get(url, headers=headers, params=params)
|
||||
return response.json()
|
||||
|
||||
# Search for keyword
|
||||
results = search_notes("project management", token)
|
||||
|
||||
# Search with label
|
||||
results = search_notes("#todo", token)
|
||||
|
||||
# Search for exact phrase
|
||||
results = search_notes('"exact phrase"', token)
|
||||
|
||||
# Complex search with ordering and limit
|
||||
results = search_notes(
|
||||
"type:text #important",
|
||||
token,
|
||||
orderBy="dateModified",
|
||||
orderDirection="desc",
|
||||
limit=10
|
||||
)</code></pre>
|
||||
</div>
|
||||
|
||||
<h2 id="common-use-cases">Common Use Cases</h2>
|
||||
|
||||
<h3>1. Daily Journal Entry</h3>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>from datetime import date
|
||||
import requests
|
||||
|
||||
class TriliumJournal:
|
||||
def __init__(self, base_url, token):
|
||||
self.base_url = base_url
|
||||
self.headers = {'Authorization': token}
|
||||
|
||||
def create_journal_entry(self, content, tags=[]):
|
||||
# Get today's day note
|
||||
today = date.today().strftime('%Y-%m-%d')
|
||||
day_note_url = f"{self.base_url}/calendar/days/{today}"
|
||||
day_note = requests.get(day_note_url, headers=self.headers).json()
|
||||
|
||||
# Create entry
|
||||
entry_data = {
|
||||
"parentNoteId": day_note['noteId'],
|
||||
"title": f"Entry - {date.today().strftime('%H:%M')}",
|
||||
"type": "text",
|
||||
"content": content
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
f"{self.base_url}/create-note",
|
||||
headers={**self.headers, 'Content-Type': 'application/json'},
|
||||
json=entry_data
|
||||
)
|
||||
|
||||
entry = response.json()
|
||||
|
||||
# Add tags
|
||||
for tag in tags:
|
||||
self.add_tag(entry['note']['noteId'], tag)
|
||||
|
||||
return entry
|
||||
|
||||
def add_tag(self, note_id, tag_name):
|
||||
attr_data = {
|
||||
"noteId": note_id,
|
||||
"type": "label",
|
||||
"name": tag_name,
|
||||
"value": ""
|
||||
}
|
||||
|
||||
requests.post(
|
||||
f"{self.base_url}/attributes",
|
||||
headers={**self.headers, 'Content-Type': 'application/json'},
|
||||
json=attr_data
|
||||
)
|
||||
|
||||
# Usage
|
||||
journal = TriliumJournal("http://localhost:8080/etapi", "your-token")
|
||||
entry = journal.create_journal_entry(
|
||||
"<p>Today's meeting went well. Key decisions:</p><ul><li>Item 1</li></ul>",
|
||||
tags=["meeting", "important"]
|
||||
)</code></pre>
|
||||
</div>
|
||||
|
||||
<h2 id="error-handling">Error Handling</h2>
|
||||
|
||||
<h3>Common Error Codes</h3>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Code</th>
|
||||
<th>Description</th>
|
||||
<th>Resolution</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>400</td>
|
||||
<td>BAD_REQUEST</td>
|
||||
<td>Invalid request format</td>
|
||||
<td>Check request body and parameters</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>401</td>
|
||||
<td>UNAUTHORIZED</td>
|
||||
<td>Invalid or missing token</td>
|
||||
<td>Verify authentication token</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>404</td>
|
||||
<td>NOTE_NOT_FOUND</td>
|
||||
<td>Note doesn't exist</td>
|
||||
<td>Check note ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>400</td>
|
||||
<td>NOTE_IS_PROTECTED</td>
|
||||
<td>Cannot modify protected note</td>
|
||||
<td>Unlock protected session first</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>429</td>
|
||||
<td>TOO_MANY_REQUESTS</td>
|
||||
<td>Rate limit exceeded</td>
|
||||
<td>Wait before retrying</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Error Response Format</h3>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>{
|
||||
"status": 400,
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Note title cannot be empty"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Handling Errors in Code</h3>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>class ETAPIError(Exception):
|
||||
def __init__(self, status, code, message):
|
||||
self.status = status
|
||||
self.code = code
|
||||
self.message = message
|
||||
super().__init__(f"{code}: {message}")
|
||||
|
||||
def handle_api_response(response):
|
||||
if response.status_code >= 400:
|
||||
try:
|
||||
error = response.json()
|
||||
raise ETAPIError(
|
||||
error.get('status'),
|
||||
error.get('code'),
|
||||
error.get('message')
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
raise ETAPIError(
|
||||
response.status_code,
|
||||
'UNKNOWN_ERROR',
|
||||
response.text
|
||||
)
|
||||
|
||||
return response.json() if response.content else None
|
||||
|
||||
# Usage
|
||||
try:
|
||||
response = requests.get(
|
||||
'http://localhost:8080/etapi/notes/invalid',
|
||||
headers={'Authorization': 'token'}
|
||||
)
|
||||
note = handle_api_response(response)
|
||||
except ETAPIError as e:
|
||||
if e.code == 'NOTE_NOT_FOUND':
|
||||
print("Note doesn't exist")
|
||||
else:
|
||||
print(f"API Error: {e.message}")</code></pre>
|
||||
</div>
|
||||
|
||||
<h2 id="rate-limiting-and-best-practices">Rate Limiting and Best Practices</h2>
|
||||
|
||||
<div class="warning">
|
||||
<h4>Rate Limiting</h4>
|
||||
<p>ETAPI implements rate limiting for authentication endpoints:</p>
|
||||
<ul>
|
||||
<li><strong>Login endpoint</strong>: Maximum 10 requests per IP per hour</li>
|
||||
<li><strong>Other endpoints</strong>: No specific rate limits, but excessive requests may be throttled</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>Best Practices</h3>
|
||||
|
||||
<div class="success">
|
||||
<h4>1. Connection Pooling</h4>
|
||||
<p>Reuse HTTP connections for better performance:</p>
|
||||
<pre><code>import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.packages.urllib3.util.retry import Retry
|
||||
|
||||
session = requests.Session()
|
||||
retry = Retry(
|
||||
total=3,
|
||||
backoff_factor=0.3,
|
||||
status_forcelist=[500, 502, 503, 504]
|
||||
)
|
||||
adapter = HTTPAdapter(max_retries=retry)
|
||||
session.mount('http://', adapter)
|
||||
session.mount('https://', adapter)</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="success">
|
||||
<h4>2. Error Handling</h4>
|
||||
<p>Implement robust error handling with specific exception types for different error conditions.</p>
|
||||
</div>
|
||||
|
||||
<div class="success">
|
||||
<h4>3. Caching</h4>
|
||||
<p>Cache frequently accessed data to reduce API calls and improve performance.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="performance-considerations">Performance Considerations</h2>
|
||||
|
||||
<h3>1. Minimize API Calls</h3>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code># Bad: Multiple calls (N+1 problem)
|
||||
note = api.get_note(note_id)
|
||||
for child_id in note['childNoteIds']:
|
||||
child = api.get_note(child_id) # N+1 problem
|
||||
process(child)
|
||||
|
||||
# Good: Batch processing
|
||||
note = api.get_note(note_id)
|
||||
children = api.search_notes(
|
||||
f"note.parents.noteId={note_id}",
|
||||
limit=1000
|
||||
)
|
||||
for child in children:
|
||||
process(child)</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>2. Use Appropriate Search Depth</h3>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code># Limit search depth for better performance
|
||||
results = api.search_notes(
|
||||
"keyword",
|
||||
ancestor_note_id="root",
|
||||
ancestor_depth="lt3" # Only search 3 levels deep
|
||||
)</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<h4>Additional Resources</h4>
|
||||
<ul>
|
||||
<li><a href="https://github.com/TriliumNext/Trilium">Trilium GitHub Repository</a></li>
|
||||
<li><a href="/apps/server/src/assets/etapi.openapi.yaml">OpenAPI Specification</a></li>
|
||||
<li><a href="https://triliumnext.github.io/Docs/Wiki/search.html">Trilium Search Documentation</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
810
apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/Internal API Reference.html
vendored
Normal file
810
apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/Internal API Reference.html
vendored
Normal file
@@ -0,0 +1,810 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Internal API Reference</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
|
||||
h2 { color: #34495e; margin-top: 40px; border-bottom: 2px solid #ecf0f1; padding-bottom: 5px; }
|
||||
h3 { color: #7f8c8d; margin-top: 30px; }
|
||||
h4 { color: #95a5a6; margin-top: 25px; }
|
||||
pre {
|
||||
background: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
code {
|
||||
background: #f4f4f4;
|
||||
padding: 3px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', Monaco, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
.endpoint-box {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
.method-get { border-left-color: #28a745; }
|
||||
.method-post { border-left-color: #007bff; }
|
||||
.method-put { border-left-color: #ffc107; }
|
||||
.method-patch { border-left-color: #fd7e14; }
|
||||
.method-delete { border-left-color: #dc3545; }
|
||||
.example-box {
|
||||
background: #f1f3f4;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: #34495e;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
tr:nth-child(even) { background: #f8f9fa; }
|
||||
.warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #f39c12;
|
||||
}
|
||||
.info {
|
||||
background: #d1ecf1;
|
||||
border: 1px solid #74c0fc;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
.danger {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f1aeb5;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
.toc {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.toc ul {
|
||||
list-style-type: none;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.toc a {
|
||||
text-decoration: none;
|
||||
color: #495057;
|
||||
}
|
||||
.toc a:hover {
|
||||
color: #3498db;
|
||||
}
|
||||
.websocket-box {
|
||||
background: #e8f4f8;
|
||||
border-left: 4px solid #9b59b6;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Internal API Reference</h1>
|
||||
|
||||
<div class="toc">
|
||||
<h2>Table of Contents</h2>
|
||||
<ul>
|
||||
<li><a href="#introduction">Introduction</a></li>
|
||||
<li><a href="#authentication-and-session-management">Authentication and Session Management</a></li>
|
||||
<li><a href="#core-api-endpoints">Core API Endpoints</a></li>
|
||||
<li><a href="#websocket-real-time-updates">WebSocket Real-time Updates</a></li>
|
||||
<li><a href="#file-operations">File Operations</a></li>
|
||||
<li><a href="#import-export-operations">Import/Export Operations</a></li>
|
||||
<li><a href="#when-to-use-internal-vs-etapi">When to Use Internal vs ETAPI</a></li>
|
||||
<li><a href="#security-considerations">Security Considerations</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 id="introduction">Introduction</h2>
|
||||
|
||||
<p>The Internal API is the primary interface used by the Trilium Notes client application to communicate with the server. While powerful and feature-complete, this API is primarily designed for internal use.</p>
|
||||
|
||||
<div class="danger">
|
||||
<h4>Important Notice</h4>
|
||||
<p><strong>For external integrations, please use <a href="ETAPI%20Complete%20Guide.html">ETAPI</a> instead.</strong> The Internal API:</p>
|
||||
<ul>
|
||||
<li>May change between versions without notice</li>
|
||||
<li>Requires session-based authentication with CSRF protection</li>
|
||||
<li>Is tightly coupled with the frontend application</li>
|
||||
<li>Has limited documentation and stability guarantees</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<strong>Base URL:</strong> <code>http://localhost:8080/api</code>
|
||||
</div>
|
||||
|
||||
<h3>Key Characteristics</h3>
|
||||
<ul>
|
||||
<li>Session-based authentication with cookies</li>
|
||||
<li>CSRF token protection for state-changing operations</li>
|
||||
<li>WebSocket support for real-time updates</li>
|
||||
<li>Full feature parity with the Trilium UI</li>
|
||||
<li>Complex request/response formats optimized for the client</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="authentication-and-session-management">Authentication and Session Management</h2>
|
||||
|
||||
<h3>Password Login</h3>
|
||||
|
||||
<div class="endpoint-box method-post">
|
||||
<strong>POST</strong> <code>/api/login</code>
|
||||
<p>Authenticates user with password and creates a session.</p>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<strong>Request:</strong>
|
||||
<pre><code>const formData = new URLSearchParams();
|
||||
formData.append('password', 'your-password');
|
||||
|
||||
const response = await fetch('http://localhost:8080/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: formData,
|
||||
credentials: 'include' // Important for cookie handling
|
||||
});</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<strong>Response:</strong>
|
||||
<pre><code>{
|
||||
"success": true,
|
||||
"message": "Login successful"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<p>The server sets a session cookie (<code>trilium.sid</code>) that must be included in subsequent requests.</p>
|
||||
|
||||
<h3>TOTP Authentication (2FA)</h3>
|
||||
|
||||
<p>If 2FA is enabled, include the TOTP token:</p>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>formData.append('password', 'your-password');
|
||||
formData.append('totpToken', '123456');</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Token Authentication</h3>
|
||||
|
||||
<div class="endpoint-box method-post">
|
||||
<strong>POST</strong> <code>/api/login/token</code>
|
||||
<p>Generate an API token for programmatic access:</p>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>const response = await fetch('http://localhost:8080/api/login/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
password: 'your-password',
|
||||
tokenName: 'My Integration'
|
||||
})
|
||||
});
|
||||
|
||||
const { authToken } = await response.json();
|
||||
// Use this token in Authorization header for future requests</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Protected Session</h3>
|
||||
|
||||
<div class="endpoint-box method-post">
|
||||
<strong>POST</strong> <code>/api/login/protected</code>
|
||||
<p>Enter protected session to access encrypted notes:</p>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>await fetch('http://localhost:8080/api/login/protected', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
password: 'your-password'
|
||||
}),
|
||||
credentials: 'include'
|
||||
});</code></pre>
|
||||
</div>
|
||||
|
||||
<h2 id="core-api-endpoints">Core API Endpoints</h2>
|
||||
|
||||
<h3>Notes</h3>
|
||||
|
||||
<h4>Get Note</h4>
|
||||
|
||||
<div class="endpoint-box method-get">
|
||||
<strong>GET</strong> <code>/api/notes/{noteId}</code>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>const response = await fetch('http://localhost:8080/api/notes/root', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const note = await response.json();</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<strong>Response:</strong>
|
||||
<pre><code>{
|
||||
"noteId": "root",
|
||||
"title": "Trilium Notes",
|
||||
"type": "text",
|
||||
"mime": "text/html",
|
||||
"isProtected": false,
|
||||
"isDeleted": false,
|
||||
"dateCreated": "2024-01-01 00:00:00.000+0000",
|
||||
"dateModified": "2024-01-15 10:30:00.000+0000",
|
||||
"utcDateCreated": "2024-01-01 00:00:00.000Z",
|
||||
"utcDateModified": "2024-01-15 10:30:00.000Z",
|
||||
"parentBranches": [
|
||||
{
|
||||
"branchId": "root_root",
|
||||
"parentNoteId": "none",
|
||||
"prefix": null,
|
||||
"notePosition": 10
|
||||
}
|
||||
],
|
||||
"attributes": [],
|
||||
"cssClass": "",
|
||||
"iconClass": "bx bx-folder"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h4>Create Note</h4>
|
||||
|
||||
<div class="endpoint-box method-post">
|
||||
<strong>POST</strong> <code>/api/notes/{parentNoteId}/children</code>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>const response = await fetch('http://localhost:8080/api/notes/root/children', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: 'New Note',
|
||||
type: 'text',
|
||||
content: '<p>Note content</p>',
|
||||
isProtected: false
|
||||
}),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const { note, branch } = await response.json();</code></pre>
|
||||
</div>
|
||||
|
||||
<h4>Update Note</h4>
|
||||
|
||||
<div class="endpoint-box method-put">
|
||||
<strong>PUT</strong> <code>/api/notes/{noteId}</code>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>await fetch(`http://localhost:8080/api/notes/${noteId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: 'Updated Title',
|
||||
type: 'text',
|
||||
mime: 'text/html'
|
||||
}),
|
||||
credentials: 'include'
|
||||
});</code></pre>
|
||||
</div>
|
||||
|
||||
<h4>Get Note Content</h4>
|
||||
|
||||
<div class="endpoint-box method-get">
|
||||
<strong>GET</strong> <code>/api/notes/{noteId}/content</code>
|
||||
<p>Returns the actual content of the note:</p>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>const response = await fetch(`http://localhost:8080/api/notes/${noteId}/content`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const content = await response.text();</code></pre>
|
||||
</div>
|
||||
|
||||
<h4>Save Note Content</h4>
|
||||
|
||||
<div class="endpoint-box method-put">
|
||||
<strong>PUT</strong> <code>/api/notes/{noteId}/content</code>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>await fetch(`http://localhost:8080/api/notes/${noteId}/content`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'text/html',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
body: '<p>Updated content</p>',
|
||||
credentials: 'include'
|
||||
});</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Tree Operations</h3>
|
||||
|
||||
<h4>Move Note</h4>
|
||||
|
||||
<div class="endpoint-box method-put">
|
||||
<strong>PUT</strong> <code>/api/branches/{branchId}/move</code>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>await fetch(`http://localhost:8080/api/branches/${branchId}/move`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
parentNoteId: 'newParentId',
|
||||
beforeNoteId: 'siblingNoteId' // optional, for positioning
|
||||
}),
|
||||
credentials: 'include'
|
||||
});</code></pre>
|
||||
</div>
|
||||
|
||||
<h4>Clone Note</h4>
|
||||
|
||||
<div class="endpoint-box method-post">
|
||||
<strong>POST</strong> <code>/api/notes/{noteId}/clone</code>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>const response = await fetch(`http://localhost:8080/api/notes/${noteId}/clone`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
parentNoteId: 'targetParentId',
|
||||
prefix: 'Copy of '
|
||||
}),
|
||||
credentials: 'include'
|
||||
});</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Search</h3>
|
||||
|
||||
<h4>Search Notes</h4>
|
||||
|
||||
<div class="endpoint-box method-get">
|
||||
<strong>GET</strong> <code>/api/search</code>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>const params = new URLSearchParams({
|
||||
query: '#todo OR #task',
|
||||
fastSearch: 'false',
|
||||
includeArchivedNotes: 'false',
|
||||
ancestorNoteId: 'root',
|
||||
orderBy: 'relevancy',
|
||||
orderDirection: 'desc',
|
||||
limit: '50'
|
||||
});
|
||||
|
||||
const response = await fetch(`http://localhost:8080/api/search?${params}`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const { results } = await response.json();</code></pre>
|
||||
</div>
|
||||
|
||||
<h2 id="websocket-real-time-updates">WebSocket Real-time Updates</h2>
|
||||
|
||||
<div class="websocket-box">
|
||||
<p>The Internal API provides WebSocket connections for real-time synchronization and updates.</p>
|
||||
</div>
|
||||
|
||||
<h3>Connection Setup</h3>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>class TriliumWebSocket {
|
||||
constructor() {
|
||||
this.ws = null;
|
||||
this.reconnectInterval = 5000;
|
||||
this.shouldReconnect = true;
|
||||
}
|
||||
|
||||
connect() {
|
||||
// WebSocket URL same as base URL but with ws:// protocol
|
||||
const wsUrl = 'ws://localhost:8080';
|
||||
|
||||
this.ws = new WebSocket(wsUrl);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
this.sendPing();
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
this.handleMessage(message);
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
if (this.shouldReconnect) {
|
||||
setTimeout(() => this.connect(), this.reconnectInterval);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleMessage(message) {
|
||||
switch (message.type) {
|
||||
case 'sync':
|
||||
this.handleSync(message.data);
|
||||
break;
|
||||
case 'entity-changes':
|
||||
this.handleEntityChanges(message.data);
|
||||
break;
|
||||
case 'refresh-tree':
|
||||
this.refreshTree();
|
||||
break;
|
||||
case 'create-note':
|
||||
this.handleNoteCreated(message.data);
|
||||
break;
|
||||
case 'update-note':
|
||||
this.handleNoteUpdated(message.data);
|
||||
break;
|
||||
case 'delete-note':
|
||||
this.handleNoteDeleted(message.data);
|
||||
break;
|
||||
default:
|
||||
console.log('Unknown message type:', message.type);
|
||||
}
|
||||
}
|
||||
|
||||
sendPing() {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify({ type: 'ping' }));
|
||||
setTimeout(() => this.sendPing(), 30000); // Ping every 30 seconds
|
||||
}
|
||||
}
|
||||
|
||||
send(type, data) {
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify({ type, data }));
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Message Types</h3>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Direction</th>
|
||||
<th>Description</th>
|
||||
<th>Data Format</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>sync</code></td>
|
||||
<td>Incoming</td>
|
||||
<td>Synchronization data</td>
|
||||
<td><code>{ entityChanges: [], lastSyncedPush: number }</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>entity-changes</code></td>
|
||||
<td>Incoming</td>
|
||||
<td>Entity modifications</td>
|
||||
<td><code>[{ entityName, entityId, action }]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>refresh-tree</code></td>
|
||||
<td>Incoming</td>
|
||||
<td>Tree structure changed</td>
|
||||
<td><code>None</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ping</code></td>
|
||||
<td>Outgoing</td>
|
||||
<td>Keep connection alive</td>
|
||||
<td><code>None</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>log-error</code></td>
|
||||
<td>Outgoing</td>
|
||||
<td>Log client error</td>
|
||||
<td><code>{ error, stack }</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 id="file-operations">File Operations</h2>
|
||||
|
||||
<h3>Upload File</h3>
|
||||
|
||||
<div class="endpoint-box method-post">
|
||||
<strong>POST</strong> <code>/api/notes/{noteId}/attachments/upload</code>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>const formData = new FormData();
|
||||
formData.append('file', fileInput.files[0]);
|
||||
|
||||
const response = await fetch(`/api/notes/${noteId}/attachments/upload`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
body: formData,
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const attachment = await response.json();</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Download Attachment</h3>
|
||||
|
||||
<div class="endpoint-box method-get">
|
||||
<strong>GET</strong> <code>/api/attachments/{attachmentId}/download</code>
|
||||
</div>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>const response = await fetch(`/api/attachments/${attachmentId}/download`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'attachment.pdf';
|
||||
a.click();</code></pre>
|
||||
</div>
|
||||
|
||||
<h2 id="when-to-use-internal-vs-etapi">When to Use Internal vs ETAPI</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Use Internal API When</th>
|
||||
<th>Use ETAPI When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Building custom Trilium clients</td>
|
||||
<td>Building external integrations</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Needing WebSocket real-time updates</td>
|
||||
<td>Creating automation scripts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Requiring full feature parity with the UI</td>
|
||||
<td>Developing third-party applications</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Working within the Trilium frontend environment</td>
|
||||
<td>Needing stable, documented API</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Accessing advanced features not available in ETAPI</td>
|
||||
<td>Working with different programming languages</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Feature Comparison</h3>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
<th>Internal API</th>
|
||||
<th>ETAPI</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Authentication</strong></td>
|
||||
<td>Session/Cookie</td>
|
||||
<td>Token</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>CSRF Protection</strong></td>
|
||||
<td>Required</td>
|
||||
<td>Not needed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>WebSocket</strong></td>
|
||||
<td>Yes</td>
|
||||
<td>No</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Stability</strong></td>
|
||||
<td>May change</td>
|
||||
<td>Stable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Documentation</strong></td>
|
||||
<td>Limited</td>
|
||||
<td>Comprehensive</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Real-time updates</strong></td>
|
||||
<td>Yes</td>
|
||||
<td>No</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 id="security-considerations">Security Considerations</h2>
|
||||
|
||||
<h3>CSRF Protection</h3>
|
||||
|
||||
<p>All state-changing operations require a CSRF token:</p>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>// Get CSRF token from meta tag or API
|
||||
async function getCsrfToken() {
|
||||
const response = await fetch('/api/csrf-token', {
|
||||
credentials: 'include'
|
||||
});
|
||||
const { token } = await response.json();
|
||||
return token;
|
||||
}
|
||||
|
||||
// Use in requests
|
||||
const csrfToken = await getCsrfToken();
|
||||
|
||||
await fetch('/api/notes', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include'
|
||||
});</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Session Management</h3>
|
||||
|
||||
<div class="example-box">
|
||||
<pre><code>class TriliumSession {
|
||||
constructor() {
|
||||
this.isAuthenticated = false;
|
||||
this.csrfToken = null;
|
||||
}
|
||||
|
||||
async login(password) {
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('password', password);
|
||||
|
||||
const response = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: formData,
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.isAuthenticated = true;
|
||||
this.csrfToken = await this.getCsrfToken();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async getCsrfToken() {
|
||||
const response = await fetch('/api/csrf-token', {
|
||||
credentials: 'include'
|
||||
});
|
||||
const { token } = await response.json();
|
||||
return token;
|
||||
}
|
||||
|
||||
async request(url, options = {}) {
|
||||
if (!this.isAuthenticated) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...options.headers
|
||||
};
|
||||
|
||||
if (options.method && options.method !== 'GET') {
|
||||
headers['X-CSRF-Token'] = this.csrfToken;
|
||||
}
|
||||
|
||||
return fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
credentials: 'include'
|
||||
});
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="warning">
|
||||
<h4>Important Security Notes</h4>
|
||||
<ul>
|
||||
<li>Always include <code>credentials: 'include'</code> for session-based requests</li>
|
||||
<li>Use CSRF tokens for all state-changing operations</li>
|
||||
<li>Handle protected notes with proper authentication</li>
|
||||
<li>Validate all user input before sending to the API</li>
|
||||
<li>Use HTTPS in production environments</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<h4>Related Documentation</h4>
|
||||
<ul>
|
||||
<li><a href="ETAPI%20Complete%20Guide.html">ETAPI Complete Guide</a></li>
|
||||
<li><a href="WebSocket%20API.html">WebSocket API Documentation</a></li>
|
||||
<li><a href="Script%20API%20Cookbook.html">Script API Cookbook</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
937
apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/Script API Cookbook.html
vendored
Normal file
937
apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/Script API Cookbook.html
vendored
Normal file
@@ -0,0 +1,937 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Script API Cookbook</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
h1 { color: #2c3e50; border-bottom: 3px solid #8e44ad; padding-bottom: 10px; }
|
||||
h2 { color: #34495e; margin-top: 40px; border-bottom: 2px solid #ecf0f1; padding-bottom: 5px; }
|
||||
h3 { color: #7f8c8d; margin-top: 30px; }
|
||||
h4 { color: #95a5a6; margin-top: 25px; }
|
||||
pre {
|
||||
background: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
border-left: 4px solid #8e44ad;
|
||||
}
|
||||
code {
|
||||
background: #f4f4f4;
|
||||
padding: 3px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', Monaco, monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
.recipe-box {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #8e44ad;
|
||||
}
|
||||
.backend-script { border-left-color: #e74c3c; }
|
||||
.frontend-script { border-left-color: #3498db; }
|
||||
.example-box {
|
||||
background: #f1f3f4;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: #8e44ad;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
tr:nth-child(even) { background: #f8f9fa; }
|
||||
.warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #f39c12;
|
||||
}
|
||||
.info {
|
||||
background: #d1ecf1;
|
||||
border: 1px solid #74c0fc;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
.success {
|
||||
background: #d4edda;
|
||||
border: 1px solid #51cf66;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
.toc {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.toc ul {
|
||||
list-style-type: none;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.toc a {
|
||||
text-decoration: none;
|
||||
color: #495057;
|
||||
}
|
||||
.toc a:hover {
|
||||
color: #8e44ad;
|
||||
}
|
||||
.use-case {
|
||||
background: #f8f4ff;
|
||||
border-left: 4px solid #8e44ad;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Script API Cookbook</h1>
|
||||
|
||||
<div class="toc">
|
||||
<h2>Table of Contents</h2>
|
||||
<ul>
|
||||
<li><a href="#introduction">Introduction</a></li>
|
||||
<li><a href="#backend-script-recipes">Backend Script Recipes</a></li>
|
||||
<li><a href="#frontend-script-recipes">Frontend Script Recipes</a></li>
|
||||
<li><a href="#common-patterns">Common Patterns</a></li>
|
||||
<li><a href="#note-manipulation">Note Manipulation</a></li>
|
||||
<li><a href="#attribute-operations">Attribute Operations</a></li>
|
||||
<li><a href="#search-and-filtering">Search and Filtering</a></li>
|
||||
<li><a href="#automation-examples">Automation Examples</a></li>
|
||||
<li><a href="#integration-with-external-services">Integration with External Services</a></li>
|
||||
<li><a href="#best-practices">Best Practices</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 id="introduction">Introduction</h2>
|
||||
|
||||
<p>Trilium's Script API provides powerful automation capabilities through JavaScript code that runs either on the backend (Node.js) or frontend (browser). This cookbook contains practical recipes and patterns for common scripting tasks.</p>
|
||||
|
||||
<h3>Script Types</h3>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Environment</th>
|
||||
<th>Access</th>
|
||||
<th>Use Cases</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Backend Script</strong></td>
|
||||
<td>Node.js</td>
|
||||
<td>Full database, file system, network</td>
|
||||
<td>Automation, data processing, integrations</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Frontend Script</strong></td>
|
||||
<td>Browser</td>
|
||||
<td>UI manipulation, user interaction</td>
|
||||
<td>Custom widgets, UI enhancements</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Custom Widget</strong></td>
|
||||
<td>Browser</td>
|
||||
<td>Widget lifecycle, note context</td>
|
||||
<td>Interactive components, visualizations</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Basic Script Structure</h3>
|
||||
|
||||
<div class="recipe-box backend-script">
|
||||
<h4>Backend Script:</h4>
|
||||
<pre><code>// Access to api object is automatic
|
||||
const note = await api.getNoteWithLabel('todoList');
|
||||
const children = await note.getChildNotes();
|
||||
|
||||
// Return value becomes script output
|
||||
return {
|
||||
noteTitle: note.title,
|
||||
childCount: children.length
|
||||
};</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="recipe-box frontend-script">
|
||||
<h4>Frontend Script:</h4>
|
||||
<pre><code>// Access to api object is automatic
|
||||
api.showMessage('Script executed!');
|
||||
|
||||
// Manipulate UI
|
||||
const $button = $('<button>').text('Click Me').click(() => {
|
||||
api.showMessage('Button clicked!');
|
||||
});
|
||||
|
||||
$('body').append($button);</code></pre>
|
||||
</div>
|
||||
|
||||
<h2 id="backend-script-recipes">Backend Script Recipes</h2>
|
||||
|
||||
<h3>1. Daily Note Generator</h3>
|
||||
|
||||
<div class="use-case">
|
||||
<strong>Use Case:</strong> Automatically create daily notes with template content
|
||||
</div>
|
||||
|
||||
<div class="recipe-box backend-script">
|
||||
<pre><code>// #run=hourly
|
||||
|
||||
async function createDailyNote() {
|
||||
const today = api.dayjs().format('YYYY-MM-DD');
|
||||
const dayNote = await api.getDayNote(today);
|
||||
|
||||
// Check if content already exists
|
||||
const content = await dayNote.getContent();
|
||||
if (content && content.length > 100) {
|
||||
return; // Already has content
|
||||
}
|
||||
|
||||
// Get template
|
||||
const template = await api.getNoteWithLabel('dailyTemplate');
|
||||
if (!template) {
|
||||
await dayNote.setContent(`
|
||||
<h2>📅 ${api.dayjs().format('dddd, MMMM D, YYYY')}</h2>
|
||||
|
||||
<h3>☀️ Morning Routine</h3>
|
||||
<ul>
|
||||
<li>[ ] Morning meditation</li>
|
||||
<li>[ ] Exercise</li>
|
||||
<li>[ ] Review daily goals</li>
|
||||
</ul>
|
||||
|
||||
<h3>📋 Today's Tasks</h3>
|
||||
<ul>
|
||||
<li></li>
|
||||
</ul>
|
||||
|
||||
<h3>📝 Notes</h3>
|
||||
<p></p>
|
||||
|
||||
<h3>🌙 Evening Reflection</h3>
|
||||
<p></p>
|
||||
`);
|
||||
} else {
|
||||
const templateContent = await template.getContent();
|
||||
await dayNote.setContent(templateContent);
|
||||
}
|
||||
|
||||
// Add metadata
|
||||
await dayNote.setLabel('type', 'daily');
|
||||
await dayNote.setLabel('created', api.dayjs().format());
|
||||
|
||||
api.log(`Daily note created for ${today}`);
|
||||
}
|
||||
|
||||
await createDailyNote();</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>2. Note Statistics Collector</h3>
|
||||
|
||||
<div class="use-case">
|
||||
<strong>Use Case:</strong> Collect and display statistics about your notes
|
||||
</div>
|
||||
|
||||
<div class="recipe-box backend-script">
|
||||
<pre><code>async function collectStatistics() {
|
||||
const stats = {
|
||||
totalNotes: 0,
|
||||
notesByType: {},
|
||||
notesByMonth: {},
|
||||
largestNotes: [],
|
||||
recentlyModified: [],
|
||||
tagCloud: {}
|
||||
};
|
||||
|
||||
// Get all notes
|
||||
const notes = await api.searchForNotes('');
|
||||
stats.totalNotes = notes.length;
|
||||
|
||||
for (const note of notes) {
|
||||
// Count by type
|
||||
stats.notesByType[note.type] = (stats.notesByType[note.type] || 0) + 1;
|
||||
|
||||
// Count by creation month
|
||||
const month = api.dayjs(note.utcDateCreated).format('YYYY-MM');
|
||||
stats.notesByMonth[month] = (stats.notesByMonth[month] || 0) + 1;
|
||||
|
||||
// Track largest notes
|
||||
const content = await note.getContent();
|
||||
if (content) {
|
||||
stats.largestNotes.push({
|
||||
noteId: note.noteId,
|
||||
title: note.title,
|
||||
size: content.length
|
||||
});
|
||||
}
|
||||
|
||||
// Collect labels for tag cloud
|
||||
const labels = await note.getLabels();
|
||||
for (const label of labels) {
|
||||
if (!label.name.startsWith('child:')) {
|
||||
stats.tagCloud[label.name] = (stats.tagCloud[label.name] || 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort largest notes
|
||||
stats.largestNotes.sort((a, b) => b.size - a.size);
|
||||
stats.largestNotes = stats.largestNotes.slice(0, 10);
|
||||
|
||||
// Create or update statistics note
|
||||
let statsNote = await api.getNoteWithLabel('statistics');
|
||||
if (!statsNote) {
|
||||
statsNote = await api.createTextNote('root', 'Statistics', '');
|
||||
await statsNote.setLabel('statistics');
|
||||
}
|
||||
|
||||
// Generate report
|
||||
const report = `
|
||||
<h1>📊 Note Statistics</h1>
|
||||
<p>Generated: ${api.dayjs().format('YYYY-MM-DD HH:mm:ss')}</p>
|
||||
|
||||
<h2>Overview</h2>
|
||||
<ul>
|
||||
<li>Total Notes: <strong>${stats.totalNotes}</strong></li>
|
||||
<li>Note Types: ${Object.entries(stats.notesByType)
|
||||
.map(([type, count]) => `${type} (${count})`)
|
||||
.join(', ')}</li>
|
||||
</ul>
|
||||
|
||||
<h2>Largest Notes</h2>
|
||||
<ol>
|
||||
${stats.largestNotes.map(n =>
|
||||
`<li><a href="#root/${n.noteId}">${n.title}</a> - ${(n.size / 1024).toFixed(1)} KB</li>`
|
||||
).join('')}
|
||||
</ol>
|
||||
|
||||
<h2>Top Tags</h2>
|
||||
<div class="tag-cloud">
|
||||
${Object.entries(stats.tagCloud)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 20)
|
||||
.map(([tag, count]) =>
|
||||
`<span style="font-size: ${Math.min(200, 100 + count * 5)}%">#${tag} (${count})</span>`
|
||||
).join(' ')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
await statsNote.setContent(report);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
return await collectStatistics();</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>3. Task Aggregator</h3>
|
||||
|
||||
<div class="use-case">
|
||||
<strong>Use Case:</strong> Collect all tasks from different notes and create a dashboard
|
||||
</div>
|
||||
|
||||
<div class="recipe-box backend-script">
|
||||
<pre><code>async function aggregateTasks() {
|
||||
// Find all notes with todos
|
||||
const todoNotes = await api.searchForNotes('#todo OR #task OR content:"[ ]" OR content:"[x]"');
|
||||
|
||||
const tasks = {
|
||||
pending: [],
|
||||
completed: [],
|
||||
overdue: []
|
||||
};
|
||||
|
||||
for (const note of todoNotes) {
|
||||
const content = await note.getContent();
|
||||
if (!content) continue;
|
||||
|
||||
// Parse checkbox tasks
|
||||
const checkboxRegex = /\[([ x])\]\s*(.+?)(?=\n|\<|$)/gi;
|
||||
let match;
|
||||
|
||||
while ((match = checkboxRegex.exec(content)) !== null) {
|
||||
const isCompleted = match[1] === 'x';
|
||||
const taskText = match[2].replace(/<[^>]*>/g, ''); // Strip HTML
|
||||
|
||||
const task = {
|
||||
noteId: note.noteId,
|
||||
noteTitle: note.title,
|
||||
text: taskText,
|
||||
completed: isCompleted
|
||||
};
|
||||
|
||||
// Check for due date
|
||||
const dueDateLabel = await note.getLabel('dueDate');
|
||||
if (dueDateLabel) {
|
||||
task.dueDate = dueDateLabel.value;
|
||||
const dueDate = api.dayjs(dueDateLabel.value);
|
||||
if (!isCompleted && dueDate.isBefore(api.dayjs())) {
|
||||
tasks.overdue.push(task);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isCompleted) {
|
||||
tasks.completed.push(task);
|
||||
} else {
|
||||
tasks.pending.push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create or update task dashboard
|
||||
let dashboard = await api.getNoteWithLabel('taskDashboard');
|
||||
if (!dashboard) {
|
||||
dashboard = await api.createTextNote('root', '📋 Task Dashboard', '');
|
||||
await dashboard.setLabel('taskDashboard');
|
||||
}
|
||||
|
||||
const dashboardContent = `
|
||||
<h1>📋 Task Dashboard</h1>
|
||||
<p>Last updated: ${api.dayjs().format('YYYY-MM-DD HH:mm:ss')}</p>
|
||||
|
||||
<h2>⚠️ Overdue (${tasks.overdue.length})</h2>
|
||||
<ul>
|
||||
${tasks.overdue.map(t =>
|
||||
`<li style="color: red;">
|
||||
<strong>${t.text}</strong>
|
||||
(Due: ${api.dayjs(t.dueDate).format('MMM D')})
|
||||
- <a href="#root/${t.noteId}">${t.noteTitle}</a>
|
||||
</li>`
|
||||
).join('')}
|
||||
</ul>
|
||||
|
||||
<h2>📌 Pending (${tasks.pending.length})</h2>
|
||||
<ul>
|
||||
${tasks.pending.slice(0, 20).map(t =>
|
||||
`<li>
|
||||
${t.text}
|
||||
${t.dueDate ? `(Due: ${api.dayjs(t.dueDate).format('MMM D')})` : ''}
|
||||
- <a href="#root/${t.noteId}">${t.noteTitle}</a>
|
||||
</li>`
|
||||
).join('')}
|
||||
</ul>
|
||||
${tasks.pending.length > 20 ? `<p><em>...and ${tasks.pending.length - 20} more</em></p>` : ''}
|
||||
|
||||
<h2>✅ Recently Completed (${tasks.completed.length})</h2>
|
||||
<ul>
|
||||
${tasks.completed.slice(0, 10).map(t =>
|
||||
`<li style="text-decoration: line-through; opacity: 0.7;">
|
||||
${t.text} - <a href="#root/${t.noteId}">${t.noteTitle}</a>
|
||||
</li>`
|
||||
).join('')}
|
||||
</ul>
|
||||
`;
|
||||
|
||||
await dashboard.setContent(dashboardContent);
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
return await aggregateTasks();</code></pre>
|
||||
</div>
|
||||
|
||||
<h2 id="frontend-script-recipes">Frontend Script Recipes</h2>
|
||||
|
||||
<h3>4. Quick Note Creator</h3>
|
||||
|
||||
<div class="use-case">
|
||||
<strong>Use Case:</strong> Add a floating button to quickly create notes from anywhere in the UI
|
||||
</div>
|
||||
|
||||
<div class="recipe-box frontend-script">
|
||||
<pre><code>// Create floating button
|
||||
const $button = $(`
|
||||
<div id="quick-note-btn" style="
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: #4CAF50;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
z-index: 10000;
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
">+</div>
|
||||
`);
|
||||
|
||||
// Create modal
|
||||
const $modal = $(`
|
||||
<div id="quick-note-modal" style="
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
|
||||
z-index: 10001;
|
||||
min-width: 400px;
|
||||
">
|
||||
<h3>Quick Note</h3>
|
||||
<input type="text" id="quick-note-title" placeholder="Title..." style="
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
">
|
||||
<textarea id="quick-note-content" placeholder="Content..." style="
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
resize: vertical;
|
||||
"></textarea>
|
||||
<div>
|
||||
<button id="quick-note-save" style="
|
||||
padding: 10px 20px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
">Save</button>
|
||||
<button id="quick-note-cancel" style="
|
||||
padding: 10px 20px;
|
||||
background: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Add to page
|
||||
$('body').append($button, $modal);
|
||||
|
||||
// Handle button click
|
||||
$button.click(() => {
|
||||
$modal.show();
|
||||
$('#quick-note-title').focus();
|
||||
});
|
||||
|
||||
// Handle save
|
||||
$('#quick-note-save').click(async () => {
|
||||
const title = $('#quick-note-title').val() || 'Quick Note';
|
||||
const content = $('#quick-note-content').val() || '';
|
||||
|
||||
// Get current note or use inbox
|
||||
const currentNote = api.getActiveContextNote();
|
||||
const parentNoteId = currentNote ? currentNote.noteId : (await api.getDayNote()).noteId;
|
||||
|
||||
// Create note
|
||||
const { note } = await api.runOnBackend(async (parentId, noteTitle, noteContent) => {
|
||||
const parent = await api.getNote(parentId);
|
||||
const newNote = await api.createNote(parent, noteTitle, noteContent, 'text');
|
||||
return { note: newNote.getPojo() };
|
||||
}, [parentNoteId, title, `<h2>${title}</h2><p>${content}</p>`]);
|
||||
|
||||
api.showMessage(`Note "${title}" created!`);
|
||||
|
||||
// Clear and close
|
||||
$('#quick-note-title').val('');
|
||||
$('#quick-note-content').val('');
|
||||
$modal.hide();
|
||||
|
||||
// Navigate to new note
|
||||
await api.activateNewNote(note.noteId);
|
||||
});
|
||||
|
||||
// Handle cancel
|
||||
$('#quick-note-cancel').click(() => {
|
||||
$modal.hide();
|
||||
});
|
||||
|
||||
// Keyboard shortcuts
|
||||
$(document).keydown((e) => {
|
||||
// Ctrl+Shift+N to open quick note
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'N') {
|
||||
e.preventDefault();
|
||||
$button.click();
|
||||
}
|
||||
|
||||
// Escape to close
|
||||
if (e.key === 'Escape' && $modal.is(':visible')) {
|
||||
$modal.hide();
|
||||
}
|
||||
});</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>5. Markdown Preview Toggle</h3>
|
||||
|
||||
<div class="use-case">
|
||||
<strong>Use Case:</strong> Add live markdown preview for text notes
|
||||
</div>
|
||||
|
||||
<div class="recipe-box frontend-script">
|
||||
<pre><code>// Create preview pane
|
||||
const $previewPane = $(`
|
||||
<div id="markdown-preview" style="
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 10px;
|
||||
width: 45%;
|
||||
height: calc(100% - 60px);
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
z-index: 100;
|
||||
">
|
||||
<div id="preview-content"></div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Create toggle button
|
||||
const $toggleBtn = $(`
|
||||
<button id="preview-toggle" style="
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 8px 15px;
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
z-index: 101;
|
||||
">
|
||||
<i class="bx bx-show"></i> Preview
|
||||
</button>
|
||||
`);
|
||||
|
||||
// Add to note detail
|
||||
$('.note-detail-text').css('position', 'relative').append($previewPane, $toggleBtn);
|
||||
|
||||
let previewVisible = false;
|
||||
|
||||
// Load markdown library (simplified conversion)
|
||||
const convertToMarkdown = (html) => {
|
||||
return html
|
||||
.replace(/<h1[^>]*>(.*?)<\/h1>/g, '# $1\n')
|
||||
.replace(/<h2[^>]*>(.*?)<\/h2>/g, '## $1\n')
|
||||
.replace(/<h3[^>]*>(.*?)<\/h3>/g, '### $1\n')
|
||||
.replace(/<p[^>]*>(.*?)<\/p>/g, '$1\n\n')
|
||||
.replace(/<strong[^>]*>(.*?)<\/strong>/g, '**$1**')
|
||||
.replace(/<em[^>]*>(.*?)<\/em>/g, '*$1*')
|
||||
.replace(/<code[^>]*>(.*?)<\/code>/g, '`$1`')
|
||||
.replace(/<ul[^>]*>/g, '')
|
||||
.replace(/<\/ul>/g, '\n')
|
||||
.replace(/<li[^>]*>(.*?)<\/li>/g, '- $1\n')
|
||||
.replace(/<br[^>]*>/g, '\n')
|
||||
.replace(/<[^>]+>/g, ''); // Remove remaining HTML tags
|
||||
};
|
||||
|
||||
// Toggle preview
|
||||
$toggleBtn.click(() => {
|
||||
previewVisible = !previewVisible;
|
||||
|
||||
if (previewVisible) {
|
||||
$previewPane.show();
|
||||
$('.note-detail-text .note-detail-editable').css('width', '50%');
|
||||
$toggleBtn.html('<i class="bx bx-hide"></i> Hide');
|
||||
updatePreview();
|
||||
} else {
|
||||
$previewPane.hide();
|
||||
$('.note-detail-text .note-detail-editable').css('width', '100%');
|
||||
$toggleBtn.html('<i class="bx bx-show"></i> Preview');
|
||||
}
|
||||
});
|
||||
|
||||
// Update preview function
|
||||
async function updatePreview() {
|
||||
if (!previewVisible) return;
|
||||
|
||||
const content = await api.getActiveContextTextEditor().getContent();
|
||||
const markdown = convertToMarkdown(content);
|
||||
|
||||
// Simple markdown to HTML conversion
|
||||
const html = markdown
|
||||
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
.replace(/`(.*?)`/g, '<code>$1</code>')
|
||||
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/^(?!<[h|u])(.+)$/gm, '<p>$1</p>');
|
||||
|
||||
$('#preview-content').html(html);
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h2 id="integration-with-external-services">Integration with External Services</h2>
|
||||
|
||||
<h3>6. GitHub Integration</h3>
|
||||
|
||||
<div class="use-case">
|
||||
<strong>Use Case:</strong> Sync GitHub issues with Trilium notes
|
||||
</div>
|
||||
|
||||
<div class="recipe-box backend-script">
|
||||
<pre><code>// Requires axios library
|
||||
const axios = require('axios');
|
||||
|
||||
class GitHubSync {
|
||||
constructor(token, repo) {
|
||||
this.token = token;
|
||||
this.repo = repo; // format: "owner/repo"
|
||||
this.apiBase = 'https://api.github.com';
|
||||
}
|
||||
|
||||
async getIssues(state = 'open') {
|
||||
const response = await axios.get(`${this.apiBase}/repos/${this.repo}/issues`, {
|
||||
headers: {
|
||||
'Authorization': `token ${this.token}`,
|
||||
'Accept': 'application/vnd.github.v3+json'
|
||||
},
|
||||
params: { state }
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async syncIssuesToNotes() {
|
||||
// Get or create GitHub folder
|
||||
let githubFolder = await api.getNoteWithLabel('githubSync');
|
||||
if (!githubFolder) {
|
||||
githubFolder = await api.createTextNote('root', 'GitHub Issues', '');
|
||||
await githubFolder.setLabel('githubSync');
|
||||
}
|
||||
|
||||
const issues = await this.getIssues();
|
||||
const syncedNotes = [];
|
||||
|
||||
for (const issue of issues) {
|
||||
// Check if issue note already exists
|
||||
let issueNote = await api.getNoteWithLabel(`github:issue:${issue.number}`);
|
||||
|
||||
const content = `
|
||||
<h1>${issue.title}</h1>
|
||||
|
||||
<table>
|
||||
<tr><th>Issue #</th><td>${issue.number}</td></tr>
|
||||
<tr><th>State</th><td>${issue.state}</td></tr>
|
||||
<tr><th>Author</th><td>${issue.user.login}</td></tr>
|
||||
<tr><th>Created</th><td>${api.dayjs(issue.created_at).format('YYYY-MM-DD HH:mm')}</td></tr>
|
||||
<tr><th>Labels</th><td>${issue.labels.map(l => l.name).join(', ')}</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Description</h2>
|
||||
<div style="background: #f5f5f5; padding: 10px; border-radius: 5px;">
|
||||
${issue.body || 'No description'}
|
||||
</div>
|
||||
|
||||
<h2>Links</h2>
|
||||
<ul>
|
||||
<li><a href="${issue.html_url}">View on GitHub</a></li>
|
||||
<li><a href="${issue.url}">API URL</a></li>
|
||||
</ul>
|
||||
`;
|
||||
|
||||
if (!issueNote) {
|
||||
// Create new note
|
||||
issueNote = await api.createNote(
|
||||
githubFolder,
|
||||
`#${issue.number}: ${issue.title}`,
|
||||
content
|
||||
);
|
||||
await issueNote.setLabel(`github:issue:${issue.number}`);
|
||||
} else {
|
||||
// Update existing note
|
||||
await issueNote.setContent(content);
|
||||
}
|
||||
|
||||
// Set labels based on issue state and labels
|
||||
await issueNote.setLabel('githubIssue');
|
||||
await issueNote.setLabel('state', issue.state);
|
||||
|
||||
for (const label of issue.labels) {
|
||||
await issueNote.setLabel(`gh:${label.name}`);
|
||||
}
|
||||
|
||||
syncedNotes.push({
|
||||
noteId: issueNote.noteId,
|
||||
issueNumber: issue.number,
|
||||
title: issue.title
|
||||
});
|
||||
}
|
||||
|
||||
api.log(`Synced ${syncedNotes.length} GitHub issues`);
|
||||
return syncedNotes;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const github = new GitHubSync(
|
||||
process.env.GITHUB_TOKEN || 'your-token',
|
||||
'your-org/your-repo'
|
||||
);
|
||||
|
||||
// Sync issues to notes
|
||||
const synced = await github.syncIssuesToNotes();
|
||||
return synced;</code></pre>
|
||||
</div>
|
||||
|
||||
<h2 id="best-practices">Best Practices</h2>
|
||||
|
||||
<h3>Error Handling</h3>
|
||||
|
||||
<div class="success">
|
||||
<p>Always wrap scripts in try-catch blocks:</p>
|
||||
<pre><code>async function safeScriptExecution() {
|
||||
try {
|
||||
// Your script code here
|
||||
const result = await riskyOperation();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result
|
||||
};
|
||||
} catch (error) {
|
||||
api.log(`Error in script: ${error.message}`, 'error');
|
||||
|
||||
// Create error report note
|
||||
const errorNote = await api.createTextNote(
|
||||
'root',
|
||||
`Script Error - ${api.dayjs().format('YYYY-MM-DD HH:mm:ss')}`,
|
||||
`
|
||||
<h1>Script Error</h1>
|
||||
<p><strong>Error:</strong> ${error.message}</p>
|
||||
<p><strong>Stack:</strong></p>
|
||||
<pre>${error.stack}</pre>
|
||||
<p><strong>Script:</strong> ${api.currentNote.title}</p>
|
||||
`
|
||||
);
|
||||
|
||||
await errorNote.setLabel('scriptError');
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return await safeScriptExecution();</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Performance Optimization</h3>
|
||||
|
||||
<div class="success">
|
||||
<p>Use batch operations and caching:</p>
|
||||
<pre><code>class OptimizedNoteProcessor {
|
||||
constructor() {
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
async processNotes(noteIds) {
|
||||
// Batch fetch notes
|
||||
const notes = await Promise.all(
|
||||
noteIds.map(id => this.getCachedNote(id))
|
||||
);
|
||||
|
||||
// Process in chunks to avoid memory issues
|
||||
const chunkSize = 100;
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < notes.length; i += chunkSize) {
|
||||
const chunk = notes.slice(i, i + chunkSize);
|
||||
const chunkResults = await Promise.all(
|
||||
chunk.map(note => this.processNote(note))
|
||||
);
|
||||
results.push(...chunkResults);
|
||||
|
||||
// Allow other operations
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async getCachedNote(noteId) {
|
||||
if (!this.cache.has(noteId)) {
|
||||
const note = await api.getNote(noteId);
|
||||
this.cache.set(noteId, note);
|
||||
}
|
||||
return this.cache.get(noteId);
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="warning">
|
||||
<h4>Key Takeaways</h4>
|
||||
<ol>
|
||||
<li><strong>Use Backend Scripts</strong> for data processing, automation, and integrations</li>
|
||||
<li><strong>Use Frontend Scripts</strong> for UI enhancements and user interactions</li>
|
||||
<li><strong>Always handle errors</strong> gracefully and provide meaningful feedback</li>
|
||||
<li><strong>Optimize performance</strong> with caching and batch operations</li>
|
||||
<li><strong>Organize complex scripts</strong> into modules for reusability</li>
|
||||
<li><strong>Test your scripts</strong> to ensure reliability</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<h4>Related Documentation</h4>
|
||||
<ul>
|
||||
<li><a href="https://triliumnext.github.io/Docs/api/Backend_Script_API.html">Backend Script API Reference</a></li>
|
||||
<li><a href="https://triliumnext.github.io/Docs/api/Frontend_Script_API.html">Frontend Script API Reference</a></li>
|
||||
<li><a href="Custom%20Widget%20Development.html">Custom Widget Development</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
1780
apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/WebSocket API.html
vendored
Normal file
1780
apps/server/src/assets/doc_notes/en/Developer Guide/API Documentation/WebSocket API.html
vendored
Normal file
File diff suppressed because it is too large
Load Diff
122
apps/server/src/assets/doc_notes/en/Developer Guide/Architecture.html
vendored
Normal file
122
apps/server/src/assets/doc_notes/en/Developer Guide/Architecture.html
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Trilium Architecture Documentation</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
||||
h1 { color: #2c3e50; }
|
||||
h2 { color: #34495e; margin-top: 30px; }
|
||||
ul { line-height: 1.8; }
|
||||
a { color: #3498db; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.overview { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; }
|
||||
.quick-start { background: #e8f5e9; padding: 20px; border-radius: 8px; margin: 20px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Trilium Architecture Documentation</h1>
|
||||
|
||||
<p>This comprehensive guide documents the architecture of Trilium Notes, providing developers with detailed information about the system's core components, data flow, and design patterns.</p>
|
||||
|
||||
<h2>Table of Contents</h2>
|
||||
<ol>
|
||||
<li><a href="Three-Layer-Cache-System.html">Three-Layer Cache System</a></li>
|
||||
<li><a href="Entity-System.html">Entity System</a></li>
|
||||
<li><a href="Widget-Based-UI-Architecture.html">Widget-Based UI Architecture</a></li>
|
||||
<li><a href="API-Architecture.html">API Architecture</a></li>
|
||||
<li><a href="Monorepo-Structure.html">Monorepo Structure</a></li>
|
||||
</ol>
|
||||
|
||||
<div class="overview">
|
||||
<h2>Overview</h2>
|
||||
<p>Trilium Notes is built as a TypeScript monorepo using NX, featuring a sophisticated architecture that balances performance, flexibility, and maintainability. The system is designed around several key architectural patterns:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Three-layer caching system</strong> for optimal performance across backend, frontend, and shared content</li>
|
||||
<li><strong>Entity-based data model</strong> supporting hierarchical note structures with multiple parent relationships</li>
|
||||
<li><strong>Widget-based UI architecture</strong> enabling modular and extensible interface components</li>
|
||||
<li><strong>Multiple API layers</strong> for internal operations, external integrations, and real-time synchronization</li>
|
||||
<li><strong>Monorepo structure</strong> facilitating code sharing and consistent development patterns</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="quick-start">
|
||||
<h2>Quick Start for Developers</h2>
|
||||
|
||||
<p>If you're new to Trilium development, start with these sections:</p>
|
||||
|
||||
<ol>
|
||||
<li><a href="Monorepo-Structure.html">Monorepo Structure</a> - Understand the project organization</li>
|
||||
<li><a href="Entity-System.html">Entity System</a> - Learn about the core data model</li>
|
||||
<li><a href="Three-Layer-Cache-System.html">Three-Layer Cache System</a> - Understand data flow and caching</li>
|
||||
</ol>
|
||||
|
||||
<p>For UI development, refer to:</p>
|
||||
<ul>
|
||||
<li><a href="Widget-Based-UI-Architecture.html">Widget-Based UI Architecture</a></li>
|
||||
</ul>
|
||||
|
||||
<p>For API integration, see:</p>
|
||||
<ul>
|
||||
<li><a href="API-Architecture.html">API Architecture</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Architecture Principles</h2>
|
||||
|
||||
<h3>Performance First</h3>
|
||||
<ul>
|
||||
<li>Lazy loading of note content</li>
|
||||
<li>Efficient caching at multiple layers</li>
|
||||
<li>Optimized database queries with prepared statements</li>
|
||||
</ul>
|
||||
|
||||
<h3>Flexibility</h3>
|
||||
<ul>
|
||||
<li>Support for multiple note types</li>
|
||||
<li>Extensible through scripting</li>
|
||||
<li>Plugin architecture for UI widgets</li>
|
||||
</ul>
|
||||
|
||||
<h3>Data Integrity</h3>
|
||||
<ul>
|
||||
<li>Transactional database operations</li>
|
||||
<li>Revision history for all changes</li>
|
||||
<li>Synchronization conflict resolution</li>
|
||||
</ul>
|
||||
|
||||
<h3>Security</h3>
|
||||
<ul>
|
||||
<li>Per-note encryption</li>
|
||||
<li>Protected sessions</li>
|
||||
<li>API authentication tokens</li>
|
||||
</ul>
|
||||
|
||||
<h2>Development Workflow</h2>
|
||||
|
||||
<ol>
|
||||
<li><strong>Setup Development Environment</strong>
|
||||
<pre><code>pnpm install
|
||||
pnpm run server:start</code></pre>
|
||||
</li>
|
||||
|
||||
<li><strong>Make Changes</strong>
|
||||
<ul>
|
||||
<li>Backend changes in <code>apps/server/src/</code></li>
|
||||
<li>Frontend changes in <code>apps/client/src/</code></li>
|
||||
<li>Shared code in <code>packages/</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li><strong>Test Your Changes</strong>
|
||||
<pre><code>pnpm test:all
|
||||
pnpm nx run <project>:lint</code></pre>
|
||||
</li>
|
||||
|
||||
<li><strong>Build for Production</strong>
|
||||
<pre><code>pnpm nx build server
|
||||
pnpm nx build client</code></pre>
|
||||
</li>
|
||||
</ol>
|
||||
</body>
|
||||
</html>
|
||||
367
apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/API-Architecture.html
vendored
Normal file
367
apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/API-Architecture.html
vendored
Normal file
@@ -0,0 +1,367 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>API Architecture</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; }
|
||||
h1 { color: #2c3e50; }
|
||||
h2 { color: #34495e; margin-top: 30px; }
|
||||
h3 { color: #7f8c8d; }
|
||||
pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }
|
||||
code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-family: 'Courier New', monospace; }
|
||||
.api-layer { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; }
|
||||
.internal-api { border-left: 4px solid #3498db; }
|
||||
.etapi { border-left: 4px solid #e67e22; }
|
||||
.websocket { border-left: 4px solid #9b59b6; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
||||
th { background: #f8f9fa; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>API Architecture</h1>
|
||||
|
||||
<p>Trilium provides multiple API layers for different use cases: Internal API for frontend-backend communication, ETAPI for external integrations, and WebSocket for real-time synchronization.</p>
|
||||
|
||||
<h2>API Layers Overview</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>API Layer</th>
|
||||
<th>Purpose</th>
|
||||
<th>Authentication</th>
|
||||
<th>Primary Users</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Internal API</td>
|
||||
<td>Frontend-backend communication</td>
|
||||
<td>Session-based</td>
|
||||
<td>Web/Desktop clients</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ETAPI</td>
|
||||
<td>External integrations</td>
|
||||
<td>Token-based</td>
|
||||
<td>Third-party apps, scripts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WebSocket</td>
|
||||
<td>Real-time sync</td>
|
||||
<td>Session/Token</td>
|
||||
<td>All clients</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="api-layer internal-api">
|
||||
<h2>Internal API</h2>
|
||||
<p><strong>Location:</strong> <code>/apps/server/src/routes/api/</code></p>
|
||||
|
||||
<p>The Internal API handles communication between Trilium's frontend and backend, providing full access to application functionality.</p>
|
||||
|
||||
<h3>Key Endpoints</h3>
|
||||
|
||||
<h4>Note Operations</h4>
|
||||
<pre><code>// Get note with content
|
||||
GET /api/notes/:noteId
|
||||
|
||||
// Update note
|
||||
PUT /api/notes/:noteId
|
||||
Body: {
|
||||
title?: string,
|
||||
content?: string,
|
||||
type?: string,
|
||||
mime?: string
|
||||
}
|
||||
|
||||
// Create note
|
||||
POST /api/notes/:parentNoteId/children
|
||||
Body: {
|
||||
title: string,
|
||||
type: string,
|
||||
content?: string,
|
||||
position?: number
|
||||
}
|
||||
|
||||
// Delete note
|
||||
DELETE /api/notes/:noteId</code></pre>
|
||||
|
||||
<h4>Tree Operations</h4>
|
||||
<pre><code>// Get tree structure
|
||||
GET /api/tree
|
||||
Query: {
|
||||
subTreeNoteId?: string,
|
||||
includeAttributes?: boolean
|
||||
}
|
||||
|
||||
// Move branch
|
||||
PUT /api/branches/:branchId/move
|
||||
Body: {
|
||||
parentNoteId: string,
|
||||
position: number
|
||||
}</code></pre>
|
||||
|
||||
<h4>Search Operations</h4>
|
||||
<pre><code>// Execute search
|
||||
GET /api/search
|
||||
Query: {
|
||||
query: string,
|
||||
fastSearch?: boolean,
|
||||
includeArchivedNotes?: boolean
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="api-layer etapi">
|
||||
<h2>ETAPI (External API)</h2>
|
||||
<p><strong>Location:</strong> <code>/apps/server/src/etapi/</code></p>
|
||||
|
||||
<p>ETAPI provides a stable, versioned API for external applications and scripts to interact with Trilium.</p>
|
||||
|
||||
<h3>Authentication</h3>
|
||||
<pre><code>// Creating ETAPI token
|
||||
POST /etapi/auth/login
|
||||
Body: {
|
||||
username: string,
|
||||
password: string
|
||||
}
|
||||
Response: {
|
||||
authToken: string
|
||||
}
|
||||
|
||||
// Using token in requests
|
||||
GET /etapi/notes/:noteId
|
||||
Headers: {
|
||||
Authorization: "authToken"
|
||||
}</code></pre>
|
||||
|
||||
<h3>Key Endpoints</h3>
|
||||
|
||||
<h4>Note CRUD Operations</h4>
|
||||
<pre><code>// Create note
|
||||
POST /etapi/notes
|
||||
Body: {
|
||||
parentNoteId: string,
|
||||
title: string,
|
||||
type: string,
|
||||
content?: string
|
||||
}
|
||||
|
||||
// Get note
|
||||
GET /etapi/notes/:noteId
|
||||
|
||||
// Update note content
|
||||
PUT /etapi/notes/:noteId/content
|
||||
Body: string | Buffer
|
||||
|
||||
// Delete note
|
||||
DELETE /etapi/notes/:noteId</code></pre>
|
||||
|
||||
<h4>Search</h4>
|
||||
<pre><code>// Search notes
|
||||
GET /etapi/notes/search
|
||||
Query: {
|
||||
search: string,
|
||||
limit?: number,
|
||||
orderBy?: string
|
||||
}</code></pre>
|
||||
|
||||
<h3>Client Example (JavaScript)</h3>
|
||||
<pre><code>class EtapiClient {
|
||||
constructor(serverUrl, authToken) {
|
||||
this.serverUrl = serverUrl;
|
||||
this.authToken = authToken;
|
||||
}
|
||||
|
||||
async getNote(noteId) {
|
||||
const response = await fetch(
|
||||
`${this.serverUrl}/etapi/notes/${noteId}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': this.authToken
|
||||
}
|
||||
}
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async createNote(parentNoteId, title, content) {
|
||||
const response = await fetch(
|
||||
`${this.serverUrl}/etapi/notes`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': this.authToken,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
parentNoteId,
|
||||
title,
|
||||
type: 'text',
|
||||
content
|
||||
})
|
||||
}
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="api-layer websocket">
|
||||
<h2>WebSocket Real-time Synchronization</h2>
|
||||
<p><strong>Location:</strong> <code>/apps/server/src/services/ws.ts</code></p>
|
||||
|
||||
<p>WebSocket connections provide real-time updates and synchronization between clients.</p>
|
||||
|
||||
<h3>Message Types</h3>
|
||||
<ul>
|
||||
<li><code>entity-changes</code> - Entity updates</li>
|
||||
<li><code>sync</code> - Sync events</li>
|
||||
<li><code>note-content-change</code> - Content updates</li>
|
||||
<li><code>refresh-tree</code> - Tree structure changes</li>
|
||||
<li><code>options-changed</code> - Configuration updates</li>
|
||||
</ul>
|
||||
|
||||
<h3>Connection Example</h3>
|
||||
<pre><code>// Client connection
|
||||
const ws = new WebSocket('wss://server/ws');
|
||||
|
||||
ws.on('open', () => {
|
||||
// Authenticate
|
||||
ws.send(JSON.stringify({
|
||||
type: 'auth',
|
||||
token: sessionToken
|
||||
}));
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
const message = JSON.parse(data);
|
||||
handleWSMessage(message);
|
||||
});
|
||||
|
||||
// Handle messages
|
||||
function handleWSMessage(message) {
|
||||
switch (message.type) {
|
||||
case 'entity-changes':
|
||||
handleEntityChanges(message.data);
|
||||
break;
|
||||
case 'refresh-tree':
|
||||
froca.loadInitialTree();
|
||||
break;
|
||||
case 'note-content-change':
|
||||
handleContentChange(message.data);
|
||||
break;
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h2>API Security</h2>
|
||||
|
||||
<h3>Authentication Methods</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th>API</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Session-based</td>
|
||||
<td>Internal API</td>
|
||||
<td>Cookie-based sessions for web clients</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Token-based</td>
|
||||
<td>ETAPI</td>
|
||||
<td>Bearer tokens for external apps</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WebSocket auth</td>
|
||||
<td>WebSocket</td>
|
||||
<td>Initial auth message after connection</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Rate Limiting</h3>
|
||||
<pre><code>// Global rate limit
|
||||
const globalLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 1000 // limit each IP to 1000 requests
|
||||
});
|
||||
|
||||
// Strict limit for authentication
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5,
|
||||
message: 'Too many authentication attempts'
|
||||
});</code></pre>
|
||||
|
||||
<h2>Performance Optimization</h2>
|
||||
|
||||
<h3>Batch Operations</h3>
|
||||
<pre><code>// Batch API endpoint
|
||||
router.post('/api/batch', async (req, res) => {
|
||||
const operations = req.body.operations;
|
||||
const results = [];
|
||||
|
||||
await sql.transactional(async () => {
|
||||
for (const op of operations) {
|
||||
const result = await executeOperation(op);
|
||||
results.push(result);
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ results });
|
||||
});</code></pre>
|
||||
|
||||
<h3>Streaming Responses</h3>
|
||||
<pre><code>// Stream large data
|
||||
router.get('/api/export', (req, res) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/x-ndjson',
|
||||
'Transfer-Encoding': 'chunked'
|
||||
});
|
||||
|
||||
const noteStream = createNoteExportStream();
|
||||
|
||||
noteStream.on('data', (note) => {
|
||||
res.write(JSON.stringify(note) + '\n');
|
||||
});
|
||||
|
||||
noteStream.on('end', () => {
|
||||
res.end();
|
||||
});
|
||||
});</code></pre>
|
||||
|
||||
<h2>Best Practices</h2>
|
||||
|
||||
<h3>API Design</h3>
|
||||
<ol>
|
||||
<li><strong>RESTful conventions:</strong> Use appropriate HTTP methods and status codes</li>
|
||||
<li><strong>Consistent naming:</strong> Use camelCase for JSON properties</li>
|
||||
<li><strong>Versioning:</strong> Version the API to maintain compatibility</li>
|
||||
<li><strong>Documentation:</strong> Keep OpenAPI spec up to date</li>
|
||||
</ol>
|
||||
|
||||
<h3>Security</h3>
|
||||
<ol>
|
||||
<li><strong>Authentication:</strong> Always verify user identity</li>
|
||||
<li><strong>Authorization:</strong> Check permissions for each operation</li>
|
||||
<li><strong>Validation:</strong> Validate all input data</li>
|
||||
<li><strong>Rate limiting:</strong> Prevent abuse with appropriate limits</li>
|
||||
</ol>
|
||||
|
||||
<h3>Performance</h3>
|
||||
<ol>
|
||||
<li><strong>Pagination:</strong> Limit response sizes with pagination</li>
|
||||
<li><strong>Caching:</strong> Cache frequently accessed data</li>
|
||||
<li><strong>Batch operations:</strong> Support bulk operations</li>
|
||||
<li><strong>Async processing:</strong> Use queues for long-running tasks</li>
|
||||
</ol>
|
||||
</body>
|
||||
</html>
|
||||
268
apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Entity-System.html
vendored
Normal file
268
apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Entity-System.html
vendored
Normal file
@@ -0,0 +1,268 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Entity System Architecture</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; }
|
||||
h1 { color: #2c3e50; }
|
||||
h2 { color: #34495e; margin-top: 30px; }
|
||||
h3 { color: #7f8c8d; }
|
||||
pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }
|
||||
code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-family: 'Courier New', monospace; }
|
||||
.entity-box { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #3498db; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
||||
th { background: #f8f9fa; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Entity System Architecture</h1>
|
||||
|
||||
<p>The Entity System forms the core data model of Trilium Notes, providing a flexible and powerful structure for organizing information. This document details the entities, their relationships, and usage patterns.</p>
|
||||
|
||||
<h2>Core Entities Overview</h2>
|
||||
|
||||
<div class="entity-box">
|
||||
<h3>BNote - Notes with Content and Metadata</h3>
|
||||
<p><strong>Location:</strong> <code>/apps/server/src/becca/entities/bnote.ts</code></p>
|
||||
|
||||
<p>Notes are the fundamental unit of information in Trilium. Each note can contain different types of content and maintain relationships with other notes.</p>
|
||||
|
||||
<h4>Properties</h4>
|
||||
<ul>
|
||||
<li><code>noteId</code> - Unique identifier</li>
|
||||
<li><code>title</code> - Display title</li>
|
||||
<li><code>type</code> - Content type (text, code, file, etc.)</li>
|
||||
<li><code>mime</code> - MIME type for content</li>
|
||||
<li><code>isProtected</code> - Encryption flag</li>
|
||||
<li><code>dateCreated</code> - Creation timestamp</li>
|
||||
<li><code>dateModified</code> - Last modification</li>
|
||||
</ul>
|
||||
|
||||
<h4>Note Types</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Use Case</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>text</code></td>
|
||||
<td>Rich text with HTML formatting</td>
|
||||
<td>General notes, documentation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>code</code></td>
|
||||
<td>Source code with syntax highlighting</td>
|
||||
<td>Code snippets, scripts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file</code></td>
|
||||
<td>Binary file attachment</td>
|
||||
<td>PDFs, documents, archives</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>image</code></td>
|
||||
<td>Image with preview</td>
|
||||
<td>Pictures, diagrams</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>search</code></td>
|
||||
<td>Saved search query</td>
|
||||
<td>Dynamic note collections</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>canvas</code></td>
|
||||
<td>Drawing canvas (Excalidraw)</td>
|
||||
<td>Diagrams, sketches</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>mermaid</code></td>
|
||||
<td>Mermaid diagram</td>
|
||||
<td>Flowcharts, graphs</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="entity-box">
|
||||
<h3>BBranch - Hierarchical Relationships</h3>
|
||||
<p><strong>Location:</strong> <code>/apps/server/src/becca/entities/bbranch.ts</code></p>
|
||||
|
||||
<p>Branches define the parent-child relationships between notes, allowing a note to have multiple parents (cloning).</p>
|
||||
|
||||
<h4>Key Features</h4>
|
||||
<ul>
|
||||
<li><strong>Multiple Parents:</strong> Notes can appear in multiple locations</li>
|
||||
<li><strong>Ordering:</strong> Explicit positioning among siblings</li>
|
||||
<li><strong>Prefixes:</strong> Optional labels for context (e.g., "Chapter 1:")</li>
|
||||
<li><strong>UI State:</strong> Expansion state persisted per branch</li>
|
||||
</ul>
|
||||
|
||||
<h4>Usage Example</h4>
|
||||
<pre><code>// Create parent-child relationship
|
||||
const branch = new BBranch({
|
||||
noteId: childNote.noteId,
|
||||
parentNoteId: parentNote.noteId,
|
||||
notePosition: 10
|
||||
});
|
||||
branch.save();
|
||||
|
||||
// Clone note to another parent
|
||||
const cloneBranch = childNote.cloneTo(otherParent.noteId);</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="entity-box">
|
||||
<h3>BAttribute - Key-Value Metadata</h3>
|
||||
<p><strong>Location:</strong> <code>/apps/server/src/becca/entities/battribute.ts</code></p>
|
||||
|
||||
<p>Attributes provide flexible metadata and relationships between notes.</p>
|
||||
|
||||
<h4>Types</h4>
|
||||
<ol>
|
||||
<li><strong>Labels:</strong> Key-value pairs for metadata</li>
|
||||
<li><strong>Relations:</strong> References to other notes</li>
|
||||
</ol>
|
||||
|
||||
<h4>Common Patterns</h4>
|
||||
<pre><code>// Add label
|
||||
note.addLabel("status", "active");
|
||||
note.addLabel("priority", "high");
|
||||
|
||||
// Add relation
|
||||
note.addRelation("template", templateNoteId);
|
||||
note.addRelation("renderNote", renderNoteId);
|
||||
|
||||
// Query by attributes
|
||||
const todos = becca.findAttributes("label", "todoItem");</code></pre>
|
||||
|
||||
<h4>System Attributes</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Attribute</th>
|
||||
<th>Type</th>
|
||||
<th>Purpose</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>#hidePromotedAttributes</code></td>
|
||||
<td>Label</td>
|
||||
<td>Hide promoted attributes in UI</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>#readOnly</code></td>
|
||||
<td>Label</td>
|
||||
<td>Prevent note editing</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>#workspace</code></td>
|
||||
<td>Label</td>
|
||||
<td>Workspace organization</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>~template</code></td>
|
||||
<td>Relation</td>
|
||||
<td>Note template reference</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>~renderNote</code></td>
|
||||
<td>Relation</td>
|
||||
<td>Custom rendering</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="entity-box">
|
||||
<h3>BRevision - Version History</h3>
|
||||
<p><strong>Location:</strong> <code>/apps/server/src/becca/entities/brevision.ts</code></p>
|
||||
|
||||
<p>Revisions provide version history and recovery capabilities.</p>
|
||||
|
||||
<h4>Revision Strategy</h4>
|
||||
<ul>
|
||||
<li>Created automatically on significant changes</li>
|
||||
<li>Configurable retention period</li>
|
||||
<li>Day/week/month/year retention rules</li>
|
||||
<li>Protected note revisions are encrypted</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="entity-box">
|
||||
<h3>BOption - Application Configuration</h3>
|
||||
<p><strong>Location:</strong> <code>/apps/server/src/becca/entities/boption.ts</code></p>
|
||||
|
||||
<p>Options store application and user preferences.</p>
|
||||
|
||||
<h4>Common Options</h4>
|
||||
<pre><code>// Theme settings
|
||||
setOption("theme", "dark");
|
||||
|
||||
// Protected session timeout
|
||||
setOption("protectedSessionTimeout", "600");
|
||||
|
||||
// Sync settings
|
||||
setOption("syncServerHost", "https://sync.server");</code></pre>
|
||||
</div>
|
||||
|
||||
<h2>Entity Relationships</h2>
|
||||
|
||||
<h3>Parent-Child Hierarchy</h3>
|
||||
<pre><code>// Single parent
|
||||
childNote.setParent(parentNote.noteId);
|
||||
|
||||
// Multiple parents (cloning)
|
||||
childNote.cloneTo(parent1.noteId);
|
||||
childNote.cloneTo(parent2.noteId);
|
||||
|
||||
// Get parents
|
||||
const parents = childNote.getParentNotes();
|
||||
|
||||
// Get children
|
||||
const children = parentNote.getChildNotes();</code></pre>
|
||||
|
||||
<h3>Attribute Relationships</h3>
|
||||
<pre><code>// Direct relations
|
||||
note.addRelation("author", authorNote.noteId);
|
||||
|
||||
// Bidirectional relations
|
||||
note1.addRelation("related", note2.noteId);
|
||||
note2.addRelation("related", note1.noteId);
|
||||
|
||||
// Get related notes
|
||||
const related = note.getRelations("related");</code></pre>
|
||||
|
||||
<h2>Best Practices</h2>
|
||||
|
||||
<h3>Entity Creation</h3>
|
||||
<pre><code>// Always use transactions for multiple operations
|
||||
sql.transactional(() => {
|
||||
const note = new BNote({...});
|
||||
note.save();
|
||||
|
||||
note.addLabel("status", "draft");
|
||||
note.addRelation("template", templateId);
|
||||
});</code></pre>
|
||||
|
||||
<h3>Entity Updates</h3>
|
||||
<pre><code>// Check existence before update
|
||||
const note = becca.getNote(noteId);
|
||||
if (note) {
|
||||
note.title = "Updated";
|
||||
note.save();
|
||||
}</code></pre>
|
||||
|
||||
<h3>Querying</h3>
|
||||
<pre><code>// Use indexed queries
|
||||
const attrs = becca.findAttributes("label", "task");
|
||||
|
||||
// Avoid N+1 queries
|
||||
const noteIds = [...];
|
||||
const notes = becca.getNotes(noteIds); // Single batch</code></pre>
|
||||
</body>
|
||||
</html>
|
||||
325
apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Monorepo-Structure.html
vendored
Normal file
325
apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Monorepo-Structure.html
vendored
Normal file
@@ -0,0 +1,325 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Monorepo Structure</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; }
|
||||
h1 { color: #2c3e50; }
|
||||
h2 { color: #34495e; margin-top: 30px; }
|
||||
h3 { color: #7f8c8d; }
|
||||
pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }
|
||||
code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-family: 'Courier New', monospace; }
|
||||
.directory-tree { background: #2c3e50; color: #ecf0f1; padding: 20px; border-radius: 8px; margin: 20px 0; }
|
||||
.app-section { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #3498db; }
|
||||
.package-section { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #27ae60; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
||||
th { background: #f8f9fa; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Monorepo Structure</h1>
|
||||
|
||||
<p>Trilium is organized as a TypeScript monorepo using NX, facilitating code sharing, consistent tooling, and efficient build processes.</p>
|
||||
|
||||
<h2>Project Organization</h2>
|
||||
|
||||
<div class="directory-tree">
|
||||
<pre>TriliumNext/Trilium/
|
||||
├── apps/ # Runnable applications
|
||||
│ ├── client/ # Frontend web application
|
||||
│ ├── server/ # Node.js backend server
|
||||
│ ├── desktop/ # Electron desktop application
|
||||
│ ├── web-clipper/ # Browser extension
|
||||
│ ├── db-compare/ # Database comparison tool
|
||||
│ ├── dump-db/ # Database dump utility
|
||||
│ └── edit-docs/ # Documentation editor
|
||||
├── packages/ # Shared libraries
|
||||
│ ├── commons/ # Shared interfaces and utilities
|
||||
│ ├── ckeditor5/ # Rich text editor
|
||||
│ ├── codemirror/ # Code editor
|
||||
│ ├── highlightjs/ # Syntax highlighting
|
||||
│ └── ckeditor5-*/ # CKEditor plugins
|
||||
├── docs/ # Documentation
|
||||
├── nx.json # NX workspace configuration
|
||||
├── package.json # Root package configuration
|
||||
├── pnpm-workspace.yaml # PNPM workspace configuration
|
||||
└── tsconfig.base.json # Base TypeScript configuration</pre>
|
||||
</div>
|
||||
|
||||
<h2>Applications</h2>
|
||||
|
||||
<div class="app-section">
|
||||
<h3>Client (/apps/client)</h3>
|
||||
<p>The frontend application shared by both server and desktop versions.</p>
|
||||
|
||||
<h4>Structure</h4>
|
||||
<pre>apps/client/
|
||||
├── src/
|
||||
│ ├── components/ # Core UI components
|
||||
│ ├── entities/ # Frontend entities
|
||||
│ ├── services/ # Business logic
|
||||
│ ├── widgets/ # UI widgets system
|
||||
│ └── desktop.ts # Entry point
|
||||
├── package.json
|
||||
└── vite.config.ts # Vite configuration</pre>
|
||||
|
||||
<h4>Key Files</h4>
|
||||
<ul>
|
||||
<li><code>desktop.ts</code> - Main application initialization</li>
|
||||
<li><code>services/froca.ts</code> - Frontend cache implementation</li>
|
||||
<li><code>widgets/basic_widget.ts</code> - Base widget class</li>
|
||||
<li><code>services/server.ts</code> - API communication layer</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="app-section">
|
||||
<h3>Server (/apps/server)</h3>
|
||||
<p>The Node.js backend providing API, database, and business logic.</p>
|
||||
|
||||
<h4>Structure</h4>
|
||||
<pre>apps/server/
|
||||
├── src/
|
||||
│ ├── becca/ # Backend cache system
|
||||
│ ├── routes/ # Express routes
|
||||
│ ├── etapi/ # External API
|
||||
│ ├── services/ # Business services
|
||||
│ ├── share/ # Note sharing
|
||||
│ └── main.ts # Server entry point
|
||||
├── package.json
|
||||
└── webpack.config.js # Webpack configuration</pre>
|
||||
|
||||
<h4>Key Services</h4>
|
||||
<ul>
|
||||
<li><code>services/sql.ts</code> - Database access layer</li>
|
||||
<li><code>services/sync.ts</code> - Synchronization logic</li>
|
||||
<li><code>services/ws.ts</code> - WebSocket server</li>
|
||||
<li><code>services/protected_session.ts</code> - Encryption handling</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="app-section">
|
||||
<h3>Desktop (/apps/desktop)</h3>
|
||||
<p>Electron wrapper for the desktop application.</p>
|
||||
|
||||
<h4>Key Components</h4>
|
||||
<ul>
|
||||
<li><code>main.ts</code> - Electron main process</li>
|
||||
<li><code>preload.ts</code> - Preload script</li>
|
||||
<li><code>electron-builder.yml</code> - Build configuration</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Packages</h2>
|
||||
|
||||
<div class="package-section">
|
||||
<h3>Commons (/packages/commons)</h3>
|
||||
<p>Shared TypeScript interfaces and utilities used across applications.</p>
|
||||
|
||||
<pre><code>// packages/commons/src/types.ts
|
||||
export interface NoteRow {
|
||||
noteId: string;
|
||||
title: string;
|
||||
type: string;
|
||||
mime: string;
|
||||
isProtected: boolean;
|
||||
dateCreated: string;
|
||||
dateModified: string;
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="package-section">
|
||||
<h3>CKEditor5 (/packages/ckeditor5)</h3>
|
||||
<p>Custom CKEditor5 build with Trilium-specific plugins.</p>
|
||||
|
||||
<h4>Custom Plugins</h4>
|
||||
<ul>
|
||||
<li><strong>Admonition:</strong> Note boxes with icons</li>
|
||||
<li><strong>Footnotes:</strong> Reference footnotes</li>
|
||||
<li><strong>Math:</strong> LaTeX equation rendering</li>
|
||||
<li><strong>Mermaid:</strong> Diagram integration</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Build System</h2>
|
||||
|
||||
<h3>Development Commands</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Command</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>pnpm install</code></td>
|
||||
<td>Install dependencies</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pnpm run server:start</code></td>
|
||||
<td>Start development server</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pnpm nx run desktop:serve</code></td>
|
||||
<td>Start desktop app</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pnpm test:all</code></td>
|
||||
<td>Run all tests</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pnpm nx build server</code></td>
|
||||
<td>Build server</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Build Commands</h3>
|
||||
<pre><code># Build specific project
|
||||
pnpm nx build server
|
||||
pnpm nx build client
|
||||
|
||||
# Build all projects
|
||||
pnpm nx run-many --target=build --all
|
||||
|
||||
# Production build
|
||||
pnpm nx build server --configuration=production
|
||||
|
||||
# Build only affected projects
|
||||
pnpm nx affected:build --base=main</code></pre>
|
||||
|
||||
<h2>Development Workflow</h2>
|
||||
|
||||
<h3>Initial Setup</h3>
|
||||
<pre><code># Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Enable corepack for pnpm
|
||||
corepack enable
|
||||
|
||||
# Build all packages
|
||||
pnpm nx run-many --target=build --all</code></pre>
|
||||
|
||||
<h3>Testing</h3>
|
||||
<pre><code># Run all tests
|
||||
pnpm test:all
|
||||
|
||||
# Run tests for specific project
|
||||
pnpm nx test server
|
||||
|
||||
# Run tests in watch mode
|
||||
pnpm nx test server --watch
|
||||
|
||||
# Generate coverage
|
||||
pnpm nx test server --coverage</code></pre>
|
||||
|
||||
<h3>Linting and Type Checking</h3>
|
||||
<pre><code># Lint specific project
|
||||
pnpm nx lint server
|
||||
|
||||
# Type check
|
||||
pnpm nx run server:typecheck
|
||||
|
||||
# Fix lint issues
|
||||
pnpm nx lint server --fix</code></pre>
|
||||
|
||||
<h2>TypeScript Configuration</h2>
|
||||
|
||||
<h3>Base Configuration</h3>
|
||||
<pre><code>{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "dom"],
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@triliumnext/commons": ["packages/commons/src/index.ts"]
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<h2>Build Optimization</h2>
|
||||
|
||||
<h3>NX Features</h3>
|
||||
<ul>
|
||||
<li><strong>Build Caching:</strong> Speeds up subsequent builds</li>
|
||||
<li><strong>Affected Commands:</strong> Build/test only changed code</li>
|
||||
<li><strong>Parallel Execution:</strong> Run tasks in parallel</li>
|
||||
<li><strong>Dependency Graph:</strong> Visualize project dependencies</li>
|
||||
</ul>
|
||||
|
||||
<h3>Optimization Commands</h3>
|
||||
<pre><code># Show project graph
|
||||
pnpm nx graph
|
||||
|
||||
# Clear cache
|
||||
pnpm nx reset
|
||||
|
||||
# Profile build performance
|
||||
pnpm nx build server --profile
|
||||
|
||||
# Run with cache disabled
|
||||
pnpm nx build server --skip-nx-cache</code></pre>
|
||||
|
||||
<h2>Production Builds</h2>
|
||||
|
||||
<h3>Docker Build</h3>
|
||||
<pre><code>FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json pnpm-lock.yaml ./
|
||||
RUN corepack enable && pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
RUN pnpm nx build server --configuration=production
|
||||
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/dist/apps/server ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
CMD ["node", "main.js"]</code></pre>
|
||||
|
||||
<h2>Best Practices</h2>
|
||||
|
||||
<h3>Project Structure</h3>
|
||||
<ol>
|
||||
<li><strong>Keep packages focused:</strong> Each package should have a single, clear purpose</li>
|
||||
<li><strong>Minimize circular dependencies:</strong> Use dependency graph to identify issues</li>
|
||||
<li><strong>Share common code:</strong> Extract shared logic to packages/commons</li>
|
||||
</ol>
|
||||
|
||||
<h3>Development</h3>
|
||||
<ol>
|
||||
<li><strong>Use NX generators:</strong> Generate consistent code structure</li>
|
||||
<li><strong>Leverage caching:</strong> Don't skip-nx-cache unless debugging</li>
|
||||
<li><strong>Run affected commands:</strong> Save time by only building/testing changed code</li>
|
||||
</ol>
|
||||
|
||||
<h2>Troubleshooting</h2>
|
||||
|
||||
<h3>Common Issues</h3>
|
||||
|
||||
<h4>Build Cache Issues</h4>
|
||||
<pre><code># Clear NX cache
|
||||
pnpm nx reset
|
||||
|
||||
# Clear node_modules and reinstall
|
||||
rm -rf node_modules
|
||||
pnpm install</code></pre>
|
||||
|
||||
<h4>Dependency Conflicts</h4>
|
||||
<pre><code># Check for duplicate packages
|
||||
pnpm list --depth=0
|
||||
|
||||
# Update all dependencies
|
||||
pnpm update --recursive</code></pre>
|
||||
|
||||
<h4>Debug Commands</h4>
|
||||
<pre><code># Verbose output
|
||||
pnpm nx build server --verbose
|
||||
|
||||
# Show affected projects
|
||||
pnpm nx print-affected --type=app --select=projects</code></pre>
|
||||
</body>
|
||||
</html>
|
||||
266
apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Three-Layer-Cache-System.html
vendored
Normal file
266
apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Three-Layer-Cache-System.html
vendored
Normal file
@@ -0,0 +1,266 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Three-Layer Cache System Architecture</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; }
|
||||
h1 { color: #2c3e50; }
|
||||
h2 { color: #34495e; margin-top: 30px; }
|
||||
h3 { color: #7f8c8d; }
|
||||
pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }
|
||||
code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-family: 'Courier New', monospace; }
|
||||
.cache-layer { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; }
|
||||
.becca { background: #e1f5fe; }
|
||||
.froca { background: #fff3e0; }
|
||||
.shaca { background: #f3e5f5; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
||||
th { background: #f8f9fa; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Three-Layer Cache System Architecture</h1>
|
||||
|
||||
<p>Trilium implements a sophisticated three-layer caching system to optimize performance and reduce database load. This architecture ensures fast access to frequently used data while maintaining consistency across different application contexts.</p>
|
||||
|
||||
<h2>Overview</h2>
|
||||
|
||||
<p>The three cache layers are:</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>Becca</strong> (Backend Cache) - Server-side entity cache</li>
|
||||
<li><strong>Froca</strong> (Frontend Cache) - Client-side mirror of backend data</li>
|
||||
<li><strong>Shaca</strong> (Share Cache) - Optimized cache for shared/published notes</li>
|
||||
</ol>
|
||||
|
||||
<div class="cache-layer becca">
|
||||
<h2>Becca (Backend Cache)</h2>
|
||||
|
||||
<p><strong>Location:</strong> <code>/apps/server/src/becca/</code></p>
|
||||
|
||||
<p>Becca is the authoritative cache layer that maintains all notes, branches, attributes, and options in server memory.</p>
|
||||
|
||||
<h3>Key Components</h3>
|
||||
|
||||
<h4>Becca Interface</h4>
|
||||
<pre><code>export default class Becca {
|
||||
loaded: boolean;
|
||||
notes: Record<string, BNote>;
|
||||
branches: Record<string, BBranch>;
|
||||
childParentToBranch: Record<string, BBranch>;
|
||||
attributes: Record<string, BAttribute>;
|
||||
attributeIndex: Record<string, BAttribute[]>;
|
||||
options: Record<string, BOption>;
|
||||
etapiTokens: Record<string, BEtapiToken>;
|
||||
allNoteSetCache: NoteSet | null;
|
||||
}</code></pre>
|
||||
|
||||
<h3>Features</h3>
|
||||
<ul>
|
||||
<li><strong>In-memory storage:</strong> All active entities are kept in memory for fast access</li>
|
||||
<li><strong>Lazy loading:</strong> Related entities (revisions, attachments) loaded on demand</li>
|
||||
<li><strong>Index structures:</strong> Optimized lookups via childParentToBranch and attributeIndex</li>
|
||||
<li><strong>Cache invalidation:</strong> Automatic cache updates on entity changes</li>
|
||||
<li><strong>Protected note decryption:</strong> On-demand decryption of encrypted content</li>
|
||||
</ul>
|
||||
|
||||
<h3>Usage Example</h3>
|
||||
<pre><code>import becca from "./becca/becca.js";
|
||||
|
||||
// Get a note
|
||||
const note = becca.getNote("noteId");
|
||||
|
||||
// Find attributes by type and name
|
||||
const labels = becca.findAttributes("label", "todoItem");
|
||||
|
||||
// Get branch relationships
|
||||
const branch = becca.getBranchFromChildAndParent(childId, parentId);</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="cache-layer froca">
|
||||
<h2>Froca (Frontend Cache)</h2>
|
||||
|
||||
<p><strong>Location:</strong> <code>/apps/client/src/services/froca.ts</code></p>
|
||||
|
||||
<p>Froca is the frontend mirror of Becca, maintaining a subset of backend data for client-side operations.</p>
|
||||
|
||||
<h3>Key Components</h3>
|
||||
|
||||
<pre><code>class FrocaImpl implements Froca {
|
||||
notes: Record<string, FNote>;
|
||||
branches: Record<string, FBranch>;
|
||||
attributes: Record<string, FAttribute>;
|
||||
attachments: Record<string, FAttachment>;
|
||||
blobPromises: Record<string, Promise<FBlob | null> | null>;
|
||||
}</code></pre>
|
||||
|
||||
<h3>Features</h3>
|
||||
<ul>
|
||||
<li><strong>Lazy loading:</strong> Notes loaded on-demand with their immediate context</li>
|
||||
<li><strong>Subtree loading:</strong> Efficient loading of note hierarchies</li>
|
||||
<li><strong>Real-time updates:</strong> WebSocket synchronization with backend changes</li>
|
||||
<li><strong>Search note support:</strong> Virtual branches for search results</li>
|
||||
<li><strong>Promise-based blob loading:</strong> Asynchronous content loading</li>
|
||||
</ul>
|
||||
|
||||
<h3>Loading Strategy</h3>
|
||||
<pre><code>// Initial load - loads root and immediate children
|
||||
await froca.loadInitialTree();
|
||||
|
||||
// Load subtree on demand
|
||||
const note = await froca.loadSubTree(noteId);
|
||||
|
||||
// Reload specific notes
|
||||
await froca.reloadNotes([noteId1, noteId2]);</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="cache-layer shaca">
|
||||
<h2>Shaca (Share Cache)</h2>
|
||||
|
||||
<p><strong>Location:</strong> <code>/apps/server/src/share/shaca/</code></p>
|
||||
|
||||
<p>Shaca is a specialized cache for publicly shared notes, optimized for read-only access.</p>
|
||||
|
||||
<h3>Key Components</h3>
|
||||
|
||||
<pre><code>export default class Shaca {
|
||||
notes: Record<string, SNote>;
|
||||
branches: Record<string, SBranch>;
|
||||
childParentToBranch: Record<string, SBranch>;
|
||||
attributes: Record<string, SAttribute>;
|
||||
attachments: Record<string, SAttachment>;
|
||||
aliasToNote: Record<string, SNote>;
|
||||
shareRootNote: SNote | null;
|
||||
shareIndexEnabled: boolean;
|
||||
}</code></pre>
|
||||
|
||||
<h3>Features</h3>
|
||||
<ul>
|
||||
<li><strong>Read-only optimization:</strong> Streamlined for public access</li>
|
||||
<li><strong>Alias support:</strong> URL-friendly note access via aliases</li>
|
||||
<li><strong>Share index:</strong> Optional indexing of all shared subtrees</li>
|
||||
<li><strong>Minimal memory footprint:</strong> Only shared content cached</li>
|
||||
<li><strong>Security isolation:</strong> Separate from main application cache</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Cache Interaction and Data Flow</h2>
|
||||
|
||||
<h3>Create/Update Flow</h3>
|
||||
<ol>
|
||||
<li>Client sends update request to API</li>
|
||||
<li>API updates Becca cache</li>
|
||||
<li>Becca persists change to database</li>
|
||||
<li>API pushes update to Froca via WebSocket</li>
|
||||
<li>Froca updates UI components</li>
|
||||
</ol>
|
||||
|
||||
<h3>Read Flow</h3>
|
||||
<ol>
|
||||
<li>Client requests note from Froca</li>
|
||||
<li>If cached: Return immediately</li>
|
||||
<li>If not cached: Fetch from API</li>
|
||||
<li>API retrieves from Becca</li>
|
||||
<li>Froca caches and returns data</li>
|
||||
</ol>
|
||||
|
||||
<h2>Performance Considerations</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cache Layer</th>
|
||||
<th>Memory Usage</th>
|
||||
<th>Loading Strategy</th>
|
||||
<th>Use Case</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Becca</td>
|
||||
<td>100-500MB typical</td>
|
||||
<td>Full load on startup</td>
|
||||
<td>Server operations</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Froca</td>
|
||||
<td>Variable (on-demand)</td>
|
||||
<td>Progressive loading</td>
|
||||
<td>Client UI</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Shaca</td>
|
||||
<td>Minimal</td>
|
||||
<td>Lazy loading</td>
|
||||
<td>Public sharing</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Best Practices</h2>
|
||||
|
||||
<h3>When to Use Each Cache</h3>
|
||||
|
||||
<p><strong>Use Becca when:</strong></p>
|
||||
<ul>
|
||||
<li>Implementing server-side business logic</li>
|
||||
<li>Performing bulk operations</li>
|
||||
<li>Handling synchronization</li>
|
||||
<li>Managing protected notes</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Use Froca when:</strong></p>
|
||||
<ul>
|
||||
<li>Building UI components</li>
|
||||
<li>Handling user interactions</li>
|
||||
<li>Displaying note content</li>
|
||||
<li>Managing client state</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Use Shaca when:</strong></p>
|
||||
<ul>
|
||||
<li>Serving public content</li>
|
||||
<li>Building share pages</li>
|
||||
<li>Implementing read-only access</li>
|
||||
<li>Creating public APIs</li>
|
||||
</ul>
|
||||
|
||||
<h3>Cache Invalidation</h3>
|
||||
<pre><code>// Becca - automatic on entity save
|
||||
note.save(); // Cache updated automatically
|
||||
|
||||
// Froca - manual reload when needed
|
||||
await froca.reloadNotes([noteId]);
|
||||
|
||||
// Shaca - rebuild on share changes
|
||||
shaca.reset();
|
||||
shaca.load();</code></pre>
|
||||
|
||||
<h2>Troubleshooting</h2>
|
||||
|
||||
<h3>Common Issues</h3>
|
||||
|
||||
<ol>
|
||||
<li><strong>Cache Inconsistency</strong>
|
||||
<ul>
|
||||
<li>Symptom: UI shows outdated data</li>
|
||||
<li>Solution: Force reload with <code>froca.reloadNotes()</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li><strong>Memory Growth</strong>
|
||||
<ul>
|
||||
<li>Symptom: Server memory usage increases</li>
|
||||
<li>Solution: Check for memory leaks in custom scripts</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li><strong>Slow Initial Load</strong>
|
||||
<ul>
|
||||
<li>Symptom: Long startup time</li>
|
||||
<li>Solution: Optimize database queries, add indexes</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</body>
|
||||
</html>
|
||||
286
apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Widget-Based-UI-Architecture.html
vendored
Normal file
286
apps/server/src/assets/doc_notes/en/Developer Guide/Architecture/Widget-Based-UI-Architecture.html
vendored
Normal file
@@ -0,0 +1,286 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Widget-Based UI Architecture</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; }
|
||||
h1 { color: #2c3e50; }
|
||||
h2 { color: #34495e; margin-top: 30px; }
|
||||
h3 { color: #7f8c8d; }
|
||||
pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }
|
||||
code { background: #f4f4f4; padding: 2px 5px; border-radius: 3px; font-family: 'Courier New', monospace; }
|
||||
.widget-class { background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #27ae60; }
|
||||
.example-box { background: #ecf0f1; padding: 15px; border-radius: 5px; margin: 15px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Widget-Based UI Architecture</h1>
|
||||
|
||||
<p>Trilium's frontend is built on a modular widget system that provides flexibility, reusability, and maintainability. This architecture enables dynamic UI composition and extensibility through custom widgets.</p>
|
||||
|
||||
<h2>Widget System Overview</h2>
|
||||
|
||||
<p>The widget hierarchy follows an inheritance pattern where each level adds specific functionality:</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>Component</strong> - Base class for all UI components</li>
|
||||
<li><strong>BasicWidget</strong> - UI foundation with DOM manipulation</li>
|
||||
<li><strong>NoteContextAwareWidget</strong> - Note-aware components</li>
|
||||
<li><strong>RightPanelWidget</strong> - Side panel widgets</li>
|
||||
<li><strong>TypeWidgets</strong> - Note type specific widgets</li>
|
||||
<li><strong>CustomWidgets</strong> - User-created widgets</li>
|
||||
</ol>
|
||||
|
||||
<div class="widget-class">
|
||||
<h2>Core Widget Classes</h2>
|
||||
|
||||
<h3>BasicWidget</h3>
|
||||
<p><strong>Location:</strong> <code>/apps/client/src/widgets/basic_widget.ts</code></p>
|
||||
|
||||
<p>Base class for all UI widgets, providing DOM manipulation and styling capabilities.</p>
|
||||
|
||||
<h4>Key Methods</h4>
|
||||
<ul>
|
||||
<li><code>id(id: string)</code> - Set widget ID</li>
|
||||
<li><code>class(className: string)</code> - Add CSS class</li>
|
||||
<li><code>css(name: string, value: string)</code> - Set CSS property</li>
|
||||
<li><code>child(...components)</code> - Add child widgets</li>
|
||||
<li><code>doRender()</code> - Render widget HTML</li>
|
||||
</ul>
|
||||
|
||||
<div class="example-box">
|
||||
<h4>Usage Example</h4>
|
||||
<pre><code>class MyWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div>')
|
||||
.addClass('my-widget')
|
||||
.append($('<h3>').text('Widget Title'));
|
||||
|
||||
return this.$widget;
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
this.$widget.find('h3').text(note.title);
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="widget-class">
|
||||
<h3>NoteContextAwareWidget</h3>
|
||||
<p><strong>Location:</strong> <code>/apps/client/src/widgets/note_context_aware_widget.ts</code></p>
|
||||
|
||||
<p>Base class for widgets that respond to note context changes.</p>
|
||||
|
||||
<h4>Lifecycle Methods</h4>
|
||||
<ul>
|
||||
<li><code>refreshWithNote(note)</code> - Called when note context changes</li>
|
||||
<li><code>noteSwitched()</code> - Called when user switches notes</li>
|
||||
<li><code>activeContextChanged()</code> - Called on context change</li>
|
||||
<li><code>noteTypeMimeChanged()</code> - React to note type changes</li>
|
||||
</ul>
|
||||
|
||||
<div class="example-box">
|
||||
<h4>Context Management Example</h4>
|
||||
<pre><code>class MyNoteWidget extends NoteContextAwareWidget {
|
||||
async refreshWithNote(note) {
|
||||
// Called when note context changes
|
||||
this.$widget.find('.note-title').text(note.title);
|
||||
this.$widget.find('.note-type').text(note.type);
|
||||
|
||||
// Access note attributes
|
||||
const labels = note.getLabels();
|
||||
const relations = note.getRelations();
|
||||
}
|
||||
|
||||
async noteSwitched() {
|
||||
// Called when user switches to different note
|
||||
console.log(`Switched to note: ${this.noteId}`);
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="widget-class">
|
||||
<h3>RightPanelWidget</h3>
|
||||
<p><strong>Location:</strong> <code>/apps/client/src/widgets/right_panel_widget.ts</code></p>
|
||||
|
||||
<p>Base class for widgets displayed in the right sidebar panel.</p>
|
||||
|
||||
<h4>Required Methods</h4>
|
||||
<ul>
|
||||
<li><code>getTitle()</code> - Widget title</li>
|
||||
<li><code>getIcon()</code> - Widget icon</li>
|
||||
<li><code>getPosition()</code> - Display order</li>
|
||||
<li><code>doRenderBody()</code> - Render widget content</li>
|
||||
</ul>
|
||||
|
||||
<div class="example-box">
|
||||
<h4>Right Panel Widget Example</h4>
|
||||
<pre><code>class InfoWidget extends RightPanelWidget {
|
||||
getTitle() { return "Note Info"; }
|
||||
getIcon() { return "info"; }
|
||||
getPosition() { return 100; }
|
||||
|
||||
async doRenderBody() {
|
||||
return $('<div class="info-widget">')
|
||||
.append($('<div class="created">'))
|
||||
.append($('<div class="modified">'))
|
||||
.append($('<div class="word-count">'));
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
this.$body.find('.created').text(`Created: ${note.dateCreated}`);
|
||||
this.$body.find('.modified').text(`Modified: ${note.dateModified}`);
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Type-Specific Widgets</h2>
|
||||
|
||||
<p><strong>Location:</strong> <code>/apps/client/src/widgets/type_widgets/</code></p>
|
||||
|
||||
<p>Each note type has a specialized widget for rendering and editing:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>TextTypeWidget</strong> - Rich text editor using CKEditor</li>
|
||||
<li><strong>CodeTypeWidget</strong> - Code editor using CodeMirror</li>
|
||||
<li><strong>FileTypeWidget</strong> - File attachment viewer</li>
|
||||
<li><strong>ImageTypeWidget</strong> - Image viewer with editing</li>
|
||||
<li><strong>CanvasTypeWidget</strong> - Excalidraw integration</li>
|
||||
<li><strong>MermaidTypeWidget</strong> - Mermaid diagram renderer</li>
|
||||
</ul>
|
||||
|
||||
<h2>Widget Communication</h2>
|
||||
|
||||
<h3>Event System</h3>
|
||||
<pre><code>// Publishing events
|
||||
class PublisherWidget extends BasicWidget {
|
||||
async handleClick() {
|
||||
// Local event
|
||||
this.trigger('itemSelected', { itemId: '123' });
|
||||
|
||||
// Global event
|
||||
appContext.triggerEvent('noteChanged', { noteId: this.noteId });
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribing to events
|
||||
class SubscriberWidget extends BasicWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Local event subscription
|
||||
this.on('itemSelected', (event) => {
|
||||
console.log('Item selected:', event.itemId);
|
||||
});
|
||||
|
||||
// Global event subscription
|
||||
appContext.addEventListener('noteChanged', (event) => {
|
||||
this.handleNoteChange(event.noteId);
|
||||
});
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<h2>Custom Widget Development</h2>
|
||||
|
||||
<h3>Creating Custom Widgets</h3>
|
||||
<pre><code>// 1. Define widget class
|
||||
class TaskListWidget extends NoteContextAwareWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div class="task-list-widget">');
|
||||
this.$list = $('<ul>').appendTo(this.$widget);
|
||||
return this.$widget;
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
const tasks = await this.loadTasks(note);
|
||||
|
||||
this.$list.empty();
|
||||
for (const task of tasks) {
|
||||
$('<li>')
|
||||
.text(task.title)
|
||||
.toggleClass('completed', task.completed)
|
||||
.appendTo(this.$list);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadTasks(note) {
|
||||
// Load task data from note attributes
|
||||
const taskLabels = note.getLabels('task');
|
||||
return taskLabels.map(label => JSON.parse(label.value));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Register widget
|
||||
api.addWidget(TaskListWidget);</code></pre>
|
||||
|
||||
<h2>Performance Optimization</h2>
|
||||
|
||||
<h3>Lazy Loading</h3>
|
||||
<pre><code>class LazyWidget extends BasicWidget {
|
||||
private contentLoaded = false;
|
||||
|
||||
async becomeVisible() {
|
||||
if (!this.contentLoaded) {
|
||||
await this.loadContent();
|
||||
this.contentLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadContent() {
|
||||
// Heavy content loading
|
||||
const data = await server.get('expensive-data');
|
||||
this.renderContent(data);
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<h3>Debouncing Updates</h3>
|
||||
<pre><code>class DebouncedWidget extends NoteContextAwareWidget {
|
||||
private refreshDebounced = utils.debounce(
|
||||
() => this.doRefresh(),
|
||||
500
|
||||
);
|
||||
|
||||
async refreshWithNote(note) {
|
||||
// Debounce rapid updates
|
||||
this.refreshDebounced();
|
||||
}
|
||||
|
||||
private async doRefresh() {
|
||||
// Actual refresh logic
|
||||
}
|
||||
}</code></pre>
|
||||
|
||||
<h2>Best Practices</h2>
|
||||
|
||||
<h3>Widget Design</h3>
|
||||
<ol>
|
||||
<li><strong>Single Responsibility:</strong> Each widget should have one clear purpose</li>
|
||||
<li><strong>Composition over Inheritance:</strong> Use composition for complex UIs</li>
|
||||
<li><strong>Lazy Initialization:</strong> Load resources only when needed</li>
|
||||
<li><strong>Event Cleanup:</strong> Remove event listeners in cleanup()</li>
|
||||
</ol>
|
||||
|
||||
<h3>Error Handling</h3>
|
||||
<pre><code>class ResilientWidget extends BasicWidget {
|
||||
async refreshWithNote(note) {
|
||||
try {
|
||||
await this.loadData(note);
|
||||
} catch (error) {
|
||||
this.showError('Failed to load data');
|
||||
console.error('Widget error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private showError(message) {
|
||||
this.$widget.html(`
|
||||
<div class="alert alert-danger">
|
||||
${message}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}</code></pre>
|
||||
</body>
|
||||
</html>
|
||||
1504
apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Backend Script Development.html
vendored
Normal file
1504
apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Backend Script Development.html
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1718
apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Custom Note Type Development.html
vendored
Normal file
1718
apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Custom Note Type Development.html
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,828 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Custom Widget Development Guide - Trilium Developer Guide</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.content {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
border-bottom: 3px solid #3498db;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
h2 {
|
||||
color: #34495e;
|
||||
margin-top: 30px;
|
||||
border-bottom: 1px solid #ecf0f1;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
h3 {
|
||||
color: #555;
|
||||
margin-top: 25px;
|
||||
}
|
||||
pre {
|
||||
background: #f8f8f8;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
code {
|
||||
background: #f3f4f6;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
blockquote {
|
||||
border-left: 4px solid #3498db;
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
ul, ol {
|
||||
margin: 16px 0;
|
||||
padding-left: 30px;
|
||||
}
|
||||
li {
|
||||
margin: 8px 0;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.info {
|
||||
background: #d1ecf1;
|
||||
border: 1px solid #17a2b8;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<h1>Custom Widget Development Guide</h1>
|
||||
|
||||
<p>This guide provides comprehensive instructions for creating custom widgets in Trilium Notes. Widgets are fundamental UI components that enable you to extend Trilium's functionality with custom interfaces and behaviors.</p>
|
||||
|
||||
<h2>Prerequisites</h2>
|
||||
|
||||
<p>Before developing custom widgets, ensure you have:
|
||||
- Basic knowledge of TypeScript/JavaScript
|
||||
- Understanding of jQuery and DOM manipulation
|
||||
- Familiarity with Trilium's note structure
|
||||
- A development environment with Trilium running locally</p>
|
||||
|
||||
<h2>Understanding Widget Architecture</h2>
|
||||
|
||||
<h3>Widget Hierarchy</h3>
|
||||
|
||||
<p>Trilium's widget system follows a hierarchical structure:</p>
|
||||
|
||||
<pre><code>Component (base class)
|
||||
└── BasicWidget
|
||||
├── NoteContextAwareWidget
|
||||
│ ├── TypeWidget (for note type widgets)
|
||||
│ └── RightPanelWidget
|
||||
└── Custom widgets (buttons, containers, etc.)
|
||||
</code></pre>
|
||||
|
||||
<h3>Core Widget Classes</h3>
|
||||
|
||||
<p>#### BasicWidget
|
||||
The foundation class for all widgets. Provides basic rendering, positioning, and visibility management.</p>
|
||||
|
||||
<pre><code class="language-typescript">import BasicWidget from "../widgets/basic_widget.js";
|
||||
|
||||
<p>class MyCustomWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div class="my-widget">Hello Widget</div>');
|
||||
}
|
||||
}
|
||||
</code></pre></p>
|
||||
|
||||
<p>#### NoteContextAwareWidget
|
||||
Extends BasicWidget to respond to note changes. Use this when your widget needs to update based on the active note.</p>
|
||||
|
||||
<pre><code class="language-typescript">import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js";
|
||||
|
||||
<p>class NoteInfoWidget extends NoteContextAwareWidget {
|
||||
async refreshWithNote(note) {
|
||||
if (!note) return;
|
||||
|
||||
this.$widget.find('.note-title').text(note.title);
|
||||
this.$widget.find('.note-type').text(note.type);
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(<code>
|
||||
<div class="note-info-widget">
|
||||
<div class="note-title"></div>
|
||||
<div class="note-type"></div>
|
||||
</div>
|
||||
</code>);
|
||||
}
|
||||
}
|
||||
</code></pre></p>
|
||||
|
||||
<p>#### RightPanelWidget
|
||||
Specialized widget for rendering panels in the right sidebar with a consistent card layout.</p>
|
||||
|
||||
<pre><code class="language-typescript">import RightPanelWidget from "../widgets/right_panel_widget.js";
|
||||
|
||||
<p>class StatisticsWidget extends RightPanelWidget {
|
||||
get widgetTitle() {
|
||||
return "Note Statistics";
|
||||
}
|
||||
|
||||
async doRenderBody() {
|
||||
this.$body.html(<code>
|
||||
<div class="stats-container">
|
||||
<div class="word-count">Words: <span>0</span></div>
|
||||
<div class="char-count">Characters: <span>0</span></div>
|
||||
</div>
|
||||
</code>);
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
const content = await note.getContent();
|
||||
const wordCount = content.split(/\s+/).length;
|
||||
const charCount = content.length;
|
||||
|
||||
this.$body.find('.word-count span').text(wordCount);
|
||||
this.$body.find('.char-count span').text(charCount);
|
||||
}
|
||||
}
|
||||
</code></pre></p>
|
||||
|
||||
<h2>Widget Lifecycle</h2>
|
||||
|
||||
<h3>Initialization Phase</h3>
|
||||
<li><strong>Constructor</strong>: Set up initial state and child widgets</li>
|
||||
<li><strong>render()</strong>: Called to create the widget's DOM structure</li>
|
||||
<li><strong>doRender()</strong>: Override this to create your widget's HTML</li>
|
||||
|
||||
<h3>Update Phase</h3>
|
||||
<li><strong>refresh()</strong>: Called when widget needs updating</li>
|
||||
<li><strong>refreshWithNote()</strong>: Called for NoteContextAwareWidget when note changes</li>
|
||||
<li><strong>Event handlers</strong>: Respond to various Trilium events</li>
|
||||
|
||||
<h3>Cleanup Phase</h3>
|
||||
<li><strong>cleanup()</strong>: Override to clean up resources, event listeners, etc.</li>
|
||||
<li><strong>remove()</strong>: Removes widget from DOM</li>
|
||||
|
||||
<h2>Event Handling</h2>
|
||||
|
||||
<h3>Subscribing to Events</h3>
|
||||
|
||||
<p>Widgets can listen to Trilium's event system:</p>
|
||||
|
||||
<pre><code class="language-typescript">class EventAwareWidget extends NoteContextAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
// Events are automatically subscribed based on method names
|
||||
}
|
||||
|
||||
// Called when entities are reloaded
|
||||
async entitiesReloadedEvent({ loadResults }) {
|
||||
console.log('Entities reloaded');
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
// Called when note content changes
|
||||
async noteContentChangedEvent({ noteId }) {
|
||||
if (this.noteId === noteId) {
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Called when active context changes
|
||||
async activeContextChangedEvent({ noteContext }) {
|
||||
this.noteContext = noteContext;
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3>Common Events</h3>
|
||||
|
||||
<p>- <code>noteSwitched</code>: Active note changed
|
||||
- <code>activeContextChanged</code>: Active tab/context changed
|
||||
- <code>entitiesReloaded</code>: Notes, branches, or attributes reloaded
|
||||
- <code>noteContentChanged</code>: Note content modified
|
||||
- <code>noteTypeMimeChanged</code>: Note type or MIME changed
|
||||
- <code>frocaReloaded</code>: Frontend cache reloaded</p>
|
||||
|
||||
<h2>State Management</h2>
|
||||
|
||||
<h3>Local State</h3>
|
||||
Store widget-specific state in instance properties:
|
||||
|
||||
<pre><code class="language-typescript">class StatefulWidget extends BasicWidget {
|
||||
constructor() {
|
||||
super();
|
||||
this.isExpanded = false;
|
||||
this.cachedData = null;
|
||||
}
|
||||
|
||||
toggleExpanded() {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
this.$widget.toggleClass('expanded', this.isExpanded);
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3>Persistent State</h3>
|
||||
Use options or attributes for persistent state:
|
||||
|
||||
<pre><code class="language-typescript">class PersistentWidget extends NoteContextAwareWidget {
|
||||
async saveState(state) {
|
||||
await server.put('options', {
|
||||
name: 'widgetState',
|
||||
value: JSON.stringify(state)
|
||||
});
|
||||
}
|
||||
|
||||
async loadState() {
|
||||
const option = await server.get('options/widgetState');
|
||||
return option ? JSON.parse(option.value) : {};
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h2>Accessing Trilium APIs</h2>
|
||||
|
||||
<h3>Frontend Services</h3>
|
||||
|
||||
<pre><code class="language-typescript">import froca from "../services/froca.js";
|
||||
import server from "../services/server.js";
|
||||
import linkService from "../services/link.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
|
||||
<p>class ApiWidget extends NoteContextAwareWidget {
|
||||
async doRenderBody() {
|
||||
// Access notes
|
||||
const note = await froca.getNote(this.noteId);
|
||||
|
||||
// Get attributes
|
||||
const attributes = note.getAttributes();
|
||||
|
||||
// Create links
|
||||
const $link = await linkService.createLink(note.noteId);
|
||||
|
||||
// Show notifications
|
||||
toastService.showMessage("Widget loaded");
|
||||
|
||||
// Open dialogs
|
||||
const result = await dialogService.confirm("Continue?");
|
||||
}
|
||||
}
|
||||
</code></pre></p>
|
||||
|
||||
<h3>Server Communication</h3>
|
||||
|
||||
<pre><code class="language-typescript">class ServerWidget extends BasicWidget {
|
||||
async loadData() {
|
||||
// GET request
|
||||
const data = await server.get('custom-api/data');
|
||||
|
||||
// POST request
|
||||
const result = await server.post('custom-api/process', {
|
||||
noteId: this.noteId,
|
||||
action: 'analyze'
|
||||
});
|
||||
|
||||
// PUT request
|
||||
await server.put(<code>notes/${this.noteId}</code>, {
|
||||
title: 'Updated Title'
|
||||
});
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h2>Styling Widgets</h2>
|
||||
|
||||
<h3>Inline Styles</h3>
|
||||
<pre><code class="language-typescript">class StyledWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div>');
|
||||
this.css('padding', '10px')
|
||||
.css('background-color', '#f0f0f0')
|
||||
.css('border-radius', '4px');
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3>CSS Classes</h3>
|
||||
<pre><code class="language-typescript">class ClassedWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div>');
|
||||
this.class('custom-widget')
|
||||
.class('bordered');
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3>CSS Blocks</h3>
|
||||
<pre><code class="language-typescript">class CSSBlockWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div class="my-widget">Content</div>');
|
||||
|
||||
this.cssBlock(<code>
|
||||
.my-widget {
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.my-widget:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
</code>);
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h2>Performance Optimization</h2>
|
||||
|
||||
<h3>Lazy Loading</h3>
|
||||
<pre><code class="language-typescript">class LazyWidget extends NoteContextAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
this.dataLoaded = false;
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
if (!this.isVisible()) {
|
||||
return; // Don't load if not visible
|
||||
}
|
||||
|
||||
if (!this.dataLoaded) {
|
||||
await this.loadExpensiveData();
|
||||
this.dataLoaded = true;
|
||||
}
|
||||
|
||||
this.updateDisplay();
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3>Debouncing Updates</h3>
|
||||
<pre><code class="language-typescript">import SpacedUpdate from "../services/spaced_update.js";
|
||||
|
||||
<p>class DebouncedWidget extends NoteContextAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
this.spacedUpdate = new SpacedUpdate(async () => {
|
||||
await this.performUpdate();
|
||||
}, 500); // 500ms delay
|
||||
}
|
||||
|
||||
async handleInput(value) {
|
||||
await this.spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
}
|
||||
</code></pre></p>
|
||||
|
||||
<h3>Caching</h3>
|
||||
<pre><code class="language-typescript">class CachedWidget extends NoteContextAwareWidget {
|
||||
constructor() {
|
||||
super();
|
||||
this.cache = new Map();
|
||||
}
|
||||
|
||||
async getProcessedData(noteId) {
|
||||
if (!this.cache.has(noteId)) {
|
||||
const data = await this.processExpensiveOperation(noteId);
|
||||
this.cache.set(noteId, data);
|
||||
}
|
||||
return this.cache.get(noteId);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h2>Debugging Widgets</h2>
|
||||
|
||||
<h3>Console Logging</h3>
|
||||
<pre><code class="language-typescript">class DebugWidget extends BasicWidget {
|
||||
doRender() {
|
||||
console.log('Widget rendering', this.componentId);
|
||||
console.time('render');
|
||||
|
||||
this.$widget = $('<div>');
|
||||
|
||||
console.timeEnd('render');
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3>Error Handling</h3>
|
||||
<pre><code class="language-typescript">class SafeWidget extends NoteContextAwareWidget {
|
||||
async refreshWithNote(note) {
|
||||
try {
|
||||
await this.riskyOperation();
|
||||
} catch (error) {
|
||||
console.error('Widget error:', error);
|
||||
this.logRenderingError(error);
|
||||
this.$widget.html('<div class="error">Failed to load</div>');
|
||||
}
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3>Development Tools</h3>
|
||||
<pre><code class="language-typescript">class DevWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div>');
|
||||
|
||||
// Add debug information in development
|
||||
if (window.glob.isDev) {
|
||||
this.$widget.attr('data-debug', 'true');
|
||||
this.$widget.append(<code>
|
||||
<div class="debug-info">
|
||||
Component ID: ${this.componentId}
|
||||
Position: ${this.position}
|
||||
</div>
|
||||
</code>);
|
||||
}
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h2>Complete Example: Note Statistics Widget</h2>
|
||||
|
||||
<p>Here's a complete example implementing a custom note statistics widget:</p>
|
||||
|
||||
<pre><code class="language-typescript">import RightPanelWidget from "../widgets/right_panel_widget.js";
|
||||
import server from "../services/server.js";
|
||||
import froca from "../services/froca.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import SpacedUpdate from "../services/spaced_update.js";
|
||||
|
||||
<p>class NoteStatisticsWidget extends RightPanelWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Initialize state
|
||||
this.statistics = {
|
||||
words: 0,
|
||||
characters: 0,
|
||||
paragraphs: 0,
|
||||
readingTime: 0,
|
||||
links: 0,
|
||||
images: 0
|
||||
};
|
||||
|
||||
// Debounce updates for performance
|
||||
this.spacedUpdate = new SpacedUpdate(async () => {
|
||||
await this.calculateStatistics();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
get widgetTitle() {
|
||||
return "Note Statistics";
|
||||
}
|
||||
|
||||
get help() {
|
||||
return {
|
||||
title: "Note Statistics",
|
||||
text: "Displays various statistics about the current note including word count, reading time, and more."
|
||||
};
|
||||
}
|
||||
|
||||
async doRenderBody() {
|
||||
this.$body.html(<code>
|
||||
<div class="note-statistics">
|
||||
<div class="stat-group">
|
||||
<h5>Content</h5>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Words:</span>
|
||||
<span class="stat-value" data-stat="words">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Characters:</span>
|
||||
<span class="stat-value" data-stat="characters">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Paragraphs:</span>
|
||||
<span class="stat-value" data-stat="paragraphs">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-group">
|
||||
<h5>Reading</h5>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Reading time:</span>
|
||||
<span class="stat-value" data-stat="readingTime">0 min</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-group">
|
||||
<h5>Elements</h5>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Links:</span>
|
||||
<span class="stat-value" data-stat="links">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Images:</span>
|
||||
<span class="stat-value" data-stat="images">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-actions">
|
||||
<button class="btn btn-sm refresh-stats">Refresh</button>
|
||||
<button class="btn btn-sm export-stats">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
</code>);
|
||||
|
||||
this.cssBlock(<code>
|
||||
.note-statistics {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.stat-group {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.stat-group:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.stat-group h5 {
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--muted-text-color);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.stat-actions {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stat-actions .btn {
|
||||
flex: 1;
|
||||
}
|
||||
</code>);
|
||||
|
||||
// Bind events
|
||||
this.$body.on('click', '.refresh-stats', () => this.handleRefresh());
|
||||
this.$body.on('click', '.export-stats', () => this.handleExport());
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
if (!note) {
|
||||
this.clearStatistics();
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule statistics calculation
|
||||
await this.spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
|
||||
async calculateStatistics() {
|
||||
try {
|
||||
const note = this.note;
|
||||
if (!note) return;
|
||||
|
||||
const content = await note.getContent();
|
||||
|
||||
if (note.type === 'text') {
|
||||
// Parse HTML content
|
||||
const $content = $('<div>').html(content);
|
||||
const textContent = $content.text();
|
||||
|
||||
// Calculate statistics
|
||||
this.statistics.words = this.countWords(textContent);
|
||||
this.statistics.characters = textContent.length;
|
||||
this.statistics.paragraphs = $content.find('p').length;
|
||||
this.statistics.readingTime = Math.ceil(this.statistics.words / 200);
|
||||
this.statistics.links = $content.find('a').length;
|
||||
this.statistics.images = $content.find('img').length;
|
||||
} else if (note.type === 'code') {
|
||||
// For code notes, count lines and characters
|
||||
const lines = content.split('\n');
|
||||
this.statistics.words = lines.length; // Show lines instead of words
|
||||
this.statistics.characters = content.length;
|
||||
this.statistics.paragraphs = 0;
|
||||
this.statistics.readingTime = 0;
|
||||
this.statistics.links = 0;
|
||||
this.statistics.images = 0;
|
||||
}
|
||||
|
||||
this.updateDisplay();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to calculate statistics:', error);
|
||||
toastService.showError("Failed to calculate statistics");
|
||||
}
|
||||
}
|
||||
|
||||
countWords(text) {
|
||||
const words = text.match(/\b\w+\b/g);
|
||||
return words ? words.length : 0;
|
||||
}
|
||||
|
||||
clearStatistics() {
|
||||
this.statistics = {
|
||||
words: 0,
|
||||
characters: 0,
|
||||
paragraphs: 0,
|
||||
readingTime: 0,
|
||||
links: 0,
|
||||
images: 0
|
||||
};
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
updateDisplay() {
|
||||
this.$body.find('[data-stat="words"]').text(this.statistics.words);
|
||||
this.$body.find('[data-stat="characters"]').text(this.statistics.characters);
|
||||
this.$body.find('[data-stat="paragraphs"]').text(this.statistics.paragraphs);
|
||||
this.$body.find('[data-stat="readingTime"]').text(<code>${this.statistics.readingTime} min</code>);
|
||||
this.$body.find('[data-stat="links"]').text(this.statistics.links);
|
||||
this.$body.find('[data-stat="images"]').text(this.statistics.images);
|
||||
}
|
||||
|
||||
async handleRefresh() {
|
||||
await this.calculateStatistics();
|
||||
toastService.showMessage("Statistics refreshed");
|
||||
}
|
||||
|
||||
async handleExport() {
|
||||
const note = this.note;
|
||||
if (!note) return;
|
||||
|
||||
const exportData = {
|
||||
noteId: note.noteId,
|
||||
title: note.title,
|
||||
statistics: this.statistics,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Create a CSV
|
||||
const csv = [
|
||||
'Metric,Value',
|
||||
<code>Words,${this.statistics.words}</code>,
|
||||
<code>Characters,${this.statistics.characters}</code>,
|
||||
<code>Paragraphs,${this.statistics.paragraphs}</code>,
|
||||
<code>Reading Time,${this.statistics.readingTime} minutes</code>,
|
||||
<code>Links,${this.statistics.links}</code>,
|
||||
<code>Images,${this.statistics.images}</code>
|
||||
].join('\n');
|
||||
|
||||
// Download CSV
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = <code>statistics-${note.noteId}.csv</code>;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toastService.showMessage("Statistics exported");
|
||||
}
|
||||
|
||||
async noteContentChangedEvent({ noteId }) {
|
||||
if (this.noteId === noteId) {
|
||||
await this.spacedUpdate.scheduleUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.$body.off('click');
|
||||
this.spacedUpdate = null;
|
||||
}
|
||||
}</p>
|
||||
|
||||
<p>export default NoteStatisticsWidget;
|
||||
</code></pre></p>
|
||||
|
||||
<h2>Best Practices</h2>
|
||||
|
||||
<h3>1. Memory Management</h3>
|
||||
- Clean up event listeners in <code>cleanup()</code>
|
||||
- Clear caches and timers when widget is destroyed
|
||||
- Avoid circular references
|
||||
|
||||
<h3>2. Performance</h3>
|
||||
- Use debouncing for frequent updates
|
||||
- Implement lazy loading for expensive operations
|
||||
- Cache computed values when appropriate
|
||||
|
||||
<h3>3. Error Handling</h3>
|
||||
- Always wrap async operations in try-catch
|
||||
- Provide user feedback for errors
|
||||
- Log errors for debugging
|
||||
|
||||
<h3>4. User Experience</h3>
|
||||
- Show loading states for async operations
|
||||
- Provide clear error messages
|
||||
- Ensure widgets are responsive
|
||||
|
||||
<h3>5. Code Organization</h3>
|
||||
- Keep widgets focused on a single responsibility
|
||||
- Extract reusable logic into services
|
||||
- Use composition over inheritance when possible
|
||||
|
||||
<h2>Troubleshooting</h2>
|
||||
|
||||
<h3>Widget Not Rendering</h3>
|
||||
- Check <code>doRender()</code> creates <code>this.$widget</code>
|
||||
- Verify widget is properly registered
|
||||
- Check console for errors
|
||||
|
||||
<h3>Events Not Firing</h3>
|
||||
- Ensure event method name matches pattern: <code>${eventName}Event</code>
|
||||
- Check event is being triggered
|
||||
- Verify widget is active/visible
|
||||
|
||||
<h3>State Not Persisting</h3>
|
||||
- Use options or attributes for persistence
|
||||
- Check save operations complete successfully
|
||||
- Verify data serialization
|
||||
|
||||
<h3>Performance Issues</h3>
|
||||
- Profile with browser dev tools
|
||||
- Implement caching and debouncing
|
||||
- Optimize DOM operations
|
||||
|
||||
<h2>Next Steps</h2>
|
||||
|
||||
<p>- Explore existing widgets in <code>/apps/client/src/widgets/</code> for examples
|
||||
- Review the Frontend Script API documentation
|
||||
- Join the Trilium community for support and sharing widgets</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1125
apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Frontend Script Development.html
vendored
Normal file
1125
apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Frontend Script Development.html
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1266
apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Theme Development Guide.html
vendored
Normal file
1266
apps/server/src/assets/doc_notes/en/Developer Guide/Plugin Development/Theme Development Guide.html
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,119 @@
|
||||
<h1>Anthropic Configuration Guide</h1>
|
||||
|
||||
<h2>Overview</h2>
|
||||
<p>Anthropic provides access to the Claude 3 family of models, known for their strong analytical capabilities, safety features, and large context windows. This guide will help you configure Anthropic as your AI provider in Trilium Notes.</p>
|
||||
|
||||
<h2>Getting Started</h2>
|
||||
|
||||
<h3>Step 1: Create an Anthropic Account</h3>
|
||||
<ol>
|
||||
<li>Visit <a href="https://console.anthropic.com/signup" target="_blank">Anthropic Console</a></li>
|
||||
<li>Sign up with your email address</li>
|
||||
<li>Verify your email</li>
|
||||
<li>Complete account setup and billing information</li>
|
||||
</ol>
|
||||
|
||||
<h3>Step 2: Generate an API Key</h3>
|
||||
<ol>
|
||||
<li>Log into the <a href="https://console.anthropic.com/" target="_blank">Anthropic Console</a></li>
|
||||
<li>Navigate to <strong>API Keys</strong> section</li>
|
||||
<li>Click <strong>"Create Key"</strong></li>
|
||||
<li>Name your key (e.g., "Trilium Integration")</li>
|
||||
<li><strong>Important:</strong> Copy and save the key immediately</li>
|
||||
<li>Store securely - the key won't be shown again</li>
|
||||
</ol>
|
||||
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Security Notice</p>
|
||||
<p>Your API key provides full access to your Anthropic account. Never share it publicly or commit it to version control.</p>
|
||||
</div>
|
||||
|
||||
<h3>Step 3: Configure in Trilium</h3>
|
||||
<ol>
|
||||
<li>Open Trilium Notes</li>
|
||||
<li>Go to <strong>Options <20> AI/LLM</strong></li>
|
||||
<li>Enable AI features</li>
|
||||
<li>Select <strong>Anthropic</strong> from the provider dropdown</li>
|
||||
<li>Enter your configuration:
|
||||
<ul>
|
||||
<li><strong>API Key:</strong> Your Anthropic API key (sk-ant-...)</li>
|
||||
<li><strong>Base URL:</strong> <code>https://api.anthropic.com</code> (default)</li>
|
||||
<li><strong>Default Model:</strong> Choose from available Claude models</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Click <strong>Test Connection</strong> to verify</li>
|
||||
</ol>
|
||||
|
||||
<h2>Available Models</h2>
|
||||
|
||||
<h3>Claude 3 Model Family</h3>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Model</th>
|
||||
<th>Best For</th>
|
||||
<th>Context Window</th>
|
||||
<th>Speed</th>
|
||||
<th>Cost (per 1M tokens)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>claude-3-opus-20240229</code></td>
|
||||
<td>Most capable, complex analysis, research</td>
|
||||
<td>200,000 tokens</td>
|
||||
<td>Slower</td>
|
||||
<td>$15 input / $75 output</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>claude-3-sonnet-20240229</code></td>
|
||||
<td>Balanced performance, general use</td>
|
||||
<td>200,000 tokens</td>
|
||||
<td>Medium</td>
|
||||
<td>$3 input / $15 output</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>claude-3-haiku-20240307</code></td>
|
||||
<td>Fast responses, simple tasks</td>
|
||||
<td>200,000 tokens</td>
|
||||
<td>Fastest</td>
|
||||
<td>$0.25 input / $1.25 output</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Configuration Options</h2>
|
||||
|
||||
<h3>Model Parameters</h3>
|
||||
<ul>
|
||||
<li><strong>Temperature (0.0-1.0):</strong> Controls response randomness
|
||||
<ul>
|
||||
<li>0.0-0.3: Precise, consistent responses</li>
|
||||
<li>0.4-0.7: Balanced creativity and accuracy</li>
|
||||
<li>0.8-1.0: Creative, varied outputs</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Max Tokens:</strong> Maximum response length
|
||||
<ul>
|
||||
<li>Default: 4096 tokens</li>
|
||||
<li>Maximum: Model-dependent (up to 200K)</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Best Practices</h2>
|
||||
|
||||
<h3>Prompt Engineering for Claude</h3>
|
||||
<ul>
|
||||
<li><strong>Be Direct:</strong> Claude responds well to clear, direct instructions</li>
|
||||
<li><strong>Use XML Tags:</strong> Structure complex prompts with XML-like tags for better organization</li>
|
||||
<li><strong>Provide Examples:</strong> Claude excels at following patterns from examples</li>
|
||||
<li><strong>Think Step-by-Step:</strong> For complex tasks, ask Claude to reason through steps</li>
|
||||
</ul>
|
||||
|
||||
<h2>Additional Resources</h2>
|
||||
<ul>
|
||||
<li><a href="https://docs.anthropic.com/" target="_blank">Anthropic API Documentation</a></li>
|
||||
<li><a href="https://console.anthropic.com/" target="_blank">Anthropic Console</a></li>
|
||||
<li><a href="https://www.anthropic.com/claude" target="_blank">Claude Model Information</a></li>
|
||||
</ul>
|
||||
@@ -0,0 +1,255 @@
|
||||
<h1>OpenAI Configuration Guide</h1>
|
||||
|
||||
<h2>Overview</h2>
|
||||
<p>OpenAI provides access to GPT-4, GPT-3.5-turbo, and other advanced language models through their API. This guide will help you set up and configure OpenAI as your AI provider in Trilium Notes.</p>
|
||||
|
||||
<h2>Getting Started</h2>
|
||||
|
||||
<h3>Step 1: Create an OpenAI Account</h3>
|
||||
<ol>
|
||||
<li>Visit <a href="https://platform.openai.com/signup" target="_blank">OpenAI Platform</a></li>
|
||||
<li>Sign up with your email or Google/Microsoft account</li>
|
||||
<li>Verify your email address</li>
|
||||
<li>Complete your profile information</li>
|
||||
</ol>
|
||||
|
||||
<h3>Step 2: Obtain an API Key</h3>
|
||||
<ol>
|
||||
<li>Navigate to <a href="https://platform.openai.com/api-keys" target="_blank">API Keys</a> in your OpenAI account</li>
|
||||
<li>Click <strong>"Create new secret key"</strong></li>
|
||||
<li>Give your key a descriptive name (e.g., "Trilium Notes")</li>
|
||||
<li><strong>Important:</strong> Copy the key immediately - it won't be shown again!</li>
|
||||
<li>Store the key securely (password manager recommended)</li>
|
||||
</ol>
|
||||
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Security Note</p>
|
||||
<p>Never share your API key or commit it to version control. Treat it like a password.</p>
|
||||
</div>
|
||||
|
||||
<h3>Step 3: Configure in Trilium</h3>
|
||||
<ol>
|
||||
<li>Open Trilium Notes</li>
|
||||
<li>Navigate to <strong>Options <20> AI/LLM</strong></li>
|
||||
<li>Enable AI features if not already enabled</li>
|
||||
<li>Select <strong>OpenAI</strong> from the provider dropdown</li>
|
||||
<li>Enter your configuration:
|
||||
<ul>
|
||||
<li><strong>API Key:</strong> Paste your OpenAI API key</li>
|
||||
<li><strong>Base URL:</strong> <code>https://api.openai.com/v1</code> (default)</li>
|
||||
<li><strong>Default Model:</strong> Select from available models (see below)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Click <strong>Test Connection</strong> to verify setup</li>
|
||||
</ol>
|
||||
|
||||
<h2>Available Models</h2>
|
||||
|
||||
<h3>Chat Models</h3>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Model</th>
|
||||
<th>Best For</th>
|
||||
<th>Context Window</th>
|
||||
<th>Cost (per 1K tokens)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>gpt-4-turbo-preview</code></td>
|
||||
<td>Complex reasoning, analysis, latest knowledge</td>
|
||||
<td>128,000 tokens</td>
|
||||
<td>$0.01 input / $0.03 output</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>gpt-4</code></td>
|
||||
<td>High-quality responses, complex tasks</td>
|
||||
<td>8,192 tokens</td>
|
||||
<td>$0.03 input / $0.06 output</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>gpt-4-32k</code></td>
|
||||
<td>Long documents, extensive context</td>
|
||||
<td>32,768 tokens</td>
|
||||
<td>$0.06 input / $0.12 output</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>gpt-3.5-turbo</code></td>
|
||||
<td>Quick responses, general use, cost-effective</td>
|
||||
<td>16,385 tokens</td>
|
||||
<td>$0.0005 input / $0.0015 output</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>gpt-3.5-turbo-16k</code></td>
|
||||
<td>Longer conversations, more context</td>
|
||||
<td>16,385 tokens</td>
|
||||
<td>$0.003 input / $0.004 output</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Embedding Models</h3>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Model</th>
|
||||
<th>Dimensions</th>
|
||||
<th>Performance</th>
|
||||
<th>Cost (per 1M tokens)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>text-embedding-3-small</code></td>
|
||||
<td>1,536</td>
|
||||
<td>Good, cost-effective</td>
|
||||
<td>$0.02</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>text-embedding-3-large</code></td>
|
||||
<td>3,072</td>
|
||||
<td>Best quality</td>
|
||||
<td>$0.13</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>text-embedding-ada-002</code></td>
|
||||
<td>1,536</td>
|
||||
<td>Legacy, still supported</td>
|
||||
<td>$0.10</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Configuration Options</h2>
|
||||
|
||||
<h3>Model Parameters</h3>
|
||||
<ul>
|
||||
<li><strong>Temperature (0.0-2.0):</strong> Controls randomness. Lower = more focused, Higher = more creative
|
||||
<ul>
|
||||
<li>0.0-0.3: Factual, deterministic responses</li>
|
||||
<li>0.4-0.7: Balanced (recommended)</li>
|
||||
<li>0.8-1.0: Creative, varied responses</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Max Tokens:</strong> Maximum response length (1 token H 0.75 words)
|
||||
<ul>
|
||||
<li>Default: 4000 tokens</li>
|
||||
<li>Adjust based on needs and cost considerations</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Top P (0.0-1.0):</strong> Nucleus sampling threshold
|
||||
<ul>
|
||||
<li>Default: 1.0 (consider all tokens)</li>
|
||||
<li>Lower values = more focused responses</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Advanced Settings</h3>
|
||||
<h4>Custom Endpoints</h4>
|
||||
<p>For Azure OpenAI or OpenAI-compatible services:</p>
|
||||
<pre><code>Base URL: https://your-resource.openai.azure.com/
|
||||
API Version: 2024-02-15-preview
|
||||
Deployment Name: your-deployment-name</code></pre>
|
||||
|
||||
<h4>Rate Limiting</h4>
|
||||
<p>Configure to avoid hitting API limits:</p>
|
||||
<ul>
|
||||
<li>Tier 1: 60 requests/minute, 200,000 tokens/minute</li>
|
||||
<li>Tier 2: 120 requests/minute, 400,000 tokens/minute</li>
|
||||
<li>Higher tiers available upon request</li>
|
||||
</ul>
|
||||
|
||||
<h2>Cost Management</h2>
|
||||
|
||||
<h3>Estimating Costs</h3>
|
||||
<p>Typical usage patterns and estimated monthly costs:</p>
|
||||
<ul>
|
||||
<li><strong>Light Use (Personal):</strong> ~$5-10/month
|
||||
<ul>
|
||||
<li>50 queries/day with GPT-3.5-turbo</li>
|
||||
<li>Basic embeddings for 1000 notes</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Regular Use (Professional):</strong> ~$20-50/month
|
||||
<ul>
|
||||
<li>100 queries/day with mix of GPT-4 and GPT-3.5</li>
|
||||
<li>Comprehensive embeddings for 5000 notes</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Heavy Use (Research/Business):</strong> ~$100+/month
|
||||
<ul>
|
||||
<li>200+ queries/day primarily with GPT-4</li>
|
||||
<li>Large-scale embeddings and regular updates</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3>Cost Optimization Tips</h3>
|
||||
<ol>
|
||||
<li><strong>Use GPT-3.5-turbo for simple queries</strong> - 60x cheaper than GPT-4</li>
|
||||
<li><strong>Enable response caching</strong> - Avoid repeated API calls</li>
|
||||
<li><strong>Set token limits</strong> - Prevent unexpectedly long responses</li>
|
||||
<li><strong>Use text-embedding-3-small</strong> - Good quality at low cost</li>
|
||||
<li><strong>Monitor usage</strong> - Check OpenAI dashboard regularly</li>
|
||||
</ol>
|
||||
|
||||
<h2>Troubleshooting</h2>
|
||||
|
||||
<h3>Common Issues</h3>
|
||||
|
||||
<div class="admonition info">
|
||||
<p class="admonition-title">Invalid API Key</p>
|
||||
<p><strong>Solution:</strong> Verify the key is copied correctly without spaces. Check it hasn't been revoked in your OpenAI dashboard.</p>
|
||||
</div>
|
||||
|
||||
<div class="admonition info">
|
||||
<p class="admonition-title">Rate Limit Exceeded</p>
|
||||
<p><strong>Solution:</strong> Wait a few minutes and retry. Consider upgrading your API tier or implementing request throttling.</p>
|
||||
</div>
|
||||
|
||||
<div class="admonition info">
|
||||
<p class="admonition-title">Model Not Found</p>
|
||||
<p><strong>Solution:</strong> Ensure you have access to the model. GPT-4 requires separate approval from OpenAI.</p>
|
||||
</div>
|
||||
|
||||
<div class="admonition info">
|
||||
<p class="admonition-title">Connection Timeout</p>
|
||||
<p><strong>Solution:</strong> Check your internet connection and firewall settings. Ensure port 443 is open for HTTPS.</p>
|
||||
</div>
|
||||
|
||||
<h2>Best Practices</h2>
|
||||
|
||||
<h3>Security</h3>
|
||||
<ul>
|
||||
<li>Rotate API keys monthly</li>
|
||||
<li>Use separate keys for development and production</li>
|
||||
<li>Monitor usage for unusual activity</li>
|
||||
<li>Set spending limits in OpenAI dashboard</li>
|
||||
</ul>
|
||||
|
||||
<h3>Performance</h3>
|
||||
<ul>
|
||||
<li>Start with smaller models and upgrade as needed</li>
|
||||
<li>Use streaming for better perceived performance</li>
|
||||
<li>Implement retry logic with exponential backoff</li>
|
||||
<li>Cache frequently requested information</li>
|
||||
</ul>
|
||||
|
||||
<h3>Quality</h3>
|
||||
<ul>
|
||||
<li>Provide clear, specific prompts</li>
|
||||
<li>Use system prompts to set behavior</li>
|
||||
<li>Include examples in prompts when needed</li>
|
||||
<li>Test different temperature settings</li>
|
||||
</ul>
|
||||
|
||||
<h2>Additional Resources</h2>
|
||||
<ul>
|
||||
<li><a href="https://platform.openai.com/docs" target="_blank">OpenAI API Documentation</a></li>
|
||||
<li><a href="https://platform.openai.com/usage" target="_blank">Usage Dashboard</a></li>
|
||||
<li><a href="https://openai.com/pricing" target="_blank">Pricing Calculator</a></li>
|
||||
<li><a href="https://status.openai.com/" target="_blank">API Status Page</a></li>
|
||||
<li><a href="https://cookbook.openai.com/" target="_blank">OpenAI Cookbook</a></li>
|
||||
</ul>
|
||||
274
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI/Features and Usage.html
generated
vendored
Normal file
274
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI/Features and Usage.html
generated
vendored
Normal file
@@ -0,0 +1,274 @@
|
||||
<h1>AI Features and Usage Guide</h1>
|
||||
|
||||
<h2>Overview</h2>
|
||||
<p>This guide explains how to effectively use AI features in Trilium Notes to enhance your knowledge management workflow.</p>
|
||||
|
||||
<h2>Chat with Notes</h2>
|
||||
|
||||
<h3>Getting Started</h3>
|
||||
<p>The Chat with Notes feature provides an interactive AI assistant that has access to your knowledge base.</p>
|
||||
|
||||
<h4>Accessing the Chat Interface</h4>
|
||||
<ol>
|
||||
<li><strong>Via Launcher Button:</strong> Click the "Chat with Notes" button in the left sidebar</li>
|
||||
<li><strong>Via Keyboard Shortcut:</strong> Press <code>Ctrl+Shift+C</code> (customizable)</li>
|
||||
<li><strong>Via Command Palette:</strong> Press <code>Ctrl+P</code> and type "chat"</li>
|
||||
</ol>
|
||||
|
||||
<h3>Basic Chat Operations</h3>
|
||||
|
||||
<h4>Asking Questions</h4>
|
||||
<p>The AI can answer questions based on your notes:</p>
|
||||
<pre><code>User: What are the main points from my meeting notes this week?
|
||||
AI: Based on your meeting notes from this week, here are the main points:
|
||||
- Monday: Project timeline discussed, deadline set for March 30
|
||||
- Wednesday: Budget approved for Q2
|
||||
- Friday: New team member onboarding planned</code></pre>
|
||||
|
||||
<h4>Summarizing Content</h4>
|
||||
<pre><code>User: Summarize my research notes on machine learning
|
||||
AI: Your machine learning research covers:
|
||||
1. Supervised learning algorithms (Random Forests, SVM)
|
||||
2. Neural network architectures
|
||||
3. Data preprocessing techniques
|
||||
4. Model evaluation metrics</code></pre>
|
||||
|
||||
<h2>AI-Powered Tools</h2>
|
||||
|
||||
<h3>Available Tools</h3>
|
||||
<p>The AI can execute various tools to interact with your notes:</p>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tool</th>
|
||||
<th>Function</th>
|
||||
<th>Example Usage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>search_notes</code></td>
|
||||
<td>Semantic search across notes</td>
|
||||
<td>"Find all notes about project management"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>keyword_search</code></td>
|
||||
<td>Exact keyword matching</td>
|
||||
<td>"Search for notes containing 'API documentation'"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>read_note</code></td>
|
||||
<td>Access full note content</td>
|
||||
<td>"Show me the content of my budget proposal"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>create_note</code></td>
|
||||
<td>Generate new notes</td>
|
||||
<td>"Create a note summarizing this discussion"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>update_note</code></td>
|
||||
<td>Modify existing notes</td>
|
||||
<td>"Add these action items to my todo list"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>manage_attributes</code></td>
|
||||
<td>Handle note metadata</td>
|
||||
<td>"Add #important label to my proposal"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>calendar_integration</code></td>
|
||||
<td>Work with date notes</td>
|
||||
<td>"What's on my calendar tomorrow?"</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Tool Execution Examples</h3>
|
||||
|
||||
<div class="admonition example">
|
||||
<p class="admonition-title">Research Assistant Example</p>
|
||||
<pre><code>User: Help me research quantum computing for my presentation
|
||||
|
||||
AI: I'll help you research quantum computing. Let me search your notes.
|
||||
[Executing search_notes: "quantum computing"]
|
||||
Found 3 notes on quantum topics.
|
||||
|
||||
[Executing read_note: "Quantum Basics"]
|
||||
You have notes covering superposition and entanglement.
|
||||
|
||||
[Executing create_note: "Quantum Computing Presentation Outline"]
|
||||
Created outline with:
|
||||
1. Introduction to quantum concepts
|
||||
2. Current applications
|
||||
3. Future possibilities</code></pre>
|
||||
</div>
|
||||
|
||||
<h2>Semantic Search</h2>
|
||||
|
||||
<h3>How It Works</h3>
|
||||
<p>Semantic search uses AI embeddings to find conceptually related content, even when exact keywords don't match.</p>
|
||||
|
||||
<h4>Benefits</h4>
|
||||
<ul>
|
||||
<li>Find related concepts without exact keywords</li>
|
||||
<li>Discover unexpected connections</li>
|
||||
<li>More intuitive search results</li>
|
||||
<li>Better context understanding</li>
|
||||
</ul>
|
||||
|
||||
<h4>Example Searches</h4>
|
||||
<pre><code>Query: "improving productivity"
|
||||
Finds: Time management, GTD methodology, focus strategies, tool optimization
|
||||
|
||||
Query: "customer satisfaction"
|
||||
Finds: User feedback, NPS scores, support tickets, product reviews</code></pre>
|
||||
|
||||
<h2>Content Generation</h2>
|
||||
|
||||
<h3>Note Creation</h3>
|
||||
<p>AI can help create various types of notes:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Meeting Summaries:</strong> "Create a summary of today's standup discussion"</li>
|
||||
<li><strong>Research Outlines:</strong> "Generate an outline for my thesis on climate change"</li>
|
||||
<li><strong>Documentation:</strong> "Create API documentation from these code snippets"</li>
|
||||
<li><strong>Reports:</strong> "Generate a weekly progress report from my task notes"</li>
|
||||
</ul>
|
||||
|
||||
<h3>Content Enhancement</h3>
|
||||
<p>Improve existing notes with AI assistance:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Expansion:</strong> "Expand the bullet points in my ideas note"</li>
|
||||
<li><strong>Clarification:</strong> "Rewrite this technical explanation for beginners"</li>
|
||||
<li><strong>Formatting:</strong> "Convert my rough notes into a structured document"</li>
|
||||
<li><strong>Translation:</strong> "Translate this note to Spanish"</li>
|
||||
</ul>
|
||||
|
||||
<h2>Workflow Integration</h2>
|
||||
|
||||
<h3>Daily Review Workflow</h3>
|
||||
<pre><code>User: Give me my daily review
|
||||
|
||||
AI: Daily Review for [Today's Date]:
|
||||
✅ Completed Tasks:
|
||||
- Reviewed design mockups
|
||||
- Sent project update
|
||||
- Prepared agenda
|
||||
|
||||
📝 Notes Created:
|
||||
- "Client Feedback"
|
||||
- "API Research"
|
||||
- "Team Notes"
|
||||
|
||||
📅 Tomorrow:
|
||||
- Project review at 10 AM
|
||||
- Sprint planning at 2 PM</code></pre>
|
||||
|
||||
<h3>Research Workflow</h3>
|
||||
<ol>
|
||||
<li><strong>Gather Information:</strong> "Find all notes about [topic]"</li>
|
||||
<li><strong>Synthesize:</strong> "Create a summary of these research notes"</li>
|
||||
<li><strong>Organize:</strong> "Suggest a structure for organizing this information"</li>
|
||||
<li><strong>Generate:</strong> "Create a draft based on this research"</li>
|
||||
</ol>
|
||||
|
||||
<h2>Best Practices</h2>
|
||||
|
||||
<h3>Effective Prompting</h3>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Instead of...</th>
|
||||
<th>Try...</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>"Tell me about my notes"</td>
|
||||
<td>"Summarize my project management notes from Q1"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>"What should I do?"</td>
|
||||
<td>"Based on my project timeline, what tasks should I prioritize?"</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>"Give me information"</td>
|
||||
<td>"Create a bullet-point summary of my research on AI"</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Managing Conversations</h3>
|
||||
<ul>
|
||||
<li><strong>Start fresh:</strong> Begin new chats for different topics</li>
|
||||
<li><strong>Be specific:</strong> Reference note titles when possible</li>
|
||||
<li><strong>Iterate:</strong> Use follow-up questions to refine responses</li>
|
||||
<li><strong>Provide context:</strong> Include relevant background information</li>
|
||||
</ul>
|
||||
|
||||
<h3>Performance Tips</h3>
|
||||
<ul>
|
||||
<li><strong>For speed:</strong> Use simpler models for basic tasks</li>
|
||||
<li><strong>For quality:</strong> Provide examples of desired output</li>
|
||||
<li><strong>For cost:</strong> Enable caching and batch similar queries</li>
|
||||
<li><strong>For relevance:</strong> Keep your notes well-organized and labeled</li>
|
||||
</ul>
|
||||
|
||||
<h2>Advanced Usage Patterns</h2>
|
||||
|
||||
<h3>Creating Knowledge Graphs</h3>
|
||||
<pre><code>User: Create a knowledge graph of my programming notes
|
||||
|
||||
AI: Analyzing your programming notes...
|
||||
[Creates visual representation showing connections between:
|
||||
- Languages (Python, JavaScript, Java)
|
||||
- Frameworks (React, Django, Spring)
|
||||
- Concepts (OOP, Functional, Design Patterns)
|
||||
- Projects (Web App, API, Mobile)]</code></pre>
|
||||
|
||||
<h3>Automated Analysis</h3>
|
||||
<pre><code>User: Analyze sentiment in my journal entries this month
|
||||
|
||||
AI: Sentiment Analysis - Past 30 Days:
|
||||
📈 Positive Trend: Mood improving over time
|
||||
😊 Positive themes: achievements, learning, relationships
|
||||
😟 Challenges: workload, deadlines
|
||||
💡 Suggestion: Schedule breaks after intense work periods</code></pre>
|
||||
|
||||
<h2>Privacy and Control</h2>
|
||||
|
||||
<h3>Excluding Notes from AI</h3>
|
||||
<p>To prevent specific notes from being accessed by AI:</p>
|
||||
<ol>
|
||||
<li>Add the <code>#excludeFromAI</code> label to sensitive notes</li>
|
||||
<li>These notes won't be included in searches or context</li>
|
||||
<li>Perfect for personal or confidential information</li>
|
||||
</ol>
|
||||
|
||||
<h3>Data Usage</h3>
|
||||
<ul>
|
||||
<li>Only explicitly requested notes are sent to AI</li>
|
||||
<li>No automatic uploads or background processing</li>
|
||||
<li>You control what data the AI can access</li>
|
||||
</ul>
|
||||
|
||||
<h2>Troubleshooting Common Issues</h2>
|
||||
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">AI Not Finding Relevant Notes</p>
|
||||
<p><strong>Solution:</strong> Ensure embeddings are generated. Go to Settings → AI/LLM and click "Recreate All Embeddings".</p>
|
||||
</div>
|
||||
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Tools Not Executing</p>
|
||||
<p><strong>Solution:</strong> Verify tool calling is enabled in your AI settings and your provider supports function calling.</p>
|
||||
</div>
|
||||
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Slow Responses</p>
|
||||
<p><strong>Solution:</strong> Try using a faster model (GPT-3.5-turbo, Claude Haiku) or reduce the context size in settings.</p>
|
||||
</div>
|
||||
27
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI/Introduction.html
generated
vendored
27
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI/Introduction.html
generated
vendored
@@ -3,12 +3,27 @@
|
||||
height="1364">
|
||||
<figcaption>An example chat with an LLM</figcaption>
|
||||
</figure>
|
||||
<p>The AI / LLM features within Trilium Notes are designed to allow you to
|
||||
interact with your Notes in a variety of ways, using as many of the major
|
||||
providers as we can support. </p>
|
||||
<p>In addition to being able to send chats to LLM providers such as OpenAI,
|
||||
Anthropic, and Ollama - we also support agentic tool calling, and embeddings.</p>
|
||||
<p>The quickest way to get started is to navigate to the “AI/LLM” settings:</p>
|
||||
|
||||
<h2>Overview</h2>
|
||||
<p>The AI / LLM features within Trilium Notes are designed to enhance your note-taking and knowledge management experience through intelligent search, content generation, and interactive assistance. These features integrate seamlessly with your personal knowledge base while maintaining complete control over your data.</p>
|
||||
|
||||
<h3>Key Capabilities</h3>
|
||||
<ul>
|
||||
<li><strong>Chat with Notes</strong> - An interactive AI assistant that can answer questions based on your note content, provide summaries, and help discover connections</li>
|
||||
<li><strong>Semantic Search</strong> - Find conceptually related notes even when exact keywords don't match</li>
|
||||
<li><strong>Tool-Enabled Actions</strong> - AI can create, update, search, and manage your notes automatically</li>
|
||||
<li><strong>Content Generation</strong> - Generate summaries, expand ideas, and assist with writing</li>
|
||||
</ul>
|
||||
|
||||
<h3>Supported Providers</h3>
|
||||
<p>We support multiple AI providers to give you flexibility in choosing between cloud and local options:</p>
|
||||
<ul>
|
||||
<li><strong>OpenAI</strong> - GPT-4, GPT-3.5-turbo models with excellent general knowledge</li>
|
||||
<li><strong>Anthropic</strong> - Claude 3 family with strong analytical capabilities</li>
|
||||
<li><strong>Ollama</strong> - Run AI models locally for complete privacy and offline use</li>
|
||||
</ul>
|
||||
|
||||
<p>The quickest way to get started is to navigate to the "AI/LLM" settings:</p>
|
||||
<figure
|
||||
class="image image_resized" style="width:74.04%;">
|
||||
<img style="aspect-ratio:1916/1906;" src="5_Introduction_image.png" width="1916"
|
||||
|
||||
328
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI/Security and Privacy.html
generated
vendored
Normal file
328
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI/Security and Privacy.html
generated
vendored
Normal file
@@ -0,0 +1,328 @@
|
||||
<h1>Security and Privacy Guidelines</h1>
|
||||
|
||||
<h2>Overview</h2>
|
||||
<p>This document outlines important security considerations and privacy best practices for using AI features in Trilium Notes. Your privacy and data security are paramount.</p>
|
||||
|
||||
<h2>Data Privacy by Provider</h2>
|
||||
|
||||
<h3>Cloud Providers (OpenAI, Anthropic)</h3>
|
||||
|
||||
<div class="admonition info">
|
||||
<p class="admonition-title">What Gets Sent</p>
|
||||
<ul>
|
||||
<li>Selected note content based on your queries</li>
|
||||
<li>Your questions and prompts</li>
|
||||
<li>System instructions for AI behavior</li>
|
||||
<li>Tool execution parameters</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="admonition success">
|
||||
<p class="admonition-title">What Stays Private</p>
|
||||
<ul>
|
||||
<li>Notes marked with <code>#excludeFromAI</code> label</li>
|
||||
<li>Encrypted note content (unless explicitly decrypted)</li>
|
||||
<li>System metadata and file paths</li>
|
||||
<li>Other users' data in multi-user setups</li>
|
||||
<li>Your API keys and credentials</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h4>Provider Data Policies</h4>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Provider</th>
|
||||
<th>Data Usage</th>
|
||||
<th>Retention</th>
|
||||
<th>Compliance</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>OpenAI</td>
|
||||
<td>API data not used for training</td>
|
||||
<td>30 days for abuse monitoring</td>
|
||||
<td>SOC 2 Type II</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Anthropic</td>
|
||||
<td>No training on API inputs</td>
|
||||
<td>Limited retention period</td>
|
||||
<td>SOC 2 Type II</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Local Provider (Ollama)</h3>
|
||||
|
||||
<div class="admonition success">
|
||||
<p class="admonition-title">Complete Privacy with Ollama</p>
|
||||
<ul>
|
||||
<li>✅ No data leaves your machine</li>
|
||||
<li>✅ No external API calls</li>
|
||||
<li>✅ No usage tracking or telemetry</li>
|
||||
<li>✅ Works completely offline</li>
|
||||
<li>✅ You control all models and data</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Protecting Sensitive Information</h2>
|
||||
|
||||
<h3>Using the Exclusion System</h3>
|
||||
|
||||
<h4>Excluding Individual Notes</h4>
|
||||
<ol>
|
||||
<li>Open the note you want to protect</li>
|
||||
<li>Add the label: <code>#excludeFromAI</code></li>
|
||||
<li>The note will be completely excluded from AI processing</li>
|
||||
</ol>
|
||||
|
||||
<h4>Bulk Exclusion Script</h4>
|
||||
<pre><code>// Script to exclude all notes in a folder
|
||||
const folder = api.getNoteWithLabel('confidential');
|
||||
const descendants = folder.getDescendants();
|
||||
|
||||
for (const note of descendants) {
|
||||
note.addLabel('excludeFromAI');
|
||||
}
|
||||
|
||||
api.showMessage(`Excluded ${descendants.length} notes from AI`);</code></pre>
|
||||
|
||||
<h4>Verifying Exclusions</h4>
|
||||
<p>To see which notes are excluded from AI:</p>
|
||||
<ol>
|
||||
<li>Go to Search</li>
|
||||
<li>Enter: <code>#excludeFromAI</code></li>
|
||||
<li>Review the list of protected notes</li>
|
||||
</ol>
|
||||
|
||||
<h3>Content Filtering</h3>
|
||||
|
||||
<p>Trilium can automatically filter sensitive patterns:</p>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Pattern Type</th>
|
||||
<th>Example</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Social Security Numbers</td>
|
||||
<td>XXX-XX-XXXX</td>
|
||||
<td>Automatically redacted</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Credit Card Numbers</td>
|
||||
<td>16-digit numbers</td>
|
||||
<td>Automatically redacted</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>API Keys</td>
|
||||
<td>Strings matching key patterns</td>
|
||||
<td>Automatically redacted</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Passwords</td>
|
||||
<td>password: fields</td>
|
||||
<td>Automatically redacted</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>API Key Security</h2>
|
||||
|
||||
<h3>Best Practices</h3>
|
||||
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">Never Do This</p>
|
||||
<ul>
|
||||
<li>❌ Share API keys in notes or messages</li>
|
||||
<li>❌ Commit keys to version control</li>
|
||||
<li>❌ Use the same key across multiple applications</li>
|
||||
<li>❌ Store keys in plain text files</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="admonition success">
|
||||
<p class="admonition-title">Always Do This</p>
|
||||
<ul>
|
||||
<li>✅ Store keys in Trilium's secure settings</li>
|
||||
<li>✅ Rotate keys regularly (monthly recommended)</li>
|
||||
<li>✅ Use separate keys for development/production</li>
|
||||
<li>✅ Set spending limits in provider dashboards</li>
|
||||
<li>✅ Monitor usage for unusual activity</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>Key Rotation Schedule</h3>
|
||||
<ol>
|
||||
<li><strong>Monthly:</strong> Rotate API keys</li>
|
||||
<li><strong>Immediately:</strong> If key may be compromised</li>
|
||||
<li><strong>Quarterly:</strong> Review and audit all keys</li>
|
||||
<li><strong>Annually:</strong> Full security review</li>
|
||||
</ol>
|
||||
|
||||
<h2>Network Security</h2>
|
||||
|
||||
<h3>Secure Connections</h3>
|
||||
<ul>
|
||||
<li>All API communications use HTTPS/TLS encryption</li>
|
||||
<li>Certificate verification is enabled by default</li>
|
||||
<li>Minimum TLS version 1.2 required</li>
|
||||
</ul>
|
||||
|
||||
<h3>Firewall Considerations</h3>
|
||||
<p>Required ports for AI providers:</p>
|
||||
<ul>
|
||||
<li><strong>OpenAI/Anthropic:</strong> Port 443 (HTTPS)</li>
|
||||
<li><strong>Ollama:</strong> Port 11434 (local only by default)</li>
|
||||
</ul>
|
||||
|
||||
<h2>Compliance and Regulations</h2>
|
||||
|
||||
<h3>GDPR Compliance</h3>
|
||||
|
||||
<h4>Your Rights</h4>
|
||||
<ul>
|
||||
<li><strong>Right to Access:</strong> Export all AI-related data</li>
|
||||
<li><strong>Right to Deletion:</strong> Remove AI chat history and embeddings</li>
|
||||
<li><strong>Right to Portability:</strong> Export data in standard formats</li>
|
||||
<li><strong>Right to Restriction:</strong> Limit AI processing of your data</li>
|
||||
</ul>
|
||||
|
||||
<h4>Data Minimization</h4>
|
||||
<ul>
|
||||
<li>Send only necessary data to AI providers</li>
|
||||
<li>Regularly clean up old chat sessions</li>
|
||||
<li>Delete unused embeddings</li>
|
||||
<li>Implement retention policies</li>
|
||||
</ul>
|
||||
|
||||
<h3>Healthcare Data (HIPAA)</h3>
|
||||
|
||||
<div class="admonition warning">
|
||||
<p class="admonition-title">For Healthcare Professionals</p>
|
||||
<p>If handling protected health information (PHI):</p>
|
||||
<ul>
|
||||
<li>Use Ollama (local) exclusively - no cloud providers</li>
|
||||
<li>Or ensure Business Associate Agreement (BAA) with provider</li>
|
||||
<li>Enable maximum audit logging</li>
|
||||
<li>Implement additional encryption</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Security Configurations by Use Case</h2>
|
||||
|
||||
<h3>Personal Use (Maximum Privacy)</h3>
|
||||
<pre><code>Provider: Ollama (local)
|
||||
Model: llama3 or mistral
|
||||
Embeddings: mxbai-embed-large
|
||||
Network: Localhost only
|
||||
Exclusions: Personal notes labeled</code></pre>
|
||||
|
||||
<h3>Professional Use (Balanced)</h3>
|
||||
<pre><code>Provider: OpenAI or Anthropic
|
||||
Model: GPT-4 or Claude Sonnet
|
||||
API Keys: Rotated monthly
|
||||
Exclusions: Confidential projects
|
||||
Audit: Logging enabled</code></pre>
|
||||
|
||||
<h3>Enterprise Use (Maximum Security)</h3>
|
||||
<pre><code>Provider: Azure OpenAI (private instance)
|
||||
Authentication: SSO + MFA
|
||||
Network: VPN required
|
||||
Audit: Full logging to SIEM
|
||||
DLP: Content scanning enabled</code></pre>
|
||||
|
||||
<h2>Incident Response</h2>
|
||||
|
||||
<h3>If API Key is Compromised</h3>
|
||||
|
||||
<div class="admonition danger">
|
||||
<p class="admonition-title">Immediate Actions Required</p>
|
||||
<ol>
|
||||
<li><strong>Revoke the key immediately</strong> in provider dashboard</li>
|
||||
<li><strong>Generate new key</strong> and update in Trilium</li>
|
||||
<li><strong>Review usage logs</strong> for unauthorized activity</li>
|
||||
<li><strong>Check billing</strong> for unexpected charges</li>
|
||||
<li><strong>Document the incident</strong> for future reference</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<h3>If Sensitive Data Was Sent</h3>
|
||||
<ol>
|
||||
<li>Contact the AI provider's support team</li>
|
||||
<li>Request data deletion if possible</li>
|
||||
<li>Add affected notes to exclusion list</li>
|
||||
<li>Review and update security practices</li>
|
||||
<li>Consider switching to local AI (Ollama)</li>
|
||||
</ol>
|
||||
|
||||
<h2>Security Checklist</h2>
|
||||
|
||||
<h3>Initial Setup</h3>
|
||||
<ul class="checklist">
|
||||
<li>☐ Reviewed provider privacy policies</li>
|
||||
<li>☐ Created strong, unique API keys</li>
|
||||
<li>☐ Configured exclusion labels for sensitive notes</li>
|
||||
<li>☐ Tested security configuration</li>
|
||||
<li>☐ Set up spending limits</li>
|
||||
<li>☐ Enabled audit logging</li>
|
||||
</ul>
|
||||
|
||||
<h3>Ongoing Maintenance</h3>
|
||||
<ul class="checklist">
|
||||
<li>☐ Rotate API keys monthly</li>
|
||||
<li>☐ Review audit logs weekly</li>
|
||||
<li>☐ Update exclusion lists as needed</li>
|
||||
<li>☐ Monitor usage and costs</li>
|
||||
<li>☐ Check for security updates</li>
|
||||
<li>☐ Verify no sensitive data in logs</li>
|
||||
</ul>
|
||||
|
||||
<h2>Privacy-First Recommendations</h2>
|
||||
|
||||
<h3>For Maximum Privacy</h3>
|
||||
<p><strong>Use Ollama exclusively:</strong></p>
|
||||
<ul>
|
||||
<li>Complete data control</li>
|
||||
<li>No external dependencies</li>
|
||||
<li>Works offline</li>
|
||||
<li>No usage tracking</li>
|
||||
</ul>
|
||||
|
||||
<h3>For Convenience with Privacy</h3>
|
||||
<p><strong>Hybrid approach:</strong></p>
|
||||
<ul>
|
||||
<li>Ollama for sensitive content</li>
|
||||
<li>Cloud providers for general queries</li>
|
||||
<li>Strict exclusion labels</li>
|
||||
<li>Regular key rotation</li>
|
||||
</ul>
|
||||
|
||||
<h3>For Teams and Organizations</h3>
|
||||
<p><strong>Enterprise configuration:</strong></p>
|
||||
<ul>
|
||||
<li>Private AI instances (Azure OpenAI)</li>
|
||||
<li>Centralized key management</li>
|
||||
<li>Audit logging to SIEM</li>
|
||||
<li>DLP integration</li>
|
||||
<li>Regular security training</li>
|
||||
</ul>
|
||||
|
||||
<h2>Additional Resources</h2>
|
||||
<ul>
|
||||
<li><a href="https://openai.com/policies/privacy-policy" target="_blank">OpenAI Privacy Policy</a></li>
|
||||
<li><a href="https://www.anthropic.com/privacy" target="_blank">Anthropic Privacy Policy</a></li>
|
||||
<li><a href="https://gdpr.eu/" target="_blank">GDPR Information</a></li>
|
||||
<li><a href="https://www.hhs.gov/hipaa/index.html" target="_blank">HIPAA Guidelines</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="admonition tip">
|
||||
<p class="admonition-title">Remember</p>
|
||||
<p>Security and privacy require ongoing attention. Start with the most restrictive settings and gradually relax them only as needed. When in doubt, prefer local processing with Ollama for sensitive data.</p>
|
||||
</div>
|
||||
264
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Bulk Operations.html
generated
vendored
Normal file
264
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Bulk Operations.html
generated
vendored
Normal file
@@ -0,0 +1,264 @@
|
||||
<h1>Bulk Operations</h1>
|
||||
|
||||
<p>Execute actions on multiple notes simultaneously to save time and ensure consistency across your note collection.</p>
|
||||
|
||||
<h2>Quick Start</h2>
|
||||
|
||||
<p>Access bulk operations through:</p>
|
||||
<ul>
|
||||
<li>Search results menu → "Bulk Actions"</li>
|
||||
<li>Select multiple notes → Right-click → "Bulk Operations"</li>
|
||||
<li>Script API: <code>api.executeBulkActions(noteIds, actions)</code></li>
|
||||
</ul>
|
||||
|
||||
<h2>Available Operations</h2>
|
||||
|
||||
<h3>Note Operations</h3>
|
||||
|
||||
<div class="operation-card">
|
||||
<h4>Move Notes</h4>
|
||||
<p>Relocate multiple notes to a new parent location.</p>
|
||||
<pre><code>{
|
||||
"name": "moveNote",
|
||||
"targetParentNoteId": "target_note_id"
|
||||
}</code></pre>
|
||||
<p class="note">Notes with multiple parents will be cloned rather than moved.</p>
|
||||
</div>
|
||||
|
||||
<div class="operation-card">
|
||||
<h4>Delete Notes</h4>
|
||||
<p>Permanently remove multiple notes from the database.</p>
|
||||
<pre><code>{
|
||||
"name": "deleteNote"
|
||||
}</code></pre>
|
||||
<div class="warning">
|
||||
<strong>Warning:</strong> This operation cannot be undone. Ensure you have backups.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="operation-card">
|
||||
<h4>Rename Notes</h4>
|
||||
<p>Update titles using dynamic patterns with variables.</p>
|
||||
<pre><code>{
|
||||
"name": "renameNote",
|
||||
"newTitle": "Project: ${note.title}"
|
||||
}</code></pre>
|
||||
<p>Available variables: <code>${note.title}</code>, <code>${note.noteId}</code>, <code>${note.dateCreated}</code></p>
|
||||
</div>
|
||||
|
||||
<h3>Attribute Operations</h3>
|
||||
|
||||
<div class="operation-card">
|
||||
<h4>Add Label</h4>
|
||||
<p>Attach labels to multiple notes at once.</p>
|
||||
<pre><code>{
|
||||
"name": "addLabel",
|
||||
"labelName": "reviewed",
|
||||
"labelValue": "true"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="operation-card">
|
||||
<h4>Update Label Value</h4>
|
||||
<p>Modify existing label values across multiple notes.</p>
|
||||
<pre><code>{
|
||||
"name": "updateLabelValue",
|
||||
"labelName": "status",
|
||||
"labelValue": "completed"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="operation-card">
|
||||
<h4>Add Relation</h4>
|
||||
<p>Create relationships between notes and a target.</p>
|
||||
<pre><code>{
|
||||
"name": "addRelation",
|
||||
"relationName": "references",
|
||||
"targetNoteId": "bibliography_note"
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h3>Custom Script Execution</h3>
|
||||
|
||||
<div class="operation-card">
|
||||
<h4>Execute Script</h4>
|
||||
<p>Run custom JavaScript code on each selected note.</p>
|
||||
<pre><code>{
|
||||
"name": "executeScript",
|
||||
"script": "note.setLabel('processed', new Date().toISOString());"
|
||||
}</code></pre>
|
||||
<p>The <code>note</code> variable is available in the script context.</p>
|
||||
</div>
|
||||
|
||||
<h2>Including Descendants</h2>
|
||||
|
||||
<p>Apply operations to entire subtrees by enabling the "Include descendants" option:</p>
|
||||
|
||||
<pre><code>api.executeBulkActions(noteIds, actions, true);</code></pre>
|
||||
|
||||
<div class="info">
|
||||
<strong>Tip:</strong> This is useful for moving or deleting entire project hierarchies.
|
||||
</div>
|
||||
|
||||
<h2>Performance Guidelines</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Number of Notes</th>
|
||||
<th>Expected Performance</th>
|
||||
<th>Recommendations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>< 100</td>
|
||||
<td>Instant</td>
|
||||
<td>Execute directly</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>100-1000</td>
|
||||
<td>Few seconds</td>
|
||||
<td>Show progress indicator</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>> 1000</td>
|
||||
<td>May take minutes</td>
|
||||
<td>Run during low activity, consider batching</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Example: Project Archival</h2>
|
||||
|
||||
<p>Archive completed projects with metadata:</p>
|
||||
|
||||
<pre><code class="language-javascript">// Find completed projects
|
||||
const projectNotes = api.searchForNotes('#project #status=completed');
|
||||
|
||||
// Define archive actions
|
||||
const archiveActions = [
|
||||
{
|
||||
name: 'moveNote',
|
||||
targetParentNoteId: 'archive_folder_id'
|
||||
},
|
||||
{
|
||||
name: 'addLabel',
|
||||
labelName: 'archivedDate',
|
||||
labelValue: new Date().toISOString()
|
||||
},
|
||||
{
|
||||
name: 'deleteLabel',
|
||||
labelName: 'active'
|
||||
}
|
||||
];
|
||||
|
||||
// Execute with descendants
|
||||
api.executeBulkActions(projectNotes, archiveActions, true);</code></pre>
|
||||
|
||||
<h2>Safety Considerations</h2>
|
||||
|
||||
<ul>
|
||||
<li><strong>Always backup</strong> before large bulk operations</li>
|
||||
<li><strong>Test first</strong> on a small subset of notes</li>
|
||||
<li><strong>Review affected count</strong> before confirming</li>
|
||||
<li><strong>Use transactions</strong> for related operations</li>
|
||||
<li><strong>Monitor logs</strong> during execution</li>
|
||||
</ul>
|
||||
|
||||
<h2>Troubleshooting</h2>
|
||||
|
||||
<details>
|
||||
<summary><strong>Operation appears to hang</strong></summary>
|
||||
<p>For large operations (> 1000 notes), the process may take several minutes. Check server logs for progress. Consider breaking into smaller batches.</p>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Some notes not affected</strong></summary>
|
||||
<p>Verify that:</p>
|
||||
<ul>
|
||||
<li>Notes exist and are accessible</li>
|
||||
<li>Protected notes have unlocked session</li>
|
||||
<li>No validation errors in logs</li>
|
||||
<li>Target attributes don't have constraints</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Memory errors on large operations</strong></summary>
|
||||
<p>Increase Node.js heap size:</p>
|
||||
<pre><code>NODE_OPTIONS="--max-old-space-size=4096" npm start</code></pre>
|
||||
<p>Or process in smaller batches of 500 notes.</p>
|
||||
</details>
|
||||
|
||||
<style>
|
||||
.operation-card {
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
background: var(--accented-background-color);
|
||||
}
|
||||
|
||||
.operation-card h4 {
|
||||
margin-top: 0;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.info {
|
||||
background: #d1ecf1;
|
||||
border: 1px solid #17a2b8;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.note {
|
||||
font-style: italic;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--code-background-color);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
details {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--main-border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid var(--main-border-color);
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--accented-background-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -1,27 +1,335 @@
|
||||
<p>Trilium supports configuration via a file named <code>config.ini</code> and
|
||||
environment variables. Please review the file named <a href="https://github.com/TriliumNext/Trilium/blob/main/apps/server/src/assets/config-sample.ini">config-sample.ini</a> in
|
||||
the <a href="https://github.com/TriliumNext/Trilium">Trilium</a> repository
|
||||
to see what values are supported.</p>
|
||||
<p>You can provide the same values via environment variables instead of the <code>config.ini</code> file,
|
||||
and these environment variables use the following format:</p>
|
||||
<p>Trilium supports configuration via a file named <code>config.ini</code> and environment variables. This document provides a comprehensive reference for all configuration options.</p>
|
||||
|
||||
<h2>Configuration Precedence</h2>
|
||||
<p>Configuration values are loaded in the following order of precedence (highest to lowest):</p>
|
||||
<ol>
|
||||
<li>Environment variables should be prefixed with <code>TRILIUM_</code> and
|
||||
use underscores to represent the INI section structure.</li>
|
||||
<li>The format is: <code>TRILIUM_<SECTION>_<KEY>=<VALUE></code>
|
||||
</li>
|
||||
<li>The environment variables will override any matching values from config.ini</li>
|
||||
</ol>
|
||||
<p>For example, if you have this in your config.ini:</p><pre><code class="language-text-x-trilium-auto">[Network]
|
||||
host=localhost
|
||||
port=8080</code></pre>
|
||||
<p>You can override these values using environment variables:</p><pre><code class="language-text-x-trilium-auto">TRILIUM_NETWORK_HOST=0.0.0.0
|
||||
TRILIUM_NETWORK_PORT=9000</code></pre>
|
||||
<p>The code will:</p>
|
||||
<ol>
|
||||
<li>First load the <code>config.ini</code> file as before</li>
|
||||
<li>Then scan all environment variables for ones starting with <code>TRILIUM_</code>
|
||||
</li>
|
||||
<li>Parse these variables into section/key pairs</li>
|
||||
<li>Merge them with the config from the file, with environment variables taking
|
||||
precedence</li>
|
||||
<li><strong>Environment variables</strong> (checked first)</li>
|
||||
<li><strong>config.ini file values</strong></li>
|
||||
<li><strong>Default values</strong></li>
|
||||
</ol>
|
||||
|
||||
<h2>Environment Variable Patterns</h2>
|
||||
<p>Trilium supports multiple environment variable patterns for flexibility. The primary pattern is: <code>TRILIUM_[SECTION]_[KEY]</code></p>
|
||||
<p>Where:</p>
|
||||
<ul>
|
||||
<li><code>SECTION</code> is the INI section name in UPPERCASE</li>
|
||||
<li><code>KEY</code> is the camelCase configuration key converted to UPPERCASE (e.g., <code>instanceName</code> → <code>INSTANCENAME</code>)</li>
|
||||
</ul>
|
||||
<p>Additionally, shorter aliases are available for common configurations (see Alternative Variables section below).</p>
|
||||
|
||||
<h2>Environment Variable Reference</h2>
|
||||
|
||||
<h3>General Section</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment Variable</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_GENERAL_INSTANCENAME</code></td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>Instance name for API identification</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_GENERAL_NOAUTHENTICATION</code></td>
|
||||
<td>boolean</td>
|
||||
<td>false</td>
|
||||
<td>Disable authentication (server only)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_GENERAL_NOBACKUP</code></td>
|
||||
<td>boolean</td>
|
||||
<td>false</td>
|
||||
<td>Disable automatic backups</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_GENERAL_NODESKTOPICON</code></td>
|
||||
<td>boolean</td>
|
||||
<td>false</td>
|
||||
<td>Disable desktop icon creation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_GENERAL_READONLY</code></td>
|
||||
<td>boolean</td>
|
||||
<td>false</td>
|
||||
<td>Enable read-only mode</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Network Section</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment Variable</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_HOST</code></td>
|
||||
<td>string</td>
|
||||
<td>"0.0.0.0"</td>
|
||||
<td>Server host binding</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_PORT</code></td>
|
||||
<td>string</td>
|
||||
<td>"3000"</td>
|
||||
<td>Server port</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_HTTPS</code></td>
|
||||
<td>boolean</td>
|
||||
<td>false</td>
|
||||
<td>Enable HTTPS</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_CERTPATH</code></td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>SSL certificate path</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_KEYPATH</code></td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>SSL key path</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_TRUSTEDREVERSEPROXY</code></td>
|
||||
<td>boolean/string</td>
|
||||
<td>false</td>
|
||||
<td>Reverse proxy trust settings</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_CORSALLOWORIGIN</code></td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>CORS allowed origins</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_CORSALLOWMETHODS</code></td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>CORS allowed methods</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_NETWORK_CORSALLOWHEADERS</code></td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>CORS allowed headers</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Session Section</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment Variable</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_SESSION_COOKIEMAXAGE</code></td>
|
||||
<td>integer</td>
|
||||
<td>1814400</td>
|
||||
<td>Session cookie max age in seconds (21 days)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Sync Section</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment Variable</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_SYNC_SYNCSERVERHOST</code></td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>Sync server host URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_SYNC_SYNCSERVERTIMEOUT</code></td>
|
||||
<td>string</td>
|
||||
<td>"120000"</td>
|
||||
<td>Sync server timeout in milliseconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_SYNC_SYNCPROXY</code></td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>Sync proxy URL</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>MultiFactorAuthentication Section</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment Variable</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code></td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>OAuth/OpenID base URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code></td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>OAuth client ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code></td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>OAuth client secret</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code></td>
|
||||
<td>string</td>
|
||||
<td>"https://accounts.google.com"</td>
|
||||
<td>OAuth issuer base URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code></td>
|
||||
<td>string</td>
|
||||
<td>"Google"</td>
|
||||
<td>OAuth issuer display name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code></td>
|
||||
<td>string</td>
|
||||
<td>""</td>
|
||||
<td>OAuth issuer icon URL</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Logging Section</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment Variable</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>TRILIUM_LOGGING_RETENTIONDAYS</code></td>
|
||||
<td>integer</td>
|
||||
<td>90</td>
|
||||
<td>Number of days to retain log files</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Alternative Environment Variables</h2>
|
||||
<p>The following alternative environment variable names are also supported and work identically to their longer counterparts:</p>
|
||||
|
||||
<h3>Network CORS Variables</h3>
|
||||
<ul>
|
||||
<li><code>TRILIUM_NETWORK_CORS_ALLOW_ORIGIN</code> (alternative to <code>TRILIUM_NETWORK_CORSALLOWORIGIN</code>)</li>
|
||||
<li><code>TRILIUM_NETWORK_CORS_ALLOW_METHODS</code> (alternative to <code>TRILIUM_NETWORK_CORSALLOWMETHODS</code>)</li>
|
||||
<li><code>TRILIUM_NETWORK_CORS_ALLOW_HEADERS</code> (alternative to <code>TRILIUM_NETWORK_CORSALLOWHEADERS</code>)</li>
|
||||
</ul>
|
||||
|
||||
<h3>Sync Variables</h3>
|
||||
<ul>
|
||||
<li><code>TRILIUM_SYNC_SERVER_HOST</code> (alternative to <code>TRILIUM_SYNC_SYNCSERVERHOST</code>)</li>
|
||||
<li><code>TRILIUM_SYNC_SERVER_TIMEOUT</code> (alternative to <code>TRILIUM_SYNC_SYNCSERVERTIMEOUT</code>)</li>
|
||||
<li><code>TRILIUM_SYNC_SERVER_PROXY</code> (alternative to <code>TRILIUM_SYNC_SYNCPROXY</code>)</li>
|
||||
</ul>
|
||||
|
||||
<h3>OAuth/MFA Variables</h3>
|
||||
<ul>
|
||||
<li><code>TRILIUM_OAUTH_BASE_URL</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>)</li>
|
||||
<li><code>TRILIUM_OAUTH_CLIENT_ID</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>)</li>
|
||||
<li><code>TRILIUM_OAUTH_CLIENT_SECRET</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code>)</li>
|
||||
<li><code>TRILIUM_OAUTH_ISSUER_BASE_URL</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>)</li>
|
||||
<li><code>TRILIUM_OAUTH_ISSUER_NAME</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>)</li>
|
||||
<li><code>TRILIUM_OAUTH_ISSUER_ICON</code> (alternative to <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code>)</li>
|
||||
</ul>
|
||||
|
||||
<h3>Logging Variables</h3>
|
||||
<ul>
|
||||
<li><code>TRILIUM_LOGGING_RETENTION_DAYS</code> (alternative to <code>TRILIUM_LOGGING_RETENTIONDAYS</code>)</li>
|
||||
</ul>
|
||||
|
||||
<h2>Boolean Values</h2>
|
||||
<p>Boolean environment variables accept the following values:</p>
|
||||
<ul>
|
||||
<li><strong>True</strong>: <code>"true"</code>, <code>"1"</code>, <code>1</code></li>
|
||||
<li><strong>False</strong>: <code>"false"</code>, <code>"0"</code>, <code>0</code></li>
|
||||
<li>Any other value defaults to <code>false</code></li>
|
||||
</ul>
|
||||
|
||||
<h2>Using Environment Variables</h2>
|
||||
<p>Both naming patterns are fully supported and can be used interchangeably:</p>
|
||||
<ul>
|
||||
<li>The longer format follows the section/key pattern for consistency with the INI file structure</li>
|
||||
<li>The shorter alternatives provide convenience for common configurations</li>
|
||||
<li>You can use whichever format you prefer - both are equally valid</li>
|
||||
</ul>
|
||||
|
||||
<h2>Examples</h2>
|
||||
|
||||
<h3>Docker Compose Example</h3>
|
||||
<pre><code class="language-yaml">services:
|
||||
trilium:
|
||||
image: triliumnext/notes
|
||||
environment:
|
||||
# Using full format
|
||||
TRILIUM_GENERAL_INSTANCENAME: "My Trilium Instance"
|
||||
TRILIUM_NETWORK_PORT: "8080"
|
||||
TRILIUM_NETWORK_CORSALLOWORIGIN: "https://myapp.com"
|
||||
TRILIUM_SYNC_SYNCSERVERHOST: "https://sync.example.com"
|
||||
TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL: "https://auth.example.com"
|
||||
|
||||
# Or using shorter alternatives (equally valid)
|
||||
# TRILIUM_NETWORK_CORS_ALLOW_ORIGIN: "https://myapp.com"
|
||||
# TRILIUM_SYNC_SERVER_HOST: "https://sync.example.com"
|
||||
# TRILIUM_OAUTH_BASE_URL: "https://auth.example.com"</code></pre>
|
||||
|
||||
<h3>Shell Export Example</h3>
|
||||
<pre><code class="language-bash"># Using either format
|
||||
export TRILIUM_GENERAL_NOAUTHENTICATION=false
|
||||
export TRILIUM_NETWORK_HTTPS=true
|
||||
export TRILIUM_NETWORK_CERTPATH=/path/to/cert.pem
|
||||
export TRILIUM_NETWORK_KEYPATH=/path/to/key.pem
|
||||
export TRILIUM_LOGGING_RETENTIONDAYS=30
|
||||
|
||||
# Start Trilium
|
||||
npm start</code></pre>
|
||||
|
||||
<h2>config.ini Reference</h2>
|
||||
<p>For the complete list of configuration options and their INI file format, please review the <a href="https://github.com/TriliumNext/Trilium/blob/main/apps/server/src/assets/config-sample.ini">config-sample.ini</a> file in the Trilium repository.</p>
|
||||
378
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Note Revisions.html
generated
vendored
Normal file
378
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Note Revisions.html
generated
vendored
Normal file
@@ -0,0 +1,378 @@
|
||||
<h1>Note Revisions</h1>
|
||||
|
||||
<p>Track and restore previous versions of your notes with Trilium's comprehensive revision system.</p>
|
||||
|
||||
<h2>Understanding Revisions</h2>
|
||||
|
||||
<p>Revisions are automatic snapshots of your note content captured at specific intervals. Each revision preserves:</p>
|
||||
<ul>
|
||||
<li>Complete note content</li>
|
||||
<li>Note title at revision time</li>
|
||||
<li>Type and MIME information</li>
|
||||
<li>Creation and modification timestamps</li>
|
||||
<li>Protection status</li>
|
||||
</ul>
|
||||
|
||||
<h2>Accessing Revision History</h2>
|
||||
|
||||
<div class="access-methods">
|
||||
<div class="method">
|
||||
<h4>Via Note Menu</h4>
|
||||
<p>Click note menu (⋮) → "Revisions"</p>
|
||||
</div>
|
||||
<div class="method">
|
||||
<h4>Keyboard Shortcut</h4>
|
||||
<p>Press <kbd>Alt</kbd> + <kbd>R</kbd></p>
|
||||
</div>
|
||||
<div class="method">
|
||||
<h4>Script API</h4>
|
||||
<p><code>note.getRevisions()</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Automatic Creation Rules</h2>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>Default Triggers</h3>
|
||||
<ul>
|
||||
<li>After 5 minutes of continuous editing</li>
|
||||
<li>When switching to a different note</li>
|
||||
<li>Before major operations (delete, move)</li>
|
||||
<li>Content size changes > 20%</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Retention Policies</h2>
|
||||
|
||||
<table class="retention-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Age</th>
|
||||
<th>Retention</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>< 1 day</td>
|
||||
<td>Keep all</td>
|
||||
<td>Every revision saved</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1-7 days</td>
|
||||
<td>Daily</td>
|
||||
<td>One per day</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>7-30 days</td>
|
||||
<td>Weekly</td>
|
||||
<td>One per week</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>> 30 days</td>
|
||||
<td>Monthly</td>
|
||||
<td>One per month</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Custom Retention Settings</h2>
|
||||
|
||||
<p>Configure retention per note using attributes:</p>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>Keep All Revisions</h4>
|
||||
<pre><code>#revisionRetention=all</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>Keep for Specific Duration</h4>
|
||||
<pre><code>#revisionRetention=90d</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>Disable Revisions</h4>
|
||||
<pre><code>#disableRevisions</code></pre>
|
||||
</div>
|
||||
|
||||
<h2>Comparing Revisions</h2>
|
||||
|
||||
<div class="feature-box">
|
||||
<h3>Visual Comparison</h3>
|
||||
<ol>
|
||||
<li>Open revision history</li>
|
||||
<li>Select two revisions</li>
|
||||
<li>Click "Compare"</li>
|
||||
<li>View side-by-side differences</li>
|
||||
</ol>
|
||||
<p class="highlight">Added content shown in <span style="color: green;">green</span>, removed in <span style="color: red;">red</span>.</p>
|
||||
</div>
|
||||
|
||||
<h2>Restoring Revisions</h2>
|
||||
|
||||
<div class="steps">
|
||||
<h3>Manual Restoration</h3>
|
||||
<ol>
|
||||
<li>Open revision history (<kbd>Alt</kbd> + <kbd>R</kbd>)</li>
|
||||
<li>Browse revisions by date/time</li>
|
||||
<li>Preview revision content</li>
|
||||
<li>Click "Restore this version"</li>
|
||||
<li>Confirm replacement</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="warning">
|
||||
<strong>Important:</strong> Restoring a revision replaces current content. Consider creating a manual snapshot first.
|
||||
</div>
|
||||
|
||||
<h2>Creating Manual Snapshots</h2>
|
||||
|
||||
<p>Force revision creation for important milestones:</p>
|
||||
|
||||
<pre><code class="language-javascript">// Via Script API
|
||||
api.createRevision(note.noteId, {
|
||||
title: note.title,
|
||||
content: note.getContent(),
|
||||
reason: 'Before major refactoring'
|
||||
});</code></pre>
|
||||
|
||||
<h2>Protected Note Revisions</h2>
|
||||
|
||||
<div class="security-box">
|
||||
<h3>Encryption Behavior</h3>
|
||||
<ul>
|
||||
<li>Revisions inherit note's protection status</li>
|
||||
<li>Title and content encrypted separately</li>
|
||||
<li>Requires unlocked session to view</li>
|
||||
<li>Protection changes apply to all revisions</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2>Storage Management</h2>
|
||||
|
||||
<div class="metrics">
|
||||
<h3>Typical Storage Usage</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Text notes</td>
|
||||
<td>~2KB per revision</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Code notes</td>
|
||||
<td>~5KB per revision</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Image notes</td>
|
||||
<td>Deduplicated via blobs</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>File notes</td>
|
||||
<td>Full file size</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>Cleanup Operations</h2>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>Automatic Cleanup</h4>
|
||||
<pre><code class="language-javascript">// Configure in options
|
||||
api.setOption('revisionCleanupDays', 90);
|
||||
api.setOption('revisionCleanupEnabled', true);</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="code-example">
|
||||
<h4>Manual Cleanup</h4>
|
||||
<pre><code class="language-javascript">// Delete revisions older than 90 days
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - 90);
|
||||
|
||||
for (const note of api.getAllNotes()) {
|
||||
const revisions = note.getRevisions();
|
||||
for (const revision of revisions) {
|
||||
if (revision.dateCreated < cutoffDate) {
|
||||
revision.delete();
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<h2>Performance Tips</h2>
|
||||
|
||||
<ul class="tips">
|
||||
<li><strong>Large files:</strong> Consider disabling revisions for binary attachments</li>
|
||||
<li><strong>Frequent edits:</strong> Increase revision interval to reduce storage</li>
|
||||
<li><strong>Cleanup:</strong> Run cleanup during low-activity periods</li>
|
||||
<li><strong>Monitoring:</strong> Check database size regularly</li>
|
||||
</ul>
|
||||
|
||||
<h2>Troubleshooting</h2>
|
||||
|
||||
<details>
|
||||
<summary><strong>Revisions not appearing</strong></summary>
|
||||
<ul>
|
||||
<li>Check if <code>#disableRevisions</code> is set</li>
|
||||
<li>Verify revision creation interval in options</li>
|
||||
<li>Ensure sufficient disk space</li>
|
||||
<li>Check database write permissions</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Cannot view revision content</strong></summary>
|
||||
<ul>
|
||||
<li>For protected notes, unlock protected session</li>
|
||||
<li>Verify blob storage integrity</li>
|
||||
<li>Check database consistency</li>
|
||||
<li>Review error logs for details</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Excessive storage usage</strong></summary>
|
||||
<ul>
|
||||
<li>Implement aggressive cleanup policy</li>
|
||||
<li>Exclude binary notes from tracking</li>
|
||||
<li>Archive old revisions externally</li>
|
||||
<li>Consider compression options</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<style>
|
||||
.access-methods {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.method {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
background: var(--accented-background-color);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.method h4 {
|
||||
margin-top: 0;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.info-box, .feature-box, .security-box {
|
||||
background: #e8f4f8;
|
||||
border-left: 4px solid #17a2b8;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.code-example {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.code-example h4 {
|
||||
margin-bottom: 5px;
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
.retention-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.retention-table th,
|
||||
.retention-table td {
|
||||
border: 1px solid var(--main-border-color);
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.retention-table th {
|
||||
background: var(--accented-background-color);
|
||||
}
|
||||
|
||||
.metrics table {
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.metrics td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.steps {
|
||||
background: var(--accented-background-color);
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.steps ol {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.tips {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tips li {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.tips li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
kbd {
|
||||
background: #f4f4f4;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
details {
|
||||
margin: 15px 0;
|
||||
padding: 15px;
|
||||
background: var(--accented-background-color);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--code-background-color);
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--code-background-color);
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
232
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Advanced-Search-Expressions.html
generated
vendored
Normal file
232
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Advanced-Search-Expressions.html
generated
vendored
Normal file
@@ -0,0 +1,232 @@
|
||||
<h1>Advanced Search Expressions</h1>
|
||||
<p>This guide covers complex search expressions that combine multiple criteria, use advanced operators, and leverage Trilium's relationship system for sophisticated queries.</p>
|
||||
|
||||
<h2>Complex Query Construction</h2>
|
||||
|
||||
<h3>Boolean Logic with Parentheses</h3>
|
||||
<p>Use parentheses to group expressions and control evaluation order:</p>
|
||||
<pre><code>(#book OR #article) AND #author=Tolkien</code></pre>
|
||||
<p>Finds notes that are either books or articles, written by Tolkien.</p>
|
||||
<pre><code>#project AND (#status=active OR #status=pending)</code></pre>
|
||||
<p>Finds active or pending projects.</p>
|
||||
<pre><code>meeting AND (#priority=high OR #urgent) AND note.dateCreated >= TODAY-7</code></pre>
|
||||
<p>Finds recent high-priority or urgent meetings.</p>
|
||||
|
||||
<h3>Negation Patterns</h3>
|
||||
<p>Use <code>NOT</code> or the <code>not()</code> function to exclude certain criteria:</p>
|
||||
<pre><code>#book AND not(#genre=fiction)</code></pre>
|
||||
<p>Finds non-fiction books.</p>
|
||||
<pre><code>project AND not(note.isArchived=true)</code></pre>
|
||||
<p>Finds non-archived notes containing "project".</p>
|
||||
<pre><code>#!completed</code></pre>
|
||||
<p>Short syntax for notes without the "completed" label.</p>
|
||||
|
||||
<h3>Mixed Search Types</h3>
|
||||
<p>Combine full-text, attribute, and property searches:</p>
|
||||
<pre><code>development #category=work note.type=text note.dateModified >= TODAY-30</code></pre>
|
||||
<p>Finds text notes about development, categorized as work, modified in the last 30 days.</p>
|
||||
|
||||
<h2>Advanced Attribute Searches</h2>
|
||||
|
||||
<h3>Fuzzy Attribute Matching</h3>
|
||||
<p>When fuzzy attribute search is enabled, you can use partial matches:</p>
|
||||
<pre><code>#lang</code></pre>
|
||||
<p>Matches labels like "language", "languages", "programming-lang", etc.</p>
|
||||
<pre><code>#category=prog</code></pre>
|
||||
<p>Matches categories like "programming", "progress", "program", etc.</p>
|
||||
|
||||
<h3>Multiple Attribute Conditions</h3>
|
||||
<pre><code>#book #author=Tolkien #publicationYear>=1950 #publicationYear<1960</code></pre>
|
||||
<p>Finds Tolkien's books published in the 1950s.</p>
|
||||
<pre><code>#task #priority=high #status!=completed</code></pre>
|
||||
<p>Finds high-priority incomplete tasks.</p>
|
||||
|
||||
<h3>Complex Label Value Patterns</h3>
|
||||
<p>Use various operators for sophisticated label matching:</p>
|
||||
<pre><code>#isbn %= '978-[0-9-]+'</code></pre>
|
||||
<p>Finds notes with ISBN labels matching the pattern (regex).</p>
|
||||
<pre><code>#email *=* @company.com</code></pre>
|
||||
<p>Finds notes with email labels containing "@company.com".</p>
|
||||
<pre><code>#version >= 2.0</code></pre>
|
||||
<p>Finds notes with version labels of 2.0 or higher (numeric comparison).</p>
|
||||
|
||||
<h2>Relationship Traversal</h2>
|
||||
|
||||
<h3>Basic Relation Queries</h3>
|
||||
<pre><code>~author.title *=* Tolkien</code></pre>
|
||||
<p>Finds notes with an "author" relation to notes containing "Tolkien" in the title.</p>
|
||||
<pre><code>~project.labels.status = active</code></pre>
|
||||
<p>Finds notes related to projects with active status.</p>
|
||||
|
||||
<h3>Multi-Level Relationships</h3>
|
||||
<pre><code>~author.relations.publisher.title = "Penguin Books"</code></pre>
|
||||
<p>Finds notes authored by someone published by Penguin Books.</p>
|
||||
<pre><code>~project.children.title *=* documentation</code></pre>
|
||||
<p>Finds notes related to projects that have child notes about documentation.</p>
|
||||
|
||||
<h3>Relationship Direction</h3>
|
||||
<pre><code>note.children.title = "Chapter 1"</code></pre>
|
||||
<p>Finds parent notes that have a child titled "Chapter 1".</p>
|
||||
<pre><code>note.parents.labels.category = book</code></pre>
|
||||
<p>Finds notes whose parents are categorized as books.</p>
|
||||
<pre><code>note.ancestors.title = "Literature"</code></pre>
|
||||
<p>Finds notes with "Literature" anywhere in their ancestor chain.</p>
|
||||
|
||||
<h2>Property-Based Searches</h2>
|
||||
|
||||
<h3>Note Metadata Queries</h3>
|
||||
<pre><code>note.type=code note.mime=text/javascript note.dateCreated >= MONTH</code></pre>
|
||||
<p>Finds JavaScript code notes created this month.</p>
|
||||
<pre><code>note.isProtected=true note.contentSize > 1000</code></pre>
|
||||
<p>Finds large protected notes.</p>
|
||||
<pre><code>note.childrenCount >= 10 note.type=text</code></pre>
|
||||
<p>Finds text notes with many children.</p>
|
||||
|
||||
<h3>Advanced Property Combinations</h3>
|
||||
<pre><code>note.parentCount > 1 #template</code></pre>
|
||||
<p>Finds template notes that are cloned in multiple places.</p>
|
||||
<pre><code>note.attributeCount > 5 note.type=text note.contentSize < 500</code></pre>
|
||||
<p>Finds small text notes with many attributes (heavily tagged short notes).</p>
|
||||
<pre><code>note.revisionCount > 10 note.dateModified >= TODAY-7</code></pre>
|
||||
<p>Finds frequently edited notes modified recently.</p>
|
||||
|
||||
<h2>Date and Time Expressions</h2>
|
||||
|
||||
<h3>Relative Date Calculations</h3>
|
||||
<pre><code>#dueDate <= TODAY+7 #dueDate >= TODAY</code></pre>
|
||||
<p>Finds tasks due in the next week.</p>
|
||||
<pre><code>note.dateCreated >= MONTH-2 note.dateCreated < MONTH</code></pre>
|
||||
<p>Finds notes created in the past two months.</p>
|
||||
<pre><code>#eventDate = YEAR note.dateCreated >= YEAR-1</code></pre>
|
||||
<p>Finds events scheduled for this year that were planned last year.</p>
|
||||
|
||||
<h3>Complex Date Logic</h3>
|
||||
<pre><code>(#startDate <= TODAY AND #endDate >= TODAY) OR #status=ongoing</code></pre>
|
||||
<p>Finds current events or ongoing items.</p>
|
||||
<pre><code>#reminderDate <= NOW+3600 #reminderDate > NOW</code></pre>
|
||||
<p>Finds reminders due in the next hour (using seconds offset).</p>
|
||||
|
||||
<h2>Fuzzy Search Techniques</h2>
|
||||
|
||||
<h3>Fuzzy Exact Matching</h3>
|
||||
<pre><code>#title ~= managment</code></pre>
|
||||
<p>Finds notes with titles like "management" even with typos.</p>
|
||||
<pre><code>~category.title ~= progaming</code></pre>
|
||||
<p>Finds notes related to categories like "programming" with misspellings.</p>
|
||||
|
||||
<h3>Fuzzy Contains Matching</h3>
|
||||
<pre><code>note.content ~* algoritm</code></pre>
|
||||
<p>Finds notes containing words like "algorithm" with spelling variations.</p>
|
||||
<pre><code>#description ~* recieve</code></pre>
|
||||
<p>Finds notes with descriptions containing "receive" despite the common misspelling.</p>
|
||||
|
||||
<h3>Progressive Fuzzy Strategy</h3>
|
||||
<p>By default, Trilium uses exact matching first, then fuzzy as fallback:</p>
|
||||
<pre><code>development project</code></pre>
|
||||
<p>First finds exact matches for "development" and "project", then adds fuzzy matches if needed.</p>
|
||||
|
||||
<p>To force fuzzy behavior:</p>
|
||||
<pre><code>#title ~= development #category ~= projet</code></pre>
|
||||
|
||||
<h2>Ordering and Limiting</h2>
|
||||
|
||||
<h3>Multiple Sort Criteria</h3>
|
||||
<pre><code>#book orderBy #publicationYear desc, note.title asc limit 20</code></pre>
|
||||
<p>Orders books by publication year (newest first), then by title alphabetically, limited to 20 results.</p>
|
||||
<pre><code>#task orderBy #priority desc, #dueDate asc</code></pre>
|
||||
<p>Orders tasks by priority (high first), then by due date (earliest first).</p>
|
||||
|
||||
<h3>Dynamic Ordering</h3>
|
||||
<pre><code>#meeting note.dateCreated >= TODAY-30 orderBy note.dateModified desc</code></pre>
|
||||
<p>Finds recent meetings ordered by last modification.</p>
|
||||
<pre><code>#project #status=active orderBy note.childrenCount desc limit 10</code></pre>
|
||||
<p>Finds the 10 most complex active projects (by number of sub-notes).</p>
|
||||
|
||||
<h2>Performance Optimization Patterns</h2>
|
||||
|
||||
<h3>Efficient Query Structure</h3>
|
||||
<p>Start with the most selective criteria:</p>
|
||||
<pre><code>#book #author=Tolkien note.dateCreated >= 1950-01-01</code></pre>
|
||||
<p>Better than:</p>
|
||||
<pre><code>note.dateCreated >= 1950-01-01 #book #author=Tolkien</code></pre>
|
||||
|
||||
<h3>Fast Search for Large Datasets</h3>
|
||||
<pre><code>#category=project #status=active</code></pre>
|
||||
<p>With fast search enabled, this searches only attributes, not content.</p>
|
||||
|
||||
<h3>Limiting Expensive Operations</h3>
|
||||
<pre><code>note.content *=* "complex search term" limit 50</code></pre>
|
||||
<p>Limits content search to prevent performance issues.</p>
|
||||
|
||||
<h2>Error Handling and Debugging</h2>
|
||||
|
||||
<h3>Syntax Validation</h3>
|
||||
<p>Invalid syntax produces helpful error messages:</p>
|
||||
<pre><code>#book AND OR #author=Tolkien</code></pre>
|
||||
<p>Error: "Mixed usage of AND/OR - always use parentheses to group AND/OR expressions."</p>
|
||||
|
||||
<h3>Debug Mode</h3>
|
||||
<p>Enable debug mode to see how queries are parsed:</p>
|
||||
<pre><code>#book #author=Tolkien</code></pre>
|
||||
<p>With debug enabled, shows the internal expression tree structure.</p>
|
||||
|
||||
<h3>Common Pitfalls</h3>
|
||||
<ul>
|
||||
<li>Unescaped special characters: Use quotes or backslashes</li>
|
||||
<li>Missing parentheses in complex boolean expressions</li>
|
||||
<li>Incorrect property names: Use <code>note.title</code> not <code>title</code></li>
|
||||
<li>Case sensitivity assumptions: All searches are case-insensitive</li>
|
||||
</ul>
|
||||
|
||||
<h2>Expression Shortcuts</h2>
|
||||
|
||||
<h3>Label Shortcuts</h3>
|
||||
<p>Full syntax:</p>
|
||||
<pre><code>note.labels.category = book</code></pre>
|
||||
<p>Shortcut:</p>
|
||||
<pre><code>#category = book</code></pre>
|
||||
|
||||
<h3>Relation Shortcuts</h3>
|
||||
<p>Full syntax:</p>
|
||||
<pre><code>note.relations.author.title *=* Tolkien</code></pre>
|
||||
<p>Shortcut:</p>
|
||||
<pre><code>~author.title *=* Tolkien</code></pre>
|
||||
|
||||
<h3>Property Shortcuts</h3>
|
||||
<p>Some properties have convenient shortcuts:</p>
|
||||
<pre><code>note.text *=* content</code></pre>
|
||||
<p>Searches both title and content for "content".</p>
|
||||
|
||||
<h2>Real-World Complex Examples</h2>
|
||||
|
||||
<h3>Project Management</h3>
|
||||
<pre><code>(#project OR #task) AND #status!=completed AND
|
||||
(#priority=high OR #dueDate <= TODAY+7) AND
|
||||
not(note.isArchived=true)
|
||||
orderBy #priority desc, #dueDate asc</code></pre>
|
||||
|
||||
<h3>Research Organization</h3>
|
||||
<pre><code>(#paper OR #article OR #book) AND
|
||||
~author.title *=* smith AND
|
||||
#topic *=* "machine learning" AND
|
||||
note.dateCreated >= YEAR-2
|
||||
orderBy #citationCount desc limit 25</code></pre>
|
||||
|
||||
<h3>Content Management</h3>
|
||||
<pre><code>note.type=text AND note.contentSize > 5000 AND
|
||||
#category=documentation AND note.childrenCount >= 3 AND
|
||||
note.dateModified >= MONTH-1
|
||||
orderBy note.dateModified desc</code></pre>
|
||||
|
||||
<h3>Knowledge Base Maintenance</h3>
|
||||
<pre><code>note.attributeCount = 0 AND note.childrenCount = 0 AND
|
||||
note.parentCount = 1 AND note.contentSize < 100 AND
|
||||
note.dateModified < TODAY-90</code></pre>
|
||||
<p>Finds potential cleanup candidates: small, untagged, isolated notes not modified in 90 days.</p>
|
||||
|
||||
<h2>Next Steps</h2>
|
||||
<ul>
|
||||
<li><a class="reference-link" href="#root/_help_search_examples">Search Examples and Use Cases</a> - Practical applications</li>
|
||||
<li><a class="reference-link" href="#root/_help_search_saved">Saved Searches</a> - Creating reusable search configurations</li>
|
||||
<li><a class="reference-link" href="#root/_help_search_technical">Technical Search Details</a> - Implementation details and performance tuning</li>
|
||||
</ul>
|
||||
360
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Saved-Searches.html
generated
vendored
Normal file
360
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Saved-Searches.html
generated
vendored
Normal file
@@ -0,0 +1,360 @@
|
||||
<h1>Saved Searches</h1>
|
||||
<p>Saved searches in Trilium allow you to create dynamic collections of notes that automatically update based on search criteria. They appear as special notes in your tree and provide a powerful way to organize and access related content.</p>
|
||||
|
||||
<h2>Understanding Saved Searches</h2>
|
||||
<p>A saved search is a special note type that:</p>
|
||||
<ul>
|
||||
<li>Stores search criteria and configuration</li>
|
||||
<li>Dynamically displays matching notes as children</li>
|
||||
<li>Updates automatically when notes change</li>
|
||||
<li>Can be bookmarked and accessed like any other note</li>
|
||||
<li>Supports all search features including ordering and limits</li>
|
||||
</ul>
|
||||
|
||||
<h2>Creating Saved Searches</h2>
|
||||
|
||||
<h3>From Search Dialog</h3>
|
||||
<ol>
|
||||
<li>Open the search dialog (Ctrl+S or search icon)</li>
|
||||
<li>Configure your search criteria and options</li>
|
||||
<li>Click "Save to note" button</li>
|
||||
<li>Choose a name and location for the saved search</li>
|
||||
</ol>
|
||||
|
||||
<h3>Manual Creation</h3>
|
||||
<ol>
|
||||
<li>Create a new note and set its type to "Saved Search"</li>
|
||||
<li>Configure the search using labels:
|
||||
<ul>
|
||||
<li><code>#searchString</code> - The search query</li>
|
||||
<li><code>#fastSearch</code> - Enable fast search mode</li>
|
||||
<li><code>#includeArchivedNotes</code> - Include archived notes</li>
|
||||
<li><code>#orderBy</code> - Sort field</li>
|
||||
<li><code>#orderDirection</code> - "asc" or "desc"</li>
|
||||
<li><code>#limit</code> - Maximum number of results</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h3>Using Search Scripts</h3>
|
||||
<p>For complex logic, create a JavaScript note and link it:</p>
|
||||
<ul>
|
||||
<li><code>~searchScript</code> - Relation pointing to a backend script note</li>
|
||||
</ul>
|
||||
|
||||
<h2>Basic Saved Search Examples</h2>
|
||||
|
||||
<h3>Simple Text Search</h3>
|
||||
<pre><code>#searchString=project management</code></pre>
|
||||
<p>Finds all notes containing "project management".</p>
|
||||
|
||||
<h3>Tag-Based Collection</h3>
|
||||
<pre><code>#searchString=#book #author=Tolkien
|
||||
#orderBy=publicationYear
|
||||
#orderDirection=desc</code></pre>
|
||||
<p>Creates a collection of Tolkien's books ordered by publication year.</p>
|
||||
|
||||
<h3>Task Dashboard</h3>
|
||||
<pre><code>#searchString=#task #status!=completed #assignee=me
|
||||
#orderBy=priority
|
||||
#orderDirection=desc
|
||||
#limit=20</code></pre>
|
||||
<p>Shows your top 20 incomplete tasks by priority.</p>
|
||||
|
||||
<h3>Recent Activity</h3>
|
||||
<pre><code>#searchString=note.dateModified >= TODAY-7
|
||||
#orderBy=dateModified
|
||||
#orderDirection=desc
|
||||
#limit=50</code></pre>
|
||||
<p>Shows the 50 most recently modified notes from the last week.</p>
|
||||
|
||||
<h2>Advanced Saved Search Patterns</h2>
|
||||
|
||||
<h3>Dynamic Date-Based Collections</h3>
|
||||
|
||||
<h4>This Week's Content</h4>
|
||||
<pre><code>#searchString=note.dateCreated >= TODAY-7 note.dateCreated < TODAY
|
||||
#orderBy=dateCreated
|
||||
#orderDirection=desc</code></pre>
|
||||
|
||||
<h4>Monthly Review Collection</h4>
|
||||
<pre><code>#searchString=#reviewed=false note.dateCreated >= MONTH note.dateCreated < MONTH+1
|
||||
#orderBy=dateCreated</code></pre>
|
||||
|
||||
<h4>Upcoming Deadlines</h4>
|
||||
<pre><code>#searchString=#dueDate >= TODAY #dueDate <= TODAY+14 #status!=completed
|
||||
#orderBy=dueDate
|
||||
#orderDirection=asc</code></pre>
|
||||
|
||||
<h3>Project-Specific Collections</h3>
|
||||
|
||||
<h4>Project Dashboard</h4>
|
||||
<pre><code>#searchString=#project=alpha (#task OR #milestone OR #document)
|
||||
#orderBy=priority
|
||||
#orderDirection=desc</code></pre>
|
||||
|
||||
<h4>Project Health Monitor</h4>
|
||||
<pre><code>#searchString=#project=alpha #status=blocked OR (#dueDate < TODAY #status!=completed)
|
||||
#orderBy=dueDate
|
||||
#orderDirection=asc</code></pre>
|
||||
|
||||
<h3>Content Type Collections</h3>
|
||||
|
||||
<h4>Documentation Hub</h4>
|
||||
<pre><code>#searchString=(#documentation OR #guide OR #manual) #product=api
|
||||
#orderBy=dateModified
|
||||
#orderDirection=desc</code></pre>
|
||||
|
||||
<h4>Learning Path</h4>
|
||||
<pre><code>#searchString=#course #level=beginner #topic=programming
|
||||
#orderBy=difficulty
|
||||
#orderDirection=asc</code></pre>
|
||||
|
||||
<h2>Search Script Examples</h2>
|
||||
<p>For complex logic that can't be expressed in search strings, use JavaScript:</p>
|
||||
|
||||
<h3>Custom Business Logic</h3>
|
||||
<pre><code>// Find notes that need attention based on complex criteria
|
||||
const api = require('api');
|
||||
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - 30);
|
||||
|
||||
const results = [];
|
||||
|
||||
// Find high-priority tasks overdue by more than a week
|
||||
const overdueTasks = api.searchForNotes(`
|
||||
#task #priority=high #dueDate < TODAY-7 #status!=completed
|
||||
`);
|
||||
|
||||
// Find projects with no recent activity
|
||||
const staleProjects = api.searchForNotes(`
|
||||
#project #status=active note.dateModified < TODAY-30
|
||||
`);
|
||||
|
||||
// Find notes with many attributes but no content
|
||||
const overlabeledNotes = api.searchForNotes(`
|
||||
note.attributeCount > 5 note.contentSize < 100
|
||||
`);
|
||||
|
||||
return [...overdueTasks, ...staleProjects, ...overlabeledNotes]
|
||||
.map(note => note.noteId);</code></pre>
|
||||
|
||||
<h3>Dynamic Tag-Based Grouping</h3>
|
||||
<pre><code>// Group notes by quarter based on creation date
|
||||
const api = require('api');
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
const results = [];
|
||||
|
||||
for (let quarter = 1; quarter <= 4; quarter++) {
|
||||
const startMonth = (quarter - 1) * 3 + 1;
|
||||
const endMonth = quarter * 3;
|
||||
|
||||
const quarterNotes = api.searchForNotes(`
|
||||
note.dateCreated >= "${currentYear}-${String(startMonth).padStart(2, '0')}-01"
|
||||
note.dateCreated < "${currentYear}-${String(endMonth + 1).padStart(2, '0')}-01"
|
||||
#project
|
||||
`);
|
||||
|
||||
results.push(...quarterNotes.map(note => note.noteId));
|
||||
}
|
||||
|
||||
return results;</code></pre>
|
||||
|
||||
<h3>Conditional Search Logic</h3>
|
||||
<pre><code>// Smart dashboard that changes based on day of week
|
||||
const api = require('api');
|
||||
|
||||
const today = new Date();
|
||||
const dayOfWeek = today.getDay(); // 0 = Sunday, 1 = Monday, etc.
|
||||
|
||||
let searchQuery;
|
||||
|
||||
if (dayOfWeek === 1) { // Monday - weekly planning
|
||||
searchQuery = '#task #status=planned #week=' + getWeekNumber(today);
|
||||
} else if (dayOfWeek === 5) { // Friday - weekly review
|
||||
searchQuery = '#task #completed=true #week=' + getWeekNumber(today);
|
||||
} else { // Regular days - focus on today's work
|
||||
searchQuery = '#task #dueDate=TODAY #status!=completed';
|
||||
}
|
||||
|
||||
const notes = api.searchForNotes(searchQuery);
|
||||
return notes.map(note => note.noteId);
|
||||
|
||||
function getWeekNumber(date) {
|
||||
const firstDay = new Date(date.getFullYear(), 0, 1);
|
||||
const pastDays = Math.floor((date - firstDay) / 86400000);
|
||||
return Math.ceil((pastDays + firstDay.getDay() + 1) / 7);
|
||||
}</code></pre>
|
||||
|
||||
<h2>Performance Optimization</h2>
|
||||
|
||||
<h3>Fast Search for Large Collections</h3>
|
||||
<p>For collections that don't need content search:</p>
|
||||
<pre><code>#searchString=#category=reference #type=article
|
||||
#fastSearch=true
|
||||
#limit=100</code></pre>
|
||||
|
||||
<h3>Efficient Ordering</h3>
|
||||
<p>Use indexed properties for better performance:</p>
|
||||
<pre><code>#orderBy=dateCreated
|
||||
#orderBy=title
|
||||
#orderBy=noteId</code></pre>
|
||||
<p>Avoid complex calculated orderings in large collections.</p>
|
||||
|
||||
<h3>Result Limiting</h3>
|
||||
<p>Always set reasonable limits for large collections:</p>
|
||||
<pre><code>#limit=50</code></pre>
|
||||
<p>For very large result sets, consider breaking into multiple saved searches.</p>
|
||||
|
||||
<h2>Saved Search Organization</h2>
|
||||
|
||||
<h3>Hierarchical Organization</h3>
|
||||
<p>Create a folder structure for saved searches:</p>
|
||||
<pre><code>📁 Searches
|
||||
├── 📁 Projects
|
||||
│ ├── 🔍 Active Projects
|
||||
│ ├── 🔍 Overdue Tasks
|
||||
│ └── 🔍 Project Archive
|
||||
├── 📁 Content
|
||||
│ ├── 🔍 Recent Drafts
|
||||
│ ├── 🔍 Published Articles
|
||||
│ └── 🔍 Review Queue
|
||||
└── 📁 Maintenance
|
||||
├── 🔍 Untagged Notes
|
||||
├── 🔍 Cleanup Candidates
|
||||
└── 🔍 Orphaned Notes</code></pre>
|
||||
|
||||
<h3>Search Naming Conventions</h3>
|
||||
<p>Use clear, descriptive names:</p>
|
||||
<ul>
|
||||
<li>"Active High-Priority Tasks"</li>
|
||||
<li>"This Month's Meeting Notes"</li>
|
||||
<li>"Unprocessed Inbox Items"</li>
|
||||
<li>"Literature Review Papers"</li>
|
||||
</ul>
|
||||
|
||||
<h3>Search Labels</h3>
|
||||
<p>Tag saved searches for organization:</p>
|
||||
<pre><code>#searchType=dashboard
|
||||
#searchType=maintenance
|
||||
#searchType=archive
|
||||
#frequency=daily
|
||||
#frequency=weekly</code></pre>
|
||||
|
||||
<h2>Dashboard Creation</h2>
|
||||
|
||||
<h3>Personal Dashboard</h3>
|
||||
<p>Combine multiple saved searches in a parent note:</p>
|
||||
<pre><code>📋 My Dashboard
|
||||
├── 🔍 Today's Tasks
|
||||
├── 🔍 Urgent Items
|
||||
├── 🔍 Recent Notes
|
||||
├── 🔍 Upcoming Deadlines
|
||||
└── 🔍 Weekly Review Items</code></pre>
|
||||
|
||||
<h3>Project Dashboard</h3>
|
||||
<pre><code>📋 Project Alpha Dashboard
|
||||
├── 🔍 Active Tasks
|
||||
├── 🔍 Blocked Items
|
||||
├── 🔍 Recent Updates
|
||||
├── 🔍 Milestones
|
||||
└── 🔍 Team Notes</code></pre>
|
||||
|
||||
<h3>Content Dashboard</h3>
|
||||
<pre><code>📋 Content Management
|
||||
├── 🔍 Draft Articles
|
||||
├── 🔍 Review Queue
|
||||
├── 🔍 Published This Month
|
||||
├── 🔍 High-Engagement Posts
|
||||
└── 🔍 Content Ideas</code></pre>
|
||||
|
||||
<h2>Maintenance and Updates</h2>
|
||||
|
||||
<h3>Regular Review</h3>
|
||||
<p>Periodically review saved searches for:</p>
|
||||
<ul>
|
||||
<li>Outdated search criteria</li>
|
||||
<li>Performance issues</li>
|
||||
<li>Unused collections</li>
|
||||
<li>Scope creep</li>
|
||||
</ul>
|
||||
|
||||
<h3>Search Evolution</h3>
|
||||
<p>As your note-taking evolves, update searches:</p>
|
||||
<ul>
|
||||
<li>Add new tags to existing searches</li>
|
||||
<li>Refine criteria based on usage patterns</li>
|
||||
<li>Split large collections into smaller ones</li>
|
||||
<li>Merge rarely-used collections</li>
|
||||
</ul>
|
||||
|
||||
<h3>Performance Monitoring</h3>
|
||||
<p>Watch for performance issues:</p>
|
||||
<ul>
|
||||
<li>Slow-loading saved searches</li>
|
||||
<li>Memory usage with large result sets</li>
|
||||
<li>Search timeout errors</li>
|
||||
</ul>
|
||||
|
||||
<h2>Troubleshooting</h2>
|
||||
|
||||
<h3>Common Issues</h3>
|
||||
|
||||
<h4>Empty Results</h4>
|
||||
<ul>
|
||||
<li>Check search syntax</li>
|
||||
<li>Verify tag spellings</li>
|
||||
<li>Ensure notes have required attributes</li>
|
||||
<li>Test search components individually</li>
|
||||
</ul>
|
||||
|
||||
<h4>Performance Problems</h4>
|
||||
<ul>
|
||||
<li>Add <code>#fastSearch=true</code> for attribute-only searches</li>
|
||||
<li>Reduce result limits</li>
|
||||
<li>Simplify complex criteria</li>
|
||||
<li>Use indexed properties for ordering</li>
|
||||
</ul>
|
||||
|
||||
<h4>Unexpected Results</h4>
|
||||
<ul>
|
||||
<li>Enable debug mode to see query parsing</li>
|
||||
<li>Test search in search dialog first</li>
|
||||
<li>Check for case sensitivity issues</li>
|
||||
<li>Verify date formats and ranges</li>
|
||||
</ul>
|
||||
|
||||
<h3>Best Practices</h3>
|
||||
|
||||
<h4>Search Design</h4>
|
||||
<ul>
|
||||
<li>Start simple and add complexity gradually</li>
|
||||
<li>Test searches thoroughly before saving</li>
|
||||
<li>Document complex search logic</li>
|
||||
<li>Use meaningful names and descriptions</li>
|
||||
</ul>
|
||||
|
||||
<h4>Performance</h4>
|
||||
<ul>
|
||||
<li>Set appropriate limits</li>
|
||||
<li>Use fast search when possible</li>
|
||||
<li>Avoid overly complex expressions</li>
|
||||
<li>Monitor search execution time</li>
|
||||
</ul>
|
||||
|
||||
<h4>Organization</h4>
|
||||
<ul>
|
||||
<li>Group related searches</li>
|
||||
<li>Use consistent naming conventions</li>
|
||||
<li>Archive unused searches</li>
|
||||
<li>Regular cleanup and maintenance</li>
|
||||
</ul>
|
||||
|
||||
<h2>Next Steps</h2>
|
||||
<ul>
|
||||
<li><a class="reference-link" href="#root/_help_search_technical">Technical Search Details</a> - Understanding search performance and implementation</li>
|
||||
<li><a class="reference-link" href="#root/_help_search_examples">Search Examples and Use Cases</a> - More practical examples</li>
|
||||
<li><a class="reference-link" href="#root/_help_search_advanced">Advanced Search Expressions</a> - Complex query construction</li>
|
||||
</ul>
|
||||
160
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Documentation.html
generated
vendored
Normal file
160
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Documentation.html
generated
vendored
Normal file
@@ -0,0 +1,160 @@
|
||||
<h1>Trilium Search Documentation</h1>
|
||||
<p>Welcome to the comprehensive guide for Trilium's powerful search capabilities. This documentation covers everything from basic text searches to advanced query expressions and performance optimization.</p>
|
||||
|
||||
<h2>Quick Start</h2>
|
||||
<p>New to Trilium search? Start here:</p>
|
||||
<ul>
|
||||
<li><strong><a class="reference-link" href="#root/_help_search_fundamentals">Search Fundamentals</a></strong> - Basic concepts, syntax, and operators</li>
|
||||
</ul>
|
||||
|
||||
<h2>Documentation Sections</h2>
|
||||
|
||||
<h3>Core Search Features</h3>
|
||||
<ul>
|
||||
<li><strong><a class="reference-link" href="#root/_help_search_fundamentals">Search Fundamentals</a></strong> - Basic search syntax, operators, and concepts</li>
|
||||
<li><strong><a class="reference-link" href="#root/_help_search_advanced">Advanced Search Expressions</a></strong> - Complex queries, boolean logic, and relationship traversal</li>
|
||||
</ul>
|
||||
|
||||
<h3>Practical Applications</h3>
|
||||
<ul>
|
||||
<li><strong><a class="reference-link" href="#root/_help_search_examples">Search Examples and Use Cases</a></strong> - Real-world examples for common workflows</li>
|
||||
<li><strong><a class="reference-link" href="#root/_help_search_saved">Saved Searches</a></strong> - Creating dynamic collections and dashboards</li>
|
||||
</ul>
|
||||
|
||||
<h3>Technical Reference</h3>
|
||||
<ul>
|
||||
<li><strong><a class="reference-link" href="#root/_help_search_technical">Technical Search Details</a></strong> - Performance, implementation, and optimization</li>
|
||||
</ul>
|
||||
|
||||
<h2>Key Search Capabilities</h2>
|
||||
|
||||
<h3>Full-Text Search</h3>
|
||||
<ul>
|
||||
<li>Search note titles and content</li>
|
||||
<li>Exact phrase matching with quotes</li>
|
||||
<li>Case-insensitive with diacritic normalization</li>
|
||||
<li>Support for multiple note types (text, code, mermaid, canvas)</li>
|
||||
</ul>
|
||||
|
||||
<h3>Attribute-Based Search</h3>
|
||||
<ul>
|
||||
<li>Label searches: <code>#tag</code>, <code>#category=book</code></li>
|
||||
<li>Relation searches: <code>~author</code>, <code>~author.title=Tolkien</code></li>
|
||||
<li>Complex attribute combinations</li>
|
||||
<li>Fuzzy attribute matching</li>
|
||||
</ul>
|
||||
|
||||
<h3>Property Search</h3>
|
||||
<ul>
|
||||
<li>Note metadata: <code>note.type=text</code>, <code>note.dateCreated >= TODAY-7</code></li>
|
||||
<li>Hierarchical queries: <code>note.parents.title=Books</code></li>
|
||||
<li>Relationship traversal: <code>note.children.labels.status=active</code></li>
|
||||
</ul>
|
||||
|
||||
<h3>Advanced Features</h3>
|
||||
<ul>
|
||||
<li><strong>Progressive Search</strong>: Exact matching first, fuzzy fallback when needed</li>
|
||||
<li><strong>Fuzzy Search</strong>: Typo tolerance and spelling variations</li>
|
||||
<li><strong>Boolean Logic</strong>: Complex AND/OR/NOT combinations</li>
|
||||
<li><strong>Date Arithmetic</strong>: Dynamic date calculations (TODAY-30, YEAR+1)</li>
|
||||
<li><strong>Regular Expressions</strong>: Pattern matching with <code>%=</code> operator</li>
|
||||
<li><strong>Ordering and Limiting</strong>: Custom sort orders and result limits</li>
|
||||
</ul>
|
||||
|
||||
<h2>Search Operators Quick Reference</h2>
|
||||
|
||||
<h3>Text Operators</h3>
|
||||
<ul>
|
||||
<li><code>=</code> - Exact match</li>
|
||||
<li><code>!=</code> - Not equal</li>
|
||||
<li><code>*=*</code> - Contains</li>
|
||||
<li><code>=*</code> - Starts with</li>
|
||||
<li><code>*=</code> - Ends with</li>
|
||||
<li><code>%=</code> - Regular expression</li>
|
||||
<li><code>~=</code> - Fuzzy exact match</li>
|
||||
<li><code>~*</code> - Fuzzy contains match</li>
|
||||
</ul>
|
||||
|
||||
<h3>Numeric Operators</h3>
|
||||
<ul>
|
||||
<li><code>=</code>, <code>!=</code>, <code>></code>, <code>>=</code>, <code><</code>, <code><=</code></li>
|
||||
</ul>
|
||||
|
||||
<h3>Boolean Operators</h3>
|
||||
<ul>
|
||||
<li><code>AND</code>, <code>OR</code>, <code>NOT</code></li>
|
||||
</ul>
|
||||
|
||||
<h3>Special Syntax</h3>
|
||||
<ul>
|
||||
<li><code>#labelName</code> - Label exists</li>
|
||||
<li><code>#labelName=value</code> - Label equals value</li>
|
||||
<li><code>~relationName</code> - Relation exists</li>
|
||||
<li><code>~relationName.property</code> - Relation target property</li>
|
||||
<li><code>note.property</code> - Note property access</li>
|
||||
<li><code>"exact phrase"</code> - Quoted phrase search</li>
|
||||
</ul>
|
||||
|
||||
<h2>Common Search Patterns</h2>
|
||||
|
||||
<h3>Simple Searches</h3>
|
||||
<pre><code>hello world # Find notes containing both words
|
||||
"project management" # Find exact phrase
|
||||
#task # Find notes with "task" label
|
||||
~author # Find notes with "author" relation</code></pre>
|
||||
|
||||
<h3>Attribute Searches</h3>
|
||||
<pre><code>#book #author=Tolkien # Books by Tolkien
|
||||
#task #priority=high #status!=completed # High-priority incomplete tasks
|
||||
~project.title *=* alpha # Notes related to projects with "alpha" in title</code></pre>
|
||||
|
||||
<h3>Date-Based Searches</h3>
|
||||
<pre><code>note.dateCreated >= TODAY-7 # Notes created in last week
|
||||
#dueDate <= TODAY+30 # Items due in next 30 days
|
||||
#eventDate = YEAR # Events scheduled for this year</code></pre>
|
||||
|
||||
<h3>Complex Queries</h3>
|
||||
<pre><code>(#book OR #article) AND #topic=programming AND note.dateModified >= MONTH
|
||||
#project AND (#status=active OR #status=pending) AND not(note.isArchived=true)</code></pre>
|
||||
|
||||
<h2>Getting Started Checklist</h2>
|
||||
<ol>
|
||||
<li><strong>Learn Basic Syntax</strong> - Start with simple text and tag searches</li>
|
||||
<li><strong>Understand Operators</strong> - Master the core operators (<code>=</code>, <code>*=*</code>, etc.)</li>
|
||||
<li><strong>Practice Attributes</strong> - Use <code>#</code> for labels and <code>~</code> for relations</li>
|
||||
<li><strong>Try Boolean Logic</strong> - Combine searches with AND/OR/NOT</li>
|
||||
<li><strong>Explore Properties</strong> - Use <code>note.</code> prefix for metadata searches</li>
|
||||
<li><strong>Create Saved Searches</strong> - Turn useful queries into dynamic collections</li>
|
||||
<li><strong>Optimize Performance</strong> - Learn about fast search and limits</li>
|
||||
</ol>
|
||||
|
||||
<h2>Performance Tips</h2>
|
||||
<ul>
|
||||
<li><strong>Use Fast Search</strong> for attribute-only queries</li>
|
||||
<li><strong>Set Reasonable Limits</strong> to prevent large result sets</li>
|
||||
<li><strong>Start Specific</strong> with the most selective criteria first</li>
|
||||
<li><strong>Leverage Attributes</strong> instead of content search when possible</li>
|
||||
<li><strong>Cache Common Queries</strong> as saved searches</li>
|
||||
</ul>
|
||||
|
||||
<h2>Need Help?</h2>
|
||||
<ul>
|
||||
<li><strong>Examples</strong>: Check <a class="reference-link" href="#root/_help_search_examples">Search Examples and Use Cases</a> for practical patterns</li>
|
||||
<li><strong>Complex Queries</strong>: See <a class="reference-link" href="#root/_help_search_advanced">Advanced Search Expressions</a> for sophisticated techniques</li>
|
||||
<li><strong>Performance Issues</strong>: Review <a class="reference-link" href="#root/_help_search_technical">Technical Search Details</a> for optimization</li>
|
||||
<li><strong>Dynamic Collections</strong>: Learn about <a class="reference-link" href="#root/_help_search_saved">Saved Searches</a> for automated organization</li>
|
||||
</ul>
|
||||
|
||||
<h2>Search Workflow Integration</h2>
|
||||
<p>Trilium's search integrates seamlessly with your note-taking workflow:</p>
|
||||
<ul>
|
||||
<li><strong>Quick Search</strong> (Ctrl+S) for instant access</li>
|
||||
<li><strong>Saved Searches</strong> for dynamic organization</li>
|
||||
<li><strong>Search from Subtree</strong> for focused queries</li>
|
||||
<li><strong>Auto-complete</strong> suggestions in search dialogs</li>
|
||||
<li><strong>URL-triggered searches</strong> for bookmarkable queries</li>
|
||||
</ul>
|
||||
|
||||
<p>Start with the fundamentals and gradually explore advanced features as your needs grow. Trilium's search system is designed to scale from simple text queries to sophisticated knowledge management systems.</p>
|
||||
|
||||
<p>Happy searching! 🔍</p>
|
||||
245
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Examples-and-Use-Cases.html
generated
vendored
Normal file
245
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Examples-and-Use-Cases.html
generated
vendored
Normal file
@@ -0,0 +1,245 @@
|
||||
<h1>Search Examples and Use Cases</h1>
|
||||
<p>This guide provides practical examples of how to use Trilium's search capabilities for common organizational patterns and workflows.</p>
|
||||
|
||||
<h2>Personal Knowledge Management</h2>
|
||||
|
||||
<h3>Research and Learning</h3>
|
||||
<p>Track your learning progress and find related materials:</p>
|
||||
<pre><code>#topic=javascript #status=learning</code></pre>
|
||||
<p>Find all JavaScript materials you're currently learning.</p>
|
||||
<pre><code>#course #completed=false note.dateCreated >= MONTH-1</code></pre>
|
||||
<p>Find courses started in the last month that aren't completed.</p>
|
||||
<pre><code>#book #topic *=* programming #rating >= 4</code></pre>
|
||||
<p>Find highly-rated programming books.</p>
|
||||
<pre><code>#paper ~author.title *=* "Andrew Ng" #field=machine-learning</code></pre>
|
||||
<p>Find machine learning papers by Andrew Ng.</p>
|
||||
|
||||
<h3>Meeting and Event Management</h3>
|
||||
<p>Organize meetings, notes, and follow-ups:</p>
|
||||
<pre><code>#meeting note.dateCreated >= TODAY-7 #attendee *=* smith</code></pre>
|
||||
<p>Find this week's meetings with Smith.</p>
|
||||
<pre><code>#meeting #actionItems #status!=completed</code></pre>
|
||||
<p>Find meetings with outstanding action items.</p>
|
||||
<pre><code>#event #date >= TODAY #date <= TODAY+30</code></pre>
|
||||
<p>Find upcoming events in the next 30 days.</p>
|
||||
<pre><code>#meeting #project=alpha note.dateCreated >= MONTH</code></pre>
|
||||
<p>Find this month's meetings about project alpha.</p>
|
||||
|
||||
<h3>Note Organization and Cleanup</h3>
|
||||
<p>Maintain and organize your note structure:</p>
|
||||
<pre><code>note.childrenCount = 0 note.parentCount = 1 note.contentSize < 50 note.dateModified < TODAY-180</code></pre>
|
||||
<p>Find small, isolated notes not modified in 6 months (cleanup candidates).</p>
|
||||
<pre><code>note.attributeCount = 0 note.type=text note.contentSize > 1000</code></pre>
|
||||
<p>Find large text notes without any labels (might need categorization).</p>
|
||||
<pre><code>#draft note.dateCreated < TODAY-30</code></pre>
|
||||
<p>Find old draft notes that might need attention.</p>
|
||||
<pre><code>note.parentCount > 3 note.type=text</code></pre>
|
||||
<p>Find notes that are heavily cloned (might indicate important content).</p>
|
||||
|
||||
<h2>Project Management</h2>
|
||||
|
||||
<h3>Task Tracking</h3>
|
||||
<p>Manage tasks and project progress:</p>
|
||||
<pre><code>#task #priority=high #status!=completed #assignee=me</code></pre>
|
||||
<p>Find your high-priority incomplete tasks.</p>
|
||||
<pre><code>#task #dueDate <= TODAY+3 #dueDate >= TODAY #status!=completed</code></pre>
|
||||
<p>Find tasks due in the next 3 days.</p>
|
||||
<pre><code>#project=website #task #status=blocked</code></pre>
|
||||
<p>Find blocked tasks in the website project.</p>
|
||||
<pre><code>#task #estimatedHours > 0 #actualHours > 0 orderBy note.dateModified desc</code></pre>
|
||||
<p>Find tasks with time tracking data, sorted by recent updates.</p>
|
||||
|
||||
<h3>Project Oversight</h3>
|
||||
<p>Monitor project health and progress:</p>
|
||||
<pre><code>#project #status=active note.children.labels.status = blocked</code></pre>
|
||||
<p>Find active projects with blocked tasks.</p>
|
||||
<pre><code>#project #startDate <= TODAY-90 #status!=completed</code></pre>
|
||||
<p>Find projects that started over 90 days ago but aren't completed.</p>
|
||||
<pre><code>#milestone #targetDate <= TODAY #status!=achieved</code></pre>
|
||||
<p>Find overdue milestones.</p>
|
||||
<pre><code>#project orderBy note.childrenCount desc limit 10</code></pre>
|
||||
<p>Find the 10 largest projects by number of sub-notes.</p>
|
||||
|
||||
<h3>Resource Planning</h3>
|
||||
<p>Track resources and dependencies:</p>
|
||||
<pre><code>#resource #type=person #availability < 50</code></pre>
|
||||
<p>Find people with low availability.</p>
|
||||
<pre><code>#dependency #status=pending #project=mobile-app</code></pre>
|
||||
<p>Find pending dependencies for the mobile app project.</p>
|
||||
<pre><code>#budget #project #spent > #allocated</code></pre>
|
||||
<p>Find projects over budget.</p>
|
||||
|
||||
<h2>Content Creation and Writing</h2>
|
||||
|
||||
<h3>Writing Projects</h3>
|
||||
<p>Manage articles, books, and documentation:</p>
|
||||
<pre><code>#article #status=draft #wordCount >= 1000</code></pre>
|
||||
<p>Find substantial draft articles.</p>
|
||||
<pre><code>#chapter #book=novel #status=outline</code></pre>
|
||||
<p>Find novel chapters still in outline stage.</p>
|
||||
<pre><code>#blog-post #published=false #topic=technology</code></pre>
|
||||
<p>Find unpublished technology blog posts.</p>
|
||||
<pre><code>#documentation #lastReviewed < TODAY-90 #product=api</code></pre>
|
||||
<p>Find API documentation not reviewed in 90 days.</p>
|
||||
|
||||
<h3>Editorial Workflow</h3>
|
||||
<p>Track editing and publication status:</p>
|
||||
<pre><code>#article #editor=jane #status=review</code></pre>
|
||||
<p>Find articles assigned to Jane for review.</p>
|
||||
<pre><code>#manuscript #submissionDate >= TODAY-30 #status=pending</code></pre>
|
||||
<p>Find manuscripts submitted in the last 30 days still pending.</p>
|
||||
<pre><code>#publication #acceptanceDate >= YEAR #status=accepted</code></pre>
|
||||
<p>Find accepted publications this year.</p>
|
||||
|
||||
<h3>Content Research</h3>
|
||||
<p>Organize research materials and sources:</p>
|
||||
<pre><code>#source #reliability >= 8 #topic *=* climate</code></pre>
|
||||
<p>Find reliable sources about climate topics.</p>
|
||||
<pre><code>#quote #author *=* Einstein #verified=true</code></pre>
|
||||
<p>Find verified Einstein quotes.</p>
|
||||
<pre><code>#citation #used=false #relevance=high</code></pre>
|
||||
<p>Find high-relevance citations not yet used.</p>
|
||||
|
||||
<h2>Business and Professional Use</h2>
|
||||
|
||||
<h3>Client Management</h3>
|
||||
<p>Track client relationships and projects:</p>
|
||||
<pre><code>#client=acme #project #status=active</code></pre>
|
||||
<p>Find active projects for ACME client.</p>
|
||||
<pre><code>#meeting #client #date >= MONTH #followUp=required</code></pre>
|
||||
<p>Find client meetings this month requiring follow-up.</p>
|
||||
<pre><code>#contract #renewalDate <= TODAY+60 #renewalDate >= TODAY</code></pre>
|
||||
<p>Find contracts expiring in the next 60 days.</p>
|
||||
<pre><code>#invoice #status=unpaid #dueDate < TODAY</code></pre>
|
||||
<p>Find overdue unpaid invoices.</p>
|
||||
|
||||
<h3>Process Documentation</h3>
|
||||
<p>Maintain procedures and workflows:</p>
|
||||
<pre><code>#procedure #department=engineering #lastUpdated < TODAY-365</code></pre>
|
||||
<p>Find engineering procedures not updated in a year.</p>
|
||||
<pre><code>#workflow #status=active #automation=possible</code></pre>
|
||||
<p>Find active workflows that could be automated.</p>
|
||||
<pre><code>#checklist #process=onboarding #role=developer</code></pre>
|
||||
<p>Find onboarding checklists for developers.</p>
|
||||
|
||||
<h3>Compliance and Auditing</h3>
|
||||
<p>Track compliance requirements and audits:</p>
|
||||
<pre><code>#compliance #standard=sox #nextReview <= TODAY+30</code></pre>
|
||||
<p>Find SOX compliance items due for review soon.</p>
|
||||
<pre><code>#audit #finding #severity=high #status!=resolved</code></pre>
|
||||
<p>Find unresolved high-severity audit findings.</p>
|
||||
<pre><code>#policy #department=hr #effectiveDate >= YEAR</code></pre>
|
||||
<p>Find HR policies that became effective this year.</p>
|
||||
|
||||
<h2>Academic and Educational Use</h2>
|
||||
|
||||
<h3>Course Management</h3>
|
||||
<p>Organize courses and educational content:</p>
|
||||
<pre><code>#course #semester=fall-2024 #assignment #dueDate >= TODAY</code></pre>
|
||||
<p>Find upcoming assignments for fall 2024 courses.</p>
|
||||
<pre><code>#lecture #course=physics #topic *=* quantum</code></pre>
|
||||
<p>Find physics lectures about quantum topics.</p>
|
||||
<pre><code>#student #grade < 70 #course=mathematics</code></pre>
|
||||
<p>Find students struggling in mathematics.</p>
|
||||
<pre><code>#syllabus #course #lastUpdated < TODAY-180</code></pre>
|
||||
<p>Find syllabi not updated in 6 months.</p>
|
||||
|
||||
<h3>Research Management</h3>
|
||||
<p>Track research projects and publications:</p>
|
||||
<pre><code>#experiment #status=running #endDate <= TODAY+7</code></pre>
|
||||
<p>Find experiments ending in the next week.</p>
|
||||
<pre><code>#dataset #size > 1000000 #cleaned=true #public=false</code></pre>
|
||||
<p>Find large, cleaned, private datasets.</p>
|
||||
<pre><code>#hypothesis #tested=false #priority=high</code></pre>
|
||||
<p>Find high-priority untested hypotheses.</p>
|
||||
<pre><code>#collaboration #institution *=* stanford #status=active</code></pre>
|
||||
<p>Find active collaborations with Stanford.</p>
|
||||
|
||||
<h3>Grant and Funding</h3>
|
||||
<p>Manage funding applications and requirements:</p>
|
||||
<pre><code>#grant #deadline <= TODAY+30 #deadline >= TODAY #status=in-progress</code></pre>
|
||||
<p>Find grant applications due in the next 30 days.</p>
|
||||
<pre><code>#funding #amount >= 100000 #status=awarded #startDate >= YEAR</code></pre>
|
||||
<p>Find large grants awarded this year.</p>
|
||||
<pre><code>#report #funding #dueDate <= TODAY+14 #status!=submitted</code></pre>
|
||||
<p>Find funding reports due in 2 weeks.</p>
|
||||
|
||||
<h2>Technical Documentation</h2>
|
||||
|
||||
<h3>Code and Development</h3>
|
||||
<p>Track code-related notes and documentation:</p>
|
||||
<pre><code>#bug #severity=critical #status!=fixed #product=webapp</code></pre>
|
||||
<p>Find critical unfixed bugs in the web app.</p>
|
||||
<pre><code>#feature #version=2.0 #status=implemented #tested=false</code></pre>
|
||||
<p>Find version 2.0 features that are implemented but not tested.</p>
|
||||
<pre><code>#api #endpoint #deprecated=true #removalDate <= TODAY+90</code></pre>
|
||||
<p>Find deprecated API endpoints scheduled for removal soon.</p>
|
||||
<pre><code>#architecture #component=database #lastReviewed < TODAY-180</code></pre>
|
||||
<p>Find database architecture documentation not reviewed in 6 months.</p>
|
||||
|
||||
<h3>System Administration</h3>
|
||||
<p>Manage infrastructure and operations:</p>
|
||||
<pre><code>#server #status=maintenance #scheduledDate >= TODAY #scheduledDate <= TODAY+7</code></pre>
|
||||
<p>Find servers scheduled for maintenance this week.</p>
|
||||
<pre><code>#backup #status=failed #date >= TODAY-7</code></pre>
|
||||
<p>Find backup failures in the last week.</p>
|
||||
<pre><code>#security #vulnerability #severity=high #patched=false</code></pre>
|
||||
<p>Find unpatched high-severity vulnerabilities.</p>
|
||||
<pre><code>#monitoring #alert #frequency > 10 #period=week</code></pre>
|
||||
<p>Find alerts triggering more than 10 times per week.</p>
|
||||
|
||||
<h2>Data Analysis and Reporting</h2>
|
||||
|
||||
<h3>Performance Tracking</h3>
|
||||
<p>Monitor metrics and KPIs:</p>
|
||||
<pre><code>#metric #kpi=true #trend=declining #period=month</code></pre>
|
||||
<p>Find declining monthly KPIs.</p>
|
||||
<pre><code>#report #frequency=weekly #lastGenerated < TODAY-10</code></pre>
|
||||
<p>Find weekly reports that haven't been generated in 10 days.</p>
|
||||
<pre><code>#dashboard #stakeholder=executive #lastUpdated < TODAY-7</code></pre>
|
||||
<p>Find executive dashboards not updated this week.</p>
|
||||
|
||||
<h3>Trend Analysis</h3>
|
||||
<p>Track patterns and changes over time:</p>
|
||||
<pre><code>#data #source=sales #period=quarter #analyzed=false</code></pre>
|
||||
<p>Find unanalyzed quarterly sales data.</p>
|
||||
<pre><code>#trend #direction=up #significance=high #period=month</code></pre>
|
||||
<p>Find significant positive monthly trends.</p>
|
||||
<pre><code>#forecast #accuracy < 80 #model=linear #period=quarter</code></pre>
|
||||
<p>Find inaccurate quarterly linear forecasts.</p>
|
||||
|
||||
<h2>Search Strategy Tips</h2>
|
||||
|
||||
<h3>Building Effective Queries</h3>
|
||||
<ol>
|
||||
<li><strong>Start Specific</strong>: Begin with the most selective criteria</li>
|
||||
<li><strong>Add Gradually</strong>: Build complexity incrementally</li>
|
||||
<li><strong>Test Components</strong>: Verify each part of complex queries</li>
|
||||
<li><strong>Use Shortcuts</strong>: Leverage <code>#</code> and <code>~</code> shortcuts for efficiency</li>
|
||||
</ol>
|
||||
|
||||
<h3>Performance Optimization</h3>
|
||||
<ol>
|
||||
<li><strong>Use Fast Search</strong>: For large databases, enable fast search when content isn't needed</li>
|
||||
<li><strong>Limit Results</strong>: Add limits to prevent overwhelming result sets</li>
|
||||
<li><strong>Order Strategically</strong>: Put the most useful results first</li>
|
||||
<li><strong>Cache Common Queries</strong>: Save frequently used searches</li>
|
||||
</ol>
|
||||
|
||||
<h3>Maintenance Patterns</h3>
|
||||
<p>Regular queries for note maintenance:</p>
|
||||
<pre><code># Weekly cleanup check
|
||||
note.attributeCount = 0 note.type=text note.contentSize < 100 note.dateModified < TODAY-30
|
||||
|
||||
# Monthly project review
|
||||
#project #status=active note.dateModified < TODAY-30
|
||||
|
||||
# Quarterly archive review
|
||||
note.isArchived=false note.dateModified < TODAY-90 note.childrenCount = 0</code></pre>
|
||||
|
||||
<h2>Next Steps</h2>
|
||||
<ul>
|
||||
<li><a class="reference-link" href="#root/_help_search_saved">Saved Searches</a> - Convert these examples into reusable saved searches</li>
|
||||
<li><a class="reference-link" href="#root/_help_search_technical">Technical Search Details</a> - Understanding performance and implementation</li>
|
||||
<li><a class="reference-link" href="#root/_help_search_fundamentals">Search Fundamentals</a> - Review basic concepts and syntax</li>
|
||||
</ul>
|
||||
181
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Fundamentals.html
generated
vendored
Normal file
181
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Search-Fundamentals.html
generated
vendored
Normal file
@@ -0,0 +1,181 @@
|
||||
<h1>Search Fundamentals</h1>
|
||||
<p>Trilium's search system is a powerful tool for finding and organizing notes. It supports multiple search modes, from simple text queries to complex expressions using attributes, relationships, and note properties.</p>
|
||||
|
||||
<h2>Search Types Overview</h2>
|
||||
<p>Trilium provides three main search approaches:</p>
|
||||
<ol>
|
||||
<li><strong>Full-text Search</strong> - Searches within note titles and content</li>
|
||||
<li><strong>Attribute Search</strong> - Searches based on labels and relations attached to notes</li>
|
||||
<li><strong>Property Search</strong> - Searches based on note metadata (type, creation date, etc.)</li>
|
||||
</ol>
|
||||
<p>These can be combined in powerful ways to create precise queries.</p>
|
||||
|
||||
<h2>Basic Search Syntax</h2>
|
||||
|
||||
<h3>Simple Text Search</h3>
|
||||
<pre><code>hello world</code></pre>
|
||||
<p>Finds notes containing both "hello" and "world" anywhere in the title or content.</p>
|
||||
|
||||
<h3>Quoted Text Search</h3>
|
||||
<pre><code>"hello world"</code></pre>
|
||||
<p>Finds notes containing the exact phrase "hello world".</p>
|
||||
|
||||
<h3>Attribute Search</h3>
|
||||
<pre><code>#tag</code></pre>
|
||||
<p>Finds notes with the label "tag".</p>
|
||||
<pre><code>#category=book</code></pre>
|
||||
<p>Finds notes with label "category" set to "book".</p>
|
||||
|
||||
<h3>Relation Search</h3>
|
||||
<pre><code>~author</code></pre>
|
||||
<p>Finds notes with a relation named "author".</p>
|
||||
<pre><code>~author.title=Tolkien</code></pre>
|
||||
<p>Finds notes with an "author" relation pointing to a note titled "Tolkien".</p>
|
||||
|
||||
<h2>Search Operators</h2>
|
||||
|
||||
<h3>Text Operators</h3>
|
||||
<ul>
|
||||
<li><code>=</code> - Exact match</li>
|
||||
<li><code>!=</code> - Not equal</li>
|
||||
<li><code>*=*</code> - Contains (substring)</li>
|
||||
<li><code>=*</code> - Starts with</li>
|
||||
<li><code>*=</code> - Ends with</li>
|
||||
<li><code>%=</code> - Regular expression match</li>
|
||||
<li><code>~=</code> - Fuzzy exact match</li>
|
||||
<li><code>~*</code> - Fuzzy contains match</li>
|
||||
</ul>
|
||||
|
||||
<h3>Numeric Operators</h3>
|
||||
<ul>
|
||||
<li><code>=</code> - Equal</li>
|
||||
<li><code>!=</code> - Not equal</li>
|
||||
<li><code>></code> - Greater than</li>
|
||||
<li><code>>=</code> - Greater than or equal</li>
|
||||
<li><code><</code> - Less than</li>
|
||||
<li><code><=</code> - Less than or equal</li>
|
||||
</ul>
|
||||
|
||||
<h3>Boolean Operators</h3>
|
||||
<ul>
|
||||
<li><code>AND</code> - Both conditions must be true</li>
|
||||
<li><code>OR</code> - Either condition must be true</li>
|
||||
<li><code>NOT</code> or <code>not()</code> - Condition must be false</li>
|
||||
</ul>
|
||||
|
||||
<h2>Search Context and Scope</h2>
|
||||
|
||||
<h3>Search Scope</h3>
|
||||
<p>By default, search covers:</p>
|
||||
<ul>
|
||||
<li>Note titles</li>
|
||||
<li>Note content (for text-based note types)</li>
|
||||
<li>Label names and values</li>
|
||||
<li>Relation names</li>
|
||||
<li>Note properties</li>
|
||||
</ul>
|
||||
|
||||
<h3>Fast Search Mode</h3>
|
||||
<p>When enabled, fast search:</p>
|
||||
<ul>
|
||||
<li>Searches only titles and attributes</li>
|
||||
<li>Skips note content</li>
|
||||
<li>Provides faster results for large databases</li>
|
||||
</ul>
|
||||
|
||||
<h3>Archived Notes</h3>
|
||||
<ul>
|
||||
<li>Excluded by default</li>
|
||||
<li>Can be included with "Include archived" option</li>
|
||||
</ul>
|
||||
|
||||
<h2>Case Sensitivity and Normalization</h2>
|
||||
<ul>
|
||||
<li>All searches are case-insensitive</li>
|
||||
<li>Diacritics are normalized ("café" matches "cafe")</li>
|
||||
<li>Unicode characters are properly handled</li>
|
||||
</ul>
|
||||
|
||||
<h2>Performance Considerations</h2>
|
||||
|
||||
<h3>Content Size Limits</h3>
|
||||
<ul>
|
||||
<li>Note content is limited to 10MB for search processing</li>
|
||||
<li>Larger notes are still searchable by title and attributes</li>
|
||||
</ul>
|
||||
|
||||
<h3>Progressive Search Strategy</h3>
|
||||
<ol>
|
||||
<li><strong>Exact Search Phase</strong>: Fast exact matching (handles 90%+ of searches)</li>
|
||||
<li><strong>Fuzzy Search Phase</strong>: Activated when exact search returns fewer than 5 high-quality results</li>
|
||||
<li><strong>Result Ordering</strong>: Exact matches always appear before fuzzy matches</li>
|
||||
</ol>
|
||||
|
||||
<h3>Search Optimization Tips</h3>
|
||||
<ul>
|
||||
<li>Use specific terms rather than very common words</li>
|
||||
<li>Combine full-text with attribute searches for precision</li>
|
||||
<li>Use fast search for large databases when content search isn't needed</li>
|
||||
<li>Limit results when dealing with very large result sets</li>
|
||||
</ul>
|
||||
|
||||
<h2>Special Characters and Escaping</h2>
|
||||
|
||||
<h3>Reserved Characters</h3>
|
||||
<p>These characters have special meaning in search queries:</p>
|
||||
<ul>
|
||||
<li><code>#</code> - Label indicator</li>
|
||||
<li><code>~</code> - Relation indicator</li>
|
||||
<li><code>()</code> - Grouping</li>
|
||||
<li><code>"</code> <code>'</code> <code>`</code> - Quotes for exact phrases</li>
|
||||
</ul>
|
||||
|
||||
<h3>Escaping Special Characters</h3>
|
||||
<p>Use backslash to search for literal special characters:</p>
|
||||
<pre><code>\#hashtag</code></pre>
|
||||
<p>Searches for the literal text "#hashtag" instead of a label.</p>
|
||||
|
||||
<p>Use quotes to include special characters in phrases:</p>
|
||||
<pre><code>"note.txt file"</code></pre>
|
||||
<p>Searches for the exact phrase including the dot.</p>
|
||||
|
||||
<h2>Date and Time Values</h2>
|
||||
|
||||
<h3>Special Date Keywords</h3>
|
||||
<ul>
|
||||
<li><code>TODAY</code> - Current date</li>
|
||||
<li><code>NOW</code> - Current date and time</li>
|
||||
<li><code>MONTH</code> - Current month</li>
|
||||
<li><code>YEAR</code> - Current year</li>
|
||||
</ul>
|
||||
|
||||
<h3>Date Arithmetic</h3>
|
||||
<pre><code>#dateCreated >= TODAY-30</code></pre>
|
||||
<p>Finds notes created in the last 30 days.</p>
|
||||
<pre><code>#eventDate = YEAR+1</code></pre>
|
||||
<p>Finds notes with eventDate set to next year.</p>
|
||||
|
||||
<h2>Search Results and Scoring</h2>
|
||||
|
||||
<h3>Result Ranking</h3>
|
||||
<p>Results are ordered by:</p>
|
||||
<ol>
|
||||
<li>Relevance score (based on term frequency and position)</li>
|
||||
<li>Note depth (closer to root ranks higher)</li>
|
||||
<li>Alphabetical order for ties</li>
|
||||
</ol>
|
||||
|
||||
<h3>Progressive Search Behavior</h3>
|
||||
<ul>
|
||||
<li>Exact matches always rank before fuzzy matches</li>
|
||||
<li>High-quality exact matches prevent fuzzy search activation</li>
|
||||
<li>Fuzzy matches help find content with typos or variations</li>
|
||||
</ul>
|
||||
|
||||
<h2>Next Steps</h2>
|
||||
<ul>
|
||||
<li><a class="reference-link" href="#root/_help_search_advanced">Advanced Search Expressions</a> - Complex queries and combinations</li>
|
||||
<li><a class="reference-link" href="#root/_help_search_examples">Search Examples and Use Cases</a> - Practical applications</li>
|
||||
<li><a class="reference-link" href="#root/_help_search_saved">Saved Searches</a> - Creating dynamic collections</li>
|
||||
<li><a class="reference-link" href="#root/_help_search_technical">Technical Search Details</a> - Under-the-hood implementation</li>
|
||||
</ul>
|
||||
499
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Technical-Search-Details.html
generated
vendored
Normal file
499
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Search/Technical-Search-Details.html
generated
vendored
Normal file
@@ -0,0 +1,499 @@
|
||||
<h1>Technical Search Details</h1>
|
||||
<p>This guide provides technical information about Trilium's search implementation, performance characteristics, and optimization strategies for power users and administrators.</p>
|
||||
|
||||
<h2>Search Architecture Overview</h2>
|
||||
|
||||
<h3>Three-Layer Search System</h3>
|
||||
<p>Trilium's search operates across three cache layers:</p>
|
||||
<ol>
|
||||
<li><strong>Becca (Backend Cache)</strong>: Server-side entity cache containing notes, attributes, and relationships</li>
|
||||
<li><strong>Froca (Frontend Cache)</strong>: Client-side mirror providing fast UI updates</li>
|
||||
<li><strong>Database Layer</strong>: SQLite database with FTS (Full-Text Search) support</li>
|
||||
</ol>
|
||||
|
||||
<h3>Search Processing Pipeline</h3>
|
||||
<ol>
|
||||
<li><strong>Lexical Analysis</strong>: Query parsing and tokenization</li>
|
||||
<li><strong>Expression Building</strong>: Converting tokens to executable expressions</li>
|
||||
<li><strong>Progressive Execution</strong>: Exact search followed by optional fuzzy search</li>
|
||||
<li><strong>Result Scoring</strong>: Relevance calculation and ranking</li>
|
||||
<li><strong>Result Presentation</strong>: Formatting and highlighting</li>
|
||||
</ol>
|
||||
|
||||
<h2>Query Processing Details</h2>
|
||||
|
||||
<h3>Lexical Analysis (Lex)</h3>
|
||||
<p>The lexer breaks down search queries into components:</p>
|
||||
<pre><code>// Input: 'project #status=active note.dateCreated >= TODAY-7'
|
||||
// Output:
|
||||
{
|
||||
fulltextTokens: ['project'],
|
||||
expressionTokens: ['#status', '=', 'active', 'note', '.', 'dateCreated', '>=', 'TODAY-7']
|
||||
}</code></pre>
|
||||
|
||||
<h4>Token Types</h4>
|
||||
<ul>
|
||||
<li><strong>Fulltext Tokens</strong>: Regular search terms</li>
|
||||
<li><strong>Expression Tokens</strong>: Attributes, operators, and property references</li>
|
||||
<li><strong>Quoted Strings</strong>: Exact phrase matches</li>
|
||||
<li><strong>Escaped Characters</strong>: Literal special characters</li>
|
||||
</ul>
|
||||
|
||||
<h3>Expression Building (Parse)</h3>
|
||||
<p>Tokens are converted into executable expression trees:</p>
|
||||
<pre><code>// Expression tree for: #book AND #author=Tolkien
|
||||
AndExp([
|
||||
AttributeExistsExp('label', 'book'),
|
||||
LabelComparisonExp('label', 'author', equals('tolkien'))
|
||||
])</code></pre>
|
||||
|
||||
<h4>Expression Types</h4>
|
||||
<ul>
|
||||
<li><code>AndExp</code>, <code>OrExp</code>, <code>NotExp</code>: Boolean logic</li>
|
||||
<li><code>AttributeExistsExp</code>: Label/relation existence</li>
|
||||
<li><code>LabelComparisonExp</code>: Label value comparison</li>
|
||||
<li><code>RelationWhereExp</code>: Relation target queries</li>
|
||||
<li><code>PropertyComparisonExp</code>: Note property filtering</li>
|
||||
<li><code>NoteContentFulltextExp</code>: Content search</li>
|
||||
<li><code>OrderByAndLimitExp</code>: Result ordering and limiting</li>
|
||||
</ul>
|
||||
|
||||
<h3>Progressive Search Strategy</h3>
|
||||
|
||||
<h4>Phase 1: Exact Search</h4>
|
||||
<pre><code>// Fast exact matching
|
||||
const exactResults = performSearch(expression, searchContext, false);</code></pre>
|
||||
<p>Characteristics:</p>
|
||||
<ul>
|
||||
<li>Substring matching for text</li>
|
||||
<li>Exact attribute matching</li>
|
||||
<li>Property-based filtering</li>
|
||||
<li>Handles 90%+ of searches</li>
|
||||
<li>Sub-second response time</li>
|
||||
</ul>
|
||||
|
||||
<h4>Phase 2: Fuzzy Fallback</h4>
|
||||
<pre><code>// Activated when exact results < 5 high-quality matches
|
||||
if (highQualityResults.length < 5) {
|
||||
const fuzzyResults = performSearch(expression, searchContext, true);
|
||||
return mergeExactAndFuzzyResults(exactResults, fuzzyResults);
|
||||
}</code></pre>
|
||||
<p>Characteristics:</p>
|
||||
<ul>
|
||||
<li>Edit distance calculations</li>
|
||||
<li>Phrase proximity matching</li>
|
||||
<li>Typo tolerance</li>
|
||||
<li>Performance safeguards</li>
|
||||
<li>Exact matches always rank first</li>
|
||||
</ul>
|
||||
|
||||
<h2>Performance Characteristics</h2>
|
||||
|
||||
<h3>Search Limits and Thresholds</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Value</th>
|
||||
<th>Purpose</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>MAX_SEARCH_CONTENT_SIZE</code></td>
|
||||
<td>2MB</td>
|
||||
<td>Database-level content filtering</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>MIN_FUZZY_TOKEN_LENGTH</code></td>
|
||||
<td>3 chars</td>
|
||||
<td>Minimum length for fuzzy matching</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>MAX_EDIT_DISTANCE</code></td>
|
||||
<td>2 chars</td>
|
||||
<td>Maximum character changes for fuzzy</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>MAX_PHRASE_PROXIMITY</code></td>
|
||||
<td>10 words</td>
|
||||
<td>Maximum distance for phrase matching</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>RESULT_SUFFICIENCY_THRESHOLD</code></td>
|
||||
<td>5 results</td>
|
||||
<td>Threshold for fuzzy activation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ABSOLUTE_MAX_CONTENT_SIZE</code></td>
|
||||
<td>100MB</td>
|
||||
<td>Hard limit to prevent system crash</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ABSOLUTE_MAX_WORD_COUNT</code></td>
|
||||
<td>2M words</td>
|
||||
<td>Hard limit for word processing</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Performance Optimization</h3>
|
||||
|
||||
<h4>Database-Level Optimizations</h4>
|
||||
<pre><code>-- Content size filtering at database level
|
||||
SELECT noteId, type, mime, content, isProtected
|
||||
FROM notes JOIN blobs USING (blobId)
|
||||
WHERE type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
|
||||
AND isDeleted = 0
|
||||
AND LENGTH(content) < 2097152 -- 2MB limit</code></pre>
|
||||
|
||||
<h4>Memory Management</h4>
|
||||
<ul>
|
||||
<li>Single-array edit distance calculation</li>
|
||||
<li>Early termination for distant matches</li>
|
||||
<li>Progressive content processing</li>
|
||||
<li>Cached regular expressions</li>
|
||||
</ul>
|
||||
|
||||
<h4>Search Context Optimization</h4>
|
||||
<pre><code>// Efficient search context configuration
|
||||
const searchContext = new SearchContext({
|
||||
fastSearch: true, // Skip content search
|
||||
limit: 50, // Reasonable result limit
|
||||
orderBy: 'dateCreated', // Use indexed property
|
||||
includeArchivedNotes: false // Reduce search space
|
||||
});</code></pre>
|
||||
|
||||
<h2>Fuzzy Search Implementation</h2>
|
||||
|
||||
<h3>Edit Distance Algorithm</h3>
|
||||
<p>Trilium uses an optimized Levenshtein distance calculation:</p>
|
||||
<pre><code>// Optimized single-array implementation
|
||||
function calculateOptimizedEditDistance(str1, str2, maxDistance) {
|
||||
// Early termination checks
|
||||
if (Math.abs(str1.length - str2.length) > maxDistance) {
|
||||
return maxDistance + 1;
|
||||
}
|
||||
|
||||
// Single array optimization
|
||||
let previousRow = Array.from({ length: str2.length + 1 }, (_, i) => i);
|
||||
let currentRow = new Array(str2.length + 1);
|
||||
|
||||
for (let i = 1; i <= str1.length; i++) {
|
||||
currentRow[0] = i;
|
||||
let minInRow = i;
|
||||
|
||||
for (let j = 1; j <= str2.length; j++) {
|
||||
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
||||
currentRow[j] = Math.min(
|
||||
previousRow[j] + 1, // deletion
|
||||
currentRow[j - 1] + 1, // insertion
|
||||
previousRow[j - 1] + cost // substitution
|
||||
);
|
||||
minInRow = Math.min(minInRow, currentRow[j]);
|
||||
}
|
||||
|
||||
// Early termination if row minimum exceeds threshold
|
||||
if (minInRow > maxDistance) return maxDistance + 1;
|
||||
|
||||
[previousRow, currentRow] = [currentRow, previousRow];
|
||||
}
|
||||
|
||||
return previousRow[str2.length];
|
||||
}</code></pre>
|
||||
|
||||
<h3>Phrase Proximity Matching</h3>
|
||||
<p>For multi-token fuzzy searches:</p>
|
||||
<pre><code>// Check if tokens appear within reasonable proximity
|
||||
function hasProximityMatch(tokenPositions, maxDistance = 10) {
|
||||
// For 2 tokens, simple distance check
|
||||
if (tokenPositions.length === 2) {
|
||||
const [pos1, pos2] = tokenPositions;
|
||||
return pos1.some(p1 => pos2.some(p2 => Math.abs(p1 - p2) <= maxDistance));
|
||||
}
|
||||
|
||||
// For multiple tokens, find sequence within range
|
||||
const findSequence = (remaining, currentPos) => {
|
||||
if (remaining.length === 0) return true;
|
||||
const [nextPositions, ...rest] = remaining;
|
||||
return nextPositions.some(pos =>
|
||||
Math.abs(pos - currentPos) <= maxDistance &&
|
||||
findSequence(rest, pos)
|
||||
);
|
||||
};
|
||||
|
||||
const [firstPositions, ...rest] = tokenPositions;
|
||||
return firstPositions.some(startPos => findSequence(rest, startPos));
|
||||
}</code></pre>
|
||||
|
||||
<h2>Indexing and Storage</h2>
|
||||
|
||||
<h3>Database Schema Optimization</h3>
|
||||
<pre><code>-- Relevant indexes for search performance
|
||||
CREATE INDEX idx_notes_type ON notes(type);
|
||||
CREATE INDEX idx_notes_isDeleted ON notes(isDeleted);
|
||||
CREATE INDEX idx_notes_dateCreated ON notes(dateCreated);
|
||||
CREATE INDEX idx_notes_dateModified ON notes(dateModified);
|
||||
CREATE INDEX idx_attributes_name ON attributes(name);
|
||||
CREATE INDEX idx_attributes_type ON attributes(type);
|
||||
CREATE INDEX idx_attributes_value ON attributes(value);</code></pre>
|
||||
|
||||
<h3>Content Processing</h3>
|
||||
<p>Notes are processed differently based on type:</p>
|
||||
<pre><code>// Content preprocessing by note type
|
||||
function preprocessContent(content, type, mime) {
|
||||
content = normalize(content.toString());
|
||||
|
||||
if (type === "text" && mime === "text/html") {
|
||||
content = stripTags(content);
|
||||
content = content.replace(/ /g, " ");
|
||||
} else if (type === "mindMap" && mime === "application/json") {
|
||||
content = processMindmapContent(content);
|
||||
} else if (type === "canvas" && mime === "application/json") {
|
||||
const canvasData = JSON.parse(content);
|
||||
const textElements = canvasData.elements
|
||||
.filter(el => el.type === "text" && el.text)
|
||||
.map(el => el.text);
|
||||
content = normalize(textElements.join(" "));
|
||||
}
|
||||
|
||||
return content.trim();
|
||||
}</code></pre>
|
||||
|
||||
<h2>Search Result Processing</h2>
|
||||
|
||||
<h3>Scoring Algorithm</h3>
|
||||
<p>Results are scored based on multiple factors:</p>
|
||||
<pre><code>function computeScore(fulltextQuery, highlightedTokens, enableFuzzyMatching) {
|
||||
let score = 0;
|
||||
|
||||
// Title matches get higher score
|
||||
if (this.noteTitle.toLowerCase().includes(fulltextQuery.toLowerCase())) {
|
||||
score += 10;
|
||||
}
|
||||
|
||||
// Path matches (hierarchical context)
|
||||
const pathMatch = this.notePathArray.some(pathNote =>
|
||||
pathNote.title.toLowerCase().includes(fulltextQuery.toLowerCase())
|
||||
);
|
||||
if (pathMatch) score += 5;
|
||||
|
||||
// Attribute matches
|
||||
score += this.attributeMatches * 3;
|
||||
|
||||
// Content snippet quality
|
||||
if (this.contentSnippet && this.contentSnippet.length > 0) {
|
||||
score += 2;
|
||||
}
|
||||
|
||||
// Fuzzy match penalty
|
||||
if (enableFuzzyMatching && this.isFuzzyMatch) {
|
||||
score *= 0.8; // 20% penalty for fuzzy matches
|
||||
}
|
||||
|
||||
return score;
|
||||
}</code></pre>
|
||||
|
||||
<h3>Result Merging</h3>
|
||||
<p>Exact and fuzzy results are carefully merged:</p>
|
||||
<pre><code>function mergeExactAndFuzzyResults(exactResults, fuzzyResults) {
|
||||
// Deduplicate - exact results take precedence
|
||||
const exactNoteIds = new Set(exactResults.map(r => r.noteId));
|
||||
const additionalFuzzyResults = fuzzyResults.filter(r =>
|
||||
!exactNoteIds.has(r.noteId)
|
||||
);
|
||||
|
||||
// Sort within each category
|
||||
exactResults.sort(byScoreAndDepth);
|
||||
additionalFuzzyResults.sort(byScoreAndDepth);
|
||||
|
||||
// CRITICAL: Exact matches always come first
|
||||
return [...exactResults, ...additionalFuzzyResults];
|
||||
}</code></pre>
|
||||
|
||||
<h2>Performance Monitoring</h2>
|
||||
|
||||
<h3>Search Metrics</h3>
|
||||
<p>Monitor these performance indicators:</p>
|
||||
<pre><code>// Performance tracking
|
||||
const searchMetrics = {
|
||||
totalQueries: 0,
|
||||
exactSearchTime: 0,
|
||||
fuzzySearchTime: 0,
|
||||
resultCount: 0,
|
||||
cacheHitRate: 0,
|
||||
slowQueries: [] // queries taking > 1 second
|
||||
};</code></pre>
|
||||
|
||||
<h3>Memory Usage</h3>
|
||||
<p>Track memory consumption:</p>
|
||||
<pre><code>// Memory monitoring
|
||||
const memoryMetrics = {
|
||||
searchCacheSize: 0,
|
||||
activeSearchContexts: 0,
|
||||
largeContentNotes: 0, // notes > 1MB
|
||||
indexSize: 0
|
||||
};</code></pre>
|
||||
|
||||
<h3>Query Complexity Analysis</h3>
|
||||
<p>Identify expensive queries:</p>
|
||||
<pre><code>// Query complexity factors
|
||||
const complexityFactors = {
|
||||
tokenCount: query.split(' ').length,
|
||||
hasRegex: query.includes('%='),
|
||||
hasFuzzy: query.includes('~=') || query.includes('~*'),
|
||||
hasRelationTraversal: query.includes('.relations.'),
|
||||
hasNestedProperties: (query.match(/\./g) || []).length > 2,
|
||||
hasOrderBy: query.includes('orderBy'),
|
||||
estimatedResultSize: 'unknown'
|
||||
};</code></pre>
|
||||
|
||||
<h2>Troubleshooting Performance Issues</h2>
|
||||
|
||||
<h3>Common Performance Problems</h3>
|
||||
|
||||
<h4>Slow Full-Text Search</h4>
|
||||
<p><strong>Diagnosis:</strong></p>
|
||||
<ul>
|
||||
<li>Check note content sizes</li>
|
||||
<li>Verify content type filtering</li>
|
||||
<li>Monitor regex usage</li>
|
||||
<li>Review fuzzy search activation</li>
|
||||
</ul>
|
||||
<p><strong>Solutions:</strong></p>
|
||||
<ul>
|
||||
<li>Enable fast search for attribute-only queries</li>
|
||||
<li>Add content size limits</li>
|
||||
<li>Optimize regex patterns</li>
|
||||
<li>Tune fuzzy search thresholds</li>
|
||||
</ul>
|
||||
|
||||
<h4>Memory Issues</h4>
|
||||
<p><strong>Diagnosis:</strong></p>
|
||||
<ul>
|
||||
<li>Monitor result set sizes</li>
|
||||
<li>Check for large content processing</li>
|
||||
<li>Review search context caching</li>
|
||||
<li>Identify memory leaks</li>
|
||||
</ul>
|
||||
<p><strong>Solutions:</strong></p>
|
||||
<ul>
|
||||
<li>Add result limits</li>
|
||||
<li>Implement progressive loading</li>
|
||||
<li>Clear unused search contexts</li>
|
||||
<li>Optimize content preprocessing</li>
|
||||
</ul>
|
||||
|
||||
<h4>High CPU Usage</h4>
|
||||
<p><strong>Diagnosis:</strong></p>
|
||||
<ul>
|
||||
<li>Profile fuzzy search operations</li>
|
||||
<li>Check edit distance calculations</li>
|
||||
<li>Monitor regex compilation</li>
|
||||
<li>Review phrase proximity matching</li>
|
||||
</ul>
|
||||
<p><strong>Solutions:</strong></p>
|
||||
<ul>
|
||||
<li>Increase minimum fuzzy token length</li>
|
||||
<li>Reduce maximum edit distance</li>
|
||||
<li>Cache compiled regexes</li>
|
||||
<li>Limit phrase proximity distance</li>
|
||||
</ul>
|
||||
|
||||
<h3>Debugging Tools</h3>
|
||||
|
||||
<h4>Debug Mode</h4>
|
||||
<p>Enable search debugging:</p>
|
||||
<pre><code>// Search context with debugging
|
||||
const searchContext = new SearchContext({
|
||||
debug: true // Logs expression parsing and execution
|
||||
});</code></pre>
|
||||
<p>Output includes:</p>
|
||||
<ul>
|
||||
<li>Token parsing results</li>
|
||||
<li>Expression tree structure</li>
|
||||
<li>Execution timing</li>
|
||||
<li>Result scoring details</li>
|
||||
</ul>
|
||||
|
||||
<h4>Performance Profiling</h4>
|
||||
<pre><code>// Manual performance measurement
|
||||
const startTime = Date.now();
|
||||
const results = searchService.findResultsWithQuery(query, searchContext);
|
||||
const endTime = Date.now();
|
||||
console.log(`Search took ${endTime - startTime}ms for ${results.length} results`);</code></pre>
|
||||
|
||||
<h4>Query Analysis</h4>
|
||||
<pre><code>// Analyze query complexity
|
||||
function analyzeQuery(query) {
|
||||
return {
|
||||
tokenCount: query.split(/\s+/).length,
|
||||
hasAttributes: /#|\~/.test(query),
|
||||
hasProperties: /note\./.test(query),
|
||||
hasRegex: /%=/.test(query),
|
||||
hasFuzzy: /~[=*]/.test(query),
|
||||
complexity: calculateComplexityScore(query)
|
||||
};
|
||||
}</code></pre>
|
||||
|
||||
<h2>Configuration and Tuning</h2>
|
||||
|
||||
<h3>Server Configuration</h3>
|
||||
<p>Relevant settings in <code>config.ini</code>:</p>
|
||||
<pre><code>[Search]
|
||||
maxContentSize=2097152 # 2MB content limit
|
||||
minFuzzyTokenLength=3 # Minimum chars for fuzzy
|
||||
maxEditDistance=2 # Edit distance limit
|
||||
resultSufficiencyThreshold=5 # Fuzzy activation threshold
|
||||
enableProgressiveSearch=true # Enable progressive strategy
|
||||
cacheSearchResults=true # Cache frequent searches
|
||||
|
||||
[Performance]
|
||||
searchTimeoutMs=30000 # 30 second search timeout
|
||||
maxSearchResults=1000 # Hard limit on results
|
||||
enableSearchProfiling=false # Performance logging</code></pre>
|
||||
|
||||
<h3>Runtime Tuning</h3>
|
||||
<p>Adjust search behavior programmatically:</p>
|
||||
<pre><code>// Dynamic configuration
|
||||
const searchConfig = {
|
||||
maxContentSize: 1024 * 1024, // 1MB for faster processing
|
||||
enableFuzzySearch: false, // Exact only for speed
|
||||
resultLimit: 50, // Smaller result sets
|
||||
useIndexedPropertiesOnly: true // Skip expensive calculations
|
||||
};</code></pre>
|
||||
|
||||
<h2>Best Practices for Performance</h2>
|
||||
|
||||
<h3>Query Design</h3>
|
||||
<ol>
|
||||
<li><strong>Start Specific</strong>: Use selective criteria first</li>
|
||||
<li><strong>Limit Results</strong>: Always set reasonable limits</li>
|
||||
<li><strong>Use Indexes</strong>: Prefer indexed properties for ordering</li>
|
||||
<li><strong>Avoid Regex</strong>: Use simple operators when possible</li>
|
||||
<li><strong>Cache Common Queries</strong>: Save frequently used searches</li>
|
||||
</ol>
|
||||
|
||||
<h3>System Administration</h3>
|
||||
<ol>
|
||||
<li><strong>Monitor Performance</strong>: Track slow queries and memory usage</li>
|
||||
<li><strong>Regular Maintenance</strong>: Clean up unused notes and attributes</li>
|
||||
<li><strong>Index Optimization</strong>: Ensure database indexes are current</li>
|
||||
<li><strong>Content Management</strong>: Archive or compress large content</li>
|
||||
</ol>
|
||||
|
||||
<h3>Development Guidelines</h3>
|
||||
<ol>
|
||||
<li><strong>Test Performance</strong>: Benchmark complex queries</li>
|
||||
<li><strong>Profile Regularly</strong>: Identify performance regressions</li>
|
||||
<li><strong>Optimize Incrementally</strong>: Make small, measured improvements</li>
|
||||
<li><strong>Document Complexity</strong>: Note expensive operations</li>
|
||||
</ol>
|
||||
|
||||
<h2>Next Steps</h2>
|
||||
<ul>
|
||||
<li><a class="reference-link" href="#root/_help_search_fundamentals">Search Fundamentals</a> - Basic concepts and syntax</li>
|
||||
<li><a class="reference-link" href="#root/_help_search_advanced">Advanced Search Expressions</a> - Complex query construction</li>
|
||||
<li><a class="reference-link" href="#root/_help_search_examples">Search Examples and Use Cases</a> - Practical applications</li>
|
||||
<li><a class="reference-link" href="#root/_help_search_saved">Saved Searches</a> - Creating dynamic collections</li>
|
||||
</ul>
|
||||
600
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Advanced-Protection-Setup.html
generated
vendored
Normal file
600
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Advanced-Protection-Setup.html
generated
vendored
Normal file
@@ -0,0 +1,600 @@
|
||||
<div class="note-content">
|
||||
|
||||
<h1>Advanced Protection Setup Guide</h1>
|
||||
|
||||
<p>This guide provides step-by-step instructions for implementing advanced security features in Trilium, including enterprise-level protection measures and compliance configurations.</p>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>Target Audience:</strong> System administrators, security professionals, and advanced users implementing Trilium in production environments.
|
||||
</div>
|
||||
|
||||
<h2>Table of Contents</h2>
|
||||
|
||||
<ol>
|
||||
<li><a href="#mfa-setup">Multi-Factor Authentication Setup</a></li>
|
||||
<li><a href="#enterprise-auth">Enterprise Authentication</a></li>
|
||||
<li><a href="#advanced-encryption">Advanced Encryption Configuration</a></li>
|
||||
<li><a href="#security-monitoring">Security Monitoring Setup</a></li>
|
||||
<li><a href="#compliance-config">Compliance Configuration</a></li>
|
||||
<li><a href="#backup-security">Secure Backup Implementation</a></li>
|
||||
</ol>
|
||||
|
||||
<h2 id="mfa-setup">Multi-Factor Authentication Setup</h2>
|
||||
|
||||
<h3>Prerequisites</h3>
|
||||
<ul>
|
||||
<li>Trilium instance with password authentication configured</li>
|
||||
<li>Authenticator app (Google Authenticator, Authy, etc.)</li>
|
||||
<li>Secure storage for recovery codes</li>
|
||||
</ul>
|
||||
|
||||
<h3>Step-by-Step MFA Configuration</h3>
|
||||
|
||||
<h4>Step 1: Enable MFA</h4>
|
||||
<ol>
|
||||
<li>Navigate to <strong>Options → Security → Multi-Factor Authentication</strong></li>
|
||||
<li>Click <strong>"Enable Multi-Factor Authentication"</strong></li>
|
||||
<li>Confirm your current password when prompted</li>
|
||||
</ol>
|
||||
|
||||
<h4>Step 2: Generate TOTP Secret</h4>
|
||||
<ol>
|
||||
<li>Click <strong>"Generate New Secret"</strong></li>
|
||||
<li>A QR code will be displayed along with the secret key</li>
|
||||
<li>Copy the secret key to a secure location (backup)</li>
|
||||
</ol>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<strong>Security Note:</strong> The secret key is your backup method for setting up the authenticator on new devices. Store it securely.
|
||||
</div>
|
||||
|
||||
<h4>Step 3: Configure Authenticator App</h4>
|
||||
|
||||
<p><strong>Option A: QR Code (Recommended)</strong></p>
|
||||
<ol>
|
||||
<li>Open your authenticator app</li>
|
||||
<li>Tap "Add Account" or "+"</li>
|
||||
<li>Select "Scan QR Code"</li>
|
||||
<li>Point camera at the QR code displayed in Trilium</li>
|
||||
<li>Verify the account is added with name "Trilium Notes"</li>
|
||||
</ol>
|
||||
|
||||
<p><strong>Option B: Manual Entry</strong></p>
|
||||
<ol>
|
||||
<li>Open your authenticator app</li>
|
||||
<li>Tap "Add Account" or "+"</li>
|
||||
<li>Select "Enter Key Manually"</li>
|
||||
<li>Enter the following details:
|
||||
<ul>
|
||||
<li><strong>Account:</strong> Your email or username</li>
|
||||
<li><strong>Key:</strong> The secret key from Trilium</li>
|
||||
<li><strong>Type:</strong> Time-based</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h4>Step 4: Verify TOTP Setup</h4>
|
||||
<ol>
|
||||
<li>Wait for the authenticator to generate a 6-digit code</li>
|
||||
<li>Enter the code in the "Verification Code" field</li>
|
||||
<li>Click <strong>"Verify and Enable MFA"</strong></li>
|
||||
<li>If successful, you'll see a confirmation message</li>
|
||||
</ol>
|
||||
|
||||
<h4>Step 5: Save Recovery Codes</h4>
|
||||
<ol>
|
||||
<li>Click <strong>"Generate Recovery Codes"</strong></li>
|
||||
<li>Save the 10 recovery codes in a secure location:
|
||||
<ul>
|
||||
<li>Password manager</li>
|
||||
<li>Encrypted file</li>
|
||||
<li>Physical secure storage</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Click <strong>"I have saved my recovery codes"</strong></li>
|
||||
</ol>
|
||||
|
||||
<div class="alert alert-danger">
|
||||
<strong>Critical:</strong> Recovery codes are your only way to access Trilium if you lose access to your authenticator. Store them securely and treat them like passwords.
|
||||
</div>
|
||||
|
||||
<h3>MFA Login Process</h3>
|
||||
|
||||
<p>After enabling MFA, the login process becomes:</p>
|
||||
<ol>
|
||||
<li>Enter your username and password</li>
|
||||
<li>Click "Login"</li>
|
||||
<li>Enter the 6-digit TOTP code from your authenticator</li>
|
||||
<li>Alternatively, use a recovery code if TOTP is unavailable</li>
|
||||
<li>Click "Verify" to complete login</li>
|
||||
</ol>
|
||||
|
||||
<h3>Troubleshooting MFA</h3>
|
||||
|
||||
<h4>Common Issues</h4>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>Issue:</strong> TOTP codes are rejected<br>
|
||||
<strong>Solutions:</strong>
|
||||
<ul>
|
||||
<li>Check that your device time is synchronized (most common cause)</li>
|
||||
<li>Ensure you're entering the current 6-digit code</li>
|
||||
<li>Try the next generated code if the current one expires</li>
|
||||
<li>Verify the secret was entered correctly in your authenticator</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<strong>Issue:</strong> Authenticator app lost or unavailable<br>
|
||||
<strong>Solutions:</strong>
|
||||
<ul>
|
||||
<li>Use one of your saved recovery codes</li>
|
||||
<li>Each recovery code can only be used once</li>
|
||||
<li>Generate new recovery codes after using several</li>
|
||||
<li>Re-setup MFA if all recovery codes are used</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 id="enterprise-auth">Enterprise Authentication</h2>
|
||||
|
||||
<h3>Single Sign-On (SSO) Configuration</h3>
|
||||
|
||||
<h4>OpenID Connect Setup</h4>
|
||||
|
||||
<p>Configure Trilium to integrate with enterprise identity providers:</p>
|
||||
|
||||
<h5>Configuration File Setup</h5>
|
||||
<pre><code># config.ini
|
||||
[OpenID]
|
||||
enabled=true
|
||||
issuer=https://your-provider.com
|
||||
client_id=your-client-id
|
||||
client_secret=your-client-secret
|
||||
redirect_uri=https://your-trilium.com/auth/callback
|
||||
scope=openid email profile</code></pre>
|
||||
|
||||
<h5>Common Provider Configurations</h5>
|
||||
|
||||
<p><strong>Google Workspace:</strong></p>
|
||||
<pre><code>[OpenID]
|
||||
enabled=true
|
||||
issuer=https://accounts.google.com
|
||||
client_id=your-google-client-id.apps.googleusercontent.com
|
||||
client_secret=your-google-client-secret
|
||||
redirect_uri=https://your-trilium.com/auth/callback
|
||||
scope=openid email profile</code></pre>
|
||||
|
||||
<p><strong>Microsoft Azure AD:</strong></p>
|
||||
<pre><code>[OpenID]
|
||||
enabled=true
|
||||
issuer=https://login.microsoftonline.com/your-tenant-id/v2.0
|
||||
client_id=your-azure-client-id
|
||||
client_secret=your-azure-client-secret
|
||||
redirect_uri=https://your-trilium.com/auth/callback
|
||||
scope=openid email profile</code></pre>
|
||||
|
||||
<h4>Environment Variable Configuration</h4>
|
||||
|
||||
<p>For containerized deployments:</p>
|
||||
<pre><code># Docker environment variables
|
||||
TRILIUM_OPENID_ENABLED=true
|
||||
TRILIUM_OPENID_ISSUER=https://your-provider.com
|
||||
TRILIUM_OPENID_CLIENT_ID=your-client-id
|
||||
TRILIUM_OPENID_CLIENT_SECRET=your-client-secret
|
||||
TRILIUM_OPENID_REDIRECT_URI=https://your-trilium.com/auth/callback</code></pre>
|
||||
|
||||
<h2 id="advanced-encryption">Advanced Encryption Configuration</h2>
|
||||
|
||||
<h3>Custom Encryption Parameters</h3>
|
||||
|
||||
<div class="alert alert-danger">
|
||||
<strong>Warning:</strong> Modifying encryption parameters requires expert knowledge and may break compatibility with future versions. Only proceed if you understand the implications.
|
||||
</div>
|
||||
|
||||
<h4>Scrypt Parameter Tuning</h4>
|
||||
|
||||
<p>For high-security environments, you can increase scrypt parameters:</p>
|
||||
|
||||
<pre><code>// Location: apps/server/src/services/encryption/my_scrypt.ts
|
||||
const customScryptParams = {
|
||||
N: 32768, // Higher CPU/memory cost (default: 16384)
|
||||
r: 8, // Block size (default: 8)
|
||||
p: 2 // Parallelization (default: 1)
|
||||
};
|
||||
</code></pre>
|
||||
|
||||
<p><strong>Impact Assessment:</strong></p>
|
||||
<ul>
|
||||
<li><strong>Security:</strong> Higher parameters increase resistance to brute force</li>
|
||||
<li><strong>Performance:</strong> Significantly slower password verification</li>
|
||||
<li><strong>Compatibility:</strong> May break with future updates</li>
|
||||
<li><strong>Hardware:</strong> Requires more RAM and CPU</li>
|
||||
</ul>
|
||||
|
||||
<h3>Database-Level Encryption</h3>
|
||||
|
||||
<h4>SQLite Encryption Extension</h4>
|
||||
|
||||
<p>For additional database encryption (enterprise feature):</p>
|
||||
|
||||
<ol>
|
||||
<li>Install SQLCipher extension</li>
|
||||
<li>Configure database encryption key</li>
|
||||
<li>Modify connection string to use encryption</li>
|
||||
</ol>
|
||||
|
||||
<pre><code># Database connection with encryption
|
||||
PRAGMA key = 'your-database-encryption-key';
|
||||
PRAGMA cipher_compatibility = 4;</code></pre>
|
||||
|
||||
<h2 id="security-monitoring">Security Monitoring Setup</h2>
|
||||
|
||||
<h3>Log Configuration</h3>
|
||||
|
||||
<h4>Comprehensive Logging Setup</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Create Log Directory</strong>
|
||||
<pre><code>sudo mkdir -p /var/log/trilium
|
||||
sudo chown trilium:adm /var/log/trilium
|
||||
sudo chmod 750 /var/log/trilium</code></pre>
|
||||
</li>
|
||||
|
||||
<li><strong>Configure Log Rotation</strong>
|
||||
<pre><code># /etc/logrotate.d/trilium
|
||||
/var/log/trilium/*.log {
|
||||
daily
|
||||
missingok
|
||||
rotate 90
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 640 trilium adm
|
||||
sharedscripts
|
||||
postrotate
|
||||
systemctl reload trilium
|
||||
endscript
|
||||
}</code></pre>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h3>Security Event Monitoring</h3>
|
||||
|
||||
<h4>Real-time Monitoring Script</h4>
|
||||
|
||||
<p>Create automated monitoring for security events:</p>
|
||||
|
||||
<pre><code>#!/bin/bash
|
||||
# /usr/local/bin/trilium-security-monitor.sh
|
||||
|
||||
LOG_FILE="/var/log/trilium/security.log"
|
||||
ALERT_EMAIL="admin@yourdomain.com"
|
||||
|
||||
# Monitor failed login attempts
|
||||
check_failed_logins() {
|
||||
local count=$(grep -c "Failed login" "$LOG_FILE" 2>/dev/null || echo 0)
|
||||
if [ "$count" -gt 5 ]; then
|
||||
echo "ALERT: $count failed login attempts" | \
|
||||
mail -s "Trilium Security Alert" "$ALERT_EMAIL"
|
||||
fi
|
||||
}
|
||||
|
||||
# Monitor CSRF violations
|
||||
check_csrf_violations() {
|
||||
local count=$(grep -c "CSRF violation" "$LOG_FILE" 2>/dev/null || echo 0)
|
||||
if [ "$count" -gt 0 ]; then
|
||||
echo "ALERT: $count CSRF violations detected" | \
|
||||
mail -s "Trilium Security Alert" "$ALERT_EMAIL"
|
||||
fi
|
||||
}
|
||||
|
||||
# Run checks
|
||||
check_failed_logins
|
||||
check_csrf_violations</code></pre>
|
||||
|
||||
<h4>Automated Monitoring with Cron</h4>
|
||||
|
||||
<pre><code># Add to crontab
|
||||
*/15 * * * * /usr/local/bin/trilium-security-monitor.sh</code></pre>
|
||||
|
||||
<h2 id="compliance-config">Compliance Configuration</h2>
|
||||
|
||||
<h3>GDPR Compliance</h3>
|
||||
|
||||
<h4>Data Protection Settings</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Enable Enhanced Logging</strong>
|
||||
<pre><code># config.ini
|
||||
[Security]
|
||||
auditLogging=true
|
||||
dataAccessLogging=true
|
||||
retentionPeriod=2557</code></pre>
|
||||
</li>
|
||||
|
||||
<li><strong>Configure Data Export</strong>
|
||||
<ul>
|
||||
<li>Regular automated exports for data portability</li>
|
||||
<li>User-accessible export functionality</li>
|
||||
<li>Structured data format (JSON/XML)</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li><strong>Right to Erasure Implementation</strong>
|
||||
<ul>
|
||||
<li>Secure deletion procedures</li>
|
||||
<li>Encryption key destruction</li>
|
||||
<li>Backup purging processes</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h3>HIPAA Compliance</h3>
|
||||
|
||||
<h4>Healthcare Data Protection</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Access Controls</strong>
|
||||
<ul>
|
||||
<li>Strong authentication (password + MFA)</li>
|
||||
<li>Session timeout configuration (max 10 minutes)</li>
|
||||
<li>Automatic logout on inactivity</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li><strong>Audit Requirements</strong>
|
||||
<pre><code># Enhanced audit logging
|
||||
[HIPAA]
|
||||
auditAllAccess=true
|
||||
logUserActions=true
|
||||
retainLogs=2555 # 7 years in days
|
||||
encryptAuditLogs=true</code></pre>
|
||||
</li>
|
||||
|
||||
<li><strong>Encryption Standards</strong>
|
||||
<ul>
|
||||
<li>AES-128 minimum (meets HIPAA requirements)</li>
|
||||
<li>Key management procedures</li>
|
||||
<li>Encrypted backups and transmission</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="backup-security">Secure Backup Implementation</h2>
|
||||
|
||||
<h3>Automated Encrypted Backups</h3>
|
||||
|
||||
<h4>Backup Script Configuration</h4>
|
||||
|
||||
<pre><code>#!/bin/bash
|
||||
# /usr/local/bin/trilium-secure-backup.sh
|
||||
|
||||
BACKUP_DIR="/opt/trilium/backups"
|
||||
GPG_RECIPIENT="trilium-backup@yourdomain.com"
|
||||
RETENTION_DAYS=90
|
||||
|
||||
# Create encrypted backup
|
||||
create_backup() {
|
||||
local timestamp=$(date +%Y%m%d_%H%M%S)
|
||||
local backup_name="trilium_backup_$timestamp"
|
||||
|
||||
# Stop Trilium for consistent backup
|
||||
systemctl stop trilium
|
||||
|
||||
# Create backup
|
||||
tar czf - -C /opt/trilium/data . | \
|
||||
gpg --trust-model always --encrypt -r "$GPG_RECIPIENT" \
|
||||
> "$BACKUP_DIR/${backup_name}.tar.gz.gpg"
|
||||
|
||||
# Start Trilium
|
||||
systemctl start trilium
|
||||
|
||||
# Verify backup
|
||||
if gpg --decrypt "$BACKUP_DIR/${backup_name}.tar.gz.gpg" | tar tz > /dev/null; then
|
||||
echo "Backup verification successful: $backup_name"
|
||||
else
|
||||
echo "Backup verification failed!" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set permissions
|
||||
chown trilium:trilium "$BACKUP_DIR/${backup_name}.tar.gz.gpg"
|
||||
chmod 600 "$BACKUP_DIR/${backup_name}.tar.gz.gpg"
|
||||
}
|
||||
|
||||
# Cleanup old backups
|
||||
cleanup_backups() {
|
||||
find "$BACKUP_DIR" -name "*.tar.gz.gpg" -mtime +$RETENTION_DAYS -delete
|
||||
}
|
||||
|
||||
# Execute backup
|
||||
create_backup
|
||||
cleanup_backups</code></pre>
|
||||
|
||||
<h4>Automated Backup Schedule</h4>
|
||||
|
||||
<pre><code># Add to crontab for daily backups at 2 AM
|
||||
0 2 * * * /usr/local/bin/trilium-secure-backup.sh</code></pre>
|
||||
|
||||
<h3>Backup Verification</h3>
|
||||
|
||||
<h4>Regular Backup Testing</h4>
|
||||
|
||||
<pre><code>#!/bin/bash
|
||||
# /usr/local/bin/trilium-backup-test.sh
|
||||
|
||||
BACKUP_DIR="/opt/trilium/backups"
|
||||
TEST_DIR="/tmp/trilium-backup-test"
|
||||
|
||||
# Test latest backup
|
||||
test_latest_backup() {
|
||||
local latest_backup=$(ls -t "$BACKUP_DIR"/*.tar.gz.gpg | head -1)
|
||||
|
||||
if [ -z "$latest_backup" ]; then
|
||||
echo "No backups found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing backup: $latest_backup"
|
||||
|
||||
# Create test directory
|
||||
mkdir -p "$TEST_DIR"
|
||||
|
||||
# Decrypt and extract
|
||||
if gpg --decrypt "$latest_backup" | tar xzf - -C "$TEST_DIR"; then
|
||||
echo "Backup extraction successful"
|
||||
|
||||
# Test database integrity
|
||||
if sqlite3 "$TEST_DIR/document.db" "PRAGMA integrity_check;" | grep -q "ok"; then
|
||||
echo "Database integrity check passed"
|
||||
else
|
||||
echo "Database integrity check failed!"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Backup extraction failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -rf "$TEST_DIR"
|
||||
|
||||
echo "Backup test completed successfully"
|
||||
}
|
||||
|
||||
test_latest_backup</code></pre>
|
||||
|
||||
<h3>Off-site Backup Synchronization</h3>
|
||||
|
||||
<h4>Secure Remote Sync</h4>
|
||||
|
||||
<pre><code>#!/bin/bash
|
||||
# /usr/local/bin/trilium-remote-sync.sh
|
||||
|
||||
REMOTE_HOST="backup.yourdomain.com"
|
||||
REMOTE_USER="trilium-backup"
|
||||
REMOTE_PATH="/backups/trilium"
|
||||
LOCAL_BACKUP_DIR="/opt/trilium/backups"
|
||||
|
||||
# Sync to remote location
|
||||
sync_to_remote() {
|
||||
rsync -avz --progress --delete \
|
||||
-e "ssh -i /home/trilium/.ssh/backup_key" \
|
||||
"$LOCAL_BACKUP_DIR/" \
|
||||
"$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Remote sync completed successfully"
|
||||
else
|
||||
echo "Remote sync failed!"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
sync_to_remote</code></pre>
|
||||
|
||||
<h2>Security Assessment Tools</h2>
|
||||
|
||||
<h3>Security Scanner</h3>
|
||||
|
||||
<h4>Automated Security Assessment</h4>
|
||||
|
||||
<pre><code>#!/bin/bash
|
||||
# /usr/local/bin/trilium-security-scan.sh
|
||||
|
||||
SCORE=0
|
||||
MAX_SCORE=0
|
||||
|
||||
print_check() {
|
||||
local status="$1"
|
||||
local message="$2"
|
||||
local points="$3"
|
||||
|
||||
case "$status" in
|
||||
"PASS")
|
||||
echo "✓ $message (+$points points)"
|
||||
SCORE=$((SCORE + points))
|
||||
;;
|
||||
"FAIL")
|
||||
echo "✗ $message"
|
||||
;;
|
||||
"WARN")
|
||||
echo "! $message"
|
||||
;;
|
||||
esac
|
||||
|
||||
MAX_SCORE=$((MAX_SCORE + points))
|
||||
}
|
||||
|
||||
# Check HTTPS configuration
|
||||
check_https() {
|
||||
if curl -s -I https://localhost:8080 2>/dev/null | grep -q "HTTP/"; then
|
||||
print_check "PASS" "HTTPS is configured" 20
|
||||
else
|
||||
print_check "FAIL" "HTTPS is not configured" 20
|
||||
fi
|
||||
}
|
||||
|
||||
# Check MFA status
|
||||
check_mfa() {
|
||||
local mfa_enabled=$(sqlite3 /opt/trilium/data/document.db \
|
||||
"SELECT value FROM options WHERE name = 'mfaEnabled';" 2>/dev/null)
|
||||
if [ "$mfa_enabled" = "true" ]; then
|
||||
print_check "PASS" "MFA is enabled" 20
|
||||
else
|
||||
print_check "WARN" "MFA is not enabled" 20
|
||||
fi
|
||||
}
|
||||
|
||||
# Check file permissions
|
||||
check_permissions() {
|
||||
local db_perms=$(stat -c "%a" /opt/trilium/data/document.db 2>/dev/null)
|
||||
if [ "$db_perms" = "600" ]; then
|
||||
print_check "PASS" "Database permissions are secure" 10
|
||||
else
|
||||
print_check "FAIL" "Database permissions are too permissive" 10
|
||||
fi
|
||||
}
|
||||
|
||||
# Run all checks
|
||||
echo "=== Trilium Security Assessment ==="
|
||||
check_https
|
||||
check_mfa
|
||||
check_permissions
|
||||
|
||||
echo "=== Summary ==="
|
||||
echo "Score: $SCORE / $MAX_SCORE ($(($SCORE * 100 / $MAX_SCORE))%)"
|
||||
|
||||
if [ $SCORE -eq $MAX_SCORE ]; then
|
||||
echo "✓ Excellent security configuration!"
|
||||
elif [ $SCORE -ge $((MAX_SCORE * 80 / 100)) ]; then
|
||||
echo "✓ Good security with minor improvements needed"
|
||||
else
|
||||
echo "⚠ Security improvements required"
|
||||
fi</code></pre>
|
||||
|
||||
<h2>Support and Resources</h2>
|
||||
|
||||
<h3>Getting Help</h3>
|
||||
|
||||
<ul>
|
||||
<li><strong>Documentation:</strong> Comprehensive guides in help system</li>
|
||||
<li><strong>Community:</strong> GitHub discussions and issues</li>
|
||||
<li><strong>Security Issues:</strong> Report to security team</li>
|
||||
<li><strong>Professional Support:</strong> Enterprise support options</li>
|
||||
</ul>
|
||||
|
||||
<h3>Additional Resources</h3>
|
||||
|
||||
<ul>
|
||||
<li><a href="#comprehensive-security-guide">Comprehensive Security Guide</a></li>
|
||||
<li><a href="#protected-notes-encryption">Protected Notes and Encryption</a></li>
|
||||
<li><a href="#authentication-session-management">Authentication and Session Management</a></li>
|
||||
<li><a href="#security-best-practices">Security Best Practices</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="alert alert-success">
|
||||
<strong>Security is a Journey:</strong> Implementing these advanced protection measures is just the beginning. Regular security assessments, updates, and monitoring are essential for maintaining a secure Trilium environment.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
412
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Authentication and Session Management.html
generated
vendored
Normal file
412
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Authentication and Session Management.html
generated
vendored
Normal file
@@ -0,0 +1,412 @@
|
||||
<div class="note-content">
|
||||
|
||||
<h1>Authentication and Session Management</h1>
|
||||
|
||||
<p>Trilium provides multiple authentication methods and robust session management to secure access to your notes while maintaining usability.</p>
|
||||
|
||||
<h2>Authentication Methods</h2>
|
||||
|
||||
<h3>Password Authentication</h3>
|
||||
|
||||
<p>The primary authentication method uses a master password to secure your Trilium instance.</p>
|
||||
|
||||
<h4>Password Setup</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Initial Setup</strong>: Set during first launch or server installation</li>
|
||||
<li><strong>Password Requirements</strong>: Configurable strength requirements</li>
|
||||
<li><strong>Verification</strong>: Scrypt-based password hashing for security</li>
|
||||
<li><strong>Storage</strong>: Hashed using scrypt with random salt</li>
|
||||
</ol>
|
||||
|
||||
<h4>Password Security</h4>
|
||||
|
||||
<ul>
|
||||
<li><strong>Hashing Algorithm</strong>: Scrypt with parameters N=16384, r=8, p=1</li>
|
||||
<li><strong>Salt</strong>: Unique random salt generated per installation</li>
|
||||
<li><strong>Verification Hash</strong>: Stored separately from encryption keys</li>
|
||||
<li><strong>Timing Attack Protection</strong>: Constant-time comparison</li>
|
||||
</ul>
|
||||
|
||||
<h3>Multi-Factor Authentication (TOTP)</h3>
|
||||
|
||||
<p>Trilium supports Time-based One-Time Password (TOTP) authentication for enhanced security.</p>
|
||||
|
||||
<h4>Setup Process</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Enable MFA</strong>: Navigate to Options → Multi-Factor Authentication</li>
|
||||
<li><strong>Generate Secret</strong>: Click "Generate New Secret"</li>
|
||||
<li><strong>Add to Authenticator</strong>: Scan QR code or enter secret manually</li>
|
||||
<li><strong>Verify Setup</strong>: Enter TOTP code to confirm configuration</li>
|
||||
<li><strong>Save Recovery Codes</strong>: Store backup codes securely</li>
|
||||
</ol>
|
||||
|
||||
<h4>Supported Authenticators</h4>
|
||||
|
||||
<ul>
|
||||
<li><strong>Google Authenticator</strong>: Mobile app for Android/iOS</li>
|
||||
<li><strong>Authy</strong>: Cross-platform authenticator with cloud sync</li>
|
||||
<li><strong>Microsoft Authenticator</strong>: Integrated with Microsoft accounts</li>
|
||||
<li><strong>1Password</strong>: Built-in TOTP support</li>
|
||||
<li><strong>Any RFC 6238 Compatible App</strong>: Standard TOTP implementation</li>
|
||||
</ul>
|
||||
|
||||
<h4>TOTP Configuration</h4>
|
||||
|
||||
<pre><code class="language-typescript">// TOTP settings in options
|
||||
{
|
||||
mfaEnabled: "true", // Enable/disable MFA
|
||||
mfaMethod: "totp", // Authentication method
|
||||
totpEncryptedSecret: "...", // Encrypted TOTP secret
|
||||
totpVerificationHash: "..." // Secret verification hash
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3>Recovery Codes</h3>
|
||||
|
||||
<p>Recovery codes provide backup access when TOTP is unavailable.</p>
|
||||
|
||||
<h4>Code Generation</h4>
|
||||
|
||||
<ul>
|
||||
<li><strong>Format</strong>: Base64-encoded 24-character strings ending in "=="</li>
|
||||
<li><strong>Quantity</strong>: Multiple codes generated during setup</li>
|
||||
<li><strong>Encryption</strong>: AES-256-CBC encrypted storage</li>
|
||||
<li><strong>One-time Use</strong>: Each code invalidated after use</li>
|
||||
</ul>
|
||||
|
||||
<h4>Usage Guidelines</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Secure Storage</strong>: Keep codes in password manager or secure location</li>
|
||||
<li><strong>Limited Use</strong>: Only use when primary authentication unavailable</li>
|
||||
<li><strong>Regeneration</strong>: Generate new codes if compromised</li>
|
||||
<li><strong>Expiration</strong>: Codes replaced with timestamp when used</li>
|
||||
</ol>
|
||||
|
||||
<h3>Single Sign-On (SSO)</h3>
|
||||
|
||||
<p>Trilium supports OpenID Connect for enterprise authentication.</p>
|
||||
|
||||
<h4>Supported Providers</h4>
|
||||
|
||||
<ul>
|
||||
<li><strong>Google</strong>: Google Workspace accounts</li>
|
||||
<li><strong>Microsoft</strong>: Azure AD integration</li>
|
||||
<li><strong>GitHub</strong>: Developer account authentication</li>
|
||||
<li><strong>Custom OIDC</strong>: Any OpenID Connect provider</li>
|
||||
</ul>
|
||||
|
||||
<h4>Configuration</h4>
|
||||
|
||||
<p>Set environment variables or config.ini:</p>
|
||||
|
||||
<pre><code class="language-ini">[OpenID]
|
||||
enabled=true
|
||||
issuer=https://accounts.google.com
|
||||
client_id=your-client-id
|
||||
client_secret=your-client-secret
|
||||
redirect_uri=https://your-trilium.example.com/auth/callback
|
||||
</code></pre>
|
||||
|
||||
<h2>Session Management</h2>
|
||||
|
||||
<h3>Session Security</h3>
|
||||
|
||||
<p>Trilium implements secure session management with multiple protection layers.</p>
|
||||
|
||||
<h4>Session Storage</h4>
|
||||
|
||||
<ul>
|
||||
<li><strong>Database Storage</strong>: Sessions stored in SQLite database</li>
|
||||
<li><strong>Secure Secrets</strong>: Cryptographically secure session secrets</li>
|
||||
<li><strong>Expiration Tracking</strong>: Automatic cleanup of expired sessions</li>
|
||||
<li><strong>Multiple Sessions</strong>: Support for concurrent user sessions</li>
|
||||
</ul>
|
||||
|
||||
<h4>Session Configuration</h4>
|
||||
|
||||
<pre><code class="language-typescript">// Session settings
|
||||
{
|
||||
secret: sessionSecret, // Cryptographic secret
|
||||
resave: false, // Don't save unchanged sessions
|
||||
saveUninitialized: false, // Don't save empty sessions
|
||||
rolling: true, // Reset expiration on activity
|
||||
cookie: {
|
||||
httpOnly: true, // Prevent XSS attacks
|
||||
secure: false, // HTTPS-only in production
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24-hour expiration
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3>Session Lifecycle</h3>
|
||||
|
||||
<h4>Session Creation</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Authentication</strong>: User provides valid credentials</li>
|
||||
<li><strong>Session ID</strong>: Generate cryptographically secure session ID</li>
|
||||
<li><strong>Database Storage</strong>: Store session data with expiration</li>
|
||||
<li><strong>Cookie Setting</strong>: Send session cookie to client</li>
|
||||
<li><strong>State Tracking</strong>: Monitor authentication state changes</li>
|
||||
</ol>
|
||||
|
||||
<h4>Session Maintenance</h4>
|
||||
|
||||
<ul>
|
||||
<li><strong>Activity Tracking</strong>: Update session expiration on each request</li>
|
||||
<li><strong>State Validation</strong>: Verify session integrity on each access</li>
|
||||
<li><strong>Timeout Management</strong>: Automatic logout after inactivity</li>
|
||||
<li><strong>Cross-tab Sync</strong>: Session state synchronized across browser tabs</li>
|
||||
</ul>
|
||||
|
||||
<h4>Session Termination</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Manual Logout</strong>: User-initiated session termination</li>
|
||||
<li><strong>Timeout Expiration</strong>: Automatic logout after inactivity</li>
|
||||
<li><strong>Security Events</strong>: Forced logout on security state changes</li>
|
||||
<li><strong>Cleanup</strong>: Remove session data from database</li>
|
||||
</ol>
|
||||
|
||||
<h3>CSRF Protection</h3>
|
||||
|
||||
<p>Trilium implements double-submit cookie CSRF protection.</p>
|
||||
|
||||
<h4>Protection Mechanism</h4>
|
||||
|
||||
<ul>
|
||||
<li><strong>Token Generation</strong>: Cryptographically secure CSRF tokens</li>
|
||||
<li><strong>Cookie Storage</strong>: Token stored in httpOnly cookie</li>
|
||||
<li><strong>Header Validation</strong>: Token required in request headers</li>
|
||||
<li><strong>Double Submit</strong>: Cookie and header values must match</li>
|
||||
</ul>
|
||||
|
||||
<h4>Configuration</h4>
|
||||
|
||||
<pre><code class="language-typescript">// CSRF protection settings
|
||||
{
|
||||
cookieOptions: {
|
||||
path: "/",
|
||||
secure: false, // HTTPS-only in production
|
||||
sameSite: "strict", // Strict same-site policy
|
||||
httpOnly: true // Prevent JavaScript access
|
||||
},
|
||||
cookieName: "_csrf" // Cookie name for CSRF token
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3>Session Security Headers</h3>
|
||||
|
||||
<p>Trilium sets security headers to protect against common attacks.</p>
|
||||
|
||||
<h4>Standard Headers</h4>
|
||||
|
||||
<ul>
|
||||
<li><strong>X-Frame-Options</strong>: Prevent clickjacking attacks</li>
|
||||
<li><strong>X-Content-Type-Options</strong>: Prevent MIME sniffing</li>
|
||||
<li><strong>X-XSS-Protection</strong>: Enable browser XSS protection</li>
|
||||
<li><strong>Strict-Transport-Security</strong>: Enforce HTTPS connections</li>
|
||||
<li><strong>Content-Security-Policy</strong>: Control resource loading</li>
|
||||
</ul>
|
||||
|
||||
<h2>Authentication Flow</h2>
|
||||
|
||||
<h3>Standard Login Process</h3>
|
||||
|
||||
<ol>
|
||||
<li><strong>Initial Request</strong>: User accesses protected resource</li>
|
||||
<li><strong>Redirect</strong>: System redirects to login page</li>
|
||||
<li><strong>Credential Entry</strong>: User enters username/password</li>
|
||||
<li><strong>Verification</strong>: System validates credentials</li>
|
||||
<li><strong>MFA Challenge</strong>: TOTP prompt if MFA enabled</li>
|
||||
<li><strong>Session Creation</strong>: Generate and store session</li>
|
||||
<li><strong>Redirect</strong>: Send user to requested resource</li>
|
||||
</ol>
|
||||
|
||||
<h3>MFA Login Process</h3>
|
||||
|
||||
<ol>
|
||||
<li><strong>Primary Authentication</strong>: Password verification succeeds</li>
|
||||
<li><strong>MFA Challenge</strong>: Display TOTP input form</li>
|
||||
<li><strong>Code Verification</strong>: Validate TOTP code</li>
|
||||
<li><strong>Recovery Option</strong>: Allow recovery code if TOTP fails</li>
|
||||
<li><strong>Session Creation</strong>: Create authenticated session</li>
|
||||
<li><strong>State Tracking</strong>: Update last authentication state</li>
|
||||
</ol>
|
||||
|
||||
<h3>SSO Login Process</h3>
|
||||
|
||||
<ol>
|
||||
<li><strong>Provider Redirect</strong>: Redirect to OpenID provider</li>
|
||||
<li><strong>Provider Authentication</strong>: User authenticates with provider</li>
|
||||
<li><strong>Authorization Code</strong>: Provider returns authorization code</li>
|
||||
<li><strong>Token Exchange</strong>: Exchange code for access token</li>
|
||||
<li><strong>User Info</strong>: Retrieve user information from provider</li>
|
||||
<li><strong>Local Session</strong>: Create local session for user</li>
|
||||
<li><strong>Access Grant</strong>: Allow access to protected resources</li>
|
||||
</ol>
|
||||
|
||||
<h2>Security Best Practices</h2>
|
||||
|
||||
<h3>Password Security</h3>
|
||||
|
||||
<ol>
|
||||
<li><strong>Strong Passwords</strong>: Require complex passwords</li>
|
||||
<li><strong>Regular Updates</strong>: Encourage periodic password changes</li>
|
||||
<li><strong>Unique Passwords</strong>: Don't reuse passwords from other services</li>
|
||||
<li><strong>Secure Storage</strong>: Use password managers</li>
|
||||
</ol>
|
||||
|
||||
<h3>Session Security</h3>
|
||||
|
||||
<ol>
|
||||
<li><strong>HTTPS Only</strong>: Always use HTTPS in production</li>
|
||||
<li><strong>Secure Cookies</strong>: Enable secure flag for session cookies</li>
|
||||
<li><strong>Short Timeouts</strong>: Configure appropriate session timeouts</li>
|
||||
<li><strong>Regular Cleanup</strong>: Automatically clean expired sessions</li>
|
||||
</ol>
|
||||
|
||||
<h3>Multi-Factor Authentication</h3>
|
||||
|
||||
<ol>
|
||||
<li><strong>Enable MFA</strong>: Always enable MFA for sensitive installations</li>
|
||||
<li><strong>Secure Recovery</strong>: Store recovery codes securely</li>
|
||||
<li><strong>Regular Review</strong>: Periodically review MFA configuration</li>
|
||||
<li><strong>Backup Methods</strong>: Maintain multiple authentication methods</li>
|
||||
</ol>
|
||||
|
||||
<h2>Troubleshooting</h2>
|
||||
|
||||
<h3>Common Authentication Issues</h3>
|
||||
|
||||
<h4>Login Failures</h4>
|
||||
|
||||
<p><strong>Symptoms</strong>: Cannot login with correct credentials</p>
|
||||
|
||||
<p><strong>Possible Causes</strong>:</p>
|
||||
<ul>
|
||||
<li>Incorrect password</li>
|
||||
<li>Database connectivity issues</li>
|
||||
<li>Session storage problems</li>
|
||||
<li>Browser cookie issues</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Solutions</strong>:</p>
|
||||
<ol>
|
||||
<li>Verify password accuracy (check caps lock)</li>
|
||||
<li>Clear browser cookies and cache</li>
|
||||
<li>Check database connectivity</li>
|
||||
<li>Review server logs for errors</li>
|
||||
<li>Restart application if needed</li>
|
||||
</ol>
|
||||
|
||||
<h4>MFA Issues</h4>
|
||||
|
||||
<p><strong>Symptoms</strong>: TOTP codes rejected or recovery codes fail</p>
|
||||
|
||||
<p><strong>Possible Causes</strong>:</p>
|
||||
<ul>
|
||||
<li>Clock synchronization issues</li>
|
||||
<li>Corrupted TOTP secret</li>
|
||||
<li>Used recovery codes</li>
|
||||
<li>Configuration problems</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Solutions</strong>:</p>
|
||||
<ol>
|
||||
<li>Synchronize device time</li>
|
||||
<li>Regenerate TOTP secret</li>
|
||||
<li>Use fresh recovery codes</li>
|
||||
<li>Check MFA configuration</li>
|
||||
<li>Contact administrator if needed</li>
|
||||
</ol>
|
||||
|
||||
<h4>Session Problems</h4>
|
||||
|
||||
<p><strong>Symptoms</strong>: Frequent logouts or session errors</p>
|
||||
|
||||
<p><strong>Possible Causes</strong>:</p>
|
||||
<ul>
|
||||
<li>Short session timeout</li>
|
||||
<li>Database session storage issues</li>
|
||||
<li>Browser cookie problems</li>
|
||||
<li>Network connectivity issues</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Solutions</strong>:</p>
|
||||
<ol>
|
||||
<li>Increase session timeout</li>
|
||||
<li>Check database permissions</li>
|
||||
<li>Enable browser cookies</li>
|
||||
<li>Verify network stability</li>
|
||||
<li>Review session configuration</li>
|
||||
</ol>
|
||||
|
||||
<h3>Security Monitoring</h3>
|
||||
|
||||
<h4>Log Analysis</h4>
|
||||
|
||||
<p>Monitor authentication logs for:</p>
|
||||
<ul>
|
||||
<li>Failed login attempts</li>
|
||||
<li>MFA failures</li>
|
||||
<li>Session anomalies</li>
|
||||
<li>Unusual access patterns</li>
|
||||
</ul>
|
||||
|
||||
<h4>Alert Configuration</h4>
|
||||
|
||||
<p>Set up alerts for:</p>
|
||||
<ul>
|
||||
<li>Multiple failed logins</li>
|
||||
<li>MFA bypass attempts</li>
|
||||
<li>Session manipulation</li>
|
||||
<li>Account lockouts</li>
|
||||
</ul>
|
||||
|
||||
<h4>Regular Audits</h4>
|
||||
|
||||
<p>Perform regular security audits:</p>
|
||||
<ul>
|
||||
<li>Review authentication logs</li>
|
||||
<li>Check session configurations</li>
|
||||
<li>Validate MFA setup</li>
|
||||
<li>Test recovery procedures</li>
|
||||
</ul>
|
||||
|
||||
<h2>Configuration Reference</h2>
|
||||
|
||||
<h3>Environment Variables</h3>
|
||||
|
||||
<pre><code class="language-bash"># Authentication settings
|
||||
TRILIUM_NO_AUTHENTICATION=false
|
||||
TRILIUM_PASSWORD_MIN_LENGTH=8
|
||||
TRILIUM_SESSION_TIMEOUT=86400
|
||||
|
||||
# MFA settings
|
||||
TRILIUM_MFA_ENABLED=true
|
||||
TRILIUM_MFA_METHOD=totp
|
||||
|
||||
# OpenID settings
|
||||
TRILIUM_OPENID_ENABLED=false
|
||||
TRILIUM_OPENID_ISSUER=https://provider.example.com
|
||||
TRILIUM_OPENID_CLIENT_ID=your-client-id
|
||||
TRILIUM_OPENID_CLIENT_SECRET=your-client-secret
|
||||
</code></pre>
|
||||
|
||||
<h3>Database Options</h3>
|
||||
|
||||
<pre><code class="language-sql">-- Authentication options
|
||||
INSERT INTO options (name, value) VALUES
|
||||
('passwordMinLength', '8'),
|
||||
('sessionTimeout', '86400'),
|
||||
('mfaEnabled', 'true'),
|
||||
('mfaMethod', 'totp');
|
||||
</code></pre>
|
||||
|
||||
<p><strong>Remember</strong>: Strong authentication and session management are critical for protecting your notes. Always use HTTPS in production and enable MFA for enhanced security.</p>
|
||||
|
||||
</div>
|
||||
476
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Comprehensive-Security-Guide.html
generated
vendored
Normal file
476
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Comprehensive-Security-Guide.html
generated
vendored
Normal file
@@ -0,0 +1,476 @@
|
||||
<div class="note-content">
|
||||
|
||||
<h1>Trilium Comprehensive Security Guide</h1>
|
||||
|
||||
<p>This comprehensive guide covers all aspects of Trilium security, from protected notes to enterprise deployment security practices.</p>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>Note:</strong> This guide contains advanced security configurations. Always test changes in a non-production environment first.
|
||||
</div>
|
||||
|
||||
<h2>Table of Contents</h2>
|
||||
|
||||
<ol>
|
||||
<li><a href="#protected-notes">Protected Notes and Encryption</a></li>
|
||||
<li><a href="#authentication">Authentication and Access Control</a></li>
|
||||
<li><a href="#deployment">Secure Deployment</a></li>
|
||||
<li><a href="#best-practices">Security Best Practices</a></li>
|
||||
<li><a href="#monitoring">Security Monitoring</a></li>
|
||||
<li><a href="#incident-response">Incident Response</a></li>
|
||||
</ol>
|
||||
|
||||
<h2 id="protected-notes">Protected Notes and Encryption</h2>
|
||||
|
||||
<h3>Overview</h3>
|
||||
|
||||
<p>Trilium's Protected Notes system provides robust encryption for sensitive content using industry-standard AES-128-CBC encryption with scrypt-based key derivation.</p>
|
||||
|
||||
<h4>Key Features</h4>
|
||||
<ul>
|
||||
<li><strong>Selective Encryption:</strong> Only notes marked as protected are encrypted</li>
|
||||
<li><strong>Strong Encryption:</strong> AES-128-CBC with scrypt key derivation</li>
|
||||
<li><strong>Session-based Access:</strong> Encrypted content accessible during protected sessions</li>
|
||||
<li><strong>Zero-knowledge:</strong> Server never stores unencrypted protected content</li>
|
||||
</ul>
|
||||
|
||||
<h3>Setting Up Protected Notes</h3>
|
||||
|
||||
<h4>Initial Configuration</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Set Master Password</strong>
|
||||
<ul>
|
||||
<li>Go to <em>Options → Security → Password</em></li>
|
||||
<li>Choose a strong password (minimum 8 characters, recommended 12+)</li>
|
||||
<li>Use a unique password not used elsewhere</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li><strong>Configure Protected Session</strong>
|
||||
<ul>
|
||||
<li>Set session timeout (default: 10 minutes)</li>
|
||||
<li>Configure auto-logout preferences</li>
|
||||
<li>Enable session notifications</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li><strong>Create Your First Protected Note</strong>
|
||||
<ul>
|
||||
<li>Right-click any note → "Toggle Protected Status"</li>
|
||||
<li>Or use keyboard shortcut: <kbd>Ctrl+Shift+U</kbd></li>
|
||||
<li>Or click Actions menu → "Protect this note"</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h4>Managing Protected Sessions</h4>
|
||||
|
||||
<p><strong>Entering Protected Session:</strong></p>
|
||||
<ul>
|
||||
<li>Click the shield icon in the toolbar</li>
|
||||
<li>Use keyboard shortcut: <kbd>Ctrl+Shift+P</kbd></li>
|
||||
<li>Automatic prompt when accessing protected content</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Session Security:</strong></p>
|
||||
<ul>
|
||||
<li>Sessions timeout automatically after inactivity</li>
|
||||
<li>Green shield indicates active protected session</li>
|
||||
<li>Manual logout available via shield menu</li>
|
||||
<li>Independent sessions per browser/client</li>
|
||||
</ul>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<strong>Security Note:</strong> Protected sessions store encryption keys in memory. Always log out when finished working with protected content.
|
||||
</div>
|
||||
|
||||
<h3>Encryption Technical Details</h3>
|
||||
|
||||
<h4>Encryption Process</h4>
|
||||
<pre><code>1. Generate random 16-byte IV
|
||||
2. Compute SHA-1 digest of plaintext (integrity check)
|
||||
3. Prepend digest (4 bytes) to plaintext
|
||||
4. Encrypt with AES-128-CBC using data key and IV
|
||||
5. Prepend IV to encrypted data
|
||||
6. Encode result as Base64</code></pre>
|
||||
|
||||
<h4>Key Management</h4>
|
||||
<ul>
|
||||
<li><strong>Master Password:</strong> User-provided secret</li>
|
||||
<li><strong>Password-Derived Key:</strong> Generated using scrypt (N=16384, r=8, p=1)</li>
|
||||
<li><strong>Data Key:</strong> 32-byte random key, encrypted with password-derived key</li>
|
||||
<li><strong>Session Key:</strong> Data key loaded into memory during protected session</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="authentication">Authentication and Access Control</h2>
|
||||
|
||||
<h3>Password Authentication</h3>
|
||||
|
||||
<h4>Security Features</h4>
|
||||
<ul>
|
||||
<li><strong>Scrypt Hashing:</strong> CPU-intensive hashing prevents brute force attacks</li>
|
||||
<li><strong>Salt:</strong> Unique random salt per installation</li>
|
||||
<li><strong>Timing Attack Protection:</strong> Constant-time comparison</li>
|
||||
<li><strong>Separation:</strong> Authentication separate from encryption keys</li>
|
||||
</ul>
|
||||
|
||||
<h4>Password Management</h4>
|
||||
<ul>
|
||||
<li><strong>Strong Passwords:</strong> Use complex, unique passwords</li>
|
||||
<li><strong>Regular Updates:</strong> Change passwords periodically</li>
|
||||
<li><strong>Secure Storage:</strong> Consider using a password manager</li>
|
||||
<li><strong>Recovery:</strong> No built-in password recovery - keep backup access</li>
|
||||
</ul>
|
||||
|
||||
<h3>Multi-Factor Authentication (MFA)</h3>
|
||||
|
||||
<h4>TOTP Setup</h4>
|
||||
|
||||
<ol>
|
||||
<li>Go to <em>Options → Security → Multi-Factor Authentication</em></li>
|
||||
<li>Click "Generate New Secret"</li>
|
||||
<li>Scan QR code with authenticator app</li>
|
||||
<li>Enter TOTP code to verify setup</li>
|
||||
<li>Save recovery codes securely</li>
|
||||
</ol>
|
||||
|
||||
<h4>Supported Authenticators</h4>
|
||||
<ul>
|
||||
<li>Google Authenticator</li>
|
||||
<li>Authy</li>
|
||||
<li>Microsoft Authenticator</li>
|
||||
<li>1Password</li>
|
||||
<li>Any RFC 6238 compatible app</li>
|
||||
</ul>
|
||||
|
||||
<h4>Recovery Codes</h4>
|
||||
<ul>
|
||||
<li><strong>Format:</strong> Base64-encoded 24-character strings</li>
|
||||
<li><strong>Storage:</strong> AES-256-CBC encrypted</li>
|
||||
<li><strong>Usage:</strong> One-time use only</li>
|
||||
<li><strong>Security:</strong> Store in secure location (password manager)</li>
|
||||
</ul>
|
||||
|
||||
<h3>Session Management</h3>
|
||||
|
||||
<h4>Session Security</h4>
|
||||
<ul>
|
||||
<li><strong>Secure Storage:</strong> Sessions stored in encrypted database</li>
|
||||
<li><strong>Automatic Cleanup:</strong> Expired sessions removed periodically</li>
|
||||
<li><strong>CSRF Protection:</strong> Double-submit cookie pattern</li>
|
||||
<li><strong>Secure Cookies:</strong> HTTPOnly, Secure, SameSite attributes</li>
|
||||
</ul>
|
||||
|
||||
<h4>Configuration Options</h4>
|
||||
<ul>
|
||||
<li><strong>Session Timeout:</strong> Configurable timeout period</li>
|
||||
<li><strong>Remember Me:</strong> Extended session duration option</li>
|
||||
<li><strong>Auto Logout:</strong> Automatic logout on browser close</li>
|
||||
<li><strong>Multi-client:</strong> Independent sessions per device</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="deployment">Secure Deployment</h2>
|
||||
|
||||
<h3>HTTPS Configuration</h3>
|
||||
|
||||
<h4>SSL/TLS Requirements</h4>
|
||||
<ul>
|
||||
<li><strong>Mandatory for Production:</strong> Always use HTTPS in production</li>
|
||||
<li><strong>TLS Version:</strong> Use TLS 1.2 or higher</li>
|
||||
<li><strong>Certificates:</strong> Valid SSL certificates (Let's Encrypt recommended)</li>
|
||||
<li><strong>HSTS:</strong> Enable HTTP Strict Transport Security</li>
|
||||
</ul>
|
||||
|
||||
<h4>Security Headers</h4>
|
||||
<pre><code># Essential security headers
|
||||
X-Frame-Options: DENY
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 1; mode=block
|
||||
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
||||
Content-Security-Policy: default-src 'self'; ...</code></pre>
|
||||
|
||||
<h3>Network Security</h3>
|
||||
|
||||
<h4>Firewall Configuration</h4>
|
||||
<ul>
|
||||
<li><strong>Restrict Ports:</strong> Only allow necessary ports (22, 443)</li>
|
||||
<li><strong>Block Direct Access:</strong> Block direct access to Trilium port (8080)</li>
|
||||
<li><strong>IP Restrictions:</strong> Limit access to trusted IP ranges</li>
|
||||
<li><strong>Rate Limiting:</strong> Implement connection rate limiting</li>
|
||||
</ul>
|
||||
|
||||
<h4>Reverse Proxy</h4>
|
||||
<ul>
|
||||
<li><strong>Nginx/Apache:</strong> Use reverse proxy for SSL termination</li>
|
||||
<li><strong>Load Balancing:</strong> Distribute traffic across instances</li>
|
||||
<li><strong>Caching:</strong> Cache static content</li>
|
||||
<li><strong>Compression:</strong> Enable content compression</li>
|
||||
</ul>
|
||||
|
||||
<h3>Database Security</h3>
|
||||
|
||||
<h4>File Permissions</h4>
|
||||
<ul>
|
||||
<li><strong>Database:</strong> 600 (owner read/write only)</li>
|
||||
<li><strong>Data Directory:</strong> 700 (owner access only)</li>
|
||||
<li><strong>Configuration:</strong> 600 (owner read/write only)</li>
|
||||
<li><strong>Ownership:</strong> Dedicated trilium user</li>
|
||||
</ul>
|
||||
|
||||
<h4>Backup Security</h4>
|
||||
<ul>
|
||||
<li><strong>Encryption:</strong> Always encrypt backups</li>
|
||||
<li><strong>Storage:</strong> Secure off-site storage</li>
|
||||
<li><strong>Access Control:</strong> Limit backup access</li>
|
||||
<li><strong>Testing:</strong> Regular restoration testing</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="best-practices">Security Best Practices</h2>
|
||||
|
||||
<h3>Password Security</h3>
|
||||
|
||||
<h4>Password Requirements</h4>
|
||||
<ul>
|
||||
<li><strong>Length:</strong> Minimum 12 characters</li>
|
||||
<li><strong>Complexity:</strong> Mix of letters, numbers, symbols</li>
|
||||
<li><strong>Uniqueness:</strong> Don't reuse passwords</li>
|
||||
<li><strong>Passphrases:</strong> Consider using memorable passphrases</li>
|
||||
</ul>
|
||||
|
||||
<h4>Password Management</h4>
|
||||
<ul>
|
||||
<li><strong>Password Manager:</strong> Use reputable password manager</li>
|
||||
<li><strong>Regular Updates:</strong> Change passwords periodically</li>
|
||||
<li><strong>Secure Recovery:</strong> Store recovery information safely</li>
|
||||
<li><strong>Team Coordination:</strong> Coordinate password changes in shared environments</li>
|
||||
</ul>
|
||||
|
||||
<h3>Access Control</h3>
|
||||
|
||||
<h4>User Management</h4>
|
||||
<ul>
|
||||
<li><strong>Single User Model:</strong> Trilium designed for single-user access</li>
|
||||
<li><strong>Shared Access:</strong> Use with caution in shared environments</li>
|
||||
<li><strong>Guest Access:</strong> Disable unless specifically needed</li>
|
||||
<li><strong>Admin Privileges:</strong> Run with minimal necessary privileges</li>
|
||||
</ul>
|
||||
|
||||
<h4>Session Management</h4>
|
||||
<ul>
|
||||
<li><strong>Timeout Configuration:</strong> Set appropriate timeouts for usage pattern</li>
|
||||
<li><strong>Device Security:</strong> Lock workstation when away</li>
|
||||
<li><strong>Shared Computers:</strong> Always log out completely</li>
|
||||
<li><strong>Browser Security:</strong> Use up-to-date browsers</li>
|
||||
</ul>
|
||||
|
||||
<h3>Data Protection</h3>
|
||||
|
||||
<h4>Backup Strategy</h4>
|
||||
<ul>
|
||||
<li><strong>Regular Backups:</strong> Automated daily backups</li>
|
||||
<li><strong>Encryption:</strong> All backups encrypted with strong keys</li>
|
||||
<li><strong>Multiple Locations:</strong> Store backups in multiple secure locations</li>
|
||||
<li><strong>Version Control:</strong> Maintain multiple backup versions</li>
|
||||
<li><strong>Testing:</strong> Regular restoration testing</li>
|
||||
</ul>
|
||||
|
||||
<h4>Data Classification</h4>
|
||||
<ul>
|
||||
<li><strong>Sensitive Data:</strong> Always use protected notes for sensitive content</li>
|
||||
<li><strong>Public Data:</strong> Separate public and private information</li>
|
||||
<li><strong>Compliance:</strong> Follow industry-specific requirements</li>
|
||||
<li><strong>Retention:</strong> Implement data retention policies</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="monitoring">Security Monitoring</h2>
|
||||
|
||||
<h3>Log Monitoring</h3>
|
||||
|
||||
<h4>Security Events</h4>
|
||||
<ul>
|
||||
<li><strong>Authentication:</strong> Monitor login attempts and failures</li>
|
||||
<li><strong>Authorization:</strong> Track access to protected resources</li>
|
||||
<li><strong>Sessions:</strong> Monitor session creation and termination</li>
|
||||
<li><strong>Data Access:</strong> Log protected note access</li>
|
||||
</ul>
|
||||
|
||||
<h4>Alerting</h4>
|
||||
<ul>
|
||||
<li><strong>Failed Logins:</strong> Alert on multiple failed attempts</li>
|
||||
<li><strong>MFA Failures:</strong> Monitor MFA bypass attempts</li>
|
||||
<li><strong>Session Anomalies:</strong> Detect unusual session patterns</li>
|
||||
<li><strong>Data Changes:</strong> Monitor unexpected data modifications</li>
|
||||
</ul>
|
||||
|
||||
<h3>Intrusion Detection</h3>
|
||||
|
||||
<h4>Behavioral Analysis</h4>
|
||||
<ul>
|
||||
<li><strong>Login Patterns:</strong> Detect unusual login times/locations</li>
|
||||
<li><strong>Access Patterns:</strong> Monitor unusual data access</li>
|
||||
<li><strong>Session Behavior:</strong> Identify suspicious session activity</li>
|
||||
<li><strong>Network Activity:</strong> Monitor network connection patterns</li>
|
||||
</ul>
|
||||
|
||||
<h4>Automated Response</h4>
|
||||
<ul>
|
||||
<li><strong>Account Lockout:</strong> Temporary suspension of suspicious accounts</li>
|
||||
<li><strong>IP Blocking:</strong> Block suspicious IP addresses</li>
|
||||
<li><strong>Rate Limiting:</strong> Dynamic rate limit adjustment</li>
|
||||
<li><strong>Alert Generation:</strong> Immediate notification of threats</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="incident-response">Incident Response</h2>
|
||||
|
||||
<h3>Preparation</h3>
|
||||
|
||||
<h4>Response Plan</h4>
|
||||
<ul>
|
||||
<li><strong>Procedures:</strong> Document step-by-step response procedures</li>
|
||||
<li><strong>Contacts:</strong> Maintain emergency contact information</li>
|
||||
<li><strong>Tools:</strong> Prepare incident response tools and scripts</li>
|
||||
<li><strong>Communication:</strong> Plan user notification procedures</li>
|
||||
</ul>
|
||||
|
||||
<h4>Training</h4>
|
||||
<ul>
|
||||
<li><strong>Team Training:</strong> Regular incident response training</li>
|
||||
<li><strong>Simulations:</strong> Practice incident response scenarios</li>
|
||||
<li><strong>Documentation:</strong> Keep response procedures updated</li>
|
||||
<li><strong>Lessons Learned:</strong> Update procedures based on incidents</li>
|
||||
</ul>
|
||||
|
||||
<h3>Detection and Response</h3>
|
||||
|
||||
<h4>Immediate Actions</h4>
|
||||
<ol>
|
||||
<li><strong>Identify:</strong> Quickly identify the type and scope of incident</li>
|
||||
<li><strong>Contain:</strong> Isolate affected systems to prevent spread</li>
|
||||
<li><strong>Preserve:</strong> Preserve evidence for forensic analysis</li>
|
||||
<li><strong>Notify:</strong> Inform relevant stakeholders</li>
|
||||
<li><strong>Assess:</strong> Evaluate impact and required response</li>
|
||||
</ol>
|
||||
|
||||
<h4>Recovery Procedures</h4>
|
||||
<ul>
|
||||
<li><strong>System Isolation:</strong> Temporarily isolate compromised systems</li>
|
||||
<li><strong>Forensic Backup:</strong> Create forensic copies for analysis</li>
|
||||
<li><strong>Restore from Backup:</strong> Restore from known good backups</li>
|
||||
<li><strong>Verify Integrity:</strong> Confirm system and data integrity</li>
|
||||
<li><strong>Resume Operations:</strong> Safely resume normal operations</li>
|
||||
</ul>
|
||||
|
||||
<h3>Post-Incident</h3>
|
||||
|
||||
<h4>Documentation</h4>
|
||||
<ul>
|
||||
<li><strong>Incident Report:</strong> Complete incident documentation</li>
|
||||
<li><strong>Timeline:</strong> Detailed timeline of events and responses</li>
|
||||
<li><strong>Impact Assessment:</strong> Evaluation of damage and losses</li>
|
||||
<li><strong>Lessons Learned:</strong> Identify improvements for future</li>
|
||||
</ul>
|
||||
|
||||
<h4>Improvement</h4>
|
||||
<ul>
|
||||
<li><strong>Process Updates:</strong> Update response procedures</li>
|
||||
<li><strong>Security Enhancements:</strong> Implement additional security controls</li>
|
||||
<li><strong>Training Updates:</strong> Update training based on lessons learned</li>
|
||||
<li><strong>Regular Reviews:</strong> Periodic review of incident response capability</li>
|
||||
</ul>
|
||||
|
||||
<h2>Troubleshooting</h2>
|
||||
|
||||
<h3>Common Issues</h3>
|
||||
|
||||
<h4>Authentication Problems</h4>
|
||||
<div class="alert alert-info">
|
||||
<strong>Problem:</strong> Cannot login with correct credentials<br>
|
||||
<strong>Solutions:</strong>
|
||||
<ul>
|
||||
<li>Check password spelling and case sensitivity</li>
|
||||
<li>Clear browser cookies and cache</li>
|
||||
<li>Verify database connectivity</li>
|
||||
<li>Check server logs for errors</li>
|
||||
<li>Restart application if needed</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h4>Protected Notes Issues</h4>
|
||||
<div class="alert alert-warning">
|
||||
<strong>Problem:</strong> "Could not decrypt string" error<br>
|
||||
<strong>Solutions:</strong>
|
||||
<ul>
|
||||
<li>Verify correct password entry</li>
|
||||
<li>Check for active protected session</li>
|
||||
<li>Restart application and retry</li>
|
||||
<li>Restore from backup if corruption suspected</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h4>MFA Problems</h4>
|
||||
<div class="alert alert-info">
|
||||
<strong>Problem:</strong> TOTP codes rejected<br>
|
||||
<strong>Solutions:</strong>
|
||||
<ul>
|
||||
<li>Synchronize device time</li>
|
||||
<li>Try recovery codes</li>
|
||||
<li>Regenerate TOTP secret</li>
|
||||
<li>Check authenticator app configuration</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>Emergency Procedures</h3>
|
||||
|
||||
<h4>Password Recovery</h4>
|
||||
<div class="alert alert-danger">
|
||||
<strong>Important:</strong> Trilium cannot recover forgotten passwords. Options include:
|
||||
<ul>
|
||||
<li>Restore from backup with known password</li>
|
||||
<li>Export unprotected content before password reset</li>
|
||||
<li>Complete reset (loses all protected content)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h4>Data Recovery</h4>
|
||||
<ul>
|
||||
<li><strong>Backup Restoration:</strong> Use recent encrypted backups</li>
|
||||
<li><strong>Database Repair:</strong> Use SQLite repair tools if needed</li>
|
||||
<li><strong>Partial Recovery:</strong> Export accessible content</li>
|
||||
<li><strong>Professional Help:</strong> Contact data recovery services for critical data</li>
|
||||
</ul>
|
||||
|
||||
<h2>Compliance and Standards</h2>
|
||||
|
||||
<h3>Regulatory Compliance</h3>
|
||||
|
||||
<h4>GDPR Compliance</h4>
|
||||
<ul>
|
||||
<li><strong>Data Protection:</strong> AES encryption provides technical safeguards</li>
|
||||
<li><strong>Right to Erasure:</strong> Secure deletion of encryption keys</li>
|
||||
<li><strong>Data Portability:</strong> Export capabilities for protected content</li>
|
||||
<li><strong>Privacy by Design:</strong> Encryption built into architecture</li>
|
||||
</ul>
|
||||
|
||||
<h4>Industry Standards</h4>
|
||||
<ul>
|
||||
<li><strong>ISO 27001:</strong> Information security management compliance</li>
|
||||
<li><strong>SOC 2:</strong> Security and availability controls</li>
|
||||
<li><strong>HIPAA:</strong> Healthcare data protection requirements</li>
|
||||
<li><strong>PCI DSS:</strong> Payment card industry standards</li>
|
||||
</ul>
|
||||
|
||||
<h3>Encryption Standards</h3>
|
||||
|
||||
<h4>Algorithm Compliance</h4>
|
||||
<ul>
|
||||
<li><strong>AES-128:</strong> NIST approved, FIPS 140-2 Level 1</li>
|
||||
<li><strong>Scrypt:</strong> RFC 7914 standard</li>
|
||||
<li><strong>SHA-1:</strong> NIST standard (integrity verification only)</li>
|
||||
<li><strong>Random Generation:</strong> Cryptographically secure sources</li>
|
||||
</ul>
|
||||
|
||||
<div class="alert alert-success">
|
||||
<strong>Remember:</strong> Security is an ongoing process, not a one-time configuration. Regularly review and update your security posture to address evolving threats and requirements.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
326
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Protected Notes and Encryption.html
generated
vendored
Normal file
326
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Protected Notes and Encryption.html
generated
vendored
Normal file
@@ -0,0 +1,326 @@
|
||||
<div class="note-content">
|
||||
|
||||
<h1>Protected Notes and Encryption</h1>
|
||||
|
||||
<p>Trilium provides robust encryption capabilities through its Protected Notes system, ensuring your sensitive information remains secure even if your database is compromised.</p>
|
||||
|
||||
<h2>Overview</h2>
|
||||
|
||||
<p>Protected notes in Trilium use <strong>AES-128-CBC encryption</strong> with scrypt-based key derivation to protect sensitive content. The encryption is designed to be:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Secure</strong>: Uses industry-standard AES encryption with strong key derivation</li>
|
||||
<li><strong>Selective</strong>: Only notes marked as protected are encrypted</li>
|
||||
<li><strong>Session-based</strong>: Decrypted content remains accessible during a protected session</li>
|
||||
<li><strong>Zero-knowledge</strong>: The server never stores unencrypted protected content</li>
|
||||
</ul>
|
||||
|
||||
<h2>How Encryption Works</h2>
|
||||
|
||||
<h3>Encryption Algorithm</h3>
|
||||
|
||||
<ul>
|
||||
<li><strong>Cipher</strong>: AES-128-CBC (Advanced Encryption Standard in Cipher Block Chaining mode)</li>
|
||||
<li><strong>Key Derivation</strong>: Scrypt with configurable parameters (N=16384, r=8, p=1)</li>
|
||||
<li><strong>Initialization Vector</strong>: 16-byte random IV generated for each encryption operation</li>
|
||||
<li><strong>Integrity Protection</strong>: SHA-1 digest (first 4 bytes) prepended to plaintext for tamper detection</li>
|
||||
</ul>
|
||||
|
||||
<h3>Key Management</h3>
|
||||
|
||||
<ol>
|
||||
<li><strong>Master Password</strong>: User-provided password used for key derivation</li>
|
||||
<li><strong>Data Key</strong>: 32-byte random key generated during setup, encrypted with password-derived key</li>
|
||||
<li><strong>Password-Derived Key</strong>: Generated using scrypt from master password and salt</li>
|
||||
<li><strong>Session Key</strong>: Data key loaded into memory during protected session</li>
|
||||
</ol>
|
||||
|
||||
<h3>Encryption Process</h3>
|
||||
|
||||
<pre><code>1. Generate random 16-byte IV
|
||||
2. Compute SHA-1 digest of plaintext (use first 4 bytes)
|
||||
3. Prepend digest to plaintext
|
||||
4. Encrypt (digest + plaintext) using AES-128-CBC
|
||||
5. Prepend IV to encrypted data
|
||||
6. Encode result as Base64
|
||||
</code></pre>
|
||||
|
||||
<h3>Decryption Process</h3>
|
||||
|
||||
<pre><code>1. Decode Base64 ciphertext
|
||||
2. Extract IV (first 16 bytes) and encrypted data
|
||||
3. Decrypt using AES-128-CBC with data key and IV
|
||||
4. Extract digest (first 4 bytes) and plaintext
|
||||
5. Verify integrity by comparing computed vs. stored digest
|
||||
6. Return plaintext if verification succeeds
|
||||
</code></pre>
|
||||
|
||||
<h2>Setting Up Protected Notes</h2>
|
||||
|
||||
<h3>Initial Setup</h3>
|
||||
|
||||
<ol>
|
||||
<li><strong>Set Master Password</strong>: Configure a strong password during initial setup</li>
|
||||
<li><strong>Create Protected Note</strong>: Right-click a note and select "Toggle Protected Status"</li>
|
||||
<li><strong>Enter Protected Session</strong>: Click the shield icon or use Ctrl+Shift+P</li>
|
||||
</ol>
|
||||
|
||||
<h3>Password Requirements</h3>
|
||||
|
||||
<ul>
|
||||
<li><strong>Minimum Length</strong>: 8 characters (recommended: 12+ characters)</li>
|
||||
<li><strong>Complexity</strong>: Use a mix of uppercase, lowercase, numbers, and symbols</li>
|
||||
<li><strong>Uniqueness</strong>: Don't reuse passwords from other services</li>
|
||||
<li><strong>Storage</strong>: Consider using a password manager for complex passwords</li>
|
||||
</ul>
|
||||
|
||||
<h3>Best Practices</h3>
|
||||
|
||||
<ol>
|
||||
<li><strong>Strong Passwords</strong>: Use passphrases or generated passwords</li>
|
||||
<li><strong>Regular Changes</strong>: Update passwords periodically</li>
|
||||
<li><strong>Secure Storage</strong>: Store password recovery information securely</li>
|
||||
<li><strong>Backup Strategy</strong>: Ensure encrypted backups are properly secured</li>
|
||||
</ol>
|
||||
|
||||
<h2>Protected Sessions</h2>
|
||||
|
||||
<h3>Session Management</h3>
|
||||
|
||||
<ul>
|
||||
<li><strong>Automatic Timeout</strong>: Sessions expire after configurable timeout (default: 10 minutes)</li>
|
||||
<li><strong>Manual Control</strong>: Explicitly enter/exit protected sessions</li>
|
||||
<li><strong>Activity Tracking</strong>: Session timeout resets with each protected note access</li>
|
||||
<li><strong>Multi-client</strong>: Each client maintains its own protected session</li>
|
||||
</ul>
|
||||
|
||||
<h3>Session Lifecycle</h3>
|
||||
|
||||
<ol>
|
||||
<li><strong>Enter Session</strong>: User enters master password</li>
|
||||
<li><strong>Key Derivation</strong>: System derives data key from password</li>
|
||||
<li><strong>Session Active</strong>: Protected content accessible in plaintext</li>
|
||||
<li><strong>Timeout/Logout</strong>: Data key removed from memory</li>
|
||||
<li><strong>Protection Restored</strong>: Content returns to encrypted state</li>
|
||||
</ol>
|
||||
|
||||
<h3>Configuration Options</h3>
|
||||
|
||||
<p>Access via Options → Protected Session:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Session Timeout</strong>: Duration before automatic logout (seconds)</li>
|
||||
<li><strong>Password Verification</strong>: Enable/disable password strength requirements</li>
|
||||
<li><strong>Recovery Options</strong>: Configure password recovery mechanisms</li>
|
||||
</ul>
|
||||
|
||||
<h2>Performance Considerations</h2>
|
||||
|
||||
<h3>Encryption Overhead</h3>
|
||||
|
||||
<ul>
|
||||
<li><strong>CPU Impact</strong>: Scrypt key derivation is intentionally CPU-intensive</li>
|
||||
<li><strong>Memory Usage</strong>: Minimal additional memory for encrypted content</li>
|
||||
<li><strong>Storage Size</strong>: Encrypted content is slightly larger due to Base64 encoding</li>
|
||||
<li><strong>Network Transfer</strong>: Encrypted notes transfer as Base64 strings</li>
|
||||
</ul>
|
||||
|
||||
<h3>Optimization Tips</h3>
|
||||
|
||||
<ol>
|
||||
<li><strong>Selective Protection</strong>: Only encrypt truly sensitive notes</li>
|
||||
<li><strong>Session Management</strong>: Keep sessions active during intensive work</li>
|
||||
<li><strong>Hardware Acceleration</strong>: Modern CPUs provide AES acceleration</li>
|
||||
<li><strong>Batch Operations</strong>: Group protected note operations when possible</li>
|
||||
</ol>
|
||||
|
||||
<h2>Security Considerations</h2>
|
||||
|
||||
<h3>Threat Model</h3>
|
||||
|
||||
<p><strong>Protected Against</strong>:</p>
|
||||
<ul>
|
||||
<li>Database theft or unauthorized access</li>
|
||||
<li>Network interception (data at rest)</li>
|
||||
<li>Server-side data breaches</li>
|
||||
<li>Backup file compromise</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Not Protected Against</strong>:</p>
|
||||
<ul>
|
||||
<li>Keyloggers or screen capture malware</li>
|
||||
<li>Physical access to unlocked device</li>
|
||||
<li>Memory dumps during active session</li>
|
||||
<li>Social engineering attacks</li>
|
||||
</ul>
|
||||
|
||||
<h3>Limitations</h3>
|
||||
|
||||
<ol>
|
||||
<li><strong>Note Titles</strong>: Currently encrypted, may leak structural information</li>
|
||||
<li><strong>Metadata</strong>: Creation dates, modification times remain unencrypted</li>
|
||||
<li><strong>Search Indexing</strong>: Protected notes excluded from full-text search</li>
|
||||
<li><strong>Sync Conflicts</strong>: May be harder to resolve for protected content</li>
|
||||
</ol>
|
||||
|
||||
<h2>Troubleshooting</h2>
|
||||
|
||||
<h3>Common Issues</h3>
|
||||
|
||||
<h4>"Could not decrypt string" Error</h4>
|
||||
|
||||
<p><strong>Causes</strong>:</p>
|
||||
<ul>
|
||||
<li>Incorrect password entered</li>
|
||||
<li>Corrupted encrypted data</li>
|
||||
<li>Database migration issues</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Solutions</strong>:</p>
|
||||
<ol>
|
||||
<li>Verify password spelling and case sensitivity</li>
|
||||
<li>Check for active protected session</li>
|
||||
<li>Restart application and retry</li>
|
||||
<li>Restore from backup if corruption suspected</li>
|
||||
</ol>
|
||||
|
||||
<h4>Protected Session Won't Start</h4>
|
||||
|
||||
<p><strong>Causes</strong>:</p>
|
||||
<ul>
|
||||
<li>Password verification hash mismatch</li>
|
||||
<li>Missing encryption salt</li>
|
||||
<li>Database schema issues</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Solutions</strong>:</p>
|
||||
<ol>
|
||||
<li>Check error logs for specific error messages</li>
|
||||
<li>Verify database integrity</li>
|
||||
<li>Restore from known good backup</li>
|
||||
<li>Contact support with error details</li>
|
||||
</ol>
|
||||
|
||||
<h4>Performance Issues</h4>
|
||||
|
||||
<p><strong>Symptoms</strong>:</p>
|
||||
<ul>
|
||||
<li>Slow password verification</li>
|
||||
<li>Long delays entering protected session</li>
|
||||
<li>High CPU usage during encryption</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Solutions</strong>:</p>
|
||||
<ol>
|
||||
<li>Reduce scrypt parameters (advanced users only)</li>
|
||||
<li>Limit number of protected notes</li>
|
||||
<li>Upgrade hardware (more RAM/faster CPU)</li>
|
||||
<li>Close other resource-intensive applications</li>
|
||||
</ol>
|
||||
|
||||
<h3>Recovery Procedures</h3>
|
||||
|
||||
<h4>Password Recovery</h4>
|
||||
|
||||
<p>If you forget your master password:</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>No Built-in Recovery</strong>: Trilium cannot recover forgotten passwords</li>
|
||||
<li><strong>Backup Restoration</strong>: Restore from backup with known password</li>
|
||||
<li><strong>Data Export</strong>: Export unprotected content before password change</li>
|
||||
<li><strong>Complete Reset</strong>: Last resort - lose all protected content</li>
|
||||
</ol>
|
||||
|
||||
<h4>Data Recovery</h4>
|
||||
|
||||
<p>For corrupted protected notes:</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>Verify Backup</strong>: Check if backups contain uncorrupted data</li>
|
||||
<li><strong>Export/Import</strong>: Try exporting and re-importing the note</li>
|
||||
<li><strong>Database Repair</strong>: Use database repair tools if available</li>
|
||||
<li><strong>Professional Help</strong>: Contact data recovery services for critical data</li>
|
||||
</ol>
|
||||
|
||||
<h2>Advanced Configuration</h2>
|
||||
|
||||
<h3>Custom Encryption Parameters</h3>
|
||||
|
||||
<p><strong>Warning</strong>: Modifying encryption parameters requires advanced knowledge and may break compatibility.</p>
|
||||
|
||||
<p>For expert users, encryption parameters can be modified in the source code:</p>
|
||||
|
||||
<pre><code class="language-typescript">// In my_scrypt.ts
|
||||
const scryptParams = {
|
||||
N: 16384, // CPU/memory cost parameter
|
||||
r: 8, // Block size parameter
|
||||
p: 1 // Parallelization parameter
|
||||
};
|
||||
</code></pre>
|
||||
|
||||
<h3>Integration with External Tools</h3>
|
||||
|
||||
<p>Protected notes can be accessed programmatically:</p>
|
||||
|
||||
<pre><code class="language-javascript">// Backend script example
|
||||
const protectedNote = api.getNote('noteId');
|
||||
if (protectedNote.isProtected) {
|
||||
// Content will be encrypted unless in protected session
|
||||
const content = protectedNote.getContent();
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h2>Compliance and Auditing</h2>
|
||||
|
||||
<h3>Encryption Standards</h3>
|
||||
|
||||
<ul>
|
||||
<li><strong>Algorithm</strong>: AES-128-CBC (FIPS 140-2 approved)</li>
|
||||
<li><strong>Key Derivation</strong>: Scrypt (RFC 7914)</li>
|
||||
<li><strong>Random Generation</strong>: Node.js crypto.randomBytes() (OS entropy)</li>
|
||||
</ul>
|
||||
|
||||
<h3>Audit Trail</h3>
|
||||
|
||||
<ul>
|
||||
<li>Protected session entry/exit events logged</li>
|
||||
<li>Encryption/decryption operations tracked</li>
|
||||
<li>Password verification attempts recorded</li>
|
||||
<li>Key derivation operations monitored</li>
|
||||
</ul>
|
||||
|
||||
<h3>Compliance Considerations</h3>
|
||||
|
||||
<ul>
|
||||
<li><strong>GDPR</strong>: Encryption provides data protection safeguards</li>
|
||||
<li><strong>HIPAA</strong>: AES encryption meets security requirements</li>
|
||||
<li><strong>SOX</strong>: Audit trails support compliance requirements</li>
|
||||
<li><strong>PCI DSS</strong>: Strong encryption protects sensitive data</li>
|
||||
</ul>
|
||||
|
||||
<h2>Migration and Backup</h2>
|
||||
|
||||
<h3>Backup Strategies</h3>
|
||||
|
||||
<ol>
|
||||
<li><strong>Encrypted Backups</strong>: Regular backups preserve encrypted state</li>
|
||||
<li><strong>Unencrypted Exports</strong>: Export protected content during session</li>
|
||||
<li><strong>Key Management</strong>: Securely store password recovery information</li>
|
||||
<li><strong>Testing</strong>: Regularly test backup restoration procedures</li>
|
||||
</ol>
|
||||
|
||||
<h3>Migration Procedures</h3>
|
||||
|
||||
<p>When moving to new installation:</p>
|
||||
|
||||
<ol>
|
||||
<li><strong>Export Data</strong>: Export all notes including protected content</li>
|
||||
<li><strong>Backup Database</strong>: Create complete database backup</li>
|
||||
<li><strong>Transfer Files</strong>: Move exported files to new installation</li>
|
||||
<li><strong>Import Data</strong>: Import using same master password</li>
|
||||
<li><strong>Verify</strong>: Confirm all protected content accessible</li>
|
||||
</ol>
|
||||
|
||||
<p><strong>Remember</strong>: The security of protected notes ultimately depends on choosing a strong master password and following security best practices for your overall system.</p>
|
||||
|
||||
</div>
|
||||
457
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Security Best Practices.html
generated
vendored
Normal file
457
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Installation & Setup/Security/Security Best Practices.html
generated
vendored
Normal file
@@ -0,0 +1,457 @@
|
||||
<div class="note-content">
|
||||
|
||||
<h1>Security Best Practices</h1>
|
||||
|
||||
<p>This guide provides comprehensive security recommendations for deploying and maintaining a secure Trilium installation.</p>
|
||||
|
||||
<h2>Deployment Security</h2>
|
||||
|
||||
<h3>Server Configuration</h3>
|
||||
|
||||
<h4>HTTPS Deployment</h4>
|
||||
|
||||
<p><strong>Always use HTTPS in production environments:</strong></p>
|
||||
|
||||
<ol>
|
||||
<li><strong>TLS Configuration</strong>: Use TLS 1.2 or higher</li>
|
||||
<li><strong>Certificate Management</strong>: Use valid SSL certificates (Let's Encrypt recommended)</li>
|
||||
<li><strong>HSTS Headers</strong>: Enable HTTP Strict Transport Security</li>
|
||||
<li><strong>Secure Redirects</strong>: Redirect all HTTP traffic to HTTPS</li>
|
||||
</ol>
|
||||
|
||||
<p>Example Nginx configuration:</p>
|
||||
<pre><code class="language-nginx">server {
|
||||
listen 443 ssl http2;
|
||||
server_name your-trilium.example.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/private.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Frame-Options DENY;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h4>Network Security</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Firewall Configuration</strong>: Restrict access to necessary ports only</li>
|
||||
<li><strong>Port Security</strong>: Use non-standard ports if required</li>
|
||||
<li><strong>IP Restrictions</strong>: Limit access to trusted IP ranges</li>
|
||||
<li><strong>VPN Access</strong>: Consider VPN for remote access</li>
|
||||
</ol>
|
||||
|
||||
<p>Example firewall rules:</p>
|
||||
<pre><code class="language-bash"># Allow only HTTPS and SSH
|
||||
ufw allow 22/tcp
|
||||
ufw allow 443/tcp
|
||||
ufw deny 8080/tcp # Block direct access to Trilium
|
||||
ufw enable
|
||||
</code></pre>
|
||||
|
||||
<h3>Access Control</h3>
|
||||
|
||||
<h4>User Management</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Single User Model</strong>: Trilium is designed for single-user access</li>
|
||||
<li><strong>Shared Access</strong>: Use shared hosting or family sharing with caution</li>
|
||||
<li><strong>Guest Access</strong>: Disable guest access unless specifically needed</li>
|
||||
<li><strong>Admin Privileges</strong>: Run Trilium with minimal necessary privileges</li>
|
||||
</ol>
|
||||
|
||||
<h4>Authentication Hardening</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Strong Passwords</strong>: Enforce complex password requirements</li>
|
||||
<li><strong>Multi-Factor Authentication</strong>: Always enable MFA for production</li>
|
||||
<li><strong>Password Rotation</strong>: Regular password updates</li>
|
||||
<li><strong>Account Lockout</strong>: Monitor for brute force attempts</li>
|
||||
</ol>
|
||||
|
||||
<h3>Data Protection</h3>
|
||||
|
||||
<h4>Backup Security</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Encrypted Backups</strong>: Ensure backups are encrypted at rest</li>
|
||||
<li><strong>Secure Storage</strong>: Store backups in secure locations</li>
|
||||
<li><strong>Access Control</strong>: Limit backup access to authorized personnel</li>
|
||||
<li><strong>Regular Testing</strong>: Verify backup integrity regularly</li>
|
||||
</ol>
|
||||
|
||||
<p>Backup encryption example:</p>
|
||||
<pre><code class="language-bash"># Create encrypted backup
|
||||
tar czf - trilium-data/ | gpg --cipher-algo AES256 --compress-algo 1 --symmetric --output trilium-backup-$(date +%Y%m%d).tar.gz.gpg
|
||||
|
||||
# Restore encrypted backup
|
||||
gpg --decrypt trilium-backup-20240101.tar.gz.gpg | tar xzf -
|
||||
</code></pre>
|
||||
|
||||
<h4>Database Security</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>File Permissions</strong>: Restrict database file access (600 or 640)</li>
|
||||
<li><strong>Directory Security</strong>: Secure data directory permissions</li>
|
||||
<li><strong>Regular Monitoring</strong>: Monitor for unauthorized access attempts</li>
|
||||
<li><strong>Integrity Checks</strong>: Verify database integrity regularly</li>
|
||||
</ol>
|
||||
|
||||
<pre><code class="language-bash"># Secure file permissions
|
||||
chmod 600 /path/to/trilium/data/document.db
|
||||
chmod 700 /path/to/trilium/data/
|
||||
chown trilium:trilium /path/to/trilium/data/ -R
|
||||
</code></pre>
|
||||
|
||||
<h2>Application Security</h2>
|
||||
|
||||
<h3>Configuration Hardening</h3>
|
||||
|
||||
<h4>Security Headers</h4>
|
||||
|
||||
<p>Configure security headers for web protection:</p>
|
||||
|
||||
<pre><code class="language-typescript">// Security headers configuration
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('X-Frame-Options', 'DENY');
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
res.setHeader('Content-Security-Policy',
|
||||
"default-src 'self'; " +
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
|
||||
"style-src 'self' 'unsafe-inline';"
|
||||
);
|
||||
next();
|
||||
});
|
||||
</code></pre>
|
||||
|
||||
<h4>Session Security</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Session Timeout</strong>: Configure appropriate timeout values</li>
|
||||
<li><strong>Secure Cookies</strong>: Enable secure flag for all cookies</li>
|
||||
<li><strong>Session Regeneration</strong>: Regenerate session IDs after login</li>
|
||||
<li><strong>CSRF Protection</strong>: Enable and properly configure CSRF protection</li>
|
||||
</ol>
|
||||
|
||||
<p>Example session configuration:</p>
|
||||
<pre><code class="language-javascript">// Secure session configuration
|
||||
{
|
||||
cookie: {
|
||||
secure: true, // HTTPS only
|
||||
httpOnly: true, // Prevent XSS
|
||||
maxAge: 30 * 60 * 1000, // 30 minutes
|
||||
sameSite: 'strict' // CSRF protection
|
||||
},
|
||||
rolling: true, // Reset timeout on activity
|
||||
resave: false, // Don't save unchanged sessions
|
||||
saveUninitialized: false // Don't save empty sessions
|
||||
}
|
||||
</code></pre>
|
||||
|
||||
<h3>Input Validation</h3>
|
||||
|
||||
<h4>Content Security</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>HTML Sanitization</strong>: Properly sanitize user-generated content</li>
|
||||
<li><strong>File Upload Security</strong>: Validate file types and sizes</li>
|
||||
<li><strong>Script Execution</strong>: Control custom script execution</li>
|
||||
<li><strong>SQL Injection Prevention</strong>: Use parameterized queries</li>
|
||||
</ol>
|
||||
|
||||
<h4>API Security</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Rate Limiting</strong>: Implement API rate limiting</li>
|
||||
<li><strong>Input Validation</strong>: Validate all API inputs</li>
|
||||
<li><strong>Authentication</strong>: Require authentication for sensitive operations</li>
|
||||
<li><strong>Authorization</strong>: Implement proper access controls</li>
|
||||
</ol>
|
||||
|
||||
<p>Example rate limiting:</p>
|
||||
<pre><code class="language-typescript">import rateLimit from 'express-rate-limit';
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
message: 'Too many requests, please try again later.'
|
||||
});
|
||||
|
||||
app.use('/api/', limiter);
|
||||
</code></pre>
|
||||
|
||||
<h2>Operational Security</h2>
|
||||
|
||||
<h3>Monitoring and Logging</h3>
|
||||
|
||||
<h4>Security Logging</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Authentication Events</strong>: Log all login attempts and failures</li>
|
||||
<li><strong>Authorization Events</strong>: Track access to protected resources</li>
|
||||
<li><strong>Data Access</strong>: Monitor sensitive data access patterns</li>
|
||||
<li><strong>System Events</strong>: Log system-level security events</li>
|
||||
</ol>
|
||||
|
||||
<p>Example log monitoring:</p>
|
||||
<pre><code class="language-bash"># Monitor failed login attempts
|
||||
tail -f /var/log/trilium/security.log | grep "Failed login"
|
||||
|
||||
# Alert on multiple failures
|
||||
tail -f /var/log/trilium/security.log | awk '/Failed login/ {count++} count>=5 {print "Alert: Multiple failed logins"; count=0}'
|
||||
</code></pre>
|
||||
|
||||
<h4>Security Metrics</h4>
|
||||
|
||||
<p>Monitor key security metrics:</p>
|
||||
<ul>
|
||||
<li>Failed authentication attempts</li>
|
||||
<li>Session anomalies</li>
|
||||
<li>Unusual access patterns</li>
|
||||
<li>Data export activities</li>
|
||||
<li>Configuration changes</li>
|
||||
</ul>
|
||||
|
||||
<h3>Incident Response</h3>
|
||||
|
||||
<h4>Preparation</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Incident Response Plan</strong>: Develop and document procedures</li>
|
||||
<li><strong>Contact Lists</strong>: Maintain emergency contact information</li>
|
||||
<li><strong>Backup Procedures</strong>: Ensure rapid recovery capabilities</li>
|
||||
<li><strong>Communication Plans</strong>: Prepare user notification procedures</li>
|
||||
</ol>
|
||||
|
||||
<h4>Detection and Response</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Automated Monitoring</strong>: Implement automated threat detection</li>
|
||||
<li><strong>Alert Systems</strong>: Configure appropriate alerting thresholds</li>
|
||||
<li><strong>Response Procedures</strong>: Define step-by-step response actions</li>
|
||||
<li><strong>Forensic Preparation</strong>: Preserve evidence for analysis</li>
|
||||
</ol>
|
||||
|
||||
<p>Example incident response checklist:</p>
|
||||
<pre><code>□ Identify and isolate affected systems
|
||||
□ Preserve logs and evidence
|
||||
□ Assess scope and impact
|
||||
□ Notify relevant stakeholders
|
||||
□ Implement containment measures
|
||||
□ Begin recovery procedures
|
||||
□ Document lessons learned
|
||||
□ Update security controls
|
||||
</code></pre>
|
||||
|
||||
<h3>Regular Maintenance</h3>
|
||||
|
||||
<h4>Security Updates</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Application Updates</strong>: Keep Trilium updated to latest version</li>
|
||||
<li><strong>Dependency Updates</strong>: Regularly update dependencies</li>
|
||||
<li><strong>System Updates</strong>: Maintain OS and security patches</li>
|
||||
<li><strong>Certificate Renewal</strong>: Monitor and renew SSL certificates</li>
|
||||
</ol>
|
||||
|
||||
<h4>Security Audits</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Regular Reviews</strong>: Conduct periodic security assessments</li>
|
||||
<li><strong>Penetration Testing</strong>: Perform authorized security testing</li>
|
||||
<li><strong>Configuration Audits</strong>: Review security configurations</li>
|
||||
<li><strong>Access Reviews</strong>: Audit user access and permissions</li>
|
||||
</ol>
|
||||
|
||||
<p>Automated update checking:</p>
|
||||
<pre><code class="language-bash">#!/bin/bash
|
||||
# Check for Trilium updates
|
||||
CURRENT_VERSION=$(curl -s https://api.github.com/repos/TriliumNext/Trilium/releases/latest | grep tag_name | cut -d'"' -f4)
|
||||
INSTALLED_VERSION=$(grep version /opt/trilium/package.json | cut -d'"' -f4)
|
||||
|
||||
if [ "$CURRENT_VERSION" != "v$INSTALLED_VERSION" ]; then
|
||||
echo "Update available: $CURRENT_VERSION (current: $INSTALLED_VERSION)"
|
||||
# Add notification logic here
|
||||
fi
|
||||
</code></pre>
|
||||
|
||||
<h2>Threat Mitigation</h2>
|
||||
|
||||
<h3>Common Attack Vectors</h3>
|
||||
|
||||
<h4>Web Application Attacks</h4>
|
||||
|
||||
<p><strong>Cross-Site Scripting (XSS)</strong>:</p>
|
||||
<ul>
|
||||
<li>Content Security Policy headers</li>
|
||||
<li>Input sanitization</li>
|
||||
<li>Output encoding</li>
|
||||
<li>Secure cookie flags</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Cross-Site Request Forgery (CSRF)</strong>:</p>
|
||||
<ul>
|
||||
<li>CSRF token validation</li>
|
||||
<li>SameSite cookie attributes</li>
|
||||
<li>Referrer validation</li>
|
||||
<li>Double-submit cookies</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Session Hijacking</strong>:</p>
|
||||
<ul>
|
||||
<li>Secure session management</li>
|
||||
<li>HTTPS enforcement</li>
|
||||
<li>Session timeout controls</li>
|
||||
<li>Session regeneration</li>
|
||||
</ul>
|
||||
|
||||
<h4>Infrastructure Attacks</h4>
|
||||
|
||||
<p><strong>Denial of Service (DoS)</strong>:</p>
|
||||
<ul>
|
||||
<li>Rate limiting</li>
|
||||
<li>Request size limits</li>
|
||||
<li>Connection throttling</li>
|
||||
<li>Resource monitoring</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Data Breaches</strong>:</p>
|
||||
<ul>
|
||||
<li>Encryption at rest</li>
|
||||
<li>Access controls</li>
|
||||
<li>Audit logging</li>
|
||||
<li>Regular backups</li>
|
||||
</ul>
|
||||
|
||||
<h3>Security Controls Implementation</h3>
|
||||
|
||||
<h4>Preventive Controls</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Authentication</strong>: Strong password policies and MFA</li>
|
||||
<li><strong>Authorization</strong>: Proper access controls and permissions</li>
|
||||
<li><strong>Encryption</strong>: Data encryption at rest and in transit</li>
|
||||
<li><strong>Input Validation</strong>: Comprehensive input sanitization</li>
|
||||
</ol>
|
||||
|
||||
<h4>Detective Controls</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Logging</strong>: Comprehensive security logging</li>
|
||||
<li><strong>Monitoring</strong>: Real-time security monitoring</li>
|
||||
<li><strong>Alerting</strong>: Automated threat detection</li>
|
||||
<li><strong>Auditing</strong>: Regular security audits</li>
|
||||
</ol>
|
||||
|
||||
<h4>Responsive Controls</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Incident Response</strong>: Documented response procedures</li>
|
||||
<li><strong>Backup and Recovery</strong>: Reliable backup systems</li>
|
||||
<li><strong>Isolation</strong>: Network segmentation capabilities</li>
|
||||
<li><strong>Communication</strong>: Stakeholder notification systems</li>
|
||||
</ol>
|
||||
|
||||
<h2>Compliance Considerations</h2>
|
||||
|
||||
<h3>Data Protection Regulations</h3>
|
||||
|
||||
<h4>GDPR Compliance</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Data Minimization</strong>: Only collect necessary data</li>
|
||||
<li><strong>Consent Management</strong>: Obtain proper user consent</li>
|
||||
<li><strong>Right to Erasure</strong>: Implement data deletion capabilities</li>
|
||||
<li><strong>Data Portability</strong>: Enable data export functionality</li>
|
||||
<li><strong>Privacy by Design</strong>: Build privacy into system design</li>
|
||||
</ol>
|
||||
|
||||
<h4>HIPAA Compliance (Healthcare)</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Access Controls</strong>: Implement user authentication and authorization</li>
|
||||
<li><strong>Audit Logs</strong>: Maintain comprehensive audit trails</li>
|
||||
<li><strong>Encryption</strong>: Encrypt data at rest and in transit</li>
|
||||
<li><strong>Risk Assessment</strong>: Conduct regular risk assessments</li>
|
||||
<li><strong>Business Associate Agreements</strong>: Ensure proper agreements</li>
|
||||
</ol>
|
||||
|
||||
<h3>Industry Standards</h3>
|
||||
|
||||
<h4>ISO 27001</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Information Security Management</strong>: Implement ISMS</li>
|
||||
<li><strong>Risk Management</strong>: Conduct regular risk assessments</li>
|
||||
<li><strong>Security Controls</strong>: Implement appropriate controls</li>
|
||||
<li><strong>Continuous Improvement</strong>: Regular reviews and updates</li>
|
||||
</ol>
|
||||
|
||||
<h4>SOC 2</h4>
|
||||
|
||||
<ol>
|
||||
<li><strong>Security</strong>: Implement comprehensive security controls</li>
|
||||
<li><strong>Availability</strong>: Ensure system availability and reliability</li>
|
||||
<li><strong>Processing Integrity</strong>: Maintain data processing integrity</li>
|
||||
<li><strong>Confidentiality</strong>: Protect sensitive information</li>
|
||||
<li><strong>Privacy</strong>: Implement privacy protection measures</li>
|
||||
</ol>
|
||||
|
||||
<h2>Security Assessment Checklist</h2>
|
||||
|
||||
<h3>Infrastructure Security</h3>
|
||||
<ul>
|
||||
<li>☐ HTTPS configured with valid certificates</li>
|
||||
<li>☐ Firewall rules properly configured</li>
|
||||
<li>☐ Network access controls implemented</li>
|
||||
<li>☐ System updates current</li>
|
||||
<li>☐ Backup procedures tested</li>
|
||||
<li>☐ Monitoring systems active</li>
|
||||
</ul>
|
||||
|
||||
<h3>Application Security</h3>
|
||||
<ul>
|
||||
<li>☐ Strong authentication configured</li>
|
||||
<li>☐ Multi-factor authentication enabled</li>
|
||||
<li>☐ Session security properly configured</li>
|
||||
<li>☐ CSRF protection enabled</li>
|
||||
<li>☐ Security headers configured</li>
|
||||
<li>☐ Input validation implemented</li>
|
||||
</ul>
|
||||
|
||||
<h3>Data Security</h3>
|
||||
<ul>
|
||||
<li>☐ Database properly secured</li>
|
||||
<li>☐ File permissions configured</li>
|
||||
<li>☐ Encryption properly implemented</li>
|
||||
<li>☐ Backup encryption verified</li>
|
||||
<li>☐ Access controls tested</li>
|
||||
<li>☐ Data retention policies defined</li>
|
||||
</ul>
|
||||
|
||||
<h3>Operational Security</h3>
|
||||
<ul>
|
||||
<li>☐ Security logging enabled</li>
|
||||
<li>☐ Monitoring systems configured</li>
|
||||
<li>☐ Incident response plan documented</li>
|
||||
<li>☐ Security training completed</li>
|
||||
<li>☐ Regular audits scheduled</li>
|
||||
<li>☐ Update procedures documented</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Remember</strong>: Security is an ongoing process, not a one-time configuration. Regularly review and update your security posture to address evolving threats and requirements.</p>
|
||||
|
||||
</div>
|
||||
@@ -134,6 +134,7 @@ docker run -d --name trilium -p 8080:8080 --user $(id -u):$(id -g) -v ~/trilium-
|
||||
<li><code>TRILIUM_DATA_DIR</code>: Path to the data directory inside the container
|
||||
(default: <code>/home/node/trilium-data</code>)</li>
|
||||
</ul>
|
||||
<p>For a complete list of configuration environment variables (network settings, authentication, sync, etc.), see <a class="reference-link" href="#root/_help_Gzjqa934BdH4">Configuration (config.ini or environment variables)</a>.</p>
|
||||
<h3>Volume Permissions</h3>
|
||||
<p>If you encounter permission issues with the data volume, ensure that:</p>
|
||||
<ol>
|
||||
|
||||
@@ -49,7 +49,12 @@ class="admonition warning">
|
||||
the <code>config.ini</code> file (check <a class="reference-link" href="#root/_help_Gzjqa934BdH4">Configuration (config.ini or environment variables)</a> for
|
||||
more information).
|
||||
<ol>
|
||||
<li>You can also setup through environment variables (<code>TRILIUM_OAUTH_BASE_URL</code>, <code>TRILIUM_OAUTH_CLIENT_ID</code> and <code>TRILIUM_OAUTH_CLIENT_SECRET</code>).</li>
|
||||
<li>You can also setup through environment variables:
|
||||
<ul>
|
||||
<li>Standard: <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHBASEURL</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTID</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHCLIENTSECRET</code></li>
|
||||
<li>Legacy (still supported): <code>TRILIUM_OAUTH_BASE_URL</code>, <code>TRILIUM_OAUTH_CLIENT_ID</code>, <code>TRILIUM_OAUTH_CLIENT_SECRET</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><code>oauthBaseUrl</code> should be the link of your Trilium instance server,
|
||||
for example, <code>https://<your-trilium-domain></code>.</li>
|
||||
</ol>
|
||||
@@ -64,8 +69,12 @@ class="admonition warning">
|
||||
<p>The default OAuth issuer is Google. To use other services such as Authentik
|
||||
or Auth0, you can configure the settings via <code>oauthIssuerBaseUrl</code>, <code>oauthIssuerName</code>,
|
||||
and <code>oauthIssuerIcon</code> in the <code>config.ini</code> file. Alternatively,
|
||||
these values can be set using environment variables: <code>TRILIUM_OAUTH_ISSUER_BASE_URL</code>, <code>TRILIUM_OAUTH_ISSUER_NAME</code>,
|
||||
and <code>TRILIUM_OAUTH_ISSUER_ICON</code>. <code>oauthIssuerName</code> and <code>oauthIssuerIcon</code> are
|
||||
these values can be set using environment variables:
|
||||
<ul>
|
||||
<li>Standard: <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERBASEURL</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERNAME</code>, <code>TRILIUM_MULTIFACTORAUTHENTICATION_OAUTHISSUERICON</code></li>
|
||||
<li>Legacy (still supported): <code>TRILIUM_OAUTH_ISSUER_BASE_URL</code>, <code>TRILIUM_OAUTH_ISSUER_NAME</code>, <code>TRILIUM_OAUTH_ISSUER_ICON</code></li>
|
||||
</ul>
|
||||
<code>oauthIssuerName</code> and <code>oauthIssuerIcon</code> are
|
||||
required for displaying correct issuer information at the Login page.</p>
|
||||
</aside>
|
||||
<h4>Authentik</h4>
|
||||
|
||||
@@ -26,7 +26,10 @@ https=true
|
||||
certPath=/[username]/.acme.sh/[hostname]/fullchain.cer
|
||||
keyPath=/[username]/.acme.sh/[hostname]/example.com.key</code></pre>
|
||||
<p>You can also review the <a href="#root/_help_Gzjqa934BdH4">configuration</a> file
|
||||
to provide all <code>config.ini</code> values as environment variables instead.</p>
|
||||
to provide all <code>config.ini</code> values as environment variables instead. For example, you can configure TLS using environment variables:</p>
|
||||
<pre><code class="language-bash">export TRILIUM_NETWORK_HTTPS=true
|
||||
export TRILIUM_NETWORK_CERTPATH=/path/to/cert.pem
|
||||
export TRILIUM_NETWORK_KEYPATH=/path/to/key.pem</code></pre>
|
||||
<p>The above example shows how this is set up in an environment where the
|
||||
certificate was generated using Let's Encrypt's ACME utility. Your paths
|
||||
may differ. For Docker installations, ensure these paths are within a volume
|
||||
|
||||
@@ -6,7 +6,7 @@ info:
|
||||
contact:
|
||||
name: zadam
|
||||
email: zadam.apps@gmail.com
|
||||
url: https://github.com/zadam/trilium
|
||||
url: https://github.com/TriliumNext/Trilium
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
@@ -50,7 +50,7 @@ function filterUrlValue(value: string) {
|
||||
}
|
||||
|
||||
function buildRewardMap(note: BNote) {
|
||||
// Need to use Map instead of object: https://github.com/zadam/trilium/issues/1895
|
||||
// Need to use Map instead of object: https://github.com/TriliumNext/Trilium/issues/1895
|
||||
const map = new Map();
|
||||
|
||||
function addToRewardMap(text: string | undefined | null, rewardFactor: number) {
|
||||
@@ -188,7 +188,7 @@ function buildDateLimits(baseNote: BNote): DateLimits {
|
||||
};
|
||||
}
|
||||
|
||||
// Need to use Map instead of object: https://github.com/zadam/trilium/issues/1895
|
||||
// Need to use Map instead of object: https://github.com/TriliumNext/Trilium/issues/1895
|
||||
const wordCache = new Map();
|
||||
|
||||
const WORD_BLACKLIST = [
|
||||
|
||||
@@ -17,7 +17,7 @@ async function getBackendLog() {
|
||||
} catch (e) {
|
||||
const isErrorInstance = e instanceof Error;
|
||||
|
||||
// most probably the log file does not exist yet - https://github.com/zadam/trilium/issues/1977
|
||||
// most probably the log file does not exist yet - https://github.com/TriliumNext/Trilium/issues/1977
|
||||
if (isErrorInstance && "code" in e && e.code === "ENOENT") {
|
||||
log.error(e);
|
||||
return t("backend_log.log-does-not-exist", { fileName });
|
||||
|
||||
@@ -39,7 +39,7 @@ function processContent(content: Buffer | string | null, isProtected: boolean, i
|
||||
if (isStringContent) {
|
||||
return content === null ? "" : content.toString("utf-8");
|
||||
} else {
|
||||
// see https://github.com/zadam/trilium/issues/3523
|
||||
// see https://github.com/TriliumNext/Trilium/issues/3523
|
||||
// IIRC a zero-sized buffer can be returned as null from the database
|
||||
if (content === null) {
|
||||
// this will force de/encryption
|
||||
|
||||
@@ -515,7 +515,7 @@ class ConsistencyChecks {
|
||||
);
|
||||
|
||||
if (sqlInit.getDbSize() < 500000) {
|
||||
// querying for "content IS NULL" is expensive since content is not indexed. See e.g. https://github.com/zadam/trilium/issues/2887
|
||||
// querying for "content IS NULL" is expensive since content is not indexed. See e.g. https://github.com/TriliumNext/Trilium/issues/2887
|
||||
|
||||
this.findAndFixIssues(
|
||||
`
|
||||
|
||||
@@ -60,7 +60,7 @@ function decrypt(key: Buffer, cipherText: string | Buffer): Buffer | false | nul
|
||||
try {
|
||||
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64");
|
||||
|
||||
// old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/trilium/issues/3017
|
||||
// old encrypted data can have IV of length 13, see some details here: https://github.com/TriliumNext/Trilium/issues/3017
|
||||
const ivLength = cipherTextBufferWithIv.length % 16 === 0 ? 16 : 13;
|
||||
|
||||
const iv = cipherTextBufferWithIv.slice(0, ivLength);
|
||||
@@ -82,7 +82,7 @@ function decrypt(key: Buffer, cipherText: string | Buffer): Buffer | false | nul
|
||||
|
||||
return payload;
|
||||
} catch (e: any) {
|
||||
// recovery from https://github.com/zadam/trilium/issues/510
|
||||
// recovery from https://github.com/TriliumNext/Trilium/issues/510
|
||||
if (e.message?.includes("WRONG_FINAL_BLOCK_LENGTH") || e.message?.includes("wrong final block length")) {
|
||||
log.info("Caught WRONG_FINAL_BLOCK_LENGTH, returning cipherText instead");
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ function parseAuthToken(auth: string | undefined) {
|
||||
if (auth.startsWith("Basic ")) {
|
||||
// allow also basic auth format for systems which allow this type of authentication
|
||||
// expect ETAPI token in the password field, require "etapi" username
|
||||
// https://github.com/zadam/trilium/issues/3181
|
||||
// https://github.com/TriliumNext/Trilium/issues/3181
|
||||
const basicAuthStr = fromBase64(auth.substring(6)).toString("utf-8");
|
||||
const basicAuthChunks = basicAuthStr.split(":");
|
||||
|
||||
|
||||
@@ -332,7 +332,7 @@ async function exportToZip(taskContext: TaskContext, branch: BBranch, format: "h
|
||||
const cssUrl = `${"../".repeat(noteMeta.notePath.length - 1)}style.css`;
|
||||
const htmlTitle = escapeHtml(title);
|
||||
|
||||
// <base> element will make sure external links are openable - https://github.com/zadam/trilium/issues/1289#issuecomment-704066809
|
||||
// <base> element will make sure external links are openable - https://github.com/TriliumNext/Trilium/issues/1289#issuecomment-704066809
|
||||
content = `<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
@@ -22,7 +22,7 @@ function sanitize(dirtyHtml: string) {
|
||||
return dirtyHtml;
|
||||
}
|
||||
|
||||
// avoid H1 per https://github.com/zadam/trilium/issues/1552
|
||||
// avoid H1 per https://github.com/TriliumNext/Trilium/issues/1552
|
||||
// demote H1, and if that conflicts with existing H2, demote that, etc
|
||||
const transformTags: Record<string, string> = {};
|
||||
const lowercasedHtml = dirtyHtml.toLowerCase();
|
||||
|
||||
@@ -86,7 +86,7 @@ function saveImage(parentNoteId: string, uploadBuffer: Buffer, originalName: str
|
||||
log.info(`Saving image ${originalName} into parent ${parentNoteId}`);
|
||||
|
||||
if (trimFilename && originalName.length > 40) {
|
||||
// https://github.com/zadam/trilium/issues/2307
|
||||
// https://github.com/TriliumNext/Trilium/issues/2307
|
||||
originalName = "image";
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ function saveImageToAttachment(noteId: string, uploadBuffer: Buffer, originalNam
|
||||
log.info(`Saving image '${originalName}' as attachment into note '${noteId}'`);
|
||||
|
||||
if (trimFilename && originalName.length > 40) {
|
||||
// https://github.com/zadam/trilium/issues/2307
|
||||
// https://github.com/TriliumNext/Trilium/issues/2307
|
||||
originalName = "image";
|
||||
}
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ function importEnex(taskContext: TaskContext, file: File, parentNote: BNote): Pr
|
||||
if (currentTag === "data") {
|
||||
text = text.replace(/\s/g, "");
|
||||
|
||||
// resource can be chunked into multiple events: https://github.com/zadam/trilium/issues/3424
|
||||
// resource can be chunked into multiple events: https://github.com/TriliumNext/Trilium/issues/3424
|
||||
// it would probably make sense to do this in a more global way since it can in theory affect any field,
|
||||
// not just data
|
||||
resource.content = (resource.content || "") + text;
|
||||
|
||||
@@ -53,7 +53,7 @@ async function importOpml(taskContext: TaskContext, fileBuffer: string | Buffer,
|
||||
content = toHtml(outline.$.text);
|
||||
|
||||
if (!title || !title.trim()) {
|
||||
// https://github.com/zadam/trilium/issues/1862
|
||||
// https://github.com/TriliumNext/Trilium/issues/1862
|
||||
title = outline.$.text;
|
||||
content = "";
|
||||
}
|
||||
|
||||
@@ -477,7 +477,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
|
||||
if (note) {
|
||||
// only skeleton was created because of altered order of cloned notes in ZIP, we need to update
|
||||
// https://github.com/zadam/trilium/issues/2440
|
||||
// https://github.com/TriliumNext/Trilium/issues/2440
|
||||
if (note.type === undefined) {
|
||||
note.type = type;
|
||||
note.mime = mime;
|
||||
|
||||
@@ -19,7 +19,7 @@ function getDefaultKeyboardActions() {
|
||||
actionName: "backInNoteHistory",
|
||||
friendlyName: t("keyboard_action_names.back-in-note-history"),
|
||||
iconClass: "bx bxs-chevron-left",
|
||||
// Mac has a different history navigation shortcuts - https://github.com/zadam/trilium/issues/376
|
||||
// Mac has a different history navigation shortcuts - https://github.com/TriliumNext/Trilium/issues/376
|
||||
defaultShortcuts: isMac ? ["CommandOrControl+Left"] : ["Alt+Left"],
|
||||
description: t("keyboard_actions.back-in-note-history"),
|
||||
scope: "window"
|
||||
@@ -28,7 +28,7 @@ function getDefaultKeyboardActions() {
|
||||
actionName: "forwardInNoteHistory",
|
||||
friendlyName: t("keyboard_action_names.forward-in-note-history"),
|
||||
iconClass: "bx bxs-chevron-right",
|
||||
// Mac has a different history navigation shortcuts - https://github.com/zadam/trilium/issues/376
|
||||
// Mac has a different history navigation shortcuts - https://github.com/TriliumNext/Trilium/issues/376
|
||||
defaultShortcuts: isMac ? ["CommandOrControl+Right"] : ["Alt+Right"],
|
||||
description: t("keyboard_actions.forward-in-note-history"),
|
||||
scope: "window"
|
||||
|
||||
@@ -330,7 +330,7 @@ class NoteContentFulltextExp extends Expression {
|
||||
|
||||
|
||||
stripTags(content: string) {
|
||||
// we want to allow link to preserve URLs: https://github.com/zadam/trilium/issues/2412
|
||||
// we want to allow link to preserve URLs: https://github.com/TriliumNext/Trilium/issues/2412
|
||||
// we want to insert space in place of block tags (because they imply text separation)
|
||||
// but we don't want to insert text for typical formatting inline tags which can occur within one word
|
||||
const linkTag = "a";
|
||||
|
||||
@@ -185,7 +185,7 @@ async function pullChanges(syncContext: SyncContext) {
|
||||
break;
|
||||
} else {
|
||||
try {
|
||||
// https://github.com/zadam/trilium/issues/4310
|
||||
// https://github.com/TriliumNext/Trilium/issues/4310
|
||||
const sizeInKb = Math.round(JSON.stringify(resp).length / 1024);
|
||||
|
||||
log.info(
|
||||
|
||||
@@ -101,7 +101,7 @@ Stack: ${message.stack}`);
|
||||
});
|
||||
|
||||
webSocketServer.on("error", (error) => {
|
||||
// https://github.com/zadam/trilium/issues/3374#issuecomment-1341053765
|
||||
// https://github.com/TriliumNext/Trilium/issues/3374#issuecomment-1341053765
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export default async function startTriliumServer() {
|
||||
// for perf. issues it's good to know the rough configuration
|
||||
const cpuInfos = (await import("os")).cpus();
|
||||
if (cpuInfos && cpuInfos[0] !== undefined) {
|
||||
// https://github.com/zadam/trilium/pull/3957
|
||||
// https://github.com/TriliumNext/Trilium/pull/3957
|
||||
const cpuModel = (cpuInfos[0].model || "").trimEnd();
|
||||
log.info(`CPU model: ${cpuModel}, logical cores: ${cpuInfos.length}, freq: ${cpuInfos[0].speed} Mhz`);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
**Trilium is in maintenance mode and Web Clipper is not likely to get new releases.**
|
||||
|
||||
Trilium Web Clipper is a web browser extension which allows user to clip text, screenshots, whole pages and short notes and save them directly to [Trilium Notes](https://github.com/zadam/trilium).
|
||||
Trilium Web Clipper is a web browser extension which allows user to clip text, screenshots, whole pages and short notes and save them directly to [Trilium Notes](https://github.com/TriliumNext/Trilium).
|
||||
|
||||
For more details, see the [wiki page](https://github.com/zadam/trilium/wiki/Web-clipper).
|
||||
For more details, see the [wiki page](https://github.com/TriliumNext/Trilium/wiki/Web-clipper).
|
||||
|
||||
## Keyboard shortcuts
|
||||
Keyboard shortcuts are available for most functions:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Trilium Web Clipper (dev)",
|
||||
"version": "1.0.1",
|
||||
"description": "Save web clippings to Trilium Notes.",
|
||||
"homepage_url": "https://github.com/zadam/trilium-web-clipper",
|
||||
"homepage_url": "https://github.com/TriliumNext/Trilium-web-clipper",
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
|
||||
"icons": {
|
||||
"32": "icons/32.png",
|
||||
|
||||
@@ -98,7 +98,7 @@ async function saveLinkWithNote() {
|
||||
$("#save-button").on("click", saveLinkWithNote);
|
||||
|
||||
$("#show-help-button").on("click", () => {
|
||||
window.open("https://github.com/zadam/trilium/wiki/Web-clipper", '_blank');
|
||||
window.open("https://github.com/TriliumNext/Trilium/wiki/Web-clipper", '_blank');
|
||||
});
|
||||
|
||||
function escapeHtml(string) {
|
||||
|
||||
6227
docs/Developer Guide/!!!meta.json
vendored
6227
docs/Developer Guide/!!!meta.json
vendored
File diff suppressed because it is too large
Load Diff
2275
docs/Developer Guide/API Documentation/API Client Libraries.md
vendored
Normal file
2275
docs/Developer Guide/API Documentation/API Client Libraries.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1898
docs/Developer Guide/API Documentation/ETAPI Complete Guide.md
vendored
Normal file
1898
docs/Developer Guide/API Documentation/ETAPI Complete Guide.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1867
docs/Developer Guide/API Documentation/Internal API Reference.md
vendored
Normal file
1867
docs/Developer Guide/API Documentation/Internal API Reference.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1842
docs/Developer Guide/API Documentation/Script API Cookbook.md
vendored
Normal file
1842
docs/Developer Guide/API Documentation/Script API Cookbook.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1792
docs/Developer Guide/API Documentation/WebSocket API.md
vendored
Normal file
1792
docs/Developer Guide/API Documentation/WebSocket API.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
899
docs/Developer Guide/Architecture/API-Architecture.md
vendored
Normal file
899
docs/Developer Guide/Architecture/API-Architecture.md
vendored
Normal file
@@ -0,0 +1,899 @@
|
||||
# API Architecture
|
||||
|
||||
Trilium provides multiple API layers for different use cases: Internal API for frontend-backend communication, ETAPI for external integrations, and WebSocket for real-time synchronization. This document details each API layer's design, usage, and best practices.
|
||||
|
||||
## API Layers Overview
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Client Applications"
|
||||
WebApp[Web Application]
|
||||
Desktop[Desktop App]
|
||||
Mobile[Mobile App]
|
||||
External[External Apps]
|
||||
Scripts[User Scripts]
|
||||
end
|
||||
|
||||
subgraph "API Layers"
|
||||
Internal[Internal API<br/>REST + WebSocket]
|
||||
ETAPI[ETAPI<br/>External API]
|
||||
WS[WebSocket<br/>Real-time Sync]
|
||||
end
|
||||
|
||||
subgraph "Backend Services"
|
||||
Routes[Route Handlers]
|
||||
Services[Business Logic]
|
||||
Becca[Becca Cache]
|
||||
DB[(Database)]
|
||||
end
|
||||
|
||||
WebApp --> Internal
|
||||
Desktop --> Internal
|
||||
Mobile --> Internal
|
||||
External --> ETAPI
|
||||
Scripts --> ETAPI
|
||||
|
||||
Internal --> Routes
|
||||
ETAPI --> Routes
|
||||
WS --> Services
|
||||
|
||||
Routes --> Services
|
||||
Services --> Becca
|
||||
Becca --> DB
|
||||
|
||||
style Internal fill:#e3f2fd
|
||||
style ETAPI fill:#fff3e0
|
||||
style WS fill:#f3e5f5
|
||||
```
|
||||
|
||||
## Internal API
|
||||
|
||||
**Location**: `/apps/server/src/routes/api/`
|
||||
|
||||
The Internal API handles communication between Trilium's frontend and backend, providing full access to application functionality.
|
||||
|
||||
### Architecture
|
||||
|
||||
```typescript
|
||||
// Route structure
|
||||
/api/
|
||||
├── notes.ts // Note operations
|
||||
├── branches.ts // Branch management
|
||||
├── attributes.ts // Attribute operations
|
||||
├── tree.ts // Tree structure
|
||||
├── search.ts // Search functionality
|
||||
├── sync.ts // Synchronization
|
||||
├── options.ts // Configuration
|
||||
└── special.ts // Special operations
|
||||
```
|
||||
|
||||
### Request/Response Pattern
|
||||
|
||||
```typescript
|
||||
// Typical API endpoint structure
|
||||
router.get('/notes/:noteId', (req, res) => {
|
||||
const note = becca.getNote(req.params.noteId);
|
||||
|
||||
if (!note) {
|
||||
return res.status(404).json({
|
||||
error: 'Note not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json(note.getPojo());
|
||||
});
|
||||
|
||||
router.put('/notes/:noteId', (req, res) => {
|
||||
const note = becca.getNoteOrThrow(req.params.noteId);
|
||||
|
||||
note.title = req.body.title;
|
||||
note.content = req.body.content;
|
||||
note.save();
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
```
|
||||
|
||||
### Key Endpoints
|
||||
|
||||
#### Note Operations
|
||||
|
||||
```typescript
|
||||
// Get note with content
|
||||
GET /api/notes/:noteId
|
||||
Response: {
|
||||
noteId: string,
|
||||
title: string,
|
||||
type: string,
|
||||
content: string,
|
||||
dateCreated: string,
|
||||
dateModified: string
|
||||
}
|
||||
|
||||
// Update note
|
||||
PUT /api/notes/:noteId
|
||||
Body: {
|
||||
title?: string,
|
||||
content?: string,
|
||||
type?: string,
|
||||
mime?: string
|
||||
}
|
||||
|
||||
// Create note
|
||||
POST /api/notes/:parentNoteId/children
|
||||
Body: {
|
||||
title: string,
|
||||
type: string,
|
||||
content?: string,
|
||||
position?: number
|
||||
}
|
||||
|
||||
// Delete note
|
||||
DELETE /api/notes/:noteId
|
||||
```
|
||||
|
||||
#### Tree Operations
|
||||
|
||||
```typescript
|
||||
// Get tree structure
|
||||
GET /api/tree
|
||||
Query: {
|
||||
subTreeNoteId?: string,
|
||||
includeAttributes?: boolean
|
||||
}
|
||||
Response: {
|
||||
notes: FNoteRow[],
|
||||
branches: FBranchRow[],
|
||||
attributes: FAttributeRow[]
|
||||
}
|
||||
|
||||
// Move branch
|
||||
PUT /api/branches/:branchId/move
|
||||
Body: {
|
||||
parentNoteId: string,
|
||||
position: number
|
||||
}
|
||||
```
|
||||
|
||||
#### Search Operations
|
||||
|
||||
```typescript
|
||||
// Execute search
|
||||
GET /api/search
|
||||
Query: {
|
||||
query: string,
|
||||
fastSearch?: boolean,
|
||||
includeArchivedNotes?: boolean,
|
||||
ancestorNoteId?: string
|
||||
}
|
||||
Response: {
|
||||
results: Array<{
|
||||
noteId: string,
|
||||
title: string,
|
||||
path: string,
|
||||
score: number
|
||||
}>
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication & Security
|
||||
|
||||
```typescript
|
||||
// CSRF protection
|
||||
app.use(csrfMiddleware);
|
||||
|
||||
// Session authentication
|
||||
router.use((req, res, next) => {
|
||||
if (!req.session.loggedIn) {
|
||||
return res.status(401).json({
|
||||
error: 'Not authenticated'
|
||||
});
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Protected note access
|
||||
router.get('/notes/:noteId', (req, res) => {
|
||||
const note = becca.getNote(req.params.noteId);
|
||||
|
||||
if (note.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
|
||||
return res.status(403).json({
|
||||
error: 'Protected session required'
|
||||
});
|
||||
}
|
||||
|
||||
res.json(note.getPojo());
|
||||
});
|
||||
```
|
||||
|
||||
## ETAPI (External API)
|
||||
|
||||
**Location**: `/apps/server/src/etapi/`
|
||||
|
||||
ETAPI provides a stable, versioned API for external applications and scripts to interact with Trilium.
|
||||
|
||||
### Architecture
|
||||
|
||||
```typescript
|
||||
// ETAPI structure
|
||||
/etapi/
|
||||
├── etapi.openapi.yaml // OpenAPI specification
|
||||
├── auth.ts // Authentication
|
||||
├── notes.ts // Note endpoints
|
||||
├── branches.ts // Branch endpoints
|
||||
├── attributes.ts // Attribute endpoints
|
||||
├── attachments.ts // Attachment endpoints
|
||||
└── special_notes.ts // Special note operations
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
ETAPI uses token-based authentication:
|
||||
|
||||
```typescript
|
||||
// Creating ETAPI token
|
||||
POST /etapi/auth/login
|
||||
Body: {
|
||||
username: string,
|
||||
password: string
|
||||
}
|
||||
Response: {
|
||||
authToken: string
|
||||
}
|
||||
|
||||
// Using token in requests
|
||||
GET /etapi/notes/:noteId
|
||||
Headers: {
|
||||
Authorization: "authToken"
|
||||
}
|
||||
```
|
||||
|
||||
### Key Endpoints
|
||||
|
||||
#### Note CRUD Operations
|
||||
|
||||
```typescript
|
||||
// Create note
|
||||
POST /etapi/notes
|
||||
Body: {
|
||||
noteId?: string,
|
||||
parentNoteId: string,
|
||||
title: string,
|
||||
type: string,
|
||||
content?: string,
|
||||
position?: number
|
||||
}
|
||||
|
||||
// Get note
|
||||
GET /etapi/notes/:noteId
|
||||
Response: {
|
||||
noteId: string,
|
||||
title: string,
|
||||
type: string,
|
||||
mime: string,
|
||||
isProtected: boolean,
|
||||
attributes: Array<{
|
||||
attributeId: string,
|
||||
type: string,
|
||||
name: string,
|
||||
value: string
|
||||
}>,
|
||||
parentNoteIds: string[],
|
||||
childNoteIds: string[],
|
||||
dateCreated: string,
|
||||
dateModified: string
|
||||
}
|
||||
|
||||
// Update note content
|
||||
PUT /etapi/notes/:noteId/content
|
||||
Body: string | Buffer
|
||||
Headers: {
|
||||
"Content-Type": mime-type
|
||||
}
|
||||
|
||||
// Delete note
|
||||
DELETE /etapi/notes/:noteId
|
||||
```
|
||||
|
||||
#### Attribute Management
|
||||
|
||||
```typescript
|
||||
// Create attribute
|
||||
POST /etapi/attributes
|
||||
Body: {
|
||||
noteId: string,
|
||||
type: 'label' | 'relation',
|
||||
name: string,
|
||||
value: string,
|
||||
isInheritable?: boolean
|
||||
}
|
||||
|
||||
// Update attribute
|
||||
PATCH /etapi/attributes/:attributeId
|
||||
Body: {
|
||||
value?: string,
|
||||
isInheritable?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
#### Search
|
||||
|
||||
```typescript
|
||||
// Search notes
|
||||
GET /etapi/notes/search
|
||||
Query: {
|
||||
search: string,
|
||||
limit?: number,
|
||||
orderBy?: string,
|
||||
orderDirection?: 'asc' | 'desc'
|
||||
}
|
||||
Response: {
|
||||
results: Array<{
|
||||
noteId: string,
|
||||
title: string,
|
||||
// Other note properties
|
||||
}>
|
||||
}
|
||||
```
|
||||
|
||||
### Client Libraries
|
||||
|
||||
```javascript
|
||||
// JavaScript client example
|
||||
class EtapiClient {
|
||||
constructor(serverUrl, authToken) {
|
||||
this.serverUrl = serverUrl;
|
||||
this.authToken = authToken;
|
||||
}
|
||||
|
||||
async getNote(noteId) {
|
||||
const response = await fetch(
|
||||
`${this.serverUrl}/etapi/notes/${noteId}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': this.authToken
|
||||
}
|
||||
}
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async createNote(parentNoteId, title, content) {
|
||||
const response = await fetch(
|
||||
`${this.serverUrl}/etapi/notes`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': this.authToken,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
parentNoteId,
|
||||
title,
|
||||
type: 'text',
|
||||
content
|
||||
})
|
||||
}
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Python Client Example
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
class TriliumETAPI:
|
||||
def __init__(self, server_url, auth_token):
|
||||
self.server_url = server_url
|
||||
self.auth_token = auth_token
|
||||
self.headers = {'Authorization': auth_token}
|
||||
|
||||
def get_note(self, note_id):
|
||||
response = requests.get(
|
||||
f"{self.server_url}/etapi/notes/{note_id}",
|
||||
headers=self.headers
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def create_note(self, parent_note_id, title, content=""):
|
||||
response = requests.post(
|
||||
f"{self.server_url}/etapi/notes",
|
||||
headers=self.headers,
|
||||
json={
|
||||
'parentNoteId': parent_note_id,
|
||||
'title': title,
|
||||
'type': 'text',
|
||||
'content': content
|
||||
}
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def search_notes(self, query):
|
||||
response = requests.get(
|
||||
f"{self.server_url}/etapi/notes/search",
|
||||
headers=self.headers,
|
||||
params={'search': query}
|
||||
)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
## WebSocket Real-time Synchronization
|
||||
|
||||
**Location**: `/apps/server/src/services/ws.ts`
|
||||
|
||||
WebSocket connections provide real-time updates and synchronization between clients.
|
||||
|
||||
### Architecture
|
||||
|
||||
```typescript
|
||||
// WebSocket message types
|
||||
interface WSMessage {
|
||||
type: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
// Common message types
|
||||
type MessageType =
|
||||
| 'entity-changes' // Entity updates
|
||||
| 'sync' // Sync events
|
||||
| 'note-content-change' // Content updates
|
||||
| 'refresh-tree' // Tree structure changes
|
||||
| 'options-changed' // Configuration updates
|
||||
```
|
||||
|
||||
### Connection Management
|
||||
|
||||
```typescript
|
||||
// Client connection
|
||||
const ws = new WebSocket('wss://server/ws');
|
||||
|
||||
ws.on('open', () => {
|
||||
// Authenticate
|
||||
ws.send(JSON.stringify({
|
||||
type: 'auth',
|
||||
token: sessionToken
|
||||
}));
|
||||
});
|
||||
|
||||
ws.on('message', (data) => {
|
||||
const message = JSON.parse(data);
|
||||
handleWSMessage(message);
|
||||
});
|
||||
|
||||
// Server-side handling
|
||||
import WebSocket from 'ws';
|
||||
|
||||
const wss = new WebSocket.Server({ server });
|
||||
|
||||
wss.on('connection', (ws, req) => {
|
||||
const session = parseSession(req);
|
||||
|
||||
if (!session.authenticated) {
|
||||
ws.close(1008, 'Not authenticated');
|
||||
return;
|
||||
}
|
||||
|
||||
clients.add(ws);
|
||||
|
||||
ws.on('message', (message) => {
|
||||
handleClientMessage(ws, message);
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
clients.delete(ws);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Message Broadcasting
|
||||
|
||||
```typescript
|
||||
// Broadcast entity changes
|
||||
function broadcastEntityChanges(changes: EntityChange[]) {
|
||||
const message = {
|
||||
type: 'entity-changes',
|
||||
data: changes
|
||||
};
|
||||
|
||||
for (const client of clients) {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Targeted messages
|
||||
function sendToClient(clientId: string, message: WSMessage) {
|
||||
const client = clients.get(clientId);
|
||||
if (client?.readyState === WebSocket.OPEN) {
|
||||
client.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Real-time Sync Protocol
|
||||
|
||||
```typescript
|
||||
// Entity change notification
|
||||
{
|
||||
type: 'entity-changes',
|
||||
data: [
|
||||
{
|
||||
entityName: 'notes',
|
||||
entityId: 'noteId123',
|
||||
action: 'update',
|
||||
entity: { /* note data */ }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Sync pull request
|
||||
{
|
||||
type: 'sync-pull',
|
||||
data: {
|
||||
lastSyncId: 12345
|
||||
}
|
||||
}
|
||||
|
||||
// Sync push
|
||||
{
|
||||
type: 'sync-push',
|
||||
data: {
|
||||
entities: [ /* changed entities */ ]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Client-side Handling
|
||||
|
||||
```typescript
|
||||
// Froca WebSocket integration
|
||||
class WSClient {
|
||||
constructor() {
|
||||
this.ws = null;
|
||||
this.reconnectTimeout = null;
|
||||
this.connect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.ws = new WebSocket(this.getWSUrl());
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data);
|
||||
this.handleMessage(message);
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
// Reconnect with exponential backoff
|
||||
this.scheduleReconnect();
|
||||
};
|
||||
}
|
||||
|
||||
handleMessage(message: WSMessage) {
|
||||
switch (message.type) {
|
||||
case 'entity-changes':
|
||||
this.handleEntityChanges(message.data);
|
||||
break;
|
||||
case 'refresh-tree':
|
||||
froca.loadInitialTree();
|
||||
break;
|
||||
case 'note-content-change':
|
||||
this.handleContentChange(message.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleEntityChanges(changes: EntityChange[]) {
|
||||
for (const change of changes) {
|
||||
if (change.entityName === 'notes') {
|
||||
froca.reloadNotes([change.entityId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Security
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
```typescript
|
||||
// 1. Session-based (Internal API)
|
||||
app.use(session({
|
||||
secret: config.sessionSecret,
|
||||
resave: false,
|
||||
saveUninitialized: false
|
||||
}));
|
||||
|
||||
// 2. Token-based (ETAPI)
|
||||
router.use('/etapi', (req, res, next) => {
|
||||
const token = req.headers.authorization;
|
||||
|
||||
const etapiToken = becca.getEtapiToken(token);
|
||||
if (!etapiToken || etapiToken.isExpired()) {
|
||||
return res.status(401).json({
|
||||
error: 'Invalid or expired token'
|
||||
});
|
||||
}
|
||||
|
||||
req.etapiToken = etapiToken;
|
||||
next();
|
||||
});
|
||||
|
||||
// 3. WebSocket authentication
|
||||
ws.on('connection', (socket) => {
|
||||
socket.on('auth', (token) => {
|
||||
if (!validateToken(token)) {
|
||||
socket.close(1008, 'Invalid token');
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
```typescript
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
// Global rate limit
|
||||
const globalLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 1000 // limit each IP to 1000 requests per windowMs
|
||||
});
|
||||
|
||||
// Strict limit for authentication
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5,
|
||||
message: 'Too many authentication attempts'
|
||||
});
|
||||
|
||||
app.use('/api', globalLimiter);
|
||||
app.use('/api/auth', authLimiter);
|
||||
```
|
||||
|
||||
### Input Validation
|
||||
|
||||
```typescript
|
||||
import { body, validationResult } from 'express-validator';
|
||||
|
||||
router.post('/api/notes',
|
||||
body('title').isString().isLength({ min: 1, max: 1000 }),
|
||||
body('type').isIn(['text', 'code', 'file', 'image']),
|
||||
body('content').optional().isString(),
|
||||
(req, res) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
// Process valid input
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Caching Strategies
|
||||
|
||||
```typescript
|
||||
// Response caching
|
||||
const cache = new Map();
|
||||
|
||||
router.get('/api/notes/:noteId', (req, res) => {
|
||||
const cacheKey = `note:${req.params.noteId}`;
|
||||
const cached = cache.get(cacheKey);
|
||||
|
||||
if (cached && cached.expires > Date.now()) {
|
||||
return res.json(cached.data);
|
||||
}
|
||||
|
||||
const note = becca.getNote(req.params.noteId);
|
||||
const data = note.getPojo();
|
||||
|
||||
cache.set(cacheKey, {
|
||||
data,
|
||||
expires: Date.now() + 60000 // 1 minute
|
||||
});
|
||||
|
||||
res.json(data);
|
||||
});
|
||||
```
|
||||
|
||||
### Batch Operations
|
||||
|
||||
```typescript
|
||||
// Batch API endpoint
|
||||
router.post('/api/batch', async (req, res) => {
|
||||
const operations = req.body.operations;
|
||||
const results = [];
|
||||
|
||||
await sql.transactional(async () => {
|
||||
for (const op of operations) {
|
||||
const result = await executeOperation(op);
|
||||
results.push(result);
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ results });
|
||||
});
|
||||
|
||||
// Client batch usage
|
||||
const batch = [
|
||||
{ method: 'PUT', path: '/notes/1', body: { title: 'Note 1' }},
|
||||
{ method: 'PUT', path: '/notes/2', body: { title: 'Note 2' }},
|
||||
{ method: 'POST', path: '/notes/3/attributes', body: { type: 'label', name: 'todo' }}
|
||||
];
|
||||
|
||||
await api.post('/batch', { operations: batch });
|
||||
```
|
||||
|
||||
### Streaming Responses
|
||||
|
||||
```typescript
|
||||
// Stream large data
|
||||
router.get('/api/export', (req, res) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'application/x-ndjson',
|
||||
'Transfer-Encoding': 'chunked'
|
||||
});
|
||||
|
||||
const noteStream = createNoteExportStream();
|
||||
|
||||
noteStream.on('data', (note) => {
|
||||
res.write(JSON.stringify(note) + '\n');
|
||||
});
|
||||
|
||||
noteStream.on('end', () => {
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Standard Error Responses
|
||||
|
||||
```typescript
|
||||
// Error response format
|
||||
interface ErrorResponse {
|
||||
error: string;
|
||||
code?: string;
|
||||
details?: any;
|
||||
}
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
console.error('API Error:', err);
|
||||
|
||||
if (err instanceof NotFoundError) {
|
||||
return res.status(404).json({
|
||||
error: err.message,
|
||||
code: 'NOT_FOUND'
|
||||
});
|
||||
}
|
||||
|
||||
if (err instanceof ValidationError) {
|
||||
return res.status(400).json({
|
||||
error: err.message,
|
||||
code: 'VALIDATION_ERROR',
|
||||
details: err.details
|
||||
});
|
||||
}
|
||||
|
||||
// Generic error
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
code: 'INTERNAL_ERROR'
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
### OpenAPI/Swagger
|
||||
|
||||
```yaml
|
||||
# etapi.openapi.yaml
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: Trilium ETAPI
|
||||
version: 1.0.0
|
||||
description: External API for Trilium Notes
|
||||
|
||||
paths:
|
||||
/etapi/notes/{noteId}:
|
||||
get:
|
||||
summary: Get note by ID
|
||||
parameters:
|
||||
- name: noteId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
200:
|
||||
description: Note found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Note'
|
||||
404:
|
||||
description: Note not found
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Note:
|
||||
type: object
|
||||
properties:
|
||||
noteId:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum: [text, code, file, image]
|
||||
```
|
||||
|
||||
### API Testing
|
||||
|
||||
```typescript
|
||||
// API test example
|
||||
describe('Notes API', () => {
|
||||
it('should create a note', async () => {
|
||||
const response = await request(app)
|
||||
.post('/api/notes/root/children')
|
||||
.send({
|
||||
title: 'Test Note',
|
||||
type: 'text',
|
||||
content: 'Test content'
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveProperty('noteId');
|
||||
expect(response.body.title).toBe('Test Note');
|
||||
});
|
||||
|
||||
it('should handle errors', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/notes/invalid')
|
||||
.expect(404);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### API Design
|
||||
|
||||
1. **RESTful conventions**: Use appropriate HTTP methods and status codes
|
||||
2. **Consistent naming**: Use camelCase for JSON properties
|
||||
3. **Versioning**: Version the API to maintain compatibility
|
||||
4. **Documentation**: Keep OpenAPI spec up to date
|
||||
|
||||
### Security
|
||||
|
||||
1. **Authentication**: Always verify user identity
|
||||
2. **Authorization**: Check permissions for each operation
|
||||
3. **Validation**: Validate all input data
|
||||
4. **Rate limiting**: Prevent abuse with appropriate limits
|
||||
|
||||
### Performance
|
||||
|
||||
1. **Pagination**: Limit response sizes with pagination
|
||||
2. **Caching**: Cache frequently accessed data
|
||||
3. **Batch operations**: Support bulk operations
|
||||
4. **Async processing**: Use queues for long-running tasks
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Three-Layer Cache System](Three-Layer-Cache-System.md) - Cache architecture
|
||||
- [Entity System](Entity-System.md) - Data model
|
||||
- [ETAPI Reference](/apps/server/src/etapi/etapi.openapi.yaml) - OpenAPI specification
|
||||
612
docs/Developer Guide/Architecture/Entity-System.md
vendored
Normal file
612
docs/Developer Guide/Architecture/Entity-System.md
vendored
Normal file
@@ -0,0 +1,612 @@
|
||||
# Entity System Architecture
|
||||
|
||||
The Entity System forms the core data model of Trilium Notes, providing a flexible and powerful structure for organizing information. This document details the entities, their relationships, and usage patterns.
|
||||
|
||||
## Core Entities Overview
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
Note ||--o{ Branch : "parent-child"
|
||||
Note ||--o{ Attribute : "has"
|
||||
Note ||--o{ Revision : "history"
|
||||
Note ||--o{ Attachment : "contains"
|
||||
Attachment ||--|| Blob : "stores in"
|
||||
Revision ||--|| Blob : "stores in"
|
||||
Note }o--o{ Note : "relates via Attribute"
|
||||
|
||||
Note {
|
||||
string noteId PK
|
||||
string title
|
||||
string type
|
||||
string content
|
||||
boolean isProtected
|
||||
string dateCreated
|
||||
string dateModified
|
||||
}
|
||||
|
||||
Branch {
|
||||
string branchId PK
|
||||
string noteId FK
|
||||
string parentNoteId FK
|
||||
integer notePosition
|
||||
string prefix
|
||||
boolean isExpanded
|
||||
}
|
||||
|
||||
Attribute {
|
||||
string attributeId PK
|
||||
string noteId FK
|
||||
string type "label or relation"
|
||||
string name
|
||||
string value
|
||||
integer position
|
||||
boolean isInheritable
|
||||
}
|
||||
|
||||
Revision {
|
||||
string revisionId PK
|
||||
string noteId FK
|
||||
string title
|
||||
string type
|
||||
boolean isProtected
|
||||
string dateCreated
|
||||
}
|
||||
|
||||
Attachment {
|
||||
string attachmentId PK
|
||||
string ownerId FK
|
||||
string role
|
||||
string mime
|
||||
string title
|
||||
string blobId FK
|
||||
}
|
||||
|
||||
Blob {
|
||||
string blobId PK
|
||||
binary content
|
||||
string dateModified
|
||||
}
|
||||
|
||||
Option {
|
||||
string name PK
|
||||
string value
|
||||
boolean isSynced
|
||||
}
|
||||
```
|
||||
|
||||
## Entity Definitions
|
||||
|
||||
### BNote - Notes with Content and Metadata
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/bnote.ts`
|
||||
|
||||
Notes are the fundamental unit of information in Trilium. Each note can contain different types of content and maintain relationships with other notes.
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BNote {
|
||||
noteId: string; // Unique identifier
|
||||
title: string; // Display title
|
||||
type: string; // Content type (text, code, file, etc.)
|
||||
mime: string; // MIME type for content
|
||||
isProtected: boolean; // Encryption flag
|
||||
dateCreated: string; // Creation timestamp
|
||||
dateModified: string; // Last modification
|
||||
utcDateCreated: string; // UTC creation
|
||||
utcDateModified: string; // UTC modification
|
||||
|
||||
// Relationships
|
||||
parentBranches: BBranch[]; // Parent connections
|
||||
children: BBranch[]; // Child connections
|
||||
attributes: BAttribute[]; // Metadata
|
||||
|
||||
// Content
|
||||
content?: string | Buffer; // Note content (lazy loaded)
|
||||
|
||||
// Computed
|
||||
isDecrypted: boolean; // Decryption status
|
||||
}
|
||||
```
|
||||
|
||||
#### Note Types
|
||||
|
||||
- **text**: Rich text content with HTML formatting
|
||||
- **code**: Source code with syntax highlighting
|
||||
- **file**: Binary file attachment
|
||||
- **image**: Image with preview capabilities
|
||||
- **search**: Saved search query
|
||||
- **book**: Container for hierarchical documentation
|
||||
- **relationMap**: Visual relationship diagram
|
||||
- **canvas**: Drawing canvas (Excalidraw)
|
||||
- **mermaid**: Mermaid diagram
|
||||
- **mindMap**: Mind mapping visualization
|
||||
- **webView**: Embedded web content
|
||||
- **noteMap**: Tree visualization
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
```typescript
|
||||
// Create a new note
|
||||
const note = new BNote({
|
||||
noteId: generateNoteId(),
|
||||
title: "My Note",
|
||||
type: "text",
|
||||
mime: "text/html",
|
||||
content: "<p>Note content</p>"
|
||||
});
|
||||
note.save();
|
||||
|
||||
// Get note with content
|
||||
const note = becca.getNote(noteId);
|
||||
await note.loadContent();
|
||||
|
||||
// Update note
|
||||
note.title = "Updated Title";
|
||||
note.save();
|
||||
|
||||
// Protect note
|
||||
note.isProtected = true;
|
||||
note.encrypt();
|
||||
note.save();
|
||||
```
|
||||
|
||||
### BBranch - Hierarchical Relationships
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/bbranch.ts`
|
||||
|
||||
Branches define the parent-child relationships between notes, allowing a note to have multiple parents (cloning).
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BBranch {
|
||||
branchId: string; // Unique identifier
|
||||
noteId: string; // Child note ID
|
||||
parentNoteId: string; // Parent note ID
|
||||
notePosition: number; // Order among siblings
|
||||
prefix: string; // Optional prefix label
|
||||
isExpanded: boolean; // Tree UI state
|
||||
|
||||
// Computed
|
||||
childNote: BNote; // Reference to child
|
||||
parentNote: BNote; // Reference to parent
|
||||
}
|
||||
```
|
||||
|
||||
#### Key Features
|
||||
|
||||
- **Multiple Parents**: Notes can appear in multiple locations
|
||||
- **Ordering**: Explicit positioning among siblings
|
||||
- **Prefixes**: Optional labels for context (e.g., "Chapter 1:")
|
||||
- **UI State**: Expansion state persisted per branch
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
```typescript
|
||||
// Create parent-child relationship
|
||||
const branch = new BBranch({
|
||||
noteId: childNote.noteId,
|
||||
parentNoteId: parentNote.noteId,
|
||||
notePosition: 10
|
||||
});
|
||||
branch.save();
|
||||
|
||||
// Clone note to another parent
|
||||
const cloneBranch = childNote.cloneTo(otherParent.noteId);
|
||||
|
||||
// Reorder children
|
||||
parentNote.sortChildren((a, b) =>
|
||||
a.title.localeCompare(b.title)
|
||||
);
|
||||
|
||||
// Add prefix
|
||||
branch.prefix = "Important: ";
|
||||
branch.save();
|
||||
```
|
||||
|
||||
### BAttribute - Key-Value Metadata
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/battribute.ts`
|
||||
|
||||
Attributes provide flexible metadata and relationships between notes.
|
||||
|
||||
#### Types
|
||||
|
||||
1. **Labels**: Key-value pairs for metadata
|
||||
2. **Relations**: References to other notes
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BAttribute {
|
||||
attributeId: string; // Unique identifier
|
||||
noteId: string; // Owning note
|
||||
type: 'label' | 'relation';
|
||||
name: string; // Attribute name
|
||||
value: string; // Value or target noteId
|
||||
position: number; // Display order
|
||||
isInheritable: boolean; // Inherited by children
|
||||
|
||||
// Computed
|
||||
note: BNote; // Owner note
|
||||
targetNote?: BNote; // For relations
|
||||
}
|
||||
```
|
||||
|
||||
#### Common Patterns
|
||||
|
||||
```typescript
|
||||
// Add label
|
||||
note.addLabel("status", "active");
|
||||
note.addLabel("priority", "high");
|
||||
|
||||
// Add relation
|
||||
note.addRelation("template", templateNoteId);
|
||||
note.addRelation("renderNote", renderNoteId);
|
||||
|
||||
// Query by attributes
|
||||
const todos = becca.findAttributes("label", "todoItem");
|
||||
const templates = becca.findAttributes("label", "template");
|
||||
|
||||
// Inheritable attributes
|
||||
note.addLabel("workspace", "project", true); // Children inherit
|
||||
```
|
||||
|
||||
#### System Attributes
|
||||
|
||||
Special attributes with system behavior:
|
||||
|
||||
- `#hidePromotedAttributes`: Hide promoted attributes in UI
|
||||
- `#readOnly`: Prevent note editing
|
||||
- `#autoReadOnlyDisabled`: Disable auto read-only
|
||||
- `#hideChildrenOverview`: Hide children count
|
||||
- `~template`: Note template relation
|
||||
- `~renderNote`: Custom rendering relation
|
||||
|
||||
### BRevision - Version History
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/brevision.ts`
|
||||
|
||||
Revisions provide version history and recovery capabilities.
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BRevision {
|
||||
revisionId: string; // Unique identifier
|
||||
noteId: string; // Parent note
|
||||
type: string; // Content type
|
||||
mime: string; // MIME type
|
||||
title: string; // Historical title
|
||||
isProtected: boolean; // Encryption flag
|
||||
dateCreated: string; // Creation time
|
||||
utcDateCreated: string; // UTC time
|
||||
dateModified: string; // Content modification
|
||||
blobId: string; // Content storage
|
||||
|
||||
// Methods
|
||||
getContent(): string | Buffer;
|
||||
restore(): void;
|
||||
}
|
||||
```
|
||||
|
||||
#### Revision Strategy
|
||||
|
||||
- Created automatically on significant changes
|
||||
- Configurable retention period
|
||||
- Day/week/month/year retention rules
|
||||
- Protected note revisions are encrypted
|
||||
|
||||
#### Usage Examples
|
||||
|
||||
```typescript
|
||||
// Get note revisions
|
||||
const revisions = note.getRevisions();
|
||||
|
||||
// Restore revision
|
||||
const revision = becca.getRevision(revisionId);
|
||||
revision.restore();
|
||||
|
||||
// Manual revision creation
|
||||
note.saveRevision();
|
||||
|
||||
// Compare revisions
|
||||
const diff = revision1.getContent() !== revision2.getContent();
|
||||
```
|
||||
|
||||
### BOption - Application Configuration
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/boption.ts`
|
||||
|
||||
Options store application and user preferences.
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BOption {
|
||||
name: string; // Option key
|
||||
value: string; // Option value
|
||||
isSynced: boolean; // Sync across instances
|
||||
utcDateModified: string; // Last change
|
||||
}
|
||||
```
|
||||
|
||||
#### Common Options
|
||||
|
||||
```typescript
|
||||
// Theme settings
|
||||
setOption("theme", "dark");
|
||||
|
||||
// Protected session timeout
|
||||
setOption("protectedSessionTimeout", "600");
|
||||
|
||||
// Sync settings
|
||||
setOption("syncServerHost", "https://sync.server");
|
||||
|
||||
// Note settings
|
||||
setOption("defaultNoteType", "text");
|
||||
```
|
||||
|
||||
### BAttachment - File Attachments
|
||||
|
||||
**Location**: `/apps/server/src/becca/entities/battachment.ts`
|
||||
|
||||
Attachments link binary content to notes.
|
||||
|
||||
#### Properties
|
||||
|
||||
```typescript
|
||||
class BAttachment {
|
||||
attachmentId: string; // Unique identifier
|
||||
ownerId: string; // Parent note ID
|
||||
role: string; // Attachment role
|
||||
mime: string; // MIME type
|
||||
title: string; // Display title
|
||||
blobId: string; // Content reference
|
||||
utcDateScheduledForDeletion: string;
|
||||
|
||||
// Methods
|
||||
getContent(): Buffer;
|
||||
getBlob(): BBlob;
|
||||
}
|
||||
```
|
||||
|
||||
#### Usage Patterns
|
||||
|
||||
```typescript
|
||||
// Add attachment to note
|
||||
const attachment = note.addAttachment({
|
||||
role: "file",
|
||||
mime: "application/pdf",
|
||||
title: "document.pdf",
|
||||
content: buffer
|
||||
});
|
||||
|
||||
// Get attachments
|
||||
const attachments = note.getAttachments();
|
||||
|
||||
// Download attachment
|
||||
const content = attachment.getContent();
|
||||
```
|
||||
|
||||
## Entity Relationships
|
||||
|
||||
### Parent-Child Hierarchy
|
||||
|
||||
```typescript
|
||||
// Single parent
|
||||
childNote.setParent(parentNote.noteId);
|
||||
|
||||
// Multiple parents (cloning)
|
||||
childNote.cloneTo(parent1.noteId);
|
||||
childNote.cloneTo(parent2.noteId);
|
||||
|
||||
// Get parents
|
||||
const parents = childNote.getParentNotes();
|
||||
|
||||
// Get children
|
||||
const children = parentNote.getChildNotes();
|
||||
|
||||
// Get subtree
|
||||
const subtree = parentNote.getSubtreeNotes();
|
||||
```
|
||||
|
||||
### Attribute Relationships
|
||||
|
||||
```typescript
|
||||
// Direct relations
|
||||
note.addRelation("author", authorNote.noteId);
|
||||
|
||||
// Bidirectional relations
|
||||
note1.addRelation("related", note2.noteId);
|
||||
note2.addRelation("related", note1.noteId);
|
||||
|
||||
// Get related notes
|
||||
const related = note.getRelations("related");
|
||||
|
||||
// Get notes relating to this one
|
||||
const targetRelations = note.getTargetRelations();
|
||||
```
|
||||
|
||||
## Entity Lifecycle
|
||||
|
||||
### Creation
|
||||
|
||||
```typescript
|
||||
// Note creation
|
||||
const note = new BNote({
|
||||
noteId: generateNoteId(),
|
||||
title: "New Note",
|
||||
type: "text"
|
||||
});
|
||||
note.save();
|
||||
|
||||
// With parent
|
||||
const child = parentNote.addChild({
|
||||
title: "Child Note",
|
||||
type: "text",
|
||||
content: "Content"
|
||||
});
|
||||
```
|
||||
|
||||
### Updates
|
||||
|
||||
```typescript
|
||||
// Atomic updates
|
||||
note.title = "New Title";
|
||||
note.save();
|
||||
|
||||
// Batch updates
|
||||
sql.transactional(() => {
|
||||
note1.title = "Title 1";
|
||||
note1.save();
|
||||
|
||||
note2.content = "Content 2";
|
||||
note2.save();
|
||||
});
|
||||
```
|
||||
|
||||
### Deletion
|
||||
|
||||
```typescript
|
||||
// Soft delete (move to trash)
|
||||
note.deleteNote();
|
||||
|
||||
// Mark for deletion
|
||||
note.isDeleted = true;
|
||||
note.save();
|
||||
|
||||
// Permanent deletion (after grace period)
|
||||
note.eraseNote();
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```typescript
|
||||
// Note content loaded on demand
|
||||
const note = becca.getNote(noteId); // Metadata only
|
||||
await note.loadContent(); // Load content when needed
|
||||
|
||||
// Revisions loaded on demand
|
||||
const revisions = note.getRevisions(); // Database query
|
||||
```
|
||||
|
||||
### Batch Operations
|
||||
|
||||
```typescript
|
||||
// Efficient bulk loading
|
||||
const notes = becca.getNotes(noteIds);
|
||||
|
||||
// Batch attribute queries
|
||||
const attributes = sql.getRows(`
|
||||
SELECT * FROM attributes
|
||||
WHERE noteId IN (???)
|
||||
AND name = ?
|
||||
`, [noteIds, 'label']);
|
||||
```
|
||||
|
||||
### Indexing
|
||||
|
||||
```typescript
|
||||
// Attribute index for fast lookups
|
||||
const labels = becca.findAttributes("label", "important");
|
||||
|
||||
// Branch index for relationship queries
|
||||
const branch = becca.getBranchFromChildAndParent(childId, parentId);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Entity Creation
|
||||
|
||||
```typescript
|
||||
// Always use transactions for multiple operations
|
||||
sql.transactional(() => {
|
||||
const note = new BNote({...});
|
||||
note.save();
|
||||
|
||||
note.addLabel("status", "draft");
|
||||
note.addRelation("template", templateId);
|
||||
});
|
||||
```
|
||||
|
||||
### Entity Updates
|
||||
|
||||
```typescript
|
||||
// Check existence before update
|
||||
const note = becca.getNote(noteId);
|
||||
if (note) {
|
||||
note.title = "Updated";
|
||||
note.save();
|
||||
}
|
||||
|
||||
// Use proper error handling
|
||||
try {
|
||||
const note = becca.getNoteOrThrow(noteId);
|
||||
note.save();
|
||||
} catch (e) {
|
||||
log.error(`Note ${noteId} not found`);
|
||||
}
|
||||
```
|
||||
|
||||
### Querying
|
||||
|
||||
```typescript
|
||||
// Use indexed queries
|
||||
const attrs = becca.findAttributes("label", "task");
|
||||
|
||||
// Avoid N+1 queries
|
||||
const noteIds = [...];
|
||||
const notes = becca.getNotes(noteIds); // Single batch
|
||||
|
||||
// Use SQL for complex queries
|
||||
const results = sql.getRows(`
|
||||
SELECT n.noteId, n.title, COUNT(b.branchId) as childCount
|
||||
FROM notes n
|
||||
LEFT JOIN branches b ON b.parentNoteId = n.noteId
|
||||
GROUP BY n.noteId
|
||||
`);
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Circular References**
|
||||
```typescript
|
||||
// Detect cycles before creating branches
|
||||
if (!parentNote.hasAncestor(childNote.noteId)) {
|
||||
childNote.setParent(parentNote.noteId);
|
||||
}
|
||||
```
|
||||
|
||||
2. **Orphaned Entities**
|
||||
```typescript
|
||||
// Find orphaned notes
|
||||
const orphans = sql.getRows(`
|
||||
SELECT noteId FROM notes
|
||||
WHERE noteId != 'root'
|
||||
AND noteId NOT IN (SELECT noteId FROM branches)
|
||||
`);
|
||||
```
|
||||
|
||||
3. **Attribute Conflicts**
|
||||
```typescript
|
||||
// Handle duplicate attributes
|
||||
const existing = note.getAttribute("label", "status");
|
||||
if (existing) {
|
||||
existing.value = "new value";
|
||||
existing.save();
|
||||
} else {
|
||||
note.addLabel("status", "new value");
|
||||
}
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Three-Layer Cache System](Three-Layer-Cache-System.md) - Cache architecture
|
||||
- [Database Schema](../Development%20and%20architecture/Database/notes.md) - Database structure
|
||||
- [Script API](../../Script%20API/) - Entity API for scripts
|
||||
610
docs/Developer Guide/Architecture/Monorepo-Structure.md
vendored
Normal file
610
docs/Developer Guide/Architecture/Monorepo-Structure.md
vendored
Normal file
@@ -0,0 +1,610 @@
|
||||
# Monorepo Structure
|
||||
|
||||
Trilium is organized as a TypeScript monorepo using NX, facilitating code sharing, consistent tooling, and efficient build processes. This document provides a comprehensive overview of the project structure, build system, and development workflow.
|
||||
|
||||
## Project Organization
|
||||
|
||||
```
|
||||
TriliumNext/Trilium/
|
||||
├── apps/ # Runnable applications
|
||||
│ ├── client/ # Frontend web application
|
||||
│ ├── server/ # Node.js backend server
|
||||
│ ├── desktop/ # Electron desktop application
|
||||
│ ├── web-clipper/ # Browser extension
|
||||
│ ├── db-compare/ # Database comparison tool
|
||||
│ ├── dump-db/ # Database dump utility
|
||||
│ └── edit-docs/ # Documentation editor
|
||||
├── packages/ # Shared libraries
|
||||
│ ├── commons/ # Shared interfaces and utilities
|
||||
│ ├── ckeditor5/ # Rich text editor
|
||||
│ ├── codemirror/ # Code editor
|
||||
│ ├── highlightjs/ # Syntax highlighting
|
||||
│ ├── ckeditor5-admonition/ # CKEditor plugin
|
||||
│ ├── ckeditor5-footnotes/ # CKEditor plugin
|
||||
│ ├── ckeditor5-math/ # CKEditor plugin
|
||||
│ └── ckeditor5-mermaid/ # CKEditor plugin
|
||||
├── docs/ # Documentation
|
||||
├── nx.json # NX workspace configuration
|
||||
├── package.json # Root package configuration
|
||||
├── pnpm-workspace.yaml # PNPM workspace configuration
|
||||
└── tsconfig.base.json # Base TypeScript configuration
|
||||
```
|
||||
|
||||
## Applications
|
||||
|
||||
### Client (`/apps/client`)
|
||||
|
||||
The frontend application shared by both server and desktop versions.
|
||||
|
||||
```
|
||||
apps/client/
|
||||
├── src/
|
||||
│ ├── components/ # Core UI components
|
||||
│ ├── entities/ # Frontend entities (FNote, FBranch, etc.)
|
||||
│ ├── services/ # Business logic and API calls
|
||||
│ ├── widgets/ # UI widgets system
|
||||
│ │ ├── type_widgets/ # Note type specific widgets
|
||||
│ │ ├── dialogs/ # Dialog components
|
||||
│ │ └── panels/ # Panel widgets
|
||||
│ ├── public/
|
||||
│ │ ├── fonts/ # Font assets
|
||||
│ │ ├── images/ # Image assets
|
||||
│ │ └── libraries/ # Third-party libraries
|
||||
│ └── desktop.ts # Desktop entry point
|
||||
├── package.json
|
||||
├── project.json # NX project configuration
|
||||
└── vite.config.ts # Vite build configuration
|
||||
```
|
||||
|
||||
#### Key Files
|
||||
|
||||
- `desktop.ts` - Main application initialization
|
||||
- `services/froca.ts` - Frontend cache implementation
|
||||
- `widgets/basic_widget.ts` - Base widget class
|
||||
- `services/server.ts` - API communication layer
|
||||
|
||||
### Server (`/apps/server`)
|
||||
|
||||
The Node.js backend providing API, database, and business logic.
|
||||
|
||||
```
|
||||
apps/server/
|
||||
├── src/
|
||||
│ ├── becca/ # Backend cache system
|
||||
│ │ ├── entities/ # Core entities (BNote, BBranch, etc.)
|
||||
│ │ └── becca.ts # Cache interface
|
||||
│ ├── routes/ # Express routes
|
||||
│ │ ├── api/ # Internal API endpoints
|
||||
│ │ └── pages/ # HTML page routes
|
||||
│ ├── etapi/ # External API
|
||||
│ ├── services/ # Business services
|
||||
│ ├── share/ # Note sharing functionality
|
||||
│ │ └── shaca/ # Share cache
|
||||
│ ├── migrations/ # Database migrations
|
||||
│ ├── assets/
|
||||
│ │ ├── db/ # Database schema
|
||||
│ │ └── doc_notes/ # Documentation notes
|
||||
│ └── main.ts # Server entry point
|
||||
├── package.json
|
||||
├── project.json
|
||||
└── webpack.config.js # Webpack configuration
|
||||
```
|
||||
|
||||
#### Key Services
|
||||
|
||||
- `services/sql.ts` - Database access layer
|
||||
- `services/sync.ts` - Synchronization logic
|
||||
- `services/ws.ts` - WebSocket server
|
||||
- `services/protected_session.ts` - Encryption handling
|
||||
|
||||
### Desktop (`/apps/desktop`)
|
||||
|
||||
Electron wrapper for the desktop application.
|
||||
|
||||
```
|
||||
apps/desktop/
|
||||
├── src/
|
||||
│ ├── main.ts # Electron main process
|
||||
│ ├── preload.ts # Preload script
|
||||
│ ├── services/ # Desktop-specific services
|
||||
│ └── utils/ # Desktop utilities
|
||||
├── resources/ # Desktop resources (icons, etc.)
|
||||
├── package.json
|
||||
└── electron-builder.yml # Electron Builder configuration
|
||||
```
|
||||
|
||||
### Web Clipper (`/apps/web-clipper`)
|
||||
|
||||
Browser extension for saving web content to Trilium.
|
||||
|
||||
```
|
||||
apps/web-clipper/
|
||||
├── src/
|
||||
│ ├── background.js # Background script
|
||||
│ ├── content.js # Content script
|
||||
│ ├── popup/ # Extension popup
|
||||
│ └── options/ # Extension options
|
||||
├── manifest.json # Extension manifest
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Packages
|
||||
|
||||
### Commons (`/packages/commons`)
|
||||
|
||||
Shared TypeScript interfaces and utilities used across applications.
|
||||
|
||||
```typescript
|
||||
// packages/commons/src/types.ts
|
||||
export interface NoteRow {
|
||||
noteId: string;
|
||||
title: string;
|
||||
type: string;
|
||||
mime: string;
|
||||
isProtected: boolean;
|
||||
dateCreated: string;
|
||||
dateModified: string;
|
||||
}
|
||||
|
||||
export interface BranchRow {
|
||||
branchId: string;
|
||||
noteId: string;
|
||||
parentNoteId: string;
|
||||
notePosition: number;
|
||||
prefix: string;
|
||||
isExpanded: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### CKEditor5 (`/packages/ckeditor5`)
|
||||
|
||||
Custom CKEditor5 build with Trilium-specific plugins.
|
||||
|
||||
```
|
||||
packages/ckeditor5/
|
||||
├── src/
|
||||
│ ├── ckeditor.ts # Editor configuration
|
||||
│ ├── plugins.ts # Plugin registration
|
||||
│ └── trilium/ # Custom plugins
|
||||
├── theme/ # Editor themes
|
||||
└── package.json
|
||||
```
|
||||
|
||||
#### Custom Plugins
|
||||
|
||||
- **Admonition**: Note boxes with icons
|
||||
- **Footnotes**: Reference footnotes
|
||||
- **Math**: LaTeX equation rendering
|
||||
- **Mermaid**: Diagram integration
|
||||
|
||||
### CodeMirror (`/packages/codemirror`)
|
||||
|
||||
Code editor customizations for the code note type.
|
||||
|
||||
```typescript
|
||||
// packages/codemirror/src/index.ts
|
||||
export function createCodeMirror(element: HTMLElement, options: CodeMirrorOptions) {
|
||||
return CodeMirror(element, {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
// Trilium-specific customizations
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Build System
|
||||
|
||||
### NX Configuration
|
||||
|
||||
**`nx.json`**
|
||||
|
||||
```json
|
||||
{
|
||||
"tasksRunnerOptions": {
|
||||
"default": {
|
||||
"runner": "nx/tasks-runners/default",
|
||||
"options": {
|
||||
"cacheableOperations": ["build", "test", "lint"],
|
||||
"parallel": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
"targetDefaults": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"cache": true
|
||||
},
|
||||
"test": {
|
||||
"cache": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Project Configuration
|
||||
|
||||
Each application and package has a `project.json` defining its targets:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "server",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/webpack:webpack",
|
||||
"options": {
|
||||
"outputPath": "dist/apps/server",
|
||||
"main": "apps/server/src/main.ts",
|
||||
"tsConfig": "apps/server/tsconfig.app.json"
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@nx/node:node",
|
||||
"options": {
|
||||
"buildTarget": "server:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"options": {
|
||||
"jestConfig": "apps/server/jest.config.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Build Commands
|
||||
|
||||
```bash
|
||||
# Build specific project
|
||||
pnpm nx build server
|
||||
pnpm nx build client
|
||||
|
||||
# Build all projects
|
||||
pnpm nx run-many --target=build --all
|
||||
|
||||
# Build with dependencies
|
||||
pnpm nx build server --with-deps
|
||||
|
||||
# Production build
|
||||
pnpm nx build server --configuration=production
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Initial Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Enable corepack for pnpm
|
||||
corepack enable
|
||||
|
||||
# Build all packages
|
||||
pnpm nx run-many --target=build --all
|
||||
```
|
||||
|
||||
### Development Commands
|
||||
|
||||
```bash
|
||||
# Start development server
|
||||
pnpm run server:start
|
||||
# or
|
||||
pnpm nx run server:serve
|
||||
|
||||
# Start desktop app
|
||||
pnpm nx run desktop:serve
|
||||
|
||||
# Run client dev server
|
||||
pnpm nx run client:serve
|
||||
|
||||
# Watch mode for packages
|
||||
pnpm nx run commons:build --watch
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pnpm test:all
|
||||
|
||||
# Run tests for specific project
|
||||
pnpm nx test server
|
||||
pnpm nx test client
|
||||
|
||||
# Run tests in watch mode
|
||||
pnpm nx test server --watch
|
||||
|
||||
# Generate coverage
|
||||
pnpm nx test server --coverage
|
||||
```
|
||||
|
||||
### Linting and Type Checking
|
||||
|
||||
```bash
|
||||
# Lint specific project
|
||||
pnpm nx lint server
|
||||
|
||||
# Type check
|
||||
pnpm nx run server:typecheck
|
||||
|
||||
# Lint all projects
|
||||
pnpm nx run-many --target=lint --all
|
||||
|
||||
# Fix lint issues
|
||||
pnpm nx lint server --fix
|
||||
```
|
||||
|
||||
## Dependency Management
|
||||
|
||||
### Package Dependencies
|
||||
|
||||
Dependencies are managed at both root and project levels:
|
||||
|
||||
```json
|
||||
// Root package.json - shared dev dependencies
|
||||
{
|
||||
"devDependencies": {
|
||||
"@nx/workspace": "^17.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"eslint": "^8.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
// Project package.json - project-specific dependencies
|
||||
{
|
||||
"dependencies": {
|
||||
"express": "^4.18.0",
|
||||
"better-sqlite3": "^9.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Dependencies
|
||||
|
||||
```bash
|
||||
# Add to root
|
||||
pnpm add -D typescript
|
||||
|
||||
# Add to specific project
|
||||
pnpm add express --filter server
|
||||
|
||||
# Add to multiple projects
|
||||
pnpm add lodash --filter server --filter client
|
||||
```
|
||||
|
||||
### Workspace References
|
||||
|
||||
Internal packages are referenced using workspace protocol:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@triliumnext/commons": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Configuration
|
||||
|
||||
### Base Configuration
|
||||
|
||||
**`tsconfig.base.json`**
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "dom"],
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@triliumnext/commons": ["packages/commons/src/index.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Project-Specific Configuration
|
||||
|
||||
```json
|
||||
// apps/server/tsconfig.json
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["**/*.spec.ts"]
|
||||
}
|
||||
```
|
||||
|
||||
## Build Optimization
|
||||
|
||||
### NX Cloud
|
||||
|
||||
```bash
|
||||
# Enable NX Cloud for distributed caching
|
||||
pnpm nx connect-to-nx-cloud
|
||||
```
|
||||
|
||||
### Affected Commands
|
||||
|
||||
```bash
|
||||
# Build only affected projects
|
||||
pnpm nx affected:build --base=main
|
||||
|
||||
# Test only affected projects
|
||||
pnpm nx affected:test --base=main
|
||||
|
||||
# Lint only affected projects
|
||||
pnpm nx affected:lint --base=main
|
||||
```
|
||||
|
||||
### Build Caching
|
||||
|
||||
NX caches build outputs to speed up subsequent builds:
|
||||
|
||||
```bash
|
||||
# Clear cache
|
||||
pnpm nx reset
|
||||
|
||||
# Run with cache disabled
|
||||
pnpm nx build server --skip-nx-cache
|
||||
|
||||
# See cache statistics
|
||||
pnpm nx report
|
||||
```
|
||||
|
||||
## Production Builds
|
||||
|
||||
### Building for Production
|
||||
|
||||
```bash
|
||||
# Build server for production
|
||||
pnpm nx build server --configuration=production
|
||||
|
||||
# Build client with optimization
|
||||
pnpm nx build client --configuration=production
|
||||
|
||||
# Build desktop app
|
||||
pnpm nx build desktop --configuration=production
|
||||
pnpm electron:build # Creates distributables
|
||||
```
|
||||
|
||||
### Docker Build
|
||||
|
||||
```dockerfile
|
||||
# Multi-stage build
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json pnpm-lock.yaml ./
|
||||
RUN corepack enable && pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
RUN pnpm nx build server --configuration=production
|
||||
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/dist/apps/server ./
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
|
||||
CMD ["node", "main.js"]
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
### GitHub Actions Example
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- run: pnpm nx affected:lint --base=origin/main
|
||||
|
||||
- run: pnpm nx affected:test --base=origin/main
|
||||
|
||||
- run: pnpm nx affected:build --base=origin/main
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Build Cache Issues**
|
||||
```bash
|
||||
# Clear NX cache
|
||||
pnpm nx reset
|
||||
|
||||
# Clear node_modules and reinstall
|
||||
rm -rf node_modules
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. **Dependency Version Conflicts**
|
||||
```bash
|
||||
# Check for duplicate packages
|
||||
pnpm list --depth=0
|
||||
|
||||
# Update all dependencies
|
||||
pnpm update --recursive
|
||||
```
|
||||
|
||||
3. **TypeScript Path Resolution**
|
||||
```bash
|
||||
# Verify TypeScript paths
|
||||
pnpm nx run server:typecheck --traceResolution
|
||||
```
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Show project graph
|
||||
pnpm nx graph
|
||||
|
||||
# Show project dependencies
|
||||
pnpm nx print-affected --type=app --select=projects
|
||||
|
||||
# Verbose output
|
||||
pnpm nx build server --verbose
|
||||
|
||||
# Profile build performance
|
||||
pnpm nx build server --profile
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Project Structure
|
||||
|
||||
1. **Keep packages focused**: Each package should have a single, clear purpose
|
||||
2. **Minimize circular dependencies**: Use dependency graph to identify issues
|
||||
3. **Share common code**: Extract shared logic to packages/commons
|
||||
|
||||
### Development
|
||||
|
||||
1. **Use NX generators**: Generate consistent code structure
|
||||
2. **Leverage caching**: Don't skip-nx-cache unless debugging
|
||||
3. **Run affected commands**: Save time by only building/testing changed code
|
||||
|
||||
### Testing
|
||||
|
||||
1. **Colocate tests**: Keep test files next to source files
|
||||
2. **Use workspace scripts**: Define common scripts in root package.json
|
||||
3. **Parallel execution**: Use `--parallel` flag for faster execution
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Environment Setup](../Environment%20Setup.md) - Development environment setup
|
||||
- [Project Structure](../Project%20Structure.md) - Detailed project structure
|
||||
- [Build Information](../Development%20and%20architecture/Build%20information.md) - Build details
|
||||
89
docs/Developer Guide/Architecture/README.md
vendored
Normal file
89
docs/Developer Guide/Architecture/README.md
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
# Trilium Architecture Documentation
|
||||
|
||||
This comprehensive guide documents the architecture of Trilium Notes, providing developers with detailed information about the system's core components, data flow, and design patterns.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Three-Layer Cache System](Three-Layer-Cache-System.md)
|
||||
2. [Entity System](Entity-System.md)
|
||||
3. [Widget-Based UI Architecture](Widget-Based-UI-Architecture.md)
|
||||
4. [API Architecture](API-Architecture.md)
|
||||
5. [Monorepo Structure](Monorepo-Structure.md)
|
||||
|
||||
## Overview
|
||||
|
||||
Trilium Notes is built as a TypeScript monorepo using NX, featuring a sophisticated architecture that balances performance, flexibility, and maintainability. The system is designed around several key architectural patterns:
|
||||
|
||||
- **Three-layer caching system** for optimal performance across backend, frontend, and shared content
|
||||
- **Entity-based data model** supporting hierarchical note structures with multiple parent relationships
|
||||
- **Widget-based UI architecture** enabling modular and extensible interface components
|
||||
- **Multiple API layers** for internal operations, external integrations, and real-time synchronization
|
||||
- **Monorepo structure** facilitating code sharing and consistent development patterns
|
||||
|
||||
## Quick Start for Developers
|
||||
|
||||
If you're new to Trilium development, start with these sections:
|
||||
|
||||
1. [Monorepo Structure](Monorepo-Structure.md) - Understand the project organization
|
||||
2. [Entity System](Entity-System.md) - Learn about the core data model
|
||||
3. [Three-Layer Cache System](Three-Layer-Cache-System.md) - Understand data flow and caching
|
||||
|
||||
For UI development, refer to:
|
||||
- [Widget-Based UI Architecture](Widget-Based-UI-Architecture.md)
|
||||
|
||||
For API integration, see:
|
||||
- [API Architecture](API-Architecture.md)
|
||||
|
||||
## Architecture Principles
|
||||
|
||||
### Performance First
|
||||
- Lazy loading of note content
|
||||
- Efficient caching at multiple layers
|
||||
- Optimized database queries with prepared statements
|
||||
|
||||
### Flexibility
|
||||
- Support for multiple note types
|
||||
- Extensible through scripting
|
||||
- Plugin architecture for UI widgets
|
||||
|
||||
### Data Integrity
|
||||
- Transactional database operations
|
||||
- Revision history for all changes
|
||||
- Synchronization conflict resolution
|
||||
|
||||
### Security
|
||||
- Per-note encryption
|
||||
- Protected sessions
|
||||
- API authentication tokens
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Setup Development Environment**
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm run server:start
|
||||
```
|
||||
|
||||
2. **Make Changes**
|
||||
- Backend changes in `apps/server/src/`
|
||||
- Frontend changes in `apps/client/src/`
|
||||
- Shared code in `packages/`
|
||||
|
||||
3. **Test Your Changes**
|
||||
```bash
|
||||
pnpm test:all
|
||||
pnpm nx run <project>:lint
|
||||
```
|
||||
|
||||
4. **Build for Production**
|
||||
```bash
|
||||
pnpm nx build server
|
||||
pnpm nx build client
|
||||
```
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Development Environment Setup](../Environment%20Setup.md)
|
||||
- [Adding a New Note Type](../Development%20and%20architecture/Adding%20a%20new%20note%20type/First%20steps.md)
|
||||
- [Database Schema](../Development%20and%20architecture/Database/notes.md)
|
||||
- [Script API Documentation](../../Script%20API/)
|
||||
369
docs/Developer Guide/Architecture/Three-Layer-Cache-System.md
vendored
Normal file
369
docs/Developer Guide/Architecture/Three-Layer-Cache-System.md
vendored
Normal file
@@ -0,0 +1,369 @@
|
||||
# Three-Layer Cache System Architecture
|
||||
|
||||
Trilium implements a sophisticated three-layer caching system to optimize performance and reduce database load. This architecture ensures fast access to frequently used data while maintaining consistency across different application contexts.
|
||||
|
||||
## Overview
|
||||
|
||||
The three cache layers are:
|
||||
|
||||
1. **Becca** (Backend Cache) - Server-side entity cache
|
||||
2. **Froca** (Frontend Cache) - Client-side mirror of backend data
|
||||
3. **Shaca** (Share Cache) - Optimized cache for shared/published notes
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Database Layer"
|
||||
DB[(SQLite Database)]
|
||||
end
|
||||
|
||||
subgraph "Backend Layer"
|
||||
Becca[Becca Cache<br/>Backend Cache]
|
||||
API[API Layer]
|
||||
end
|
||||
|
||||
subgraph "Frontend Layer"
|
||||
Froca[Froca Cache<br/>Frontend Cache]
|
||||
UI[UI Components]
|
||||
end
|
||||
|
||||
subgraph "Share Layer"
|
||||
Shaca[Shaca Cache<br/>Share Cache]
|
||||
Share[Public Share Interface]
|
||||
end
|
||||
|
||||
DB <--> Becca
|
||||
Becca <--> API
|
||||
API <--> Froca
|
||||
Froca <--> UI
|
||||
DB <--> Shaca
|
||||
Shaca <--> Share
|
||||
|
||||
style Becca fill:#e1f5fe
|
||||
style Froca fill:#fff3e0
|
||||
style Shaca fill:#f3e5f5
|
||||
```
|
||||
|
||||
## Becca (Backend Cache)
|
||||
|
||||
**Location**: `/apps/server/src/becca/`
|
||||
|
||||
Becca is the authoritative cache layer that maintains all notes, branches, attributes, and options in server memory.
|
||||
|
||||
### Key Components
|
||||
|
||||
#### Becca Interface (`becca-interface.ts`)
|
||||
|
||||
```typescript
|
||||
export default class Becca {
|
||||
loaded: boolean;
|
||||
notes: Record<string, BNote>;
|
||||
branches: Record<string, BBranch>;
|
||||
childParentToBranch: Record<string, BBranch>;
|
||||
attributes: Record<string, BAttribute>;
|
||||
attributeIndex: Record<string, BAttribute[]>;
|
||||
options: Record<string, BOption>;
|
||||
etapiTokens: Record<string, BEtapiToken>;
|
||||
allNoteSetCache: NoteSet | null;
|
||||
}
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- **In-memory storage**: All active entities are kept in memory for fast access
|
||||
- **Lazy loading**: Related entities (revisions, attachments) loaded on demand
|
||||
- **Index structures**: Optimized lookups via `childParentToBranch` and `attributeIndex`
|
||||
- **Cache invalidation**: Automatic cache updates on entity changes
|
||||
- **Protected note decryption**: On-demand decryption of encrypted content
|
||||
|
||||
### Usage Example
|
||||
|
||||
```typescript
|
||||
import becca from "./becca/becca.js";
|
||||
|
||||
// Get a note
|
||||
const note = becca.getNote("noteId");
|
||||
|
||||
// Find attributes by type and name
|
||||
const labels = becca.findAttributes("label", "todoItem");
|
||||
|
||||
// Get branch relationships
|
||||
const branch = becca.getBranchFromChildAndParent(childId, parentId);
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
1. **Initialization**: Load all notes, branches, and attributes from database
|
||||
2. **Access**: Direct memory access for cached entities
|
||||
3. **Updates**: Write-through cache with immediate database persistence
|
||||
4. **Invalidation**: Automatic cache refresh on entity changes
|
||||
|
||||
## Froca (Frontend Cache)
|
||||
|
||||
**Location**: `/apps/client/src/services/froca.ts`
|
||||
|
||||
Froca is the frontend mirror of Becca, maintaining a subset of backend data for client-side operations.
|
||||
|
||||
### Key Components
|
||||
|
||||
#### Froca Implementation (`froca.ts`)
|
||||
|
||||
```typescript
|
||||
class FrocaImpl implements Froca {
|
||||
notes: Record<string, FNote>;
|
||||
branches: Record<string, FBranch>;
|
||||
attributes: Record<string, FAttribute>;
|
||||
attachments: Record<string, FAttachment>;
|
||||
blobPromises: Record<string, Promise<FBlob | null> | null>;
|
||||
}
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- **Lazy loading**: Notes loaded on-demand with their immediate context
|
||||
- **Subtree loading**: Efficient loading of note hierarchies
|
||||
- **Real-time updates**: WebSocket synchronization with backend changes
|
||||
- **Search note support**: Virtual branches for search results
|
||||
- **Promise-based blob loading**: Asynchronous content loading
|
||||
|
||||
### Loading Strategy
|
||||
|
||||
```typescript
|
||||
// Initial load - loads root and immediate children
|
||||
await froca.loadInitialTree();
|
||||
|
||||
// Load subtree on demand
|
||||
const note = await froca.loadSubTree(noteId);
|
||||
|
||||
// Reload specific notes
|
||||
await froca.reloadNotes([noteId1, noteId2]);
|
||||
```
|
||||
|
||||
### Synchronization
|
||||
|
||||
Froca maintains consistency with Becca through:
|
||||
|
||||
1. **Initial sync**: Load essential tree structure on startup
|
||||
2. **On-demand loading**: Fetch notes as needed
|
||||
3. **WebSocket updates**: Real-time push of changes from backend
|
||||
4. **Batch reloading**: Efficient refresh of multiple notes
|
||||
|
||||
## Shaca (Share Cache)
|
||||
|
||||
**Location**: `/apps/server/src/share/shaca/`
|
||||
|
||||
Shaca is a specialized cache for publicly shared notes, optimized for read-only access.
|
||||
|
||||
### Key Components
|
||||
|
||||
#### Shaca Interface (`shaca-interface.ts`)
|
||||
|
||||
```typescript
|
||||
export default class Shaca {
|
||||
notes: Record<string, SNote>;
|
||||
branches: Record<string, SBranch>;
|
||||
childParentToBranch: Record<string, SBranch>;
|
||||
attributes: Record<string, SAttribute>;
|
||||
attachments: Record<string, SAttachment>;
|
||||
aliasToNote: Record<string, SNote>;
|
||||
shareRootNote: SNote | null;
|
||||
shareIndexEnabled: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- **Read-only optimization**: Streamlined for public access
|
||||
- **Alias support**: URL-friendly note access via aliases
|
||||
- **Share index**: Optional indexing of all shared subtrees
|
||||
- **Minimal memory footprint**: Only shared content cached
|
||||
- **Security isolation**: Separate from main application cache
|
||||
|
||||
### Usage Patterns
|
||||
|
||||
```typescript
|
||||
// Get shared note by ID
|
||||
const note = shaca.getNote(noteId);
|
||||
|
||||
// Access via alias
|
||||
const aliasedNote = shaca.aliasToNote[alias];
|
||||
|
||||
// Check if note is shared
|
||||
const isShared = shaca.hasNote(noteId);
|
||||
```
|
||||
|
||||
## Cache Interaction and Data Flow
|
||||
|
||||
### 1. Create/Update Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Froca
|
||||
participant API
|
||||
participant Becca
|
||||
participant DB
|
||||
|
||||
Client->>API: Update Note
|
||||
API->>Becca: Update Cache
|
||||
Becca->>DB: Persist Change
|
||||
Becca->>API: Confirm
|
||||
API->>Froca: Push Update (WebSocket)
|
||||
Froca->>Client: Update UI
|
||||
```
|
||||
|
||||
### 2. Read Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Froca
|
||||
participant API
|
||||
participant Becca
|
||||
|
||||
Client->>Froca: Request Note
|
||||
alt Note in Cache
|
||||
Froca->>Client: Return Cached Note
|
||||
else Note not in Cache
|
||||
Froca->>API: Fetch Note
|
||||
API->>Becca: Get Note
|
||||
Becca->>API: Return Note
|
||||
API->>Froca: Send Note Data
|
||||
Froca->>Froca: Cache Note
|
||||
Froca->>Client: Return Note
|
||||
end
|
||||
```
|
||||
|
||||
### 3. Share Access Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Browser
|
||||
participant ShareUI
|
||||
participant Shaca
|
||||
participant DB
|
||||
|
||||
Browser->>ShareUI: Access Shared URL
|
||||
ShareUI->>Shaca: Get Shared Note
|
||||
alt Note in Cache
|
||||
Shaca->>ShareUI: Return Cached
|
||||
else Not in Cache
|
||||
Shaca->>DB: Load Shared Tree
|
||||
DB->>Shaca: Return Data
|
||||
Shaca->>Shaca: Build Cache
|
||||
Shaca->>ShareUI: Return Note
|
||||
end
|
||||
ShareUI->>Browser: Render Content
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Memory Management
|
||||
|
||||
- **Becca**: Keeps entire note tree in memory (~100-500MB for typical use)
|
||||
- **Froca**: Loads notes on-demand, automatic cleanup of unused notes
|
||||
- **Shaca**: Minimal footprint, only shared content
|
||||
|
||||
### Cache Warming
|
||||
|
||||
- **Becca**: Full load on server startup
|
||||
- **Froca**: Progressive loading based on user navigation
|
||||
- **Shaca**: Lazy loading with configurable index
|
||||
|
||||
### Optimization Strategies
|
||||
|
||||
1. **Attribute Indexing**: Pre-built indexes for fast attribute queries
|
||||
2. **Batch Operations**: Group updates to minimize round trips
|
||||
3. **Partial Loading**: Load only required fields for lists
|
||||
4. **WebSocket Compression**: Compressed real-time updates
|
||||
|
||||
## Best Practices
|
||||
|
||||
### When to Use Each Cache
|
||||
|
||||
**Use Becca when**:
|
||||
- Implementing server-side business logic
|
||||
- Performing bulk operations
|
||||
- Handling synchronization
|
||||
- Managing protected notes
|
||||
|
||||
**Use Froca when**:
|
||||
- Building UI components
|
||||
- Handling user interactions
|
||||
- Displaying note content
|
||||
- Managing client state
|
||||
|
||||
**Use Shaca when**:
|
||||
- Serving public content
|
||||
- Building share pages
|
||||
- Implementing read-only access
|
||||
- Creating public APIs
|
||||
|
||||
### Cache Invalidation
|
||||
|
||||
```typescript
|
||||
// Becca - automatic on entity save
|
||||
note.save(); // Cache updated automatically
|
||||
|
||||
// Froca - manual reload when needed
|
||||
await froca.reloadNotes([noteId]);
|
||||
|
||||
// Shaca - rebuild on share changes
|
||||
shaca.reset();
|
||||
shaca.load();
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
// Becca - throw on missing required entities
|
||||
const note = becca.getNoteOrThrow(noteId); // throws NotFoundError
|
||||
|
||||
// Froca - graceful degradation
|
||||
const note = await froca.getNote(noteId);
|
||||
if (!note) {
|
||||
// Handle missing note
|
||||
}
|
||||
|
||||
// Shaca - check existence first
|
||||
if (shaca.hasNote(noteId)) {
|
||||
const note = shaca.getNote(noteId);
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Cache Inconsistency**
|
||||
- Symptom: UI shows outdated data
|
||||
- Solution: Force reload with `froca.reloadNotes()`
|
||||
|
||||
2. **Memory Growth**
|
||||
- Symptom: Server memory usage increases
|
||||
- Solution: Check for memory leaks in custom scripts
|
||||
|
||||
3. **Slow Initial Load**
|
||||
- Symptom: Long startup time
|
||||
- Solution: Optimize database queries, add indexes
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```javascript
|
||||
// Check cache sizes
|
||||
console.log('Becca notes:', Object.keys(becca.notes).length);
|
||||
console.log('Froca notes:', Object.keys(froca.notes).length);
|
||||
console.log('Shaca notes:', Object.keys(shaca.notes).length);
|
||||
|
||||
// Force cache refresh
|
||||
await froca.loadInitialTree();
|
||||
|
||||
// Clear and reload Shaca
|
||||
shaca.reset();
|
||||
await shaca.load();
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Entity System](Entity-System.md) - Detailed entity documentation
|
||||
- [Database Schema](../Development%20and%20architecture/Database/notes.md) - Database structure
|
||||
- [WebSocket Synchronization](API-Architecture.md#websocket-real-time-synchronization) - Real-time updates
|
||||
635
docs/Developer Guide/Architecture/Widget-Based-UI-Architecture.md
vendored
Normal file
635
docs/Developer Guide/Architecture/Widget-Based-UI-Architecture.md
vendored
Normal file
@@ -0,0 +1,635 @@
|
||||
# Widget-Based UI Architecture
|
||||
|
||||
Trilium's frontend is built on a modular widget system that provides flexibility, reusability, and maintainability. This architecture enables dynamic UI composition and extensibility through custom widgets.
|
||||
|
||||
## Widget System Overview
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Widget Hierarchy"
|
||||
Component[Component<br/>Base Class]
|
||||
BasicWidget[BasicWidget<br/>UI Foundation]
|
||||
NoteContextAware[NoteContextAwareWidget<br/>Note-Aware]
|
||||
RightPanel[RightPanelWidget<br/>Side Panel]
|
||||
TypeWidgets[Type Widgets<br/>Note Type Specific]
|
||||
CustomWidgets[Custom Widgets<br/>User Scripts]
|
||||
end
|
||||
|
||||
Component --> BasicWidget
|
||||
BasicWidget --> NoteContextAware
|
||||
NoteContextAware --> RightPanel
|
||||
NoteContextAware --> TypeWidgets
|
||||
NoteContextAware --> CustomWidgets
|
||||
|
||||
style Component fill:#e8f5e9
|
||||
style BasicWidget fill:#c8e6c9
|
||||
style NoteContextAware fill:#a5d6a7
|
||||
```
|
||||
|
||||
## Core Widget Classes
|
||||
|
||||
### Component (Base Class)
|
||||
|
||||
**Location**: `/apps/client/src/components/component.js`
|
||||
|
||||
The foundational class for all UI components in Trilium.
|
||||
|
||||
```typescript
|
||||
class Component {
|
||||
componentId: string; // Unique identifier
|
||||
children: Component[]; // Child components
|
||||
parent: Component | null; // Parent reference
|
||||
|
||||
async refresh(): Promise<void>;
|
||||
child(...components: Component[]): this;
|
||||
handleEvent(name: string, data: any): void;
|
||||
trigger(name: string, data?: any): void;
|
||||
}
|
||||
```
|
||||
|
||||
### BasicWidget
|
||||
|
||||
**Location**: `/apps/client/src/widgets/basic_widget.ts`
|
||||
|
||||
Base class for all UI widgets, providing DOM manipulation and styling capabilities.
|
||||
|
||||
```typescript
|
||||
export class BasicWidget extends Component {
|
||||
protected $widget: JQuery;
|
||||
private attrs: Record<string, string>;
|
||||
private classes: string[];
|
||||
|
||||
// Chaining methods for declarative UI
|
||||
id(id: string): this;
|
||||
class(className: string): this;
|
||||
css(name: string, value: string): this;
|
||||
contentSized(): this;
|
||||
collapsible(): this;
|
||||
filling(): this;
|
||||
|
||||
// Conditional rendering
|
||||
optChild(condition: boolean, ...components: Component[]): this;
|
||||
optCss(condition: boolean, name: string, value: string): this;
|
||||
|
||||
// Rendering
|
||||
doRender(): JQuery;
|
||||
}
|
||||
```
|
||||
|
||||
#### Usage Example
|
||||
|
||||
```typescript
|
||||
class MyWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div>')
|
||||
.addClass('my-widget')
|
||||
.append($('<h3>').text('Widget Title'));
|
||||
|
||||
return this.$widget;
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
this.$widget.find('h3').text(note.title);
|
||||
}
|
||||
}
|
||||
|
||||
// Composing widgets
|
||||
const container = new FlexContainer('column')
|
||||
.id('main-container')
|
||||
.css('padding', '10px')
|
||||
.filling()
|
||||
.child(
|
||||
new MyWidget(),
|
||||
new ButtonWidget()
|
||||
.title('Click Me')
|
||||
.onClick(() => console.log('Clicked'))
|
||||
);
|
||||
```
|
||||
|
||||
### NoteContextAwareWidget
|
||||
|
||||
**Location**: `/apps/client/src/widgets/note_context_aware_widget.ts`
|
||||
|
||||
Base class for widgets that respond to note context changes.
|
||||
|
||||
```typescript
|
||||
class NoteContextAwareWidget extends BasicWidget {
|
||||
noteContext: NoteContext | null;
|
||||
note: FNote | null;
|
||||
noteId: string | null;
|
||||
notePath: string | null;
|
||||
|
||||
// Lifecycle methods
|
||||
async refresh(): Promise<void>;
|
||||
async refreshWithNote(note: FNote): Promise<void>;
|
||||
async noteSwitched(): Promise<void>;
|
||||
async activeContextChanged(): Promise<void>;
|
||||
|
||||
// Event handlers
|
||||
async noteTypeMimeChanged(): Promise<void>;
|
||||
async frocaReloaded(): Promise<void>;
|
||||
|
||||
// Utility methods
|
||||
isNote(noteId: string): boolean;
|
||||
get isEnabled(): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
#### Context Management
|
||||
|
||||
```typescript
|
||||
class MyNoteWidget extends NoteContextAwareWidget {
|
||||
async refreshWithNote(note: FNote) {
|
||||
// Called when note context changes
|
||||
this.$widget.find('.note-title').text(note.title);
|
||||
this.$widget.find('.note-type').text(note.type);
|
||||
|
||||
// Access note attributes
|
||||
const labels = note.getLabels();
|
||||
const relations = note.getRelations();
|
||||
}
|
||||
|
||||
async noteSwitched() {
|
||||
// Called when user switches to different note
|
||||
console.log(`Switched to note: ${this.noteId}`);
|
||||
}
|
||||
|
||||
async noteTypeMimeChanged() {
|
||||
// React to note type changes
|
||||
if (this.note?.type === 'code') {
|
||||
this.setupCodeHighlighting();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### RightPanelWidget
|
||||
|
||||
**Location**: `/apps/client/src/widgets/right_panel_widget.ts`
|
||||
|
||||
Base class for widgets displayed in the right sidebar panel.
|
||||
|
||||
```typescript
|
||||
abstract class RightPanelWidget extends NoteContextAwareWidget {
|
||||
async doRenderBody(): Promise<JQuery>;
|
||||
getTitle(): string;
|
||||
getIcon(): string;
|
||||
getPosition(): number;
|
||||
|
||||
async isEnabled(): Promise<boolean> {
|
||||
// Override to control visibility
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Creating Right Panel Widgets
|
||||
|
||||
```typescript
|
||||
class InfoWidget extends RightPanelWidget {
|
||||
getTitle() { return "Note Info"; }
|
||||
getIcon() { return "info"; }
|
||||
getPosition() { return 100; }
|
||||
|
||||
async doRenderBody() {
|
||||
return $('<div class="info-widget">')
|
||||
.append($('<div class="created">'))
|
||||
.append($('<div class="modified">'))
|
||||
.append($('<div class="word-count">'));
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
this.$body.find('.created').text(`Created: ${note.dateCreated}`);
|
||||
this.$body.find('.modified').text(`Modified: ${note.dateModified}`);
|
||||
|
||||
const wordCount = this.calculateWordCount(await note.getContent());
|
||||
this.$body.find('.word-count').text(`Words: ${wordCount}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Type-Specific Widgets
|
||||
|
||||
**Location**: `/apps/client/src/widgets/type_widgets/`
|
||||
|
||||
Each note type has a specialized widget for rendering and editing.
|
||||
|
||||
### TypeWidget Interface
|
||||
|
||||
```typescript
|
||||
abstract class TypeWidget extends NoteContextAwareWidget {
|
||||
abstract static getType(): string;
|
||||
|
||||
// Content management
|
||||
async getContent(): Promise<string>;
|
||||
async saveContent(content: string): Promise<void>;
|
||||
|
||||
// Focus management
|
||||
async focus(): Promise<void>;
|
||||
async blur(): Promise<void>;
|
||||
|
||||
// Cleanup
|
||||
async cleanup(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### Common Type Widgets
|
||||
|
||||
#### TextTypeWidget
|
||||
|
||||
```typescript
|
||||
class TextTypeWidget extends TypeWidget {
|
||||
static getType() { return 'text'; }
|
||||
|
||||
private textEditor: TextEditor;
|
||||
|
||||
async doRender() {
|
||||
const $editor = $('<div class="ck-editor">');
|
||||
this.textEditor = await TextEditor.create($editor[0], {
|
||||
noteId: this.noteId,
|
||||
content: await this.note.getContent()
|
||||
});
|
||||
|
||||
return $editor;
|
||||
}
|
||||
|
||||
async getContent() {
|
||||
return this.textEditor.getData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### CodeTypeWidget
|
||||
|
||||
```typescript
|
||||
class CodeTypeWidget extends TypeWidget {
|
||||
static getType() { return 'code'; }
|
||||
|
||||
private codeMirror: CodeMirror;
|
||||
|
||||
async doRender() {
|
||||
const $container = $('<div class="code-editor">');
|
||||
|
||||
this.codeMirror = CodeMirror($container[0], {
|
||||
value: await this.note.getContent(),
|
||||
mode: this.note.mime,
|
||||
theme: 'default',
|
||||
lineNumbers: true
|
||||
});
|
||||
|
||||
return $container;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Widget Composition
|
||||
|
||||
### Container Widgets
|
||||
|
||||
```typescript
|
||||
// Flexible container layouts
|
||||
class FlexContainer extends BasicWidget {
|
||||
constructor(private direction: 'row' | 'column') {
|
||||
super();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $('<div class="flex-container">')
|
||||
.css('display', 'flex')
|
||||
.css('flex-direction', this.direction);
|
||||
|
||||
for (const child of this.children) {
|
||||
this.$widget.append(child.render());
|
||||
}
|
||||
|
||||
return this.$widget;
|
||||
}
|
||||
}
|
||||
|
||||
// Tab container
|
||||
class TabContainer extends BasicWidget {
|
||||
private tabs: Array<{title: string, widget: BasicWidget}> = [];
|
||||
|
||||
addTab(title: string, widget: BasicWidget) {
|
||||
this.tabs.push({title, widget});
|
||||
this.child(widget);
|
||||
return this;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
// Render tab headers and content panels
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Composite Widgets
|
||||
|
||||
```typescript
|
||||
class NoteEditorWidget extends NoteContextAwareWidget {
|
||||
private typeWidget: TypeWidget;
|
||||
private titleWidget: NoteTitleWidget;
|
||||
private toolbarWidget: NoteToolbarWidget;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.child(
|
||||
this.toolbarWidget = new NoteToolbarWidget(),
|
||||
this.titleWidget = new NoteTitleWidget(),
|
||||
// Type widget added dynamically
|
||||
);
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
// Remove old type widget
|
||||
if (this.typeWidget) {
|
||||
this.typeWidget.remove();
|
||||
}
|
||||
|
||||
// Add appropriate type widget
|
||||
const WidgetClass = typeWidgetService.getWidgetClass(note.type);
|
||||
this.typeWidget = new WidgetClass();
|
||||
this.child(this.typeWidget);
|
||||
|
||||
await this.typeWidget.refresh();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Widget Communication
|
||||
|
||||
### Event System
|
||||
|
||||
```typescript
|
||||
// Publishing events
|
||||
class PublisherWidget extends BasicWidget {
|
||||
async handleClick() {
|
||||
// Local event
|
||||
this.trigger('itemSelected', { itemId: '123' });
|
||||
|
||||
// Global event
|
||||
appContext.triggerEvent('noteChanged', { noteId: this.noteId });
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribing to events
|
||||
class SubscriberWidget extends BasicWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Local event subscription
|
||||
this.on('itemSelected', (event) => {
|
||||
console.log('Item selected:', event.itemId);
|
||||
});
|
||||
|
||||
// Global event subscription
|
||||
appContext.addEventListener('noteChanged', (event) => {
|
||||
this.handleNoteChange(event.noteId);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Command System
|
||||
|
||||
```typescript
|
||||
// Registering commands
|
||||
class CommandWidget extends BasicWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.bindCommand('saveNote', () => this.saveNote());
|
||||
this.bindCommand('deleteNote', () => this.deleteNote());
|
||||
}
|
||||
|
||||
getCommands() {
|
||||
return [
|
||||
{
|
||||
command: 'myWidget:doAction',
|
||||
handler: () => this.doAction(),
|
||||
hotkey: 'ctrl+shift+a'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Widget Development
|
||||
|
||||
### Creating Custom Widgets
|
||||
|
||||
```typescript
|
||||
// 1. Define widget class
|
||||
class TaskListWidget extends NoteContextAwareWidget {
|
||||
doRender() {
|
||||
this.$widget = $('<div class="task-list-widget">');
|
||||
this.$list = $('<ul>').appendTo(this.$widget);
|
||||
return this.$widget;
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
const tasks = await this.loadTasks(note);
|
||||
|
||||
this.$list.empty();
|
||||
for (const task of tasks) {
|
||||
$('<li>')
|
||||
.text(task.title)
|
||||
.toggleClass('completed', task.completed)
|
||||
.appendTo(this.$list);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadTasks(note: FNote) {
|
||||
// Load task data from note attributes
|
||||
const taskLabels = note.getLabels('task');
|
||||
return taskLabels.map(label => JSON.parse(label.value));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Register widget
|
||||
api.addWidget(TaskListWidget);
|
||||
```
|
||||
|
||||
### Widget Lifecycle
|
||||
|
||||
```typescript
|
||||
class LifecycleWidget extends NoteContextAwareWidget {
|
||||
// 1. Construction
|
||||
constructor() {
|
||||
super();
|
||||
console.log('Widget constructed');
|
||||
}
|
||||
|
||||
// 2. Initial render
|
||||
doRender() {
|
||||
console.log('Initial render');
|
||||
return $('<div>');
|
||||
}
|
||||
|
||||
// 3. Context initialization
|
||||
async refresh() {
|
||||
console.log('Context refresh');
|
||||
await super.refresh();
|
||||
}
|
||||
|
||||
// 4. Note updates
|
||||
async refreshWithNote(note: FNote) {
|
||||
console.log('Note refresh:', note.noteId);
|
||||
}
|
||||
|
||||
// 5. Cleanup
|
||||
async cleanup() {
|
||||
console.log('Widget cleanup');
|
||||
// Release resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
```typescript
|
||||
class LazyWidget extends BasicWidget {
|
||||
private contentLoaded = false;
|
||||
|
||||
async becomeVisible() {
|
||||
if (!this.contentLoaded) {
|
||||
await this.loadContent();
|
||||
this.contentLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadContent() {
|
||||
// Heavy content loading
|
||||
const data = await server.get('expensive-data');
|
||||
this.renderContent(data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Debouncing Updates
|
||||
|
||||
```typescript
|
||||
class DebouncedWidget extends NoteContextAwareWidget {
|
||||
private refreshDebounced = utils.debounce(
|
||||
() => this.doRefresh(),
|
||||
500
|
||||
);
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
// Debounce rapid updates
|
||||
this.refreshDebounced();
|
||||
}
|
||||
|
||||
private async doRefresh() {
|
||||
// Actual refresh logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Virtual Scrolling
|
||||
|
||||
```typescript
|
||||
class VirtualListWidget extends BasicWidget {
|
||||
private visibleItems: any[] = [];
|
||||
|
||||
renderVisibleItems(scrollTop: number) {
|
||||
const itemHeight = 30;
|
||||
const containerHeight = this.$widget.height();
|
||||
|
||||
const startIndex = Math.floor(scrollTop / itemHeight);
|
||||
const endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight);
|
||||
|
||||
this.visibleItems = this.allItems.slice(startIndex, endIndex);
|
||||
this.renderItems();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Widget Design
|
||||
|
||||
1. **Single Responsibility**: Each widget should have one clear purpose
|
||||
2. **Composition over Inheritance**: Use composition for complex UIs
|
||||
3. **Lazy Initialization**: Load resources only when needed
|
||||
4. **Event Cleanup**: Remove event listeners in cleanup()
|
||||
|
||||
### State Management
|
||||
|
||||
```typescript
|
||||
class StatefulWidget extends NoteContextAwareWidget {
|
||||
private state = {
|
||||
isExpanded: false,
|
||||
selectedItems: new Set<string>()
|
||||
};
|
||||
|
||||
setState(updates: Partial<typeof this.state>) {
|
||||
Object.assign(this.state, updates);
|
||||
this.renderState();
|
||||
}
|
||||
|
||||
private renderState() {
|
||||
this.$widget.toggleClass('expanded', this.state.isExpanded);
|
||||
// Update DOM based on state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
class ResilientWidget extends BasicWidget {
|
||||
async refreshWithNote(note: FNote) {
|
||||
try {
|
||||
await this.loadData(note);
|
||||
} catch (error) {
|
||||
this.showError('Failed to load data');
|
||||
console.error('Widget error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private showError(message: string) {
|
||||
this.$widget.html(`
|
||||
<div class="alert alert-danger">
|
||||
${message}
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Widgets
|
||||
|
||||
```typescript
|
||||
// Widget test example
|
||||
describe('TaskListWidget', () => {
|
||||
let widget: TaskListWidget;
|
||||
let note: FNote;
|
||||
|
||||
beforeEach(() => {
|
||||
widget = new TaskListWidget();
|
||||
note = createMockNote({
|
||||
noteId: 'test123',
|
||||
attributes: [
|
||||
{ type: 'label', name: 'task', value: '{"title":"Task 1"}' }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should render tasks', async () => {
|
||||
await widget.refreshWithNote(note);
|
||||
|
||||
const tasks = widget.$widget.find('li');
|
||||
expect(tasks.length).toBe(1);
|
||||
expect(tasks.text()).toBe('Task 1');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Frontend Basics](../../Scripting/Frontend%20Basics.html) - Frontend scripting guide
|
||||
- [Custom Widgets](../../Scripting/Custom%20Widgets.html) - Creating custom widgets
|
||||
- [Script API](../../Script%20API/) - Widget API reference
|
||||
@@ -14,5 +14,5 @@ sudo docker run -p 8081:8080 triliumnext/trilium:v0.90.6-beta
|
||||
To enter a shell in the Docker container:
|
||||
|
||||
```
|
||||
sudo docker run -it --entrypoint=/bin/sh zadam/trilium:0.63-latest
|
||||
sudo docker run -it --entrypoint=/bin/sh TriliumNext/Trilium:0.63-latest
|
||||
```
|
||||
@@ -3,7 +3,7 @@ The note type is defined by the `type` column in <a class="reference-link" href
|
||||
|
||||
Possible types:
|
||||
|
||||
<table class="ck-table-resized"><colgroup><col> <col> <col> <col> <col></colgroup><thead><tr><th>Note Type</th><th><code>type</code> value</th><th>Corresponding MIME type</th><th>Content of the note's blob</th><th>Relevant attributes</th></tr></thead><tbody><tr><th>Text</th><td><code>text</code></td><td> </td><td>The HTML of the note.</td><td> </td></tr><tr><th><a href="https://github.com/zadam/trilium/wiki/Relation-map">Relation Map </a></th><td><code>relationMap</code></td><td><code>application/json</code></td><td><p>A JSON describing the note:</p><pre><code class="language-text-plain">{
|
||||
<table class="ck-table-resized"><colgroup><col> <col> <col> <col> <col></colgroup><thead><tr><th>Note Type</th><th><code>type</code> value</th><th>Corresponding MIME type</th><th>Content of the note's blob</th><th>Relevant attributes</th></tr></thead><tbody><tr><th>Text</th><td><code>text</code></td><td> </td><td>The HTML of the note.</td><td> </td></tr><tr><th><a href="https://github.com/TriliumNext/Trilium/wiki/Relation-map">Relation Map </a></th><td><code>relationMap</code></td><td><code>application/json</code></td><td><p>A JSON describing the note:</p><pre><code class="language-text-plain">{
|
||||
"notes": [
|
||||
{
|
||||
"noteId": "gFQDL11KEm9G",
|
||||
@@ -21,7 +21,7 @@ Possible types:
|
||||
"x": 480.29766098682165,
|
||||
"y": 116.83892021963081
|
||||
}
|
||||
}</code></pre></td><td>None</td></tr><tr><th><a href="https://github.com/zadam/trilium/wiki/Scripts">Render Note</a></th><td><code>render</code></td><td><code>text/html</code> or blank.</td><td>An empty blob.</td><td><code>~renderNote</code> pointing to the HTML note to render.</td></tr><tr><th>Canvas</th><td><code>canvas</code></td><td><code>application/json</code></td><td><pre><code class="language-text-plain">{
|
||||
}</code></pre></td><td>None</td></tr><tr><th><a href="https://github.com/TriliumNext/Trilium/wiki/Scripts">Render Note</a></th><td><code>render</code></td><td><code>text/html</code> or blank.</td><td>An empty blob.</td><td><code>~renderNote</code> pointing to the HTML note to render.</td></tr><tr><th>Canvas</th><td><code>canvas</code></td><td><code>application/json</code></td><td><pre><code class="language-text-plain">{
|
||||
"appState": {},
|
||||
"elemenets": {},
|
||||
"files": {},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user