diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index f06e7b3e9..2fe298217 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -111,6 +111,9 @@ jobs: - dockerfile: Dockerfile platform: linux/arm/v7 image: ubuntu-24.04-arm + - dockerfile: Dockerfile + platform: linux/arm/v8 + image: ubuntu-24.04-arm runs-on: ${{ matrix.image }} needs: - test_docker diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 248e32538..52afe1134 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -37,7 +37,7 @@ jobs: shell: bash forge_platform: darwin - name: linux - image: ubuntu-latest + image: ubuntu-22.04 shell: bash forge_platform: linux - name: windows @@ -102,7 +102,7 @@ jobs: arch: [x64, arm64] include: - arch: x64 - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 - arch: arm64 runs-on: ubuntu-24.04-arm runs-on: ${{ matrix.runs-on }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f571d634d..5f2bfbc62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,7 +73,7 @@ jobs: arch: [x64, arm64] include: - arch: x64 - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 - arch: arm64 runs-on: ubuntu-24.04-arm runs-on: ${{ matrix.runs-on }} diff --git a/.gitignore b/.gitignore index cf2fa89dd..ecb8c9edf 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,6 @@ apps/*/out upload .rollup.cache -*.tsbuildinfo \ No newline at end of file +*.tsbuildinfo + +/result \ No newline at end of file diff --git a/README.md b/README.md index ce0b930bc..f62084f7d 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q * [Evernote](https://triliumnext.github.io/Docs/Wiki/evernote-import) and [Markdown import & export](https://triliumnext.github.io/Docs/Wiki/markdown) * [Web Clipper](https://triliumnext.github.io/Docs/Wiki/web-clipper) for easy saving of web content * Customizable UI (sidebar buttons, user-defined widgets, ...) +* [Metrics](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics.md), along with a [Grafana Dashboard](./docs/User%20Guide/User%20Guide/Advanced%20Usage/Metrics/grafana-dashboard.json) ✨ Check out the following third-party resources/communities for more TriliumNext related goodies: diff --git a/_regroup/bin/build-docker.sh b/_regroup/bin/build-docker.sh deleted file mode 100644 index d95c289d4..000000000 --- a/_regroup/bin/build-docker.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -e # Fail on any command error - -VERSION=`jq -r ".version" package.json` -SERIES=${VERSION:0:4}-latest - -sudo docker build -t triliumnext/notes:$VERSION --network host -t triliumnext/notes:$SERIES . - -if [[ $VERSION != *"beta"* ]]; then - sudo docker tag triliumnext/notes:$VERSION triliumnext/notes:latest -fi diff --git a/_regroup/package.json b/_regroup/package.json index 80e210e1d..117be7cc6 100644 --- a/_regroup/package.json +++ b/_regroup/package.json @@ -36,12 +36,12 @@ }, "devDependencies": { "@playwright/test": "1.52.0", - "@stylistic/eslint-plugin": "4.4.0", + "@stylistic/eslint-plugin": "4.4.1", "@types/express": "5.0.1", - "@types/node": "22.15.29", + "@types/node": "22.15.30", "@types/yargs": "17.0.33", - "@vitest/coverage-v8": "3.1.4", - "eslint": "9.27.0", + "@vitest/coverage-v8": "3.2.2", + "eslint": "9.28.0", "eslint-plugin-simple-import-sort": "12.1.1", "esm": "3.2.25", "jsdoc": "4.0.4", diff --git a/_regroup/test-etapi/_login.http b/_regroup/test-etapi/_login.http deleted file mode 100644 index 9976e7cd4..000000000 --- a/_regroup/test-etapi/_login.http +++ /dev/null @@ -1,12 +0,0 @@ -POST {{triliumHost}}/etapi/auth/login -Content-Type: application/json - -{ - "password": "1234" -} - -> {% - client.assert(response.status === 201); - - client.global.set("authToken", response.body.authToken); -%} diff --git a/_regroup/test-etapi/api-metrics.http b/_regroup/test-etapi/api-metrics.http deleted file mode 100644 index 78aee7217..000000000 --- a/_regroup/test-etapi/api-metrics.http +++ /dev/null @@ -1,43 +0,0 @@ -### Test regular API metrics endpoint (requires session authentication) - -### Get metrics from regular API (default Prometheus format) -GET {{triliumHost}}/api/metrics - -> {% -client.test("API metrics endpoint returns Prometheus format by default", function() { - client.assert(response.status === 200, "Response status is not 200"); - client.assert(response.headers["content-type"].includes("text/plain"), "Content-Type should be text/plain"); - client.assert(response.body.includes("trilium_info"), "Should contain trilium_info metric"); - client.assert(response.body.includes("trilium_notes_total"), "Should contain trilium_notes_total metric"); - client.assert(response.body.includes("# HELP"), "Should contain HELP comments"); - client.assert(response.body.includes("# TYPE"), "Should contain TYPE comments"); -}); -%} - -### Get metrics in JSON format -GET {{triliumHost}}/api/metrics?format=json - -> {% -client.test("API metrics endpoint returns JSON when requested", function() { - client.assert(response.status === 200, "Response status is not 200"); - client.assert(response.headers["content-type"].includes("application/json"), "Content-Type should be application/json"); - client.assert(response.body.version, "Version info not present"); - client.assert(response.body.database, "Database info not present"); - client.assert(response.body.timestamp, "Timestamp not present"); - client.assert(typeof response.body.database.totalNotes === 'number', "Total notes should be a number"); - client.assert(typeof response.body.database.activeNotes === 'number', "Active notes should be a number"); - client.assert(response.body.noteTypes, "Note types breakdown not present"); - client.assert(response.body.attachmentTypes, "Attachment types breakdown not present"); - client.assert(response.body.statistics, "Statistics not present"); -}); -%} - -### Test invalid format parameter -GET {{triliumHost}}/api/metrics?format=xml - -> {% -client.test("Invalid format parameter returns error", function() { - client.assert(response.status === 500, "Response status should be 500"); - client.assert(response.body.message.includes("prometheus"), "Error message should mention supported formats"); -}); -%} \ No newline at end of file diff --git a/_regroup/test-etapi/app-info.http b/_regroup/test-etapi/app-info.http deleted file mode 100644 index a851005c2..000000000 --- a/_regroup/test-etapi/app-info.http +++ /dev/null @@ -1,7 +0,0 @@ -GET {{triliumHost}}/etapi/app-info -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.clipperProtocolVersion === "1.0"); -%} diff --git a/_regroup/test-etapi/basic-auth.http b/_regroup/test-etapi/basic-auth.http deleted file mode 100644 index cf79c357e..000000000 --- a/_regroup/test-etapi/basic-auth.http +++ /dev/null @@ -1,21 +0,0 @@ -GET {{triliumHost}}/etapi/app-info -Authorization: Basic etapi {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.clipperProtocolVersion === "1.0"); -%} - -### - -GET {{triliumHost}}/etapi/app-info -Authorization: Basic etapi wrong - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/app-info -Authorization: Basic wrong {{authToken}} - -> {% client.assert(response.status === 401); %} diff --git a/_regroup/test-etapi/create-backup.http b/_regroup/test-etapi/create-backup.http deleted file mode 100644 index 59ffbebc4..000000000 --- a/_regroup/test-etapi/create-backup.http +++ /dev/null @@ -1,4 +0,0 @@ -PUT {{triliumHost}}/etapi/backup/etapi_test -Authorization: {{authToken}} - -> {% client.assert(response.status === 201); %} diff --git a/_regroup/test-etapi/create-entities.http b/_regroup/test-etapi/create-entities.http deleted file mode 100644 index 98dae28b1..000000000 --- a/_regroup/test-etapi/create-entities.http +++ /dev/null @@ -1,158 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "forcedId{{$randomInt}}", - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!", - "dateCreated": "2023-08-21 23:38:51.123+0200", - "utcDateCreated": "2023-08-21 23:38:51.123Z" -} - -> {% - client.assert(response.status === 201); - client.assert(response.body.note.noteId.startsWith("forcedId")); - client.assert(response.body.note.title == "Hello"); - client.assert(response.body.note.dateCreated == "2023-08-21 23:38:51.123+0200"); - client.assert(response.body.note.utcDateCreated == "2023-08-21 23:38:51.123Z"); - client.assert(response.body.branch.parentNoteId == "root"); - - client.log(`Created note ` + response.body.note.noteId + ` and branch ` + response.body.branch.branchId); - - client.global.set("createdNoteId", response.body.note.noteId); - client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### Clone to another location - -POST {{triliumHost}}/etapi/branches -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "parentNoteId": "_hidden" -} - -> {% - client.assert(response.status === 201); - client.assert(response.body.parentNoteId == "_hidden"); - - client.global.set("clonedBranchId", response.body.branchId); - - client.log(`Created cloned branch ` + response.body.branchId); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.noteId == client.global.get("createdNoteId")); - client.assert(response.body.title == "Hello"); - // order is not defined and may fail in the future - client.assert(response.body.parentBranchIds[0] == client.global.get("clonedBranchId")) - client.assert(response.body.parentBranchIds[1] == client.global.get("createdBranchId")); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body == "Hi there!"); -%} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.branchId == client.global.get("createdBranchId")); - client.assert(response.body.parentNoteId == "root"); -%} - -### - -GET {{triliumHost}}/etapi/branches/{{clonedBranchId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.branchId == client.global.get("clonedBranchId")); - client.assert(response.body.parentNoteId == "_hidden"); -%} - -### - -POST {{triliumHost}}/etapi/attributes -Content-Type: application/json -Authorization: {{authToken}} - -{ - "attributeId": "forcedAttributeId{{$randomInt}}", - "noteId": "{{createdNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% - client.assert(response.status === 201); - client.assert(response.body.attributeId.startsWith("forcedAttributeId")); - - client.global.set("createdAttributeId", response.body.attributeId); -%} - -### - -GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.attributeId == client.global.get("createdAttributeId")); -%} - -### - -POST {{triliumHost}}/etapi/attachments -Content-Type: application/json -Authorization: {{authToken}} - -{ - "ownerId": "{{createdNoteId}}", - "role": "file", - "mime": "plain/text", - "title": "my attachment", - "content": "my text" -} - -> {% - client.assert(response.status === 201); - - client.global.set("createdAttachmentId", response.body.attachmentId); -%} - -### - -GET {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body.attachmentId == client.global.get("createdAttachmentId")); - client.assert(response.body.role == "file"); - client.assert(response.body.mime == "plain/text"); - client.assert(response.body.title == "my attachment"); -%} diff --git a/_regroup/test-etapi/delete-attachment.http b/_regroup/test-etapi/delete-attachment.http deleted file mode 100644 index d12e8de43..000000000 --- a/_regroup/test-etapi/delete-attachment.http +++ /dev/null @@ -1,52 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -POST {{triliumHost}}/etapi/attachments -Authorization: {{authToken}} -Content-Type: application/json - -{ - "ownerId": "{{createdNoteId}}", - "role": "file", - "mime": "text/plain", - "title": "my attachment", - "content": "text" -} - -> {% client.global.set("createdAttachmentId", response.body.attachmentId); %} - -### - -DELETE {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### repeat the DELETE request to test the idempotency - -DELETE {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### - -GET {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "ATTACHMENT_NOT_FOUND"); -%} diff --git a/_regroup/test-etapi/delete-attribute.http b/_regroup/test-etapi/delete-attribute.http deleted file mode 100644 index d61b75ba2..000000000 --- a/_regroup/test-etapi/delete-attribute.http +++ /dev/null @@ -1,52 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% client.global.set("createdAttributeId", response.body.attributeId); %} - -### - -DELETE {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### repeat the DELETE request to test the idempotency - -DELETE {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### - -GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "ATTRIBUTE_NOT_FOUND"); -%} diff --git a/_regroup/test-etapi/delete-cloned-branch.http b/_regroup/test-etapi/delete-cloned-branch.http deleted file mode 100644 index a87a6fa4d..000000000 --- a/_regroup/test-etapi/delete-cloned-branch.http +++ /dev/null @@ -1,87 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% - client.global.set("createdNoteId", response.body.note.noteId); - client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### Clone to another location - -POST {{triliumHost}}/etapi/branches -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "parentNoteId": "_hidden" -} - -> {% client.global.set("clonedBranchId", response.body.branchId); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/branches/{{clonedBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -DELETE {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### repeat the DELETE request to test the idempotency - -DELETE {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "BRANCH_NOT_FOUND"); -%} - -### - -GET {{triliumHost}}/etapi/branches/{{clonedBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} diff --git a/_regroup/test-etapi/delete-note-with-all-branches.http b/_regroup/test-etapi/delete-note-with-all-branches.http deleted file mode 100644 index 5a50bc4a9..000000000 --- a/_regroup/test-etapi/delete-note-with-all-branches.http +++ /dev/null @@ -1,126 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% - client.global.set("createdNoteId", response.body.note.noteId); - client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% client.global.set("createdAttributeId", response.body.attributeId); %} - -### Clone to another location - -POST {{triliumHost}}/etapi/branches -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "parentNoteId": "_hidden" -} - -> {% client.global.set("clonedBranchId", response.body.branchId); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/branches/{{clonedBranchId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -DELETE {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### repeat the DELETE request to test the idempotency - -DELETE {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% client.assert(response.status === 204, "Response status is not 204"); %} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "BRANCH_NOT_FOUND"); -%} - -### - -GET {{triliumHost}}/etapi/branches/{{clonedBranchId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code == "BRANCH_NOT_FOUND"); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "NOTE_NOT_FOUND"); -%} - -### - -GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% - client.assert(response.status === 404, "Response status is not 404"); - client.assert(response.body.code === "ATTRIBUTE_NOT_FOUND"); -%} diff --git a/_regroup/test-etapi/export-note-subtree.http b/_regroup/test-etapi/export-note-subtree.http deleted file mode 100644 index 28d90a362..000000000 --- a/_regroup/test-etapi/export-note-subtree.http +++ /dev/null @@ -1,37 +0,0 @@ -GET {{triliumHost}}/etapi/notes/root/export -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.headers.valueOf("Content-Type") == "application/zip"); -%} - -### - -GET {{triliumHost}}/etapi/notes/root/export?format=html -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.headers.valueOf("Content-Type") == "application/zip"); -%} - -### - -GET {{triliumHost}}/etapi/notes/root/export?format=markdown -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.headers.valueOf("Content-Type") == "application/zip"); -%} - -### - -GET {{triliumHost}}/etapi/notes/root/export?format=wrong -Authorization: {{authToken}} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code === "UNRECOGNIZED_EXPORT_FORMAT"); -%} diff --git a/_regroup/test-etapi/get-date-notes.http b/_regroup/test-etapi/get-date-notes.http deleted file mode 100644 index 19f0b4fc9..000000000 --- a/_regroup/test-etapi/get-date-notes.http +++ /dev/null @@ -1,72 +0,0 @@ -GET {{triliumHost}}/etapi/inbox/2022-01-01 -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/calendar/days/2022-01-01 -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/calendar/days/2022-1 -Authorization: {{authToken}} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code === "DATE_INVALID"); -%} - -### - -GET {{triliumHost}}/etapi/calendar/weeks/2022-01-01 -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/calendar/weeks/2022-1 -Authorization: {{authToken}} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code === "DATE_INVALID"); -%} - -### - -GET {{triliumHost}}/etapi/calendar/months/2022-01 -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/calendar/months/2022-1 -Authorization: {{authToken}} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code === "MONTH_INVALID"); -%} - -### - -GET {{triliumHost}}/etapi/calendar/years/2022 -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} - -### - -GET {{triliumHost}}/etapi/calendar/years/202 -Authorization: {{authToken}} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code === "YEAR_INVALID"); -%} diff --git a/_regroup/test-etapi/get-inherited-attribute-cloned.http b/_regroup/test-etapi/get-inherited-attribute-cloned.http deleted file mode 100644 index eaf8d91b1..000000000 --- a/_regroup/test-etapi/get-inherited-attribute-cloned.http +++ /dev/null @@ -1,116 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello parent", - "type": "text", - "content": "Hi there!" -} - -> {% -client.assert(response.status === 201); -client.global.set("parentNoteId", response.body.note.noteId); -client.global.set("parentBranchId", response.body.branch.branchId); -%} - -### Create inheritable parent attribute - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{parentNoteId}}", - "type": "label", - "name": "mylabel", - "value": "", - "isInheritable": true, - "position": 10 -} - -> {% -client.assert(response.status === 201); -client.global.set("parentAttributeId", response.body.attributeId); -%} - -### Create child note under root - -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello child", - "type": "text", - "content": "Hi there!" -} - -> {% -client.assert(response.status === 201); -client.global.set("childNoteId", response.body.note.noteId); -client.global.set("childBranchId", response.body.branch.branchId); -%} - -### Create child attribute - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{childNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": false, - "position": 10 -} - -> {% -client.assert(response.status === 201); -client.global.set("childAttributeId", response.body.attributeId); -%} - -### Clone child to parent - -POST {{triliumHost}}/etapi/branches -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{childNoteId}}", - "parentNoteId": "{{parentNoteId}}" -} - -> {% -client.assert(response.status === 201); -client.assert(response.body.parentNoteId == client.global.get("parentNoteId")); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{childNoteId}} -Authorization: {{authToken}} - -> {% - -function hasAttribute(list, attributeId) { - for (let i = 0; i < list.length; i++) { - if (list[i]["attributeId"] === attributeId) { - return true; - } - } - return false; -} - -client.log(JSON.stringify(response.body.attributes)); - -client.assert(response.status === 200); -client.assert(response.body.noteId == client.global.get("childNoteId")); -client.assert(response.body.attributes.length == 2); -client.assert(hasAttribute(response.body.attributes, client.global.get("parentAttributeId"))); -client.assert(hasAttribute(response.body.attributes, client.global.get("childAttributeId"))); -%} diff --git a/_regroup/test-etapi/get-inherited-attribute.http b/_regroup/test-etapi/get-inherited-attribute.http deleted file mode 100644 index 26e9af854..000000000 --- a/_regroup/test-etapi/get-inherited-attribute.http +++ /dev/null @@ -1,61 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "GetInheritedAttributes Test Note", - "type": "text", - "content": "Hi there!" -} - -> {% - client.assert(response.status === 201); - client.global.set("parentNoteId", response.body.note.noteId); -%} - -### - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{parentNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% client.global.set("createdAttributeId", response.body.attributeId); %} - -### - -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "{{parentNoteId}}", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% -client.global.set("createdNoteId", response.body.note.noteId); -client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.noteId == client.global.get("createdNoteId")); -client.assert(response.body.attributes.length == 1); -client.assert(response.body.attributes[0].attributeId == client.global.get("createdAttributeId")); -%} diff --git a/_regroup/test-etapi/get-note-content.http b/_regroup/test-etapi/get-note-content.http deleted file mode 100644 index 50c677dd8..000000000 --- a/_regroup/test-etapi/get-note-content.http +++ /dev/null @@ -1,25 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% - client.global.set("createdNoteId", response.body.note.noteId); - client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content -Authorization: {{authToken}} - -> {% - client.assert(response.status === 200); - client.assert(response.body === "Hi there!"); -%} diff --git a/_regroup/test-etapi/http-client.env.json b/_regroup/test-etapi/http-client.env.json deleted file mode 100644 index 8ede0719c..000000000 --- a/_regroup/test-etapi/http-client.env.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dev": { - "triliumHost": "http://localhost:37740" - } -} diff --git a/_regroup/test-etapi/import-zip.http b/_regroup/test-etapi/import-zip.http deleted file mode 100644 index e831a050a..000000000 --- a/_regroup/test-etapi/import-zip.http +++ /dev/null @@ -1,12 +0,0 @@ -POST {{triliumHost}}/etapi/notes/root/import -Authorization: {{authToken}} -Content-Type: application/octet-stream -Content-Transfer-Encoding: binary - -< ../db/demo.zip - -> {% - client.assert(response.status === 201); - client.assert(response.body.note.title == "Trilium Demo"); - client.assert(response.body.branch.parentNoteId == "root"); -%} diff --git a/_regroup/test-etapi/logout.http b/_regroup/test-etapi/logout.http deleted file mode 100644 index 9bd7355e0..000000000 --- a/_regroup/test-etapi/logout.http +++ /dev/null @@ -1,34 +0,0 @@ -POST {{triliumHost}}/etapi/auth/login -Content-Type: application/json - -{ - "password": "1234" -} - -> {% - client.assert(response.status === 201); - - client.global.set("testAuthToken", response.body.authToken); -%} - -### - -GET {{triliumHost}}/etapi/notes/root -Authorization: {{testAuthToken}} - -> {% client.assert(response.status === 200); %} - -### - -POST {{triliumHost}}/etapi/auth/logout -Authorization: {{testAuthToken}} -Content-Type: application/json - -> {% client.assert(response.status === 204); %} - -### - -GET {{triliumHost}}/etapi/notes/root -Authorization: {{testAuthToken}} - -> {% client.assert(response.status === 401); %} diff --git a/_regroup/test-etapi/metrics.http b/_regroup/test-etapi/metrics.http deleted file mode 100644 index 24435f954..000000000 --- a/_regroup/test-etapi/metrics.http +++ /dev/null @@ -1,82 +0,0 @@ -### Test ETAPI metrics endpoint - -# First login to get a token -POST {{triliumHost}}/etapi/auth/login -Content-Type: application/json - -{ - "password": "{{password}}" -} - -> {% -client.test("Login successful", function() { - client.assert(response.status === 201, "Response status is not 201"); - client.assert(response.body.authToken, "Auth token not present"); - client.global.set("authToken", response.body.authToken); -}); -%} - -### Get metrics with authentication (default Prometheus format) -GET {{triliumHost}}/etapi/metrics -Authorization: {{authToken}} - -> {% -client.test("Metrics endpoint returns Prometheus format by default", function() { - client.assert(response.status === 200, "Response status is not 200"); - client.assert(response.headers["content-type"].includes("text/plain"), "Content-Type should be text/plain"); - client.assert(response.body.includes("trilium_info"), "Should contain trilium_info metric"); - client.assert(response.body.includes("trilium_notes_total"), "Should contain trilium_notes_total metric"); - client.assert(response.body.includes("# HELP"), "Should contain HELP comments"); - client.assert(response.body.includes("# TYPE"), "Should contain TYPE comments"); -}); -%} - -### Get metrics in JSON format -GET {{triliumHost}}/etapi/metrics?format=json -Authorization: {{authToken}} - -> {% -client.test("Metrics endpoint returns JSON when requested", function() { - client.assert(response.status === 200, "Response status is not 200"); - client.assert(response.headers["content-type"].includes("application/json"), "Content-Type should be application/json"); - client.assert(response.body.version, "Version info not present"); - client.assert(response.body.database, "Database info not present"); - client.assert(response.body.timestamp, "Timestamp not present"); - client.assert(typeof response.body.database.totalNotes === 'number', "Total notes should be a number"); - client.assert(typeof response.body.database.activeNotes === 'number', "Active notes should be a number"); -}); -%} - -### Get metrics in Prometheus format explicitly -GET {{triliumHost}}/etapi/metrics?format=prometheus -Authorization: {{authToken}} - -> {% -client.test("Metrics endpoint returns Prometheus format when requested", function() { - client.assert(response.status === 200, "Response status is not 200"); - client.assert(response.headers["content-type"].includes("text/plain"), "Content-Type should be text/plain"); - client.assert(response.body.includes("trilium_info"), "Should contain trilium_info metric"); - client.assert(response.body.includes("trilium_notes_total"), "Should contain trilium_notes_total metric"); -}); -%} - -### Test invalid format parameter -GET {{triliumHost}}/etapi/metrics?format=xml -Authorization: {{authToken}} - -> {% -client.test("Invalid format parameter returns error", function() { - client.assert(response.status === 400, "Response status should be 400"); - client.assert(response.body.code === "INVALID_FORMAT", "Error code should be INVALID_FORMAT"); - client.assert(response.body.message.includes("prometheus"), "Error message should mention supported formats"); -}); -%} - -### Test without authentication (should fail) -GET {{triliumHost}}/etapi/metrics - -> {% -client.test("Metrics endpoint requires authentication", function() { - client.assert(response.status === 401, "Response status should be 401"); -}); -%} \ No newline at end of file diff --git a/_regroup/test-etapi/no-token.http b/_regroup/test-etapi/no-token.http deleted file mode 100644 index d8198ed2b..000000000 --- a/_regroup/test-etapi/no-token.http +++ /dev/null @@ -1,109 +0,0 @@ -GET {{triliumHost}}/etapi/notes?search=aaa - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/notes/root - -> {% client.assert(response.status === 401); %} - -### - -PATCH {{triliumHost}}/etapi/notes/root -Authorization: fakeauth - -> {% client.assert(response.status === 401); %} - -### - -DELETE {{triliumHost}}/etapi/notes/root -Authorization: fakeauth - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/branches/root -Authorization: fakeauth - -> {% client.assert(response.status === 401); %} - -### - -PATCH {{triliumHost}}/etapi/branches/root - -> {% client.assert(response.status === 401); %} - -### - -DELETE {{triliumHost}}/etapi/branches/root - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/attributes/000 - -> {% client.assert(response.status === 401); %} - -### - -PATCH {{triliumHost}}/etapi/attributes/000 - -> {% client.assert(response.status === 401); %} - -### - -DELETE {{triliumHost}}/etapi/attributes/000 - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/inbox/2022-02-22 - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/calendar/days/2022-02-22 -Authorization: fakeauth - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/calendar/weeks/2022-02-22 - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/calendar/months/2022-02 - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/calendar/years/2022 - -> {% client.assert(response.status === 401); %} - -### - -POST {{triliumHost}}/etapi/create-note - -> {% client.assert(response.status === 401); %} - -### - -GET {{triliumHost}}/etapi/app-info - -> {% client.assert(response.status === 401); %} - -### Fake URL will get a 404 even without token - -GET {{triliumHost}}/etapi/zzzzzz - -> {% client.assert(response.status === 404); %} diff --git a/_regroup/test-etapi/other.http b/_regroup/test-etapi/other.http deleted file mode 100644 index c3f92fc94..000000000 --- a/_regroup/test-etapi/other.http +++ /dev/null @@ -1,4 +0,0 @@ -POST {{triliumHost}}/etapi/refresh-note-ordering/root -Authorization: {{authToken}} - -> {% client.assert(response.status === 200); %} \ No newline at end of file diff --git a/_regroup/test-etapi/patch-attachment.http b/_regroup/test-etapi/patch-attachment.http deleted file mode 100644 index 44ffe696f..000000000 --- a/_regroup/test-etapi/patch-attachment.http +++ /dev/null @@ -1,79 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -POST {{triliumHost}}/etapi/attachments -Authorization: {{authToken}} -Content-Type: application/json - -{ - "ownerId": "{{createdNoteId}}", - "role": "file", - "mime": "text/plain", - "title": "my attachment", - "content": "text" -} - -> {% client.global.set("createdAttachmentId", response.body.attachmentId); %} - -### - -PATCH {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "title": "CHANGED", - "position": 999 -} - -### - -GET {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} - -> {% - client.assert(response.body.title === "CHANGED"); - client.assert(response.body.position === 999); -%} - -### - -PATCH {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "ownerId": "root" -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_NOT_ALLOWED"); -%} - -### - -PATCH {{triliumHost}}/etapi/attachments/{{createdAttachmentId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "title": null -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR"); -%} diff --git a/_regroup/test-etapi/patch-attribute.http b/_regroup/test-etapi/patch-attribute.http deleted file mode 100644 index 625c19446..000000000 --- a/_regroup/test-etapi/patch-attribute.http +++ /dev/null @@ -1,80 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% - client.global.set("createdNoteId", response.body.note.noteId); - client.global.set("createdBranchId", response.body.branch.branchId); -%} - -### - -POST {{triliumHost}}/etapi/attributes -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "{{createdNoteId}}", - "type": "label", - "name": "mylabel", - "value": "val", - "isInheritable": true -} - -> {% client.global.set("createdAttributeId", response.body.attributeId); %} - -### - -PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "value": "CHANGED" -} - -### - -GET {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} - -> {% -client.assert(response.body.value === "CHANGED"); -%} - -### - -PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "noteId": "root" -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_NOT_ALLOWED"); -%} - -### - -PATCH {{triliumHost}}/etapi/attributes/{{createdAttributeId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "value": null -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR"); -%} \ No newline at end of file diff --git a/_regroup/test-etapi/patch-branch.http b/_regroup/test-etapi/patch-branch.http deleted file mode 100644 index 48116120c..000000000 --- a/_regroup/test-etapi/patch-branch.http +++ /dev/null @@ -1,66 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "type": "text", - "title": "Hello", - "content": "" -} - -> {% client.global.set("createdBranchId", response.body.branch.branchId); %} - -### - -PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "prefix": "pref", - "notePosition": 666, - "isExpanded": true -} - -### - -GET {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.prefix === 'pref'); -client.assert(response.body.notePosition === 666); -client.assert(response.body.isExpanded === true); -%} - -### - -PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root" -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_NOT_ALLOWED"); -%} - -### - -PATCH {{triliumHost}}/etapi/branches/{{createdBranchId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "prefix": 123 -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR"); -%} \ No newline at end of file diff --git a/_regroup/test-etapi/patch-note.http b/_regroup/test-etapi/patch-note.http deleted file mode 100644 index 24b9251d2..000000000 --- a/_regroup/test-etapi/patch-note.http +++ /dev/null @@ -1,83 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "code", - "mime": "application/json", - "content": "{}" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.title === 'Hello'); -client.assert(response.body.type === 'code'); -client.assert(response.body.mime === 'application/json'); -%} - -### - -PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "title": "Wassup", - "type": "html", - "mime": "text/html", - "dateCreated": "2023-08-21 23:38:51.123+0200", - "utcDateCreated": "2023-08-21 23:38:51.123Z" -} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.title === 'Wassup'); -client.assert(response.body.type === 'html'); -client.assert(response.body.mime === 'text/html'); -client.assert(response.body.dateCreated == "2023-08-21 23:38:51.123+0200"); -client.assert(response.body.utcDateCreated == "2023-08-21 23:38:51.123Z"); -%} - -### - -PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "isProtected": true -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_NOT_ALLOWED"); -%} - -### - -PATCH {{triliumHost}}/etapi/notes/{{createdNoteId}} -Authorization: {{authToken}} -Content-Type: application/json - -{ - "title": true -} - -> {% - client.assert(response.status === 400); - client.assert(response.body.code == "PROPERTY_VALIDATION_ERROR"); -%} diff --git a/_regroup/test-etapi/post-revision.http b/_regroup/test-etapi/post-revision.http deleted file mode 100644 index 139397855..000000000 --- a/_regroup/test-etapi/post-revision.http +++ /dev/null @@ -1,23 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "code", - "mime": "text/plain", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -POST {{triliumHost}}/etapi/notes/{{createdNoteId}}/revision -Authorization: {{authToken}} -Content-Type: text/plain - -Changed content - -> {% client.assert(response.status === 204); %} diff --git a/_regroup/test-etapi/put-attachment-content-binary.http b/_regroup/test-etapi/put-attachment-content-binary.http deleted file mode 100644 index 6e6d6dad3..000000000 --- a/_regroup/test-etapi/put-attachment-content-binary.http +++ /dev/null @@ -1,39 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -POST {{triliumHost}}/etapi/attachments -Authorization: {{authToken}} -Content-Type: application/json - -{ - "ownerId": "{{createdNoteId}}", - "role": "file", - "mime": "text/plain", - "title": "my attachment", - "content": "text" -} - -> {% client.global.set("createdAttachmentId", response.body.attachmentId); %} - -### - -PUT {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}/content -Authorization: {{authToken}} -Content-Type: application/octet-stream -Content-Transfer-Encoding: binary - -< ../images/icon-color.png - -> {% client.assert(response.status === 204); %} diff --git a/_regroup/test-etapi/put-attachment-content.http b/_regroup/test-etapi/put-attachment-content.http deleted file mode 100644 index 57e96a4b9..000000000 --- a/_regroup/test-etapi/put-attachment-content.http +++ /dev/null @@ -1,45 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "text", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -POST {{triliumHost}}/etapi/attachments -Authorization: {{authToken}} -Content-Type: application/json - -{ - "ownerId": "{{createdNoteId}}", - "role": "file", - "mime": "text/plain", - "title": "my attachment", - "content": "text" -} - -> {% client.global.set("createdAttachmentId", response.body.attachmentId); %} - -### - -PUT {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}/content -Authorization: {{authToken}} -Content-Type: text/plain - -Changed content - -> {% client.assert(response.status === 204); %} - -### - -GET {{triliumHost}}/etapi/attachments/{{createdAttachmentId}}/content -Authorization: {{authToken}} - -> {% client.assert(response.body === "Changed content"); %} diff --git a/_regroup/test-etapi/put-note-content-binary.http b/_regroup/test-etapi/put-note-content-binary.http deleted file mode 100644 index 545b3c111..000000000 --- a/_regroup/test-etapi/put-note-content-binary.http +++ /dev/null @@ -1,25 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "image", - "mime": "image/png", - "content": "" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -PUT {{triliumHost}}/etapi/notes/{{createdNoteId}}/content -Authorization: {{authToken}} -Content-Type: application/octet-stream -Content-Transfer-Encoding: binary - -< ../images/icon-color.png - -> {% client.assert(response.status === 204); %} - diff --git a/_regroup/test-etapi/put-note-content.http b/_regroup/test-etapi/put-note-content.http deleted file mode 100644 index 670195ac2..000000000 --- a/_regroup/test-etapi/put-note-content.http +++ /dev/null @@ -1,30 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "Hello", - "type": "code", - "mime": "text/plain", - "content": "Hi there!" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -PUT {{triliumHost}}/etapi/notes/{{createdNoteId}}/content -Authorization: {{authToken}} -Content-Type: text/plain - -Changed content - -> {% client.assert(response.status === 204); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content -Authorization: {{authToken}} - -> {% client.assert(response.body === "Changed content"); %} diff --git a/_regroup/test-etapi/search.http b/_regroup/test-etapi/search.http deleted file mode 100644 index 4655f22e0..000000000 --- a/_regroup/test-etapi/search.http +++ /dev/null @@ -1,39 +0,0 @@ -POST {{triliumHost}}/etapi/create-note -Authorization: {{authToken}} -Content-Type: application/json - -{ - "parentNoteId": "root", - "title": "title", - "type": "text", - "content": "{{$uuid}}" -} - -> {% client.global.set("createdNoteId", response.body.note.noteId); %} - -### - -GET {{triliumHost}}/etapi/notes/{{createdNoteId}}/content -Authorization: {{authToken}} - -> {% client.global.set("content", response.body); %} - -### - -GET {{triliumHost}}/etapi/notes?search={{content}}&debug=true -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.results.length === 1); -%} - -### Same but with fast search which doesn't look in the content so 0 notes should be found - -GET {{triliumHost}}/etapi/notes?search={{content}}&fastSearch=true -Authorization: {{authToken}} - -> {% -client.assert(response.status === 200); -client.assert(response.body.results.length === 0); -%} diff --git a/apps/client/package.json b/apps/client/package.json index a1e4c9c6a..94f1a0ed3 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -10,7 +10,7 @@ "url": "https://github.com/TriliumNext/Notes" }, "dependencies": { - "@eslint/js": "9.27.0", + "@eslint/js": "9.28.0", "@excalidraw/excalidraw": "0.18.0", "@fullcalendar/core": "6.1.17", "@fullcalendar/daygrid": "6.1.17", @@ -64,9 +64,9 @@ "@types/leaflet-gpx": "1.3.7", "@types/mark.js": "8.11.12", "@types/react": "19.1.6", - "@types/react-dom": "19.1.5", + "@types/react-dom": "19.1.6", "copy-webpack-plugin": "13.0.0", - "happy-dom": "17.5.6", + "happy-dom": "17.6.3", "script-loader": "0.7.2", "vite-plugin-static-copy": "3.0.0" }, diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index 074e03e4c..11d32cf0d 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -269,14 +269,32 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> return true; } - const blob = await this.note.getBlob(); - if (!blob) { - return false; + // Store the initial decision about read-only status in the viewScope + // This will be "remembered" until the viewScope is refreshed + if (!this.viewScope) { + this.resetViewScope(); } - const sizeLimit = this.note.type === "text" ? options.getInt("autoReadonlySizeText") : options.getInt("autoReadonlySizeCode"); + const viewScope = this.viewScope!; - return sizeLimit && blob.contentLength > sizeLimit && !this.note.isLabelTruthy("autoReadOnlyDisabled"); + if (viewScope.isReadOnly === undefined) { + const blob = await this.note.getBlob(); + if (!blob) { + viewScope.isReadOnly = false; + return false; + } + + const sizeLimit = this.note.type === "text" + ? options.getInt("autoReadonlySizeText") + : options.getInt("autoReadonlySizeCode"); + + viewScope.isReadOnly = Boolean(sizeLimit && + blob.contentLength > sizeLimit && + !this.note.isLabelTruthy("autoReadOnlyDisabled")); + } + + // Return the cached decision, which won't change until viewScope is reset + return viewScope.isReadOnly || false; } async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { diff --git a/apps/client/src/menus/context_menu.ts b/apps/client/src/menus/context_menu.ts index 7d0bc0a2f..a8a37f462 100644 --- a/apps/client/src/menus/context_menu.ts +++ b/apps/client/src/menus/context_menu.ts @@ -192,13 +192,16 @@ class ContextMenu { // it's important to stop the propagation especially for sub-menus, otherwise the event // might be handled again by top-level menu return false; - }) - .on("mouseup", (e) =>{ + }); + + if (!this.isMobile) { + $item.on("mouseup", (e) =>{ e.stopPropagation(); // Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below. this.hide(); return false; }); + } if ("enabled" in item && item.enabled !== undefined && !item.enabled) { $item.addClass("disabled"); diff --git a/apps/client/src/server_types.ts b/apps/client/src/server_types.ts index df6e1a7fc..2aa521405 100644 --- a/apps/client/src/server_types.ts +++ b/apps/client/src/server_types.ts @@ -8,7 +8,7 @@ interface Entity { export interface EntityChange { id?: number | null; noteId?: string; - entityName: EntityRowNames; + entityName: EntityType; entityId: string; entity?: Entity; positions?: Record; @@ -22,3 +22,5 @@ export interface EntityChange { changeId?: string | null; instanceId?: string | null; } + +export type EntityType = "notes" | "branches" | "attributes" | "note_reordering" | "revisions" | "options" | "attachments" | "blobs" | "etapi_tokens" | "note_embeddings"; diff --git a/apps/client/src/services/froca_updater.ts b/apps/client/src/services/froca_updater.ts index 1f8eaa541..412d8d6cd 100644 --- a/apps/client/src/services/froca_updater.ts +++ b/apps/client/src/services/froca_updater.ts @@ -35,8 +35,8 @@ async function processEntityChanges(entityChanges: EntityChange[]) { loadResults.addOption(attributeEntity.name); } else if (ec.entityName === "attachments") { processAttachment(loadResults, ec); - } else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") { - // NOOP + } else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens" || ec.entityName === "note_embeddings") { + // NOOP - these entities are handled at the backend level and don't require frontend processing } else { throw new Error(`Unknown entityName '${ec.entityName}'`); } diff --git a/apps/client/src/services/keyboard_actions.ts b/apps/client/src/services/keyboard_actions.ts index dfa888620..3cb0ffd33 100644 --- a/apps/client/src/services/keyboard_actions.ts +++ b/apps/client/src/services/keyboard_actions.ts @@ -115,6 +115,7 @@ function updateDisplayedShortcuts($container: JQuery) { export default { updateDisplayedShortcuts, setupActionsForElement, + getAction, getActions, getActionsForScope }; diff --git a/apps/client/src/services/link.spec.ts b/apps/client/src/services/link.spec.ts index 60812ccf9..09eaf09e7 100644 --- a/apps/client/src/services/link.spec.ts +++ b/apps/client/src/services/link.spec.ts @@ -16,4 +16,24 @@ describe("Link", () => { const output = parseNavigationStateFromUrl(`#root/WWaBNf3SSA1b/mQ2tIzLVFKHL`); expect(output).toMatchObject({ notePath: "root/WWaBNf3SSA1b/mQ2tIzLVFKHL", noteId: "mQ2tIzLVFKHL" }); }); + + it("parses notePath with spaces", () => { + const output = parseNavigationStateFromUrl(` #root/WWaBNf3SSA1b/mQ2tIzLVFKHL`); + expect(output).toMatchObject({ notePath: "root/WWaBNf3SSA1b/mQ2tIzLVFKHL", noteId: "mQ2tIzLVFKHL" }); + }); + + it("ignores external URL with internal hash anchor", () => { + const output = parseNavigationStateFromUrl(`https://en.wikipedia.org/wiki/Bearded_Collie#Health`); + expect(output).toMatchObject({}); + }); + + it("ignores malformed but hash-containing external URL", () => { + const output = parseNavigationStateFromUrl("https://abc.com/#drop?searchString=firefox"); + expect(output).toStrictEqual({}); + }); + + it("ignores non-hash internal path", () => { + const output = parseNavigationStateFromUrl("/root/abc123"); + expect(output).toStrictEqual({}); + }); }); diff --git a/apps/client/src/services/link.ts b/apps/client/src/services/link.ts index 0425652f6..980d5766d 100644 --- a/apps/client/src/services/link.ts +++ b/apps/client/src/services/link.ts @@ -48,6 +48,13 @@ export interface ViewScope { viewMode?: ViewMode; attachmentId?: string; readOnlyTemporarilyDisabled?: boolean; + /** + * If true, it indicates that the note in the view should be opened in read-only mode (for supported note types such as text or code). + * + * The reason why we store this information here is that a note can become read-only as the user types content in it, and we wouldn't want + * to immediately enter read-only mode. + */ + isReadOnly?: boolean; highlightsListPreviousVisible?: boolean; highlightsListTemporarilyHidden?: boolean; tocTemporarilyHidden?: boolean; @@ -204,11 +211,17 @@ export function parseNavigationStateFromUrl(url: string | undefined) { return {}; } + url = url.trim(); const hashIdx = url.indexOf("#"); if (hashIdx === -1) { return {}; } + // Exclude external links that contain # + if (hashIdx !== 0 && !url.includes("/#root") && !url.includes("/#?searchString")) { + return {}; + } + const hash = url.substr(hashIdx + 1); // strip also the initial '#' let [notePath, paramString] = hash.split("?"); diff --git a/apps/client/src/services/load_results.ts b/apps/client/src/services/load_results.ts index 11f9a1a11..59d201f2b 100644 --- a/apps/client/src/services/load_results.ts +++ b/apps/client/src/services/load_results.ts @@ -44,9 +44,17 @@ interface OptionRow {} interface NoteReorderingRow {} -interface ContentNoteIdToComponentIdRow { +interface NoteEmbeddingRow { + embedId: string; noteId: string; - componentId: string; + providerId: string; + modelId: string; + dimension: number; + version: number; + dateCreated: string; + utcDateCreated: string; + dateModified: string; + utcDateModified: string; } type EntityRowMappings = { @@ -56,6 +64,7 @@ type EntityRowMappings = { options: OptionRow; revisions: RevisionRow; note_reordering: NoteReorderingRow; + note_embeddings: NoteEmbeddingRow; }; export type EntityRowNames = keyof EntityRowMappings; diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index ab6e45847..590c596b2 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -124,8 +124,12 @@ function formatDateISO(date: Date) { return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`; } -function formatDateTime(date: Date) { - return `${formatDate(date)} ${formatTime(date)}`; +function formatDateTime(date: Date, userSuppliedFormat?: string): string { + if (userSuppliedFormat?.trim()) { + return dayjs(date).format(userSuppliedFormat); + } else { + return `${formatDate(date)} ${formatTime(date)}`; + } } function localNowDateTime() { diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index b1b9ee921..be4cdec6e 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -24,7 +24,7 @@ --bs-body-font-family: var(--main-font-family) !important; --bs-body-font-weight: var(--main-font-weight) !important; --bs-body-color: var(--main-text-color) !important; - --bs-body-bg: var(--main-background-color) !important; + --bs-body-bg: var(--main-background-color) !important; } .table { @@ -326,6 +326,7 @@ button kbd { user-select: none; -webkit-user-select: none; --bs-dropdown-zindex: 999; + --bs-dropdown-link-active-bg: var(--active-item-background-color) !important; } body.desktop .dropdown-menu { diff --git a/apps/client/src/stylesheets/theme-dark.css b/apps/client/src/stylesheets/theme-dark.css index 3b7a8fcc2..a6aab3118 100644 --- a/apps/client/src/stylesheets/theme-dark.css +++ b/apps/client/src/stylesheets/theme-dark.css @@ -70,6 +70,7 @@ --scrollbar-border-color: #666; --scrollbar-background-color: #333; + --selection-background-color: #3399FF70; --tooltip-background-color: #333; --link-color: lightskyblue; diff --git a/apps/client/src/stylesheets/theme-light.css b/apps/client/src/stylesheets/theme-light.css index 70e81885f..b485e8a27 100644 --- a/apps/client/src/stylesheets/theme-light.css +++ b/apps/client/src/stylesheets/theme-light.css @@ -74,6 +74,7 @@ html { --scrollbar-border-color: #ddd; --scrollbar-background-color: #ddd; + --selection-background-color: #3399FF70; --tooltip-background-color: #f8f8f8; --link-color: blue; diff --git a/apps/client/src/stylesheets/theme-next/ribbon.css b/apps/client/src/stylesheets/theme-next/ribbon.css index c7765865d..e21984a76 100644 --- a/apps/client/src/stylesheets/theme-next/ribbon.css +++ b/apps/client/src/stylesheets/theme-next/ribbon.css @@ -108,6 +108,25 @@ div.editability-dropdown a.dropdown-item { font-size: 0.85em; } +/* + * Edited notes (for calendar notes) + */ + +/* The path of the note */ +.edited-notes-list small { + margin-inline-start: 4px; + font-size: inherit; + color: var(--muted-text-color); +} + +.edited-notes-list small::before { + content: "("; +} + +.edited-notes-list small::after { + content: ")"; +} + /* * Owned attributes */ diff --git a/apps/client/src/stylesheets/theme-next/shell.css b/apps/client/src/stylesheets/theme-next/shell.css index a77715136..5b11839e5 100644 --- a/apps/client/src/stylesheets/theme-next/shell.css +++ b/apps/client/src/stylesheets/theme-next/shell.css @@ -1402,6 +1402,7 @@ div.floating-buttons .show-floating-buttons-button:active { div.floating-buttons-children .close-floating-buttons-button::before, div.floating-buttons .show-floating-buttons-button::before { display: block; + line-height: 1; } /* "Show buttons" button */ diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 4d39265d1..13ee699d7 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1431,6 +1431,12 @@ "label": "Automatic read-only size (text notes)", "unit": "characters" }, + "custom_date_time_format": { + "title": "Custom Date/Time Format", + "description": "Customize the format of the date and time inserted via or the toolbar. See Day.js docs for available format tokens.", + "format_string": "Format string:", + "formatted_time": "Formatted date/time:" + }, "i18n": { "title": "Localization", "language": "Language", diff --git a/apps/client/src/widgets/llm_chat/communication.ts b/apps/client/src/widgets/llm_chat/communication.ts index bb58a47b2..614add7ad 100644 --- a/apps/client/src/widgets/llm_chat/communication.ts +++ b/apps/client/src/widgets/llm_chat/communication.ts @@ -6,8 +6,10 @@ import type { SessionResponse } from "./types.js"; /** * Create a new chat session + * @param currentNoteId - Optional current note ID for context + * @returns The noteId of the created chat note */ -export async function createChatSession(currentNoteId?: string): Promise<{chatNoteId: string | null, noteId: string | null}> { +export async function createChatSession(currentNoteId?: string): Promise { try { const resp = await server.post('llm/chat', { title: 'Note Chat', @@ -15,48 +17,42 @@ export async function createChatSession(currentNoteId?: string): Promise<{chatNo }); if (resp && resp.id) { - // The backend might provide the noteId separately from the chatNoteId - // If noteId is provided, use it; otherwise, we'll need to query for it separately - return { - chatNoteId: resp.id, - noteId: resp.noteId || null - }; + // Backend returns the chat note ID as 'id' + return resp.id; } } catch (error) { console.error('Failed to create chat session:', error); } - return { - chatNoteId: null, - noteId: null - }; + return null; } /** - * Check if a session exists + * Check if a chat note exists + * @param noteId - The ID of the chat note */ -export async function checkSessionExists(chatNoteId: string): Promise { +export async function checkSessionExists(noteId: string): Promise { try { - // Validate that we have a proper note ID format, not a session ID - // Note IDs in Trilium are typically longer or in a different format - if (chatNoteId && chatNoteId.length === 16 && /^[A-Za-z0-9]+$/.test(chatNoteId)) { - console.warn(`Invalid note ID format detected: ${chatNoteId} appears to be a legacy session ID`); - return false; - } - - const sessionCheck = await server.getWithSilentNotFound(`llm/chat/${chatNoteId}`); + const sessionCheck = await server.getWithSilentNotFound(`llm/chat/${noteId}`); return !!(sessionCheck && sessionCheck.id); } catch (error: any) { - console.log(`Error checking chat note ${chatNoteId}:`, error); + console.log(`Error checking chat note ${noteId}:`, error); return false; } } /** * Set up streaming response via WebSocket + * @param noteId - The ID of the chat note + * @param messageParams - Message parameters + * @param onContentUpdate - Callback for content updates + * @param onThinkingUpdate - Callback for thinking updates + * @param onToolExecution - Callback for tool execution + * @param onComplete - Callback for completion + * @param onError - Callback for errors */ export async function setupStreamingResponse( - chatNoteId: string, + noteId: string, messageParams: any, onContentUpdate: (content: string, isDone?: boolean) => void, onThinkingUpdate: (thinking: string) => void, @@ -64,35 +60,24 @@ export async function setupStreamingResponse( onComplete: () => void, onError: (error: Error) => void ): Promise { - // Validate that we have a proper note ID format, not a session ID - if (chatNoteId && chatNoteId.length === 16 && /^[A-Za-z0-9]+$/.test(chatNoteId)) { - console.error(`Invalid note ID format: ${chatNoteId} appears to be a legacy session ID`); - onError(new Error("Invalid note ID format - using a legacy session ID")); - return; - } - return new Promise((resolve, reject) => { let assistantResponse = ''; - let postToolResponse = ''; // Separate accumulator for post-tool execution content let receivedAnyContent = false; - let receivedPostToolContent = false; // Track if we've started receiving post-tool content let timeoutId: number | null = null; let initialTimeoutId: number | null = null; let cleanupTimeoutId: number | null = null; let receivedAnyMessage = false; - let toolsExecuted = false; // Flag to track if tools were executed in this session - let toolExecutionCompleted = false; // Flag to track if tool execution is completed let eventListener: ((event: Event) => void) | null = null; let lastMessageTimestamp = 0; // Create a unique identifier for this response process const responseId = `llm-stream-${Date.now()}-${Math.floor(Math.random() * 1000)}`; - console.log(`[${responseId}] Setting up WebSocket streaming for chat note ${chatNoteId}`); + console.log(`[${responseId}] Setting up WebSocket streaming for chat note ${noteId}`); // Send the initial request to initiate streaming (async () => { try { - const streamResponse = await server.post(`llm/chat/${chatNoteId}/messages/stream`, { + const streamResponse = await server.post(`llm/chat/${noteId}/messages/stream`, { content: messageParams.content, useAdvancedContext: messageParams.useAdvancedContext, showThinking: messageParams.showThinking, @@ -129,28 +114,14 @@ export async function setupStreamingResponse( resolve(); }; - // Function to schedule cleanup with ability to cancel - const scheduleCleanup = (delay: number) => { - // Clear any existing cleanup timeout - if (cleanupTimeoutId) { - window.clearTimeout(cleanupTimeoutId); + // Set initial timeout to catch cases where no message is received at all + initialTimeoutId = window.setTimeout(() => { + if (!receivedAnyMessage) { + console.error(`[${responseId}] No initial message received within timeout`); + performCleanup(); + reject(new Error('No response received from server')); } - - console.log(`[${responseId}] Scheduling listener cleanup in ${delay}ms`); - - // Set new cleanup timeout - cleanupTimeoutId = window.setTimeout(() => { - // Only clean up if no messages received recently (in last 2 seconds) - const timeSinceLastMessage = Date.now() - lastMessageTimestamp; - if (timeSinceLastMessage > 2000) { - performCleanup(); - } else { - console.log(`[${responseId}] Received message recently, delaying cleanup`); - // Reschedule cleanup - scheduleCleanup(2000); - } - }, delay); - }; + }, 10000); // Create a message handler for CustomEvents eventListener = (event: Event) => { @@ -158,7 +129,7 @@ export async function setupStreamingResponse( const message = customEvent.detail; // Only process messages for our chat note - if (!message || message.chatNoteId !== chatNoteId) { + if (!message || message.chatNoteId !== noteId) { return; } @@ -172,12 +143,12 @@ export async function setupStreamingResponse( cleanupTimeoutId = null; } - console.log(`[${responseId}] LLM Stream message received via CustomEvent: chatNoteId=${chatNoteId}, content=${!!message.content}, contentLength=${message.content?.length || 0}, thinking=${!!message.thinking}, toolExecution=${!!message.toolExecution}, done=${!!message.done}, type=${message.type || 'llm-stream'}`); + console.log(`[${responseId}] LLM Stream message received: content=${!!message.content}, contentLength=${message.content?.length || 0}, thinking=${!!message.thinking}, toolExecution=${!!message.toolExecution}, done=${!!message.done}`); // Mark first message received if (!receivedAnyMessage) { receivedAnyMessage = true; - console.log(`[${responseId}] First message received for chat note ${chatNoteId}`); + console.log(`[${responseId}] First message received for chat note ${noteId}`); // Clear the initial timeout since we've received a message if (initialTimeoutId !== null) { @@ -186,109 +157,33 @@ export async function setupStreamingResponse( } } - // Handle specific message types - if (message.type === 'tool_execution_start') { - toolsExecuted = true; // Mark that tools were executed - onThinkingUpdate('Executing tools...'); - // Also trigger tool execution UI with a specific format - onToolExecution({ - action: 'start', - tool: 'tools', - result: 'Executing tools...' - }); - return; // Skip accumulating content from this message + // Handle error + if (message.error) { + console.error(`[${responseId}] Stream error: ${message.error}`); + performCleanup(); + reject(new Error(message.error)); + return; } - if (message.type === 'tool_result' && message.toolExecution) { - toolsExecuted = true; // Mark that tools were executed - console.log(`[${responseId}] Processing tool result: ${JSON.stringify(message.toolExecution)}`); + // Handle thinking updates - only show if showThinking is enabled + if (message.thinking && messageParams.showThinking) { + console.log(`[${responseId}] Received thinking: ${message.thinking.substring(0, 100)}...`); + onThinkingUpdate(message.thinking); + } - // If tool execution doesn't have an action, add 'result' as the default - if (!message.toolExecution.action) { - message.toolExecution.action = 'result'; - } - - // First send a 'start' action to ensure the container is created - onToolExecution({ - action: 'start', - tool: 'tools', - result: 'Tool execution initialized' - }); - - // Then send the actual tool execution data + // Handle tool execution updates + if (message.toolExecution) { + console.log(`[${responseId}] Tool execution update:`, message.toolExecution); onToolExecution(message.toolExecution); - - // Mark tool execution as completed if this is a result or error - if (message.toolExecution.action === 'result' || message.toolExecution.action === 'complete' || message.toolExecution.action === 'error') { - toolExecutionCompleted = true; - console.log(`[${responseId}] Tool execution completed`); - } - - return; // Skip accumulating content from this message - } - - if (message.type === 'tool_execution_error' && message.toolExecution) { - toolsExecuted = true; // Mark that tools were executed - toolExecutionCompleted = true; // Mark tool execution as completed - onToolExecution({ - ...message.toolExecution, - action: 'error', - error: message.toolExecution.error || 'Unknown error during tool execution' - }); - return; // Skip accumulating content from this message - } - - if (message.type === 'tool_completion_processing') { - toolsExecuted = true; // Mark that tools were executed - toolExecutionCompleted = true; // Tools are done, now processing the result - onThinkingUpdate('Generating response with tool results...'); - // Also trigger tool execution UI with a specific format - onToolExecution({ - action: 'generating', - tool: 'tools', - result: 'Generating response with tool results...' - }); - return; // Skip accumulating content from this message } // Handle content updates if (message.content) { - console.log(`[${responseId}] Received content chunk of length ${message.content.length}, preview: "${message.content.substring(0, 50)}${message.content.length > 50 ? '...' : ''}"`); - - // If tools were executed and completed, and we're now getting new content, - // this is likely the final response after tool execution from Anthropic - if (toolsExecuted && toolExecutionCompleted && message.content) { - console.log(`[${responseId}] Post-tool execution content detected`); - - // If this is the first post-tool chunk, indicate we're starting a new response - if (!receivedPostToolContent) { - receivedPostToolContent = true; - postToolResponse = ''; // Clear any previous post-tool response - console.log(`[${responseId}] First post-tool content chunk, starting fresh accumulation`); - } - - // Accumulate post-tool execution content - postToolResponse += message.content; - console.log(`[${responseId}] Accumulated post-tool content, now ${postToolResponse.length} chars`); - - // Update the UI with the accumulated post-tool content - // This replaces the pre-tool content with our accumulated post-tool content - onContentUpdate(postToolResponse, message.done || false); - } else { - // Standard content handling for non-tool cases or initial tool response - - // Check if this is a duplicated message containing the same content we already have - if (message.done && assistantResponse.includes(message.content)) { - console.log(`[${responseId}] Ignoring duplicated content in done message`); - } else { - // Add to our accumulated response - assistantResponse += message.content; - } - - // Update the UI immediately with each chunk - onContentUpdate(assistantResponse, message.done || false); - } + // Simply append the new content - no complex deduplication + assistantResponse += message.content; + // Update the UI immediately with each chunk + onContentUpdate(assistantResponse, message.done || false); receivedAnyContent = true; // Reset timeout since we got content @@ -298,151 +193,33 @@ export async function setupStreamingResponse( // Set new timeout timeoutId = window.setTimeout(() => { - console.warn(`[${responseId}] Stream timeout for chat note ${chatNoteId}`); - - // Clean up + console.warn(`[${responseId}] Stream timeout for chat note ${noteId}`); performCleanup(); reject(new Error('Stream timeout')); }, 30000); } - // Handle tool execution updates (legacy format and standard format with llm-stream type) - if (message.toolExecution) { - // Only process if we haven't already handled this message via specific message types - if (message.type === 'llm-stream' || !message.type) { - console.log(`[${responseId}] Received tool execution update: action=${message.toolExecution.action || 'unknown'}`); - toolsExecuted = true; // Mark that tools were executed - - // Mark tool execution as completed if this is a result or error - if (message.toolExecution.action === 'result' || - message.toolExecution.action === 'complete' || - message.toolExecution.action === 'error') { - toolExecutionCompleted = true; - console.log(`[${responseId}] Tool execution completed via toolExecution message`); - } - - onToolExecution(message.toolExecution); - } - } - - // Handle tool calls from the raw data or direct in message (OpenAI format) - const toolCalls = message.tool_calls || (message.raw && message.raw.tool_calls); - if (toolCalls && Array.isArray(toolCalls)) { - console.log(`[${responseId}] Received tool calls: ${toolCalls.length} tools`); - toolsExecuted = true; // Mark that tools were executed - - // First send a 'start' action to ensure the container is created - onToolExecution({ - action: 'start', - tool: 'tools', - result: 'Tool execution initialized' - }); - - // Then process each tool call - for (const toolCall of toolCalls) { - let args = toolCall.function?.arguments || {}; - - // Try to parse arguments if they're a string - if (typeof args === 'string') { - try { - args = JSON.parse(args); - } catch (e) { - console.log(`[${responseId}] Could not parse tool arguments as JSON: ${e}`); - args = { raw: args }; - } - } - - onToolExecution({ - action: 'executing', - tool: toolCall.function?.name || 'unknown', - toolCallId: toolCall.id, - args: args - }); - } - } - - // Handle thinking state updates - if (message.thinking) { - console.log(`[${responseId}] Received thinking update: ${message.thinking.substring(0, 50)}...`); - onThinkingUpdate(message.thinking); - } - // Handle completion if (message.done) { - console.log(`[${responseId}] Stream completed for chat note ${chatNoteId}, has content: ${!!message.content}, content length: ${message.content?.length || 0}, current response: ${assistantResponse.length} chars`); + console.log(`[${responseId}] Stream completed for chat note ${noteId}, final response: ${assistantResponse.length} chars`); - // Dump message content to console for debugging - if (message.content) { - console.log(`[${responseId}] CONTENT IN DONE MESSAGE (first 200 chars): "${message.content.substring(0, 200)}..."`); - - // Check if the done message contains the exact same content as our accumulated response - // We normalize by removing whitespace to avoid false negatives due to spacing differences - const normalizedMessage = message.content.trim(); - const normalizedResponse = assistantResponse.trim(); - - if (normalizedMessage === normalizedResponse) { - console.log(`[${responseId}] Final message is identical to accumulated response, no need to update`); - } - // If the done message is longer but contains our accumulated response, use the done message - else if (normalizedMessage.includes(normalizedResponse) && normalizedMessage.length > normalizedResponse.length) { - console.log(`[${responseId}] Final message is more complete than accumulated response, using it`); - assistantResponse = message.content; - } - // If the done message is different and not already included, append it to avoid duplication - else if (!normalizedResponse.includes(normalizedMessage) && normalizedMessage.length > 0) { - console.log(`[${responseId}] Final message has unique content, using it`); - assistantResponse = message.content; - } - // Otherwise, we already have the content accumulated, so no need to update - else { - console.log(`[${responseId}] Already have this content accumulated, not updating`); - } - } - - // Clear timeout if set + // Clear all timeouts if (timeoutId !== null) { window.clearTimeout(timeoutId); timeoutId = null; } - // Always mark as done when we receive the done flag - onContentUpdate(assistantResponse, true); - - // Set a longer delay before cleanup to allow for post-tool execution messages - // Especially important for Anthropic which may send final message after tool execution - const cleanupDelay = toolsExecuted ? 15000 : 1000; // 15 seconds if tools were used, otherwise 1 second - console.log(`[${responseId}] Setting cleanup delay of ${cleanupDelay}ms since toolsExecuted=${toolsExecuted}`); - scheduleCleanup(cleanupDelay); + // Schedule cleanup after a brief delay to ensure all processing is complete + cleanupTimeoutId = window.setTimeout(() => { + performCleanup(); + }, 100); } }; - // Register event listener for the custom event - try { - window.addEventListener('llm-stream-message', eventListener); - console.log(`[${responseId}] Event listener added for llm-stream-message events`); - } catch (err) { - console.error(`[${responseId}] Error setting up event listener:`, err); - reject(err); - return; - } + // Register the event listener for WebSocket messages + window.addEventListener('llm-stream-message', eventListener); - // Set initial timeout for receiving any message - initialTimeoutId = window.setTimeout(() => { - console.warn(`[${responseId}] No messages received for initial period in chat note ${chatNoteId}`); - if (!receivedAnyMessage) { - console.error(`[${responseId}] WebSocket connection not established for chat note ${chatNoteId}`); - - if (timeoutId !== null) { - window.clearTimeout(timeoutId); - } - - // Clean up - cleanupEventListener(eventListener); - - // Show error message to user - reject(new Error('WebSocket connection not established')); - } - }, 10000); + console.log(`[${responseId}] Event listener registered, waiting for messages...`); }); } @@ -463,15 +240,9 @@ function cleanupEventListener(listener: ((event: Event) => void) | null): void { /** * Get a direct response from the server without streaming */ -export async function getDirectResponse(chatNoteId: string, messageParams: any): Promise { +export async function getDirectResponse(noteId: string, messageParams: any): Promise { try { - // Validate that we have a proper note ID format, not a session ID - if (chatNoteId && chatNoteId.length === 16 && /^[A-Za-z0-9]+$/.test(chatNoteId)) { - console.error(`Invalid note ID format: ${chatNoteId} appears to be a legacy session ID`); - throw new Error("Invalid note ID format - using a legacy session ID"); - } - - const postResponse = await server.post(`llm/chat/${chatNoteId}/messages`, { + const postResponse = await server.post(`llm/chat/${noteId}/messages`, { message: messageParams.content, includeContext: messageParams.useAdvancedContext, options: { diff --git a/apps/client/src/widgets/llm_chat/llm_chat_panel.ts b/apps/client/src/widgets/llm_chat/llm_chat_panel.ts index 32ffab50d..4565c0db2 100644 --- a/apps/client/src/widgets/llm_chat/llm_chat_panel.ts +++ b/apps/client/src/widgets/llm_chat/llm_chat_panel.ts @@ -37,9 +37,10 @@ export default class LlmChatPanel extends BasicWidget { private thinkingBubble!: HTMLElement; private thinkingText!: HTMLElement; private thinkingToggle!: HTMLElement; - private chatNoteId: string | null = null; - private noteId: string | null = null; // The actual noteId for the Chat Note - private currentNoteId: string | null = null; + + // Simplified to just use noteId - this represents the AI Chat note we're working with + private noteId: string | null = null; + private currentNoteId: string | null = null; // The note providing context (for regular notes) private _messageHandlerId: number | null = null; private _messageHandler: any = null; @@ -68,7 +69,6 @@ export default class LlmChatPanel extends BasicWidget { totalTokens?: number; }; } = { - model: 'default', temperature: 0.7, toolExecutions: [] }; @@ -90,12 +90,21 @@ export default class LlmChatPanel extends BasicWidget { this.messages = messages; } - public getChatNoteId(): string | null { - return this.chatNoteId; + public getNoteId(): string | null { + return this.noteId; } - public setChatNoteId(chatNoteId: string | null): void { - this.chatNoteId = chatNoteId; + public setNoteId(noteId: string | null): void { + this.noteId = noteId; + } + + // Deprecated - keeping for backward compatibility but mapping to noteId + public getChatNoteId(): string | null { + return this.noteId; + } + + public setChatNoteId(noteId: string | null): void { + this.noteId = noteId; } public getNoteContextChatMessages(): HTMLElement { @@ -307,16 +316,22 @@ export default class LlmChatPanel extends BasicWidget { } } - const dataToSave: ChatData = { + // Only save if we have a valid note ID + if (!this.noteId) { + console.warn('Cannot save chat data: no noteId available'); + return; + } + + const dataToSave = { messages: this.messages, - chatNoteId: this.chatNoteId, noteId: this.noteId, + chatNoteId: this.noteId, // For backward compatibility toolSteps: toolSteps, // Add sources if we have them sources: this.sources || [], // Add metadata metadata: { - model: this.metadata?.model || 'default', + model: this.metadata?.model || undefined, provider: this.metadata?.provider || undefined, temperature: this.metadata?.temperature || 0.7, lastUpdated: new Date().toISOString(), @@ -325,7 +340,7 @@ export default class LlmChatPanel extends BasicWidget { } }; - console.log(`Saving chat data with chatNoteId: ${this.chatNoteId}, noteId: ${this.noteId}, ${toolSteps.length} tool steps, ${this.sources?.length || 0} sources, ${toolExecutions.length} tool executions`); + console.log(`Saving chat data with noteId: ${this.noteId}, ${toolSteps.length} tool steps, ${this.sources?.length || 0} sources, ${toolExecutions.length} tool executions`); // Save the data to the note attribute via the callback // This is the ONLY place we should save data, letting the container widget handle persistence @@ -347,16 +362,52 @@ export default class LlmChatPanel extends BasicWidget { const savedData = await this.onGetData() as ChatData; if (savedData?.messages?.length > 0) { + // Check if we actually have new content to avoid unnecessary UI rebuilds + const currentMessageCount = this.messages.length; + const savedMessageCount = savedData.messages.length; + + // If message counts are the same, check if content is different + const hasNewContent = savedMessageCount > currentMessageCount || + JSON.stringify(this.messages) !== JSON.stringify(savedData.messages); + + if (!hasNewContent) { + console.log("No new content detected, skipping UI rebuild"); + return true; + } + + console.log(`Loading saved data: ${currentMessageCount} -> ${savedMessageCount} messages`); + + // Store current scroll position if we need to preserve it + const shouldPreserveScroll = savedMessageCount > currentMessageCount && currentMessageCount > 0; + const currentScrollTop = shouldPreserveScroll ? this.chatContainer.scrollTop : 0; + const currentScrollHeight = shouldPreserveScroll ? this.chatContainer.scrollHeight : 0; + // Load messages + const oldMessages = [...this.messages]; this.messages = savedData.messages; - // Clear and rebuild the chat UI - this.noteContextChatMessages.innerHTML = ''; + // Only rebuild UI if we have significantly different content + if (savedMessageCount > currentMessageCount) { + // We have new messages - just add the new ones instead of rebuilding everything + const newMessages = savedData.messages.slice(currentMessageCount); + console.log(`Adding ${newMessages.length} new messages to UI`); - this.messages.forEach(message => { - const role = message.role as 'user' | 'assistant'; - this.addMessageToChat(role, message.content); - }); + newMessages.forEach(message => { + const role = message.role as 'user' | 'assistant'; + this.addMessageToChat(role, message.content); + }); + } else { + // Content changed but count is same - need to rebuild + console.log("Message content changed, rebuilding UI"); + + // Clear and rebuild the chat UI + this.noteContextChatMessages.innerHTML = ''; + + this.messages.forEach(message => { + const role = message.role as 'user' | 'assistant'; + this.addMessageToChat(role, message.content); + }); + } // Restore tool execution steps if they exist if (savedData.toolSteps && Array.isArray(savedData.toolSteps) && savedData.toolSteps.length > 0) { @@ -400,13 +451,33 @@ export default class LlmChatPanel extends BasicWidget { // Load Chat Note ID if available if (savedData.noteId) { console.log(`Using noteId as Chat Note ID: ${savedData.noteId}`); - this.chatNoteId = savedData.noteId; this.noteId = savedData.noteId; } else { console.log(`No noteId found in saved data, cannot load chat session`); return false; } + // Restore scroll position if we were preserving it + if (shouldPreserveScroll) { + // Calculate the new scroll position to maintain relative position + const newScrollHeight = this.chatContainer.scrollHeight; + const scrollDifference = newScrollHeight - currentScrollHeight; + const newScrollTop = currentScrollTop + scrollDifference; + + // Only scroll down if we're near the bottom, otherwise preserve exact position + const wasNearBottom = (currentScrollTop + this.chatContainer.clientHeight) >= (currentScrollHeight - 50); + + if (wasNearBottom) { + // User was at bottom, scroll to new bottom + this.chatContainer.scrollTop = newScrollHeight; + console.log("User was at bottom, scrolling to new bottom"); + } else { + // User was not at bottom, try to preserve their position + this.chatContainer.scrollTop = newScrollTop; + console.log(`Preserving scroll position: ${currentScrollTop} -> ${newScrollTop}`); + } + } + return true; } } catch (error) { @@ -550,6 +621,15 @@ export default class LlmChatPanel extends BasicWidget { // Get current note context if needed const currentActiveNoteId = appContext.tabManager.getActiveContext()?.note?.noteId || null; + // For AI Chat notes, the note itself IS the chat session + // So currentNoteId and noteId should be the same + if (this.noteId && currentActiveNoteId === this.noteId) { + // We're in an AI Chat note - don't reset, just load saved data + console.log(`Refreshing AI Chat note ${this.noteId} - loading saved data`); + await this.loadSavedData(); + return; + } + // If we're switching to a different note, we need to reset if (this.currentNoteId !== currentActiveNoteId) { console.log(`Note ID changed from ${this.currentNoteId} to ${currentActiveNoteId}, resetting chat panel`); @@ -557,7 +637,6 @@ export default class LlmChatPanel extends BasicWidget { // Reset the UI and data this.noteContextChatMessages.innerHTML = ''; this.messages = []; - this.chatNoteId = null; this.noteId = null; // Also reset the chat note ID this.hideSources(); // Hide any sources from previous note @@ -569,7 +648,7 @@ export default class LlmChatPanel extends BasicWidget { const hasSavedData = await this.loadSavedData(); // Only create a new session if we don't have a session or saved data - if (!this.chatNoteId || !this.noteId || !hasSavedData) { + if (!this.noteId || !hasSavedData) { // Create a new chat session await this.createChatSession(); } @@ -580,19 +659,15 @@ export default class LlmChatPanel extends BasicWidget { */ private async createChatSession() { try { - // Create a new chat session, passing the current note ID if it exists - const { chatNoteId, noteId } = await createChatSession( - this.currentNoteId ? this.currentNoteId : undefined - ); + // If we already have a noteId (for AI Chat notes), use it + const contextNoteId = this.noteId || this.currentNoteId; - if (chatNoteId) { - // If we got back an ID from the API, use it - this.chatNoteId = chatNoteId; - - // For new sessions, the noteId should equal the chatNoteId - // This ensures we're using the note ID consistently - this.noteId = noteId || chatNoteId; + // Create a new chat session, passing the context note ID + const noteId = await createChatSession(contextNoteId ? contextNoteId : undefined); + if (noteId) { + // Set the note ID for this chat + this.noteId = noteId; console.log(`Created new chat session with noteId: ${this.noteId}`); } else { throw new Error("Failed to create chat session - no ID returned"); @@ -645,7 +720,7 @@ export default class LlmChatPanel extends BasicWidget { const showThinking = this.showThinkingCheckbox.checked; // Add logging to verify parameters - console.log(`Sending message with: useAdvancedContext=${useAdvancedContext}, showThinking=${showThinking}, noteId=${this.currentNoteId}, sessionId=${this.chatNoteId}`); + console.log(`Sending message with: useAdvancedContext=${useAdvancedContext}, showThinking=${showThinking}, noteId=${this.currentNoteId}, sessionId=${this.noteId}`); // Create the message parameters const messageParams = { @@ -695,11 +770,11 @@ export default class LlmChatPanel extends BasicWidget { await validateEmbeddingProviders(this.validationWarning); // Make sure we have a valid session - if (!this.chatNoteId) { + if (!this.noteId) { // If no session ID, create a new session await this.createChatSession(); - if (!this.chatNoteId) { + if (!this.noteId) { // If still no session ID, show error and return console.error("Failed to create chat session"); toastService.showError("Failed to create chat session"); @@ -730,7 +805,7 @@ export default class LlmChatPanel extends BasicWidget { await this.saveCurrentData(); // Add logging to verify parameters - console.log(`Sending message with: useAdvancedContext=${useAdvancedContext}, showThinking=${showThinking}, noteId=${this.currentNoteId}, sessionId=${this.chatNoteId}`); + console.log(`Sending message with: useAdvancedContext=${useAdvancedContext}, showThinking=${showThinking}, noteId=${this.currentNoteId}, sessionId=${this.noteId}`); // Create the message parameters const messageParams = { @@ -767,12 +842,12 @@ export default class LlmChatPanel extends BasicWidget { */ private async handleDirectResponse(messageParams: any): Promise { try { - if (!this.chatNoteId) return false; + if (!this.noteId) return false; - console.log(`Getting direct response using sessionId: ${this.chatNoteId} (noteId: ${this.noteId})`); + console.log(`Getting direct response using sessionId: ${this.noteId} (noteId: ${this.noteId})`); // Get a direct response from the server - const postResponse = await getDirectResponse(this.chatNoteId, messageParams); + const postResponse = await getDirectResponse(this.noteId, messageParams); // If the POST request returned content directly, display it if (postResponse && postResponse.content) { @@ -845,11 +920,11 @@ export default class LlmChatPanel extends BasicWidget { * Set up streaming response via WebSocket */ private async setupStreamingResponse(messageParams: any): Promise { - if (!this.chatNoteId) { + if (!this.noteId) { throw new Error("No session ID available"); } - console.log(`Setting up streaming response using sessionId: ${this.chatNoteId} (noteId: ${this.noteId})`); + console.log(`Setting up streaming response using sessionId: ${this.noteId} (noteId: ${this.noteId})`); // Store tool executions captured during streaming const toolExecutionsCache: Array<{ @@ -862,7 +937,7 @@ export default class LlmChatPanel extends BasicWidget { }> = []; return setupStreamingResponse( - this.chatNoteId, + this.noteId, messageParams, // Content update handler (content: string, isDone: boolean = false) => { @@ -898,7 +973,7 @@ export default class LlmChatPanel extends BasicWidget { similarity?: number; content?: string; }>; - }>(`llm/chat/${this.chatNoteId}`) + }>(`llm/chat/${this.noteId}`) .then((sessionData) => { console.log("Got updated session data:", sessionData); @@ -933,9 +1008,9 @@ export default class LlmChatPanel extends BasicWidget { } } - // Save the updated data to the note - this.saveCurrentData() - .catch(err => console.error("Failed to save data after streaming completed:", err)); + // DON'T save here - let the server handle saving the complete conversation + // to avoid race conditions between client and server saves + console.log("Updated metadata after streaming completion, server should save"); }) .catch(err => console.error("Error fetching session data after streaming:", err)); } @@ -973,11 +1048,9 @@ export default class LlmChatPanel extends BasicWidget { console.log(`Cached tool execution for ${toolData.tool} to be saved later`); - // Save immediately after receiving a tool execution - // This ensures we don't lose tool execution data if streaming fails - this.saveCurrentData().catch(err => { - console.error("Failed to save tool execution data:", err); - }); + // DON'T save immediately during streaming - let the server handle saving + // to avoid race conditions between client and server saves + console.log(`Tool execution cached, will be saved by server`); } }, // Complete handler @@ -995,23 +1068,19 @@ export default class LlmChatPanel extends BasicWidget { * Update the UI with streaming content */ private updateStreamingUI(assistantResponse: string, isDone: boolean = false) { - // Parse and handle thinking content if present - if (!isDone) { - const thinkingContent = this.parseThinkingContent(assistantResponse); - if (thinkingContent) { - this.updateThinkingText(thinkingContent); - // Don't display the raw response with think tags in the chat - return; - } - } - - // Get the existing assistant message or create a new one - let assistantMessageEl = this.noteContextChatMessages.querySelector('.assistant-message:last-child'); - - if (!assistantMessageEl) { - // If no assistant message yet, create one + // Track if we have a streaming message in progress + const hasStreamingMessage = !!this.noteContextChatMessages.querySelector('.assistant-message.streaming'); + + // Create a new message element or use the existing streaming one + let assistantMessageEl: HTMLElement; + + if (hasStreamingMessage) { + // Use the existing streaming message + assistantMessageEl = this.noteContextChatMessages.querySelector('.assistant-message.streaming')!; + } else { + // Create a new message element assistantMessageEl = document.createElement('div'); - assistantMessageEl.className = 'assistant-message message mb-3'; + assistantMessageEl.className = 'assistant-message message mb-3 streaming'; this.noteContextChatMessages.appendChild(assistantMessageEl); // Add assistant profile icon @@ -1026,60 +1095,37 @@ export default class LlmChatPanel extends BasicWidget { assistantMessageEl.appendChild(messageContent); } - // Clean the response to remove thinking tags before displaying - const cleanedResponse = this.removeThinkingTags(assistantResponse); - - // Update the content + // Update the content with the current response const messageContent = assistantMessageEl.querySelector('.message-content') as HTMLElement; - messageContent.innerHTML = formatMarkdown(cleanedResponse); + messageContent.innerHTML = formatMarkdown(assistantResponse); - // Apply syntax highlighting if this is the final update + // When the response is complete if (isDone) { + // Remove the streaming class to mark this message as complete + assistantMessageEl.classList.remove('streaming'); + + // Apply syntax highlighting formatCodeBlocks($(assistantMessageEl as HTMLElement)); // Hide the thinking display when response is complete this.hideThinkingDisplay(); - // Update message in the data model for storage - // Find the last assistant message to update, or add a new one if none exists - const assistantMessages = this.messages.filter(msg => msg.role === 'assistant'); - const lastAssistantMsgIndex = assistantMessages.length > 0 ? - this.messages.lastIndexOf(assistantMessages[assistantMessages.length - 1]) : -1; - - if (lastAssistantMsgIndex >= 0) { - // Update existing message with cleaned content - this.messages[lastAssistantMsgIndex].content = cleanedResponse; - } else { - // Add new message with cleaned content - this.messages.push({ - role: 'assistant', - content: cleanedResponse - }); - } - - // Hide loading indicator - hideLoadingIndicator(this.loadingIndicator); - - // Save the final state to the Chat Note - this.saveCurrentData().catch(err => { - console.error("Failed to save assistant response to note:", err); + // Always add a new message to the data model + // This ensures we preserve all distinct assistant messages + this.messages.push({ + role: 'assistant', + content: assistantResponse, + timestamp: new Date() }); + + // Save the updated message list + this.saveCurrentData(); } // Scroll to bottom this.chatContainer.scrollTop = this.chatContainer.scrollHeight; } - /** - * Remove thinking tags from response content - */ - private removeThinkingTags(content: string): string { - if (!content) return content; - - // Remove ... blocks from the content - return content.replace(/[\s\S]*?<\/think>/gi, '').trim(); - } - /** * Handle general errors in the send message flow */ diff --git a/apps/client/src/widgets/llm_chat/types.ts b/apps/client/src/widgets/llm_chat/types.ts index 300a7856a..7181651d0 100644 --- a/apps/client/src/widgets/llm_chat/types.ts +++ b/apps/client/src/widgets/llm_chat/types.ts @@ -11,7 +11,7 @@ export interface ChatResponse { export interface SessionResponse { id: string; title: string; - noteId?: string; + noteId: string; // The ID of the chat note } export interface ToolExecutionStep { @@ -33,8 +33,8 @@ export interface MessageData { export interface ChatData { messages: MessageData[]; - chatNoteId: string | null; - noteId?: string | null; + noteId: string; // The ID of the chat note + chatNoteId?: string; // Deprecated - kept for backward compatibility, should equal noteId toolSteps: ToolExecutionStep[]; sources?: Array<{ noteId: string; diff --git a/apps/client/src/widgets/ribbon_widgets/edited_notes.ts b/apps/client/src/widgets/ribbon_widgets/edited_notes.ts index 4c8d79abe..2967b5f6f 100644 --- a/apps/client/src/widgets/ribbon_widgets/edited_notes.ts +++ b/apps/client/src/widgets/ribbon_widgets/edited_notes.ts @@ -19,7 +19,7 @@ const TPL = /*html*/`
${t("edited_notes.no_edited_notes_found")}
-
+ `; diff --git a/apps/client/src/widgets/type_widgets/ai_chat.ts b/apps/client/src/widgets/type_widgets/ai_chat.ts index e96cf5f20..f733b499b 100644 --- a/apps/client/src/widgets/type_widgets/ai_chat.ts +++ b/apps/client/src/widgets/type_widgets/ai_chat.ts @@ -94,6 +94,11 @@ export default class AiChatTypeWidget extends TypeWidget { this.llmChatPanel.clearNoteContextChatMessages(); this.llmChatPanel.setMessages([]); + // Set the note ID for the chat panel + if (note) { + this.llmChatPanel.setNoteId(note.noteId); + } + // This will load saved data via the getData callback await this.llmChatPanel.refresh(); this.isInitialized = true; @@ -130,7 +135,7 @@ export default class AiChatTypeWidget extends TypeWidget { // Reset the chat panel UI this.llmChatPanel.clearNoteContextChatMessages(); this.llmChatPanel.setMessages([]); - this.llmChatPanel.setChatNoteId(null); + this.llmChatPanel.setNoteId(this.note.noteId); } // Call the parent method to refresh @@ -152,6 +157,7 @@ export default class AiChatTypeWidget extends TypeWidget { // Make sure the chat panel has the current note ID if (this.note) { this.llmChatPanel.setCurrentNoteId(this.note.noteId); + this.llmChatPanel.setNoteId(this.note.noteId); } this.initPromise = (async () => { @@ -186,7 +192,7 @@ export default class AiChatTypeWidget extends TypeWidget { // Format the data properly - this is the canonical format of the data const formattedData = { messages: data.messages || [], - chatNoteId: data.chatNoteId || this.note.noteId, + noteId: this.note.noteId, // Always use the note's own ID toolSteps: data.toolSteps || [], sources: data.sources || [], metadata: { diff --git a/apps/client/src/widgets/type_widgets/ckeditor/config.ts b/apps/client/src/widgets/type_widgets/ckeditor/config.ts index 5f92c444d..10836c718 100644 --- a/apps/client/src/widgets/type_widgets/ckeditor/config.ts +++ b/apps/client/src/widgets/type_widgets/ckeditor/config.ts @@ -189,7 +189,7 @@ export function buildClassicToolbar(multilineToolbar: boolean) { { label: "Insert", icon: "plus", - items: ["imageUpload", "|", "link", "bookmark", "internallink", "includeNote", "|", "specialCharacters", "emoji", "math", "mermaid", "horizontalLine", "pageBreak"] + items: ["imageUpload", "|", "link", "bookmark", "internallink", "includeNote", "|", "specialCharacters", "emoji", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"] }, "|", "outdent", @@ -244,7 +244,7 @@ export function buildFloatingToolbar() { { label: "Insert", icon: "plus", - items: ["internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak"] + items: ["bookmark", "internallink", "includeNote", "|", "math", "mermaid", "horizontalLine", "pageBreak", "dateTime"] }, "|", "outdent", diff --git a/apps/client/src/widgets/type_widgets/content_widget.ts b/apps/client/src/widgets/type_widgets/content_widget.ts index 00c30f5f7..614159cb7 100644 --- a/apps/client/src/widgets/type_widgets/content_widget.ts +++ b/apps/client/src/widgets/type_widgets/content_widget.ts @@ -8,6 +8,7 @@ import HeadingStyleOptions from "./options/text_notes/heading_style.js"; import TableOfContentsOptions from "./options/text_notes/table_of_contents.js"; import HighlightsListOptions from "./options/text_notes/highlights_list.js"; import TextAutoReadOnlySizeOptions from "./options/text_notes/text_auto_read_only_size.js"; +import DateTimeFormatOptions from "./options/text_notes/date_time_format.js"; import CodeEditorOptions from "./options/code_notes/code_editor.js"; import CodeAutoReadOnlySizeOptions from "./options/code_notes/code_auto_read_only_size.js"; import CodeMimeTypesOptions from "./options/code_notes/code_mime_types.js"; @@ -88,7 +89,8 @@ const CONTENT_WIDGETS: Record { - if (!("isOpen" in item) || !item.isOpen ) { + if (!("isOpen" in item) || !item.isOpen) { return; } @@ -375,9 +375,10 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { } } - insertDateTimeToTextCommand() { + insertDateTimeToTextCommand() { const date = new Date(); - const dateString = utils.formatDateTime(date); + const customDateTimeFormat = options.get("customDateTimeFormat"); + const dateString = utils.formatDateTime(date, customDateTimeFormat); this.addTextToEditor(dateString); } diff --git a/apps/client/src/widgets/type_widgets/geo_map.ts b/apps/client/src/widgets/type_widgets/geo_map.ts index 81e1712a0..7f2b3e52a 100644 --- a/apps/client/src/widgets/type_widgets/geo_map.ts +++ b/apps/client/src/widgets/type_widgets/geo_map.ts @@ -239,6 +239,9 @@ export default class GeoMapTypeWidget extends TypeWidget { wptIcons: { "": this.#buildIcon("bx bx-pin") } + }, + polyline_options: { + color: note.getLabelValue("color") ?? "blue" } }); track.addTo(this.geoMapWidget.map); diff --git a/apps/client/src/widgets/type_widgets/options/text_notes/date_time_format.ts b/apps/client/src/widgets/type_widgets/options/text_notes/date_time_format.ts new file mode 100644 index 000000000..5728ac8b4 --- /dev/null +++ b/apps/client/src/widgets/type_widgets/options/text_notes/date_time_format.ts @@ -0,0 +1,67 @@ +import OptionsWidget from "../options_widget.js"; +import { t } from "../../../../services/i18n.js"; +import type { OptionMap } from "@triliumnext/commons"; +import utils from "../../../../services/utils.js"; +import keyboardActionsService from "../../../../services/keyboard_actions.js"; +import linkService from "../../../.././services/link.js"; + +const TPL = /*html*/` +
+

${t("custom_date_time_format.title")}

+ +

+ ${t("custom_date_time_format.description")} +

+ +
+
+ + +
+
+ +
+
+
+
+`; + +export default class DateTimeFormatOptions extends OptionsWidget { + + private $formatInput!: JQuery; + private $formattedDate!: JQuery; + + doRender() { + this.$widget = $(TPL); + + this.$formatInput = this.$widget.find("input.custom-date-time-format"); + this.$formattedDate = this.$widget.find(".formatted-date"); + + this.$formatInput.on("input", () => { + const dateString = utils.formatDateTime(new Date(), this.$formatInput.val()); + this.$formattedDate.text(dateString); + }); + + this.$formatInput.on('blur keydown', (e) => { + if (e.type === 'blur' || (e.type === 'keydown' && e.key === 'Enter')) { + this.updateOption("customDateTimeFormat", this.$formatInput.val()); + } + }); + + return this.$widget; + } + + async optionsLoaded(options: OptionMap) { + const shortcutKey = (await keyboardActionsService.getAction("insertDateTimeToText")).effectiveShortcuts.join(", "); + const $link = await linkService.createLink("_hidden/_options/_optionsShortcuts", { + "title": shortcutKey, + "showTooltip": false + }); + this.$widget.find(".description").find("kbd").replaceWith($link); + + const customDateTimeFormat = options.customDateTimeFormat || "YYYY-MM-DD HH:mm"; + this.$formatInput.val(customDateTimeFormat); + const dateString = utils.formatDateTime(new Date(), customDateTimeFormat); + this.$formattedDate.text(dateString); + } +} diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 024d7d3a9..33948c6b6 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -17,7 +17,7 @@ "@types/electron-squirrel-startup": "1.0.2", "@triliumnext/server": "workspace:*", "copy-webpack-plugin": "13.0.0", - "electron": "36.3.2", + "electron": "36.4.0", "@electron-forge/cli": "7.8.1", "@electron-forge/maker-deb": "7.8.1", "@electron-forge/maker-dmg": "7.8.1", @@ -31,7 +31,6 @@ "config": { "forge": "./electron-forge/forge.config.cjs" }, - "packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977", "scripts": { "start-prod": "nx build desktop && cross-env TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=dist TRILIUM_PORT=37841 electron dist/main.js" }, diff --git a/apps/desktop/src/electron-main.ts b/apps/desktop/src/electron-main.ts index c8c28ac77..3decb970d 100644 --- a/apps/desktop/src/electron-main.ts +++ b/apps/desktop/src/electron-main.ts @@ -7,8 +7,11 @@ import tray from "@triliumnext/server/src/services/tray.js"; import options from "@triliumnext/server/src/services/options.js"; import electronDebug from "electron-debug"; import electronDl from "electron-dl"; +import { deferred } from "@triliumnext/server/src/services/utils.js"; async function main() { + const serverInitializedPromise = deferred(); + // Prevent Trilium starting twice on first install and on uninstall for the Windows installer. if ((require("electron-squirrel-startup")).default) { process.exit(0); @@ -37,7 +40,11 @@ async function main() { } }); - electron.app.on("ready", onReady); + electron.app.on("ready", async () => { + await serverInitializedPromise; + console.log("Starting Electron..."); + await onReady(); + }); electron.app.on("will-quit", () => { electron.globalShortcut.unregisterAll(); @@ -47,7 +54,10 @@ async function main() { process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true"; await initializeTranslations(); - await import("@triliumnext/server/src/main.js"); + const startTriliumServer = (await import("@triliumnext/server/src/www.js")).default; + await startTriliumServer(); + console.log("Server loaded"); + serverInitializedPromise.resolve(); } async function onReady() { diff --git a/apps/dump-db/package.json b/apps/dump-db/package.json index a63105b9c..3fd75f86f 100644 --- a/apps/dump-db/package.json +++ b/apps/dump-db/package.json @@ -12,7 +12,7 @@ }, "devDependencies": { "@types/better-sqlite3": "^7.6.11", - "@types/mime-types": "^2.1.4", + "@types/mime-types": "^3.0.0", "@types/yargs": "^17.0.33" }, "nx": { diff --git a/apps/edit-docs/demo/!!!meta.json b/apps/edit-docs/demo/!!!meta.json index 178f5bd51..166afb625 100644 --- a/apps/edit-docs/demo/!!!meta.json +++ b/apps/edit-docs/demo/!!!meta.json @@ -454,19 +454,19 @@ "isInheritable": false, "position": 10 }, - { - "type": "relation", - "name": "child:child:child:template", - "value": "kr6HIBBuXRwm", - "isInheritable": false, - "position": 20 - }, { "type": "label", "name": "iconClass", "value": "bx bx-calendar", "isInheritable": false, "position": 30 + }, + { + "type": "relation", + "name": "dateTemplate", + "value": "kr6HIBBuXRwm", + "isInheritable": false, + "position": 20 } ], "format": "html", diff --git a/apps/edit-docs/demo/root/Trilium Demo.html b/apps/edit-docs/demo/root/Trilium Demo.html index a99649f5e..59ef348b7 100644 --- a/apps/edit-docs/demo/root/Trilium Demo.html +++ b/apps/edit-docs/demo/root/Trilium Demo.html @@ -18,22 +18,28 @@ height="150">

Welcome to TriliumNext Notes! +

This is initial "demo" document provided by TriliumNext by default to showcase some of its features and also give you some ideas how you might structure your notes. You can play with it, modify note content and tree structure as you wish.

If you need any help, visit TriliumNext website: https://github.com/TriliumNext +

Cleanup

+

Once you're finished with experimenting and want to cleanup these pages, you can simply delete them all.

Formatting

+

TriliumNext supports classic formatting like italic, bold, bold and italic. Of course you can add links like this one pointing to google.com +

Lists

Ordered: +

  1. First Item
  2. @@ -48,6 +54,7 @@

Unordered: +

  • Item
  • diff --git a/apps/edit-docs/demo/root/Trilium Demo/Books/Book template.html b/apps/edit-docs/demo/root/Trilium Demo/Books/Book template.html index 5f589ccde..b6ece231e 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Books/Book template.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Books/Book template.html @@ -14,17 +14,22 @@

    Main characters

    +

    … here put main characters …

     

    Plot

    +

    … describe main plot lines …

     

    Tone

    +

     

    Genre

    +

    scifi / drama / romance

     

    Similar books

    +
    diff --git a/apps/edit-docs/demo/root/Trilium Demo/Books/To read.html b/apps/edit-docs/demo/root/Trilium Demo/Books/To read.html index 17b164b6c..30cf1d0d1 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Books/To read.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Books/To read.html @@ -14,11 +14,14 @@

    Checkout Kindle daily deals: https://www.amazon.com/gp/feature.html?docId=1000677541 +

    diff --git a/apps/edit-docs/demo/root/Trilium Demo/Formatting examples/Checkbox lists.html b/apps/edit-docs/demo/root/Trilium Demo/Formatting examples/Checkbox lists.html index 62c728414..1a581a9d0 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Formatting examples/Checkbox lists.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Formatting examples/Checkbox lists.html @@ -18,21 +18,25 @@
diff --git a/apps/edit-docs/demo/root/Trilium Demo/Formatting examples/Code blocks.html b/apps/edit-docs/demo/root/Trilium Demo/Formatting examples/Code blocks.html index 6827fa8af..c323d02b5 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Formatting examples/Code blocks.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Formatting examples/Code blocks.html @@ -24,6 +24,7 @@ alert("Hello world"); } +

For larger pieces of code it is better to use a code note, which uses a fully-fledged code editor (CodeMirror). For an example of a code note, see Custom request handler.

diff --git a/apps/edit-docs/demo/root/Trilium Demo/Formatting examples/Math.html b/apps/edit-docs/demo/root/Trilium Demo/Formatting examples/Math.html index 54a28f3ff..e646e00b4 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Formatting examples/Math.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Formatting examples/Math.html @@ -15,7 +15,9 @@

\(% \f is defined as #1f(#2) using the macro \f\relax{x} = \int_{-\infty}^\infty     \f\hat\xi\,e^{2 \pi i \xi x}     \,d\xi\)Some math examples:

\[\displaystyle \frac{1}{\Bigl(\sqrt{\phi \sqrt{5}}-\phi\Bigr) e^{\frac25 \pi}} = 1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}} {1+\frac{e^{-8\pi}} {1+\cdots} } } }\] +

Another:

\[\displaystyle \left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)\] +

Inline math is also possible: \(c^2 = a^2 + b^2\) 

 

diff --git a/apps/edit-docs/demo/root/Trilium Demo/Inbox/The Last Question.html b/apps/edit-docs/demo/root/Trilium Demo/Inbox/The Last Question.html index e0d736c1b..3cb00a8e4 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Inbox/The Last Question.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Inbox/The Last Question.html @@ -22,6 +22,7 @@

This page demonstrates two things:

diff --git a/apps/edit-docs/demo/root/Trilium Demo/Journal.html b/apps/edit-docs/demo/root/Trilium Demo/Journal.html index faac301b5..629b4870f 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Journal.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Journal.html @@ -14,6 +14,7 @@

You can read some explanation on how this journal works here: https://github.com/zadam/trilium/wiki/Day-notes +

diff --git a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/11 - November/28 - Tuesday.html b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/11 - November/28 - Tuesday.html index 5171b6ee2..e4a99c109 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/11 - November/28 - Tuesday.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/11 - November/28 - Tuesday.html @@ -18,6 +18,7 @@
  • diff --git a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/11 - November/28 - Tuesday/Christmas gift ideas.html b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/11 - November/28 - Tuesday/Christmas gift ideas.html index 2a2d08336..01ad36c16 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/11 - November/28 - Tuesday/Christmas gift ideas.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/11 - November/28 - Tuesday/Christmas gift ideas.html @@ -17,6 +17,7 @@
  • XBox
  • Candles
  • Portable speakers +
  • ...?
  • diff --git a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/11 - November/28 - Tuesday/Trusted timestamping.html b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/11 - November/28 - Tuesday/Trusted timestamping.html index 0a85f8618..bae5a1ed0 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/11 - November/28 - Tuesday/Trusted timestamping.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/11 - November/28 - Tuesday/Trusted timestamping.html @@ -14,8 +14,10 @@

    Wiki: https://en.wikipedia.org/wiki/Trusted_timestamping +

    Bozho: https://techblog.bozho.net/using-trusted-timestamping-java/ +

    Trusted timestamping is the process of securely keeping track of the creation and modification time of a document. Security here diff --git a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/18 - Monday.html b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/18 - Monday.html index ae490681e..3722a62c0 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/18 - Monday.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/18 - Monday.html @@ -16,6 +16,7 @@

    Miscellaneous notes done on monday ...

     

    Interesting video: https://www.youtube.com/watch?v=_eSAF_qT_FY&feature=youtu.be +

     

     

    diff --git a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/19 - Tuesday.html b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/19 - Tuesday.html index f0309b26d..487b71530 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/19 - Tuesday.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/19 - Tuesday.html @@ -18,6 +18,7 @@
  • diff --git a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/20 - Wednesday.html b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/20 - Wednesday.html index 28245eb1a..ae559b3c1 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/20 - Wednesday.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/20 - Wednesday.html @@ -18,6 +18,7 @@
  • diff --git a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/21 - Thursday.html b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/21 - Thursday.html index 2ff2ac742..28b77ec56 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/21 - Thursday.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/21 - Thursday.html @@ -18,6 +18,7 @@
  • diff --git a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/22 - Friday.html b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/22 - Friday.html index f8ec2e8e5..3545fd3d7 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/22 - Friday.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/22 - Friday.html @@ -18,6 +18,7 @@
  • diff --git a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/23 - Saturday.html b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/23 - Saturday.html index 6973b8531..912b715c0 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/23 - Saturday.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/23 - Saturday.html @@ -18,6 +18,7 @@
  • diff --git a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/24 - Sunday - Christmas Eve!.html b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/24 - Sunday - Christmas Eve!.html index 29a1e0cd2..c243a035c 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/24 - Sunday - Christmas Eve!.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/24 - Sunday - Christmas Eve!.html @@ -18,6 +18,7 @@
  • diff --git a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/24 - Sunday - Christmas Eve!/DONE - Buy a board game for Al.html b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/24 - Sunday - Christmas Eve!/DONE - Buy a board game for Al.html index 55d9b2909..e7367a06c 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/24 - Sunday - Christmas Eve!/DONE - Buy a board game for Al.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/24 - Sunday - Christmas Eve!/DONE - Buy a board game for Al.html @@ -18,6 +18,7 @@ width="209" height="300">

    Maybe CodeNames? https://boardgamegeek.com/boardgame/178900/codenames +

    diff --git a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/30 - Thursday.html b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/30 - Thursday.html index 36cbd2ed6..133fae97b 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/30 - Thursday.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Journal/2021/12 - December/30 - Thursday.html @@ -18,6 +18,7 @@
  • diff --git a/apps/edit-docs/demo/root/Trilium Demo/Journal/Day template.html b/apps/edit-docs/demo/root/Trilium Demo/Journal/Day template.html index 623273a9e..44bd101ae 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Journal/Day template.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Journal/Day template.html @@ -18,6 +18,7 @@
  • diff --git a/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Chichén Itzá, Mexico.html b/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Chichén Itzá, Mexico.html index ab44bd638..3992ceed9 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Chichén Itzá, Mexico.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Chichén Itzá, Mexico.html @@ -24,14 +24,17 @@ [1] +

    1. ^ +
    2. diff --git a/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Christ the Redeemer, Brazil.html b/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Christ the Redeemer, Brazil.html index 3e1f1e905..cbaae70da 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Christ the Redeemer, Brazil.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Christ the Redeemer, Brazil.html @@ -26,13 +26,16 @@ been brought to its knees.[1] +

      1. ^ +
      2. diff --git a/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Machu Picchu, Peru.html b/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Machu Picchu, Peru.html index 5e9817e5b..929103047 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Machu Picchu, Peru.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Machu Picchu, Peru.html @@ -22,13 +22,16 @@ around 1450 in polished drystone walls.[1] +

        1. ^ +
        2. diff --git a/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Petra, Jordan.html b/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Petra, Jordan.html index 1396c2691..55e0adafa 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Petra, Jordan.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/Petra, Jordan.html @@ -23,13 +23,16 @@ by earthquakes.[1] +

          1. ^ +
          2. diff --git a/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/The Colosseum, Rome, Italy.html b/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/The Colosseum, Rome, Italy.html index 650a6623c..c9abd680d 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/The Colosseum, Rome, Italy.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/The Colosseum, Rome, Italy.html @@ -26,14 +26,17 @@ [1] +

            1. ^ +
            2. diff --git a/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/The Great Wall of China.html b/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/The Great Wall of China.html index 1f5d5886f..ccedb8388 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/The Great Wall of China.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/The Great Wall of China.html @@ -23,14 +23,17 @@ [1] +

              1. ^ +
              2. diff --git a/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/The Taj Mahal, India.html b/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/The Taj Mahal, India.html index 0d692392a..755184be0 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/The Taj Mahal, India.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Note Types/Geo Map (The Seven Wonders of the World)/The Taj Mahal, India.html @@ -23,13 +23,16 @@ the complex.[1] +

                1. ^ +
                2. diff --git a/apps/edit-docs/demo/root/Trilium Demo/Scripting examples/Task manager.html b/apps/edit-docs/demo/root/Trilium Demo/Scripting examples/Task manager.html index 1b4822074..0194f9364 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Scripting examples/Task manager.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Scripting examples/Task manager.html @@ -15,6 +15,7 @@

                  This is a simple TODO/Task manager. You can see some description and explanation here: https://github.com/zadam/trilium/wiki/Task-manager +

                  Please note that this is meant as scripting example only and feature/bug support is very limited.

                  diff --git a/apps/edit-docs/demo/root/Trilium Demo/Scripting examples/Task manager/Done/Buy a board game for Alice.html b/apps/edit-docs/demo/root/Trilium Demo/Scripting examples/Task manager/Done/Buy a board game for Alice.html index 9cda89645..65b1819f2 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Scripting examples/Task manager/Done/Buy a board game for Alice.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Scripting examples/Task manager/Done/Buy a board game for Alice.html @@ -18,6 +18,7 @@ width="209" height="300">

                  Maybe CodeNames? https://boardgamegeek.com/boardgame/178900/codenames +

                  diff --git a/apps/edit-docs/demo/root/Trilium Demo/Scripting examples/Task manager/Locations/mall/Buy some book for Bob/Maybe Black Swan.html b/apps/edit-docs/demo/root/Trilium Demo/Scripting examples/Task manager/Locations/mall/Buy some book for Bob/Maybe Black Swan.html index ba291c1ae..51831b17f 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Scripting examples/Task manager/Locations/mall/Buy some book for Bob/Maybe Black Swan.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Scripting examples/Task manager/Locations/mall/Buy some book for Bob/Maybe Black Swan.html @@ -14,6 +14,7 @@

                  https://en.wikipedia.org/wiki/The_Black_Swan:_The_Impact_of_the_Highly_Improbable +

                  The Black Swan: The Impact of the Highly Improbable is a 2007 book by author and former options trader diff --git a/apps/edit-docs/demo/root/Trilium Demo/Tech/Linux/Bash scripting.html b/apps/edit-docs/demo/root/Trilium Demo/Tech/Linux/Bash scripting.html index aaa6cd1ad..55c1364be 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Tech/Linux/Bash scripting.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Tech/Linux/Bash scripting.html @@ -25,6 +25,7 @@ and Apple's macOS (formerly OS X). A version is also available for Windows 10.

                  Bash on Wikipedia +

                  diff --git a/apps/edit-docs/demo/root/Trilium Demo/Tech/Linux/Bash scripting/Bash startup modes.html b/apps/edit-docs/demo/root/Trilium Demo/Tech/Linux/Bash scripting/Bash startup modes.html index 4dad269df..2f686d2de 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Tech/Linux/Bash scripting/Bash startup modes.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Tech/Linux/Bash scripting/Bash startup modes.html @@ -14,6 +14,7 @@

                  Login shell

                  +

                  As a "login shell", Bash reads and sets (executes) the user's profile from /etc/profile and one of ~/.bash_profile, ~/.bash_login, or ~/.profile (in that order, using the first one that's readable!).

                  @@ -23,6 +24,7 @@ that only make sense for the initial user login. That's why all UNIX® shells have (should have) a "login" mode.

                  Methods to start Bash as a login shell: +

                  • the first character of argv[0] is - (a hyphen): traditional UNIX® shells @@ -31,17 +33,20 @@
                  • Bash is started with the --login option

                  Methods to test for login shell mode: +

                  Related switches: +

                  • --noprofile disables reading of all profile files

                  Interactive shell

                  +

                  When Bash starts as an interactive non-login shell, it reads and executes commands from ~/.bashrc. This file should contain, for example, aliases, since they need to be defined in every shell as they're not inherited from @@ -51,11 +56,13 @@ The classic way to have a system-wide rc file is to source /etc/bashrc from every user's ~/.bashrc.

                  Methods to test for interactive-shell mode: +

                  • the special parameter $- contains the letter i (lowercase I)

                  Related switches: +

                  • -i forces the interactive mode
                  • @@ -65,6 +72,7 @@ ~/.bashrc)

                  SH mode

                  +

                  When Bash starts in SH compatiblity mode, it tries to mimic the startup behaviour of historical versions of sh as closely as possible, while conforming to the POSIX® standard as well. The profile files read are /etc/profile @@ -74,6 +82,7 @@ file.

                  After the startup files are read, Bash enters the POSIX(r) compatiblity mode (for running, not for starting!).

                  Bash starts in sh compatiblity mode when: +

                  • diff --git a/apps/edit-docs/demo/root/Trilium Demo/Tech/Linux/Bash scripting/While loop.html b/apps/edit-docs/demo/root/Trilium Demo/Tech/Linux/Bash scripting/While loop.html index 6ab7cf106..7b0c06f5d 100644 --- a/apps/edit-docs/demo/root/Trilium Demo/Tech/Linux/Bash scripting/While loop.html +++ b/apps/edit-docs/demo/root/Trilium Demo/Tech/Linux/Bash scripting/While loop.html @@ -14,6 +14,7 @@

                    Documentation: http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html +

                    #!/bin/bash
                     
                     # This script opens 4 terminal windows.
                    diff --git a/apps/edit-docs/demo/root/Trilium Demo/Tech/Node.js/Overview/Platform architecture.html b/apps/edit-docs/demo/root/Trilium Demo/Tech/Node.js/Overview/Platform architecture.html
                    index 1a977bccc..4b51ab5be 100644
                    --- a/apps/edit-docs/demo/root/Trilium Demo/Tech/Node.js/Overview/Platform architecture.html	
                    +++ b/apps/edit-docs/demo/root/Trilium Demo/Tech/Node.js/Overview/Platform architecture.html	
                    @@ -20,6 +20,7 @@
                                 href="https://en.wikipedia.org/wiki/Node.js#cite_note-b1-31">[31]Developers can create scalable servers without using threading,
                                   by using a simplified model of event-driven programming that
                                   uses callbacks to signal the completion of a task.[31]
                    +
                             

                  diff --git a/apps/edit-docs/package.json b/apps/edit-docs/package.json index 82f97d457..3a11c9583 100644 --- a/apps/edit-docs/package.json +++ b/apps/edit-docs/package.json @@ -12,7 +12,7 @@ "@triliumnext/desktop": "workspace:*", "@types/fs-extra": "11.0.4", "copy-webpack-plugin": "13.0.0", - "electron": "36.3.2", + "electron": "36.4.0", "fs-extra": "11.3.0" }, "nx": { diff --git a/apps/edit-docs/src/utils.ts b/apps/edit-docs/src/utils.ts index 059e1e0cc..28740f3bd 100644 --- a/apps/edit-docs/src/utils.ts +++ b/apps/edit-docs/src/utils.ts @@ -38,7 +38,8 @@ export function startElectron(callback: () => void): DeferredPromise { console.log("Electron is ready!"); // Start the server. - await import("@triliumnext/server/src/main.js"); + const startTriliumServer = (await import("@triliumnext/server/src/www.js")).default; + await startTriliumServer(); // Create the main window. await windowService.createMainWindow(electron.app); diff --git a/apps/server/.edit-integration-db.env b/apps/server/.edit-integration-db.env new file mode 100644 index 000000000..94194e2b1 --- /dev/null +++ b/apps/server/.edit-integration-db.env @@ -0,0 +1,6 @@ +TRILIUM_ENV=dev +TRILIUM_DATA_DIR=./apps/server/spec/db +TRILIUM_RESOURCE_DIR=./apps/server/dist +TRILIUM_PUBLIC_SERVER=http://localhost:4200 +TRILIUM_PORT=8086 +TRILIUM_INTEGRATION_TEST=edit \ No newline at end of file diff --git a/apps/server/package.json b/apps/server/package.json index 2b8836251..186a30d1b 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -23,7 +23,7 @@ "@types/ini": "4.1.1", "@types/js-yaml": "4.0.9", "@types/jsdom": "21.1.7", - "@types/mime-types": "2.1.4", + "@types/mime-types": "3.0.0", "@types/multer": "1.4.12", "@types/safe-compare": "1.1.2", "@types/sanitize-html": "2.16.0", @@ -39,7 +39,7 @@ "@types/ws": "8.18.1", "@types/xml2js": "0.4.14", "express-http-proxy": "2.1.1", - "@anthropic-ai/sdk": "0.52.0", + "@anthropic-ai/sdk": "0.53.0", "@braintree/sanitize-url": "7.1.1", "@triliumnext/commons": "workspace:*", "@triliumnext/express-partial-content": "workspace:*", @@ -59,7 +59,7 @@ "debounce": "2.2.0", "debug": "4.4.1", "ejs": "3.1.10", - "electron": "36.3.2", + "electron": "36.4.0", "electron-debug": "4.1.0", "electron-window-state": "5.0.3", "escape-html": "1.0.3", @@ -85,10 +85,10 @@ "jsdom": "26.1.0", "marked": "15.0.12", "mime-types": "3.0.1", - "multer": "2.0.0", + "multer": "2.0.1", "normalize-strings": "1.1.1", "ollama": "0.5.16", - "openai": "4.104.0", + "openai": "5.1.1", "rand-token": "1.0.1", "safe-compare": "1.1.4", "sanitize-filename": "1.6.3", @@ -129,6 +129,23 @@ "runBuildTargetDependencies": false } }, + "edit-integration-db": { + "executor": "@nx/js:node", + "dependsOn": [ + { + "projects": [ + "client" + ], + "target": "serve" + }, + "build-without-client" + ], + "continuous": true, + "options": { + "buildTarget": "server:build-without-client:development", + "runBuildTargetDependencies": false + } + }, "package": { "dependsOn": [ "build" diff --git a/apps/server/spec/db/document.db b/apps/server/spec/db/document.db index c02b60d34..50b477ea7 100644 Binary files a/apps/server/spec/db/document.db and b/apps/server/spec/db/document.db differ diff --git a/apps/server/spec/etapi/api-metrics.spec.ts b/apps/server/spec/etapi/api-metrics.spec.ts new file mode 100644 index 000000000..a9c98df87 --- /dev/null +++ b/apps/server/spec/etapi/api-metrics.spec.ts @@ -0,0 +1,48 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import buildApp from "../../src/app.js"; +import supertest from "supertest"; + +let app: Application; +let token: string; + +// TODO: This is an API test, not ETAPI. + +describe("api/metrics", () => { + beforeAll(async () => { + app = await buildApp(); + }); + + it("returns Prometheus format by default", async () => { + const response = await supertest(app) + .get("/api/metrics") + .expect(200); + expect(response.headers["content-type"]).toContain("text/plain"); + expect(response.text).toContain("trilium_info"); + expect(response.text).toContain("trilium_notes_total"); + expect(response.text).toContain("# HELP"); + expect(response.text).toContain("# TYPE"); + }); + + it("returns JSON when requested", async() => { + const response = await supertest(app) + .get("/api/metrics?format=json") + .expect(200); + expect(response.headers["content-type"]).toContain("application/json"); + expect(response.body.version).toBeTruthy(); + expect(response.body.database).toBeTruthy(); + expect(response.body.timestamp).toBeTruthy(); + expect(response.body.database.totalNotes).toBeTypeOf("number"); + expect(response.body.database.activeNotes).toBeTypeOf("number"); + expect(response.body.noteTypes).toBeTruthy(); + expect(response.body.attachmentTypes).toBeTruthy(); + expect(response.body.statistics).toBeTruthy(); + }); + + it("returns error on invalid format", async() => { + const response = await supertest(app) + .get("/api/metrics?format=xml") + .expect(500); + expect(response.body.message).toContain("prometheus"); + }); +}); diff --git a/apps/server/spec/etapi/app-info.spec.ts b/apps/server/spec/etapi/app-info.spec.ts new file mode 100644 index 000000000..03a5a389b --- /dev/null +++ b/apps/server/spec/etapi/app-info.spec.ts @@ -0,0 +1,20 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import buildApp from "../../src/app.js"; +import supertest from "supertest"; + +let app: Application; +let token: string; + +describe("etapi/app-info", () => { + beforeAll(async () => { + app = await buildApp(); + }); + + it("retrieves correct app info", async () => { + const response = await supertest(app) + .get("/etapi/app-info") + .expect(200); + expect(response.body.clipperProtocolVersion).toBe("1.0"); + }); +}); diff --git a/apps/server/spec/etapi/attachment-content.spec.ts b/apps/server/spec/etapi/attachment-content.spec.ts new file mode 100644 index 000000000..12c90c155 --- /dev/null +++ b/apps/server/spec/etapi/attachment-content.spec.ts @@ -0,0 +1,64 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { createNote, login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; +let createdNoteId: string; +let createdAttachmentId: string; + +describe("etapi/attachment-content", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + + createdNoteId = await createNote(app, token); + + // Create an attachment + const response = await supertest(app) + .post(`/etapi/attachments`) + .auth(USER, token, { "type": "basic"}) + .send({ + "ownerId": createdNoteId, + "role": "file", + "mime": "text/plain", + "title": "my attachment", + "content": "text" + }); + createdAttachmentId = response.body.attachmentId; + expect(createdAttachmentId).toBeTruthy(); + }); + + it("changes attachment content", async () => { + const text = "Changed content"; + await supertest(app) + .put(`/etapi/attachments/${createdAttachmentId}/content`) + .auth(USER, token, { "type": "basic"}) + .set("Content-Type", "text/plain") + .send(text) + .expect(204); + + // Ensure it got changed. + const response = await supertest(app) + .get(`/etapi/attachments/${createdAttachmentId}/content`) + .auth(USER, token, { "type": "basic"}); + expect(response.text).toStrictEqual(text); + }); + + it("supports binary content", async() => { + await supertest(app) + .put(`/etapi/attachments/${createdAttachmentId}/content`) + .auth(USER, token, { "type": "basic"}) + .set("Content-Type", "application/octet-stream") + .set("Content-Transfer-Encoding", "binary") + .send(Buffer.from("Hello world")) + .expect(204); + }); + +}); diff --git a/apps/server/spec/etapi/basic-auth.spec.ts b/apps/server/spec/etapi/basic-auth.spec.ts new file mode 100644 index 000000000..6518c7a12 --- /dev/null +++ b/apps/server/spec/etapi/basic-auth.spec.ts @@ -0,0 +1,54 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; +const URL = "/etapi/notes/root"; + +describe("basic-auth", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + }); + + it("auth token works", async () => { + const response = await supertest(app) + .get(URL) + .auth(USER, token, { "type": "basic"}) + .expect(200); + }); + + it("rejects wrong password", async () => { + const response = await supertest(app) + .get(URL) + .auth(USER, "wrong", { "type": "basic"}) + .expect(401); + }); + + it("rejects wrong user", async () => { + const response = await supertest(app) + .get(URL) + .auth("wrong", token, { "type": "basic"}) + .expect(401); + }); + + it("logs out", async () => { + await supertest(app) + .post("/etapi/auth/logout") + .auth(USER, token, { "type": "basic"}) + .expect(204); + + // Ensure we can't access it anymore + await supertest(app) + .get("/etapi/notes/root") + .auth(USER, token, { "type": "basic"}) + .expect(401); + }); +}); diff --git a/apps/server/spec/etapi/create-backup.spec.ts b/apps/server/spec/etapi/create-backup.spec.ts new file mode 100644 index 000000000..00c8751aa --- /dev/null +++ b/apps/server/spec/etapi/create-backup.spec.ts @@ -0,0 +1,26 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; + +describe("etapi/backup", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + }); + + it("backup works", async () => { + const response = await supertest(app) + .put("/etapi/backup/etapi_test") + .auth(USER, token, { "type": "basic"}) + .expect(204); + }); +}); diff --git a/apps/server/spec/etapi/create-entities.spec.ts b/apps/server/spec/etapi/create-entities.spec.ts new file mode 100644 index 000000000..25dab1d45 --- /dev/null +++ b/apps/server/spec/etapi/create-entities.spec.ts @@ -0,0 +1,178 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; +import { randomInt } from "crypto"; + +let app: Application; +let token: string; +let createdNoteId: string; +let createdBranchId: string; +let clonedBranchId: string; +let createdAttributeId: string; +let createdAttachmentId: string; + +const USER = "etapi"; + +describe("etapi/create-entities", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + + ({ createdNoteId, createdBranchId } = await createNote()); + clonedBranchId = await createClone(); + createdAttributeId = await createAttribute(); + createdAttachmentId = await createAttachment(); + }); + + it("returns note info", async () => { + const response = await supertest(app) + .get(`/etapi/notes/${createdNoteId}`) + .auth(USER, token, { "type": "basic"}) + .send({ + noteId: createdNoteId, + parentNoteId: "_hidden" + }) + .expect(200); + expect(response.body).toMatchObject({ + noteId: createdNoteId, + title: "Hello" + }); + expect(new Set(response.body.parentBranchIds)) + .toStrictEqual(new Set([ clonedBranchId, createdBranchId ])); + }); + + it("obtains note content", async () => { + await supertest(app) + .get(`/etapi/notes/${createdNoteId}/content`) + .auth(USER, token, { "type": "basic"}) + .expect(200) + .expect("Hi there!"); + }); + + it("obtains created branch information", async () => { + const response = await supertest(app) + .get(`/etapi/branches/${createdBranchId}`) + .auth(USER, token, { "type": "basic"}) + .expect(200); + expect(response.body).toMatchObject({ + branchId: createdBranchId, + parentNoteId: "root" + }); + }); + + it("obtains cloned branch information", async () => { + const response = await supertest(app) + .get(`/etapi/branches/${clonedBranchId}`) + .auth(USER, token, { "type": "basic"}) + .expect(200); + expect(response.body).toMatchObject({ + branchId: clonedBranchId, + parentNoteId: "_hidden" + }); + }); + + it("obtains attribute information", async () => { + const response = await supertest(app) + .get(`/etapi/attributes/${createdAttributeId}`) + .auth(USER, token, { "type": "basic"}) + .expect(200); + expect(response.body.attributeId).toStrictEqual(createdAttributeId); + }); + + it("obtains attachment information", async () => { + const response = await supertest(app) + .get(`/etapi/attachments/${createdAttachmentId}`) + .auth(USER, token, { "type": "basic"}) + .expect(200); + expect(response.body.attachmentId).toStrictEqual(createdAttachmentId); + expect(response.body).toMatchObject({ + role: "file", + mime: "plain/text", + title: "my attachment" + }); + }); +}); + +async function createNote() { + const noteId = `forcedId${randomInt(1000)}`; + const response = await supertest(app) + .post("/etapi/create-note") + .auth(USER, token, { "type": "basic"}) + .send({ + "noteId": noteId, + "parentNoteId": "root", + "title": "Hello", + "type": "text", + "content": "Hi there!", + "dateCreated": "2023-08-21 23:38:51.123+0200", + "utcDateCreated": "2023-08-21 23:38:51.123Z" + }) + .expect(201); + expect(response.body.note.noteId).toStrictEqual(noteId); + expect(response.body).toMatchObject({ + note: { + noteId, + title: "Hello", + dateCreated: "2023-08-21 23:38:51.123+0200", + utcDateCreated: "2023-08-21 23:38:51.123Z" + }, + branch: { + parentNoteId: "root" + } + }); + + return { + createdNoteId: response.body.note.noteId, + createdBranchId: response.body.branch.branchId + }; +} + +async function createClone() { + const response = await supertest(app) + .post("/etapi/branches") + .auth(USER, token, { "type": "basic"}) + .send({ + noteId: createdNoteId, + parentNoteId: "_hidden" + }) + .expect(201); + expect(response.body.parentNoteId).toStrictEqual("_hidden"); + return response.body.branchId; +} + +async function createAttribute() { + const attributeId = `forcedId${randomInt(1000)}`; + const response = await supertest(app) + .post("/etapi/attributes") + .auth(USER, token, { "type": "basic"}) + .send({ + "attributeId": attributeId, + "noteId": createdNoteId, + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": true + }) + .expect(201); + expect(response.body.attributeId).toStrictEqual(attributeId); + return response.body.attributeId; +} + +async function createAttachment() { + const response = await supertest(app) + .post("/etapi/attachments") + .auth(USER, token, { "type": "basic"}) + .send({ + "ownerId": createdNoteId, + "role": "file", + "mime": "plain/text", + "title": "my attachment", + "content": "my text" + }) + .expect(201); + return response.body.attachmentId; +} diff --git a/apps/server/spec/etapi/delete-entities.spec.ts b/apps/server/spec/etapi/delete-entities.spec.ts new file mode 100644 index 000000000..581b1e693 --- /dev/null +++ b/apps/server/spec/etapi/delete-entities.spec.ts @@ -0,0 +1,172 @@ +import { Application } from "express"; +import { beforeAll, beforeEach, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; +import { randomInt } from "crypto"; + +let app: Application; +let token: string; +let createdNoteId: string; +let createdBranchId: string; + +const USER = "etapi"; + +type EntityType = "attachments" | "attributes" | "branches" | "notes"; + +describe("etapi/delete-entities", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + }); + + beforeEach(async () => { + ({ createdNoteId, createdBranchId } = await createNote()); + }); + + it("deletes attachment", async () => { + const attachmentId = await createAttachment(); + await deleteEntity("attachments", attachmentId); + await expectNotFound("attachments", attachmentId); + }); + + it("deletes attribute", async () => { + const attributeId = await createAttribute(); + await deleteEntity("attributes", attributeId); + await expectNotFound("attributes", attributeId); + }); + + it("deletes cloned branch", async () => { + const clonedBranchId = await createClone(); + + await expectFound("branches", createdBranchId); + await expectFound("branches", clonedBranchId); + + await deleteEntity("branches", createdBranchId); + await expectNotFound("branches", createdBranchId); + + await expectFound("branches", clonedBranchId); + await expectFound("notes", createdNoteId); + }); + + it("deletes note with all branches", async () => { + const attributeId = await createAttribute(); + + const clonedBranchId = await createClone(); + + await expectFound("notes", createdNoteId); + await expectFound("branches", createdBranchId); + await expectFound("branches", clonedBranchId); + await expectFound("attributes", attributeId); + await deleteEntity("notes", createdNoteId); + + await expectNotFound("branches", createdBranchId); + await expectNotFound("branches", clonedBranchId); + await expectNotFound("notes", createdNoteId); + await expectNotFound("attributes", attributeId); + }); +}); + +async function createNote() { + const noteId = `forcedId${randomInt(1000)}`; + const response = await supertest(app) + .post("/etapi/create-note") + .auth(USER, token, { "type": "basic"}) + .send({ + "noteId": noteId, + "parentNoteId": "root", + "title": "Hello", + "type": "text", + "content": "Hi there!", + "dateCreated": "2023-08-21 23:38:51.123+0200", + "utcDateCreated": "2023-08-21 23:38:51.123Z" + }) + .expect(201); + expect(response.body.note.noteId).toStrictEqual(noteId); + + return { + createdNoteId: response.body.note.noteId, + createdBranchId: response.body.branch.branchId + }; +} + +async function createClone() { + const response = await supertest(app) + .post("/etapi/branches") + .auth(USER, token, { "type": "basic"}) + .send({ + noteId: createdNoteId, + parentNoteId: "_hidden" + }) + .expect(201); + expect(response.body.parentNoteId).toStrictEqual("_hidden"); + return response.body.branchId; +} + +async function createAttribute() { + const attributeId = `forcedId${randomInt(1000)}`; + const response = await supertest(app) + .post("/etapi/attributes") + .auth(USER, token, { "type": "basic"}) + .send({ + "attributeId": attributeId, + "noteId": createdNoteId, + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": true + }) + .expect(201); + expect(response.body.attributeId).toStrictEqual(attributeId); + return response.body.attributeId; +} + +async function createAttachment() { + const response = await supertest(app) + .post("/etapi/attachments") + .auth(USER, token, { "type": "basic"}) + .send({ + "ownerId": createdNoteId, + "role": "file", + "mime": "plain/text", + "title": "my attachment", + "content": "my text" + }) + .expect(201); + return response.body.attachmentId; +} + +async function deleteEntity(entity: EntityType, id: string) { + // Delete twice to test idempotency. + for (let i=0; i < 2; i++) { + await supertest(app) + .delete(`/etapi/${entity}/${id}`) + .auth(USER, token, { "type": "basic"}) + .expect(204); + } +} + +const MISSING_ENTITY_ERROR_CODES: Record = { + attachments: "ATTACHMENT_NOT_FOUND", + attributes: "ATTRIBUTE_NOT_FOUND", + branches: "BRANCH_NOT_FOUND", + notes: "NOTE_NOT_FOUND" +} + +async function expectNotFound(entity: EntityType, id: string) { + const response = await supertest(app) + .get(`/etapi/${entity}/${id}`) + .auth(USER, token, { "type": "basic"}) + .expect(404); + + expect(response.body.code).toStrictEqual(MISSING_ENTITY_ERROR_CODES[entity]); +} + +async function expectFound(entity: EntityType, id: string) { + await supertest(app) + .get(`/etapi/${entity}/${id}`) + .auth(USER, token, { "type": "basic"}) + .expect(200); +} diff --git a/apps/server/spec/etapi/etapi-metrics.spec.ts b/apps/server/spec/etapi/etapi-metrics.spec.ts new file mode 100644 index 000000000..7b7d3a184 --- /dev/null +++ b/apps/server/spec/etapi/etapi-metrics.spec.ts @@ -0,0 +1,71 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; + +describe("etapi/metrics", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + }); + + it("returns Prometheus format by default", async () => { + const response = await supertest(app) + .get("/etapi/metrics") + .auth(USER, token, { "type": "basic"}) + .expect(200); + expect(response.headers["content-type"]).toContain("text/plain"); + expect(response.text).toContain("trilium_info"); + expect(response.text).toContain("trilium_notes_total"); + expect(response.text).toContain("# HELP"); + expect(response.text).toContain("# TYPE"); + }); + + it("returns JSON when requested", async() => { + const response = await supertest(app) + .get("/etapi/metrics?format=json") + .auth(USER, token, { "type": "basic"}) + .expect(200); + expect(response.headers["content-type"]).toContain("application/json"); + expect(response.body.version).toBeTruthy(); + expect(response.body.database).toBeTruthy(); + expect(response.body.timestamp).toBeTruthy(); + expect(response.body.database.totalNotes).toBeTypeOf("number"); + expect(response.body.database.activeNotes).toBeTypeOf("number"); + expect(response.body.noteTypes).toBeTruthy(); + expect(response.body.attachmentTypes).toBeTruthy(); + expect(response.body.statistics).toBeTruthy(); + }); + + it("returns Prometheus format explicitly", async () => { + const response = await supertest(app) + .get("/etapi/metrics?format=prometheus") + .auth(USER, token, { "type": "basic"}) + .expect(200); + expect(response.headers["content-type"]).toContain("text/plain"); + expect(response.text).toContain("trilium_info"); + expect(response.text).toContain("trilium_notes_total"); + }); + + it("returns error on invalid format", async() => { + const response = await supertest(app) + .get("/etapi/metrics?format=xml") + .auth(USER, token, { "type": "basic"}) + .expect(500); + expect(response.body.message).toContain("prometheus"); + }); + + it("should fail without authentication", async() => { + await supertest(app) + .get("/etapi/metrics") + .expect(401); + }); +}); diff --git a/apps/server/spec/etapi/export-note-subtree.spec.ts b/apps/server/spec/etapi/export-note-subtree.spec.ts new file mode 100644 index 000000000..f5f09b532 --- /dev/null +++ b/apps/server/spec/etapi/export-note-subtree.spec.ts @@ -0,0 +1,51 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; + +describe("etapi/export-note-subtree", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + }); + + it("export works", async () => { + await supertest(app) + .get("/etapi/notes/root/export") + .auth(USER, token, { "type": "basic"}) + .expect(200) + .expect("Content-Type", "application/zip"); + }); + + it("HTML export works", async () => { + await supertest(app) + .get("/etapi/notes/root/export?format=html") + .auth(USER, token, { "type": "basic"}) + .expect(200) + .expect("Content-Type", "application/zip"); + }); + + it("Markdown export works", async () => { + await supertest(app) + .get("/etapi/notes/root/export?format=markdown") + .auth(USER, token, { "type": "basic"}) + .expect(200) + .expect("Content-Type", "application/zip"); + }); + + it("reports wrong format", async () => { + const response = await supertest(app) + .get("/etapi/notes/root/export?format=wrong") + .auth(USER, token, { "type": "basic"}) + .expect(400); + expect(response.body.code).toStrictEqual("UNRECOGNIZED_EXPORT_FORMAT"); + }); +}); diff --git a/apps/server/spec/etapi/get-date-notes.spec.ts b/apps/server/spec/etapi/get-date-notes.spec.ts new file mode 100644 index 000000000..e1f67fec6 --- /dev/null +++ b/apps/server/spec/etapi/get-date-notes.spec.ts @@ -0,0 +1,103 @@ +import { beforeAll, describe, expect, it } from "vitest"; +import config from "../../src/services/config.js"; +import { login } from "./utils.js"; +import { Application } from "express"; +import supertest from "supertest"; +import date_notes from "../../src/services/date_notes.js"; +import cls from "../../src/services/cls.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; + +describe("etapi/get-date-notes", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + }); + + it("obtains inbox", async () => { + await supertest(app) + .get("/etapi/inbox/2022-01-01") + .auth(USER, token, { "type": "basic"}) + .expect(200); + }); + + describe("days", () => { + it("obtains day from calendar", async () => { + await supertest(app) + .get("/etapi/calendar/days/2022-01-01") + .auth(USER, token, { "type": "basic"}) + .expect(200); + }); + + it("detects invalid date", async () => { + const response = await supertest(app) + .get("/etapi/calendar/days/2022-1") + .auth(USER, token, { "type": "basic"}) + .expect(400); + expect(response.body.code).toStrictEqual("DATE_INVALID"); + }); + }); + + describe("weeks", () => { + beforeAll(() => { + cls.init(() => { + const rootCalendarNote = date_notes.getRootCalendarNote(); + rootCalendarNote.setLabel("enableWeekNote"); + }); + }); + + it("obtains week calendar", async () => { + await supertest(app) + .get("/etapi/calendar/weeks/2022-W01") + .auth(USER, token, { "type": "basic"}) + .expect(200); + }); + + it("detects invalid date", async () => { + const response = await supertest(app) + .get("/etapi/calendar/weeks/2022-1") + .auth(USER, token, { "type": "basic"}) + .expect(400); + expect(response.body.code).toStrictEqual("WEEK_INVALID"); + }); + }); + + describe("months", () => { + it("obtains month calendar", async () => { + await supertest(app) + .get("/etapi/calendar/months/2022-01") + .auth(USER, token, { "type": "basic"}) + .expect(200); + }); + + it("detects invalid month", async () => { + const response = await supertest(app) + .get("/etapi/calendar/months/2022-1") + .auth(USER, token, { "type": "basic"}) + .expect(400); + expect(response.body.code).toStrictEqual("MONTH_INVALID"); + }); + }); + + describe("years", () => { + it("obtains year calendar", async () => { + await supertest(app) + .get("/etapi/calendar/years/2022") + .auth(USER, token, { "type": "basic"}) + .expect(200); + }); + + it("detects invalid year", async () => { + const response = await supertest(app) + .get("/etapi/calendar/years/202") + .auth(USER, token, { "type": "basic"}) + .expect(400); + expect(response.body.code).toStrictEqual("YEAR_INVALID"); + }); + }); +}); diff --git a/apps/server/spec/etapi/get-inherited-attribute-cloned.spec.ts b/apps/server/spec/etapi/get-inherited-attribute-cloned.spec.ts new file mode 100644 index 000000000..5d882746f --- /dev/null +++ b/apps/server/spec/etapi/get-inherited-attribute-cloned.spec.ts @@ -0,0 +1,98 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { createNote, login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +let parentNoteId: string; + +describe("etapi/get-inherited-attribute-cloned", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + + parentNoteId = await createNote(app, token); + }); + + it("gets inherited attribute", async () => { + // Create an inheritable attribute on the parent note. + let response = await supertest(app) + .post("/etapi/attributes") + .auth("etapi", token, { "type": "basic"}) + .send({ + "noteId": parentNoteId, + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": true, + "position": 10 + }) + .expect(201); + const parentAttributeId = response.body.attributeId; + expect(parentAttributeId).toBeTruthy(); + + // Create a subnote. + response = await supertest(app) + .post("/etapi/create-note") + .auth("etapi", token, { "type": "basic"}) + .send({ + "parentNoteId": parentNoteId, + "title": "Hello", + "type": "text", + "content": "Hi there!" + }) + .expect(201); + const childNoteId = response.body.note.noteId; + + // Create child attribute + response = await supertest(app) + .post("/etapi/attributes") + .auth("etapi", token, { "type": "basic"}) + .send({ + "noteId": childNoteId, + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": false, + "position": 10 + }) + .expect(201); + const childAttributeId = response.body.attributeId; + expect(parentAttributeId).toBeTruthy(); + + // Clone child to parent + response = await supertest(app) + .post("/etapi/branches") + .auth("etapi", token, { "type": "basic"}) + .send({ + noteId: childNoteId, + parentNoteId: parentNoteId + }) + .expect(200); + parentNoteId = response.body.parentNoteId; + + // Check attribute IDs + response = await supertest(app) + .get(`/etapi/notes/${childNoteId}`) + .auth("etapi", token, { "type": "basic"}) + .expect(200); + expect(response.body.noteId).toStrictEqual(childNoteId); + expect(response.body.attributes).toHaveLength(2); + expect(hasAttribute(response.body.attributes, parentAttributeId)); + expect(hasAttribute(response.body.attributes, childAttributeId)); + }); + + function hasAttribute(list: object[], attributeId: string) { + for (let i = 0; i < list.length; i++) { + if (list[i]["attributeId"] === attributeId) { + return true; + } + } + return false; + } +}); diff --git a/apps/server/spec/etapi/get-inherited-attribute.spec.ts b/apps/server/spec/etapi/get-inherited-attribute.spec.ts new file mode 100644 index 000000000..c0e92dde1 --- /dev/null +++ b/apps/server/spec/etapi/get-inherited-attribute.spec.ts @@ -0,0 +1,60 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { createNote, login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +let parentNoteId: string; + +describe("etapi/get-inherited-attribute", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + + parentNoteId = await createNote(app, token); + }); + + it("gets inherited attribute", async () => { + // Create an inheritable attribute on the parent note. + let response = await supertest(app) + .post("/etapi/attributes") + .auth("etapi", token, { "type": "basic"}) + .send({ + "noteId": parentNoteId, + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": true + }) + .expect(201); + const createdAttributeId = response.body.attributeId; + expect(createdAttributeId).toBeTruthy(); + + // Create a subnote. + response = await supertest(app) + .post("/etapi/create-note") + .auth("etapi", token, { "type": "basic"}) + .send({ + "parentNoteId": parentNoteId, + "title": "Hello", + "type": "text", + "content": "Hi there!" + }) + .expect(201); + const createdNoteId = response.body.note.noteId; + + // Check the attribute is inherited. + response = await supertest(app) + .get(`/etapi/notes/${createdNoteId}`) + .auth("etapi", token, { "type": "basic"}) + .expect(200); + expect(response.body.noteId).toStrictEqual(createdNoteId); + expect(response.body.attributes).toHaveLength(1); + expect(response.body.attributes[0].attributeId === createdAttributeId); + }); +}); diff --git a/apps/server/spec/etapi/import-zip.spec.ts b/apps/server/spec/etapi/import-zip.spec.ts new file mode 100644 index 000000000..c42623b76 --- /dev/null +++ b/apps/server/spec/etapi/import-zip.spec.ts @@ -0,0 +1,34 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; +import { readFileSync } from "fs"; +import { join } from "path"; + +let app: Application; +let token: string; + +const USER = "etapi"; + +describe("etapi/import", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + }); + + it("demo zip can be imported", async () => { + const buffer = readFileSync(join(__dirname, "../../src/assets/db/demo.zip")); + const response = await supertest(app) + .post("/etapi/notes/root/import") + .auth(USER, token, { "type": "basic"}) + .set("Content-Type", "application/octet-stream") + .set("Content-Transfer-Encoding", "binary") + .send(buffer) + .expect(201); + expect(response.body.note.title).toStrictEqual("Journal"); + expect(response.body.branch.parentNoteId).toStrictEqual("root"); + }); +}); diff --git a/apps/server/spec/etapi/no-token.spec.ts b/apps/server/spec/etapi/no-token.spec.ts new file mode 100644 index 000000000..d4a7a2f9f --- /dev/null +++ b/apps/server/spec/etapi/no-token.spec.ts @@ -0,0 +1,54 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; +import type TestAgent from "supertest/lib/agent.js"; + +let app: Application; + +const USER = "etapi"; + +const routes = [ + "GET /etapi/notes?search=aaa", + "GET /etapi/notes/root", + "PATCH /etapi/notes/root", + "DELETE /etapi/notes/root", + "GET /etapi/branches/root", + "PATCH /etapi/branches/root", + "DELETE /etapi/branches/root", + "GET /etapi/attributes/000", + "PATCH /etapi/attributes/000", + "DELETE /etapi/attributes/000", + "GET /etapi/inbox/2022-02-22", + "GET /etapi/calendar/days/2022-02-22", + "GET /etapi/calendar/weeks/2022-02-22", + "GET /etapi/calendar/months/2022-02", + "GET /etapi/calendar/years/2022", + "POST /etapi/create-note", + "GET /etapi/app-info", +] + +describe("no-token", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + }); + + for (const route of routes) { + const [ method, url ] = route.split(" ", 2); + + it(`rejects access to ${method} ${url}`, () => { + (supertest(app)[method.toLowerCase()](url) as TestAgent) + .auth(USER, "fakeauth", { "type": "basic"}) + .expect(401) + }); + } + + it("responds with 404 even without token", () => { + supertest(app) + .get("/etapi/zzzzzz") + .expect(404); + }); +}); diff --git a/apps/server/spec/etapi/note-content.spec.ts b/apps/server/spec/etapi/note-content.spec.ts new file mode 100644 index 000000000..5b7fdcba8 --- /dev/null +++ b/apps/server/spec/etapi/note-content.spec.ts @@ -0,0 +1,72 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { createNote, login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; +let createdNoteId: string; + +describe("etapi/note-content", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + + createdNoteId = await createNote(app, token); + }); + + it("get content", async () => { + const response = await getContentResponse(); + expect(response.text).toStrictEqual("Hi there!"); + }); + + it("put note content", async () => { + const text = "Changed content"; + await supertest(app) + .put(`/etapi/notes/${createdNoteId}/content`) + .auth(USER, token, { "type": "basic"}) + .set("Content-Type", "text/plain") + .send(text) + .expect(204); + + const response = await getContentResponse(); + expect(response.text).toStrictEqual(text); + }); + + it("put note content binary", async () => { + // First, create a binary note + const response = await supertest(app) + .post("/etapi/create-note") + .auth("etapi", token, { "type": "basic"}) + .send({ + "parentNoteId": "root", + "title": "Hello", + "mime": "image/png", + "type": "image", + "content": "" + }) + .expect(201); + const createdNoteId = response.body.note.noteId; + + // Put binary content + await supertest(app) + .put(`/etapi/notes/${createdNoteId}/content`) + .auth(USER, token, { "type": "basic"}) + .set("Content-Type", "application/octet-stream") + .set("Content-Transfer-Encoding", "binary") + .send(Buffer.from("Hello world")) + .expect(204); + }); + + function getContentResponse() { + return supertest(app) + .get(`/etapi/notes/${createdNoteId}/content`) + .auth(USER, token, { "type": "basic"}) + .expect(200); + } +}); diff --git a/apps/server/spec/etapi/other.spec.ts b/apps/server/spec/etapi/other.spec.ts new file mode 100644 index 000000000..d7c1d38b3 --- /dev/null +++ b/apps/server/spec/etapi/other.spec.ts @@ -0,0 +1,26 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; + +describe("etapi/refresh-note-ordering/root", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + }); + + it("refreshes note ordering", async () => { + await supertest(app) + .post("/etapi/refresh-note-ordering/root") + .auth(USER, token, { "type": "basic"}) + .expect(204); + }); +}); diff --git a/apps/server/spec/etapi/patch-attachment.spec.ts b/apps/server/spec/etapi/patch-attachment.spec.ts new file mode 100644 index 000000000..706ac7fbb --- /dev/null +++ b/apps/server/spec/etapi/patch-attachment.spec.ts @@ -0,0 +1,78 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { createNote, login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; +let createdNoteId: string; +let createdAttachmentId: string; + +describe("etapi/attachment-content", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + + createdNoteId = await createNote(app, token); + + // Create an attachment + const response = await supertest(app) + .post(`/etapi/attachments`) + .auth(USER, token, { "type": "basic"}) + .send({ + "ownerId": createdNoteId, + "role": "file", + "mime": "text/plain", + "title": "my attachment", + "content": "text" + }); + createdAttachmentId = response.body.attachmentId; + expect(createdAttachmentId).toBeTruthy(); + }); + + it("changes title and position", async () => { + const state = { + title: "CHANGED", + position: 999 + } + await supertest(app) + .patch(`/etapi/attachments/${createdAttachmentId}`) + .auth(USER, token, { "type": "basic"}) + .send(state) + .expect(200); + + // Ensure it got changed. + const response = await supertest(app) + .get(`/etapi/attachments/${createdAttachmentId}`) + .auth(USER, token, { "type": "basic"}); + expect(response.body).toMatchObject(state); + }); + + it("forbids changing owner", async () => { + const response = await supertest(app) + .patch(`/etapi/attachments/${createdAttachmentId}`) + .auth(USER, token, { "type": "basic"}) + .send({ + ownerId: "root" + }) + .expect(400); + expect(response.body.code).toStrictEqual("PROPERTY_NOT_ALLOWED"); + }); + + it("handles validation error", async () => { + const response = await supertest(app) + .patch(`/etapi/attachments/${createdAttachmentId}`) + .auth(USER, token, { "type": "basic"}) + .send({ + title: null + }) + .expect(400); + expect(response.body.code).toStrictEqual("PROPERTY_VALIDATION_ERROR"); + }); + +}); diff --git a/apps/server/spec/etapi/patch-attribute.spec.ts b/apps/server/spec/etapi/patch-attribute.spec.ts new file mode 100644 index 000000000..821a4e3db --- /dev/null +++ b/apps/server/spec/etapi/patch-attribute.spec.ts @@ -0,0 +1,77 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { createNote, login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; +let createdNoteId: string; +let createdAttributeId: string; + +describe("etapi/patch-attribute", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + + createdNoteId = await createNote(app, token); + + // Create an attribute + const response = await supertest(app) + .post(`/etapi/attributes`) + .auth(USER, token, { "type": "basic"}) + .send({ + "noteId": createdNoteId, + "type": "label", + "name": "mylabel", + "value": "val", + "isInheritable": true + }); + createdAttributeId = response.body.attributeId; + expect(createdAttributeId).toBeTruthy(); + }); + + it("changes name and value", async () => { + const state = { + value: "CHANGED" + }; + await supertest(app) + .patch(`/etapi/attributes/${createdAttributeId}`) + .auth(USER, token, { "type": "basic"}) + .send(state) + .expect(200); + + // Ensure it got changed. + const response = await supertest(app) + .get(`/etapi/attributes/${createdAttributeId}`) + .auth(USER, token, { "type": "basic"}); + expect(response.body).toMatchObject(state); + }); + + it("forbids setting disallowed property", async () => { + const response = await supertest(app) + .patch(`/etapi/attributes/${createdAttributeId}`) + .auth(USER, token, { "type": "basic"}) + .send({ + noteId: "root" + }) + .expect(400); + expect(response.body.code).toStrictEqual("PROPERTY_NOT_ALLOWED"); + }); + + it("forbids setting wrong data type", async () => { + const response = await supertest(app) + .patch(`/etapi/attributes/${createdAttributeId}`) + .auth(USER, token, { "type": "basic"}) + .send({ + value: null + }) + .expect(400); + expect(response.body.code).toStrictEqual("PROPERTY_VALIDATION_ERROR"); + }); + +}); diff --git a/apps/server/spec/etapi/patch-branch.spec.ts b/apps/server/spec/etapi/patch-branch.spec.ts new file mode 100644 index 000000000..ecca59b2d --- /dev/null +++ b/apps/server/spec/etapi/patch-branch.spec.ts @@ -0,0 +1,77 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { createNote, login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; +let createdBranchId: string; + +describe("etapi/attachment-content", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + + // Create a note and a branch. + const response = await supertest(app) + .post("/etapi/create-note") + .auth("etapi", token, { "type": "basic"}) + .send({ + "parentNoteId": "root", + "title": "Hello", + "type": "text", + "content": "", + }) + .expect(201); + + createdBranchId = response.body.branch.branchId; + }); + + it("can patch branch info", async () => { + const state = { + prefix: "pref", + notePosition: 666, + isExpanded: true + }; + + await supertest(app) + .patch(`/etapi/branches/${createdBranchId}`) + .auth("etapi", token, { "type": "basic"}) + .send(state) + .expect(200); + + const response = await supertest(app) + .get(`/etapi/branches/${createdBranchId}`) + .auth("etapi", token, { "type": "basic"}) + .expect(200); + expect(response.body).toMatchObject(state); + }); + + it("rejects not allowed property", async () => { + const response = await supertest(app) + .patch(`/etapi/branches/${createdBranchId}`) + .auth("etapi", token, { "type": "basic"}) + .send({ + parentNoteId: "root" + }) + .expect(400); + expect(response.body.code).toStrictEqual("PROPERTY_NOT_ALLOWED"); + }); + + it("rejects invalid property value", async () => { + const response = await supertest(app) + .patch(`/etapi/branches/${createdBranchId}`) + .auth("etapi", token, { "type": "basic"}) + .send({ + prefix: 123 + }) + .expect(400); + expect(response.body.code).toStrictEqual("PROPERTY_VALIDATION_ERROR"); + }); + +}); diff --git a/apps/server/spec/etapi/patch-note.spec.ts b/apps/server/spec/etapi/patch-note.spec.ts new file mode 100644 index 000000000..178808762 --- /dev/null +++ b/apps/server/spec/etapi/patch-note.spec.ts @@ -0,0 +1,89 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; +let createdNoteId: string; + +describe("etapi/patch-note", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + + const response = await supertest(app) + .post("/etapi/create-note") + .auth("etapi", token, { "type": "basic"}) + .send({ + "parentNoteId": "root", + "title": "Hello", + "type": "code", + "mime": "application/json", + "content": "{}" + }) + .expect(201); + + const createdNoteId = response.body.note.noteId as string; + expect(createdNoteId).toBeTruthy(); + }); + + it("obtains correct note information", async () => { + await expectNoteToMatch({ + title: "Hello", + type: "code", + mime: "application/json" + }); + }); + + it("patches type, mime and creation dates", async () => { + const changes = { + "title": "Wassup", + "type": "html", + "mime": "text/html", + "dateCreated": "2023-08-21 23:38:51.123+0200", + "utcDateCreated": "2023-08-21 23:38:51.123Z" + }; + await supertest(app) + .patch(`/etapi/notes/${createdNoteId}`) + .auth("etapi", token, { "type": "basic"}) + .send(changes) + .expect(200); + await expectNoteToMatch(changes); + }); + + it("refuses setting protection", async () => { + const response = await supertest(app) + .patch(`/etapi/notes/${createdNoteId}`) + .auth("etapi", token, { "type": "basic"}) + .send({ + isProtected: true + }) + .expect(400); + expect(response.body.code).toStrictEqual("PROPERTY_NOT_ALLOWED"); + }); + + it("refuses incorrect type", async () => { + const response = await supertest(app) + .patch(`/etapi/notes/${createdNoteId}`) + .auth("etapi", token, { "type": "basic"}) + .send({ + title: true + }) + .expect(400); + expect(response.body.code).toStrictEqual("PROPERTY_VALIDATION_ERROR"); + }); + + async function expectNoteToMatch(state: object) { + const response = await supertest(app) + .get(`/etapi/notes/${createdNoteId}`) + .auth("etapi", token, { "type": "basic"}) + .expect(200); + expect(response.body).toMatchObject(state); + } +}); diff --git a/apps/server/spec/etapi/post-revision.spec.ts b/apps/server/spec/etapi/post-revision.spec.ts new file mode 100644 index 000000000..20b4d15dd --- /dev/null +++ b/apps/server/spec/etapi/post-revision.spec.ts @@ -0,0 +1,29 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { createNote, login } from "./utils.js"; +import config from "../../src/services/config.js"; + +let app: Application; +let token: string; + +const USER = "etapi"; +let createdNoteId: string; + +describe("etapi/post-revision", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + createdNoteId = await createNote(app, token); + }); + + it("posts note revision", async () => { + await supertest(app) + .post(`/etapi/notes/${createdNoteId}/revision`) + .auth(USER, token, { "type": "basic"}) + .send("Changed content") + .expect(204); + }); +}); diff --git a/apps/server/spec/etapi/search.spec.ts b/apps/server/spec/etapi/search.spec.ts new file mode 100644 index 000000000..bfd14e740 --- /dev/null +++ b/apps/server/spec/etapi/search.spec.ts @@ -0,0 +1,40 @@ +import { Application } from "express"; +import { beforeAll, describe, expect, it } from "vitest"; +import supertest from "supertest"; +import { createNote, login } from "./utils.js"; +import config from "../../src/services/config.js"; +import { randomUUID } from "crypto"; + +let app: Application; +let token: string; + +const USER = "etapi"; +let content: string; + +describe("etapi/search", () => { + beforeAll(async () => { + config.General.noAuthentication = false; + const buildApp = (await (import("../../src/app.js"))).default; + app = await buildApp(); + token = await login(app); + + content = randomUUID(); + await createNote(app, token, content); + }); + + it("finds by content", async () => { + const response = await supertest(app) + .get(`/etapi/notes?search=${content}&debug=true`) + .auth(USER, token, { "type": "basic"}) + .expect(200); + expect(response.body.results).toHaveLength(1); + }); + + it("does not find by content when fast search is on", async () => { + const response = await supertest(app) + .get(`/etapi/notes?search=${content}&debug=true&fastSearch=true`) + .auth(USER, token, { "type": "basic"}) + .expect(200); + expect(response.body.results).toHaveLength(0); + }); +}); diff --git a/apps/server/spec/etapi/utils.ts b/apps/server/spec/etapi/utils.ts new file mode 100644 index 000000000..140c6820d --- /dev/null +++ b/apps/server/spec/etapi/utils.ts @@ -0,0 +1,33 @@ +import type { Application } from "express"; +import supertest from "supertest"; +import { expect } from "vitest"; + +export async function login(app: Application) { + // Obtain auth token. + const response = await supertest(app) + .post("/etapi/auth/login") + .send({ + "password": "demo1234" + }) + .expect(201); + const token = response.body.authToken; + expect(token).toBeTruthy(); + return token; +} + +export async function createNote(app: Application, token: string, content?: string) { + const response = await supertest(app) + .post("/etapi/create-note") + .auth("etapi", token, { "type": "basic"}) + .send({ + "parentNoteId": "root", + "title": "Hello", + "type": "text", + "content": content ?? "Hi there!", + }) + .expect(201); + + const noteId = response.body.note.noteId as string; + expect(noteId).toStrictEqual(noteId); + return noteId; +} diff --git a/apps/server/spec/setup.ts b/apps/server/spec/setup.ts index 2fae6e73c..74e7ff746 100644 --- a/apps/server/spec/setup.ts +++ b/apps/server/spec/setup.ts @@ -3,6 +3,13 @@ import i18next from "i18next"; import { join } from "path"; import dayjs from "dayjs"; +// Initialize environment variables. +process.env.TRILIUM_DATA_DIR = join(__dirname, "db"); +process.env.TRILIUM_RESOURCE_DIR = join(__dirname, "../src"); +process.env.TRILIUM_INTEGRATION_TEST = "memory"; +process.env.TRILIUM_ENV = "dev"; +process.env.TRILIUM_PUBLIC_SERVER = "http://localhost:4200"; + beforeAll(async () => { // Initialize the translations manually to avoid any side effects. const Backend = (await import("i18next-fs-backend")).default; diff --git a/apps/server/src/assets/db/demo.zip b/apps/server/src/assets/db/demo.zip index 8a0d61779..322fbe7de 100644 Binary files a/apps/server/src/assets/db/demo.zip and b/apps/server/src/assets/db/demo.zip differ diff --git a/apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json b/apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json index f5f7da775..a6eecd810 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json +++ b/apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json @@ -1 +1 @@ -[{"id":"_help_Otzi9La2YAUX","title":"Installation & Setup","type":"book","attributes":[{"name":"iconClass","value":"bx bx-cog","type":"label"}],"children":[{"id":"_help_poXkQfguuA0U","title":"Desktop Installation","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Desktop Installation"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_WOcw2SLH6tbX","title":"Server Installation","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_Dgg7bR3b6K9j","title":"1. Installing the server","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_3tW6mORuTHnB","title":"Packaged version for Linux","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Packaged version for Linux"},{"name":"iconClass","value":"bx bxl-tux","type":"label"}]},{"id":"_help_rWX5eY045zbE","title":"Using Docker","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Using Docker"},{"name":"iconClass","value":"bx bxl-docker","type":"label"}]},{"id":"_help_moVgBcoxE3EK","title":"On NixOS","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/On NixOS"},{"name":"iconClass","value":"bx bxl-tux","type":"label"}]},{"id":"_help_J1Bb6lVlwU5T","title":"Manually","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Manually"},{"name":"iconClass","value":"bx bx-code-alt","type":"label"}]},{"id":"_help_DCmT6e7clMoP","title":"Using Kubernetes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Using Kubernetes"},{"name":"iconClass","value":"bx bxl-kubernetes","type":"label"}]},{"id":"_help_klCWNks3ReaQ","title":"Multiple server instances","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Multiple server instances"},{"name":"iconClass","value":"bx bxs-user-account","type":"label"}]}]},{"id":"_help_vcjrb3VVYPZI","title":"2. Reverse proxy","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_ud6MShXL4WpO","title":"Nginx","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/2. Reverse proxy/Nginx"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_fDLvzOx29Pfg","title":"Apache","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/2. Reverse proxy/Apache"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_l2VkvOwUNfZj","title":"TLS Configuration","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/TLS Configuration"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_cbkrhQjrkKrh","title":"Synchronization","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Synchronization"},{"name":"iconClass","value":"bx bx-sync","type":"label"}]},{"id":"_help_RDslemsQ6gCp","title":"Mobile Frontend","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Mobile Frontend"},{"name":"iconClass","value":"bx bx-mobile-alt","type":"label"}]},{"id":"_help_MtPxeAWVAzMg","title":"Web Clipper","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Web Clipper"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_n1lujUxCwipy","title":"Upgrading TriliumNext","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Upgrading TriliumNext"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_ODY7qQn5m2FT","title":"Backup","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Backup"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_tAassRL4RSQL","title":"Data directory","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Data directory"},{"name":"iconClass","value":"bx bx-folder-open","type":"label"}]}]},{"id":"_help_gh7bpGYxajRS","title":"Basic Concepts and Features","type":"book","attributes":[{"name":"iconClass","value":"bx bx-help-circle","type":"label"}],"children":[{"id":"_help_Vc8PjrjAGuOp","title":"UI Elements","type":"book","attributes":[{"name":"iconClass","value":"bx bx-window-alt","type":"label"}],"children":[{"id":"_help_x0JgW8UqGXvq","title":"Vertical and horizontal layout","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Vertical and horizontal layout"},{"name":"iconClass","value":"bx bxs-layout","type":"label"}]},{"id":"_help_x3i7MxGccDuM","title":"Global menu","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Global menu"},{"name":"iconClass","value":"bx bx-menu","type":"label"}]},{"id":"_help_oPVyFC7WL2Lp","title":"Note Tree","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree"},{"name":"iconClass","value":"bx bxs-tree-alt","type":"label"}],"children":[{"id":"_help_YtSN43OrfzaA","title":"Note tree contextual menu","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Note tree contextual menu"},{"name":"iconClass","value":"bx bx-menu","type":"label"}]},{"id":"_help_yTjUdsOi4CIE","title":"Multiple selection","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Multiple selection"},{"name":"iconClass","value":"bx bx-list-plus","type":"label"}]}]},{"id":"_help_BlN9DFI679QC","title":"Ribbon","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Ribbon"},{"name":"iconClass","value":"bx bx-dots-horizontal","type":"label"}]},{"id":"_help_3seOhtN8uLIY","title":"Tabs","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Tabs"},{"name":"iconClass","value":"bx bx-dock-top","type":"label"}]},{"id":"_help_xYmIYSP6wE3F","title":"Launch Bar","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Launch Bar"},{"name":"iconClass","value":"bx bx-sidebar","type":"label"}]},{"id":"_help_8YBEPzcpUgxw","title":"Note buttons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note buttons"},{"name":"iconClass","value":"bx bx-dots-vertical-rounded","type":"label"}]},{"id":"_help_4TIF1oA4VQRO","title":"Options","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Options"},{"name":"iconClass","value":"bx bx-cog","type":"label"}]},{"id":"_help_luNhaphA37EO","title":"Split View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Split View"},{"name":"iconClass","value":"bx bx-dock-right","type":"label"}]},{"id":"_help_XpOYSgsLkTJy","title":"Floating buttons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Floating buttons"},{"name":"iconClass","value":"bx bx-rectangle","type":"label"}]},{"id":"_help_RnaPdbciOfeq","title":"Right Sidebar","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Right Sidebar"},{"name":"iconClass","value":"bx bxs-dock-right","type":"label"}]},{"id":"_help_r5JGHN99bVKn","title":"Recent Changes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Recent Changes"},{"name":"iconClass","value":"bx bx-history","type":"label"}]},{"id":"_help_ny318J39E5Z0","title":"Zoom","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Zoom"},{"name":"iconClass","value":"bx bx-zoom-in","type":"label"}]}]},{"id":"_help_BFs8mudNFgCS","title":"Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes"},{"name":"iconClass","value":"bx bx-notepad","type":"label"}],"children":[{"id":"_help_p9kXRFAkwN4o","title":"Note Icons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Note Icons"},{"name":"iconClass","value":"bx bxs-grid","type":"label"}]},{"id":"_help_0vhv7lsOLy82","title":"Attachments","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Attachments"},{"name":"iconClass","value":"bx bx-paperclip","type":"label"}]},{"id":"_help_IakOLONlIfGI","title":"Cloning Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Cloning Notes"},{"name":"iconClass","value":"bx bx-duplicate","type":"label"}],"children":[{"id":"_help_TBwsyfadTA18","title":"Branch prefix","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Cloning Notes/Branch prefix"},{"name":"iconClass","value":"bx bx-rename","type":"label"}]}]},{"id":"_help_bwg0e8ewQMak","title":"Protected Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Protected Notes"},{"name":"iconClass","value":"bx bx-lock-alt","type":"label"}]},{"id":"_help_MKmLg5x6xkor","title":"Archived Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Archived Notes"},{"name":"iconClass","value":"bx bx-box","type":"label"}]},{"id":"_help_vZWERwf8U3nx","title":"Note Revisions","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Note Revisions"},{"name":"iconClass","value":"bx bx-history","type":"label"}]},{"id":"_help_aGlEvb9hyDhS","title":"Sorting Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Sorting Notes"},{"name":"iconClass","value":"bx bx-sort-up","type":"label"}]},{"id":"_help_NRnIZmSMc5sj","title":"Export as PDF","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Export as PDF"},{"name":"iconClass","value":"bx bxs-file-pdf","type":"label"}]},{"id":"_help_CoFPLs3dRlXc","title":"Read-Only Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Read-Only Notes"},{"name":"iconClass","value":"bx bx-edit-alt","type":"label"}]},{"id":"_help_0ESUbbAxVnoK","title":"Note List","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Note List"},{"name":"iconClass","value":"bx bxs-grid","type":"label"}],"children":[{"id":"_help_xWbu3jpNWapp","title":"Calendar View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Calendar View"},{"name":"iconClass","value":"bx bx-calendar","type":"label"}]}]}]},{"id":"_help_wArbEsdSae6g","title":"Navigation","type":"book","attributes":[{"name":"iconClass","value":"bx bx-navigation","type":"label"}],"children":[{"id":"_help_kBrnXNG3Hplm","title":"Tree Concepts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Tree Concepts"},{"name":"iconClass","value":"bx bx-pyramid","type":"label"}]},{"id":"_help_MMiBEQljMQh2","title":"Note Navigation","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Note Navigation"},{"name":"iconClass","value":"bx bxs-navigation","type":"label"}]},{"id":"_help_Ms1nauBra7gq","title":"Quick search","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Quick search"},{"name":"iconClass","value":"bx bx-search-alt-2","type":"label"}]},{"id":"_help_F1r9QtzQLZqm","title":"Jump to Note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Jump to Note"},{"name":"iconClass","value":"bx bx-send","type":"label"}]},{"id":"_help_eIg8jdvaoNNd","title":"Search","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Search"},{"name":"iconClass","value":"bx bx-search-alt-2","type":"label"}]},{"id":"_help_u3YFHC9tQlpm","title":"Bookmarks","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Bookmarks"},{"name":"iconClass","value":"bx bx-bookmarks","type":"label"}]},{"id":"_help_OR8WJ7Iz9K4U","title":"Note Hoisting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Note Hoisting"},{"name":"iconClass","value":"bx bxs-chevrons-up","type":"label"}]},{"id":"_help_9sRHySam5fXb","title":"Workspaces","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Workspaces"},{"name":"iconClass","value":"bx bx-door-open","type":"label"}]},{"id":"_help_xWtq5NUHOwql","title":"Similar Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Similar Notes"},{"name":"iconClass","value":"bx bx-bar-chart","type":"label"}]},{"id":"_help_McngOG2jbUWX","title":"Search in note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Search in note"},{"name":"iconClass","value":"bx bx-search-alt-2","type":"label"}]}]},{"id":"_help_A9Oc6YKKc65v","title":"Keyboard Shortcuts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Keyboard Shortcuts"},{"name":"iconClass","value":"bx bxs-keyboard","type":"label"}]},{"id":"_help_Wy267RK4M69c","title":"Themes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Themes"},{"name":"iconClass","value":"bx bx-palette","type":"label"}],"children":[{"id":"_help_VbjZvtUek0Ln","title":"Theme Gallery","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Themes/Theme Gallery"},{"name":"iconClass","value":"bx bx-book-reader","type":"label"}]}]},{"id":"_help_mHbBMPDPkVV5","title":"Import & Export","type":"book","attributes":[{"name":"iconClass","value":"bx bx-import","type":"label"}],"children":[{"id":"_help_Oau6X9rCuegd","title":"Markdown","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export/Markdown"},{"name":"iconClass","value":"bx bxl-markdown","type":"label"}]},{"id":"_help_syuSEKf2rUGr","title":"Evernote","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export/Evernote"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_GnhlmrATVqcH","title":"OneNote","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export/OneNote"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_rC3pL2aptaRE","title":"Zen mode","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Zen mode"},{"name":"iconClass","value":"bx bxs-yin-yang","type":"label"}]}]},{"id":"_help_s3YCWHBfmYuM","title":"Quick Start","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Quick Start"},{"name":"iconClass","value":"bx bx-run","type":"label"}]},{"id":"_help_i6dbnitykE5D","title":"FAQ","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/FAQ"},{"name":"iconClass","value":"bx bx-question-mark","type":"label"}]},{"id":"_help_KSZ04uQ2D1St","title":"Note Types","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types"},{"name":"iconClass","value":"bx bx-edit","type":"label"}],"children":[{"id":"_help_iPIMuisry3hd","title":"Text","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text"},{"name":"iconClass","value":"bx bx-note","type":"label"}],"children":[{"id":"_help_NwBbFdNZ9h7O","title":"Block quotes & admonitions","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Block quotes & admonitions"},{"name":"iconClass","value":"bx bx-info-circle","type":"label"}]},{"id":"_help_oSuaNgyyKnhu","title":"Bookmarks","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Bookmarks"},{"name":"iconClass","value":"bx bx-bookmark","type":"label"}]},{"id":"_help_veGu4faJErEM","title":"Content language & Right-to-left support","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Content language & Right-to-le"},{"name":"iconClass","value":"bx bx-align-right","type":"label"}]},{"id":"_help_2x0ZAX9ePtzV","title":"Cut to subnote","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Cut to subnote"},{"name":"iconClass","value":"bx bx-cut","type":"label"}]},{"id":"_help_UYuUB1ZekNQU","title":"Developer-specific formatting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Developer-specific formatting"},{"name":"iconClass","value":"bx bx-code-alt","type":"label"}],"children":[{"id":"_help_QxEyIjRBizuC","title":"Code blocks","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Developer-specific formatting/Code blocks"},{"name":"iconClass","value":"bx bx-code","type":"label"}]}]},{"id":"_help_AgjCISero73a","title":"Footnotes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Footnotes"},{"name":"iconClass","value":"bx bx-bracket","type":"label"}]},{"id":"_help_nRhnJkTT8cPs","title":"Formatting toolbar","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Formatting toolbar"},{"name":"iconClass","value":"bx bx-text","type":"label"}]},{"id":"_help_Gr6xFaF6ioJ5","title":"General formatting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/General formatting"},{"name":"iconClass","value":"bx bx-bold","type":"label"}]},{"id":"_help_AxshuNRegLAv","title":"Highlights list","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Highlights list"},{"name":"iconClass","value":"bx bx-highlight","type":"label"}]},{"id":"_help_mT0HEkOsz6i1","title":"Images","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Images"},{"name":"iconClass","value":"bx bx-image-alt","type":"label"}],"children":[{"id":"_help_0Ofbk1aSuVRu","title":"Image references","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Images/Image references"},{"name":"iconClass","value":"bx bxs-file-image","type":"label"}]}]},{"id":"_help_nBAXQFj20hS1","title":"Include Note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Include Note"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_CohkqWQC1iBv","title":"Insert buttons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Insert buttons"},{"name":"iconClass","value":"bx bx-plus","type":"label"}]},{"id":"_help_oiVPnW8QfnvS","title":"Keyboard shortcuts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Keyboard shortcuts"},{"name":"iconClass","value":"bx bxs-keyboard","type":"label"}]},{"id":"_help_QEAPj01N5f7w","title":"Links","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Links"},{"name":"iconClass","value":"bx bx-link-alt","type":"label"}]},{"id":"_help_S6Xx8QIWTV66","title":"Lists","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Lists"},{"name":"iconClass","value":"bx bx-list-ul","type":"label"}]},{"id":"_help_QrtTYPmdd1qq","title":"Markdown-like formatting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Markdown-like formatting"},{"name":"iconClass","value":"bx bxl-markdown","type":"label"}]},{"id":"_help_YfYAtQBcfo5V","title":"Math Equations","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Math Equations"},{"name":"iconClass","value":"bx bx-math","type":"label"}]},{"id":"_help_dEHYtoWWi8ct","title":"Other features","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Other features"},{"name":"iconClass","value":"bx bxs-grid","type":"label"}]},{"id":"_help_BFvAtE74rbP6","title":"Table of contents","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Table of contents"},{"name":"iconClass","value":"bx bx-heading","type":"label"}]},{"id":"_help_NdowYOC1GFKS","title":"Tables","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Tables"},{"name":"iconClass","value":"bx bx-table","type":"label"}]}]},{"id":"_help_6f9hih2hXXZk","title":"Code","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Code"},{"name":"iconClass","value":"bx bx-code","type":"label"}]},{"id":"_help_m523cpzocqaD","title":"Saved Search","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Saved Search"},{"name":"iconClass","value":"bx bx-file-find","type":"label"}]},{"id":"_help_iRwzGnHPzonm","title":"Relation Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Relation Map"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]},{"id":"_help_bdUJEHsAPYQR","title":"Note Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Note Map"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]},{"id":"_help_HcABDtFCkbFN","title":"Render Note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Render Note"},{"name":"iconClass","value":"bx bx-extension","type":"label"}]},{"id":"_help_GTwFsgaA0lCt","title":"Book","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Book"},{"name":"iconClass","value":"bx bx-book","type":"label"}]},{"id":"_help_s1aBHPd79XYj","title":"Mermaid Diagrams","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Mermaid Diagrams"},{"name":"iconClass","value":"bx bx-selection","type":"label"}],"children":[{"id":"_help_RH6yLjjWJHof","title":"ELK layout","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Mermaid Diagrams/ELK layout"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]}]},{"id":"_help_grjYqerjn243","title":"Canvas","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Canvas"},{"name":"iconClass","value":"bx bx-pen","type":"label"}]},{"id":"_help_1vHRoWCEjj0L","title":"Web View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Web View"},{"name":"iconClass","value":"bx bx-globe-alt","type":"label"}]},{"id":"_help_gBbsAeiuUxI5","title":"Mind Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Mind Map"},{"name":"iconClass","value":"bx bx-sitemap","type":"label"}]},{"id":"_help_81SGnPGMk7Xc","title":"Geo Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Geo Map"},{"name":"iconClass","value":"bx bx-map-alt","type":"label"}]},{"id":"_help_W8vYD3Q1zjCR","title":"File","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/File"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_BgmBlOIl72jZ","title":"Troubleshooting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting"},{"name":"iconClass","value":"bx bx-bug","type":"label"}],"children":[{"id":"_help_wy8So3yZZlH9","title":"Reporting issues","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Reporting issues"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_x59R8J8KV5Bp","title":"Anonymized Database","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Anonymized Database"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_qzNzp9LYQyPT","title":"Error logs","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Error logs"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_vdlYGAcpXAgc","title":"Synchronization fails with 504 Gateway Timeout","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Synchronization fails with 504"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_s8alTXmpFR61","title":"Refreshing the application","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Refreshing the application"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_pKK96zzmvBGf","title":"Theme development","type":"book","attributes":[{"name":"iconClass","value":"bx bx-palette","type":"label"}],"children":[{"id":"_help_7NfNr5pZpVKV","title":"Creating a custom theme","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Creating a custom theme"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_WFGzWeUK6arS","title":"Customize the Next theme","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Customize the Next theme"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_WN5z4M8ASACJ","title":"Reference","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Reference"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_AlhDUqhENtH7","title":"Custom app-wide CSS","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Custom app-wide CSS"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_tC7s2alapj8V","title":"Advanced Usage","type":"book","attributes":[{"name":"iconClass","value":"bx bx-rocket","type":"label"}],"children":[{"id":"_help_zEY4DaJG4YT5","title":"Attributes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes"},{"name":"iconClass","value":"bx bx-list-check","type":"label"}],"children":[{"id":"_help_HI6GBBIduIgv","title":"Labels","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Labels"},{"name":"iconClass","value":"bx bx-hash","type":"label"}]},{"id":"_help_Cq5X6iKQop6R","title":"Relations","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Relations"},{"name":"iconClass","value":"bx bx-transfer","type":"label"}]},{"id":"_help_bwZpz2ajCEwO","title":"Attribute Inheritance","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Attribute Inheritance"},{"name":"iconClass","value":"bx bx-list-plus","type":"label"}]},{"id":"_help_OFXdgB2nNk1F","title":"Promoted Attributes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Promoted Attributes"},{"name":"iconClass","value":"bx bx-table","type":"label"}]}]},{"id":"_help_KC1HB96bqqHX","title":"Templates","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Templates"},{"name":"iconClass","value":"bx bx-copy","type":"label"}]},{"id":"_help_BCkXAVs63Ttv","title":"Note Map (Link map, Tree map)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Note Map (Link map, Tree map)"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]},{"id":"_help_R9pX4DGra2Vt","title":"Sharing","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Sharing"},{"name":"iconClass","value":"bx bx-share-alt","type":"label"}],"children":[{"id":"_help_Qjt68inQ2bRj","title":"Serving directly the content of a note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Sharing/Serving directly the content o"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_5668rwcirq1t","title":"Advanced Showcases","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_l0tKav7yLHGF","title":"Day Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes"},{"name":"iconClass","value":"bx bx-calendar","type":"label"}]},{"id":"_help_R7abl2fc6Mxi","title":"Weight Tracker","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases/Weight Tracker"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_xYjQUYhpbUEW","title":"Task Manager","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases/Task Manager"},{"name":"iconClass","value":"bx bx-calendar-check","type":"label"}]}]},{"id":"_help_J5Ex1ZrMbyJ6","title":"Custom Request Handler","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Custom Request Handler"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_d3fAXQ2diepH","title":"Custom Resource Providers","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Custom Resource Providers"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_pgxEVkzLl1OP","title":"ETAPI (REST API)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/ETAPI (REST API)"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_9qPsTWBorUhQ","title":"API Reference","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"/etapi/docs"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_47ZrP6FNuoG8","title":"Default Note Title","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Default Note Title"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_wX4HbRucYSDD","title":"Database","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database"},{"name":"iconClass","value":"bx bx-data","type":"label"}],"children":[{"id":"_help_oyIAJ9PvvwHX","title":"Manually altering the database","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database/Manually altering the database"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_YKWqdJhzi2VY","title":"SQL Console","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database/Manually altering the database/SQL Console"},{"name":"iconClass","value":"bx bx-data","type":"label"}]}]},{"id":"_help_6tZeKvSHEUiB","title":"Demo Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database/Demo Notes"},{"name":"iconClass","value":"bx bx-package","type":"label"}]}]},{"id":"_help_Gzjqa934BdH4","title":"Configuration (config.ini or environment variables)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Configuration (config.ini or e"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_c5xB8m4g2IY6","title":"Trilium instance","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Configuration (config.ini or environment variables)/Trilium instance"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_LWtBjFej3wX3","title":"Cross-Origin Resource Sharing (CORS)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Configuration (config.ini or environment variables)/Cross-Origin Resource Sharing "},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_ivYnonVFBxbQ","title":"Bulk Actions","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Bulk Actions"},{"name":"iconClass","value":"bx bx-list-plus","type":"label"}]},{"id":"_help_4FahAwuGTAwC","title":"Note source","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Note source"},{"name":"iconClass","value":"bx bx-code","type":"label"}]},{"id":"_help_1YeN2MzFUluU","title":"Technologies used","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used"},{"name":"iconClass","value":"bx bxs-component","type":"label"}],"children":[{"id":"_help_MI26XDLSAlCD","title":"CKEditor","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/CKEditor"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_N4IDkixaDG9C","title":"MindElixir","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/MindElixir"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_H0mM1lTxF9JI","title":"Excalidraw","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/Excalidraw"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_MQHyy2dIFgxS","title":"Leaflet","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/Leaflet"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_m1lbrzyKDaRB","title":"Note ID","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Note ID"},{"name":"iconClass","value":"bx bx-hash","type":"label"}]},{"id":"_help_0vTSyvhPTAOz","title":"Internal API","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_z8O2VG4ZZJD7","title":"API Reference","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"/api/docs"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_2mUhVmZK8RF3","title":"Hidden Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Hidden Notes"},{"name":"iconClass","value":"bx bx-hide","type":"label"}]},{"id":"_help_uYF7pmepw27K","title":"Metrics","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Metrics"},{"name":"iconClass","value":"bx bxs-data","type":"label"}]}]},{"id":"_help_LMAv4Uy3Wk6J","title":"AI","type":"book","attributes":[{"name":"iconClass","value":"bx bx-bot","type":"label"}],"children":[{"id":"_help_GBBMSlVSOIGP","title":"Introduction","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/Introduction"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_WkM7gsEUyCXs","title":"AI Provider Information","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/AI Provider Information"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_7EdTxPADv95W","title":"Ollama","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_vvUCN7FDkq7G","title":"Installing Ollama","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/AI Provider Information/Ollama/Installing Ollama"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_ZavFigBX9AwP","title":"OpenAI","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/AI Provider Information/OpenAI"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_e0lkirXEiSNc","title":"Anthropic","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/AI Provider Information/Anthropic"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]}]},{"id":"_help_CdNpE2pqjmI6","title":"Scripting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting"},{"name":"iconClass","value":"bx bxs-file-js","type":"label"}],"children":[{"id":"_help_yIhgI5H7A2Sm","title":"Frontend Basics","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_es8OU2GuguFU","title":"Examples","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_TjLYAo3JMO8X","title":"\"New Task\" launcher button","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Examples/New Task launcher button"},{"name":"iconClass","value":"bx bx-task","type":"label"}]},{"id":"_help_7kZPMD0uFwkH","title":"Downloading responses from Google Forms","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Examples/Downloading responses from Goo"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_DL92EjAaXT26","title":"Using promoted attributes to configure scripts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Examples/Using promoted attributes to c"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_GPERMystNGTB","title":"Events","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Events"},{"name":"iconClass","value":"bx bx-rss","type":"label"}]},{"id":"_help_MgibgPcfeuGz","title":"Custom Widgets","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Custom Widgets"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_YNxAqkI5Kg1M","title":"Word count widget","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Custom Widgets/Word count widget"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_SynTBQiBsdYJ","title":"Widget Basics","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Custom Widgets/Widget Basics"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_GLks18SNjxmC","title":"Script API","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Script API"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_Q2z6av6JZVWm","title":"Frontend API","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://triliumnext.github.io/Notes/Script%20API/interfaces/Frontend_Script_API.Api.html"},{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_habiZ3HU8Kw8","title":"FNote","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://triliumnext.github.io/Notes/Script%20API/classes/Frontend_Script_API.FNote.html"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_MEtfsqa5VwNi","title":"Backend API","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://triliumnext.github.io/Notes/Script%20API/interfaces/Backend_Script_API.Api.html"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]}]}] \ No newline at end of file +[{"id":"_help_Otzi9La2YAUX","title":"Installation & Setup","type":"book","attributes":[{"name":"iconClass","value":"bx bx-cog","type":"label"}],"children":[{"id":"_help_poXkQfguuA0U","title":"Desktop Installation","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Desktop Installation"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_WOcw2SLH6tbX","title":"Server Installation","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_Dgg7bR3b6K9j","title":"1. Installing the server","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_3tW6mORuTHnB","title":"Packaged version for Linux","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Packaged version for Linux"},{"name":"iconClass","value":"bx bxl-tux","type":"label"}]},{"id":"_help_rWX5eY045zbE","title":"Using Docker","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Using Docker"},{"name":"iconClass","value":"bx bxl-docker","type":"label"}]},{"id":"_help_moVgBcoxE3EK","title":"On NixOS","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/On NixOS"},{"name":"iconClass","value":"bx bxl-tux","type":"label"}]},{"id":"_help_J1Bb6lVlwU5T","title":"Manually","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Manually"},{"name":"iconClass","value":"bx bx-code-alt","type":"label"}]},{"id":"_help_DCmT6e7clMoP","title":"Using Kubernetes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Using Kubernetes"},{"name":"iconClass","value":"bx bxl-kubernetes","type":"label"}]},{"id":"_help_klCWNks3ReaQ","title":"Multiple server instances","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/1. Installing the server/Multiple server instances"},{"name":"iconClass","value":"bx bxs-user-account","type":"label"}]}]},{"id":"_help_vcjrb3VVYPZI","title":"2. Reverse proxy","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_ud6MShXL4WpO","title":"Nginx","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/2. Reverse proxy/Nginx"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_fDLvzOx29Pfg","title":"Apache","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/2. Reverse proxy/Apache"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_l2VkvOwUNfZj","title":"TLS Configuration","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Server Installation/TLS Configuration"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_cbkrhQjrkKrh","title":"Synchronization","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Synchronization"},{"name":"iconClass","value":"bx bx-sync","type":"label"}]},{"id":"_help_RDslemsQ6gCp","title":"Mobile Frontend","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Mobile Frontend"},{"name":"iconClass","value":"bx bx-mobile-alt","type":"label"}]},{"id":"_help_MtPxeAWVAzMg","title":"Web Clipper","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Web Clipper"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_n1lujUxCwipy","title":"Upgrading TriliumNext","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Upgrading TriliumNext"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_ODY7qQn5m2FT","title":"Backup","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Backup"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_tAassRL4RSQL","title":"Data directory","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Installation & Setup/Data directory"},{"name":"iconClass","value":"bx bx-folder-open","type":"label"}]}]},{"id":"_help_gh7bpGYxajRS","title":"Basic Concepts and Features","type":"book","attributes":[{"name":"iconClass","value":"bx bx-help-circle","type":"label"}],"children":[{"id":"_help_Vc8PjrjAGuOp","title":"UI Elements","type":"book","attributes":[{"name":"iconClass","value":"bx bx-window-alt","type":"label"}],"children":[{"id":"_help_x0JgW8UqGXvq","title":"Vertical and horizontal layout","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Vertical and horizontal layout"},{"name":"iconClass","value":"bx bxs-layout","type":"label"}]},{"id":"_help_x3i7MxGccDuM","title":"Global menu","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Global menu"},{"name":"iconClass","value":"bx bx-menu","type":"label"}]},{"id":"_help_oPVyFC7WL2Lp","title":"Note Tree","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree"},{"name":"iconClass","value":"bx bxs-tree-alt","type":"label"}],"children":[{"id":"_help_YtSN43OrfzaA","title":"Note tree contextual menu","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Note tree contextual menu"},{"name":"iconClass","value":"bx bx-menu","type":"label"}]},{"id":"_help_yTjUdsOi4CIE","title":"Multiple selection","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Multiple selection"},{"name":"iconClass","value":"bx bx-list-plus","type":"label"}]}]},{"id":"_help_BlN9DFI679QC","title":"Ribbon","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Ribbon"},{"name":"iconClass","value":"bx bx-dots-horizontal","type":"label"}]},{"id":"_help_3seOhtN8uLIY","title":"Tabs","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Tabs"},{"name":"iconClass","value":"bx bx-dock-top","type":"label"}]},{"id":"_help_xYmIYSP6wE3F","title":"Launch Bar","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Launch Bar"},{"name":"iconClass","value":"bx bx-sidebar","type":"label"}]},{"id":"_help_8YBEPzcpUgxw","title":"Note buttons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Note buttons"},{"name":"iconClass","value":"bx bx-dots-vertical-rounded","type":"label"}]},{"id":"_help_4TIF1oA4VQRO","title":"Options","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Options"},{"name":"iconClass","value":"bx bx-cog","type":"label"}]},{"id":"_help_luNhaphA37EO","title":"Split View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Split View"},{"name":"iconClass","value":"bx bx-dock-right","type":"label"}]},{"id":"_help_XpOYSgsLkTJy","title":"Floating buttons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Floating buttons"},{"name":"iconClass","value":"bx bx-rectangle","type":"label"}]},{"id":"_help_RnaPdbciOfeq","title":"Right Sidebar","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Right Sidebar"},{"name":"iconClass","value":"bx bxs-dock-right","type":"label"}]},{"id":"_help_r5JGHN99bVKn","title":"Recent Changes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Recent Changes"},{"name":"iconClass","value":"bx bx-history","type":"label"}]},{"id":"_help_ny318J39E5Z0","title":"Zoom","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/UI Elements/Zoom"},{"name":"iconClass","value":"bx bx-zoom-in","type":"label"}]}]},{"id":"_help_BFs8mudNFgCS","title":"Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes"},{"name":"iconClass","value":"bx bx-notepad","type":"label"}],"children":[{"id":"_help_p9kXRFAkwN4o","title":"Note Icons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Note Icons"},{"name":"iconClass","value":"bx bxs-grid","type":"label"}]},{"id":"_help_0vhv7lsOLy82","title":"Attachments","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Attachments"},{"name":"iconClass","value":"bx bx-paperclip","type":"label"}]},{"id":"_help_IakOLONlIfGI","title":"Cloning Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Cloning Notes"},{"name":"iconClass","value":"bx bx-duplicate","type":"label"}],"children":[{"id":"_help_TBwsyfadTA18","title":"Branch prefix","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Cloning Notes/Branch prefix"},{"name":"iconClass","value":"bx bx-rename","type":"label"}]}]},{"id":"_help_bwg0e8ewQMak","title":"Protected Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Protected Notes"},{"name":"iconClass","value":"bx bx-lock-alt","type":"label"}]},{"id":"_help_MKmLg5x6xkor","title":"Archived Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Archived Notes"},{"name":"iconClass","value":"bx bx-box","type":"label"}]},{"id":"_help_vZWERwf8U3nx","title":"Note Revisions","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Note Revisions"},{"name":"iconClass","value":"bx bx-history","type":"label"}]},{"id":"_help_aGlEvb9hyDhS","title":"Sorting Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Sorting Notes"},{"name":"iconClass","value":"bx bx-sort-up","type":"label"}]},{"id":"_help_NRnIZmSMc5sj","title":"Export as PDF","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Export as PDF"},{"name":"iconClass","value":"bx bxs-file-pdf","type":"label"}]},{"id":"_help_CoFPLs3dRlXc","title":"Read-Only Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Read-Only Notes"},{"name":"iconClass","value":"bx bx-edit-alt","type":"label"}]},{"id":"_help_0ESUbbAxVnoK","title":"Note List","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Note List"},{"name":"iconClass","value":"bx bxs-grid","type":"label"}],"children":[{"id":"_help_xWbu3jpNWapp","title":"Calendar View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Notes/Note List/Calendar View"},{"name":"iconClass","value":"bx bx-calendar","type":"label"}]}]}]},{"id":"_help_wArbEsdSae6g","title":"Navigation","type":"book","attributes":[{"name":"iconClass","value":"bx bx-navigation","type":"label"}],"children":[{"id":"_help_kBrnXNG3Hplm","title":"Tree Concepts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Tree Concepts"},{"name":"iconClass","value":"bx bx-pyramid","type":"label"}]},{"id":"_help_MMiBEQljMQh2","title":"Note Navigation","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Note Navigation"},{"name":"iconClass","value":"bx bxs-navigation","type":"label"}]},{"id":"_help_Ms1nauBra7gq","title":"Quick search","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Quick search"},{"name":"iconClass","value":"bx bx-search-alt-2","type":"label"}]},{"id":"_help_F1r9QtzQLZqm","title":"Jump to Note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Jump to Note"},{"name":"iconClass","value":"bx bx-send","type":"label"}]},{"id":"_help_eIg8jdvaoNNd","title":"Search","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Search"},{"name":"iconClass","value":"bx bx-search-alt-2","type":"label"}]},{"id":"_help_u3YFHC9tQlpm","title":"Bookmarks","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Bookmarks"},{"name":"iconClass","value":"bx bx-bookmarks","type":"label"}]},{"id":"_help_OR8WJ7Iz9K4U","title":"Note Hoisting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Note Hoisting"},{"name":"iconClass","value":"bx bxs-chevrons-up","type":"label"}]},{"id":"_help_9sRHySam5fXb","title":"Workspaces","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Workspaces"},{"name":"iconClass","value":"bx bx-door-open","type":"label"}]},{"id":"_help_xWtq5NUHOwql","title":"Similar Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Similar Notes"},{"name":"iconClass","value":"bx bx-bar-chart","type":"label"}]},{"id":"_help_McngOG2jbUWX","title":"Search in note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Navigation/Search in note"},{"name":"iconClass","value":"bx bx-search-alt-2","type":"label"}]}]},{"id":"_help_A9Oc6YKKc65v","title":"Keyboard Shortcuts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Keyboard Shortcuts"},{"name":"iconClass","value":"bx bxs-keyboard","type":"label"}]},{"id":"_help_Wy267RK4M69c","title":"Themes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Themes"},{"name":"iconClass","value":"bx bx-palette","type":"label"}],"children":[{"id":"_help_VbjZvtUek0Ln","title":"Theme Gallery","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Themes/Theme Gallery"},{"name":"iconClass","value":"bx bx-book-reader","type":"label"}]}]},{"id":"_help_mHbBMPDPkVV5","title":"Import & Export","type":"book","attributes":[{"name":"iconClass","value":"bx bx-import","type":"label"}],"children":[{"id":"_help_Oau6X9rCuegd","title":"Markdown","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export/Markdown"},{"name":"iconClass","value":"bx bxl-markdown","type":"label"}]},{"id":"_help_syuSEKf2rUGr","title":"Evernote","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export/Evernote"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_GnhlmrATVqcH","title":"OneNote","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Import & Export/OneNote"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_rC3pL2aptaRE","title":"Zen mode","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Basic Concepts and Features/Zen mode"},{"name":"iconClass","value":"bx bxs-yin-yang","type":"label"}]}]},{"id":"_help_s3YCWHBfmYuM","title":"Quick Start","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Quick Start"},{"name":"iconClass","value":"bx bx-run","type":"label"}]},{"id":"_help_i6dbnitykE5D","title":"FAQ","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/FAQ"},{"name":"iconClass","value":"bx bx-question-mark","type":"label"}]},{"id":"_help_KSZ04uQ2D1St","title":"Note Types","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types"},{"name":"iconClass","value":"bx bx-edit","type":"label"}],"children":[{"id":"_help_iPIMuisry3hd","title":"Text","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text"},{"name":"iconClass","value":"bx bx-note","type":"label"}],"children":[{"id":"_help_NwBbFdNZ9h7O","title":"Block quotes & admonitions","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Block quotes & admonitions"},{"name":"iconClass","value":"bx bx-info-circle","type":"label"}]},{"id":"_help_oSuaNgyyKnhu","title":"Bookmarks","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Bookmarks"},{"name":"iconClass","value":"bx bx-bookmark","type":"label"}]},{"id":"_help_veGu4faJErEM","title":"Content language & Right-to-left support","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Content language & Right-to-le"},{"name":"iconClass","value":"bx bx-align-right","type":"label"}]},{"id":"_help_2x0ZAX9ePtzV","title":"Cut to subnote","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Cut to subnote"},{"name":"iconClass","value":"bx bx-cut","type":"label"}]},{"id":"_help_UYuUB1ZekNQU","title":"Developer-specific formatting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Developer-specific formatting"},{"name":"iconClass","value":"bx bx-code-alt","type":"label"}],"children":[{"id":"_help_QxEyIjRBizuC","title":"Code blocks","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Developer-specific formatting/Code blocks"},{"name":"iconClass","value":"bx bx-code","type":"label"}]}]},{"id":"_help_AgjCISero73a","title":"Footnotes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Footnotes"},{"name":"iconClass","value":"bx bx-bracket","type":"label"}]},{"id":"_help_nRhnJkTT8cPs","title":"Formatting toolbar","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Formatting toolbar"},{"name":"iconClass","value":"bx bx-text","type":"label"}]},{"id":"_help_Gr6xFaF6ioJ5","title":"General formatting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/General formatting"},{"name":"iconClass","value":"bx bx-bold","type":"label"}]},{"id":"_help_AxshuNRegLAv","title":"Highlights list","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Highlights list"},{"name":"iconClass","value":"bx bx-highlight","type":"label"}]},{"id":"_help_mT0HEkOsz6i1","title":"Images","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Images"},{"name":"iconClass","value":"bx bx-image-alt","type":"label"}],"children":[{"id":"_help_0Ofbk1aSuVRu","title":"Image references","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Images/Image references"},{"name":"iconClass","value":"bx bxs-file-image","type":"label"}]}]},{"id":"_help_nBAXQFj20hS1","title":"Include Note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Include Note"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_CohkqWQC1iBv","title":"Insert buttons","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Insert buttons"},{"name":"iconClass","value":"bx bx-plus","type":"label"}]},{"id":"_help_oiVPnW8QfnvS","title":"Keyboard shortcuts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Keyboard shortcuts"},{"name":"iconClass","value":"bx bxs-keyboard","type":"label"}]},{"id":"_help_QEAPj01N5f7w","title":"Links","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Links"},{"name":"iconClass","value":"bx bx-link-alt","type":"label"}]},{"id":"_help_S6Xx8QIWTV66","title":"Lists","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Lists"},{"name":"iconClass","value":"bx bx-list-ul","type":"label"}]},{"id":"_help_QrtTYPmdd1qq","title":"Markdown-like formatting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Markdown-like formatting"},{"name":"iconClass","value":"bx bxl-markdown","type":"label"}]},{"id":"_help_YfYAtQBcfo5V","title":"Math Equations","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Math Equations"},{"name":"iconClass","value":"bx bx-math","type":"label"}]},{"id":"_help_dEHYtoWWi8ct","title":"Other features","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Other features"},{"name":"iconClass","value":"bx bxs-grid","type":"label"}]},{"id":"_help_BFvAtE74rbP6","title":"Table of contents","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Table of contents"},{"name":"iconClass","value":"bx bx-heading","type":"label"}]},{"id":"_help_NdowYOC1GFKS","title":"Tables","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Text/Tables"},{"name":"iconClass","value":"bx bx-table","type":"label"}]}]},{"id":"_help_6f9hih2hXXZk","title":"Code","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Code"},{"name":"iconClass","value":"bx bx-code","type":"label"}]},{"id":"_help_m523cpzocqaD","title":"Saved Search","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Saved Search"},{"name":"iconClass","value":"bx bx-file-find","type":"label"}]},{"id":"_help_iRwzGnHPzonm","title":"Relation Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Relation Map"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]},{"id":"_help_bdUJEHsAPYQR","title":"Note Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Note Map"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]},{"id":"_help_HcABDtFCkbFN","title":"Render Note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Render Note"},{"name":"iconClass","value":"bx bx-extension","type":"label"}]},{"id":"_help_GTwFsgaA0lCt","title":"Book","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Book"},{"name":"iconClass","value":"bx bx-book","type":"label"}]},{"id":"_help_s1aBHPd79XYj","title":"Mermaid Diagrams","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Mermaid Diagrams"},{"name":"iconClass","value":"bx bx-selection","type":"label"}],"children":[{"id":"_help_RH6yLjjWJHof","title":"ELK layout","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Mermaid Diagrams/ELK layout"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]}]},{"id":"_help_grjYqerjn243","title":"Canvas","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Canvas"},{"name":"iconClass","value":"bx bx-pen","type":"label"}]},{"id":"_help_1vHRoWCEjj0L","title":"Web View","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Web View"},{"name":"iconClass","value":"bx bx-globe-alt","type":"label"}]},{"id":"_help_gBbsAeiuUxI5","title":"Mind Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Mind Map"},{"name":"iconClass","value":"bx bx-sitemap","type":"label"}]},{"id":"_help_81SGnPGMk7Xc","title":"Geo Map","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/Geo Map"},{"name":"iconClass","value":"bx bx-map-alt","type":"label"}]},{"id":"_help_W8vYD3Q1zjCR","title":"File","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Note Types/File"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_BgmBlOIl72jZ","title":"Troubleshooting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting"},{"name":"iconClass","value":"bx bx-bug","type":"label"}],"children":[{"id":"_help_wy8So3yZZlH9","title":"Reporting issues","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Reporting issues"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_x59R8J8KV5Bp","title":"Anonymized Database","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Anonymized Database"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_qzNzp9LYQyPT","title":"Error logs","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Error logs"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_vdlYGAcpXAgc","title":"Synchronization fails with 504 Gateway Timeout","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Synchronization fails with 504"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_s8alTXmpFR61","title":"Refreshing the application","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Troubleshooting/Refreshing the application"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_pKK96zzmvBGf","title":"Theme development","type":"book","attributes":[{"name":"iconClass","value":"bx bx-palette","type":"label"}],"children":[{"id":"_help_7NfNr5pZpVKV","title":"Creating a custom theme","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Creating a custom theme"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_WFGzWeUK6arS","title":"Customize the Next theme","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Customize the Next theme"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_WN5z4M8ASACJ","title":"Reference","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Reference"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_AlhDUqhENtH7","title":"Custom app-wide CSS","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Theme development/Custom app-wide CSS"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_tC7s2alapj8V","title":"Advanced Usage","type":"book","attributes":[{"name":"iconClass","value":"bx bx-rocket","type":"label"}],"children":[{"id":"_help_zEY4DaJG4YT5","title":"Attributes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes"},{"name":"iconClass","value":"bx bx-list-check","type":"label"}],"children":[{"id":"_help_HI6GBBIduIgv","title":"Labels","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Labels"},{"name":"iconClass","value":"bx bx-hash","type":"label"}]},{"id":"_help_Cq5X6iKQop6R","title":"Relations","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Relations"},{"name":"iconClass","value":"bx bx-transfer","type":"label"}]},{"id":"_help_bwZpz2ajCEwO","title":"Attribute Inheritance","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Attribute Inheritance"},{"name":"iconClass","value":"bx bx-list-plus","type":"label"}]},{"id":"_help_OFXdgB2nNk1F","title":"Promoted Attributes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Attributes/Promoted Attributes"},{"name":"iconClass","value":"bx bx-table","type":"label"}]}]},{"id":"_help_KC1HB96bqqHX","title":"Templates","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Templates"},{"name":"iconClass","value":"bx bx-copy","type":"label"}]},{"id":"_help_BCkXAVs63Ttv","title":"Note Map (Link map, Tree map)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Note Map (Link map, Tree map)"},{"name":"iconClass","value":"bx bxs-network-chart","type":"label"}]},{"id":"_help_R9pX4DGra2Vt","title":"Sharing","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Sharing"},{"name":"iconClass","value":"bx bx-share-alt","type":"label"}],"children":[{"id":"_help_Qjt68inQ2bRj","title":"Serving directly the content of a note","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Sharing/Serving directly the content o"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_5668rwcirq1t","title":"Advanced Showcases","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_l0tKav7yLHGF","title":"Day Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes"},{"name":"iconClass","value":"bx bx-calendar","type":"label"}]},{"id":"_help_R7abl2fc6Mxi","title":"Weight Tracker","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases/Weight Tracker"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_xYjQUYhpbUEW","title":"Task Manager","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Advanced Showcases/Task Manager"},{"name":"iconClass","value":"bx bx-calendar-check","type":"label"}]}]},{"id":"_help_J5Ex1ZrMbyJ6","title":"Custom Request Handler","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Custom Request Handler"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_d3fAXQ2diepH","title":"Custom Resource Providers","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Custom Resource Providers"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_pgxEVkzLl1OP","title":"ETAPI (REST API)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/ETAPI (REST API)"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_9qPsTWBorUhQ","title":"API Reference","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"/etapi/docs"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_47ZrP6FNuoG8","title":"Default Note Title","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Default Note Title"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_wX4HbRucYSDD","title":"Database","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database"},{"name":"iconClass","value":"bx bx-data","type":"label"}],"children":[{"id":"_help_oyIAJ9PvvwHX","title":"Manually altering the database","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database/Manually altering the database"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_YKWqdJhzi2VY","title":"SQL Console","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database/Manually altering the database/SQL Console"},{"name":"iconClass","value":"bx bx-data","type":"label"}]}]},{"id":"_help_6tZeKvSHEUiB","title":"Demo Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Database/Demo Notes"},{"name":"iconClass","value":"bx bx-package","type":"label"}]}]},{"id":"_help_Gzjqa934BdH4","title":"Configuration (config.ini or environment variables)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Configuration (config.ini or e"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_c5xB8m4g2IY6","title":"Trilium instance","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Configuration (config.ini or environment variables)/Trilium instance"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_LWtBjFej3wX3","title":"Cross-Origin Resource Sharing (CORS)","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Configuration (config.ini or environment variables)/Cross-Origin Resource Sharing "},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_ivYnonVFBxbQ","title":"Bulk Actions","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Bulk Actions"},{"name":"iconClass","value":"bx bx-list-plus","type":"label"}]},{"id":"_help_4FahAwuGTAwC","title":"Note source","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Note source"},{"name":"iconClass","value":"bx bx-code","type":"label"}]},{"id":"_help_1YeN2MzFUluU","title":"Technologies used","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used"},{"name":"iconClass","value":"bx bxs-component","type":"label"}],"children":[{"id":"_help_MI26XDLSAlCD","title":"CKEditor","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/CKEditor"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_N4IDkixaDG9C","title":"MindElixir","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/MindElixir"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_H0mM1lTxF9JI","title":"Excalidraw","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/Excalidraw"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_MQHyy2dIFgxS","title":"Leaflet","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Technologies used/Leaflet"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_m1lbrzyKDaRB","title":"Note ID","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Note ID"},{"name":"iconClass","value":"bx bx-hash","type":"label"}]},{"id":"_help_0vTSyvhPTAOz","title":"Internal API","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_z8O2VG4ZZJD7","title":"API Reference","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"/api/docs"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_2mUhVmZK8RF3","title":"Hidden Notes","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Hidden Notes"},{"name":"iconClass","value":"bx bx-hide","type":"label"}]},{"id":"_help_uYF7pmepw27K","title":"Metrics","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Advanced Usage/Metrics"},{"name":"iconClass","value":"bx bxs-data","type":"label"}],"children":[{"id":"_help_bOP3TB56fL1V","title":"grafana-dashboard.json","type":"doc","attributes":[{"name":"iconClass","value":"bx bx-file","type":"label"}]}]}]},{"id":"_help_LMAv4Uy3Wk6J","title":"AI","type":"book","attributes":[{"name":"iconClass","value":"bx bx-bot","type":"label"}],"children":[{"id":"_help_GBBMSlVSOIGP","title":"Introduction","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/Introduction"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_WkM7gsEUyCXs","title":"AI Provider Information","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/AI Provider Information"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_7EdTxPADv95W","title":"Ollama","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_vvUCN7FDkq7G","title":"Installing Ollama","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/AI Provider Information/Ollama/Installing Ollama"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_ZavFigBX9AwP","title":"OpenAI","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/AI Provider Information/OpenAI"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_e0lkirXEiSNc","title":"Anthropic","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/AI/AI Provider Information/Anthropic"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]}]},{"id":"_help_CdNpE2pqjmI6","title":"Scripting","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting"},{"name":"iconClass","value":"bx bxs-file-js","type":"label"}],"children":[{"id":"_help_yIhgI5H7A2Sm","title":"Frontend Basics","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Frontend Basics"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_es8OU2GuguFU","title":"Examples","type":"book","attributes":[{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_TjLYAo3JMO8X","title":"\"New Task\" launcher button","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Examples/New Task launcher button"},{"name":"iconClass","value":"bx bx-task","type":"label"}]},{"id":"_help_7kZPMD0uFwkH","title":"Downloading responses from Google Forms","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Examples/Downloading responses from Goo"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_DL92EjAaXT26","title":"Using promoted attributes to configure scripts","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Examples/Using promoted attributes to c"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_GPERMystNGTB","title":"Events","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Events"},{"name":"iconClass","value":"bx bx-rss","type":"label"}]},{"id":"_help_MgibgPcfeuGz","title":"Custom Widgets","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Custom Widgets"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_YNxAqkI5Kg1M","title":"Word count widget","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Custom Widgets/Word count widget"},{"name":"iconClass","value":"bx bx-file","type":"label"}]},{"id":"_help_SynTBQiBsdYJ","title":"Widget Basics","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Custom Widgets/Widget Basics"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_GLks18SNjxmC","title":"Script API","type":"doc","attributes":[{"type":"label","name":"docName","value":"User Guide/User Guide/Scripting/Script API"},{"name":"iconClass","value":"bx bx-file","type":"label"}],"children":[{"id":"_help_Q2z6av6JZVWm","title":"Frontend API","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://triliumnext.github.io/Notes/Script%20API/interfaces/Frontend_Script_API.Api.html"},{"name":"iconClass","value":"bx bx-folder","type":"label"}],"children":[{"id":"_help_habiZ3HU8Kw8","title":"FNote","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://triliumnext.github.io/Notes/Script%20API/classes/Frontend_Script_API.FNote.html"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]},{"id":"_help_MEtfsqa5VwNi","title":"Backend API","type":"webView","attributes":[{"type":"label","name":"webViewSrc","value":"https://triliumnext.github.io/Notes/Script%20API/interfaces/Backend_Script_API.Api.html"},{"name":"iconClass","value":"bx bx-file","type":"label"}]}]}]}] \ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/1_Metrics_image.png b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/1_Metrics_image.png new file mode 100644 index 000000000..683789547 Binary files /dev/null and b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/1_Metrics_image.png differ diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/2_Metrics_image.png b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/2_Metrics_image.png new file mode 100644 index 000000000..08181d986 Binary files /dev/null and b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/2_Metrics_image.png differ diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes.html index 9b1b80b2b..2bfdb4108 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Advanced Showcases/Day Notes.html @@ -28,7 +28,7 @@ where you can track your daily weight. This data is then used in Weight tracker.

                  Week Note and Quarter Note

                  Week and quarter notes are disabled by default, since it might be too - much for some people. To enable them, you need to set #enableWeekNotes and #enableQuarterNotes attributes + much for some people. To enable them, you need to set #enableWeekNote and #enableQuarterNote attributes on the root calendar note, which is identified by #calendarRoot label. Week note is affected by the first week of year option. Be careful when you already have some week notes created, it will not automatically change @@ -40,15 +40,26 @@ (identified by #calendarRoot label):

                  • yearTemplate
                  • -
                  • quarterTemplate (if #enableQuarterNotes is set)
                  • +
                  • quarterTemplate (if #enableQuarterNote is set)
                  • monthTemplate
                  • -
                  • weekTemplate (if #enableWeekNotes is set)
                  • +
                  • weekTemplate (if #enableWeekNote is set)
                  • dateTemplate

                  All of these are relations. When Trilium creates a new note for year or month or date, it will take a look at the root and attach a corresponding ~template relation to the newly created role. Using this, you can e.g. create your daily template with e.g. checkboxes for daily routine etc.

                  +

                  Migrate from old template usage

                  +

                  If you have been using Journal prior to version v0.93.0, the previous + template pattern likely used was ~child:template=. +
                  To transition to the new system:

                  +
                    +
                  1. Set up the new template pattern in the Calendar root note.
                  2. +
                  3. Use Bulk Actions to remove child:template and child:child:template from + all notes under the Journal (calendar root).
                  4. +
                  5. Ensure that all old template patterns are fully removed to prevent conflicts + with the new setup.
                  6. +

                  Naming pattern

                  You can customize the title of generated journal notes by defining a #datePattern, #weekPattern, #monthPattern, #quarterPattern and #yearPattern attribute on a root calendar note (identified by #calendarRoot label). @@ -138,9 +149,4 @@

                  Trilium has some special support for day notes in the form of backend Script API - see e.g. getDayNote() function.

                  Day (and year, month) notes are created with a label - e.g. #dateNote="2025-03-09" this - can then be used by other scripts to add new notes to day note etc.

                  -

                  Journal also has relation child:child:child:template=Day template (see - [[attribute inheritance]]) which effectively adds [[template]] to day notes - (grand-grand-grand children of Journal). Please note that, when you enable - week notes or quarter notes, it will not automatically change the relation - for the child level.

                  \ No newline at end of file + can then be used by other scripts to add new notes to day note etc.

                  \ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Metrics.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Metrics.html index 805db1df2..7f2d204f9 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Metrics.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Metrics.html @@ -79,4 +79,24 @@ trilium_notes_total 1234 1701432000
                3. 400 - Invalid format parameter
                4. 401 - Missing or invalid ETAPI token
                5. 500 - Internal server error
                6. - \ No newline at end of file + +

                   

                  +

                  Grafana Dashboard

                  +
                  + +
                  +

                   

                  +

                  You can also use the Grafana Dashboard that has been created for TriliumNext + - just take the JSON from grafana-dashboard.json and + then import the dashboard, following these screenshots:

                  +
                  + +
                  +

                  Then paste the JSON, and hit load:

                  +
                  + +
                  +

                   

                  \ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Metrics/grafana-dashboard.json b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Metrics/grafana-dashboard.json new file mode 100644 index 000000000..2e1e4511e --- /dev/null +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Metrics/grafana-dashboard.json @@ -0,0 +1,1335 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": 549, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 100, + "panels": [], + "title": "🏠 Trilium Overview", + "type": "row" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "description": "Current Trilium version and build information", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 101, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_info{job=~'$job',instance=~'$instance'}", + "format": "table", + "instant": true, + "refId": "A" + } + ], + "title": "📋 Instance Information", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value": true, + "__name__": true, + "instance": true, + "job": true + }, + "indexByName": {}, + "renameByName": { + "build_date": "Build Date", + "build_revision": "Git Revision", + "db_version": "DB Version", + "node_version": "Node.js", + "sync_version": "Sync Version", + "version": "Version" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "description": "Database file size in human-readable format", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 500000000 + }, + { + "color": "red", + "value": 1000000000 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 12, + "y": 1 + }, + "id": 102, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "center", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_database_size_bytes{job=~'$job',instance=~'$instance'}", + "refId": "A" + } + ], + "title": "💾 Database Size", + "type": "stat" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "description": "Total active notes in your Trilium instance", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "yellow", + "value": 1000 + }, + { + "color": "red", + "value": 5000 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 1 + }, + "id": 103, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "center", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_notes_active{job=~'$job',instance=~'$instance'}", + "refId": "A" + } + ], + "title": "📝 Active Notes", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 7 + }, + "id": 200, + "panels": [], + "title": "📊 Key Metrics", + "type": "row" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "description": "Total notes including deleted ones", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 8 + }, + "id": 201, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "values": [] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_notes_active{job=~'$job',instance=~'$instance'}", + "legendFormat": "Active Notes", + "refId": "A" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_notes_deleted{job=~'$job',instance=~'$instance'}", + "legendFormat": "Deleted Notes", + "refId": "B" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_notes_protected{job=~'$job',instance=~'$instance'}", + "legendFormat": "Protected Notes", + "refId": "C" + } + ], + "title": "📝 Notes Distribution", + "type": "piechart" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "description": "Breakdown of attachments by MIME type", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 8 + }, + "id": 202, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "values": [] + }, + "pieType": "donut", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_attachments_by_type{job=~'$job',instance=~'$instance'}", + "legendFormat": "{{mime_type}}", + "refId": "A" + } + ], + "title": "🖼️ Attachments by Type", + "type": "piechart" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "description": "Distribution of notes by their content type", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 203, + "options": { + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "values": [ + "value", + "percent" + ] + }, + "pieType": "donut", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_notes_by_type{job=~'$job',instance=~'$instance'}", + "legendFormat": "{{type}}", + "refId": "A" + } + ], + "title": "📄 Notes by Content Type", + "type": "piechart" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 300, + "panels": [], + "title": "📈 Trends & Time Series", + "type": "row" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "description": "Growth of notes over time", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 8, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active Notes" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Notes" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 17 + }, + "id": 301, + "options": { + "legend": { + "calcs": [ + "last", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_notes_active{job=~'$job',instance=~'$instance'}", + "legendFormat": "Active Notes", + "refId": "A" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_notes_total{job=~'$job',instance=~'$instance'}", + "legendFormat": "Total Notes", + "refId": "B" + } + ], + "title": "📈 Notes Growth Over Time", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "description": "Attachment storage trends", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "hue", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 3, + "pointSize": 8, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active Attachments" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Attachments" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 17 + }, + "id": 302, + "options": { + "legend": { + "calcs": [ + "last", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_attachments_active{job=~'$job',instance=~'$instance'}", + "legendFormat": "Active Attachments", + "refId": "A" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_attachments_total{job=~'$job',instance=~'$instance'}", + "legendFormat": "Total Attachments", + "refId": "B" + } + ], + "title": "📎 Attachments Growth Over Time", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 25 + }, + "id": 400, + "panels": [], + "title": "🔧 Advanced Metrics", + "type": "row" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "description": "Number of branches connecting notes", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 26 + }, + "id": 401, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_branches_total{job=~'$job',instance=~'$instance'}", + "refId": "A" + } + ], + "title": "🌳 Total Branches", + "type": "stat" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "description": "Number of note attributes", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 26 + }, + "id": 402, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_attributes_total{job=~'$job',instance=~'$instance'}", + "refId": "A" + } + ], + "title": "🏷️ Attributes", + "type": "stat" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "description": "Number of note revisions", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 26 + }, + "id": 403, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_revisions_total{job=~'$job',instance=~'$instance'}", + "refId": "A" + } + ], + "title": "🔄 Revisions", + "type": "stat" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "description": "Number of ETAPI tokens", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 26 + }, + "id": 404, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_etapi_tokens_total{job=~'$job',instance=~'$instance'}", + "refId": "A" + } + ], + "title": "🔑 API Tokens", + "type": "stat" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "description": "Various storage and system metrics", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Recent Notes" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 30 + }, + "id": 405, + "options": { + "legend": { + "calcs": [ + "last" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_blobs_total{job=~'$job',instance=~'$instance'}", + "legendFormat": "Blob Records", + "refId": "A" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_recent_notes_total{job=~'$job',instance=~'$instance'}", + "legendFormat": "Recent Notes", + "refId": "B" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_embeddings_total{job=~'$job',instance=~'$instance'}", + "legendFormat": "Embeddings", + "refId": "C" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_embedding_providers_total{job=~'$job',instance=~'$instance'}", + "legendFormat": "Embedding Providers", + "refId": "D" + } + ], + "title": "📊 Storage & System Metrics", + "type": "timeseries" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "description": "Timeline showing when content was created and last modified", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 8, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "always", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "dateTimeAsIso" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 30 + }, + "id": 406, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.0.1", + "targets": [ + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_oldest_note_timestamp{job=~'$job',instance=~'$instance'} * 1000", + "legendFormat": "Oldest Note", + "refId": "A" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_newest_note_timestamp{job=~'$job',instance=~'$instance'} * 1000", + "legendFormat": "Newest Note", + "refId": "B" + }, + { + "datasource": { + "uid": "${datasource}" + }, + "expr": "trilium_last_modified_timestamp{job=~'$job',instance=~'$instance'} * 1000", + "legendFormat": "Last Modified", + "refId": "C" + } + ], + "title": "⏰ Content Timeline", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "1m", + "schemaVersion": 41, + "tags": [ + "trilium", + "notes", + "monitoring", + "enhanced" + ], + "templating": { + "list": [ + { + "current": { + "text": "myprom", + "value": "PA04845DA3A4B088E" + }, + "includeAll": false, + "label": "Datasource", + "name": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "//", + "type": "datasource" + }, + { + "allValue": ".*", + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": { + "UID": "", + "type": "" + }, + "includeAll": true, + "label": "Job", + "multi": true, + "name": "job", + "options": [], + "query": "query_result(up)", + "refresh": 1, + "regex": "/job=\"([^\"]+)\"/", + "sort": 1, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "includeAll": true, + "label": "Instance", + "multi": true, + "name": "instance", + "options": [], + "query": "trilium_database_size_bytes", + "refresh": 1, + "regex": "/instance=\"([^\"]+)\"/", + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "TriliumNext Dashboard", + "uid": "06993f9b-a477-4723-bf18-47743393b382", + "version": 5 +} \ No newline at end of file diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Metrics_image.png b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Metrics_image.png new file mode 100644 index 000000000..ae68ddd02 Binary files /dev/null and b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Metrics_image.png differ diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Templates.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Templates.html index c0ebf0b9c..4c91ffc9c 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Templates.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Templates.html @@ -40,7 +40,19 @@ you can also mark templates with #workspaceTemplate to display them only in the workspace.

                  Templates can also be added or changed after note creation by creating - a ~template relation pointing to the desired template note.

                  + a ~template relation pointing to the desired template note. 

                  +

                  To specify a template for child notes, you can use a ~child:template relation + pointing to the appropriate template note. There is no limit to the depth + of the hierarchy — you can use ~child:child:template, ~child:child:child:template, + and so on.

                  +

                  Additional Notes

                  From a visual perspective, templates can define #iconClass and #cssClass attributes, allowing all instance notes (e.g., books) to display a specific icon and diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Keyboard Shortcuts.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Keyboard Shortcuts.html index ddd6b647d..a9c2d7c65 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Keyboard Shortcuts.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Keyboard Shortcuts.html @@ -77,7 +77,7 @@ class="reference-link" href="#root/_help_QrtTYPmdd1qq">Markdown-like formatting.

                    -
                  •  Enter in tree pane switches from tree pane into note title. +
                  • Enter in tree pane switches from tree pane into note title. Enter from note title switches focus to text editor. Ctrl+. switches back from editor to tree pane.
                  • Ctrl+. - jump away from the editor to tree pane and diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Geo Map.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Geo Map.html index 674422cad..b85d13a7c 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Geo Map.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Geo Map.html @@ -39,6 +39,7 @@ +

                    Repositioning the map

                    • Click and drag the map in order to move across the map.
                    • @@ -102,12 +103,11 @@ +

                      How the location of the markers is stored

                      The location of a marker is stored in the #geolocation attribute of the child notes:

                      -

                      - -

                      +

                      This value can be added manually if needed. The value of the attribute is made up of the latitude and longitude separated by a comma.

                      Repositioning markers

                      @@ -210,6 +210,7 @@ +

                      Adding from OpenStreetMap

                      Similarly to the Google Maps approach:

                      @@ -259,6 +260,7 @@
                      +

                      Adding GPS tracks (.gpx)

                      Trilium has basic support for displaying GPS tracks on the geo map.

                      -

                      Grid-like artifacts on the map

                      + +

                      Grid-like artifacts on the map

                      This occurs if the application is not at 100% zoom which causes the pixels of the map to not render correctly due to fractional scaling. The only possible solution is to set the UI zoom at 100% (default keyboard shortcut diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text/Keyboard shortcuts.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text/Keyboard shortcuts.html index df312d7bf..f26061d1c 100644 --- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text/Keyboard shortcuts.html +++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Note Types/Text/Keyboard shortcuts.html @@ -60,7 +60,7 @@ - Mark selected text as keyboard shortcut + Mark selected text as keyboard shortcut Ctrl + Alt + K @@ -68,7 +68,7 @@ - Insert Math Equations + Insert Math Equations Ctrl + M @@ -76,7 +76,7 @@ - Move blocks (lists, paragraphs, etc.) up + Move blocks (lists, paragraphs, etc.) up Ctrl+  +  @@ -87,7 +87,7 @@ - Move blocks (lists, paragraphs, etc.) down + Move blocks (lists, paragraphs, etc.) down Ctrl+ + @@ -102,6 +102,7 @@ +

                      Common shortcuts