mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 00:06:30 +01:00
Compare commits
25 Commits
feat/impro
...
feat/ui-op
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a9c0234e2 | ||
|
|
3e3cc8c541 | ||
|
|
d1538508e8 | ||
|
|
9b1da8c311 | ||
|
|
e4a8258acf | ||
|
|
5e88043c7b | ||
|
|
bedf9112fb | ||
|
|
03681d23c5 | ||
|
|
4c8da70ef3 | ||
|
|
ed5da5cd4a | ||
|
|
dc5fccdbcd | ||
|
|
91aea333c7 | ||
|
|
a0de01cff1 | ||
|
|
a41ed34193 | ||
|
|
49e8811c18 | ||
|
|
488563a82e | ||
|
|
a1b18c7f97 | ||
|
|
9958a6e1bf | ||
|
|
1fc6d8aca7 | ||
|
|
3e9ec2d943 | ||
|
|
1420def1c3 | ||
|
|
3b4184e765 | ||
|
|
b70e25d348 | ||
|
|
772c0bbe1a | ||
|
|
144021c053 |
@@ -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 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.
|
||||
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.
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ fi
|
||||
VERSION=$1
|
||||
SERIES=${VERSION:0:4}-latest
|
||||
|
||||
docker push TriliumNext/Trilium:$VERSION
|
||||
docker push TriliumNext/Trilium:$SERIES
|
||||
docker push zadam/trilium:$VERSION
|
||||
docker push zadam/trilium:$SERIES
|
||||
|
||||
if [[ $1 != *"beta"* ]]; then
|
||||
docker push TriliumNext/Trilium:latest
|
||||
docker push zadam/trilium:latest
|
||||
fi
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"chore:generate-openapi": "tsx bin/generate-openapi.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.54.2",
|
||||
"@playwright/test": "1.55.0",
|
||||
"@stylistic/eslint-plugin": "5.2.3",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/node": "22.17.2",
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.50.1",
|
||||
"globals": "16.3.0",
|
||||
"i18next": "25.3.6",
|
||||
"i18next": "25.4.0",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
@@ -52,7 +52,7 @@
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.27.1",
|
||||
"react-i18next": "15.6.1",
|
||||
"react-i18next": "15.7.0",
|
||||
"split.js": "1.6.5",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
|
||||
@@ -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/TriliumNext/Trilium/issues/3761
|
||||
// inherited promoted attributes should stay grouped: https://github.com/zadam/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/TriliumNext/Trilium/issues/4274
|
||||
// https://github.com/zadam/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/TriliumNext/Trilium/issues/2401
|
||||
// https://github.com/zadam/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/TriliumNext/Trilium/issues/2995
|
||||
// https://github.com/zadam/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/TriliumNext/Trilium/issues/2794 https://github.com/TriliumNext/Trilium/issues/2988
|
||||
// https://github.com/zadam/trilium/issues/2794 https://github.com/zadam/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/TriliumNext/Trilium/issues/3202 */
|
||||
/* https://github.com/zadam/trilium/issues/3202 */
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,14 @@
|
||||
--ck-mention-list-max-height: 500px;
|
||||
}
|
||||
|
||||
body#trilium-app.motion-disabled *,
|
||||
body#trilium-app.motion-disabled *::before,
|
||||
body#trilium-app.motion-disabled *::after {
|
||||
/* Disable transitions and animations */
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.table {
|
||||
--bs-table-bg: transparent !important;
|
||||
}
|
||||
@@ -40,7 +48,7 @@
|
||||
}
|
||||
|
||||
html {
|
||||
/* this fixes FF filter vs. position fixed bug: https://github.com/TriliumNext/Trilium/issues/233 */
|
||||
/* this fixes FF filter vs. position fixed bug: https://github.com/zadam/trilium/issues/233 */
|
||||
height: 100vh;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
@@ -355,7 +363,7 @@ body.desktop .tabulator-popup-container {
|
||||
|
||||
@supports (animation-fill-mode: forwards) {
|
||||
/* Delay the opening of submenus */
|
||||
body.desktop .dropdown-submenu .dropdown-menu {
|
||||
body.desktop:not(.motion-disabled) .dropdown-submenu .dropdown-menu {
|
||||
opacity: 0;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: var(--submenu-opening-delay);
|
||||
@@ -543,7 +551,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/TriliumNext/Trilium/issues/502 (problem only in smaller font sizes) */
|
||||
/* Making this narrower because https://github.com/zadam/trilium/issues/502 (problem only in smaller font sizes) */
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
z-index: 1000;
|
||||
@@ -1117,7 +1125,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/TriliumNext/Trilium/issues/1590 */
|
||||
display: flex; /* see https://github.com/zadam/trilium/issues/1590 */
|
||||
}
|
||||
|
||||
.include-note.ck-placeholder::before {
|
||||
@@ -1251,7 +1259,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/TriliumNext/Trilium/issues/3136 */
|
||||
/* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -1113,6 +1113,10 @@
|
||||
"layout-vertical-description": "launcher bar is on the left (default)",
|
||||
"layout-horizontal-description": "launcher bar is underneath the tab bar, the tab bar is now full width."
|
||||
},
|
||||
"ui-performance": {
|
||||
"title": "Performance",
|
||||
"enable-motion": "Enable transitions and animations"
|
||||
},
|
||||
"ai_llm": {
|
||||
"not_started": "Not started",
|
||||
"title": "AI Settings",
|
||||
|
||||
1
apps/client/src/translations/ko/translation.json
Normal file
1
apps/client/src/translations/ko/translation.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -409,6 +409,11 @@
|
||||
"template": "Esta nota aparecerá na seleção de modelos disponíveis ao criar uma nova nota",
|
||||
"toc": "<code>#toc</code> ou <code>#toc=show</code> irá forçar a exibição do Sumário, <code>#toc=hide</code> irá forçar que ele fique oculto. Se o rótulo não existir, será considerado o ajuste global",
|
||||
"color": "define a cor da nota na árvore de notas, links etc. Use qualquer valor de cor CSS válido, como 'red' ou #a13d5f",
|
||||
"keyboard_shortcut": "Define um atalho de teclado que irá pular imediatamente para esta nota. Exemplo: 'ctrl+alt+e'. É necessário recarregar o frontend para que a alteração tenha efeito."
|
||||
"keyboard_shortcut": "Define um atalho de teclado que irá pular imediatamente para esta nota. Exemplo: 'ctrl+alt+e'. É necessário recarregar o frontend para que a alteração tenha efeito.",
|
||||
"hide_highlight_widget": "Ocultar o widget da lista de destaques",
|
||||
"keep_current_hoisting": "Abrir este link não alterará o destaque, mesmo que a nota não seja exibível na subárvore destacada atual.",
|
||||
"execute_button": "Titulo do botão que executará a nota de código atual",
|
||||
"exclude_from_note_map": "Notas com este rótulo ficarão ocultas no Mapa de Notas",
|
||||
"new_notes_on_top": "Novas notas serão criadas no topo da nota raiz, não na parte inferior."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"branch_prefix": {
|
||||
"save": "Зберегти",
|
||||
"edit_branch_prefix": "Редагувати префікс гілки",
|
||||
"help_on_tree_prefix": "Довідка щодо префіксів гілок",
|
||||
"help_on_tree_prefix": "Довідка щодо префіксу дерева",
|
||||
"prefix": "Префікс: ",
|
||||
"branch_prefix_saved": "Префікс гілки збережено."
|
||||
},
|
||||
@@ -27,7 +27,32 @@
|
||||
"sync_version": "Версія синхронізації:"
|
||||
},
|
||||
"global_menu": {
|
||||
"about": "Про Trilium Notes"
|
||||
"about": "Про Trilium Notes",
|
||||
"menu": "Меню",
|
||||
"options": "Параметри",
|
||||
"open_new_window": "Відкрити Нове вікно",
|
||||
"switch_to_mobile_version": "Перейти на мобільну версію",
|
||||
"switch_to_desktop_version": "Перейти на версію для ПК",
|
||||
"zoom": "Масштаб",
|
||||
"toggle_fullscreen": "Увімкнути повноекранний режим",
|
||||
"zoom_out": "Зменшити масштаб",
|
||||
"reset_zoom_level": "Скинути масштабування",
|
||||
"zoom_in": "Збільшити масштаб",
|
||||
"configure_launchbar": "Налаштувати панель запуску",
|
||||
"show_shared_notes_subtree": "Показати піддерево спільних нотаток",
|
||||
"advanced": "Розширені",
|
||||
"open_dev_tools": "Відкрити інструменти розробника",
|
||||
"open_sql_console": "Відкрити консоль SQL",
|
||||
"open_sql_console_history": "Відкрити історію консолі SQL",
|
||||
"open_search_history": "Відкрити історію пошуку",
|
||||
"show_backend_log": "Показати Backend Log",
|
||||
"reload_hint": "Перезавантаження може допомогти з деякими візуальними збоями без перезавантаження всієї програми.",
|
||||
"reload_frontend": "Перезавантажити інтерфейс",
|
||||
"show_hidden_subtree": "Показати приховане піддерево",
|
||||
"show_help": "Показати довідку",
|
||||
"logout": "Вийти",
|
||||
"show-cheatsheet": "Показати Шпаргалку",
|
||||
"toggle-zen-mode": "Дзен-режим"
|
||||
},
|
||||
"modal": {
|
||||
"help_title": "Показати більше інформації про це вікно"
|
||||
@@ -38,13 +63,13 @@
|
||||
"message": "Сталася критична помилка, яка перешкоджає запуску клієнтської програми:\n\n{{message}}\n\nНайімовірніше, це спричинено несподіваною помилкою скрипту. Спробуйте запустити програму в безпечному режимі та вирішити проблему."
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "Помилка ініціалізації віджета",
|
||||
"title": "Не вдалася ініціалізація віджета",
|
||||
"message-custom": "Не вдалося ініціалізувати користувацький віджет із нотатки з ID \"{{id}}\" під назвою \"{{title}}\" через:\n\n{{message}}",
|
||||
"message-unknown": "Невідомий віджет не вдалося ініціалізувати через:\n\n{{message}}"
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Не вдалося завантажити власний скрипт",
|
||||
"message": "Скрипт із нотатки з ID \"{{id}}\" під назвою \"{{title}}\" не вдалося виконати через:\n\n{{message}}"
|
||||
"title": "Не вдалося завантажити користувацький скрипт",
|
||||
"message": "Скрипт з нотатки з ID \"{{id}}\" з заголовком \"{{title}}\" не вдалося виконати через:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"bulk_actions": {
|
||||
@@ -57,15 +82,15 @@
|
||||
"none_yet": "Поки що немає... додайте дію, натиснувши одну з доступних вище.",
|
||||
"include_descendants": "Включити нащадків вибраних нотаток",
|
||||
"labels": "Мітки",
|
||||
"relations": "Відносини",
|
||||
"relations": "Зв'язки",
|
||||
"notes": "Нотатки",
|
||||
"other": "Інше"
|
||||
},
|
||||
"clone_to": {
|
||||
"clone_notes_to": "Клонувати нотатки до...",
|
||||
"target_parent_note": "Цільова батьківська нотатка",
|
||||
"search_for_note_by_its_name": "Знайти нотатку за назвою",
|
||||
"help_on_links": "Допомога з посиланнями",
|
||||
"search_for_note_by_its_name": "пошук нотатки за назвою",
|
||||
"help_on_links": "Довідка щодо посилань",
|
||||
"notes_to_clone": "Нотатки для клонування",
|
||||
"cloned_note_prefix_title": "Клонована нотатка буде відображатися в дереві нотаток із заданим префіксом",
|
||||
"prefix_optional": "Префікс (необов'язково)",
|
||||
@@ -123,7 +148,7 @@
|
||||
"delete_notes_preview": "Видалити попередній перегляд нотаток",
|
||||
"close": "Закрити",
|
||||
"delete_all_clones_description": "Видалити також усі клони (можна скасувати в останніх змінах)",
|
||||
"erase_notes_description": "Звичайне (м’яке) видалення лише позначає нотатки як видалені, і їх можна відновити (у діалоговому вікні останніх змін) протягом певного періоду часу. Якщо позначити цю опцію, нотатки будуть видалені негайно, і їх неможливо буде відновити.",
|
||||
"erase_notes_description": "Звичайне (м’яке) видалення лише позначає нотатки як видалені і їх можна відновити (у діалоговому вікні останніх змін) протягом певного періоду часу. Якщо позначити цю опцію, нотатки будуть видалені негайно і їх неможливо буде відновити.",
|
||||
"erase_notes_warning": "Стерти нотатки назавжди (скасувати не можна), включаючи всі клони. Це призведе до перезавантаження програми.",
|
||||
"notes_to_be_deleted": "Наступні нотатки будуть видалені ({{notesCount}})",
|
||||
"no_note_to_delete": "Жодну нотатку не буде видалено (лише клони).",
|
||||
@@ -136,16 +161,16 @@
|
||||
"export_note_title": "Експорт нотатки",
|
||||
"close": "Закрити",
|
||||
"export_type_subtree": "Ця нотатка та всі її нащадки",
|
||||
"format_html": "HTML – рекомендовано, оскільки він зберігає всі формати",
|
||||
"format_html_zip": "HTML у ZIP-архіві – це рекомендовано, оскільки це зберігає все форматування.",
|
||||
"format_markdown": "Markdown – це зберігає більшу частину форматування.",
|
||||
"format_html": "HTML – рекомендовано, оскільки зберігає форматування",
|
||||
"format_html_zip": "HTML у ZIP-архіві – рекомендовано, зберігає форматування.",
|
||||
"format_markdown": "Markdown – зберігає більшу частину форматування.",
|
||||
"format_opml": "OPML – формат обміну структурами лише для тексту. Форматування, зображення та файли не включено.",
|
||||
"opml_version_1": "OPML версії 1.0 – лише звичайний текст",
|
||||
"opml_version_2": "OPML v2.0 - також дозволяє HTML",
|
||||
"export_type_single": "Тільки ця нотатка без її нащадків",
|
||||
"export": "Експорт",
|
||||
"choose_export_type": "Спочатку виберіть тип експорту",
|
||||
"export_status": "Стан експорту",
|
||||
"export_status": "Статус експорту",
|
||||
"export_in_progress": "Триває експорт: {{progressCount}}",
|
||||
"export_finished_successfully": "Експорт успішно завершено.",
|
||||
"format_pdf": "PDF – для друку або спільного використання."
|
||||
@@ -185,7 +210,7 @@
|
||||
"pasteNotes": "вставити нотатку(и) як піднотатку в активну нотатку (яка або переміщується, або клонується залежно від того, чи була вона скопійована, чи вирізана в буфер обміну)",
|
||||
"deleteNotes": "видалити нотатку / піддерево",
|
||||
"editingNotes": "Редагування нотаток",
|
||||
"editNoteTitle": "На панелі дерева перемкнеться з панелі дерева на назву нотатки. Введення з назви нотатки перемкне фокус на текстовий редактор. <kbd>Ctrl+.</kbd> перемкнеться назад з редактора на панель дерева.",
|
||||
"editNoteTitle": "на панелі дерева перемкнеться з панелі дерева на заголовок нотатки. Введення з заголовку нотатки перемкне фокус на текстовий редактор. <kbd>Ctrl+.</kbd> перемкнеться назад з редактора на панель дерева.",
|
||||
"createEditLink": "створити / редагувати зовнішнє посилання",
|
||||
"createInternalLink": "створити внутрішнє посилання",
|
||||
"followLink": "перейти за посиланням під курсором",
|
||||
@@ -201,14 +226,14 @@
|
||||
"showDevTools": "показати інструменти розробника",
|
||||
"showSQLConsole": "показати консоль SQL",
|
||||
"other": "Інше",
|
||||
"quickSearch": "зосередження на швидкому введенні пошукового запиту",
|
||||
"quickSearch": "фокус на швидкому введенні пошуку",
|
||||
"inPageSearch": "пошук на сторінці"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "Імпортувати в нотатку",
|
||||
"chooseImportFile": "Вибрати файл імпорту",
|
||||
"importDescription": "Вміст вибраного(их) файлу(ів) буде імпортовано як дочірню(і) нотатку(и) до",
|
||||
"options": "Опції",
|
||||
"options": "Параметри",
|
||||
"safeImportTooltip": "Експортовані файли Trilium <code>.zip</code> можуть містити виконувані скрипти, які можуть мати шкідливу поведінку. Безпечний імпорт деактивує автоматичне виконання всіх імпортованих скриптів. Зніміть позначку \"Безпечний імпорт\", лише якщо імпортований архів має містити виконувані скрипти, і ви повністю довіряєте вмісту файлу імпорту.",
|
||||
"safeImport": "Безпечний імпорт",
|
||||
"explodeArchivesTooltip": "Якщо цей прапорець позначено, Trilium читатиме файли <code>.zip</code>, <code>.enex</code> та <code>.opml</code> і створюватиме нотатки з файлів усередині цих архівів. Якщо прапорець знято, Trilium додаватиме самі архіви до нотатки.",
|
||||
@@ -216,10 +241,19 @@
|
||||
"shrinkImagesTooltip": "<p>Якщо ви позначите цей параметр, Trilium спробує зменшити імпортовані зображення шляхом масштабування та оптимізації, що може вплинути на сприйняту якість зображення. Якщо не позначити, зображення будуть імпортовані без змін.</p><p>Це не стосується імпорту <code>.zip</code> з метаданими, оскільки передбачається, що ці файли вже оптимізовані.</p>",
|
||||
"shrinkImages": "Зменшити зображення",
|
||||
"textImportedAsText": "Імпортувати HTML, Markdown та TXT як текстові нотатки, якщо це незрозуміло з метаданих",
|
||||
"codeImportedAsCode": "Імпортувати розпізнані файли коду (наприклад, <code>.json</code>) як нотатки до коду, якщо це незрозуміло з метаданих",
|
||||
"codeImportedAsCode": "Імпортувати розпізнані файли коду (наприклад, <code>.json</code>) як нотатки з кодом, якщо це незрозуміло з метаданих",
|
||||
"replaceUnderscoresWithSpaces": "Замінити підкреслення пробілами в назвах імпортованих нотаток",
|
||||
"import": "Імпорт",
|
||||
"failed": "Помилка імпорту: {{message}}."
|
||||
"failed": "Помилка імпорту: {{message}}.",
|
||||
"html_import_tags": {
|
||||
"title": "Теги імпорту HTML",
|
||||
"description": "Налаштуйте, які теги HTML слід зберігати під час імпорту нотаток. Теги, яких немає в цьому списку, будуть видалені під час імпорту. Деякі теги (наприклад, 'script') завжди видаляються з міркувань безпеки.",
|
||||
"placeholder": "Введіть теги HTML, по одному на рядок",
|
||||
"reset_button": "Скинути до Список за замовчуванням"
|
||||
},
|
||||
"import-status": "Статус імпорту",
|
||||
"in-progress": "Триває імпорт: {{progress}}",
|
||||
"successful": "Імпорт успішно завершено."
|
||||
},
|
||||
"prompt": {
|
||||
"title": "Підказка",
|
||||
@@ -242,7 +276,455 @@
|
||||
"confirm_undelete": "Ви хочете відновити цю нотатку та її піднотатки?"
|
||||
},
|
||||
"revisions": {
|
||||
"note_revisions": "Зміни нотаток",
|
||||
"delete_all_revisions": "Видалити всі редакції цієї нотатки"
|
||||
"note_revisions": "Версії нотаток",
|
||||
"delete_all_revisions": "Видалити всі версії цієї нотатки",
|
||||
"delete_all_button": "Видалити всі версії",
|
||||
"help_title": "Довідка щодо версій нотаток",
|
||||
"revision_last_edited": "Цю версію востаннє редагували {{date}}",
|
||||
"confirm_delete_all": "Ви хочете видалити всі версії цієї нотатки?",
|
||||
"no_revisions": "Поки що немає версій цієї нотатки...",
|
||||
"restore_button": "Відновити",
|
||||
"confirm_restore": "Ви хочете відновити цю версію? Це замінить поточний заголовок та вміст нотатки цієї версії.",
|
||||
"delete_button": "Видалити",
|
||||
"confirm_delete": "Ви хочете видалити цю версію?",
|
||||
"revisions_deleted": "Версії нотаток видалено.",
|
||||
"revision_restored": "Версію нотатки відновлено.",
|
||||
"revision_deleted": "Версію нотатки видалено.",
|
||||
"snapshot_interval": "Інтервал знімків версій нотатки: {{seconds}}s.",
|
||||
"maximum_revisions": "Ліміт знімків версій нотатки: {{number}}.",
|
||||
"settings": "Налаштування версій нотатки",
|
||||
"download_button": "Завантажити",
|
||||
"mime": "МІМЕ: ",
|
||||
"file_size": "Розмір файлу:",
|
||||
"preview": "Попередній перегляд:",
|
||||
"preview_not_available": "Попередній перегляд недоступний для цього типу нотатки."
|
||||
},
|
||||
"include_note": {
|
||||
"dialog_title": "Включити нотатку",
|
||||
"label_note": "Нотатка",
|
||||
"placeholder_search": "пошук нотатки за її назвою",
|
||||
"box_size_prompt": "Розмір вмісту з вкладеною нотаткою:",
|
||||
"box_size_small": "маленький (~ 10 рядків)",
|
||||
"box_size_medium": "середній (~ 30 рядків)",
|
||||
"box_size_full": "повний (вміст показує повний текст)",
|
||||
"button_include": "Включити Нотатку"
|
||||
},
|
||||
"info": {
|
||||
"modalTitle": "Інформаційне повідомлення",
|
||||
"closeButton": "Закрити",
|
||||
"okButton": "ОК"
|
||||
},
|
||||
"jump_to_note": {
|
||||
"search_placeholder": "Пошук нотатки за її назвою або типом > для команд...",
|
||||
"search_button": "Повнотекстовий Пошук"
|
||||
},
|
||||
"markdown_import": {
|
||||
"dialog_title": "Імпорт з Markdown",
|
||||
"modal_body_text": "Через \"пісочницю\" браузера неможливо безпосередньо зчитувати буфер обміну з JavaScript. Будь ласка, вставте код Markdown для імпорту в текстове поле нижче та натисніть кнопку \"Імпортувати\"",
|
||||
"import_button": "Імпорт",
|
||||
"import_success": "Вміст Markdown імпортовано в документ."
|
||||
},
|
||||
"move_to": {
|
||||
"dialog_title": "Перемістити нотатки до ...",
|
||||
"notes_to_move": "Нотатки для переміщення",
|
||||
"search_placeholder": "пошук нотатки за її назвою",
|
||||
"move_button": "Перейти до вибраної нотатки",
|
||||
"error_no_path": "Немає шляху для переміщення.",
|
||||
"move_success_message": "Вибрані нотатки переміщено до ",
|
||||
"target_parent_note": "Цільова батьківська нотатка"
|
||||
},
|
||||
"note_type_chooser": {
|
||||
"change_path_prompt": "Змінити місце створення нової нотатки:",
|
||||
"search_placeholder": "пошук шляху за назвою (за замовчуванням, якщо порожня)",
|
||||
"modal_title": "Вибрати тип нотатки",
|
||||
"modal_body": "Вибрати тип/шаблон нової нотатки:",
|
||||
"templates": "Шаблони",
|
||||
"builtin_templates": "Вбудовані Шаблони"
|
||||
},
|
||||
"password_not_set": {
|
||||
"title": "Пароль не встановлено",
|
||||
"body1": "Захищені нотатки шифруються за допомогою пароля користувача, але пароль ще не встановлено.",
|
||||
"body2": "Щоб захистити нотатки, натисніть кнопку нижче, щоб відкрити діалогове вікно Параметри та встановити пароль.",
|
||||
"go_to_password_options": "Перейти до параметрів пароля"
|
||||
},
|
||||
"sort_child_notes": {
|
||||
"sort_children_by": "Сортування дочірніх за...",
|
||||
"sorting_criteria": "Критерії сортування",
|
||||
"title": "заголовок",
|
||||
"date_created": "дата створення",
|
||||
"date_modified": "дата зміни",
|
||||
"sorting_direction": "Напрямок сортування",
|
||||
"ascending": "зростання",
|
||||
"descending": "спадний",
|
||||
"folders": "Папки",
|
||||
"sort_folders_at_top": "сортувати папки зверху",
|
||||
"natural_sort": "Нативне сортування",
|
||||
"sort_with_respect_to_different_character_sorting": "сортувати з урахуванням різних правил сортування та порівняння символів у різних мовах або регіонах.",
|
||||
"natural_sort_language": "Мова нативного сортування",
|
||||
"the_language_code_for_natural_sort": "Код мови для нативного сортування, наприклад, \"zh-CN\" для китайської.",
|
||||
"sort": "Сортування"
|
||||
},
|
||||
"upload_attachments": {
|
||||
"upload_attachments_to_note": "Завантажити вкладення до нотатки",
|
||||
"choose_files": "Вибрати файли",
|
||||
"files_will_be_uploaded": "Файли будуть завантажені як вкладення до {{noteTitle}}",
|
||||
"options": "Параметри",
|
||||
"shrink_images": "Зменшити зображення",
|
||||
"upload": "Скачати",
|
||||
"tooltip": "Якщо ви позначите цей параметр, Trilium спробує зменшити розмір завантажених зображень шляхом масштабування та оптимізації, що може вплинути на сприйняту якість зображення. Якщо вимкнути цей параметр, зображення будуть завантажені без змін."
|
||||
},
|
||||
"attribute_detail": {
|
||||
"attr_detail_title": "Деталі Атрибуту Заголовок",
|
||||
"close_button_title": "Скасувати зміни та закрити",
|
||||
"attr_is_owned_by": "Атрибут належить",
|
||||
"attr_name_title": "Ім'я атрибута може складатися лише з буквено-цифрових символів, двокрапки та символу підкреслення",
|
||||
"name": "Назва",
|
||||
"value": "Значення",
|
||||
"target_note_title": "Зв'язок — це іменований зв'язок між джерелом та цільовою нотаткою.",
|
||||
"target_note": "Цільова нотатка",
|
||||
"promoted_title": "Просунутий атрибут відображається на нотатці у помітному вигляді.",
|
||||
"promoted": "Просунуті",
|
||||
"promoted_alias_title": "Назва, яка відображатиметься в інтерфейсі просунутих атрибутів.",
|
||||
"promoted_alias": "Псевдонім",
|
||||
"multiplicity_title": "Множинність визначає, скільки атрибутів з однаковою назвою можна створити — максимум 1 або більше 1.",
|
||||
"multiplicity": "Множинність",
|
||||
"single_value": "Одне значення",
|
||||
"multi_value": "Декілька значень",
|
||||
"label_type_title": "Тип мітки допоможе Trilium вибрати відповідний інтерфейс для введення значення мітки.",
|
||||
"label_type": "Тип",
|
||||
"text": "Текст",
|
||||
"number": "Номер",
|
||||
"boolean": "Булева",
|
||||
"date": "Дата",
|
||||
"date_time": "Дата & Час",
|
||||
"time": "Час",
|
||||
"url": "URL",
|
||||
"precision_title": "Яка кількість цифр після числа з плаваючою комою має бути доступна в інтерфейсі налаштування значень.",
|
||||
"precision": "Точність",
|
||||
"digits": "цифри",
|
||||
"inverse_relation_title": "Додаткове налаштування для визначення, який зв'язок є зворотнім цьому зв'язку. Приклад: Батьківська - Дочірня інверсним зв'язком одне до одного.",
|
||||
"inverse_relation": "Інверсний зв'язок",
|
||||
"inheritable_title": "Спадковий атрибут буде успадкований усіма нащадками в цьому дереві.",
|
||||
"inheritable": "Спадковий",
|
||||
"save_and_close": "Зберегти & закрити <kbd>Ctrl+Enter</kbd>",
|
||||
"delete": "Видалити",
|
||||
"related_notes_title": "Інші нотатки з цією міткою",
|
||||
"more_notes": "Більше нотаток",
|
||||
"label": "Деталі Мітки",
|
||||
"label_definition": "Деталі визначення мітки",
|
||||
"relation": "Деталі зв'язку",
|
||||
"relation_definition": "Деталі визначення зв'язку",
|
||||
"disable_versioning": "вимикає автоматичне керування версіями. Корисно, наприклад, для великих, але неважливих нотаток, наприклад, великих JS-бібліотек, що використовуються для написання скриптів",
|
||||
"calendar_root": "позначити нотатку, яка буде використовуватись для щоденника за замовчуванням. Тільки одна може бути такою.",
|
||||
"archived": "нотатки з цією міткою не будуть видимими за замовчуванням у результатах пошуку (також у діалогових вікнах Перейти до..., Додати посилання... тощо).",
|
||||
"exclude_from_export": "нотатки (з їхнім піддеревом) не будуть включені до жодного експорту нотаток",
|
||||
"run": "визначає, за яких подій має запускатися скрипт. Можливі значення:\n<ul>\n<li>frontendStartup – коли запускається (або оновлюється) інтерфейс Trilium, але не на мобільному пристрої.</li>\n<li>mobileStartup – коли запускається (або оновлюється) інтерфейс Trilium на мобільному пристрої.</li>\n<li>backendStartup – коли запускається бекенд Trilium</li>\n<li>hourly – запускається раз на годину. Ви можете використовувати додаткову мітку <code>runAtHour</code>, щоб вказати, о котрій годині.</li>\n<li>daily – запускається раз на день</li>\n</ul>",
|
||||
"run_on_instance": "Визначити, на якому екземплярі Trilium це має бути запущено. За замовчуванням використовувати всі екземпляри.",
|
||||
"run_at_hour": "О котрій годині це має запускатися? Слід використовувати разом з <code>#run=hourly</code>. Можна визначити кілька разів для більшої кількості запусків протягом дня.",
|
||||
"disable_inclusion": "скрипти з цією міткою не будуть включені до виконання базового скрипта.",
|
||||
"sorted": "зберігає дочірні нотатки, відсортовані за заголовком в алфавітному порядку",
|
||||
"sort_direction": "ASC (за замовчуванням) або DESC",
|
||||
"sort_folders_first": "Папки (нотатки з дочірніми) слід сортувати зверху",
|
||||
"top": "зберегти задану нотатку зверху в батьківській (застосовується лише до відсортованих батьківських)",
|
||||
"hide_promoted_attributes": "Сховати просунуті атрибути для цієї нотатки",
|
||||
"read_only": "редактор перебуває в режимі лише для читання. Працює лише для тексту та нотаток з кодом.",
|
||||
"auto_read_only_disabled": "текстові/кодові нотатки можна автоматично перевести в режим читання, якщо вони занадто великі. Ви можете вимкнути цю поведінку для кожної окремої нотатки, додавши до неї цю позначку",
|
||||
"app_css": "позначає CSS-нотатки, які завантажуються в програму Trilium і, таким чином, можуть бути використані для зміни зовнішнього вигляду Trilium.",
|
||||
"app_theme": "позначає CSS-нотатки, які є повноцінними темами Trilium і тому доступні в параметрах Trilium.",
|
||||
"app_theme_base": "встановіть значення \"наступна\", \"наступна-світла\" або \"наступна-темна\", щоб використовувати відповідну тему TriliumNext (автоматичну, світлу або темну) як основу для власної теми, замість застарілої.",
|
||||
"css_class": "значення цієї мітки потім додається як CSS-клас до вузла, що представляє задану нотатку в дереві. Це може бути корисним для розширеного налаштування тем. Можна використовувати в шаблонах нотаток.",
|
||||
"icon_class": "значення цієї мітки додається як CSS-клас до значка на дереві, що може допомогти візуально розрізнити нотатки в дереві. Прикладом може бути bx bx-home - значки взяті з boxicons. Можна використовувати в шаблонах нотаток.",
|
||||
"page_size": "кількість елементів на сторінці у списку нотаток",
|
||||
"custom_request_handler": "див. <a href=\"javascript:\" data-help-page=\"custom-request-handler.html\">Спеціальний обробник запитів</a>",
|
||||
"custom_resource_provider": "див. <a href=\"javascript:\" data-help-page=\"custom-request-handler.html\">Спеціальний обробник запитів</a>",
|
||||
"widget": "позначає цю нотатку як користувацький віджет, який буде додано до дерева компонентів Trilium",
|
||||
"workspace": "позначає цю нотатку як робочу область, що дозволяє легко хостити",
|
||||
"workspace_icon_class": "визначає значка CSS-класу, який буде використовуватися у вкладці при хостингу цієї нотатки",
|
||||
"workspace_tab_background_color": "Колір CSS, що використовується у вкладці нотатки під час перенесення до цієї нотатки",
|
||||
"workspace_calendar_root": "Визначає календар для кожного робочого простору",
|
||||
"workspace_template": "Ця нотатка з'явиться у списку доступних шаблонів під час створення нової нотатки, але лише після перенесення її в робочу область, що містить цей шаблон",
|
||||
"search_home": "нові пошукові нотатки будуть створені як дочірні елементи цієї нотатки",
|
||||
"workspace_search_home": "нові пошукові нотатки будуть створені як дочірні елементи цієї нотатки після хостингу до якогось предка цієї нотатки робочої області",
|
||||
"inbox": "розташування Вхідних за замовчуванням для нових нотаток – під час створення нотатки за допомогою кнопки Нова нотатка на бічній панелі, нотатки будуть створені як дочірні нотатки в нотатці з міткою <code>#inbox</code>.",
|
||||
"workspace_inbox": "розташування Вхідні за замовчуванням для нових нотаток, коли вони переносяться до якоїсь батьківської нотатки в робочій області",
|
||||
"sql_console_home": "розташування нотаток консолі SQL за замовчуванням",
|
||||
"bookmark_folder": "нотатка з цією міткою відображатиметься в закладках як папка (що дозволить доступ до її дочірніх елементів)",
|
||||
"share_hidden_from_tree": "ця нотатка прихована в лівому дереві навігації, але все ще доступна за її URL-адресою",
|
||||
"share_external_link": "нотатка діятиме як посилання на зовнішній вебсайт у дереві спільного доступу",
|
||||
"share_alias": "визначає псевдонім, за допомогою якого нотатка буде доступна за адресою https://your_trilium_host/share/[your_alias]",
|
||||
"share_omit_default_css": "CSS сторінки спільного доступу за замовчуванням буде пропущено. Використовуйте, коли ви вносите значні зміни стилю.",
|
||||
"share_description": "визначити текст, який буде додано до метатегу HTML для опису",
|
||||
"share_raw": "нотатка буде надаватися у необробленому форматі, без HTML-оболонки",
|
||||
"share_disallow_robot_indexing": "заборонити індексацію цієї нотатки роботами через заголовок <code>X-Robots-Tag: noindex</code>",
|
||||
"share_credentials": "потрібні облікові дані для доступу до цієї спільної нотатки. Значення повинно бути у форматі «ім'я користувача:пароль». Не забудьте зробити це спадковим, щоб застосувати до дочірніх нотаток/зображень.",
|
||||
"share_index": "нотатка з цією міткою відобразить список усіх коренів спільних нотаток",
|
||||
"display_relations": "назви зв'язків, розділені комами, які слід відображати. Усі інші будуть приховані.",
|
||||
"hide_relations": "назви зв'язків, розділені комами, які слід приховати. Усі інші будуть відображені.",
|
||||
"title_template": "заголовок нотаток за замовчуванням, створених як дочірні елементи цієї нотатки. Значення оцінюється як рядок JavaScript\nі таким чином може бути збагачене динамічним контентом за допомогою вставлених змінних <code>now</code> та <code>parentNote</code>. Приклади:\n\n<ul>\nЛітературні твори <li><code>${parentNote.getLabelValue('authorName')}</code></li>\n<li><code>Журнал для ${now.format('РРРР-ММ-ДД ГГ:мм:сс')</code></li>\n</ul>\n\nДив. <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">вікі з деталями</a>, документацію API для <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> та <a href=\"https://day.js.org/docs/en/display/format\">now</a> для отримання детальної інформації.",
|
||||
"template": "Ця нотатка з'явиться у списку доступних шаблонів під час створення нової нотатки",
|
||||
"toc": "<code>#toc</code> або <code>#toc=show</code> примусово покаже Зміст, <code>#toc=hide</code> примусово приховає його. Якщо мітка не існує, дотримується глобального налаштування",
|
||||
"color": "визначає колір нотатки в дереві нотаток, посиланнях тощо. Використовуйте будь-яке дійсне значення кольору CSS, наприклад, 'red' або #a13d5f",
|
||||
"keyboard_shortcut": "Визначає комбінацію клавіш, щоб негайно перейти до цієї нотатки. Приклад: «ctrl+alt+e». Щоб зміни набули чинності, потрібне перезавантаження інтерфейсу.",
|
||||
"keep_current_hoisting": "Відкриття цього посилання не змінить хостинг, навіть якщо нотатка не відображається в поточному піддереві хостінгу.",
|
||||
"execute_button": "Назва кнопки, яка виконає поточну нотатку з кодом",
|
||||
"execute_description": "Більш детальний опис поточного коду, що відображається разом із кнопкою виконання",
|
||||
"exclude_from_note_map": "Нотатки з цією міткою будуть приховані на Карті Нотатки",
|
||||
"new_notes_on_top": "Нові нотатки будуть створені зверху батьківської нотатки, а не знизу.",
|
||||
"hide_highlight_widget": "Приховати віджет Списку виділення",
|
||||
"run_on_note_creation": "виконується, коли нотатка створюється на серверній частині. Використовуйте цей зв'язок, якщо потрібно запустити скрипт для всіх нотаток, створених у певному піддереві. У такому випадку створіть його на батьківській нотатці піддерева та зробіть його успадковуваним. Нова нотатка, створена в піддереві (будь-якої глибини), запустить скрипт.",
|
||||
"run_on_child_note_creation": "виконується, коли створюється нова нотатка під нотаткою, де визначено цей зв'язок",
|
||||
"run_on_note_title_change": "виконується, коли змінюється заголовок нотатки (включає також створення нотатки)",
|
||||
"run_on_note_content_change": "виконується, коли змінюється вміст нотатки (включаючи також створення нотатки).",
|
||||
"run_on_note_change": "виконується, коли нотатку змінено (включає також створення нотатки). Не включає зміни вмісту",
|
||||
"run_on_note_deletion": "виконується під час видалення нотатки",
|
||||
"run_on_branch_creation": "виконується під час створення гілки. Гілка — це зв'язок між батьківською та дочірньою нотаткою та створюється, наприклад, під час клонування або переміщення нотатки.",
|
||||
"run_on_branch_change": "виконується, коли гілка оновлюється.",
|
||||
"run_on_branch_deletion": "виконується, коли гілку видаляють. Гілка — це зв'язок між батьківською та дочірньою нотатками та видаляється, наприклад, під час переміщення нотатки (стара гілка/посилання видаляється).",
|
||||
"share_root": "позначає нотатку, яка подається на кореневому каталозі /share.",
|
||||
"run_on_attribute_creation": "виконується, коли для нотатки створюється новий атрибут, який визначає цей зв'язок",
|
||||
"run_on_attribute_change": " виконується, коли змінюється атрибут нотатки, яка визначає цей зв'язок. Це також спрацьовує, коли атрибут видаляється",
|
||||
"relation_template": "атрибути нотатки будуть успадковані навіть без зв'язку \"батьківський-дочірній\", вміст нотатки та піддерево будуть додані до екземпляра нотатки, якщо він порожній. Див. документацію для отримання детальної інформації.",
|
||||
"inherit": "атрибути нотатки будуть успадковані навіть без зв'язку «батьківський-дочірній». Див. шаблон зв'язку для подібної концепції. Див. успадкування атрибутів у документації.",
|
||||
"render_note": "нотатки типу \"render HTML note\" будуть відображатися за допомогою нотатки з кодом (HTML або скрипта), і необхідно вказати за допомогою цього зв'язку, яку нотатку слід відображати",
|
||||
"widget_relation": "ціль цього зв'язку буде виконано та відображено як віджет на бічній панелі",
|
||||
"share_css": "CSS-нотатка, яка буде вставлена на сторінку спільного доступу. CSS-нотатка також має бути в спільному піддереві. Також розгляньте можливість використання 'share_hidden_from_tree' та 'share_omit_default_css'.",
|
||||
"share_js": "JavaScript-нотатка, яка буде вставлена на сторінку спільного доступу. JS-нотатка також має бути в спільному піддереві. Розгляньте можливість використання 'share_hidden_from_tree'.",
|
||||
"share_template": "Вбудована нотатка JavaScript, яка використовуватиметься як шаблон для відображення спільної нотатки. Повертатиметься до шаблону за замовчуванням. Розгляньте можливість використання 'share_hidden_from_tree'.",
|
||||
"share_favicon": "Нотатку до значка веб-сторінки, яку потрібно встановити на спільній сторінці. Зазвичай потрібно встановити її як спільний кореневий каталог і зробити успадковуваною. Нотатку до значка веб-сторінки також потрібно розмістити у спільному піддереві. Розгляньте можливість використання 'share_hidden_from_tree'.",
|
||||
"is_owned_by_note": "належить до нотатки",
|
||||
"other_notes_with_name": "Інші нотатки з назвою {{attributeType}} \"{{attributeName}}\"",
|
||||
"and_more": "... та ще {{count}}.",
|
||||
"print_landscape": "Під час експорту в PDF змінює орієнтацію сторінки на альбомну замість портретної.",
|
||||
"print_page_size": "Під час експорту в PDF змінює розмір сторінки. Підтримувані значення: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
|
||||
"color_type": "Колір"
|
||||
},
|
||||
"multi_factor_authentication": {
|
||||
"mfa_method": "Метод МФА"
|
||||
},
|
||||
"attribute_editor": {
|
||||
"help_text_body1": "Щоб додати мітку, просто введіть, наприклад, <code>#rock</code> або, якщо ви хочете додати також значення, то, наприклад, <code>#year = 2020</code>",
|
||||
"help_text_body2": "Для зв'язку введіть <code>~author = @</code>, що має викликати автозаповнення, де ви зможете знайти потрібну нотатку.",
|
||||
"help_text_body3": "Або ж ви можете додати мітку та зв'язок за допомогою кнопки <code>+</code> праворуч.",
|
||||
"save_attributes": "Зберегти атрибути <enter>",
|
||||
"add_a_new_attribute": "Додати новий атрибут",
|
||||
"add_new_label": "Додати нову мітку <kbd data-command=\"addNewLabel\"></kbd>",
|
||||
"add_new_relation": "Додати новий зв'язок <kbd data-command=\"addNewRelation\"></kbd>",
|
||||
"add_new_label_definition": "Додати нове визначення мітки",
|
||||
"add_new_relation_definition": "Додати нове визначення зв'язку",
|
||||
"placeholder": "Введіть мітки та зв'язки тут"
|
||||
},
|
||||
"abstract_bulk_action": {
|
||||
"remove_this_search_action": "Видалити цю дію пошуку"
|
||||
},
|
||||
"execute_script": {
|
||||
"execute_script": "Виконати скрипт",
|
||||
"help_text": "Ви можете виконувати прості скрипти у нотатках, що збігаються.",
|
||||
"example_1": "Наприклад, щоб додати рядок до заголовка нотатки, використовуйте цей невеликий скрипт:",
|
||||
"example_2": "Більш складним прикладом може бути видалення всіх атрибутів нотаток, що збігаються:"
|
||||
},
|
||||
"add_label": {
|
||||
"add_label": "Додати мітку",
|
||||
"label_name_placeholder": "назва мітки",
|
||||
"label_name_title": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку.",
|
||||
"to_value": "до значення",
|
||||
"new_value_placeholder": "нове значення",
|
||||
"help_text": "У всіх нотатках, що збігаються:",
|
||||
"help_text_item1": "створити вказану мітку, якщо нотатка ще її не має",
|
||||
"help_text_item2": "або змінити значення існуючої мітки",
|
||||
"help_text_note": "Ви також можете викликати цей метод без значення, у такому випадку мітку буде присвоєно нотатці без значення."
|
||||
},
|
||||
"delete_label": {
|
||||
"delete_label": "Видалити мітку",
|
||||
"label_name_placeholder": "назва мітки",
|
||||
"label_name_title": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку."
|
||||
},
|
||||
"rename_label": {
|
||||
"rename_label": "Перейменувати мітку",
|
||||
"rename_label_from": "Перейменувати мітку з",
|
||||
"old_name_placeholder": "стара назва",
|
||||
"to": "До",
|
||||
"new_name_placeholder": "нова назва",
|
||||
"name_title": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку."
|
||||
},
|
||||
"update_label_value": {
|
||||
"update_label_value": "Оновити значення мітки",
|
||||
"label_name_placeholder": "назва мітки",
|
||||
"label_name_title": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку.",
|
||||
"to_value": "до значення",
|
||||
"new_value_placeholder": "нове значення",
|
||||
"help_text": "Для всіх нотаток, що збігаються, змініть значення існуючої мітки.",
|
||||
"help_text_note": "Ви також можете викликати цей метод без значення, у такому випадку мітку буде присвоєно нотатці без значення."
|
||||
},
|
||||
"delete_note": {
|
||||
"delete_note": "Видалити нотатку",
|
||||
"delete_matched_notes": "Видалити нотатки, що збігаються",
|
||||
"delete_matched_notes_description": "Це видалить нотатки, що збігаються.",
|
||||
"undelete_notes_instruction": "Після видалення їх можна відновити в діалоговому вікні «Останні зміни».",
|
||||
"erase_notes_instruction": "Щоб остаточно видалити нотатки, після видалення перейдіть до меню Опції -> Інше та натисніть кнопку «Стерти видалені нотатки зараз»."
|
||||
},
|
||||
"delete_revisions": {
|
||||
"all_past_note_revisions": "Усі попередні версії нотаток, що збігаються, будуть видалені. Сама нотатка буде повністю збережена. Іншими словами, історія нотатки буде видалена.",
|
||||
"delete_note_revisions": "Видалити версії нотаток"
|
||||
},
|
||||
"move_note": {
|
||||
"on_all_matched_notes": "У всіх нотатках, що збігаються",
|
||||
"move_note": "Перемістити нотатку",
|
||||
"to": "до",
|
||||
"target_parent_note": "цільова батьківська нотатка",
|
||||
"move_note_new_parent": "перемістити нотатку до нового батьківського елемента, якщо нотатка має лише один батьківський елемент (тобто стара гілка видаляється, а нова гілка в новому батьківському елементі створюється)",
|
||||
"clone_note_new_parent": "клонувати нотатку до нового батьківського елемента, якщо нотатка має кілька клонів/гілок (незрозуміло, яку гілку слід видалити)",
|
||||
"nothing_will_happen": "нічого не станеться, якщо нотатку не можна перемістити до цільової нотатки (тобто це створить деревоподібний цикл)"
|
||||
},
|
||||
"rename_note": {
|
||||
"example_note": "<code>Нотатка</code> – усі нотатки, що збігаються будуть перейменовані на \"Нотатка\"",
|
||||
"example_new_title": "<code>NEW: ${note.title}</code> – назви нотаток, що збігаються, мають префікс 'НОВА:'",
|
||||
"example_date_prefix": "<code>${note.dateCreatedObj.format('MM-DD:')}: ${note. Title}</code> - нотатки, що збігаються мають префікс у вигляді місяця та дати створення нотатки",
|
||||
"rename_note": "Перейменувати нотатку",
|
||||
"rename_note_title_to": "Перейменувати заголовок нотатки на",
|
||||
"new_note_title": "новий заголовок нотатки",
|
||||
"click_help_icon": "Натисніть значок довідки праворуч, щоб переглянути всі опції",
|
||||
"evaluated_as_js_string": "Надане значення обчислюється як рядок JavaScript і тому може бути доповнено динамічним контентом за допомогою вставленої змінної <code>note</code> (нотатка перейменовується). Приклади:",
|
||||
"api_docs": "Див. документацію API для <a href='https://zadam.github.io/trilium/backend_api/Note.html'>note</a> та її <a href='https://day.js.org/docs/en/display/format'>властивостей dateCreatedObj / utcDateCreatedObj</a> для отримання детальної інформації."
|
||||
},
|
||||
"add_relation": {
|
||||
"create_relation_on_all_matched_notes": "Для всіх нотаток, що збігаються, створити задані зв'язки.",
|
||||
"add_relation": "Додати зв'язок",
|
||||
"relation_name": "назва зв'язку",
|
||||
"allowed_characters": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку.",
|
||||
"to": "до",
|
||||
"target_note": "цільова нотатка"
|
||||
},
|
||||
"update_relation_target": {
|
||||
"on_all_matched_notes": "У всіх нотатках, що збігаються",
|
||||
"update_relation": "Оновити зв'язок",
|
||||
"relation_name": "назва зв'язку",
|
||||
"allowed_characters": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку.",
|
||||
"to": "до",
|
||||
"target_note": "цільова нотатка",
|
||||
"change_target_note": "змінити цільову нотатку існуючого зв'язку",
|
||||
"update_relation_target": "Оновити ціль зв'язку"
|
||||
},
|
||||
"search_script": {
|
||||
"example_code": "// 1. попередня фільтрація за допомогою стандартного пошуку\nconst candidateNotes = api.searchForNotes(\"#journal\");\n\n// 2. застосування користувацьких критеріїв пошуку\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;"
|
||||
},
|
||||
"delete_relation": {
|
||||
"delete_relation": "Видалити зв'язок",
|
||||
"relation_name": "назва зв'язку",
|
||||
"allowed_characters": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку."
|
||||
},
|
||||
"rename_relation": {
|
||||
"rename_relation": "Перейменувати зв'язок",
|
||||
"rename_relation_from": "Перейменувати зв'язок з",
|
||||
"old_name": "стара назва",
|
||||
"to": "До",
|
||||
"new_name": "нова назва",
|
||||
"allowed_characters": "Дозволено використовувати буквено-цифрові символи, підкреслення та двокрапку."
|
||||
},
|
||||
"attachments_actions": {
|
||||
"open_externally": "Відкрити у зовнішній програмі",
|
||||
"open_externally_title": "Файл буде відкрито в зовнішній програмі та відстежуватися на наявність змін. Після цього ви зможете завантажити змінену версію назад до Trilium.",
|
||||
"open_custom": "Відкрити користувацький",
|
||||
"open_custom_title": "Файл буде відкрито в зовнішній програмі та відстежуватися на наявність змін. Після цього ви зможете завантажити змінену версію назад до Trilium.",
|
||||
"download": "Завантажити",
|
||||
"rename_attachment": "Перейменувати вкладення",
|
||||
"upload_new_revision": "Завантажити нову версію",
|
||||
"copy_link_to_clipboard": "Копіювати посилання в буфер обміну",
|
||||
"convert_attachment_into_note": "Перетворити вкладення на нотатку",
|
||||
"delete_attachment": "Видалити вкладення",
|
||||
"upload_success": "Нову версію вкладеного файлу завантажено.",
|
||||
"upload_failed": "Не вдалося завантажити нову версію вкладеного файлу.",
|
||||
"open_externally_detail_page": "Відкриття вкладення ззовні доступне лише зі сторінки з деталями, спочатку натисніть на деталі вкладення та повторіть дію.",
|
||||
"open_custom_client_only": "Налаштування відкриття вкладень можна виконати лише з клієнтської версії для ПК.",
|
||||
"delete_confirm": "Ви впевнені, що хочете видалити вкладення '{{title}}'?",
|
||||
"delete_success": "Вкладення '{{title}}' видалено.",
|
||||
"convert_confirm": "Ви впевнені, що хочете перетворити вкладення '{{title}}' на окрему нотатку?",
|
||||
"convert_success": "Вкладення '{{title}}' перетворено на нотатку.",
|
||||
"enter_new_name": "Будь ласка, введіть назву нового вкладення"
|
||||
},
|
||||
"calendar": {
|
||||
"mon": "Пн",
|
||||
"tue": "Вт",
|
||||
"wed": "Ср",
|
||||
"thu": "Чт",
|
||||
"fri": "Пт",
|
||||
"sat": "Сб",
|
||||
"sun": "Нд",
|
||||
"cannot_find_day_note": "Не вдається знайти денну нотатку",
|
||||
"cannot_find_week_note": "Не вдається знайти нотатку тижня",
|
||||
"january": "Січень",
|
||||
"febuary": "Лютий",
|
||||
"march": "Березень",
|
||||
"april": "Квітень",
|
||||
"may": "Травень",
|
||||
"june": "Червень",
|
||||
"july": "Липень",
|
||||
"august": "Серпень",
|
||||
"september": "Вересень",
|
||||
"october": "Жовтень",
|
||||
"november": "Листопад",
|
||||
"december": "Грудень"
|
||||
},
|
||||
"close_pane_button": {
|
||||
"close_this_pane": "Закрити цю панель"
|
||||
},
|
||||
"create_pane_button": {
|
||||
"create_new_split": "Створити новий поділ"
|
||||
},
|
||||
"edit_button": {
|
||||
"edit_this_note": "Редагувати цю нотатку"
|
||||
},
|
||||
"show_toc_widget_button": {
|
||||
"show_toc": "Показати зміст"
|
||||
},
|
||||
"show_highlights_list_widget_button": {
|
||||
"show_highlights_list": "Показати Список основних моментів"
|
||||
},
|
||||
"zen_mode": {
|
||||
"button_exit": "Вихід з Дзен-режиму"
|
||||
},
|
||||
"sync_status": {
|
||||
"unknown": "<p>Стан синхронізації буде відомий після початку наступної спроби синхронізації.</p><p>Натисніть, щоб запустити синхронізацію зараз.</p>",
|
||||
"connected_with_changes": "<p>Підключено до сервера синхронізації. <br>Є деякі невиконані зміни, які ще потрібно синхронізувати.</p><p>Натисніть, щоб розпочати синхронізацію.</p>",
|
||||
"connected_no_changes": "<p>Підключено до сервера синхронізації.<br>Усі зміни вже синхронізовано.</p><p>Натисніть, щоб розпочати синхронізацію.</p>",
|
||||
"disconnected_with_changes": "<p>Встановлення з’єднання із сервером синхронізації не вдалося.<br>Є деякі невиконані зміни, які ще потрібно синхронізувати.</p><p>Натисніть, щоб розпочати синхронізацію.</p>",
|
||||
"disconnected_no_changes": "<p>Встановлення з’єднання із сервером синхронізації не вдалося.<br>Усі відомі зміни синхронізовано.</p><p>Натисніть, щоб розпочати синхронізацію.</p>",
|
||||
"in_progress": "Триває синхронізація із сервером."
|
||||
},
|
||||
"left_pane_toggle": {
|
||||
"show_panel": "Показати панель",
|
||||
"hide_panel": "Приховати панель"
|
||||
},
|
||||
"move_pane_button": {
|
||||
"move_left": "Переміститися вліво",
|
||||
"move_right": "Переміститися вправо"
|
||||
},
|
||||
"note_actions": {
|
||||
"convert_into_attachment": "Перетворити на вкладення",
|
||||
"re_render_note": "Повторно відобразити нотатку",
|
||||
"search_in_note": "Пошук у нотатці",
|
||||
"note_source": "Джерело нотатки",
|
||||
"note_attachments": "Вкладення нотатки",
|
||||
"open_note_externally": "Відкрити нотатку у зовнішній програмі",
|
||||
"open_note_externally_title": "Файл буде відкрито в зовнішній програмі та відстежуватися на наявність змін. Після цього ви зможете завантажити змінену версію назад до Trilium.",
|
||||
"open_note_custom": "Відкрити нотатку користувача",
|
||||
"import_files": "Імпорт файлів",
|
||||
"export_note": "Експорт нотатки",
|
||||
"delete_note": "Видалити нотатку",
|
||||
"print_note": "Друк нотатки",
|
||||
"save_revision": "Зберегти версію",
|
||||
"convert_into_attachment_failed": "Не вдалося конвертувати нотатку '{{title}}'.",
|
||||
"convert_into_attachment_successful": "Нотатку '{{title}}' перетворено на вкладення.",
|
||||
"convert_into_attachment_prompt": "Ви впевнені, що хочете перетворити нотатку '{{title}}' на вкладення батьківської нотатки?",
|
||||
"print_pdf": "Експортувати як PDF..."
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Віджет кнопки '{{componentId}}' не має визначеного обробника кліків"
|
||||
},
|
||||
"protected_session_status": {
|
||||
"active": "Захищений сеанс активний. Натисніть, щоб вийти з захищеного сеансу.",
|
||||
"inactive": "Натисніть, щоб увійти до захищеного сеансу"
|
||||
},
|
||||
"revisions_button": {
|
||||
"note_revisions": "Версії нотатки"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/TriliumNext/Trilium/pull/3812
|
||||
// https://github.com/zadam/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/TriliumNext/Trilium/pull/3812
|
||||
// https://github.com/zadam/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/TriliumNext/Trilium/pull/3812
|
||||
// https://github.com/zadam/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/TriliumNext/Trilium/issues/4160
|
||||
this.$editor.on("blur", () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/zadam/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/TriliumNext/Trilium/issues/3090
|
||||
// https://github.com/zadam/trilium/issues/3090
|
||||
console.warn("Ignoring blur event because a different note is loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import utils from "../../services/utils.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import { EventData } from "../../components/app_context.js";
|
||||
import FlexContainer from "./flex_container.js";
|
||||
import options from "../../services/options.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
/**
|
||||
* The root container is the top-most widget/container, from which the entire layout derives.
|
||||
@@ -20,6 +22,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
this.id("root-widget");
|
||||
this.css("height", "100dvh");
|
||||
this.originalViewportHeight = getViewportHeight();
|
||||
|
||||
}
|
||||
|
||||
render(): JQuery<HTMLElement> {
|
||||
@@ -27,15 +30,27 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
||||
window.visualViewport?.addEventListener("resize", () => this.#onMobileResize());
|
||||
}
|
||||
|
||||
this.#setMotion(options.is("motionEnabled"));
|
||||
|
||||
return super.render();
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.isOptionReloaded("motionEnabled")) {
|
||||
this.#setMotion(options.is("motionEnabled"));
|
||||
}
|
||||
}
|
||||
|
||||
#onMobileResize() {
|
||||
const currentViewportHeight = getViewportHeight();
|
||||
const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight);
|
||||
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
|
||||
}
|
||||
|
||||
#setMotion(enabled: boolean) {
|
||||
document.body.classList.toggle("motion-disabled", !enabled);
|
||||
jQuery.fx.off = !enabled;
|
||||
}
|
||||
}
|
||||
|
||||
function getViewportHeight() {
|
||||
|
||||
@@ -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/TriliumNext/Trilium/issues/2522
|
||||
// https://github.com/zadam/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/TriliumNext/Trilium/issues/4165
|
||||
// (intersection is false). https://github.com/zadam/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/TriliumNext/Trilium/pull/1120 for discussion
|
||||
// see https://github.com/zadam/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/TriliumNext/Trilium/issues/3664
|
||||
// don't activate the active note, see discussion in https://github.com/zadam/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/TriliumNext/Trilium/issues/1192
|
||||
* Some context: https://github.com/zadam/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/TriliumNext/Trilium/issues/1865
|
||||
// needed by Firefox: https://github.com/zadam/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/TriliumNext/Trilium/issues/3761
|
||||
// inherited attributes should stay grouped: https://github.com/zadam/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/TriliumNext/Trilium/issues/4468 */
|
||||
/* https://github.com/zadam/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/TriliumNext/Trilium/issues/3782
|
||||
// https://github.com/zadam/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/TriliumNext/Trilium/issues/4363
|
||||
* Reduce indent if a larger headings are not being used: https://github.com/zadam/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/TriliumNext/Trilium/issues/2828
|
||||
// See https://github.com/zadam/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/TriliumNext/Trilium/issues/3780 */
|
||||
/* https://github.com/zadam/trilium/issues/3780 */
|
||||
/* https://github.com/excalidraw/excalidraw/issues/6567 */
|
||||
.excalidraw .dropdown-menu {
|
||||
display: block;
|
||||
|
||||
@@ -88,6 +88,7 @@ export default function AppearanceSettings() {
|
||||
<ApplicationTheme />
|
||||
{overrideThemeFonts === "true" && <Fonts />}
|
||||
{isElectron() && <ElectronIntegration /> }
|
||||
<Performance />
|
||||
<MaxContentWidth />
|
||||
<RelatedSettings items={[
|
||||
{
|
||||
@@ -245,6 +246,20 @@ function ElectronIntegration() {
|
||||
)
|
||||
}
|
||||
|
||||
function Performance() {
|
||||
const [ motionEnabled, setMotionEnabled ] = useTriliumOptionBool("motionEnabled");
|
||||
|
||||
return <OptionsSection title={t("ui-performance.title")}>
|
||||
<FormGroup name="motion-enabled">
|
||||
<FormCheckbox
|
||||
label={t("ui-performance.enable-motion")}
|
||||
currentValue={motionEnabled} onChange={setMotionEnabled}
|
||||
/>
|
||||
</FormGroup>
|
||||
</OptionsSection>
|
||||
}
|
||||
|
||||
|
||||
function MaxContentWidth() {
|
||||
const [ maxContentWidth, setMaxContentWidth ] = useTriliumOption("maxContentWidth");
|
||||
|
||||
|
||||
@@ -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/TriliumNext/Trilium/issues/1590 for example of such conflict)
|
||||
// (see https://github.com/zadam/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/TriliumNext/Trilium/issues/4271
|
||||
// needed for excalidraw export https://github.com/zadam/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/TriliumNext/Trilium/issues/3017
|
||||
// old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/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/TriliumNext/Trilium/issues/510
|
||||
// recovery from https://github.com/zadam/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/TriliumNext/Trilium/wiki/Day-notes">https://github.com/TriliumNext/Trilium/wiki/Day-notes</a>
|
||||
<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>
|
||||
</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/TriliumNext/Trilium/wiki/Custom-request-handler for details.
|
||||
* See https://github.com/zadam/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/TriliumNext/Trilium/wiki/Task-manager">https://github.com/TriliumNext/Trilium/wiki/Task-manager</a>
|
||||
here: <a href="https://github.com/zadam/trilium/wiki/Task-manager">https://github.com/zadam/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/TriliumNext/Trilium/wiki/Weight-tracker" target="_blank">here</a></strong>.
|
||||
<strong>See explanation <a href="https://github.com/zadam/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/TriliumNext/Trilium/wiki/Themes
|
||||
* You can read some details on theming here: http://github.com/zadam/trilium/wiki/Themes
|
||||
*/
|
||||
|
||||
@font-face { /* This will be used as main UI font (see below) */
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"i18next": "25.3.6",
|
||||
"i18next": "25.4.0",
|
||||
"i18next-fs-backend": "2.6.0",
|
||||
"image-type": "6.0.0",
|
||||
"ini": "5.0.0",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,720 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,810 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,937 +0,0 @@
|
||||
<!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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,122 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,367 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,268 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,325 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,266 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,286 +0,0 @@
|
||||
<!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>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,828 +0,0 @@
|
||||
<!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>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,119 +0,0 @@
|
||||
<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>
|
||||
@@ -1,255 +0,0 @@
|
||||
<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
274
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI/Features and Usage.html
generated
vendored
@@ -1,274 +0,0 @@
|
||||
<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,27 +3,12 @@
|
||||
height="1364">
|
||||
<figcaption>An example chat with an LLM</figcaption>
|
||||
</figure>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<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
328
apps/server/src/assets/doc_notes/en/User Guide/User Guide/AI/Security and Privacy.html
generated
vendored
@@ -1,328 +0,0 @@
|
||||
<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>
|
||||
@@ -1,264 +0,0 @@
|
||||
<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,335 +1,27 @@
|
||||
<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>
|
||||
<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>
|
||||
<ol>
|
||||
<li><strong>Environment variables</strong> (checked first)</li>
|
||||
<li><strong>config.ini file values</strong></li>
|
||||
<li><strong>Default values</strong></li>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</ol>
|
||||
378
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Note Revisions.html
generated
vendored
378
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Note Revisions.html
generated
vendored
@@ -1,378 +0,0 @@
|
||||
<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>
|
||||
@@ -1,232 +0,0 @@
|
||||
<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>
|
||||
@@ -1,360 +0,0 @@
|
||||
<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>
|
||||
@@ -1,160 +0,0 @@
|
||||
<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>
|
||||
@@ -1,245 +0,0 @@
|
||||
<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>
|
||||
@@ -1,181 +0,0 @@
|
||||
<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>
|
||||
@@ -1,499 +0,0 @@
|
||||
<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>
|
||||
@@ -1,600 +0,0 @@
|
||||
<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>
|
||||
@@ -1,412 +0,0 @@
|
||||
<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>
|
||||
@@ -1,476 +0,0 @@
|
||||
<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>
|
||||
@@ -1,326 +0,0 @@
|
||||
<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>
|
||||
@@ -1,457 +0,0 @@
|
||||
<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,7 +134,6 @@ 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,12 +49,7 @@ 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:
|
||||
<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>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><code>oauthBaseUrl</code> should be the link of your Trilium instance server,
|
||||
for example, <code>https://<your-trilium-domain></code>.</li>
|
||||
</ol>
|
||||
@@ -69,12 +64,8 @@ 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:
|
||||
<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
|
||||
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
|
||||
required for displaying correct issuer information at the Login page.</p>
|
||||
</aside>
|
||||
<h4>Authentik</h4>
|
||||
|
||||
@@ -26,10 +26,7 @@ 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. 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>
|
||||
to provide all <code>config.ini</code> values as environment variables instead.</p>
|
||||
<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/TriliumNext/Trilium
|
||||
url: https://github.com/zadam/trilium
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
@@ -1,305 +1,338 @@
|
||||
{
|
||||
"keyboard_actions": {
|
||||
"open-jump-to-note-dialog": "Öffne das Dialogfeld \"Zu Notiz springen\"",
|
||||
"search-in-subtree": "Nach Notizen im Unterbaum der aktuellen Notiz suchen",
|
||||
"expand-subtree": "Unterbaum der aktuellen Notiz ausklappen",
|
||||
"collapse-tree": "Gesamten Notizbaum einklappen",
|
||||
"collapse-subtree": "Unterbaum der aktuellen Notiz einklappen",
|
||||
"sort-child-notes": "Untergeordnete Notizen sortieren",
|
||||
"creating-and-moving-notes": "Notizen erstellen und verschieben",
|
||||
"create-note-into-inbox": "Erstelle eine Notiz im Posteingang (falls definiert) oder in der Tagesnotiz",
|
||||
"delete-note": "Notiz löschen",
|
||||
"move-note-up": "Notiz nach oben verschieben",
|
||||
"move-note-down": "Notiz nach unten verschieben",
|
||||
"move-note-up-in-hierarchy": "Notiz in der Hierarchie nach oben verschieben",
|
||||
"move-note-down-in-hierarchy": "Notiz in der Hierarchie nach unten verschieben",
|
||||
"edit-note-title": "Vom Notiz-Baum zur Notiz-Detailansicht springen und den Titel bearbeiten",
|
||||
"edit-branch-prefix": "Dialog zum Bearbeiten des Zweigpräfixes anzeigen",
|
||||
"note-clipboard": "Notiz-Zwischenablage",
|
||||
"copy-notes-to-clipboard": "Ausgewählte Notizen in die Zwischenablage kopieren",
|
||||
"paste-notes-from-clipboard": "Notizen aus der Zwischenablage in die aktive Notiz einfügen",
|
||||
"cut-notes-to-clipboard": "Ausgewählte Notizen in die Zwischenablage ausschneiden",
|
||||
"select-all-notes-in-parent": "Alle Notizen der aktuellen Notizenebene auswählen",
|
||||
"add-note-above-to-the-selection": "Notiz oberhalb der Auswahl hinzufügen",
|
||||
"add-note-below-to-selection": "Notiz unterhalb der Auswahl hinzufügen",
|
||||
"duplicate-subtree": "Unterbaum duplizieren",
|
||||
"tabs-and-windows": "Tabs & Fenster",
|
||||
"open-new-tab": "Neuen Tab öffnen",
|
||||
"close-active-tab": "Aktiven Tab schließen",
|
||||
"reopen-last-tab": "Zuletzt geschlossenen Tab wieder öffnen",
|
||||
"activate-next-tab": "Rechten Tab aktivieren",
|
||||
"activate-previous-tab": "Linken Tab aktivieren",
|
||||
"open-new-window": "Neues leeres Fenster öffnen",
|
||||
"toggle-tray": "Anwendung im Systemtray anzeigen/verstecken",
|
||||
"first-tab": "Ersten Tab in der Liste aktivieren",
|
||||
"second-tab": "Zweiten Tab in der Liste aktivieren",
|
||||
"third-tab": "Dritten Tab in der Liste aktivieren",
|
||||
"fourth-tab": "Vierten Tab in der Liste aktivieren",
|
||||
"fifth-tab": "Fünften Tab in der Liste aktivieren",
|
||||
"sixth-tab": "Sechsten Tab in der Liste aktivieren",
|
||||
"seventh-tab": "Siebten Tab in der Liste aktivieren",
|
||||
"eight-tab": "Achten Tab in der Liste aktivieren",
|
||||
"ninth-tab": "Neunten Tab in der Liste aktivieren",
|
||||
"last-tab": "Letzten Tab in der Liste aktivieren",
|
||||
"dialogs": "Dialoge",
|
||||
"show-note-source": "Notizquellen-Dialog anzeigen",
|
||||
"show-options": "Optionen-Dialog anzeigen",
|
||||
"show-revisions": "Notizrevisionen-Dialog anzeigen",
|
||||
"show-recent-changes": "Letzte Änderungen-Dialog anzeigen",
|
||||
"show-sql-console": "SQL-Konsole-Dialog anzeigen",
|
||||
"show-backend-log": "Backend-Logs-Dialog anzeigen",
|
||||
"text-note-operations": "Textnotiz-Operationen",
|
||||
"add-link-to-text": "Dialogfeld zum Hinzufügen eines Links zum Text öffnen",
|
||||
"follow-link-under-cursor": "Folge dem Link, unter dem Mauszeiger",
|
||||
"insert-date-and-time-to-text": "Aktuelles Datum & Uhrzeit in den Text einfügen",
|
||||
"paste-markdown-into-text": "Markdown aus der Zwischenablage in die Textnotiz einfügen",
|
||||
"cut-into-note": "Auswahl aus der aktuellen Notiz ausschneiden und eine Unternotiz mit dem ausgewählten Text erstellen",
|
||||
"add-include-note-to-text": "Notiz-Einfügen-Dialog öffnen",
|
||||
"edit-readonly-note": "Schreibgeschützte Notiz bearbeiten",
|
||||
"attributes-labels-and-relations": "Attribute (Labels & Verknüpfungen)",
|
||||
"add-new-label": "Neues Label erstellen",
|
||||
"create-new-relation": "Neue Verknüpfungen",
|
||||
"ribbon-tabs": "Ribbon-Tabs",
|
||||
"toggle-basic-properties": "Grundattribute umschalten",
|
||||
"toggle-file-properties": "Dateiattribute umschalten",
|
||||
"toggle-image-properties": "Bildattribute umschalten",
|
||||
"toggle-owned-attributes": "Eigene Attribute umschalten",
|
||||
"toggle-inherited-attributes": "Vererbte Attribute umschalten",
|
||||
"toggle-promoted-attributes": "Beworbene Attribute umschalten",
|
||||
"toggle-link-map": "Link-Karte umschalten",
|
||||
"toggle-note-info": "Notizinformationen umschalten",
|
||||
"toggle-note-paths": "Notizpfade umschalten",
|
||||
"toggle-similar-notes": "Ähnliche Notizen umschalten",
|
||||
"other": "Sonstige",
|
||||
"toggle-right-pane": "Anzeige der rechten Leiste umschalten, das Inhaltsverzeichnis und Markierungen enthält",
|
||||
"print-active-note": "Aktive Notiz drucken",
|
||||
"open-note-externally": "Notiz als Datei mit Standardanwendung öffnen",
|
||||
"render-active-note": "Aktive Notiz rendern (erneut rendern)",
|
||||
"run-active-note": "Aktive JavaScript(Frontend/Backend)-Codenotiz ausführen",
|
||||
"toggle-note-hoisting": "Notiz-Fokus der aktiven Notiz umschalten",
|
||||
"unhoist": "Notiz-Fokus aufheben",
|
||||
"reload-frontend-app": "Frontend-App neuladen",
|
||||
"open-dev-tools": "Entwicklertools öffnen",
|
||||
"toggle-left-note-tree-panel": "Linke Notizbaum-Leiste umschalten",
|
||||
"toggle-full-screen": "Vollbildmodus umschalten",
|
||||
"zoom-out": "Herauszoomen",
|
||||
"zoom-in": "Hineinzoomen",
|
||||
"note-navigation": "Notiznavigation",
|
||||
"reset-zoom-level": "Zoomlevel zurücksetzen",
|
||||
"copy-without-formatting": "Ausgewählten Text ohne Formatierung kopieren",
|
||||
"force-save-revision": "Erstellen / Speichern einer neuen Notizrevision der aktiven Notiz erzwingen",
|
||||
"show-help": "Eingebaute Hilfe / Cheat-Sheet anzeigen",
|
||||
"toggle-book-properties": "Buch-Eigenschaften umschalten",
|
||||
"clone-notes-to": "Ausgewählte Notizen duplizieren",
|
||||
"open-command-palette": "Kommandopalette öffnen",
|
||||
"export-as-pdf": "Aktuelle Notiz als PDF exportieren",
|
||||
"back-in-note-history": "Navigiere zur vorherigen Notiz im Verlauf",
|
||||
"forward-in-note-history": "Navigiere zur nächsten Notiz im Verlauf",
|
||||
"scroll-to-active-note": "Zum aktiven Notizbaumeintrag springen",
|
||||
"quick-search": "Schnellsuche öffnen",
|
||||
"create-note-after": "Erstelle eine neue Notiz nach der aktuellen Notiz",
|
||||
"create-note-into": "Unternotiz zur aktiven Notiz anlegen",
|
||||
"move-notes-to": "Ausgewählte Notizen verschieben",
|
||||
"show-cheatsheet": "Übersicht der Tastenkombinationen anzeigen",
|
||||
"find-in-text": "Suchleiste umschalten",
|
||||
"toggle-classic-editor-toolbar": "Schalte um zum Formatierungs-Tab für den Editor mit fester Werkzeugleiste",
|
||||
"toggle-zen-mode": "Zen-Modus ein-/ausschalten (reduzierte Benutzeroberfläche für ablenkungsfreies Arbeiten)"
|
||||
},
|
||||
"login": {
|
||||
"title": "Anmeldung",
|
||||
"heading": "Trilium Anmeldung",
|
||||
"incorrect-password": "Das Passwort ist falsch. Bitte versuche es erneut.",
|
||||
"password": "Passwort",
|
||||
"remember-me": "Angemeldet bleiben",
|
||||
"button": "Anmelden"
|
||||
},
|
||||
"set_password": {
|
||||
"title": "Passwort festlegen",
|
||||
"heading": "Passwort festlegen",
|
||||
"description": "Bevor du Trilium im Web verwenden kannst, musst du zuerst ein Passwort festlegen. Du wirst dieses Passwort dann zur Anmeldung verwenden.",
|
||||
"password": "Passwort",
|
||||
"password-confirmation": "Passwortbestätigung",
|
||||
"button": "Passwort festlegen"
|
||||
},
|
||||
"javascript-required": "Trilium erfordert, dass JavaScript aktiviert ist.",
|
||||
"setup": {
|
||||
"heading": "Trilium Notes Setup",
|
||||
"new-document": "Ich bin ein neuer Benutzer und möchte ein neues Trilium-Dokument für meine Notizen erstellen",
|
||||
"sync-from-desktop": "Ich habe bereits eine Desktop-Instanz und möchte die Synchronisierung damit einrichten",
|
||||
"sync-from-server": "Ich habe bereits eine Server-Instanz und möchte die Synchronisierung damit einrichten",
|
||||
"next": "Weiter",
|
||||
"init-in-progress": "Dokumenteninitialisierung läuft",
|
||||
"redirecting": "Du wirst in Kürze zur Anwendung weitergeleitet.",
|
||||
"title": "Setup"
|
||||
},
|
||||
"setup_sync-from-desktop": {
|
||||
"heading": "Synchronisation vom Desktop",
|
||||
"description": "Dieses Setup muss von der Desktop-Instanz aus initiiert werden:",
|
||||
"step1": "Öffne deine Trilium Notes Desktop-Instanz.",
|
||||
"step2": "Klicke im Trilium-Menü auf Optionen.",
|
||||
"step3": "Klicke auf die Kategorie Synchronisation.",
|
||||
"step4": "Ändere die Server-Instanzadresse auf: {{- host}} und klicke auf Speichern.",
|
||||
"step5": "Klicke auf den Button \"Test-Synchronisation\", um zu überprüfen, ob die Verbindung erfolgreich ist.",
|
||||
"step6": "Sobald du diese Schritte abgeschlossen hast, klicke auf {{- link}}.",
|
||||
"step6-here": "hier"
|
||||
},
|
||||
"setup_sync-from-server": {
|
||||
"heading": "Synchronisation vom Server",
|
||||
"instructions": "Bitte gib unten die Trilium-Server-Adresse und die Zugangsdaten ein. Dies wird das gesamte Trilium-Dokument vom Server herunterladen und die Synchronisation einrichten. Je nach Dokumentgröße und Verbindungsgeschwindigkeit kann dies eine Weile dauern.",
|
||||
"server-host": "Trilium Server-Adresse",
|
||||
"server-host-placeholder": "https://<hostname>:<port>",
|
||||
"proxy-server": "Proxy-Server (optional)",
|
||||
"proxy-server-placeholder": "https://<hostname>:<port>",
|
||||
"note": "Hinweis:",
|
||||
"proxy-instruction": "Wenn du die Proxy-Einstellung leer lässt, wird der System-Proxy verwendet (gilt nur für die Desktop-Anwendung)",
|
||||
"password": "Passwort",
|
||||
"password-placeholder": "Passwort",
|
||||
"back": "Zurück",
|
||||
"finish-setup": "Setup abschließen"
|
||||
},
|
||||
"setup_sync-in-progress": {
|
||||
"heading": "Synchronisation läuft",
|
||||
"successful": "Die Synchronisation wurde erfolgreich eingerichtet. Es wird eine Weile dauern, bis die erste Synchronisation abgeschlossen ist. Sobald dies erledigt ist, wirst du zur Anmeldeseite weitergeleitet.",
|
||||
"outstanding-items": "Ausstehende Synchronisationselemente:",
|
||||
"outstanding-items-default": "N/A"
|
||||
},
|
||||
"share_404": {
|
||||
"title": "Nicht gefunden",
|
||||
"heading": "Nicht gefunden"
|
||||
},
|
||||
"share_page": {
|
||||
"parent": "Übergeordnete Notiz:",
|
||||
"clipped-from": "Diese Notiz wurde ursprünglich von {{- url}} ausgeschnitten",
|
||||
"child-notes": "Untergeordnete Notizen:",
|
||||
"no-content": "Diese Notiz hat keinen Inhalt."
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Montag",
|
||||
"tuesday": "Dienstag",
|
||||
"wednesday": "Mittwoch",
|
||||
"thursday": "Donnerstag",
|
||||
"friday": "Freitag",
|
||||
"saturday": "Samstag",
|
||||
"sunday": "Sonntag"
|
||||
},
|
||||
"months": {
|
||||
"january": "Januar",
|
||||
"february": "Februar",
|
||||
"march": "März",
|
||||
"april": "April",
|
||||
"may": "Mai",
|
||||
"june": "Juni",
|
||||
"july": "Juli",
|
||||
"august": "August",
|
||||
"september": "September",
|
||||
"october": "Oktober",
|
||||
"november": "November",
|
||||
"december": "Dezember"
|
||||
},
|
||||
"special_notes": {
|
||||
"search_prefix": "Suche:"
|
||||
},
|
||||
"test_sync": {
|
||||
"not-configured": "Der Synchronisations-Server-Host ist nicht konfiguriert. Bitte konfiguriere zuerst die Synchronisation.",
|
||||
"successful": "Die Server-Verbindung wurde erfolgreich hergestellt, die Synchronisation wurde gestartet."
|
||||
},
|
||||
"hidden-subtree": {
|
||||
"root-title": "Versteckte Notizen",
|
||||
"search-history-title": "Suchverlauf",
|
||||
"note-map-title": "Notiz Karte",
|
||||
"sql-console-history-title": "SQL Konsolen Verlauf",
|
||||
"shared-notes-title": "Geteilte Notizen",
|
||||
"bulk-action-title": "Massenverarbeitung",
|
||||
"backend-log-title": "Backend Log",
|
||||
"user-hidden-title": "Versteckt vom Nutzer",
|
||||
"launch-bar-templates-title": "Startleiste Vorlagen",
|
||||
"base-abstract-launcher-title": "Basis Abstrakte Startleiste",
|
||||
"command-launcher-title": "Befehlslauncher",
|
||||
"note-launcher-title": "Notiz Launcher",
|
||||
"script-launcher-title": "Script Launcher",
|
||||
"built-in-widget-title": "Eingebautes Widget",
|
||||
"spacer-title": "Freifeld",
|
||||
"custom-widget-title": "Custom Widget",
|
||||
"launch-bar-title": "Launchbar",
|
||||
"available-launchers-title": "Verfügbare Launchers",
|
||||
"go-to-previous-note-title": "Zur vorherigen Notiz gehen",
|
||||
"go-to-next-note-title": "Zur nächsten Notiz gehen",
|
||||
"new-note-title": "Neue Notiz",
|
||||
"search-notes-title": "Notizen durchsuchen",
|
||||
"calendar-title": "Kalender",
|
||||
"recent-changes-title": "neue Änderungen",
|
||||
"bookmarks-title": "Lesezeichen",
|
||||
"open-today-journal-note-title": "Heutigen Journaleintrag öffnen",
|
||||
"quick-search-title": "Schnellsuche",
|
||||
"protected-session-title": "Geschützte Sitzung",
|
||||
"sync-status-title": "Sync Status",
|
||||
"settings-title": "Einstellungen",
|
||||
"options-title": "Optionen",
|
||||
"appearance-title": "Erscheinungsbild",
|
||||
"shortcuts-title": "Tastaturkürzel",
|
||||
"text-notes": "Text Notizen",
|
||||
"code-notes-title": "Code Notizen",
|
||||
"images-title": "Bilder",
|
||||
"spellcheck-title": "Rechtschreibprüfung",
|
||||
"password-title": "Passwort",
|
||||
"etapi-title": "ETAPI",
|
||||
"backup-title": "Sicherung",
|
||||
"sync-title": "Sync",
|
||||
"other": "Weitere",
|
||||
"advanced-title": "Erweitert",
|
||||
"visible-launchers-title": "Sichtbare Launcher",
|
||||
"user-guide": "Nutzerhandbuch"
|
||||
},
|
||||
"notes": {
|
||||
"new-note": "Neue Notiz",
|
||||
"duplicate-note-suffix": "(dup)",
|
||||
"duplicate-note-title": "{{- noteTitle }} {{ duplicateNoteSuffix }}"
|
||||
},
|
||||
"backend_log": {
|
||||
"log-does-not-exist": "Die Backend-Log-Datei '{{ fileName }}' existiert (noch) nicht.",
|
||||
"reading-log-failed": "Das Lesen der Backend-Log-Datei '{{ fileName }}' ist fehlgeschlagen."
|
||||
},
|
||||
"content_renderer": {
|
||||
"note-cannot-be-displayed": "Dieser Notiztyp kann nicht angezeigt werden."
|
||||
},
|
||||
"pdf": {
|
||||
"export_filter": "PDF Dokument (*.pdf)",
|
||||
"unable-to-export-message": "Die aktuelle Notiz konnte nicht als PDF exportiert werden.",
|
||||
"unable-to-export-title": "Export als PDF fehlgeschlagen",
|
||||
"unable-to-save-message": "Die ausgewählte Datei konnte nicht beschrieben werden. Erneut versuchen oder ein anderes Ziel auswählen."
|
||||
},
|
||||
"tray": {
|
||||
"tooltip": "Trilium Notes",
|
||||
"close": "Trilium schließen",
|
||||
"recents": "Kürzliche Notizen",
|
||||
"bookmarks": "Lesezeichen",
|
||||
"today": "Heutigen Journal Eintrag öffnen",
|
||||
"new-note": "Neue Notiz",
|
||||
"show-windows": "Fenster anzeigen"
|
||||
},
|
||||
"hidden_subtree_templates": {
|
||||
"table": "Tabelle",
|
||||
"board_status_done": "Erledigt"
|
||||
},
|
||||
"keyboard_action_names": {
|
||||
"copy-notes-to-clipboard": "Notizen in Zwischenablage kopieren",
|
||||
"paste-notes-from-clipboard": "Notizen aus Zwischenablage einfügen",
|
||||
"back-in-note-history": "Zurück im Notizverlauf",
|
||||
"forward-in-note-history": "Vorwärts im Notizverlauf",
|
||||
"jump-to-note": "Wechseln zu...",
|
||||
"command-palette": "Befehlsübersicht",
|
||||
"scroll-to-active-note": "Zur aktiven Notiz scrollen",
|
||||
"quick-search": "Schnellsuche",
|
||||
"search-in-subtree": "In Unterzweig suchen",
|
||||
"expand-subtree": "Unterzweig aufklappen",
|
||||
"collapse-tree": "Baumstruktur einklappen",
|
||||
"collapse-subtree": "Unterzweig einklappen",
|
||||
"sort-child-notes": "Unternotizen sortieren",
|
||||
"create-note-after": "Erstelle eine neue Notiz dahinter",
|
||||
"create-note-into": "Erstelle eine neue Notiz davor",
|
||||
"create-note-into-inbox": "Neue Notiz in Inbox erstellen",
|
||||
"delete-notes": "Notizen löschen",
|
||||
"move-note-up": "Notiz nach oben verschieben",
|
||||
"move-note-down": "Notiz nach unten verschieben"
|
||||
}
|
||||
"keyboard_actions": {
|
||||
"open-jump-to-note-dialog": "Öffne das Dialogfeld \"Zu Notiz springen\"",
|
||||
"search-in-subtree": "Nach Notizen im Unterbaum der aktuellen Notiz suchen",
|
||||
"expand-subtree": "Unterbaum der aktuellen Notiz ausklappen",
|
||||
"collapse-tree": "Gesamten Notizbaum einklappen",
|
||||
"collapse-subtree": "Unterbaum der aktuellen Notiz einklappen",
|
||||
"sort-child-notes": "Untergeordnete Notizen sortieren",
|
||||
"creating-and-moving-notes": "Notizen erstellen und verschieben",
|
||||
"create-note-into-inbox": "Erstelle eine Notiz im Posteingang (falls definiert) oder in der Tagesnotiz",
|
||||
"delete-note": "Notiz löschen",
|
||||
"move-note-up": "Notiz nach oben verschieben",
|
||||
"move-note-down": "Notiz nach unten verschieben",
|
||||
"move-note-up-in-hierarchy": "Notiz in der Hierarchie nach oben verschieben",
|
||||
"move-note-down-in-hierarchy": "Notiz in der Hierarchie nach unten verschieben",
|
||||
"edit-note-title": "Vom Notiz-Baum zur Notiz-Detailansicht springen und den Titel bearbeiten",
|
||||
"edit-branch-prefix": "Dialog zum Bearbeiten des Zweigpräfixes anzeigen",
|
||||
"note-clipboard": "Notiz-Zwischenablage",
|
||||
"copy-notes-to-clipboard": "Ausgewählte Notizen in die Zwischenablage kopieren",
|
||||
"paste-notes-from-clipboard": "Notizen aus der Zwischenablage in die aktive Notiz einfügen",
|
||||
"cut-notes-to-clipboard": "Ausgewählte Notizen in die Zwischenablage ausschneiden",
|
||||
"select-all-notes-in-parent": "Alle Notizen der aktuellen Notizenebene auswählen",
|
||||
"add-note-above-to-the-selection": "Notiz oberhalb der Auswahl hinzufügen",
|
||||
"add-note-below-to-selection": "Notiz unterhalb der Auswahl hinzufügen",
|
||||
"duplicate-subtree": "Unterbaum duplizieren",
|
||||
"tabs-and-windows": "Tabs & Fenster",
|
||||
"open-new-tab": "Neuen Tab öffnen",
|
||||
"close-active-tab": "Aktiven Tab schließen",
|
||||
"reopen-last-tab": "Zuletzt geschlossenen Tab wieder öffnen",
|
||||
"activate-next-tab": "Rechten Tab aktivieren",
|
||||
"activate-previous-tab": "Linken Tab aktivieren",
|
||||
"open-new-window": "Neues leeres Fenster öffnen",
|
||||
"toggle-tray": "Anwendung im Systemtray anzeigen/verstecken",
|
||||
"first-tab": "Ersten Tab in der Liste aktivieren",
|
||||
"second-tab": "Zweiten Tab in der Liste aktivieren",
|
||||
"third-tab": "Dritten Tab in der Liste aktivieren",
|
||||
"fourth-tab": "Vierten Tab in der Liste aktivieren",
|
||||
"fifth-tab": "Fünften Tab in der Liste aktivieren",
|
||||
"sixth-tab": "Sechsten Tab in der Liste aktivieren",
|
||||
"seventh-tab": "Siebten Tab in der Liste aktivieren",
|
||||
"eight-tab": "Achten Tab in der Liste aktivieren",
|
||||
"ninth-tab": "Neunten Tab in der Liste aktivieren",
|
||||
"last-tab": "Letzten Tab in der Liste aktivieren",
|
||||
"dialogs": "Dialoge",
|
||||
"show-note-source": "Notizquellen-Dialog anzeigen",
|
||||
"show-options": "Optionen-Dialog anzeigen",
|
||||
"show-revisions": "Notizrevisionen-Dialog anzeigen",
|
||||
"show-recent-changes": "Letzte Änderungen-Dialog anzeigen",
|
||||
"show-sql-console": "SQL-Konsole-Dialog anzeigen",
|
||||
"show-backend-log": "Backend-Logs-Dialog anzeigen",
|
||||
"text-note-operations": "Textnotiz-Operationen",
|
||||
"add-link-to-text": "Dialogfeld zum Hinzufügen eines Links zum Text öffnen",
|
||||
"follow-link-under-cursor": "Folge dem Link, unter dem Mauszeiger",
|
||||
"insert-date-and-time-to-text": "Aktuelles Datum & Uhrzeit in den Text einfügen",
|
||||
"paste-markdown-into-text": "Markdown aus der Zwischenablage in die Textnotiz einfügen",
|
||||
"cut-into-note": "Auswahl aus der aktuellen Notiz ausschneiden und eine Unternotiz mit dem ausgewählten Text erstellen",
|
||||
"add-include-note-to-text": "Notiz-Einfügen-Dialog öffnen",
|
||||
"edit-readonly-note": "Schreibgeschützte Notiz bearbeiten",
|
||||
"attributes-labels-and-relations": "Attribute (Labels & Verknüpfungen)",
|
||||
"add-new-label": "Neues Label erstellen",
|
||||
"create-new-relation": "Neue Verknüpfungen",
|
||||
"ribbon-tabs": "Ribbon-Tabs",
|
||||
"toggle-basic-properties": "Grundattribute umschalten",
|
||||
"toggle-file-properties": "Dateiattribute umschalten",
|
||||
"toggle-image-properties": "Bildattribute umschalten",
|
||||
"toggle-owned-attributes": "Eigene Attribute umschalten",
|
||||
"toggle-inherited-attributes": "Vererbte Attribute umschalten",
|
||||
"toggle-promoted-attributes": "Beworbene Attribute umschalten",
|
||||
"toggle-link-map": "Link-Karte umschalten",
|
||||
"toggle-note-info": "Notizinformationen umschalten",
|
||||
"toggle-note-paths": "Notizpfade umschalten",
|
||||
"toggle-similar-notes": "Ähnliche Notizen umschalten",
|
||||
"other": "Sonstige",
|
||||
"toggle-right-pane": "Anzeige der rechten Leiste umschalten, das Inhaltsverzeichnis und Markierungen enthält",
|
||||
"print-active-note": "Aktive Notiz drucken",
|
||||
"open-note-externally": "Notiz als Datei mit Standardanwendung öffnen",
|
||||
"render-active-note": "Aktive Notiz rendern (erneut rendern)",
|
||||
"run-active-note": "Aktive JavaScript(Frontend/Backend)-Codenotiz ausführen",
|
||||
"toggle-note-hoisting": "Notiz-Fokus der aktiven Notiz umschalten",
|
||||
"unhoist": "Notiz-Fokus aufheben",
|
||||
"reload-frontend-app": "Frontend-App neuladen",
|
||||
"open-dev-tools": "Entwicklertools öffnen",
|
||||
"toggle-left-note-tree-panel": "Linke Notizbaum-Leiste umschalten",
|
||||
"toggle-full-screen": "Vollbildmodus umschalten",
|
||||
"zoom-out": "Herauszoomen",
|
||||
"zoom-in": "Hineinzoomen",
|
||||
"note-navigation": "Notiznavigation",
|
||||
"reset-zoom-level": "Zoomlevel zurücksetzen",
|
||||
"copy-without-formatting": "Ausgewählten Text ohne Formatierung kopieren",
|
||||
"force-save-revision": "Erstellen / Speichern einer neuen Notizrevision der aktiven Notiz erzwingen",
|
||||
"show-help": "Eingebaute Hilfe / Cheat-Sheet anzeigen",
|
||||
"toggle-book-properties": "Buch-Eigenschaften umschalten",
|
||||
"clone-notes-to": "Ausgewählte Notizen duplizieren",
|
||||
"open-command-palette": "Kommandopalette öffnen",
|
||||
"export-as-pdf": "Aktuelle Notiz als PDF exportieren",
|
||||
"back-in-note-history": "Navigiere zur vorherigen Notiz im Verlauf",
|
||||
"forward-in-note-history": "Navigiere zur nächsten Notiz im Verlauf",
|
||||
"scroll-to-active-note": "Zum aktiven Notizbaumeintrag springen",
|
||||
"quick-search": "Schnellsuche öffnen",
|
||||
"create-note-after": "Erstelle eine neue Notiz nach der aktuellen Notiz",
|
||||
"create-note-into": "Unternotiz zur aktiven Notiz anlegen",
|
||||
"move-notes-to": "Ausgewählte Notizen verschieben",
|
||||
"show-cheatsheet": "Übersicht der Tastenkombinationen anzeigen",
|
||||
"find-in-text": "Suchleiste umschalten",
|
||||
"toggle-classic-editor-toolbar": "Schalte um zum Formatierungs-Tab für den Editor mit fester Werkzeugleiste",
|
||||
"toggle-zen-mode": "Zen-Modus ein-/ausschalten (reduzierte Benutzeroberfläche für ablenkungsfreies Arbeiten)"
|
||||
},
|
||||
"login": {
|
||||
"title": "Anmeldung",
|
||||
"heading": "Trilium Anmeldung",
|
||||
"incorrect-password": "Das Passwort ist falsch. Bitte versuche es erneut.",
|
||||
"password": "Passwort",
|
||||
"remember-me": "Angemeldet bleiben",
|
||||
"button": "Anmelden"
|
||||
},
|
||||
"set_password": {
|
||||
"title": "Passwort festlegen",
|
||||
"heading": "Passwort festlegen",
|
||||
"description": "Bevor du Trilium im Web verwenden kannst, musst du zuerst ein Passwort festlegen. Du wirst dieses Passwort dann zur Anmeldung verwenden.",
|
||||
"password": "Passwort",
|
||||
"password-confirmation": "Passwortbestätigung",
|
||||
"button": "Passwort festlegen"
|
||||
},
|
||||
"javascript-required": "Trilium erfordert, dass JavaScript aktiviert ist.",
|
||||
"setup": {
|
||||
"heading": "Trilium Notes Setup",
|
||||
"new-document": "Ich bin ein neuer Benutzer und möchte ein neues Trilium-Dokument für meine Notizen erstellen",
|
||||
"sync-from-desktop": "Ich habe bereits eine Desktop-Instanz und möchte die Synchronisierung damit einrichten",
|
||||
"sync-from-server": "Ich habe bereits eine Server-Instanz und möchte die Synchronisierung damit einrichten",
|
||||
"next": "Weiter",
|
||||
"init-in-progress": "Dokumenteninitialisierung läuft",
|
||||
"redirecting": "Du wirst in Kürze zur Anwendung weitergeleitet.",
|
||||
"title": "Setup"
|
||||
},
|
||||
"setup_sync-from-desktop": {
|
||||
"heading": "Synchronisation vom Desktop",
|
||||
"description": "Dieses Setup muss von der Desktop-Instanz aus initiiert werden:",
|
||||
"step1": "Öffne deine Trilium Notes Desktop-Instanz.",
|
||||
"step2": "Klicke im Trilium-Menü auf Optionen.",
|
||||
"step3": "Klicke auf die Kategorie Synchronisation.",
|
||||
"step4": "Ändere die Server-Instanzadresse auf: {{- host}} und klicke auf Speichern.",
|
||||
"step5": "Klicke auf den Button \"Test-Synchronisation\", um zu überprüfen, ob die Verbindung erfolgreich ist.",
|
||||
"step6": "Sobald du diese Schritte abgeschlossen hast, klicke auf {{- link}}.",
|
||||
"step6-here": "hier"
|
||||
},
|
||||
"setup_sync-from-server": {
|
||||
"heading": "Synchronisation vom Server",
|
||||
"instructions": "Bitte gib unten die Trilium-Server-Adresse und die Zugangsdaten ein. Dies wird das gesamte Trilium-Dokument vom Server herunterladen und die Synchronisation einrichten. Je nach Dokumentgröße und Verbindungsgeschwindigkeit kann dies eine Weile dauern.",
|
||||
"server-host": "Trilium Server-Adresse",
|
||||
"server-host-placeholder": "https://<hostname>:<port>",
|
||||
"proxy-server": "Proxy-Server (optional)",
|
||||
"proxy-server-placeholder": "https://<hostname>:<port>",
|
||||
"note": "Hinweis:",
|
||||
"proxy-instruction": "Wenn du die Proxy-Einstellung leer lässt, wird der System-Proxy verwendet (gilt nur für die Desktop-Anwendung)",
|
||||
"password": "Passwort",
|
||||
"password-placeholder": "Passwort",
|
||||
"back": "Zurück",
|
||||
"finish-setup": "Setup abschließen"
|
||||
},
|
||||
"setup_sync-in-progress": {
|
||||
"heading": "Synchronisation läuft",
|
||||
"successful": "Die Synchronisation wurde erfolgreich eingerichtet. Es wird eine Weile dauern, bis die erste Synchronisation abgeschlossen ist. Sobald dies erledigt ist, wirst du zur Anmeldeseite weitergeleitet.",
|
||||
"outstanding-items": "Ausstehende Synchronisationselemente:",
|
||||
"outstanding-items-default": "N/A"
|
||||
},
|
||||
"share_404": {
|
||||
"title": "Nicht gefunden",
|
||||
"heading": "Nicht gefunden"
|
||||
},
|
||||
"share_page": {
|
||||
"parent": "Übergeordnete Notiz:",
|
||||
"clipped-from": "Diese Notiz wurde ursprünglich von {{- url}} ausgeschnitten",
|
||||
"child-notes": "Untergeordnete Notizen:",
|
||||
"no-content": "Diese Notiz hat keinen Inhalt."
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Montag",
|
||||
"tuesday": "Dienstag",
|
||||
"wednesday": "Mittwoch",
|
||||
"thursday": "Donnerstag",
|
||||
"friday": "Freitag",
|
||||
"saturday": "Samstag",
|
||||
"sunday": "Sonntag"
|
||||
},
|
||||
"months": {
|
||||
"january": "Januar",
|
||||
"february": "Februar",
|
||||
"march": "März",
|
||||
"april": "April",
|
||||
"may": "Mai",
|
||||
"june": "Juni",
|
||||
"july": "Juli",
|
||||
"august": "August",
|
||||
"september": "September",
|
||||
"october": "Oktober",
|
||||
"november": "November",
|
||||
"december": "Dezember"
|
||||
},
|
||||
"special_notes": {
|
||||
"search_prefix": "Suche:"
|
||||
},
|
||||
"test_sync": {
|
||||
"not-configured": "Der Synchronisations-Server-Host ist nicht konfiguriert. Bitte konfiguriere zuerst die Synchronisation.",
|
||||
"successful": "Die Server-Verbindung wurde erfolgreich hergestellt, die Synchronisation wurde gestartet."
|
||||
},
|
||||
"hidden-subtree": {
|
||||
"root-title": "Versteckte Notizen",
|
||||
"search-history-title": "Suchverlauf",
|
||||
"note-map-title": "Notiz Karte",
|
||||
"sql-console-history-title": "SQL Konsolen Verlauf",
|
||||
"shared-notes-title": "Geteilte Notizen",
|
||||
"bulk-action-title": "Massenverarbeitung",
|
||||
"backend-log-title": "Backend Log",
|
||||
"user-hidden-title": "Versteckt vom Nutzer",
|
||||
"launch-bar-templates-title": "Startleiste Vorlagen",
|
||||
"base-abstract-launcher-title": "Basis Abstrakte Startleiste",
|
||||
"command-launcher-title": "Befehlslauncher",
|
||||
"note-launcher-title": "Notiz Launcher",
|
||||
"script-launcher-title": "Script Launcher",
|
||||
"built-in-widget-title": "Eingebautes Widget",
|
||||
"spacer-title": "Freifeld",
|
||||
"custom-widget-title": "Custom Widget",
|
||||
"launch-bar-title": "Launchbar",
|
||||
"available-launchers-title": "Verfügbare Launchers",
|
||||
"go-to-previous-note-title": "Zur vorherigen Notiz gehen",
|
||||
"go-to-next-note-title": "Zur nächsten Notiz gehen",
|
||||
"new-note-title": "Neue Notiz",
|
||||
"search-notes-title": "Notizen durchsuchen",
|
||||
"calendar-title": "Kalender",
|
||||
"recent-changes-title": "neue Änderungen",
|
||||
"bookmarks-title": "Lesezeichen",
|
||||
"open-today-journal-note-title": "Heutigen Journaleintrag öffnen",
|
||||
"quick-search-title": "Schnellsuche",
|
||||
"protected-session-title": "Geschützte Sitzung",
|
||||
"sync-status-title": "Sync Status",
|
||||
"settings-title": "Einstellungen",
|
||||
"options-title": "Optionen",
|
||||
"appearance-title": "Erscheinungsbild",
|
||||
"shortcuts-title": "Tastaturkürzel",
|
||||
"text-notes": "Text Notizen",
|
||||
"code-notes-title": "Code Notizen",
|
||||
"images-title": "Bilder",
|
||||
"spellcheck-title": "Rechtschreibprüfung",
|
||||
"password-title": "Passwort",
|
||||
"etapi-title": "ETAPI",
|
||||
"backup-title": "Sicherung",
|
||||
"sync-title": "Sync",
|
||||
"other": "Weitere",
|
||||
"advanced-title": "Erweitert",
|
||||
"visible-launchers-title": "Sichtbare Launcher",
|
||||
"user-guide": "Nutzerhandbuch"
|
||||
},
|
||||
"notes": {
|
||||
"new-note": "Neue Notiz",
|
||||
"duplicate-note-suffix": "(dup)",
|
||||
"duplicate-note-title": "{{- noteTitle }} {{ duplicateNoteSuffix }}"
|
||||
},
|
||||
"backend_log": {
|
||||
"log-does-not-exist": "Die Backend-Log-Datei '{{ fileName }}' existiert (noch) nicht.",
|
||||
"reading-log-failed": "Das Lesen der Backend-Log-Datei '{{ fileName }}' ist fehlgeschlagen."
|
||||
},
|
||||
"content_renderer": {
|
||||
"note-cannot-be-displayed": "Dieser Notiztyp kann nicht angezeigt werden."
|
||||
},
|
||||
"pdf": {
|
||||
"export_filter": "PDF Dokument (*.pdf)",
|
||||
"unable-to-export-message": "Die aktuelle Notiz konnte nicht als PDF exportiert werden.",
|
||||
"unable-to-export-title": "Export als PDF fehlgeschlagen",
|
||||
"unable-to-save-message": "Die ausgewählte Datei konnte nicht beschrieben werden. Erneut versuchen oder ein anderes Ziel auswählen."
|
||||
},
|
||||
"tray": {
|
||||
"tooltip": "Trilium Notes",
|
||||
"close": "Trilium schließen",
|
||||
"recents": "Kürzliche Notizen",
|
||||
"bookmarks": "Lesezeichen",
|
||||
"today": "Heutigen Journal Eintrag öffnen",
|
||||
"new-note": "Neue Notiz",
|
||||
"show-windows": "Fenster anzeigen"
|
||||
},
|
||||
"hidden_subtree_templates": {
|
||||
"table": "Tabelle",
|
||||
"board_status_done": "Erledigt"
|
||||
},
|
||||
"keyboard_action_names": {
|
||||
"copy-notes-to-clipboard": "Notizen in Zwischenablage kopieren",
|
||||
"paste-notes-from-clipboard": "Notizen aus Zwischenablage einfügen",
|
||||
"back-in-note-history": "Zurück im Notizverlauf",
|
||||
"forward-in-note-history": "Vorwärts im Notizverlauf",
|
||||
"jump-to-note": "Wechseln zu...",
|
||||
"command-palette": "Befehlsübersicht",
|
||||
"scroll-to-active-note": "Zur aktiven Notiz scrollen",
|
||||
"quick-search": "Schnellsuche",
|
||||
"search-in-subtree": "In Unterzweig suchen",
|
||||
"expand-subtree": "Unterzweig aufklappen",
|
||||
"collapse-tree": "Baumstruktur einklappen",
|
||||
"collapse-subtree": "Unterzweig einklappen",
|
||||
"sort-child-notes": "Unternotizen sortieren",
|
||||
"create-note-after": "Erstelle eine neue Notiz dahinter",
|
||||
"create-note-into": "Erstelle eine neue Notiz davor",
|
||||
"create-note-into-inbox": "Neue Notiz in Inbox erstellen",
|
||||
"delete-notes": "Notizen löschen",
|
||||
"move-note-up": "Notiz nach oben verschieben",
|
||||
"move-note-down": "Notiz nach unten verschieben",
|
||||
"edit-note-title": "Bearbeite Notiz Titel",
|
||||
"clone-notes-to": "Vervielfältige Notiz nach",
|
||||
"move-notes-to": "Verschiebe Notiz nach",
|
||||
"cut-notes-to-clipboard": "Notizen in Zwischenablage ausschneiden",
|
||||
"add-note-above-to-selection": "Notiz oberhalb der Selektion hinzufügen",
|
||||
"add-note-below-to-selection": "Notiz unterhalb der Selektion hinzufügen",
|
||||
"open-new-tab": "Öffne im neuen Tab",
|
||||
"close-active-tab": "Schließe aktiven Tab",
|
||||
"reopen-last-tab": "Öffne zuletzt geschlossenen Tab",
|
||||
"activate-next-tab": "Aktiviere nächsten Tab",
|
||||
"activate-previous-tab": "Aktiviere vorherigen Tab",
|
||||
"open-new-window": "Öffne im neuen Fenster",
|
||||
"toggle-system-tray-icon": "Systemablage-Symbol umschalten",
|
||||
"toggle-zen-mode": "Zen-Modus umschalten",
|
||||
"switch-to-first-tab": "Wechsle zum ersten Tab",
|
||||
"switch-to-second-tab": "Wechsle zum zweiten Tab",
|
||||
"switch-to-third-tab": "Wechsle zum dritten Tab",
|
||||
"switch-to-fourth-tab": "Wechsle zum vierten Tab",
|
||||
"switch-to-fifth-tab": "Wechsle zum fünften Tab",
|
||||
"switch-to-sixth-tab": "Wechsle zum sechsten Tab",
|
||||
"switch-to-seventh-tab": "Wechsle zum siebten Tab",
|
||||
"switch-to-eighth-tab": "Wechsle zum achten Tab",
|
||||
"switch-to-ninth-tab": "Wechsle zum neunten Tab",
|
||||
"switch-to-last-tab": "Wechsle zum letzten Tab",
|
||||
"show-note-source": "Zeige Notiz Quelle",
|
||||
"show-options": "Zeige Optionen",
|
||||
"show-revisions": "Zeige Revisionen",
|
||||
"show-recent-changes": "Zeige letzte Änderungen",
|
||||
"show-sql-console": "Zeige SQL Konsole",
|
||||
"show-backend-log": "Zeige Backend-Protokoll",
|
||||
"show-help": "Zeige Hilfe",
|
||||
"show-cheatsheet": "Zeige Cheatsheet",
|
||||
"add-link-to-text": "Link zum Text hinzufügen"
|
||||
}
|
||||
}
|
||||
|
||||
1
apps/server/src/assets/translations/ko/server.json
Normal file
1
apps/server/src/assets/translations/ko/server.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -2,9 +2,9 @@
|
||||
"keyboard_actions": {
|
||||
"back-in-note-history": "Перейти до попередньої нотатки в історії",
|
||||
"forward-in-note-history": "Перейти до наступної нотатки в історії",
|
||||
"open-command-palette": "Відкрити панель команд",
|
||||
"open-command-palette": "Відкрити палітру команд",
|
||||
"scroll-to-active-note": "Прокрутити дерево нотаток до активної нотатки",
|
||||
"quick-search": "Показати панель швидкого пошуку",
|
||||
"quick-search": "Активувати панель швидкого пошуку",
|
||||
"search-in-subtree": "Пошук нотаток в піддереві активної нотатки",
|
||||
"expand-subtree": "Розкрити піддерево поточної нотатки",
|
||||
"collapse-tree": "Згорнути все дерево нотаток",
|
||||
@@ -13,8 +13,8 @@
|
||||
"sort-child-notes": "Сортувати дочірні нотатки",
|
||||
"creating-and-moving-notes": "Створення та переміщення нотаток",
|
||||
"create-note-after": "Створити нотатку після активної нотатки",
|
||||
"create-note-into": "Створити нотатку як дочірній елемент активної нотатки",
|
||||
"create-note-into-inbox": "Створити нотатку у «Вхідні» (якщо визначено) або в щоденнику",
|
||||
"create-note-into": "Створити нотатку як дочірню до активної нотатки",
|
||||
"create-note-into-inbox": "Створити нотатку у \"Вхідні\" (якщо визначено) або в щоденнику",
|
||||
"delete-note": "Видалити нотатку",
|
||||
"move-note-up": "Перемістити нотатку вгору",
|
||||
"move-note-down": "Перемістити нотатку вниз",
|
||||
@@ -24,18 +24,18 @@
|
||||
"edit-branch-prefix": "Показати вікно \"Редагувати префікс гілки\"",
|
||||
"clone-notes-to": "Клонувати вибрані нотатки",
|
||||
"move-notes-to": "Перемістити вибрані нотатки",
|
||||
"note-clipboard": "Буфер обміну нотаток",
|
||||
"note-clipboard": "Буфер обміну нотатки",
|
||||
"copy-notes-to-clipboard": "Копіювати вибрані нотатки в буфер обміну",
|
||||
"paste-notes-from-clipboard": "Вставити нотатки з буфера обміну в активну нотатку",
|
||||
"paste-notes-from-clipboard": "Вставити нотатки з буферу обміну в активну нотатку",
|
||||
"cut-notes-to-clipboard": "Вирізати вибрані нотатки в буфер обміну",
|
||||
"select-all-notes-in-parent": "Вибрати всі нотатки з поточного рівня нотаток",
|
||||
"add-note-above-to-the-selection": "Додати нотатку вище до виділення",
|
||||
"add-note-below-to-selection": "Додати нотатку нижче до вибраного",
|
||||
"duplicate-subtree": "Дублікат гілок дерева",
|
||||
"duplicate-subtree": "Дублювання піддерева",
|
||||
"tabs-and-windows": "Вкладки & Вікна",
|
||||
"open-new-tab": "Відкрити нову вкладку",
|
||||
"close-active-tab": "Закрити активну вкладку",
|
||||
"reopen-last-tab": "Знову відкрити останню закриту вкладку",
|
||||
"reopen-last-tab": "Відкрити останню закриту вкладку",
|
||||
"activate-next-tab": "Активувати вкладку праворуч",
|
||||
"activate-previous-tab": "Активувати вкладку ліворуч",
|
||||
"open-new-window": "Відкрити нове порожнє вікно",
|
||||
@@ -51,10 +51,10 @@
|
||||
"ninth-tab": "Активувати дев'яту вкладку у списку",
|
||||
"last-tab": "Активувати останню вкладку у списку",
|
||||
"dialogs": "Діалоги",
|
||||
"show-note-source": "Показати вікно «Джерело нотатки»",
|
||||
"show-note-source": "Показати \"Джерело нотатки\"",
|
||||
"show-options": "Відкрити \"Параметри\"",
|
||||
"show-revisions": "Показати вікно \"Зміни нотаток\"",
|
||||
"show-recent-changes": "Показати вікно \"Останні зміни\"",
|
||||
"show-revisions": "Показати \"Версії нотаток\"",
|
||||
"show-recent-changes": "Показати \"Останні зміни\"",
|
||||
"show-sql-console": "Відкрити \"Консоль SQL\"",
|
||||
"show-backend-log": "Відкрити \"Backend Log\"",
|
||||
"show-help": "Відкрити вбудований Посібник користувача",
|
||||
@@ -62,33 +62,33 @@
|
||||
"text-note-operations": "Дії з текстовими нотатками",
|
||||
"add-link-to-text": "Відкрити діалогове вікно для додавання посилання до тексту",
|
||||
"follow-link-under-cursor": "Перейдіть за посиланням, на якому знаходиться курсор",
|
||||
"insert-date-and-time-to-text": "Вставити поточну дату та час у текст",
|
||||
"insert-date-and-time-to-text": "Вставити поточну дату & час у текст",
|
||||
"paste-markdown-into-text": "Вставити Markdown з буфера обміну в текстову нотатку",
|
||||
"cut-into-note": "Вирізати виділений фрагмент із поточної нотатки та створити піднотатку з виділеним текстом",
|
||||
"add-include-note-to-text": "Відкрити вікно для додавання примітки",
|
||||
"add-include-note-to-text": "Відкрити вікно для додавання нотатки",
|
||||
"edit-readonly-note": "Редагувати нотатку, доступну тільки для читання",
|
||||
"attributes-labels-and-relations": "Атрибути (мітки та зв'язки)",
|
||||
"attributes-labels-and-relations": "Атрибути (мітки & зв'язки)",
|
||||
"add-new-label": "Створити нову мітку",
|
||||
"create-new-relation": "Створити новий зв'язок",
|
||||
"ribbon-tabs": "Вкладки стрічки",
|
||||
"toggle-basic-properties": "Увімкнути Основні властивості",
|
||||
"toggle-file-properties": "Увімкнути Властивості файлу",
|
||||
"toggle-image-properties": "Увімкнути Властивості зображення",
|
||||
"toggle-owned-attributes": "Увімкнути Власні атрибути",
|
||||
"toggle-inherited-attributes": "Увімкнути Успадковані атрибути",
|
||||
"toggle-promoted-attributes": "Увімкнути Рекламні атрибути",
|
||||
"toggle-link-map": "Увімкнути Карта посилань",
|
||||
"toggle-note-info": "Увімкнути Інформація про нотатку",
|
||||
"toggle-note-paths": "Увімкнути Шляхі нотатки",
|
||||
"toggle-similar-notes": "Увімкнути Схожі нотатки",
|
||||
"toggle-basic-properties": "Увімкнути Основні Властивості",
|
||||
"toggle-file-properties": "Увімкнути Властивості Файлу",
|
||||
"toggle-image-properties": "Увімкнути Властивості Зображення",
|
||||
"toggle-owned-attributes": "Увімкнути Власні Атрибути",
|
||||
"toggle-inherited-attributes": "Увімкнути Успадковані Атрибути",
|
||||
"toggle-promoted-attributes": "Увімкнути Просунуті Атрибути",
|
||||
"toggle-link-map": "Увімкнути Карта Посилань",
|
||||
"toggle-note-info": "Увімкнути Інформація про Нотатку",
|
||||
"toggle-note-paths": "Увімкнути Шляхи Нотатки",
|
||||
"toggle-similar-notes": "Увімкнути Схожі Нотатки",
|
||||
"other": "Інше",
|
||||
"toggle-right-pane": "Увімкнути відображення правої панелі, яка містить Зміст та Основні моменти",
|
||||
"print-active-note": "Друк активної нотатки",
|
||||
"open-note-externally": "Відкрити нотатку як файл у програмі за замовчуванням",
|
||||
"render-active-note": "Відтворити (повторно відтворити) активну нотатку",
|
||||
"run-active-note": "Виконати активний код JavaScript (фронтенд/бекенд) нотатки",
|
||||
"toggle-note-hoisting": "Увімкнути Хостинг активної нотатки",
|
||||
"unhoist": "Зніміть з будь-якого місця",
|
||||
"run-active-note": "Виконати активний код JavaScript (frontend/backend) нотатки з кодом",
|
||||
"toggle-note-hoisting": "Увімкнути хостинг активної нотатки",
|
||||
"unhoist": "Зняти з будь-якого місця",
|
||||
"reload-frontend-app": "Перезавантажити інтерфейс",
|
||||
"open-dev-tools": "Відкрити інструменти розробника",
|
||||
"find-in-text": "Увімкнути панель пошуку",
|
||||
@@ -97,53 +97,53 @@
|
||||
"zoom-out": "Зменшити масштаб",
|
||||
"zoom-in": "Збільшити масштаб",
|
||||
"note-navigation": "Навігація по нотатках",
|
||||
"reset-zoom-level": "Скинути рівень масштабування",
|
||||
"reset-zoom-level": "Скинути масштабування",
|
||||
"copy-without-formatting": "Копіювати виділений текст без форматування",
|
||||
"force-save-revision": "Примусове створення/збереження нової версії активної нотатки",
|
||||
"toggle-book-properties": "Увімкнути Властивості колекції",
|
||||
"toggle-book-properties": "Увімкнути Властивості Колекції",
|
||||
"toggle-classic-editor-toolbar": "Увімкнути вкладку Форматування для редактора з фіксованою панеллю інструментів",
|
||||
"export-as-pdf": "Експортувати поточну нотатку у PDF",
|
||||
"toggle-zen-mode": "Вмикає/вимикає дзен-режим (мінімальний інтерфейс для більш цілеспрямованого редагування)"
|
||||
"export-as-pdf": "Експортувати поточну нотатку в PDF",
|
||||
"toggle-zen-mode": "Вмикає/вимикає Дзен-режим (мінімальний інтерфейс для більш сфокусованого редагування)"
|
||||
},
|
||||
"keyboard_action_names": {
|
||||
"back-in-note-history": "Назад до Історії нотаток",
|
||||
"forward-in-note-history": "Вперед в Історії нотаток",
|
||||
"back-in-note-history": "Назад до Історії Нотаток",
|
||||
"forward-in-note-history": "Вперед в Історія Нотаток",
|
||||
"jump-to-note": "Перейти до...",
|
||||
"command-palette": "Палітра команд",
|
||||
"scroll-to-active-note": "Прокрутити до Активної нотатки",
|
||||
"quick-search": "Швидкий пошук",
|
||||
"search-in-subtree": "Пошук у піддереві",
|
||||
"expand-subtree": "Розгорнути піддерево",
|
||||
"collapse-tree": "Згорнути дерево",
|
||||
"collapse-subtree": "Згорнути піддерево",
|
||||
"sort-child-notes": "Сортувати дочірні нотатки",
|
||||
"create-note-after": "Створити нотатку після",
|
||||
"create-note-into": "Створити нотатку в",
|
||||
"create-note-into-inbox": "Створити нотатку у \"Вхідні\"",
|
||||
"delete-notes": "Видалити нотатки",
|
||||
"move-note-up": "Перемістити нотатку вгору",
|
||||
"move-note-down": "Перемістити нотатку вниз",
|
||||
"move-note-up-in-hierarchy": "Перемістити нотатку вгору в ієрархії",
|
||||
"move-note-down-in-hierarchy": "Перемістити нотатку вниз в ієрархії",
|
||||
"edit-note-title": "Редагувати назву нотатки",
|
||||
"edit-branch-prefix": "Редагувати префікс гілки",
|
||||
"clone-notes-to": "Клонувати нотатки до",
|
||||
"move-notes-to": "Перемістити нотатки до",
|
||||
"copy-notes-to-clipboard": "Копіювати нотатки в буфер обміну",
|
||||
"paste-notes-from-clipboard": "Вставити нотатки з буфера обміну",
|
||||
"cut-notes-to-clipboard": "Вирізати нотатки в буфер обміну",
|
||||
"select-all-notes-in-parent": "Вибрати всі нотатки в Батьківські",
|
||||
"add-note-above-to-selection": "Додати примітку вище до виділення",
|
||||
"add-note-below-to-selection": "Додати нотатку нижче до виділення",
|
||||
"duplicate-subtree": "Дублікат піддерева",
|
||||
"open-new-tab": "Відкрити нову вкладку",
|
||||
"close-active-tab": "Закрити активну вкладку",
|
||||
"reopen-last-tab": "Відкрити останню вкладку",
|
||||
"activate-next-tab": "Активувати наступну вкладку",
|
||||
"activate-previous-tab": "Активувати попередню вкладку",
|
||||
"open-new-window": "Відкрити нове вікно",
|
||||
"command-palette": "Палітра Команд",
|
||||
"scroll-to-active-note": "Прокрутити до Активної Нотатки",
|
||||
"quick-search": "Швидкий Пошук",
|
||||
"search-in-subtree": "Пошук у Піддереві",
|
||||
"expand-subtree": "Розгорнути Піддерево",
|
||||
"collapse-tree": "Згорнути Дерево",
|
||||
"collapse-subtree": "Згорнути Піддерево",
|
||||
"sort-child-notes": "Сортувати Дочірні Нотатки",
|
||||
"create-note-after": "Створити Нотатку після",
|
||||
"create-note-into": "Створити Нотатку в",
|
||||
"create-note-into-inbox": "Створити Нотатку у \"Вхідні\"",
|
||||
"delete-notes": "Видалити Нотатки",
|
||||
"move-note-up": "Перемістити Нотатку вгору",
|
||||
"move-note-down": "Перемістити Нотатку вниз",
|
||||
"move-note-up-in-hierarchy": "Перемістити Нотатку вгору в ієрархії",
|
||||
"move-note-down-in-hierarchy": "Перемістити Нотатку вниз в ієрархії",
|
||||
"edit-note-title": "Редагувати Заголовок Нотатки",
|
||||
"edit-branch-prefix": "Редагувати Префікс Гілки",
|
||||
"clone-notes-to": "Клонувати Нотатки до",
|
||||
"move-notes-to": "Перемістити Нотатки до",
|
||||
"copy-notes-to-clipboard": "Копіювати Нотатки в Буфер обміну",
|
||||
"paste-notes-from-clipboard": "Вставити Нотатки з Буфера обміну",
|
||||
"cut-notes-to-clipboard": "Вирізати Нотатки в Буфер обміну",
|
||||
"select-all-notes-in-parent": "Вибрати всі Нотатки в \"Батьківські\"",
|
||||
"add-note-above-to-selection": "Додати Нотатку вище до виділення",
|
||||
"add-note-below-to-selection": "Додати Нотатку нижче до виділення",
|
||||
"duplicate-subtree": "Дублікат Піддерева",
|
||||
"open-new-tab": "Відкрити Нову Вкладку",
|
||||
"close-active-tab": "Закрити Активну Вкладку",
|
||||
"reopen-last-tab": "Відкрити Останню Вкладку",
|
||||
"activate-next-tab": "Активувати Наступну Вкладку",
|
||||
"activate-previous-tab": "Активувати Попередню Вкладку",
|
||||
"open-new-window": "Відкрити Нове Вікно",
|
||||
"toggle-system-tray-icon": "Увімкнути Значок системного трея",
|
||||
"toggle-zen-mode": "Увімкнути режим дзен",
|
||||
"toggle-zen-mode": "Увімкнути Дзен-режим",
|
||||
"switch-to-first-tab": "Перейти до першої вкладки",
|
||||
"switch-to-second-tab": "Перейти до другої вкладки",
|
||||
"switch-to-third-tab": "Перейти до третьої вкладки",
|
||||
@@ -153,13 +153,53 @@
|
||||
"switch-to-seventh-tab": "Перейти до сьомої вкладки",
|
||||
"switch-to-eighth-tab": "Перейти до восьмої вкладки",
|
||||
"find-in-text": "Знайти в тексті",
|
||||
"toggle-left-pane": "Перемкнути ліву панель",
|
||||
"toggle-full-screen": "Перемкнути повноекранний режим",
|
||||
"toggle-left-pane": "Увімкнути ліву панель",
|
||||
"toggle-full-screen": "Увімкнути повноекранний режим",
|
||||
"zoom-out": "Зменшити масштаб",
|
||||
"zoom-in": "Збільшити масштаб",
|
||||
"reset-zoom-level": "Скинути рівень масштабування",
|
||||
"reset-zoom-level": "Скинути масштабування",
|
||||
"copy-without-formatting": "Копіювати без форматування",
|
||||
"force-save-revision": "Примусове збереження версії"
|
||||
"force-save-revision": "Примусове збереження версії",
|
||||
"switch-to-ninth-tab": "Перейти на дев'яту вкладку",
|
||||
"switch-to-last-tab": "Перейти до останньої вкладки",
|
||||
"show-note-source": "Показати Джерело Нотатки",
|
||||
"show-options": "Показати Параметри",
|
||||
"show-revisions": "Показати Версії",
|
||||
"show-recent-changes": "Показати Останні Зміни",
|
||||
"show-sql-console": "Показати консоль SQL",
|
||||
"show-backend-log": "Показати Backend Log",
|
||||
"show-help": "Показати Довідку",
|
||||
"show-cheatsheet": "Показати Шпаргалку",
|
||||
"add-link-to-text": "Додати посилання до тексту",
|
||||
"follow-link-under-cursor": "Перейти за посиланням під курсором",
|
||||
"insert-date-and-time-to-text": "Вставити Дату та Час у текст",
|
||||
"paste-markdown-into-text": "Вставити Markdown у текст",
|
||||
"cut-into-note": "Вирізати у Нотатку",
|
||||
"add-include-note-to-text": "Додати включену Нотатку до тексту",
|
||||
"edit-read-only-note": "Редагувати нотатку лише для читання",
|
||||
"add-new-label": "Додати Нову Мітку",
|
||||
"add-new-relation": "Додати Новий Зв'язок",
|
||||
"toggle-ribbon-tab-classic-editor": "Включити вкладку стрічки \"Класичний Редактор\"",
|
||||
"toggle-ribbon-tab-basic-properties": "Включити вкладку стрічки \"Основні властивості\"",
|
||||
"toggle-ribbon-tab-book-properties": "Включити вкладку стрічки \"Властивості Книги\"",
|
||||
"toggle-ribbon-tab-file-properties": "Включити вкладку стрічки \"Властивості файлу\"",
|
||||
"toggle-ribbon-tab-image-properties": "Включити вкладку стрічки \"Властивості Зображення\"",
|
||||
"toggle-ribbon-tab-owned-attributes": "Включити вкладку стрічки \"Власні атрибути\"",
|
||||
"toggle-ribbon-tab-inherited-attributes": "Включити вкладку стрічки \"Успадковані Атрибути\"",
|
||||
"toggle-ribbon-tab-promoted-attributes": "Включити вкладку стрічки \"Просунуті Атрибути\"",
|
||||
"toggle-ribbon-tab-note-map": "Включити вкладку стрічки \"Карта Нотатки\"",
|
||||
"toggle-ribbon-tab-note-info": "Включити вкладку стрічки \"Інформація про Нотатку\"",
|
||||
"toggle-ribbon-tab-note-paths": "Включити вкладку стрічки \"Шляхи Нотатки\"",
|
||||
"toggle-ribbon-tab-similar-notes": "Включити вкладку стрічки \"Схожі Нотатки\"",
|
||||
"toggle-right-pane": "Включити праву панель",
|
||||
"print-active-note": "Друк Активної Нотатки",
|
||||
"export-active-note-as-pdf": "Експорт активної нотатки у PDF",
|
||||
"open-note-externally": "Відкрити Нотатку зовнішньою програмою",
|
||||
"render-active-note": "Відтворити Активну Нотатку",
|
||||
"run-active-note": "Запустити Активну Нотатку",
|
||||
"toggle-note-hoisting": "Включити хостинг нотатки",
|
||||
"reload-frontend-app": "Перезавантажити інтерфейс програми",
|
||||
"open-developer-tools": "Відкрити Інструменти розробника"
|
||||
},
|
||||
"login": {
|
||||
"title": "Увійти",
|
||||
@@ -172,6 +212,215 @@
|
||||
"sign_in_with_sso": "Увійти за допомогою {{ ssoIssuerName }}"
|
||||
},
|
||||
"set_password": {
|
||||
"title": "Встановити пароль"
|
||||
"title": "Встановити пароль",
|
||||
"heading": "Встановити пароль",
|
||||
"description": "Перш ніж почати користуватися Trilium з веб-сайту, вам потрібно спочатку встановити пароль. Потім ви будете використовувати цей пароль для входу в систему.",
|
||||
"password": "Пароль",
|
||||
"password-confirmation": "Підтвердження пароля",
|
||||
"button": "Встановити пароль"
|
||||
},
|
||||
"javascript-required": "Для роботи Trilium потрібен JavaScript.",
|
||||
"setup": {
|
||||
"heading": "Налаштування Trilium Notes",
|
||||
"new-document": "Я новий користувач і хочу створити новий документ Trilium для своїх нотаток",
|
||||
"sync-from-desktop": "У мене вже є екземпляр для ПК і я хочу налаштувати синхронізацію з ним",
|
||||
"sync-from-server": "У мене вже є екземпляр сервера, і я хочу налаштувати синхронізацію з ним",
|
||||
"next": "Наступна",
|
||||
"init-in-progress": "Триває ініціалізація документа",
|
||||
"redirecting": "Невдовзі вас буде перенаправлено до програми.",
|
||||
"title": "Налаштування"
|
||||
},
|
||||
"setup_sync-from-desktop": {
|
||||
"heading": "Синхронізація з ПК",
|
||||
"description": "Це налаштування потрібно ініціювати з екземпляра ПК:",
|
||||
"step1": "Відкрийте екземпляр Trilium Notes на ПК.",
|
||||
"step2": "У меню Trilium натисніть Параметри.",
|
||||
"step3": "Натисніть на Категорія Синхронізації.",
|
||||
"step4": "Змініть адресу екземпляра сервера на: {{- host}} та натисніть кнопку Зберегти.",
|
||||
"step5": "Натисніть \"Тест синхронізації\", щоб перевірити успішність підключення.",
|
||||
"step6": "Після виконання цих кроків натисніть {{- link}}.",
|
||||
"step6-here": "тут"
|
||||
},
|
||||
"setup_sync-from-server": {
|
||||
"heading": "Синхронізація з сервера",
|
||||
"instructions": "Будь ласка, введіть адресу сервера Trilium та облікові дані нижче. Це завантажить весь документ Trilium із сервера та налаштує синхронізацію з ним. Залежно від розміру документа та швидкості вашого з’єднання, це може зайняти деякий час.",
|
||||
"server-host": "Адреса сервера Trilium",
|
||||
"server-host-placeholder": "https://<hostname>:<port>",
|
||||
"proxy-server": "Проксі-сервер (необов'язково)",
|
||||
"proxy-server-placeholder": "https://<hostname>:<port>",
|
||||
"note": "Нотатка:",
|
||||
"proxy-instruction": "Якщо залишити налаштування проксі-сервера порожнім, використовуватиметься системний проксі-сервер (стосується лише ПК програми)",
|
||||
"password": "Пароль",
|
||||
"password-placeholder": "Пароль",
|
||||
"back": "Назад",
|
||||
"finish-setup": "Завершити налаштування"
|
||||
},
|
||||
"setup_sync-in-progress": {
|
||||
"heading": "Триває синхронізація",
|
||||
"successful": "Синхронізацію налаштовано правильно. Початкова синхронізація завершиться через деякий час. Після її завершення вас буде перенаправлено на сторінку входу.",
|
||||
"outstanding-items": "Незавершені елементи синхронізації:",
|
||||
"outstanding-items-default": "Недоступно"
|
||||
},
|
||||
"share_404": {
|
||||
"title": "Не знайдено",
|
||||
"heading": "Не знайдено"
|
||||
},
|
||||
"share_page": {
|
||||
"parent": "батьківська:",
|
||||
"clipped-from": "Цю нотатку було спочатку вирізано з {{- url}}",
|
||||
"child-notes": "Дочірні нотатки:",
|
||||
"no-content": "Ця нотатка не має вмісту."
|
||||
},
|
||||
"weekdays": {
|
||||
"monday": "Понеділок",
|
||||
"tuesday": "Вівторок",
|
||||
"wednesday": "Середа",
|
||||
"thursday": "Четвер",
|
||||
"friday": "П'ятниця",
|
||||
"saturday": "Субота",
|
||||
"sunday": "Неділя"
|
||||
},
|
||||
"weekdayNumber": "Тиждень {weekNumber}",
|
||||
"months": {
|
||||
"january": "Січень",
|
||||
"february": "Лютий",
|
||||
"march": "Березень",
|
||||
"april": "Квітень",
|
||||
"may": "Травень",
|
||||
"june": "Червень",
|
||||
"july": "Липень",
|
||||
"august": "Серпень",
|
||||
"september": "Вересень",
|
||||
"october": "Жовтень",
|
||||
"november": "Листопад",
|
||||
"december": "Грудень"
|
||||
},
|
||||
"quarterNumber": "Квартал {quarterNumber}",
|
||||
"special_notes": {
|
||||
"search_prefix": "Пошук:"
|
||||
},
|
||||
"test_sync": {
|
||||
"not-configured": "Хост сервера синхронізації не налаштовано. Спочатку налаштуйте синхронізацію.",
|
||||
"successful": "Установлення зв'язку з сервером синхронізації пройшло успішно, синхронізація розпочалася."
|
||||
},
|
||||
"hidden-subtree": {
|
||||
"root-title": "Приховані Нотатки",
|
||||
"search-history-title": "Історія Пошуку",
|
||||
"note-map-title": "Карта Нотатки",
|
||||
"sql-console-history-title": "Історія консолі SQL",
|
||||
"shared-notes-title": "Спільні Нотатки",
|
||||
"bulk-action-title": "Масова дія",
|
||||
"backend-log-title": "Backend Log",
|
||||
"user-hidden-title": "Прихований користувач",
|
||||
"launch-bar-templates-title": "Шаблони Панелі запуску",
|
||||
"base-abstract-launcher-title": "Базовий Абстрактний лаунчер",
|
||||
"command-launcher-title": "Командний лаунчер",
|
||||
"note-launcher-title": "Лаунчер нотаток",
|
||||
"script-launcher-title": "Запуск скриптів",
|
||||
"built-in-widget-title": "Вбудований віджет",
|
||||
"custom-widget-title": "Користувацький віджет",
|
||||
"launch-bar-title": "Панель запуску",
|
||||
"available-launchers-title": "Доступні лаунчери",
|
||||
"go-to-previous-note-title": "Перейти до попередньої нотатки",
|
||||
"go-to-next-note-title": "Перейти до наступної нотатки",
|
||||
"new-note-title": "Нова нотатка",
|
||||
"search-notes-title": "Пошук нотаток",
|
||||
"jump-to-note-title": "Перейти до...",
|
||||
"calendar-title": "Календар",
|
||||
"recent-changes-title": "Останні зміни",
|
||||
"bookmarks-title": "Закладки",
|
||||
"open-today-journal-note-title": "Відкрити Щоденник за сьогодні",
|
||||
"quick-search-title": "Швидкий пошук",
|
||||
"protected-session-title": "Захищений сеанс",
|
||||
"sync-status-title": "Статус синхронізації",
|
||||
"settings-title": "Налаштування",
|
||||
"llm-chat-title": "Чат з Нотатками",
|
||||
"options-title": "Параметри",
|
||||
"appearance-title": "Зовнішній вигляд",
|
||||
"shortcuts-title": "Швидкі клавіші",
|
||||
"text-notes": "Текстові Нотатки",
|
||||
"code-notes-title": "Нотатка з кодом",
|
||||
"images-title": "Зображення",
|
||||
"spellcheck-title": "Перевірка орфографії",
|
||||
"password-title": "Пароль",
|
||||
"multi-factor-authentication-title": "МФА",
|
||||
"etapi-title": "ETAPI",
|
||||
"backup-title": "Резервне копіювання",
|
||||
"sync-title": "Синхронізація",
|
||||
"ai-llm-title": "AI/LLM",
|
||||
"other": "Інше",
|
||||
"advanced-title": "Розширені",
|
||||
"visible-launchers-title": "Видимі лаунчери",
|
||||
"user-guide": "Посібник користувача",
|
||||
"localization": "Мова & регіон",
|
||||
"inbox-title": "Вхідні"
|
||||
},
|
||||
"notes": {
|
||||
"new-note": "Нова нотатка",
|
||||
"duplicate-note-suffix": "(дублікат)",
|
||||
"duplicate-note-title": "{{- noteTitle }} {{ duplicateNoteSuffix }}"
|
||||
},
|
||||
"backend_log": {
|
||||
"log-does-not-exist": "Файл backend log '{{ fileName }}' не існує.",
|
||||
"reading-log-failed": "Не вдалося прочитати backend log file {{fileName}}."
|
||||
},
|
||||
"content_renderer": {
|
||||
"note-cannot-be-displayed": "Цей тип нотатки не може бути відображений."
|
||||
},
|
||||
"pdf": {
|
||||
"export_filter": "PDF Document (*.pdf)",
|
||||
"unable-to-export-message": "Поточну нотатку не вдалося експортувати у PDF.",
|
||||
"unable-to-export-title": "Не вдається експортувати у PDF",
|
||||
"unable-to-save-message": "Не вдалося записати вибраний файл. Спробуйте ще раз або виберіть інше місце призначення."
|
||||
},
|
||||
"tray": {
|
||||
"tooltip": "Trilium Notes",
|
||||
"close": "Вихід з Trilium",
|
||||
"recents": "Останні нотатки",
|
||||
"bookmarks": "Закладки",
|
||||
"today": "Відкрити Щоденник за сьогодні",
|
||||
"new-note": "Нова нотатка",
|
||||
"show-windows": "Показати вікна",
|
||||
"open_new_window": "Відкрити нове вікно"
|
||||
},
|
||||
"migration": {
|
||||
"old_version": "Пряма міграція з вашої поточної версії не підтримується. Будь ласка, спочатку оновіть систему до останньої версії v0.60.4, а потім лише потім до цієї.",
|
||||
"error_message": "Помилка під час міграції до версії {{version}}: {{stack}}",
|
||||
"wrong_db_version": "Версія бази даних ({{version}}) новіша за ту, яку очікує програма ({{targetVersion}}), це означає, що її було створено новішою та несумісною версією Trilium. Оновіть Trilium до останньої версії, щоб вирішити цю проблему."
|
||||
},
|
||||
"modals": {
|
||||
"error_title": "Помилка"
|
||||
},
|
||||
"share_theme": {
|
||||
"site-theme": "Тема сайту",
|
||||
"search_placeholder": "Пошук...",
|
||||
"image_alt": "Зображення статті",
|
||||
"last-updated": "Останнє оновлення: {{- date}}",
|
||||
"subpages": "Підсторінки:",
|
||||
"on-this-page": "На цій сторінці",
|
||||
"expand": "Розгорнути"
|
||||
},
|
||||
"hidden_subtree_templates": {
|
||||
"text-snippet": "Фрагмент тексту",
|
||||
"description": "Опис",
|
||||
"list-view": "Список",
|
||||
"grid-view": "Сітка",
|
||||
"calendar": "Календар",
|
||||
"table": "Таблиця",
|
||||
"geo-map": "Географічна карта",
|
||||
"start-date": "Дата початку",
|
||||
"end-date": "Дата завершення",
|
||||
"start-time": "Час початку",
|
||||
"end-time": "Час завершення",
|
||||
"geolocation": "Геолокація",
|
||||
"built-in-templates": "Вбудовані шаблони",
|
||||
"board": "Дошка",
|
||||
"status": "Статус",
|
||||
"board_note_first": "Перша нотатка",
|
||||
"board_note_second": "Друга нотатка",
|
||||
"board_note_third": "Третя нотатка",
|
||||
"board_status_todo": "Зробити",
|
||||
"board_status_progress": "У процесі",
|
||||
"board_status_done": "Готово"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
|
||||
<title>Trilium Notes</title>
|
||||
</head>
|
||||
<body class="desktop heading-style-<%= headingStyle %> layout-<%= layoutOrientation %> platform-<%= platform %> <%= isElectron ? 'electron' : '' %> <%= hasNativeTitleBar ? 'native-titlebar' : '' %> <%= hasBackgroundEffects ? 'background-effects' : '' %>">
|
||||
<body id="trilium-app" class="desktop heading-style-<%= headingStyle %> layout-<%= layoutOrientation %> platform-<%= platform %> <%= isElectron ? 'electron' : '' %> <%= hasNativeTitleBar ? 'native-titlebar' : '' %> <%= hasBackgroundEffects ? 'background-effects' : '' %>">
|
||||
<noscript><%= t("javascript-required") %></noscript>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -50,7 +50,7 @@ function filterUrlValue(value: string) {
|
||||
}
|
||||
|
||||
function buildRewardMap(note: BNote) {
|
||||
// Need to use Map instead of object: https://github.com/TriliumNext/Trilium/issues/1895
|
||||
// Need to use Map instead of object: https://github.com/zadam/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/TriliumNext/Trilium/issues/1895
|
||||
// Need to use Map instead of object: https://github.com/zadam/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/TriliumNext/Trilium/issues/1977
|
||||
// most probably the log file does not exist yet - https://github.com/zadam/trilium/issues/1977
|
||||
if (isErrorInstance && "code" in e && e.code === "ENOENT") {
|
||||
log.error(e);
|
||||
return t("backend_log.log-does-not-exist", { fileName });
|
||||
|
||||
@@ -63,6 +63,7 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([
|
||||
"dailyBackupEnabled",
|
||||
"weeklyBackupEnabled",
|
||||
"monthlyBackupEnabled",
|
||||
"motionEnabled",
|
||||
"maxContentWidth",
|
||||
"compressImages",
|
||||
"downloadImagesAutomatically",
|
||||
|
||||
@@ -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/TriliumNext/Trilium/issues/3523
|
||||
// see https://github.com/zadam/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/TriliumNext/Trilium/issues/2887
|
||||
// querying for "content IS NULL" is expensive since content is not indexed. See e.g. https://github.com/zadam/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/TriliumNext/Trilium/issues/3017
|
||||
// old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/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/TriliumNext/Trilium/issues/510
|
||||
// recovery from https://github.com/zadam/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/TriliumNext/Trilium/issues/3181
|
||||
// https://github.com/zadam/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/TriliumNext/Trilium/issues/1289#issuecomment-704066809
|
||||
// <base> element will make sure external links are openable - https://github.com/zadam/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/TriliumNext/Trilium/issues/1552
|
||||
// avoid H1 per https://github.com/zadam/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/TriliumNext/Trilium/issues/2307
|
||||
// https://github.com/zadam/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/TriliumNext/Trilium/issues/2307
|
||||
// https://github.com/zadam/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/TriliumNext/Trilium/issues/3424
|
||||
// resource can be chunked into multiple events: https://github.com/zadam/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/TriliumNext/Trilium/issues/1862
|
||||
// https://github.com/zadam/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/TriliumNext/Trilium/issues/2440
|
||||
// https://github.com/zadam/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/TriliumNext/Trilium/issues/376
|
||||
// Mac has a different history navigation shortcuts - https://github.com/zadam/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/TriliumNext/Trilium/issues/376
|
||||
// Mac has a different history navigation shortcuts - https://github.com/zadam/trilium/issues/376
|
||||
defaultShortcuts: isMac ? ["CommandOrControl+Right"] : ["Alt+Right"],
|
||||
description: t("keyboard_actions.forward-in-note-history"),
|
||||
scope: "window"
|
||||
|
||||
@@ -152,6 +152,7 @@ const defaultOptions: DefaultOption[] = [
|
||||
},
|
||||
isSynced: false
|
||||
},
|
||||
{ name: "motionEnabled", value: "true", isSynced: false },
|
||||
|
||||
// Internationalization
|
||||
{ name: "locale", value: "en", isSynced: true },
|
||||
|
||||
@@ -330,7 +330,7 @@ class NoteContentFulltextExp extends Expression {
|
||||
|
||||
|
||||
stripTags(content: string) {
|
||||
// we want to allow link to preserve URLs: https://github.com/TriliumNext/Trilium/issues/2412
|
||||
// we want to allow link to preserve URLs: https://github.com/zadam/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/TriliumNext/Trilium/issues/4310
|
||||
// https://github.com/zadam/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/TriliumNext/Trilium/issues/3374#issuecomment-1341053765
|
||||
// https://github.com/zadam/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/TriliumNext/Trilium/pull/3957
|
||||
// https://github.com/zadam/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/TriliumNext/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/zadam/trilium).
|
||||
|
||||
For more details, see the [wiki page](https://github.com/TriliumNext/Trilium/wiki/Web-clipper).
|
||||
For more details, see the [wiki page](https://github.com/zadam/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/TriliumNext/Trilium-web-clipper",
|
||||
"homepage_url": "https://github.com/zadam/trilium-web-clipper",
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
|
||||
"icons": {
|
||||
"32": "icons/32.png",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user