diff --git a/README.md b/README.md index 02a9c969b..84b64afc8 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Download the binary release for your platform from the [latest release page](htt If your distribution is listed in the table below, use your distribution's package. -[![Packaging status](https://repology.org/badge/vertical-allrepos/trilium-next-desktop.svg)](https://repology.org/project/trilium-next-desktop/versions) +[![Packaging status](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions) You may also download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the `trilium` executable. diff --git a/_regroup/demo/style.css b/_regroup/demo/style.css deleted file mode 100644 index 0ebbae93d..000000000 --- a/_regroup/demo/style.css +++ /dev/null @@ -1,593 +0,0 @@ -/* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */ - -.printed-content .ck-widget__selection-handle, .printed-content .ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */ - display: none; -} - -.page-break { - page-break-after: always; -} - -.printed-content .page-break:after, -.printed-content .page-break > * { - display: none !important; -} - -.ck-content li p { - margin: 0 !important; -} - -.admonition { - --accent-color: var(--card-border-color); - border: 1px solid var(--accent-color); - box-shadow: var(--card-box-shadow); - background: var(--card-background-color); - border-radius: 0.5em; - padding: 1em; - margin: 1.25em 0; - position: relative; - overflow: hidden; -} - -.admonition p:last-child { - margin-bottom: 0; -} - -.admonition p, h2 { - margin-top: 0; -} - -.admonition.note { --accent-color: #69c7ff; } -.admonition.tip { --accent-color: #40c025; } -.admonition.important { --accent-color: #9839f7; } -.admonition.caution { --accent-color: #ff2e2e; } -.admonition.warning { --accent-color: #e2aa03; } - -/* - * CKEditor 5 (v41.0.0) content styles. - * Generated on Fri, 26 Jan 2024 10:23:49 GMT. - * For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html - */ - -:root { - --ck-color-image-caption-background: hsl(0, 0%, 97%); - --ck-color-image-caption-text: hsl(0, 0%, 20%); - --ck-color-mention-background: hsla(341, 100%, 30%, 0.1); - --ck-color-mention-text: hsl(341, 100%, 30%); - --ck-color-selector-caption-background: hsl(0, 0%, 97%); - --ck-color-selector-caption-text: hsl(0, 0%, 20%); - --ck-highlight-marker-blue: hsl(201, 97%, 72%); - --ck-highlight-marker-green: hsl(120, 93%, 68%); - --ck-highlight-marker-pink: hsl(345, 96%, 73%); - --ck-highlight-marker-yellow: hsl(60, 97%, 73%); - --ck-highlight-pen-green: hsl(112, 100%, 27%); - --ck-highlight-pen-red: hsl(0, 85%, 49%); - --ck-image-style-spacing: 1.5em; - --ck-inline-image-style-spacing: calc(var(--ck-image-style-spacing) / 2); - --ck-todo-list-checkmark-size: 16px; -} - -/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */ -.ck-content .table .ck-table-resized { - table-layout: fixed; -} -/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */ -.ck-content .table table { - overflow: hidden; -} -/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */ -.ck-content .table td, -.ck-content .table th { - overflow-wrap: break-word; - position: relative; -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content .table { - margin: 0.9em auto; - display: table; -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content .table table { - border-collapse: collapse; - border-spacing: 0; - width: 100%; - height: 100%; - border: 1px double hsl(0, 0%, 70%); -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content .table table td, -.ck-content .table table th { - min-width: 2em; - padding: .4em; - border: 1px solid hsl(0, 0%, 75%); -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content .table table th { - font-weight: bold; - background: hsla(0, 0%, 0%, 5%); -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content[dir="rtl"] .table th { - text-align: right; -} -/* @ckeditor/ckeditor5-table/theme/table.css */ -.ck-content[dir="ltr"] .table th { - text-align: left; -} -/* @ckeditor/ckeditor5-table/theme/tablecaption.css */ -.ck-content .table > figcaption { - display: table-caption; - caption-side: top; - word-break: break-word; - text-align: center; - color: var(--ck-color-selector-caption-text); - background-color: var(--ck-color-selector-caption-background); - padding: .6em; - font-size: .75em; - outline-offset: -1px; -} -/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ -.ck-content .page-break { - position: relative; - clear: both; - padding: 5px 0; - display: flex; - align-items: center; - justify-content: center; -} -/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ -.ck-content .page-break::after { - content: ''; - position: absolute; - border-bottom: 2px dashed hsl(0, 0%, 77%); - width: 100%; -} -/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ -.ck-content .page-break__label { - position: relative; - z-index: 1; - padding: .3em .6em; - display: block; - text-transform: uppercase; - border: 1px solid hsl(0, 0%, 77%); - border-radius: 2px; - font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif; - font-size: 0.75em; - font-weight: bold; - color: hsl(0, 0%, 20%); - background: hsl(0, 0%, 100%); - box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15); - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} -/* @ckeditor/ckeditor5-media-embed/theme/mediaembed.css */ -.ck-content .media { - clear: both; - margin: 0.9em 0; - display: block; - min-width: 15em; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list { - list-style: none; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list li { - position: relative; - margin-bottom: 5px; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list li .todo-list { - margin-top: 5px; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label > input { - -webkit-appearance: none; - display: inline-block; - position: relative; - width: var(--ck-todo-list-checkmark-size); - height: var(--ck-todo-list-checkmark-size); - vertical-align: middle; - border: 0; - left: -25px; - margin-right: -15px; - right: 0; - margin-left: 0; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content[dir=rtl] .todo-list .todo-list__label > input { - left: 0; - margin-right: 0; - right: -25px; - margin-left: -15px; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label > input::before { - display: block; - position: absolute; - box-sizing: border-box; - content: ''; - width: 100%; - height: 100%; - border: 1px solid hsl(0, 0%, 20%); - border-radius: 2px; - transition: 250ms ease-in-out box-shadow; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label > input::after { - display: block; - position: absolute; - box-sizing: content-box; - pointer-events: none; - content: ''; - left: calc( var(--ck-todo-list-checkmark-size) / 3 ); - top: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - width: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - height: calc( var(--ck-todo-list-checkmark-size) / 2.6 ); - border-style: solid; - border-color: transparent; - border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0; - transform: rotate(45deg); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label > input[checked]::before { - background: hsl(126, 64%, 41%); - border-color: hsl(126, 64%, 41%); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label > input[checked]::after { - border-color: hsl(0, 0%, 100%); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label .todo-list__label__description { - vertical-align: middle; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] { - position: absolute; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > input, -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input { - cursor: pointer; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > input:hover::before, .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input:hover::before { - box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input { - -webkit-appearance: none; - display: inline-block; - position: relative; - width: var(--ck-todo-list-checkmark-size); - height: var(--ck-todo-list-checkmark-size); - vertical-align: middle; - border: 0; - left: -25px; - margin-right: -15px; - right: 0; - margin-left: 0; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content[dir=rtl] .todo-list .todo-list__label > span[contenteditable=false] > input { - left: 0; - margin-right: 0; - right: -25px; - margin-left: -15px; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::before { - display: block; - position: absolute; - box-sizing: border-box; - content: ''; - width: 100%; - height: 100%; - border: 1px solid hsl(0, 0%, 20%); - border-radius: 2px; - transition: 250ms ease-in-out box-shadow; -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::after { - display: block; - position: absolute; - box-sizing: content-box; - pointer-events: none; - content: ''; - left: calc( var(--ck-todo-list-checkmark-size) / 3 ); - top: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - width: calc( var(--ck-todo-list-checkmark-size) / 5.3 ); - height: calc( var(--ck-todo-list-checkmark-size) / 2.6 ); - border-style: solid; - border-color: transparent; - border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0; - transform: rotate(45deg); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::before { - background: hsl(126, 64%, 41%); - border-color: hsl(126, 64%, 41%); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::after { - border-color: hsl(0, 0%, 100%); -} -/* @ckeditor/ckeditor5-list/theme/todolist.css */ -.ck-editor__editable.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] { - position: absolute; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ol { - list-style-type: decimal; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ol ol { - list-style-type: lower-latin; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ol ol ol { - list-style-type: lower-roman; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ol ol ol ol { - list-style-type: upper-latin; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ol ol ol ol ol { - list-style-type: upper-roman; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ul { - list-style-type: disc; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ul ul { - list-style-type: circle; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ul ul ul { - list-style-type: square; -} -/* @ckeditor/ckeditor5-list/theme/list.css */ -.ck-content ul ul ul ul { - list-style-type: square; -} -/* @ckeditor/ckeditor5-image/theme/image.css */ -.ck-content .image { - display: table; - clear: both; - text-align: center; - margin: 0.9em auto; - min-width: 50px; -} -/* @ckeditor/ckeditor5-image/theme/image.css */ -.ck-content .image img { - display: block; - margin: 0 auto; - max-width: 100%; - min-width: 100%; - height: auto; -} -/* @ckeditor/ckeditor5-image/theme/image.css */ -.ck-content .image-inline { - /* - * Normally, the .image-inline would have "display: inline-block" and "img { width: 100% }" (to follow the wrapper while resizing).; - * Unfortunately, together with "srcset", it gets automatically stretched up to the width of the editing root. - * This strange behavior does not happen with inline-flex. - */ - display: inline-flex; - max-width: 100%; - align-items: flex-start; -} -/* @ckeditor/ckeditor5-image/theme/image.css */ -.ck-content .image-inline picture { - display: flex; -} -/* @ckeditor/ckeditor5-image/theme/image.css */ -.ck-content .image-inline picture, -.ck-content .image-inline img { - flex-grow: 1; - flex-shrink: 1; - max-width: 100%; -} -/* @ckeditor/ckeditor5-image/theme/imageresize.css */ -.ck-content img.image_resized { - height: auto; -} -/* @ckeditor/ckeditor5-image/theme/imageresize.css */ -.ck-content .image.image_resized { - max-width: 100%; - display: block; - box-sizing: border-box; -} -/* @ckeditor/ckeditor5-image/theme/imageresize.css */ -.ck-content .image.image_resized img { - width: 100%; -} -/* @ckeditor/ckeditor5-image/theme/imageresize.css */ -.ck-content .image.image_resized > figcaption { - display: block; -} -/* @ckeditor/ckeditor5-image/theme/imagecaption.css */ -.ck-content .image > figcaption { - display: table-caption; - caption-side: bottom; - word-break: break-word; - color: var(--ck-color-image-caption-text); - background-color: var(--ck-color-image-caption-background); - padding: .6em; - font-size: .75em; - outline-offset: -1px; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-block-align-left, -.ck-content .image-style-block-align-right { - max-width: calc(100% - var(--ck-image-style-spacing)); -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-align-left, -.ck-content .image-style-align-right { - clear: none; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-side { - float: right; - margin-left: var(--ck-image-style-spacing); - max-width: 50%; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-align-left { - float: left; - margin-right: var(--ck-image-style-spacing); -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-align-center { - margin-left: auto; - margin-right: auto; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-align-right { - float: right; - margin-left: var(--ck-image-style-spacing); -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-block-align-right { - margin-right: 0; - margin-left: auto; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-style-block-align-left { - margin-left: 0; - margin-right: auto; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content p + .image-style-align-left, -.ck-content p + .image-style-align-right, -.ck-content p + .image-style-side { - margin-top: 0; -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-inline.image-style-align-left, -.ck-content .image-inline.image-style-align-right { - margin-top: var(--ck-inline-image-style-spacing); - margin-bottom: var(--ck-inline-image-style-spacing); -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-inline.image-style-align-left { - margin-right: var(--ck-inline-image-style-spacing); -} -/* @ckeditor/ckeditor5-image/theme/imagestyle.css */ -.ck-content .image-inline.image-style-align-right { - margin-left: var(--ck-inline-image-style-spacing); -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .marker-yellow { - background-color: var(--ck-highlight-marker-yellow); -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .marker-green { - background-color: var(--ck-highlight-marker-green); -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .marker-pink { - background-color: var(--ck-highlight-marker-pink); -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .marker-blue { - background-color: var(--ck-highlight-marker-blue); -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .pen-red { - color: var(--ck-highlight-pen-red); - background-color: transparent; -} -/* @ckeditor/ckeditor5-highlight/theme/highlight.css */ -.ck-content .pen-green { - color: var(--ck-highlight-pen-green); - background-color: transparent; -} -/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */ -.ck-content blockquote { - overflow: hidden; - padding-right: 1.5em; - padding-left: 1.5em; - margin-left: 0; - margin-right: 0; - font-style: italic; - border-left: solid 5px hsl(0, 0%, 80%); -} -/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */ -.ck-content[dir="rtl"] blockquote { - border-left: 0; - border-right: solid 5px hsl(0, 0%, 80%); -} -/* @ckeditor/ckeditor5-basic-styles/theme/code.css */ -.ck-content code { - background-color: hsla(0, 0%, 78%, 0.3); - padding: .15em; - border-radius: 2px; -} -/* @ckeditor/ckeditor5-font/theme/fontsize.css */ -.ck-content .text-tiny { - font-size: .7em; -} -/* @ckeditor/ckeditor5-font/theme/fontsize.css */ -.ck-content .text-small { - font-size: .85em; -} -/* @ckeditor/ckeditor5-font/theme/fontsize.css */ -.ck-content .text-big { - font-size: 1.4em; -} -/* @ckeditor/ckeditor5-font/theme/fontsize.css */ -.ck-content .text-huge { - font-size: 1.8em; -} -/* @ckeditor/ckeditor5-mention/theme/mention.css */ -.ck-content .mention { - background: var(--ck-color-mention-background); - color: var(--ck-color-mention-text); -} -/* @ckeditor/ckeditor5-horizontal-line/theme/horizontalline.css */ -.ck-content hr { - margin: 15px 0; - height: 4px; - background: hsl(0, 0%, 87%); - border: 0; -} -/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */ -.ck-content pre { - padding: 1em; - text-align: left; - direction: ltr; - tab-size: 4; - white-space: pre-wrap; - font-style: normal; - min-width: 200px; - border: 0px; - border-radius: 6px; - box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2); -} -.ck-content pre:not(.hljs) { - color: hsl(0, 0%, 20.8%); - background: hsla(0, 0%, 78%, 0.3); -} -/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */ -.ck-content pre code { - background: unset; - padding: 0; - border-radius: 0; -} -@media print { - /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ - .ck-content .page-break { - padding: 0; - } - /* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */ - .ck-content .page-break::after { - display: none; - } -} diff --git a/_regroup/test-etapi/api-metrics.http b/_regroup/test-etapi/api-metrics.http new file mode 100644 index 000000000..78aee7217 --- /dev/null +++ b/_regroup/test-etapi/api-metrics.http @@ -0,0 +1,43 @@ +### 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/metrics.http b/_regroup/test-etapi/metrics.http new file mode 100644 index 000000000..24435f954 --- /dev/null +++ b/_regroup/test-etapi/metrics.http @@ -0,0 +1,82 @@ +### 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/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index dd2391b67..074e03e4c 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -159,6 +159,9 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> } saveToRecentNotes(resolvedNotePath: string) { + if (options.is("databaseReadonly")) { + return; + } setTimeout(async () => { // we include the note in the recent list only if the user stayed on the note at least 5 seconds if (resolvedNotePath && resolvedNotePath === this.notePath) { @@ -254,6 +257,10 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> return false; } + if (options.is("databaseReadonly")) { + return true; + } + if (this.note.isLabelTruthy("readOnly")) { return true; } diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index cf2876a5a..fa83470ce 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -44,6 +44,9 @@ export default class TabManager extends Component { if (!appContext.isMainWindow) { return; } + if (options.is("databaseReadonly")) { + return; + } const openNoteContexts = this.noteContexts .map((nc) => nc.getPojoState()) diff --git a/apps/client/src/services/clipboard.ts b/apps/client/src/services/clipboard.ts index feffee065..40354393c 100644 --- a/apps/client/src/services/clipboard.ts +++ b/apps/client/src/services/clipboard.ts @@ -4,6 +4,7 @@ import froca from "./froca.js"; import linkService from "./link.js"; import utils from "./utils.js"; import { t } from "./i18n.js"; +import toast from "./toast.js"; let clipboardBranchIds: string[] = []; let clipboardMode: string | null = null; @@ -108,6 +109,39 @@ function isClipboardEmpty() { return clipboardBranchIds.length === 0; } +export function copyText(text: string) { + if (!text) { + return; + } + + let succeeded = false; + + try { + if (navigator.clipboard) { + navigator.clipboard.writeText(text); + succeeded = true; + } else { + // Fallback method: https://stackoverflow.com/a/72239825 + const textArea = document.createElement("textarea"); + textArea.value = text; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + succeeded = document.execCommand('copy'); + document.body.removeChild(textArea); + } + } catch (e) { + console.warn(e); + succeeded = false; + } + + if (succeeded) { + toast.showMessage(t("code_block.copy_success")); + } else { + toast.showError(t("code_block.copy_failed")); + } +} + export default { pasteAfter, pasteInto, diff --git a/apps/client/src/services/content_renderer.ts b/apps/client/src/services/content_renderer.ts index 0664f6a5c..08ed561ff 100644 --- a/apps/client/src/services/content_renderer.ts +++ b/apps/client/src/services/content_renderer.ts @@ -9,7 +9,7 @@ import treeService from "./tree.js"; import FNote from "../entities/fnote.js"; import FAttachment from "../entities/fattachment.js"; import imageContextMenuService from "../menus/image_context_menu.js"; -import { applySingleBlockSyntaxHighlight, applySyntaxHighlight } from "./syntax_highlight.js"; +import { applySingleBlockSyntaxHighlight, formatCodeBlocks } from "./syntax_highlight.js"; import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js"; import renderDoc from "./doc_renderer.js"; import { t } from "../services/i18n.js"; @@ -106,7 +106,7 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery>((resolve) => { @@ -41,7 +41,7 @@ function processContent(url: string, $content: JQuery) { $img.attr("src", dir + "/" + $img.attr("src")); }); - applySyntaxHighlight($content); + formatCodeBlocks($content); } function getUrl(docNameValue: string, language: string) { diff --git a/apps/client/src/services/syntax_highlight.ts b/apps/client/src/services/syntax_highlight.ts index 7dfb29f30..bfe887b9d 100644 --- a/apps/client/src/services/syntax_highlight.ts +++ b/apps/client/src/services/syntax_highlight.ts @@ -1,21 +1,23 @@ -import { ensureMimeTypes, highlight, highlightAuto, loadTheme, Themes } from "@triliumnext/highlightjs"; +import { ensureMimeTypes, highlight, highlightAuto, loadTheme, Themes, type AutoHighlightResult, type HighlightResult, type Theme } from "@triliumnext/highlightjs"; import mime_types from "./mime_types.js"; import options from "./options.js"; +import { t } from "./i18n.js"; +import { copyText } from "./clipboard.js"; let highlightingLoaded = false; /** * Identifies all the code blocks (as `pre code`) under the specified hierarchy and uses the highlight.js library to obtain the highlighted text which is then applied on to the code blocks. + * Additionally, adds a "Copy to clipboard" button. * * @param $container the container under which to look for code blocks and to apply syntax highlighting to them. */ -export async function applySyntaxHighlight($container: JQuery) { - if (!isSyntaxHighlightEnabled()) { - return; +export async function formatCodeBlocks($container: JQuery) { + const syntaxHighlightingEnabled = isSyntaxHighlightEnabled(); + if (syntaxHighlightingEnabled) { + await ensureMimeTypesForHighlighting(); } - await ensureMimeTypesForHighlighting(); - const codeBlocks = $container.find("pre code"); for (const codeBlock of codeBlocks) { const normalizedMimeType = extractLanguageFromClassList(codeBlock); @@ -23,10 +25,22 @@ export async function applySyntaxHighlight($container: JQuery) { continue; } - applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType); + applyCopyToClipboardButton($(codeBlock)); + + if (syntaxHighlightingEnabled) { + applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType); + } } } +export function applyCopyToClipboardButton($codeBlock: JQuery) { + const $copyButton = $("