From 60319c6d3709718e4d079ca7cd7ebf1394d3422a Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:32:46 -0700 Subject: [PATCH 01/38] Fix: prevent stale db filename during workflow actions (#12289) --- src/documents/signals/handlers.py | 2 + src/documents/tests/test_workflows.py | 58 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 591d235bd..5be8855e6 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -833,6 +833,8 @@ def run_workflows( if not use_overrides: # limit title to 128 characters document.title = document.title[:128] + # Make sure the filename and archive filename are accurate + document.refresh_from_db(fields=["filename", "archive_filename"]) # save first before setting tags document.save() document.tags.set(doc_tag_ids) diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index deb40a165..ba1e72e1f 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -25,6 +25,7 @@ from rest_framework.test import APIClient from rest_framework.test import APITestCase from documents.file_handling import create_source_path_directory +from documents.file_handling import generate_filename from documents.file_handling import generate_unique_filename from documents.signals.handlers import run_workflows from documents.workflows.webhooks import send_webhook @@ -898,6 +899,63 @@ class TestWorkflows( expected_str = f"Document matched {trigger} from {w}" self.assertIn(expected_str, cm.output[0]) + def test_workflow_assign_custom_field_keeps_storage_filename_in_sync(self) -> None: + """ + GIVEN: + - Existing document with a storage path template that depends on a custom field + - Existing workflow triggered on document update assigning that custom field + WHEN: + - Workflow runs for the document + THEN: + - The database filename remains aligned with the moved file on disk + """ + storage_path = StoragePath.objects.create( + name="workflow-custom-field-path", + path="{{ custom_fields|get_cf_value('Custom Field 1', 'none') }}/{{ title }}", + ) + doc = Document.objects.create( + title="workflow custom field sync", + mime_type="application/pdf", + checksum="workflow-custom-field-sync", + storage_path=storage_path, + original_filename="workflow-custom-field-sync.pdf", + ) + CustomFieldInstance.objects.create( + document=doc, + field=self.cf1, + value_text="initial", + ) + + generated = generate_unique_filename(doc) + destination = (settings.ORIGINALS_DIR / generated).resolve() + create_source_path_directory(destination) + shutil.copy(self.SAMPLE_DIR / "simple.pdf", destination) + Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix()) + doc.refresh_from_db() + + trigger = WorkflowTrigger.objects.create( + type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, + ) + action = WorkflowAction.objects.create( + type=WorkflowAction.WorkflowActionType.ASSIGNMENT, + assign_custom_fields_values={self.cf1.pk: "cars"}, + ) + action.assign_custom_fields.add(self.cf1.pk) + workflow = Workflow.objects.create( + name="Workflow custom field filename sync", + order=0, + ) + workflow.triggers.add(trigger) + workflow.actions.add(action) + workflow.save() + + run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc) + + doc.refresh_from_db() + expected_filename = generate_filename(doc) + self.assertEqual(Path(doc.filename), expected_filename) + self.assertTrue(doc.source_path.is_file()) + def test_document_added_workflow(self): trigger = WorkflowTrigger.objects.create( type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED, From ba0a80a8ad04e9dc2e1ae1d83d556c2a2d2f629c Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 11 Mar 2026 07:39:32 -0700 Subject: [PATCH 02/38] Fix: prevent wrapping with larger amounts of tags on small cards, reset moreTags setting to correct count (#12302) --- .../document-card-small.component.html | 2 +- .../document-card-small.component.scss | 10 ++++++++++ .../document-card-small.component.spec.ts | 10 ++++++++++ .../document-card-small.component.ts | 1 + 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index b154324c7..b3a29aed4 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -15,7 +15,7 @@ } @if (document && displayFields?.includes(DisplayField.TAGS)) { -
+
@for (tagID of tagIDs; track tagID) { } diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss index 508c5251a..dce77802e 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss @@ -72,4 +72,14 @@ a { max-width: 80%; row-gap: .2rem; line-height: 1; + + &.tags-no-wrap { + ::ng-deep .badge { + display: inline-block; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } } diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.spec.ts b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.spec.ts index 63cfc5a50..982b3980b 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.spec.ts +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.spec.ts @@ -82,6 +82,16 @@ describe('DocumentCardSmallComponent', () => { ).toHaveLength(6) }) + it('should clear hidden tag counter when tag count falls below the limit', () => { + expect(component.moreTags).toEqual(3) + + component.document.tags = [1, 2, 3, 4, 5, 6] + fixture.detectChanges() + + expect(component.moreTags).toBeNull() + expect(fixture.nativeElement.textContent).not.toContain('+ 3') + }) + it('should try to close the preview on mouse leave', () => { component.popupPreview = { close: jest.fn(), diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts index ad428dfab..05f84d752 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts @@ -126,6 +126,7 @@ export class DocumentCardSmallComponent this.moreTags = this.document.tags.length - (limit - 1) return this.document.tags.slice(0, limit - 1) } else { + this.moreTags = null return this.document.tags } } From d919c341b12ba1fcc2a3f9bc84b4e98160582990 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:57:35 -0700 Subject: [PATCH 03/38] Fix: clear descendant selections in dropdown when parent toggled (#12326) --- .../filterable-dropdown.component.spec.ts | 53 +++++++++++++++++++ .../filterable-dropdown.component.ts | 35 ++++++++++++ 2 files changed, 88 insertions(+) diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts index 2ecf95f2b..1763239b1 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts @@ -631,6 +631,59 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => ]) }) + it('deselecting a parent clears selected descendants', () => { + const root: Tag = { id: 100, name: 'Root Tag' } + const child: Tag = { id: 101, name: 'Child Tag', parent: root.id } + const grandchild: Tag = { + id: 102, + name: 'Grandchild Tag', + parent: child.id, + } + const other: Tag = { id: 103, name: 'Other Tag' } + + selectionModel.items = [root, child, grandchild, other] + selectionModel.set(root.id, ToggleableItemState.Selected, false) + selectionModel.set(child.id, ToggleableItemState.Selected, false) + selectionModel.set(grandchild.id, ToggleableItemState.Selected, false) + selectionModel.set(other.id, ToggleableItemState.Selected, false) + + selectionModel.toggle(root.id, false) + + expect(selectionModel.getSelectedItems()).toEqual([other]) + }) + + it('un-excluding a parent clears excluded descendants', () => { + const root: Tag = { id: 110, name: 'Root Tag' } + const child: Tag = { id: 111, name: 'Child Tag', parent: root.id } + const other: Tag = { id: 112, name: 'Other Tag' } + + selectionModel.items = [root, child, other] + selectionModel.set(root.id, ToggleableItemState.Excluded, false) + selectionModel.set(child.id, ToggleableItemState.Excluded, false) + selectionModel.set(other.id, ToggleableItemState.Excluded, false) + + selectionModel.exclude(root.id, false) + + expect(selectionModel.getExcludedItems()).toEqual([other]) + }) + + it('excluding a selected parent clears selected descendants', () => { + const root: Tag = { id: 120, name: 'Root Tag' } + const child: Tag = { id: 121, name: 'Child Tag', parent: root.id } + const other: Tag = { id: 122, name: 'Other Tag' } + + selectionModel.manyToOne = true + selectionModel.items = [root, child, other] + selectionModel.set(root.id, ToggleableItemState.Selected, false) + selectionModel.set(child.id, ToggleableItemState.Selected, false) + selectionModel.set(other.id, ToggleableItemState.Selected, false) + + selectionModel.exclude(root.id, false) + + expect(selectionModel.getExcludedItems()).toEqual([root]) + expect(selectionModel.getSelectedItems()).toEqual([other]) + }) + it('resorts items immediately when document count sorting enabled', () => { const apple: Tag = { id: 55, name: 'Apple' } const zebra: Tag = { id: 56, name: 'Zebra' } diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts index ec5425630..bc15e3374 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts @@ -231,6 +231,7 @@ export class FilterableDropdownSelectionModel { state == ToggleableItemState.Excluded ) { this.temporarySelectionStates.delete(id) + this.clearDescendantSelections(id) } if (!id) { @@ -257,6 +258,7 @@ export class FilterableDropdownSelectionModel { if (this.manyToOne || this.singleSelect) { this.temporarySelectionStates.set(id, ToggleableItemState.Excluded) + this.clearDescendantSelections(id) if (this.singleSelect) { for (let key of this.temporarySelectionStates.keys()) { @@ -277,9 +279,15 @@ export class FilterableDropdownSelectionModel { newState = ToggleableItemState.NotSelected } this.temporarySelectionStates.set(id, newState) + if (newState == ToggleableItemState.Excluded) { + this.clearDescendantSelections(id) + } } } else if (!id || state == ToggleableItemState.Excluded) { this.temporarySelectionStates.delete(id) + if (id) { + this.clearDescendantSelections(id) + } } if (fireEvent) { @@ -291,6 +299,33 @@ export class FilterableDropdownSelectionModel { return this.selectionStates.get(id) || ToggleableItemState.NotSelected } + private clearDescendantSelections(id: number) { + for (const descendantID of this.getDescendantIDs(id)) { + this.temporarySelectionStates.delete(descendantID) + } + } + + private getDescendantIDs(id: number): number[] { + const descendants: number[] = [] + const queue: number[] = [id] + + while (queue.length) { + const parentID = queue.shift() + for (const item of this._items) { + if ( + typeof item?.id === 'number' && + typeof (item as any)['parent'] === 'number' && + (item as any)['parent'] === parentID + ) { + descendants.push(item.id) + queue.push(item.id) + } + } + } + + return descendants + } + get logicalOperator(): LogicalOperator { return this.temporaryLogicalOperator } From 40255cfdbb8cf12c38df01a507b01e3f4991a697 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:06:16 -0700 Subject: [PATCH 04/38] Fix: correct dropdown list active color in dark mode (#12328) --- src-ui/src/theme.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src-ui/src/theme.scss b/src-ui/src/theme.scss index eacc3b4e7..e6a4ea113 100644 --- a/src-ui/src/theme.scss +++ b/src-ui/src/theme.scss @@ -149,6 +149,10 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true + '@angular-devkit/architect@0.2102.2': + resolution: {integrity: sha512-CDvFtXwyBtMRkTQnm+LfBNLL0yLV8ZGskrM1T6VkcGwXGFDott1FxUdj96ViodYsYL5fbJr0MNA6TlLcanV3kQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true + '@angular-devkit/build-angular@21.1.2': resolution: {integrity: sha512-i/FTbqVwj0Wk6B5RA2H9iVsDC/kIK/5koSEwkIQjXGZuDVFUoEuWiIR2PGGSSQ9u3DmkpVPZmKEXWRl+g7Qn5g==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} @@ -360,8 +365,17 @@ packages: chokidar: optional: true - '@angular-devkit/schematics@21.2.0': - resolution: {integrity: sha512-3kn3FI5v7BQ7Zct6raek+WgvyDwOJ8wElbyC903GxMQCDBRGGcevhHvTAIHhknihEsrgplzPhTlWeMbk1JfdFg==} + '@angular-devkit/core@21.2.2': + resolution: {integrity: sha512-xUeKGe4BDQpkz0E6fnAPIJXE0y0nqtap0KhJIBhvN7xi3NenIzTmoi6T9Yv5OOBUdLZbOm4SOel8MhdXiIBpAQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^5.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/schematics@21.2.2': + resolution: {integrity: sha512-CCeyQxGUq+oyGnHd7PfcYIVbj9pRnqjQq0rAojoAqs1BJdtInx9weLBCLy+AjM3NHePeZrnwm+wEVr8apED8kg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} '@angular-eslint/builder@21.3.0': @@ -454,8 +468,8 @@ packages: vitest: optional: true - '@angular/build@21.2.0': - resolution: {integrity: sha512-K0EqiHz2y7TSyD4adWD0+C/P9khKlrsSWavXWxGRvoSJC/H3I3SK5Z6BWwftBibXR1Fis7njwvl5IGAlQrDchA==} + '@angular/build@21.2.2': + resolution: {integrity: sha512-Vq2eIneNxzhHm1MwEmRqEJDwHU9ODfSRDaMWwtysGMhpoMQmLdfTqkQDmkC2qVUr8mV8Z1i5I+oe5ZJaMr/PlQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: '@angular/compiler': ^21.0.0 @@ -465,7 +479,7 @@ packages: '@angular/platform-browser': ^21.0.0 '@angular/platform-server': ^21.0.0 '@angular/service-worker': ^21.0.0 - '@angular/ssr': ^21.2.0 + '@angular/ssr': ^21.2.2 karma: ^6.4.0 less: ^4.2.0 ng-packagr: ^21.0.0 @@ -500,46 +514,46 @@ packages: vitest: optional: true - '@angular/cdk@21.2.0': - resolution: {integrity: sha512-1P0TNL1F51NC7JAaXabaAHY7Y1zBloLSZXfml1POa4a116V+y/QZfPGsxM0LwD1qSSXhSb2LNl7duTtJAP39bA==} + '@angular/cdk@21.2.2': + resolution: {integrity: sha512-9AsZkwqy07No7+0qPydcJfXB6SpA9qLDBanoesNj5KsiZJ62PJH3oIjVyNeQEEe1HQWmSwBnhwN12OPLNMUlnw==} peerDependencies: '@angular/common': ^21.0.0 || ^22.0.0 '@angular/core': ^21.0.0 || ^22.0.0 '@angular/platform-browser': ^21.0.0 || ^22.0.0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/cli@21.2.0': - resolution: {integrity: sha512-yaGEpckqgOemcHkoWeH92i9eNrcbr9iE/dnxL+Du6s9spTAXJ2jjtYfszhmowuQZkCK5rjecMb8ctNtHlaGCjg==} + '@angular/cli@21.2.2': + resolution: {integrity: sha512-eZo8/qX+ZIpIWc0CN+cCX13Lbgi/031wAp8DRVhDDO6SMVtcr/ObOQ2S16+pQdOMXxiG3vby6IhzJuz9WACzMQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true - '@angular/common@21.2.0': - resolution: {integrity: sha512-6zJMPi0i/XDniEgv3/t2BjuDHiOG44lgIR5PYyxqGpgJ0kqB5hku/0TuentNEi1VnBYgthnfhjek7c+lakXmhw==} + '@angular/common@21.2.4': + resolution: {integrity: sha512-NrP6qOuUpo3fqq14UJ1b2bIRtWsfvxh1qLqOyFV4gfBrHhXd0XffU1LUlUw1qp4w1uBSgPJ0/N5bSPUWrAguVg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/core': 21.2.0 + '@angular/core': 21.2.4 rxjs: ^6.5.3 || ^7.4.0 - '@angular/compiler-cli@21.2.0': - resolution: {integrity: sha512-gZd58p0/JjgdxMX3v+LjCB6e3dBIfNVr/YzXoh55TfffdBCUQY94hl1+DFQkJ72K5EX+1zbaz03dIm30kw1bGw==} + '@angular/compiler-cli@21.2.4': + resolution: {integrity: sha512-vGjd7DZo/Ox50pQCm5EycmBu91JclimPtZoyNXu/2hSxz3oAkzwiHCwlHwk2g58eheSSp+lYtYRLmHAqSVZLjg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} hasBin: true peerDependencies: - '@angular/compiler': 21.2.0 + '@angular/compiler': 21.2.4 typescript: '>=5.9 <6.1' peerDependenciesMeta: typescript: optional: true - '@angular/compiler@21.2.0': - resolution: {integrity: sha512-0RPkma8UVNpse/VJcXT9w6SKzTMz4J/uMGj0l9enM1frg9xrx1fwi/lLmaVV9Nr9LfqPjQdxNFFlvaBB7g/2zg==} + '@angular/compiler@21.2.4': + resolution: {integrity: sha512-9+ulVK3idIo/Tu4X2ic7/V0+Uj7pqrOAbOuIirYe6Ymm3AjexuFRiGBbfcH0VJhQ5cf8TvIJ1fuh+MI4JiRIxA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - '@angular/core@21.2.0': - resolution: {integrity: sha512-VnTbmZq3g3Q+s3nCZ8VUDMLjMezOg/bqUxAJ/DrRWCrEcTP5JO3mrNPs3FHj+qlB0T+BQP7uQv6QTzPVKybwoA==} + '@angular/core@21.2.4': + resolution: {integrity: sha512-2+gd67ZuXHpGOqeb2o7XZPueEWEP81eJza2tSHkT5QMV8lnYllDEmaNnkPxnIjSLGP1O3PmiXxo4z8ibHkLZwg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/compiler': 21.2.0 + '@angular/compiler': 21.2.4 rxjs: ^6.5.3 || ^7.4.0 zone.js: ~0.15.0 || ~0.16.0 peerDependenciesMeta: @@ -548,50 +562,50 @@ packages: zone.js: optional: true - '@angular/forms@21.2.0': - resolution: {integrity: sha512-NduUtPWLauH/FLayEDkLyaKAGqKzXbcfO7468LOWCXN3crhNVQyIWRQPOUcdpoJwDAGLpN85m3DhJhXNnA9c5w==} + '@angular/forms@21.2.4': + resolution: {integrity: sha512-1fOhctA9ADEBYjI3nPQUR5dHsK2+UWAjup37Ksldk/k0w8UpD5YsN7JVNvsDMZRFMucKYcGykPblU7pABtsqnQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/common': 21.2.0 - '@angular/core': 21.2.0 - '@angular/platform-browser': 21.2.0 + '@angular/common': 21.2.4 + '@angular/core': 21.2.4 + '@angular/platform-browser': 21.2.4 rxjs: ^6.5.3 || ^7.4.0 - '@angular/localize@21.2.0': - resolution: {integrity: sha512-blVjzwHSaKbFNCQN/RZy8rSbFgajMw3kBzGrDY08atMDOPn90L2nE4dot+9d0JlKAX2gL8Qfx44YgIWBI5MfsA==} + '@angular/localize@21.2.4': + resolution: {integrity: sha512-brKKeH+jaTlY4coIOinKQtitLCguQzyniKYtfrhCvZSN0ap4W4PljAT5w3l+1a8e7/ThM1JVQpqtVCCcJHJZSg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} hasBin: true peerDependencies: - '@angular/compiler': 21.2.0 - '@angular/compiler-cli': 21.2.0 + '@angular/compiler': 21.2.4 + '@angular/compiler-cli': 21.2.4 - '@angular/platform-browser-dynamic@21.2.0': - resolution: {integrity: sha512-eTHNTnTEP25eCyu4MJdPAAc/7Ib5XtR/dqUlzZdNoAldREPNw95FF12QMunvnen66v3CvCYdND8rAlbz2LkK7g==} + '@angular/platform-browser-dynamic@21.2.4': + resolution: {integrity: sha512-LRJLnGh4rdgD0+S5xuDd4YRm5bV8WP2e6F1Pe5rIr6N4V9ofgpB0/uOjYy9se99FJZjoyPnpxaKsp8+XA753Zg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/common': 21.2.0 - '@angular/compiler': 21.2.0 - '@angular/core': 21.2.0 - '@angular/platform-browser': 21.2.0 + '@angular/common': 21.2.4 + '@angular/compiler': 21.2.4 + '@angular/core': 21.2.4 + '@angular/platform-browser': 21.2.4 - '@angular/platform-browser@21.2.0': - resolution: {integrity: sha512-IUGukpvvT2B5Dl76qzk6rY7UIHUT9u4BhT2AwVz+5JqcX9KwQtYD17Gt7wj6bvIgCXKWG+CfN8Zd9DECOCYWjg==} + '@angular/platform-browser@21.2.4': + resolution: {integrity: sha512-1A9e/cQVu+3BkRCktLcO3RZGuw8NOTHw1frUUrpAz+iMyvIT4sDRFbL+U1g8qmOCZqRNC1Pi1HZfZ1kl6kvrcQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/animations': 21.2.0 - '@angular/common': 21.2.0 - '@angular/core': 21.2.0 + '@angular/animations': 21.2.4 + '@angular/common': 21.2.4 + '@angular/core': 21.2.4 peerDependenciesMeta: '@angular/animations': optional: true - '@angular/router@21.2.0': - resolution: {integrity: sha512-siliJ+jJRUCRZ0cdkqc7zww9Didz56Z0Z2YPIuR2n5TZLiuJY+jAf6xotXKp/v6v8XoGJwLiRNipGgNDRIAlWA==} + '@angular/router@21.2.4': + resolution: {integrity: sha512-OjWze4XT8i2MThcBXMv7ru1k6/5L6QYZbcXuseqimFCHm2avEJ+mXPovY066fMBZJhqbXdjB82OhHAWkIHjglQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/common': 21.2.0 - '@angular/core': 21.2.0 - '@angular/platform-browser': 21.2.0 + '@angular/common': 21.2.4 + '@angular/core': 21.2.4 + '@angular/platform-browser': 21.2.4 rxjs: ^6.5.3 || ^7.4.0 '@asamuzakjp/css-color@3.2.0': @@ -2847,8 +2861,8 @@ packages: cpu: [x64] os: [win32] - '@schematics/angular@21.2.0': - resolution: {integrity: sha512-GQUIeGzZwCT9/W5MAkKnkwETROPbA1eRmy3JF56jLmvr95tJnypGOG8jGYy0d+tcEVujIouh48r4J3bJQg5mrw==} + '@schematics/angular@21.2.2': + resolution: {integrity: sha512-Ywa6HDtX7TRBQZTVMMnxX3Mk7yVnG8KtSFaXWrkx779+q8tqYdBwNwAqbNd4Zatr1GccKaz9xcptHJta5+DTxw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} '@sigstore/bundle@4.0.0': @@ -6873,7 +6887,7 @@ snapshots: '@angular-builders/common@5.0.3(@types/node@25.3.3)(chokidar@5.0.0)(typescript@5.9.3)': dependencies: - '@angular-devkit/core': 21.2.0(chokidar@5.0.0) + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) ts-node: 10.9.2(@types/node@25.3.3)(typescript@5.9.3) tsconfig-paths: 4.2.0 transitivePeerDependencies: @@ -6883,14 +6897,14 @@ snapshots: - chokidar - typescript - '@angular-builders/custom-webpack@21.0.3(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@25.3.3)(ts-node@10.9.2(@types/node@25.3.3)(typescript@5.9.3)))(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.7.0)': + '@angular-builders/custom-webpack@21.0.3(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@25.3.3)(ts-node@10.9.2(@types/node@25.3.3)(typescript@5.9.3)))(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.7.0)': dependencies: '@angular-builders/common': 5.0.3(@types/node@25.3.3)(chokidar@5.0.0)(typescript@5.9.3) '@angular-devkit/architect': 0.2101.2(chokidar@5.0.0) - '@angular-devkit/build-angular': 21.1.2(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@25.3.3)(ts-node@10.9.2(@types/node@25.3.3)(typescript@5.9.3)))(jiti@2.6.1)(typescript@5.9.3)(yaml@2.7.0) - '@angular-devkit/core': 21.2.0(chokidar@5.0.0) - '@angular/build': 21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.7.0) - '@angular/compiler-cli': 21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3) + '@angular-devkit/build-angular': 21.1.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@25.3.3)(ts-node@10.9.2(@types/node@25.3.3)(typescript@5.9.3)))(jiti@2.6.1)(typescript@5.9.3)(yaml@2.7.0) + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) + '@angular/build': 21.2.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.7.0) + '@angular/compiler-cli': 21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3) lodash: 4.17.23 webpack-merge: 6.0.1 transitivePeerDependencies: @@ -6936,17 +6950,17 @@ snapshots: - webpack-cli - yaml - '@angular-builders/jest@21.0.3(b3fc6e706e4ec543940067da51c1bcc4)': + '@angular-builders/jest@21.0.3(d3759a42701812e83e3b36381edcbc70)': dependencies: '@angular-builders/common': 5.0.3(@types/node@25.3.3)(chokidar@5.0.0)(typescript@5.9.3) '@angular-devkit/architect': 0.2101.2(chokidar@5.0.0) - '@angular-devkit/build-angular': 21.1.2(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@25.3.3)(ts-node@10.9.2(@types/node@25.3.3)(typescript@5.9.3)))(jiti@2.6.1)(typescript@5.9.3)(yaml@2.7.0) - '@angular-devkit/core': 21.2.0(chokidar@5.0.0) - '@angular/compiler-cli': 21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3) - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) - '@angular/platform-browser-dynamic': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))) + '@angular-devkit/build-angular': 21.1.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@25.3.3)(ts-node@10.9.2(@types/node@25.3.3)(typescript@5.9.3)))(jiti@2.6.1)(typescript@5.9.3)(yaml@2.7.0) + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) + '@angular/compiler-cli': 21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser-dynamic': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))) jest: 30.2.0(@types/node@25.3.3)(ts-node@10.9.2(@types/node@25.3.3)(typescript@5.9.3)) - jest-preset-angular: 16.1.1(c76dc1c8ec36d3a138dfbfdecb5c07d6) + jest-preset-angular: 16.1.1(d878552686fd57cfb81e628ed4a9814b) lodash: 4.17.23 transitivePeerDependencies: - '@angular/platform-browser' @@ -6976,14 +6990,21 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-devkit/build-angular@21.1.2(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@25.3.3)(ts-node@10.9.2(@types/node@25.3.3)(typescript@5.9.3)))(jiti@2.6.1)(typescript@5.9.3)(yaml@2.7.0)': + '@angular-devkit/architect@0.2102.2(chokidar@5.0.0)': + dependencies: + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) + rxjs: 7.8.2 + transitivePeerDependencies: + - chokidar + + '@angular-devkit/build-angular@21.1.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@25.3.3)(ts-node@10.9.2(@types/node@25.3.3)(typescript@5.9.3)))(jiti@2.6.1)(typescript@5.9.3)(yaml@2.7.0)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2101.2(chokidar@5.0.0) '@angular-devkit/build-webpack': 0.2101.2(chokidar@5.0.0)(webpack-dev-server@5.2.2(tslib@2.8.1)(webpack@5.105.3))(webpack@5.104.1(esbuild@0.27.2)) '@angular-devkit/core': 21.1.2(chokidar@5.0.0) - '@angular/build': 21.1.2(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.7.0) - '@angular/compiler-cli': 21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3) + '@angular/build': 21.1.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.7.0) + '@angular/compiler-cli': 21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3) '@babel/core': 7.28.5 '@babel/generator': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 @@ -6994,7 +7015,7 @@ snapshots: '@babel/preset-env': 7.28.5(@babel/core@7.28.5) '@babel/runtime': 7.28.4 '@discoveryjs/json-ext': 0.6.3 - '@ngtools/webpack': 21.1.2(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.2)) + '@ngtools/webpack': 21.1.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.2)) ansi-colors: 4.1.3 autoprefixer: 10.4.23(postcss@8.5.6) babel-loader: 10.0.0(@babel/core@7.28.5)(webpack@5.104.1(esbuild@0.27.2)) @@ -7035,9 +7056,9 @@ snapshots: webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.104.1(esbuild@0.27.2)) optionalDependencies: - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) - '@angular/localize': 21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0) - '@angular/platform-browser': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/localize': 21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4) + '@angular/platform-browser': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)) esbuild: 0.27.2 jest: 30.2.0(@types/node@25.3.3)(ts-node@10.9.2(@types/node@25.3.3)(typescript@5.9.3)) jest-environment-jsdom: 30.2.0(canvas@3.0.0) @@ -7095,9 +7116,20 @@ snapshots: optionalDependencies: chokidar: 5.0.0 - '@angular-devkit/schematics@21.2.0(chokidar@5.0.0)': + '@angular-devkit/core@21.2.2(chokidar@5.0.0)': dependencies: - '@angular-devkit/core': 21.2.0(chokidar@5.0.0) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.3 + rxjs: 7.8.2 + source-map: 0.7.6 + optionalDependencies: + chokidar: 5.0.0 + + '@angular-devkit/schematics@21.2.2(chokidar@5.0.0)': + dependencies: + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) jsonc-parser: 3.3.1 magic-string: 0.30.21 ora: 9.3.0 @@ -7105,11 +7137,11 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-eslint/builder@21.3.0(@angular/cli@21.2.0(@types/node@25.3.3)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': + '@angular-eslint/builder@21.3.0(@angular/cli@21.2.2(@types/node@25.3.3)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@angular-devkit/architect': 0.2102.0(chokidar@5.0.0) - '@angular-devkit/core': 21.2.0(chokidar@5.0.0) - '@angular/cli': 21.2.0(@types/node@25.3.3)(chokidar@5.0.0) + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) + '@angular/cli': 21.2.2(@types/node@25.3.3)(chokidar@5.0.0) eslint: 10.0.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -7138,13 +7170,13 @@ snapshots: ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 - '@angular-eslint/schematics@21.3.0(@angular-eslint/template-parser@21.3.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(@angular/cli@21.2.0(@types/node@25.3.3)(chokidar@5.0.0))(@typescript-eslint/types@8.54.0)(@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(chokidar@5.0.0)(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': + '@angular-eslint/schematics@21.3.0(@angular-eslint/template-parser@21.3.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(@angular/cli@21.2.2(@types/node@25.3.3)(chokidar@5.0.0))(@typescript-eslint/types@8.54.0)(@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(chokidar@5.0.0)(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@angular-devkit/core': 21.2.0(chokidar@5.0.0) - '@angular-devkit/schematics': 21.2.0(chokidar@5.0.0) + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) + '@angular-devkit/schematics': 21.2.2(chokidar@5.0.0) '@angular-eslint/eslint-plugin': 21.3.0(@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) '@angular-eslint/eslint-plugin-template': 21.3.0(@angular-eslint/template-parser@21.3.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/types@8.54.0)(@typescript-eslint/utils@8.54.0(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.2(jiti@2.6.1))(typescript@5.9.3) - '@angular/cli': 21.2.0(@types/node@25.3.3)(chokidar@5.0.0) + '@angular/cli': 21.2.2(@types/node@25.3.3)(chokidar@5.0.0) ignore: 7.0.5 semver: 7.7.4 strip-json-comments: 3.1.1 @@ -7170,12 +7202,12 @@ snapshots: eslint: 10.0.2(jiti@2.6.1) typescript: 5.9.3 - '@angular/build@21.1.2(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.7.0)': + '@angular/build@21.1.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.7.0)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2101.2(chokidar@5.0.0) - '@angular/compiler': 21.2.0 - '@angular/compiler-cli': 21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3) + '@angular/compiler': 21.2.4 + '@angular/compiler-cli': 21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3) '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-split-export-declaration': 7.24.7 @@ -7204,9 +7236,9 @@ snapshots: vite: 7.3.0(@types/node@25.3.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.7.0) watchpack: 2.5.0 optionalDependencies: - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) - '@angular/localize': 21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0) - '@angular/platform-browser': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/localize': 21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4) + '@angular/platform-browser': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)) less: 4.4.2 lmdb: 3.4.4 postcss: 8.5.6 @@ -7223,12 +7255,12 @@ snapshots: - tsx - yaml - '@angular/build@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.7.0)': + '@angular/build@21.2.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(@types/node@25.3.3)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(yaml@2.7.0)': dependencies: '@ampproject/remapping': 2.3.0 - '@angular-devkit/architect': 0.2102.0(chokidar@5.0.0) - '@angular/compiler': 21.2.0 - '@angular/compiler-cli': 21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3) + '@angular-devkit/architect': 0.2102.2(chokidar@5.0.0) + '@angular/compiler': 21.2.4 + '@angular/compiler-cli': 21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3) '@babel/core': 7.29.0 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-split-export-declaration': 7.24.7 @@ -7257,9 +7289,9 @@ snapshots: vite: 7.3.1(@types/node@25.3.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.3)(terser@5.44.1)(yaml@2.7.0) watchpack: 2.5.1 optionalDependencies: - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) - '@angular/localize': 21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0) - '@angular/platform-browser': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/localize': 21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4) + '@angular/platform-browser': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)) less: 4.4.2 lmdb: 3.5.1 postcss: 8.5.6 @@ -7276,24 +7308,24 @@ snapshots: - tsx - yaml - '@angular/cdk@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2)': + '@angular/cdk@21.2.2(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2)': dependencies: - '@angular/common': 21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) - '@angular/platform-browser': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)) parse5: 8.0.0 rxjs: 7.8.2 tslib: 2.8.1 - '@angular/cli@21.2.0(@types/node@25.3.3)(chokidar@5.0.0)': + '@angular/cli@21.2.2(@types/node@25.3.3)(chokidar@5.0.0)': dependencies: - '@angular-devkit/architect': 0.2102.0(chokidar@5.0.0) - '@angular-devkit/core': 21.2.0(chokidar@5.0.0) - '@angular-devkit/schematics': 21.2.0(chokidar@5.0.0) + '@angular-devkit/architect': 0.2102.2(chokidar@5.0.0) + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) + '@angular-devkit/schematics': 21.2.2(chokidar@5.0.0) '@inquirer/prompts': 7.10.1(@types/node@25.3.3) '@listr2/prompt-adapter-inquirer': 3.0.5(@inquirer/prompts@7.10.1(@types/node@25.3.3))(@types/node@25.3.3)(listr2@9.0.5) '@modelcontextprotocol/sdk': 1.26.0(zod@4.3.6) - '@schematics/angular': 21.2.0(chokidar@5.0.0) + '@schematics/angular': 21.2.2(chokidar@5.0.0) '@yarnpkg/lockfile': 1.1.0 algoliasearch: 5.48.1 ini: 6.0.0 @@ -7311,15 +7343,15 @@ snapshots: - chokidar - supports-color - '@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2)': + '@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2)': dependencies: - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) rxjs: 7.8.2 tslib: 2.8.1 - '@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3)': + '@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3)': dependencies: - '@angular/compiler': 21.2.0 + '@angular/compiler': 21.2.4 '@babel/core': 7.29.0 '@jridgewell/sourcemap-codec': 1.5.5 chokidar: 5.0.0 @@ -7333,31 +7365,31 @@ snapshots: transitivePeerDependencies: - supports-color - '@angular/compiler@21.2.0': + '@angular/compiler@21.2.4': dependencies: tslib: 2.8.1 - '@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)': + '@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)': dependencies: rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@angular/compiler': 21.2.0 + '@angular/compiler': 21.2.4 zone.js: 0.16.1 - '@angular/forms@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2)': + '@angular/forms@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2)': dependencies: - '@angular/common': 21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) - '@angular/platform-browser': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)) '@standard-schema/spec': 1.1.0 rxjs: 7.8.2 tslib: 2.8.1 - '@angular/localize@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0)': + '@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)': dependencies: - '@angular/compiler': 21.2.0 - '@angular/compiler-cli': 21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3) + '@angular/compiler': 21.2.4 + '@angular/compiler-cli': 21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3) '@babel/core': 7.29.0 '@types/babel__core': 7.20.5 tinyglobby: 0.2.15 @@ -7365,25 +7397,25 @@ snapshots: transitivePeerDependencies: - supports-color - '@angular/platform-browser-dynamic@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))': + '@angular/platform-browser-dynamic@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))': dependencies: - '@angular/common': 21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) - '@angular/compiler': 21.2.0 - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) - '@angular/platform-browser': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/compiler': 21.2.4 + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)) tslib: 2.8.1 - '@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))': + '@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))': dependencies: - '@angular/common': 21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) tslib: 2.8.1 - '@angular/router@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2)': + '@angular/router@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2)': dependencies: - '@angular/common': 21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) - '@angular/platform-browser': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)) rxjs: 7.8.2 tslib: 2.8.1 @@ -9202,35 +9234,35 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@ng-bootstrap/ng-bootstrap@20.0.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/forms@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2))(@angular/localize@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0))(@popperjs/core@2.11.8)(rxjs@7.8.2)': + '@ng-bootstrap/ng-bootstrap@20.0.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/forms@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@popperjs/core@2.11.8)(rxjs@7.8.2)': dependencies: - '@angular/common': 21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) - '@angular/forms': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) - '@angular/localize': 21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/forms': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@angular/localize': 21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4) '@popperjs/core': 2.11.8 rxjs: 7.8.2 tslib: 2.8.1 - '@ng-select/ng-select@21.4.1(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/forms@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2))': + '@ng-select/ng-select@21.4.1(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/forms@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2))': dependencies: - '@angular/common': 21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) - '@angular/forms': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/forms': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) tslib: 2.8.1 - ? '@ngneat/dirty-check-forms@3.0.3(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/forms@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2))(@angular/router@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2))(lodash-es@4.17.21)(rxjs@7.8.2)' + ? '@ngneat/dirty-check-forms@3.0.3(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/forms@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2))(@angular/router@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2))(lodash-es@4.17.21)(rxjs@7.8.2)' : dependencies: - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) - '@angular/forms': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) - '@angular/router': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/forms': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@angular/router': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) lodash-es: 4.17.21 rxjs: 7.8.2 tslib: 2.8.1 - '@ngtools/webpack@21.1.2(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.2))': + '@ngtools/webpack@21.1.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(typescript@5.9.3)(webpack@5.104.1(esbuild@0.27.2))': dependencies: - '@angular/compiler-cli': 21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3) + '@angular/compiler-cli': 21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3) typescript: 5.9.3 webpack: 5.104.1(esbuild@0.27.2) @@ -9587,10 +9619,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true - '@schematics/angular@21.2.0(chokidar@5.0.0)': + '@schematics/angular@21.2.2(chokidar@5.0.0)': dependencies: - '@angular-devkit/core': 21.2.0(chokidar@5.0.0) - '@angular-devkit/schematics': 21.2.0(chokidar@5.0.0) + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) + '@angular-devkit/schematics': 21.2.2(chokidar@5.0.0) jsonc-parser: 3.3.1 transitivePeerDependencies: - chokidar @@ -10626,7 +10658,7 @@ snapshots: postcss-modules-scope: 3.2.1(postcss@8.5.6) postcss-modules-values: 4.0.0(postcss@8.5.6) postcss-value-parser: 4.2.0 - semver: 7.7.3 + semver: 7.7.4 optionalDependencies: webpack: 5.104.1(esbuild@0.27.2) @@ -11756,12 +11788,12 @@ snapshots: optionalDependencies: jest-resolve: 30.2.0 - jest-preset-angular@16.1.1(c76dc1c8ec36d3a138dfbfdecb5c07d6): + jest-preset-angular@16.1.1(d878552686fd57cfb81e628ed4a9814b): dependencies: - '@angular/compiler-cli': 21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3) - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) - '@angular/platform-browser': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)) - '@angular/platform-browser-dynamic': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.0)(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))) + '@angular/compiler-cli': 21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/platform-browser': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/platform-browser-dynamic': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))) '@jest/environment-jsdom-abstract': 30.2.0(canvas@3.0.0)(jsdom@26.1.0(canvas@3.0.0)) bs-logger: 0.2.6 esbuild-wasm: 0.27.3 @@ -12373,46 +12405,46 @@ snapshots: neo-async@2.6.2: {} - ngx-bootstrap-icons@1.9.3(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)): + ngx-bootstrap-icons@1.9.3(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)): dependencies: - '@angular/common': 21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) tslib: 2.8.1 - ngx-color@10.1.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)): + ngx-color@10.1.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)): dependencies: - '@angular/common': 21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) '@ctrl/tinycolor': 4.2.0 material-colors: 1.2.6 tslib: 2.8.1 - ngx-cookie-service@21.1.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)): + ngx-cookie-service@21.1.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)): dependencies: - '@angular/common': 21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) tslib: 2.8.1 - ngx-device-detector@11.0.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)): + ngx-device-detector@11.0.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)): dependencies: - '@angular/common': 21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) tslib: 2.8.1 - ngx-ui-tour-core@16.0.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/router@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2))(rxjs@7.8.2): + ngx-ui-tour-core@16.0.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/router@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2))(rxjs@7.8.2): dependencies: - '@angular/common': 21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) - '@angular/router': 21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/router': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) rxjs: 7.8.2 tslib: 2.8.1 - ngx-ui-tour-ng-bootstrap@18.0.0(9f28d3e6eaf246a683609aafac107126): + ngx-ui-tour-ng-bootstrap@18.0.0(f247d97663488c516a027bc34de144d4): dependencies: - '@angular/common': 21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) - '@angular/core': 21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1) - '@ng-bootstrap/ng-bootstrap': 20.0.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/forms@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2))(@angular/localize@21.2.0(@angular/compiler-cli@21.2.0(@angular/compiler@21.2.0)(typescript@5.9.3))(@angular/compiler@21.2.0))(@popperjs/core@2.11.8)(rxjs@7.8.2) - ngx-ui-tour-core: 16.0.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/router@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.0(@angular/common@21.2.0(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.0(@angular/compiler@21.2.0)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2))(rxjs@7.8.2) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1) + '@ng-bootstrap/ng-bootstrap': 20.0.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/forms@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@popperjs/core@2.11.8)(rxjs@7.8.2) + ngx-ui-tour-core: 16.0.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/router@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2))(rxjs@7.8.2) tslib: 2.8.1 transitivePeerDependencies: - '@angular/router' @@ -12737,7 +12769,7 @@ snapshots: cosmiconfig: 9.0.0(typescript@5.9.3) jiti: 2.6.1 postcss: 8.5.6 - semver: 7.7.3 + semver: 7.7.4 optionalDependencies: webpack: 5.104.1(esbuild@0.27.2) transitivePeerDependencies: @@ -13745,7 +13777,7 @@ snapshots: vite@7.3.0(@types/node@25.3.3)(jiti@2.6.1)(less@4.4.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.7.0): dependencies: - esbuild: 0.27.2 + esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 From 01abacab52092d0deca1e38c45990e4fef6214df Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 14 Mar 2026 05:09:13 +0000 Subject: [PATCH 06/38] Auto translate strings --- src-ui/messages.xlf | 70 ++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index e7088e90d..919ee388d 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -5,14 +5,14 @@ Close - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/alert/alert.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/alert/alert.ts 50 Slide of - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/carousel/carousel.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/carousel/carousel.ts 131,135 Currently selected slide number read by screen reader @@ -20,114 +20,114 @@ Previous - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/carousel/carousel.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/carousel/carousel.ts 159,162 Next - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/carousel/carousel.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/carousel/carousel.ts 202,203 Select month - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/datepicker/datepicker-navigation-select.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/datepicker/datepicker-navigation-select.ts 91 - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/datepicker/datepicker-navigation-select.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/datepicker/datepicker-navigation-select.ts 91 Select year - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/datepicker/datepicker-navigation-select.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/datepicker/datepicker-navigation-select.ts 91 - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/datepicker/datepicker-navigation-select.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/datepicker/datepicker-navigation-select.ts 91 Previous month - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/datepicker/datepicker-navigation.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/datepicker/datepicker-navigation.ts 83,85 - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/datepicker/datepicker-navigation.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/datepicker/datepicker-navigation.ts 112 Next month - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/datepicker/datepicker-navigation.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/datepicker/datepicker-navigation.ts 112 - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/datepicker/datepicker-navigation.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/datepicker/datepicker-navigation.ts 112 «« - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/pagination/pagination-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/pagination/pagination-config.ts 20 « - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/pagination/pagination-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/pagination/pagination-config.ts 20 » - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/pagination/pagination-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/pagination/pagination-config.ts 20 »» - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/pagination/pagination-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/pagination/pagination-config.ts 20 First - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/pagination/pagination-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/pagination/pagination-config.ts 20 Previous - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/pagination/pagination-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/pagination/pagination-config.ts 20 Next - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/pagination/pagination-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/pagination/pagination-config.ts 20 Last - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/pagination/pagination-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/pagination/pagination-config.ts 20 @@ -135,105 +135,105 @@ - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/progressbar/progressbar.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/progressbar/progressbar.ts 41,42 HH - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/timepicker/timepicker-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/timepicker/timepicker-config.ts 21 Hours - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/timepicker/timepicker-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/timepicker/timepicker-config.ts 21 MM - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/timepicker/timepicker-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/timepicker/timepicker-config.ts 21 Minutes - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/timepicker/timepicker-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/timepicker/timepicker-config.ts 21 Increment hours - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/timepicker/timepicker-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/timepicker/timepicker-config.ts 21 Decrement hours - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/timepicker/timepicker-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/timepicker/timepicker-config.ts 21 Increment minutes - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/timepicker/timepicker-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/timepicker/timepicker-config.ts 21 Decrement minutes - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/timepicker/timepicker-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/timepicker/timepicker-config.ts 21 SS - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/timepicker/timepicker-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/timepicker/timepicker-config.ts 21 Seconds - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/timepicker/timepicker-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/timepicker/timepicker-config.ts 21 Increment seconds - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/timepicker/timepicker-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/timepicker/timepicker-config.ts 21 Decrement seconds - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/timepicker/timepicker-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/timepicker/timepicker-config.ts 21 - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/timepicker/timepicker-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/timepicker/timepicker-config.ts 21 Close - node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.0_@angular+core@21.2.0_@angular+_fdecb2f5429dfeda6301fd300107de5b/node_modules/src/toast/toast-config.ts + node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@20.0.0_@angular+common@21.2.4_@angular+core@21.2.4_@angular+_a674c967733fd102e5fef61ea5e6b837/node_modules/src/toast/toast-config.ts 54 From 9d69705e268ee49d37abe4658ea741ed0bea016d Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Sat, 14 Mar 2026 12:53:52 -0700 Subject: [PATCH 07/38] Feature: Add progress information to the classifier training for a better ux (#12331) --- src/documents/classifier.py | 18 ++++++++- .../commands/document_create_classifier.py | 27 +++++++++++-- src/documents/tasks.py | 8 +++- src/documents/tests/test_management.py | 38 +++++++++++++++---- 4 files changed, 77 insertions(+), 14 deletions(-) diff --git a/src/documents/classifier.py b/src/documents/classifier.py index 1e9da7ce6..87934ab52 100644 --- a/src/documents/classifier.py +++ b/src/documents/classifier.py @@ -9,6 +9,7 @@ from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: + from collections.abc import Callable from collections.abc import Iterator from datetime import datetime @@ -191,7 +192,12 @@ class DocumentClassifier: target_file_temp.rename(target_file) - def train(self) -> bool: + def train( + self, + status_callback: Callable[[str], None] | None = None, + ) -> bool: + notify = status_callback if status_callback is not None else lambda _: None + # Get non-inbox documents docs_queryset = ( Document.objects.exclude( @@ -213,6 +219,7 @@ class DocumentClassifier: # Step 1: Extract and preprocess training data from the database. logger.debug("Gathering data from database...") + notify(f"Gathering data from {docs_queryset.count()} document(s)...") hasher = sha256() for doc in docs_queryset: y = -1 @@ -290,6 +297,7 @@ class DocumentClassifier: # Step 2: vectorize data logger.debug("Vectorizing data...") + notify("Vectorizing document content...") def content_generator() -> Iterator[str]: """ @@ -316,6 +324,7 @@ class DocumentClassifier: # Step 3: train the classifiers if num_tags > 0: logger.debug("Training tags classifier...") + notify(f"Training tags classifier ({num_tags} tag(s))...") if num_tags == 1: # Special case where only one tag has auto: @@ -339,6 +348,9 @@ class DocumentClassifier: if num_correspondents > 0: logger.debug("Training correspondent classifier...") + notify( + f"Training correspondent classifier ({num_correspondents} correspondent(s))...", + ) self.correspondent_classifier = MLPClassifier(tol=0.01) self.correspondent_classifier.fit(data_vectorized, labels_correspondent) else: @@ -349,6 +361,9 @@ class DocumentClassifier: if num_document_types > 0: logger.debug("Training document type classifier...") + notify( + f"Training document type classifier ({num_document_types} type(s))...", + ) self.document_type_classifier = MLPClassifier(tol=0.01) self.document_type_classifier.fit(data_vectorized, labels_document_type) else: @@ -361,6 +376,7 @@ class DocumentClassifier: logger.debug( "Training storage paths classifier...", ) + notify(f"Training storage path classifier ({num_storage_paths} path(s))...") self.storage_path_classifier = MLPClassifier(tol=0.01) self.storage_path_classifier.fit( data_vectorized, diff --git a/src/documents/management/commands/document_create_classifier.py b/src/documents/management/commands/document_create_classifier.py index f7903aac7..b662195a7 100644 --- a/src/documents/management/commands/document_create_classifier.py +++ b/src/documents/management/commands/document_create_classifier.py @@ -1,13 +1,32 @@ -from django.core.management.base import BaseCommand +from __future__ import annotations +import time + +from documents.management.commands.base import PaperlessCommand from documents.tasks import train_classifier -class Command(BaseCommand): +class Command(PaperlessCommand): help = ( "Trains the classifier on your data and saves the resulting models to a " "file. The document consumer will then automatically use this new model." ) + supports_progress_bar = False + supports_multiprocessing = False - def handle(self, *args, **options): - train_classifier(scheduled=False) + def handle(self, *args, **options) -> None: + start = time.monotonic() + + with ( + self.buffered_logging("paperless.tasks"), + self.buffered_logging("paperless.classifier"), + ): + train_classifier( + scheduled=False, + status_callback=lambda msg: self.console.print(f" {msg}"), + ) + + elapsed = time.monotonic() - start + self.console.print( + f"[green]✓[/green] Classifier training complete ({elapsed:.1f}s)", + ) diff --git a/src/documents/tasks.py b/src/documents/tasks.py index 86b6b2716..378695731 100644 --- a/src/documents/tasks.py +++ b/src/documents/tasks.py @@ -100,7 +100,11 @@ def index_reindex(*, iter_wrapper: IterWrapper[Document] = _identity) -> None: @shared_task -def train_classifier(*, scheduled=True) -> None: +def train_classifier( + *, + scheduled=True, + status_callback: Callable[[str], None] | None = None, +) -> None: task = PaperlessTask.objects.create( type=PaperlessTask.TaskType.SCHEDULED_TASK if scheduled @@ -136,7 +140,7 @@ def train_classifier(*, scheduled=True) -> None: classifier = DocumentClassifier() try: - if classifier.train(): + if classifier.train(status_callback=status_callback): logger.info( f"Saving updated classifier model to {settings.MODEL_FILE}...", ) diff --git a/src/documents/tests/test_management.py b/src/documents/tests/test_management.py index 03959a85b..2a62173b1 100644 --- a/src/documents/tests/test_management.py +++ b/src/documents/tests/test_management.py @@ -1,7 +1,10 @@ +from __future__ import annotations + import filecmp import shutil from io import StringIO from pathlib import Path +from typing import TYPE_CHECKING from unittest import mock import pytest @@ -11,6 +14,9 @@ from django.core.management import call_command from django.test import TestCase from django.test import override_settings +if TYPE_CHECKING: + from pytest_mock import MockerFixture + from documents.file_handling import generate_filename from documents.models import Document from documents.tasks import update_document_content_maybe_archive_file @@ -135,14 +141,32 @@ class TestRenamer(DirectoriesMixin, FileSystemAssertsMixin, TestCase): @pytest.mark.management -class TestCreateClassifier(TestCase): - @mock.patch( - "documents.management.commands.document_create_classifier.train_classifier", - ) - def test_create_classifier(self, m) -> None: - call_command("document_create_classifier") +class TestCreateClassifier: + def test_create_classifier(self, mocker: MockerFixture) -> None: + m = mocker.patch( + "documents.management.commands.document_create_classifier.train_classifier", + ) - m.assert_called_once() + call_command("document_create_classifier", "--skip-checks") + + m.assert_called_once_with(scheduled=False, status_callback=mocker.ANY) + assert callable(m.call_args.kwargs["status_callback"]) + + def test_create_classifier_callback_output(self, mocker: MockerFixture) -> None: + """Callback passed to train_classifier writes each phase message to the console.""" + m = mocker.patch( + "documents.management.commands.document_create_classifier.train_classifier", + ) + + def invoke_callback(**kwargs): + kwargs["status_callback"]("Vectorizing document content...") + + m.side_effect = invoke_callback + + stdout = StringIO() + call_command("document_create_classifier", "--skip-checks", stdout=stdout) + + assert "Vectorizing document content..." in stdout.getvalue() @pytest.mark.management From 06b2d5102c187718fbd0bfc480c766d2bec0bb45 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:13:08 -0700 Subject: [PATCH 08/38] Fix GHSA-59xh-5vwx-4c4q --- src/documents/tests/test_api_permissions.py | 72 +++++++++++++++++++++ src/paperless/views.py | 53 +++++++++++++-- 2 files changed, 119 insertions(+), 6 deletions(-) diff --git a/src/documents/tests/test_api_permissions.py b/src/documents/tests/test_api_permissions.py index 31b860745..00a613cff 100644 --- a/src/documents/tests/test_api_permissions.py +++ b/src/documents/tests/test_api_permissions.py @@ -888,6 +888,19 @@ class TestApiUser(DirectoriesMixin, APITestCase): self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + response = self.client.post( + f"{self.ENDPOINT}", + json.dumps( + { + "username": "user4", + "is_superuser": "true", + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.client.force_authenticate(user2) response = self.client.patch( @@ -920,6 +933,65 @@ class TestApiUser(DirectoriesMixin, APITestCase): returned_user1 = User.objects.get(pk=user1.pk) self.assertEqual(returned_user1.is_superuser, False) + def test_only_superusers_can_create_or_alter_staff_status(self): + """ + GIVEN: + - Existing user account + WHEN: + - API request is made to add a user account with staff status + - API request is made to change staff status + THEN: + - Only superusers can change staff status + """ + + user1 = User.objects.create_user(username="user1") + user1.user_permissions.add(*Permission.objects.all()) + user2 = User.objects.create_superuser(username="user2") + + self.client.force_authenticate(user1) + + response = self.client.patch( + f"{self.ENDPOINT}{user1.pk}/", + json.dumps( + { + "is_staff": "true", + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + response = self.client.post( + f"{self.ENDPOINT}", + json.dumps( + { + "username": "user3", + "is_staff": 1, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + self.client.force_authenticate(user2) + + response = self.client.patch( + f"{self.ENDPOINT}{user1.pk}/", + json.dumps( + { + "is_staff": True, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + returned_user1 = User.objects.get(pk=user1.pk) + self.assertEqual(returned_user1.is_staff, True) + class TestApiGroup(DirectoriesMixin, APITestCase): ENDPOINT = "/api/groups/" diff --git a/src/paperless/views.py b/src/paperless/views.py index e79c0e668..2a2ee9518 100644 --- a/src/paperless/views.py +++ b/src/paperless/views.py @@ -25,6 +25,8 @@ from drf_spectacular.utils import extend_schema_view from rest_framework.authtoken.models import Token from rest_framework.authtoken.views import ObtainAuthToken from rest_framework.decorators import action +from rest_framework.exceptions import ValidationError +from rest_framework.fields import BooleanField from rest_framework.filters import OrderingFilter from rest_framework.generics import GenericAPIView from rest_framework.pagination import PageNumberPagination @@ -103,6 +105,7 @@ class FaviconView(View): class UserViewSet(ModelViewSet): + _BOOL_NOT_PROVIDED = object() model = User queryset = User.objects.exclude( @@ -116,27 +119,65 @@ class UserViewSet(ModelViewSet): filterset_class = UserFilterSet ordering_fields = ("username",) + @staticmethod + def _parse_requested_bool(data, key: str): + if key not in data: + return UserViewSet._BOOL_NOT_PROVIDED + try: + return BooleanField().to_internal_value(data.get(key)) + except ValidationError: + # Let serializer validation report invalid values as 400 responses + return UserViewSet._BOOL_NOT_PROVIDED + def create(self, request, *args, **kwargs): - if not request.user.is_superuser and request.data.get("is_superuser") is True: - return HttpResponseForbidden( - "Superuser status can only be granted by a superuser", - ) + requested_is_superuser = self._parse_requested_bool( + request.data, + "is_superuser", + ) + requested_is_staff = self._parse_requested_bool(request.data, "is_staff") + + if not request.user.is_superuser: + if requested_is_superuser is True: + return HttpResponseForbidden( + "Superuser status can only be granted by a superuser", + ) + if requested_is_staff is True: + return HttpResponseForbidden( + "Staff status can only be granted by a superuser", + ) + return super().create(request, *args, **kwargs) def update(self, request, *args, **kwargs): user_to_update: User = self.get_object() + if not request.user.is_superuser and user_to_update.is_superuser: return HttpResponseForbidden( "Superusers can only be modified by other superusers", ) + + requested_is_superuser = self._parse_requested_bool( + request.data, + "is_superuser", + ) + requested_is_staff = self._parse_requested_bool(request.data, "is_staff") + if ( not request.user.is_superuser - and request.data.get("is_superuser") is not None - and request.data.get("is_superuser") != user_to_update.is_superuser + and requested_is_superuser is not self._BOOL_NOT_PROVIDED + and requested_is_superuser != user_to_update.is_superuser ): return HttpResponseForbidden( "Superuser status can only be changed by a superuser", ) + if ( + not request.user.is_superuser + and requested_is_staff is not self._BOOL_NOT_PROVIDED + and requested_is_staff != user_to_update.is_staff + ): + return HttpResponseForbidden( + "Staff status can only be changed by a superuser", + ) return super().update(request, *args, **kwargs) @extend_schema( From 5f26c01c6f3bfeb8b94936fc9c2b7e05ed85f4d6 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:16:11 -0700 Subject: [PATCH 09/38] Bump version to 2.20.11 --- pyproject.toml | 2 +- src-ui/package.json | 2 +- src-ui/src/environments/environment.prod.ts | 2 +- src/paperless/version.py | 2 +- uv.lock | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6225913be..ae95894c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "paperless-ngx" -version = "2.20.10" +version = "2.20.11" description = "A community-supported supercharged document management system: scan, index and archive all your physical documents" readme = "README.md" requires-python = ">=3.10" diff --git a/src-ui/package.json b/src-ui/package.json index 69eaae1b7..33feeabb9 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -1,6 +1,6 @@ { "name": "paperless-ngx-ui", - "version": "2.20.10", + "version": "2.20.11", "scripts": { "preinstall": "npx only-allow pnpm", "ng": "ng", diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index 28a27911f..bafd0dfab 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -6,7 +6,7 @@ export const environment = { apiVersion: '9', // match src/paperless/settings.py appTitle: 'Paperless-ngx', tag: 'prod', - version: '2.20.10', + version: '2.20.11', webSocketHost: window.location.host, webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', webSocketBaseUrl: base_url.pathname + 'ws/', diff --git a/src/paperless/version.py b/src/paperless/version.py index 3f35bde70..9f9841618 100644 --- a/src/paperless/version.py +++ b/src/paperless/version.py @@ -1,6 +1,6 @@ from typing import Final -__version__: Final[tuple[int, int, int]] = (2, 20, 10) +__version__: Final[tuple[int, int, int]] = (2, 20, 11) # Version string like X.Y.Z __full_version_str__: Final[str] = ".".join(map(str, __version__)) # Version string like X.Y diff --git a/uv.lock b/uv.lock index f468b7f27..3e2b7d5cb 100644 --- a/uv.lock +++ b/uv.lock @@ -1991,7 +1991,7 @@ wheels = [ [[package]] name = "paperless-ngx" -version = "2.20.10" +version = "2.20.11" source = { virtual = "." } dependencies = [ { name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, From 1e00ad5f30d7012b97ba4952bbb2069e6c3eaf85 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 15 Mar 2026 18:47:18 -0700 Subject: [PATCH 10/38] Documentation: Add v2.20.11 changelog (#12356) * Changelog v2.20.11 - GHA * Update changelog for version 2.20.11 Added security advisory and fixed dropdown list issues. --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/changelog.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 404e6d355..ec2342060 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,29 @@ # Changelog +## paperless-ngx 2.20.11 + +### Security + +- Resolve [GHSA-59xh-5vwx-4c4q](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-59xh-5vwx-4c4q) + +### Bug Fixes + +- Fix: correct dropdown list active color in dark mode [@shamoon](https://github.com/shamoon) ([#12328](https://github.com/paperless-ngx/paperless-ngx/pull/12328)) +- Fixhancement: clear descendant selections in dropdown when parent toggled [@shamoon](https://github.com/shamoon) ([#12326](https://github.com/paperless-ngx/paperless-ngx/pull/12326)) +- Fix: prevent wrapping with larger amounts of tags on small cards, reset moreTags setting to correct count [@shamoon](https://github.com/shamoon) ([#12302](https://github.com/paperless-ngx/paperless-ngx/pull/12302)) +- Fix: prevent stale db filename during workflow actions [@shamoon](https://github.com/shamoon) ([#12289](https://github.com/paperless-ngx/paperless-ngx/pull/12289)) + +### All App Changes + +
+4 changes + +- Fix: correct dropdown list active color in dark mode [@shamoon](https://github.com/shamoon) ([#12328](https://github.com/paperless-ngx/paperless-ngx/pull/12328)) +- Fixhancement: clear descendant selections in dropdown when parent toggled [@shamoon](https://github.com/shamoon) ([#12326](https://github.com/paperless-ngx/paperless-ngx/pull/12326)) +- Fix: prevent wrapping with larger amounts of tags on small cards, reset moreTags setting to correct count [@shamoon](https://github.com/shamoon) ([#12302](https://github.com/paperless-ngx/paperless-ngx/pull/12302)) +- Fix: prevent stale db filename during workflow actions [@shamoon](https://github.com/shamoon) ([#12289](https://github.com/paperless-ngx/paperless-ngx/pull/12289)) +
+ ## paperless-ngx 2.20.10 ### Bug Fixes From 6034f17c871ea5c1ac39ed78d0b1e237931db85a Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 15 Mar 2026 18:51:06 -0700 Subject: [PATCH 11/38] Format changelog --- docs/changelog.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index e791ddf17..569e30e29 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,24 +4,24 @@ ### Security -- Resolve [GHSA-59xh-5vwx-4c4q](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-59xh-5vwx-4c4q) +- Resolve [GHSA-59xh-5vwx-4c4q](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-59xh-5vwx-4c4q) ### Bug Fixes -- Fix: correct dropdown list active color in dark mode [@shamoon](https://github.com/shamoon) ([#12328](https://github.com/paperless-ngx/paperless-ngx/pull/12328)) -- Fixhancement: clear descendant selections in dropdown when parent toggled [@shamoon](https://github.com/shamoon) ([#12326](https://github.com/paperless-ngx/paperless-ngx/pull/12326)) -- Fix: prevent wrapping with larger amounts of tags on small cards, reset moreTags setting to correct count [@shamoon](https://github.com/shamoon) ([#12302](https://github.com/paperless-ngx/paperless-ngx/pull/12302)) -- Fix: prevent stale db filename during workflow actions [@shamoon](https://github.com/shamoon) ([#12289](https://github.com/paperless-ngx/paperless-ngx/pull/12289)) +- Fix: correct dropdown list active color in dark mode [@shamoon](https://github.com/shamoon) ([#12328](https://github.com/paperless-ngx/paperless-ngx/pull/12328)) +- Fixhancement: clear descendant selections in dropdown when parent toggled [@shamoon](https://github.com/shamoon) ([#12326](https://github.com/paperless-ngx/paperless-ngx/pull/12326)) +- Fix: prevent wrapping with larger amounts of tags on small cards, reset moreTags setting to correct count [@shamoon](https://github.com/shamoon) ([#12302](https://github.com/paperless-ngx/paperless-ngx/pull/12302)) +- Fix: prevent stale db filename during workflow actions [@shamoon](https://github.com/shamoon) ([#12289](https://github.com/paperless-ngx/paperless-ngx/pull/12289)) ### All App Changes
4 changes -- Fix: correct dropdown list active color in dark mode [@shamoon](https://github.com/shamoon) ([#12328](https://github.com/paperless-ngx/paperless-ngx/pull/12328)) -- Fixhancement: clear descendant selections in dropdown when parent toggled [@shamoon](https://github.com/shamoon) ([#12326](https://github.com/paperless-ngx/paperless-ngx/pull/12326)) -- Fix: prevent wrapping with larger amounts of tags on small cards, reset moreTags setting to correct count [@shamoon](https://github.com/shamoon) ([#12302](https://github.com/paperless-ngx/paperless-ngx/pull/12302)) -- Fix: prevent stale db filename during workflow actions [@shamoon](https://github.com/shamoon) ([#12289](https://github.com/paperless-ngx/paperless-ngx/pull/12289)) +- Fix: correct dropdown list active color in dark mode [@shamoon](https://github.com/shamoon) ([#12328](https://github.com/paperless-ngx/paperless-ngx/pull/12328)) +- Fixhancement: clear descendant selections in dropdown when parent toggled [@shamoon](https://github.com/shamoon) ([#12326](https://github.com/paperless-ngx/paperless-ngx/pull/12326)) +- Fix: prevent wrapping with larger amounts of tags on small cards, reset moreTags setting to correct count [@shamoon](https://github.com/shamoon) ([#12302](https://github.com/paperless-ngx/paperless-ngx/pull/12302)) +- Fix: prevent stale db filename during workflow actions [@shamoon](https://github.com/shamoon) ([#12289](https://github.com/paperless-ngx/paperless-ngx/pull/12289))
## paperless-ngx 2.20.10 From 6b8ff9763d823f3afb0aa538bd44f23faddef60e Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 01:52:49 +0000 Subject: [PATCH 12/38] Auto translate strings --- src-ui/messages.xlf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 919ee388d..9f2993a4c 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -5736,7 +5736,7 @@ Open filter src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts - 788 + 823 From 2bb4af2be673b746e648817e804661b9a2248173 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 15 Mar 2026 22:52:02 -0700 Subject: [PATCH 13/38] Change: sort custom fields alphabetically by default (#12358) --- src/documents/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/documents/views.py b/src/documents/views.py index a69293ee9..6f9caa987 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -3923,7 +3923,7 @@ class CustomFieldViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet): document_count_through = CustomFieldInstance document_count_source_field = "field_id" - queryset = CustomField.objects.all().order_by("-created") + queryset = CustomField.objects.all().order_by("name") @extend_schema_view( From 866c9fd858f1171c50ba82123896f9f9ef1cdb61 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 15 Mar 2026 23:50:54 -0700 Subject: [PATCH 14/38] Fix: correct merge bulk edit indentation --- src/documents/bulk_edit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/documents/bulk_edit.py b/src/documents/bulk_edit.py index 490dfa629..8dbcdb8a4 100644 --- a/src/documents/bulk_edit.py +++ b/src/documents/bulk_edit.py @@ -576,8 +576,8 @@ def merge( except Exception: restore_archive_serial_numbers(backup) raise - else: - consume_task.delay() + else: + consume_task.delay() return "OK" From 1caa3eb8aac2981ec64ef2dffa0073c531fd6436 Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Mon, 16 Mar 2026 08:10:35 -0700 Subject: [PATCH 15/38] Chore: Disables the system checks for management commands in tests and when unnecessary (#12332) --- .../management/commands/document_importer.py | 2 +- .../test_management_sanity_checker.py | 21 ++++- src/documents/tests/test_api_schema.py | 7 +- src/documents/tests/test_management.py | 16 ++-- .../tests/test_management_exporter.py | 72 ++++++++++++++---- src/documents/tests/test_management_fuzzy.py | 1 + .../tests/test_management_importer.py | 20 ++++- .../tests/test_management_retagger.py | 76 +++++++++++++++---- .../tests/test_management_superuser.py | 1 + .../tests/test_management_thumbnails.py | 11 ++- src/paperless_mail/tests/test_mail.py | 2 +- 11 files changed, 179 insertions(+), 50 deletions(-) diff --git a/src/documents/management/commands/document_importer.py b/src/documents/management/commands/document_importer.py index 20d50e41b..c0056c062 100644 --- a/src/documents/management/commands/document_importer.py +++ b/src/documents/management/commands/document_importer.py @@ -205,7 +205,7 @@ class Command(CryptMixin, PaperlessCommand): ContentType.objects.all().delete() Permission.objects.all().delete() for manifest_path in self.manifest_paths: - call_command("loaddata", manifest_path) + call_command("loaddata", manifest_path, skip_checks=True) except (FieldDoesNotExist, DeserializationError, IntegrityError) as e: self.stdout.write(self.style.ERROR("Database import failed")) if ( diff --git a/src/documents/tests/management/test_management_sanity_checker.py b/src/documents/tests/management/test_management_sanity_checker.py index 64f21e966..f0752ef10 100644 --- a/src/documents/tests/management/test_management_sanity_checker.py +++ b/src/documents/tests/management/test_management_sanity_checker.py @@ -163,13 +163,23 @@ class TestRenderResultsSummary: class TestDocumentSanityCheckerCommand: def test_no_issues(self, sample_doc: Document) -> None: out = StringIO() - call_command("document_sanity_checker", "--no-progress-bar", stdout=out) + call_command( + "document_sanity_checker", + "--no-progress-bar", + stdout=out, + skip_checks=True, + ) assert "No issues detected" in out.getvalue() def test_missing_original(self, sample_doc: Document) -> None: Path(sample_doc.source_path).unlink() out = StringIO() - call_command("document_sanity_checker", "--no-progress-bar", stdout=out) + call_command( + "document_sanity_checker", + "--no-progress-bar", + stdout=out, + skip_checks=True, + ) output = out.getvalue() assert "ERROR" in output assert "Original of document does not exist" in output @@ -187,7 +197,12 @@ class TestDocumentSanityCheckerCommand: Path(doc.thumbnail_path).touch() out = StringIO() - call_command("document_sanity_checker", "--no-progress-bar", stdout=out) + call_command( + "document_sanity_checker", + "--no-progress-bar", + stdout=out, + skip_checks=True, + ) output = out.getvalue() assert "ERROR" in output assert "Checksum mismatch. Stored: abc, actual:" in output diff --git a/src/documents/tests/test_api_schema.py b/src/documents/tests/test_api_schema.py index e14762f68..d8b023e6a 100644 --- a/src/documents/tests/test_api_schema.py +++ b/src/documents/tests/test_api_schema.py @@ -12,7 +12,12 @@ class TestApiSchema(APITestCase): Test that the schema is valid """ try: - call_command("spectacular", "--validate", "--fail-on-warn") + call_command( + "spectacular", + "--validate", + "--fail-on-warn", + skip_checks=True, + ) except CommandError as e: self.fail(f"Schema validation failed: {e}") diff --git a/src/documents/tests/test_management.py b/src/documents/tests/test_management.py index 2a62173b1..7719d21dd 100644 --- a/src/documents/tests/test_management.py +++ b/src/documents/tests/test_management.py @@ -41,7 +41,7 @@ class TestArchiver(DirectoriesMixin, FileSystemAssertsMixin, TestCase): doc = self.make_models() shutil.copy(sample_file, Path(self.dirs.originals_dir) / f"{doc.id:07}.pdf") - call_command("document_archiver", "--processes", "1") + call_command("document_archiver", "--processes", "1", skip_checks=True) def test_handle_document(self) -> None: doc = self.make_models() @@ -106,12 +106,12 @@ class TestArchiver(DirectoriesMixin, FileSystemAssertsMixin, TestCase): class TestMakeIndex(TestCase): @mock.patch("documents.management.commands.document_index.index_reindex") def test_reindex(self, m) -> None: - call_command("document_index", "reindex") + call_command("document_index", "reindex", skip_checks=True) m.assert_called_once() @mock.patch("documents.management.commands.document_index.index_optimize") def test_optimize(self, m) -> None: - call_command("document_index", "optimize") + call_command("document_index", "optimize", skip_checks=True) m.assert_called_once() @@ -128,7 +128,7 @@ class TestRenamer(DirectoriesMixin, FileSystemAssertsMixin, TestCase): Path(doc.archive_path).touch() with override_settings(FILENAME_FORMAT="{correspondent}/{title}"): - call_command("document_renamer") + call_command("document_renamer", skip_checks=True) doc2 = Document.objects.get(id=doc.id) @@ -147,7 +147,7 @@ class TestCreateClassifier: "documents.management.commands.document_create_classifier.train_classifier", ) - call_command("document_create_classifier", "--skip-checks") + call_command("document_create_classifier", skip_checks=True) m.assert_called_once_with(scheduled=False, status_callback=mocker.ANY) assert callable(m.call_args.kwargs["status_callback"]) @@ -164,7 +164,7 @@ class TestCreateClassifier: m.side_effect = invoke_callback stdout = StringIO() - call_command("document_create_classifier", "--skip-checks", stdout=stdout) + call_command("document_create_classifier", skip_checks=True, stdout=stdout) assert "Vectorizing document content..." in stdout.getvalue() @@ -176,7 +176,7 @@ class TestConvertMariaDBUUID(TestCase): m.alter_field.return_value = None stdout = StringIO() - call_command("convert_mariadb_uuid", stdout=stdout) + call_command("convert_mariadb_uuid", stdout=stdout, skip_checks=True) m.assert_called_once() @@ -191,6 +191,6 @@ class TestPruneAuditLogs(TestCase): object_id=1, action=LogEntry.Action.CREATE, ) - call_command("prune_audit_logs") + call_command("prune_audit_logs", skip_checks=True) self.assertEqual(LogEntry.objects.count(), 0) diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index 6cd8cd729..9307bab45 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -180,7 +180,7 @@ class TestExportImport( if data_only: args += ["--data-only"] - call_command(*args) + call_command(*args, skip_checks=True) with (self.target / "manifest.json").open() as f: manifest = json.load(f) @@ -272,7 +272,12 @@ class TestExportImport( GroupObjectPermission.objects.all().delete() self.assertEqual(Document.objects.count(), 0) - call_command("document_importer", "--no-progress-bar", self.target) + call_command( + "document_importer", + "--no-progress-bar", + self.target, + skip_checks=True, + ) self.assertEqual(Document.objects.count(), 4) self.assertEqual(Tag.objects.count(), 1) self.assertEqual(Correspondent.objects.count(), 1) @@ -438,7 +443,8 @@ class TestExportImport( filename="0000010.pdf", mime_type="application/pdf", ) - self.assertRaises(FileNotFoundError, call_command, "document_exporter", target) + with self.assertRaises(FileNotFoundError): + call_command("document_exporter", target, skip_checks=True) def test_export_zipped(self) -> None: """ @@ -458,7 +464,7 @@ class TestExportImport( args = ["document_exporter", self.target, "--zip"] - call_command(*args) + call_command(*args, skip_checks=True) expected_file = str( self.target / f"export-{timezone.localdate().isoformat()}.zip", @@ -493,7 +499,7 @@ class TestExportImport( with override_settings( FILENAME_FORMAT="{created_year}/{correspondent}/{title}", ): - call_command(*args) + call_command(*args, skip_checks=True) expected_file = str( self.target / f"export-{timezone.localdate().isoformat()}.zip", @@ -538,7 +544,7 @@ class TestExportImport( args = ["document_exporter", self.target, "--zip", "--delete"] - call_command(*args) + call_command(*args, skip_checks=True) expected_file = str( self.target / f"export-{timezone.localdate().isoformat()}.zip", @@ -565,7 +571,7 @@ class TestExportImport( args = ["document_exporter", "/tmp/foo/bar"] with self.assertRaises(CommandError) as e: - call_command(*args) + call_command(*args, skip_checks=True) self.assertEqual("That path doesn't exist", str(e.exception)) @@ -583,7 +589,7 @@ class TestExportImport( args = ["document_exporter", tmp_file.name] with self.assertRaises(CommandError) as e: - call_command(*args) + call_command(*args, skip_checks=True) self.assertEqual("That path isn't a directory", str(e.exception)) @@ -602,7 +608,7 @@ class TestExportImport( args = ["document_exporter", tmp_dir] with self.assertRaises(CommandError) as e: - call_command(*args) + call_command(*args, skip_checks=True) self.assertEqual( "That path doesn't appear to be writable", @@ -647,7 +653,12 @@ class TestExportImport( self.assertEqual(Document.objects.count(), 4) Document.objects.all().delete() self.assertEqual(Document.objects.count(), 0) - call_command("document_importer", "--no-progress-bar", self.target) + call_command( + "document_importer", + "--no-progress-bar", + self.target, + skip_checks=True, + ) self.assertEqual(Document.objects.count(), 4) def test_no_thumbnail(self) -> None: @@ -690,7 +701,12 @@ class TestExportImport( self.assertEqual(Document.objects.count(), 4) Document.objects.all().delete() self.assertEqual(Document.objects.count(), 0) - call_command("document_importer", "--no-progress-bar", self.target) + call_command( + "document_importer", + "--no-progress-bar", + self.target, + skip_checks=True, + ) self.assertEqual(Document.objects.count(), 4) def test_split_manifest(self) -> None: @@ -721,7 +737,12 @@ class TestExportImport( Document.objects.all().delete() CustomFieldInstance.objects.all().delete() self.assertEqual(Document.objects.count(), 0) - call_command("document_importer", "--no-progress-bar", self.target) + call_command( + "document_importer", + "--no-progress-bar", + self.target, + skip_checks=True, + ) self.assertEqual(Document.objects.count(), 4) self.assertEqual(CustomFieldInstance.objects.count(), 1) @@ -746,7 +767,12 @@ class TestExportImport( self.assertEqual(Document.objects.count(), 4) Document.objects.all().delete() self.assertEqual(Document.objects.count(), 0) - call_command("document_importer", "--no-progress-bar", self.target) + call_command( + "document_importer", + "--no-progress-bar", + self.target, + skip_checks=True, + ) self.assertEqual(Document.objects.count(), 4) def test_folder_prefix_with_split(self) -> None: @@ -771,7 +797,12 @@ class TestExportImport( self.assertEqual(Document.objects.count(), 4) Document.objects.all().delete() self.assertEqual(Document.objects.count(), 0) - call_command("document_importer", "--no-progress-bar", self.target) + call_command( + "document_importer", + "--no-progress-bar", + self.target, + skip_checks=True, + ) self.assertEqual(Document.objects.count(), 4) def test_import_db_transaction_failed(self) -> None: @@ -813,7 +844,12 @@ class TestExportImport( self.user = User.objects.create(username="temp_admin") with self.assertRaises(IntegrityError): - call_command("document_importer", "--no-progress-bar", self.target) + call_command( + "document_importer", + "--no-progress-bar", + self.target, + skip_checks=True, + ) self.assertEqual(ContentType.objects.count(), num_content_type_objects) self.assertEqual(Permission.objects.count(), num_permission_objects + 1) @@ -864,6 +900,7 @@ class TestExportImport( "--no-progress-bar", "--data-only", self.target, + skip_checks=True, ) self.assertEqual(Document.objects.all().count(), 4) @@ -923,6 +960,7 @@ class TestCryptExportImport( "--passphrase", "securepassword", self.target, + skip_checks=True, ) self.assertIsFile(self.target / "metadata.json") @@ -948,6 +986,7 @@ class TestCryptExportImport( "--passphrase", "securepassword", self.target, + skip_checks=True, ) account = MailAccount.objects.first() @@ -976,6 +1015,7 @@ class TestCryptExportImport( "--passphrase", "securepassword", self.target, + skip_checks=True, ) with self.assertRaises(CommandError) as err: @@ -983,6 +1023,7 @@ class TestCryptExportImport( "document_importer", "--no-progress-bar", self.target, + skip_checks=True, ) self.assertEqual( err.msg, @@ -1014,6 +1055,7 @@ class TestCryptExportImport( "--no-progress-bar", str(self.target), stdout=stdout, + skip_checks=True, ) stdout.seek(0) self.assertIn( diff --git a/src/documents/tests/test_management_fuzzy.py b/src/documents/tests/test_management_fuzzy.py index 195a3450d..7c4acabec 100644 --- a/src/documents/tests/test_management_fuzzy.py +++ b/src/documents/tests/test_management_fuzzy.py @@ -21,6 +21,7 @@ class TestFuzzyMatchCommand(TestCase): *args, stdout=stdout, stderr=stderr, + skip_checks=True, **kwargs, ) return stdout.getvalue(), stderr.getvalue() diff --git a/src/documents/tests/test_management_importer.py b/src/documents/tests/test_management_importer.py index 137b4004b..ef20cf895 100644 --- a/src/documents/tests/test_management_importer.py +++ b/src/documents/tests/test_management_importer.py @@ -41,6 +41,7 @@ class TestCommandImport( "document_importer", "--no-progress-bar", str(self.dirs.scratch_dir), + skip_checks=True, ) self.assertIn( "That directory doesn't appear to contain a manifest.json file.", @@ -67,6 +68,7 @@ class TestCommandImport( "document_importer", "--no-progress-bar", str(self.dirs.scratch_dir), + skip_checks=True, ) self.assertIn( "The manifest file contains a record which does not refer to an actual document file.", @@ -96,6 +98,7 @@ class TestCommandImport( "document_importer", "--no-progress-bar", str(self.dirs.scratch_dir), + skip_checks=True, ) self.assertIn('The manifest file refers to "noexist.pdf"', str(e.exception)) @@ -157,7 +160,7 @@ class TestCommandImport( - CommandError is raised indicating the issue """ with self.assertRaises(CommandError) as cm: - call_command("document_importer", Path("/tmp/notapath")) + call_command("document_importer", Path("/tmp/notapath"), skip_checks=True) self.assertIn("That path doesn't exist", str(cm.exception)) def test_import_source_not_readable(self) -> None: @@ -173,7 +176,7 @@ class TestCommandImport( path = Path(temp_dir) path.chmod(0o222) with self.assertRaises(CommandError) as cm: - call_command("document_importer", path) + call_command("document_importer", path, skip_checks=True) self.assertIn( "That path doesn't appear to be readable", str(cm.exception), @@ -193,7 +196,12 @@ class TestCommandImport( self.assertIsNotFile(path) with self.assertRaises(CommandError) as e: - call_command("document_importer", "--no-progress-bar", str(path)) + call_command( + "document_importer", + "--no-progress-bar", + str(path), + skip_checks=True, + ) self.assertIn("That path doesn't exist", str(e.exception)) def test_import_files_exist(self) -> None: @@ -218,6 +226,7 @@ class TestCommandImport( "--no-progress-bar", str(self.dirs.scratch_dir), stdout=stdout, + skip_checks=True, ) stdout.seek(0) self.assertIn( @@ -246,6 +255,7 @@ class TestCommandImport( "--no-progress-bar", str(self.dirs.scratch_dir), stdout=stdout, + skip_checks=True, ) stdout.seek(0) self.assertIn( @@ -282,6 +292,7 @@ class TestCommandImport( "--no-progress-bar", str(self.dirs.scratch_dir), stdout=stdout, + skip_checks=True, ) stdout.seek(0) self.assertIn( @@ -309,6 +320,7 @@ class TestCommandImport( "--no-progress-bar", str(self.dirs.scratch_dir), stdout=stdout, + skip_checks=True, ) stdout.seek(0) stdout_str = str(stdout.read()) @@ -338,6 +350,7 @@ class TestCommandImport( "--no-progress-bar", str(self.dirs.scratch_dir), stdout=stdout, + skip_checks=True, ) stdout.seek(0) stdout_str = str(stdout.read()) @@ -377,6 +390,7 @@ class TestCommandImport( "--no-progress-bar", str(zip_path), stdout=stdout, + skip_checks=True, ) stdout.seek(0) stdout_str = str(stdout.read()) diff --git a/src/documents/tests/test_management_retagger.py b/src/documents/tests/test_management_retagger.py index 3f669c00e..605068038 100644 --- a/src/documents/tests/test_management_retagger.py +++ b/src/documents/tests/test_management_retagger.py @@ -139,7 +139,7 @@ class TestRetaggerTags(DirectoriesMixin): @pytest.mark.usefixtures("documents") def test_add_tags(self, tags: TagTuple) -> None: tag_first, tag_second, *_ = tags - call_command("document_retagger", "--tags") + call_command("document_retagger", "--tags", skip_checks=True) d_first, d_second, d_unrelated, d_auto = _get_docs() assert d_first.tags.count() == 1 @@ -158,7 +158,7 @@ class TestRetaggerTags(DirectoriesMixin): tag_first, tag_second, tag_inbox, tag_no_match, _ = tags d1.tags.add(tag_second) - call_command("document_retagger", "--tags", "--overwrite") + call_command("document_retagger", "--tags", "--overwrite", skip_checks=True) d_first, d_second, d_unrelated, d_auto = _get_docs() @@ -180,7 +180,13 @@ class TestRetaggerTags(DirectoriesMixin): ], ) def test_suggest_does_not_apply_tags(self, extra_args: list[str]) -> None: - call_command("document_retagger", "--tags", "--suggest", *extra_args) + call_command( + "document_retagger", + "--tags", + "--suggest", + *extra_args, + skip_checks=True, + ) d_first, d_second, _, d_auto = _get_docs() assert d_first.tags.count() == 0 @@ -199,7 +205,7 @@ class TestRetaggerDocumentType(DirectoriesMixin): @pytest.mark.usefixtures("documents") def test_add_type(self, document_types: DocumentTypeTuple) -> None: dt_first, dt_second = document_types - call_command("document_retagger", "--document_type") + call_command("document_retagger", "--document_type", skip_checks=True) d_first, d_second, _, _ = _get_docs() assert d_first.document_type == dt_first @@ -214,7 +220,13 @@ class TestRetaggerDocumentType(DirectoriesMixin): ], ) def test_suggest_does_not_apply_document_type(self, extra_args: list[str]) -> None: - call_command("document_retagger", "--document_type", "--suggest", *extra_args) + call_command( + "document_retagger", + "--document_type", + "--suggest", + *extra_args, + skip_checks=True, + ) d_first, d_second, _, _ = _get_docs() assert d_first.document_type is None @@ -243,7 +255,12 @@ class TestRetaggerDocumentType(DirectoriesMixin): ) doc = DocumentFactory(content="ambiguous content") - call_command("document_retagger", "--document_type", *use_first_flag) + call_command( + "document_retagger", + "--document_type", + *use_first_flag, + skip_checks=True, + ) doc.refresh_from_db() assert (doc.document_type is not None) is expects_assignment @@ -260,7 +277,7 @@ class TestRetaggerCorrespondent(DirectoriesMixin): @pytest.mark.usefixtures("documents") def test_add_correspondent(self, correspondents: CorrespondentTuple) -> None: c_first, c_second = correspondents - call_command("document_retagger", "--correspondent") + call_command("document_retagger", "--correspondent", skip_checks=True) d_first, d_second, _, _ = _get_docs() assert d_first.correspondent == c_first @@ -275,7 +292,13 @@ class TestRetaggerCorrespondent(DirectoriesMixin): ], ) def test_suggest_does_not_apply_correspondent(self, extra_args: list[str]) -> None: - call_command("document_retagger", "--correspondent", "--suggest", *extra_args) + call_command( + "document_retagger", + "--correspondent", + "--suggest", + *extra_args, + skip_checks=True, + ) d_first, d_second, _, _ = _get_docs() assert d_first.correspondent is None @@ -304,7 +327,12 @@ class TestRetaggerCorrespondent(DirectoriesMixin): ) doc = DocumentFactory(content="ambiguous content") - call_command("document_retagger", "--correspondent", *use_first_flag) + call_command( + "document_retagger", + "--correspondent", + *use_first_flag, + skip_checks=True, + ) doc.refresh_from_db() assert (doc.correspondent is not None) is expects_assignment @@ -326,7 +354,7 @@ class TestRetaggerStoragePath(DirectoriesMixin): THEN matching documents get the correct path; existing path is unchanged """ sp1, sp2, sp3 = storage_paths - call_command("document_retagger", "--storage_path") + call_command("document_retagger", "--storage_path", skip_checks=True) d_first, d_second, d_unrelated, d_auto = _get_docs() assert d_first.storage_path == sp2 @@ -342,7 +370,12 @@ class TestRetaggerStoragePath(DirectoriesMixin): THEN the existing path is replaced by the newly matched path """ sp1, sp2, _ = storage_paths - call_command("document_retagger", "--storage_path", "--overwrite") + call_command( + "document_retagger", + "--storage_path", + "--overwrite", + skip_checks=True, + ) d_first, d_second, d_unrelated, d_auto = _get_docs() assert d_first.storage_path == sp2 @@ -373,7 +406,12 @@ class TestRetaggerStoragePath(DirectoriesMixin): ) doc = DocumentFactory(content="ambiguous content") - call_command("document_retagger", "--storage_path", *use_first_flag) + call_command( + "document_retagger", + "--storage_path", + *use_first_flag, + skip_checks=True, + ) doc.refresh_from_db() assert (doc.storage_path is not None) is expects_assignment @@ -402,7 +440,13 @@ class TestRetaggerIdRange(DirectoriesMixin): expected_count: int, ) -> None: DocumentFactory(content="NOT the first document") - call_command("document_retagger", "--tags", "--id-range", *id_range_args) + call_command( + "document_retagger", + "--tags", + "--id-range", + *id_range_args, + skip_checks=True, + ) tag_first, *_ = tags assert Document.objects.filter(tags__id=tag_first.id).count() == expected_count @@ -416,7 +460,7 @@ class TestRetaggerIdRange(DirectoriesMixin): ) def test_id_range_invalid_arguments_raise(self, args: list[str]) -> None: with pytest.raises((CommandError, SystemExit)): - call_command("document_retagger", *args) + call_command("document_retagger", *args, skip_checks=True) # --------------------------------------------------------------------------- @@ -430,12 +474,12 @@ class TestRetaggerEdgeCases(DirectoriesMixin): @pytest.mark.usefixtures("documents") def test_no_targets_exits_cleanly(self) -> None: """Calling the retagger with no classifier targets should not raise.""" - call_command("document_retagger") + call_command("document_retagger", skip_checks=True) @pytest.mark.usefixtures("documents") def test_inbox_only_skips_non_inbox_documents(self) -> None: """--inbox-only must restrict processing to documents with an inbox tag.""" - call_command("document_retagger", "--tags", "--inbox-only") + call_command("document_retagger", "--tags", "--inbox-only", skip_checks=True) d_first, _, d_unrelated, _ = _get_docs() assert d_first.tags.count() == 0 diff --git a/src/documents/tests/test_management_superuser.py b/src/documents/tests/test_management_superuser.py index 0a6bcb8cd..f2741ba53 100644 --- a/src/documents/tests/test_management_superuser.py +++ b/src/documents/tests/test_management_superuser.py @@ -20,6 +20,7 @@ class TestManageSuperUser(DirectoriesMixin, TestCase): "--no-color", stdout=out, stderr=StringIO(), + skip_checks=True, ) return out.getvalue() diff --git a/src/documents/tests/test_management_thumbnails.py b/src/documents/tests/test_management_thumbnails.py index 160cb4419..0b016f0cd 100644 --- a/src/documents/tests/test_management_thumbnails.py +++ b/src/documents/tests/test_management_thumbnails.py @@ -85,13 +85,20 @@ class TestMakeThumbnails(DirectoriesMixin, FileSystemAssertsMixin, TestCase): def test_command(self) -> None: self.assertIsNotFile(self.d1.thumbnail_path) self.assertIsNotFile(self.d2.thumbnail_path) - call_command("document_thumbnails", "--processes", "1") + call_command("document_thumbnails", "--processes", "1", skip_checks=True) self.assertIsFile(self.d1.thumbnail_path) self.assertIsFile(self.d2.thumbnail_path) def test_command_documentid(self) -> None: self.assertIsNotFile(self.d1.thumbnail_path) self.assertIsNotFile(self.d2.thumbnail_path) - call_command("document_thumbnails", "--processes", "1", "-d", f"{self.d1.id}") + call_command( + "document_thumbnails", + "--processes", + "1", + "-d", + f"{self.d1.id}", + skip_checks=True, + ) self.assertIsFile(self.d1.thumbnail_path) self.assertIsNotFile(self.d2.thumbnail_path) diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py index 162a764f2..72ee5331a 100644 --- a/src/paperless_mail/tests/test_mail.py +++ b/src/paperless_mail/tests/test_mail.py @@ -1665,7 +1665,7 @@ class TestManagementCommand(TestCase): "paperless_mail.management.commands.mail_fetcher.tasks.process_mail_accounts", ) def test_mail_fetcher(self, m) -> None: - call_command("mail_fetcher") + call_command("mail_fetcher", skip_checks=True) m.assert_called_once() From 58ebcc21be64102e18fbb56862526192896e88f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 09:57:23 -0700 Subject: [PATCH 16/38] Chore(deps): Bump pyjwt from 2.10.1 to 2.12.0 (#12335) Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.10.1 to 2.12.0. - [Release notes](https://github.com/jpadilla/pyjwt/releases) - [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst) - [Commits](https://github.com/jpadilla/pyjwt/compare/2.10.1...2.12.0) --- updated-dependencies: - dependency-name: pyjwt dependency-version: 2.12.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index 9d66db770..ef15b0e23 100644 --- a/uv.lock +++ b/uv.lock @@ -3683,11 +3683,11 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.10.1" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/10/e8192be5f38f3e8e7e046716de4cae33d56fd5ae08927a823bb916be36c1/pyjwt-2.12.0.tar.gz", hash = "sha256:2f62390b667cd8257de560b850bb5a883102a388829274147f1d724453f8fb02", size = 102511, upload-time = "2026-03-12T17:15:30.831Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, + { url = "https://files.pythonhosted.org/packages/15/70/70f895f404d363d291dcf62c12c85fdd47619ad9674ac0f53364d035925a/pyjwt-2.12.0-py3-none-any.whl", hash = "sha256:9bb459d1bdd0387967d287f5656bf7ec2b9a26645d1961628cda1764e087fd6e", size = 29700, upload-time = "2026-03-12T17:15:29.257Z" }, ] [package.optional-dependencies] From 54679a093ad0fafcb73b84dd18b31482efd29a20 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:36:00 -0700 Subject: [PATCH 17/38] Chore(deps): Bump pyopenssl from 25.3.0 to 26.0.0 (#12363) Bumps [pyopenssl](https://github.com/pyca/pyopenssl) from 25.3.0 to 26.0.0. - [Changelog](https://github.com/pyca/pyopenssl/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/pyopenssl/compare/25.3.0...26.0.0) --- updated-dependencies: - dependency-name: pyopenssl dependency-version: 26.0.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uv.lock b/uv.lock index ef15b0e23..d7a56fe5a 100644 --- a/uv.lock +++ b/uv.lock @@ -3710,15 +3710,15 @@ wheels = [ [[package]] name = "pyopenssl" -version = "25.3.0" +version = "26.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size = 184073, upload-time = "2025-09-17T00:32:21.037Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/11/a62e1d33b373da2b2c2cd9eb508147871c80f12b1cacde3c5d314922afdd/pyopenssl-26.0.0.tar.gz", hash = "sha256:f293934e52936f2e3413b89c6ce36df66a0b34ae1ea3a053b8c5020ff2f513fc", size = 185534, upload-time = "2026-03-15T14:28:26.353Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7d/d4f7d908fa8415571771b30669251d57c3cf313b36a856e6d7548ae01619/pyopenssl-26.0.0-py3-none-any.whl", hash = "sha256:df94d28498848b98cc1c0ffb8ef1e71e40210d3b0a8064c9d29571ed2904bf81", size = 57969, upload-time = "2026-03-15T14:28:24.864Z" }, ] [[package]] From 470018c0118183962aaa8fd01f923a6bfab68a8a Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:12:17 -0700 Subject: [PATCH 18/38] Chore: Mocks the celery and Redis pings so we don't wait for their timeout each time (#12354) --- src/documents/tests/test_api_status.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/documents/tests/test_api_status.py b/src/documents/tests/test_api_status.py index d2a092726..409f4bd7f 100644 --- a/src/documents/tests/test_api_status.py +++ b/src/documents/tests/test_api_status.py @@ -26,6 +26,23 @@ class TestSystemStatus(APITestCase): self.override = override_settings(MEDIA_ROOT=self.tmp_dir) self.override.enable() + # Mock slow network calls so tests don't block on real Redis/Celery timeouts. + # Individual tests that care about specific behaviour override these with + # their own @mock.patch decorators (which take precedence). + redis_patcher = mock.patch( + "redis.Redis.execute_command", + side_effect=Exception("Redis not available"), + ) + self.mock_redis = redis_patcher.start() + self.addCleanup(redis_patcher.stop) + + celery_patcher = mock.patch( + "celery.app.control.Inspect.ping", + side_effect=Exception("Celery not available"), + ) + self.mock_celery_ping = celery_patcher.start() + self.addCleanup(celery_patcher.stop) + def tearDown(self) -> None: super().tearDown() From 85fecac401130a3fa6b4537cdeeaa741be2bbe37 Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:15:03 -0700 Subject: [PATCH 19/38] Fix: don't try to usermod/groupmod when non-root + update docs (#12365) --- .../s6-overlay/s6-rc.d/init-modify-user/run | 11 +++++++++ docs/setup.md | 23 +++++++------------ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-modify-user/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-modify-user/run index aa617355d..f8430aee2 100755 --- a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-modify-user/run +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-modify-user/run @@ -2,6 +2,17 @@ # shellcheck shell=bash declare -r log_prefix="[init-user]" +# When the container is started as a non-root user (e.g. via `user: 999:999` +# in Docker Compose), usermod/groupmod require root and are meaningless. +# USERMAP_* variables only apply to the root-started path. +if [[ -n "${USER_IS_NON_ROOT}" ]]; then + if [[ -n "${USERMAP_UID}" || -n "${USERMAP_GID}" ]]; then + echo "${log_prefix} WARNING: USERMAP_UID/USERMAP_GID are set but have no effect when the container is started as a non-root user" + fi + echo "${log_prefix} Running as non-root user ($(id --user):$(id --group)), skipping UID/GID remapping" + exit 0 +fi + declare -r usermap_original_uid=$(id -u paperless) declare -r usermap_original_gid=$(id -g paperless) declare -r usermap_new_uid=${USERMAP_UID:-$usermap_original_uid} diff --git a/docs/setup.md b/docs/setup.md index 5795968ef..3b84fd729 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -140,24 +140,17 @@ a [superuser](usage.md#superusers) account. !!! warning - It is currently not possible to run the container rootless if additional languages are specified via `PAPERLESS_OCR_LANGUAGES`. + It is not possible to run the container rootless if additional languages are specified via `PAPERLESS_OCR_LANGUAGES`. -If you want to run Paperless as a rootless container, make this -change in `docker-compose.yml`: +If you want to run Paperless as a rootless container, set `user:` in `docker-compose.yml` to the UID and GID of your host user (use `id -u` and `id -g` to find these values). The container process starts directly as that user with no internal privilege remapping: -- Set the `user` running the container to map to the `paperless` - user in the container. This value (`user_id` below) should be - the same ID that `USERMAP_UID` and `USERMAP_GID` are set to in - `docker-compose.env`. See `USERMAP_UID` and `USERMAP_GID` - [here](configuration.md#docker). +```yaml +webserver: + image: ghcr.io/paperless-ngx/paperless-ngx:latest + user: '1000:1000' +``` -Your entry for Paperless should contain something like: - -> ``` -> webserver: -> image: ghcr.io/paperless-ngx/paperless-ngx:latest -> user: -> ``` +Do not combine this with `USERMAP_UID` or `USERMAP_GID`, which are intended for the non-rootless case described in step 3. **File systems without inotify support (e.g. NFS)** From 4d4f30b5f83fadef3d6ae609780330e4669d765c Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 16 Mar 2026 22:57:36 -0700 Subject: [PATCH 20/38] Security: validate outbound llm URLs and block internal endpoints --- docs/configuration.md | 6 ++ src/documents/tests/test_api_app_config.py | 15 ++++ src/documents/workflows/webhooks.py | 83 +++++++--------------- src/paperless/config.py | 2 + src/paperless/network.py | 76 ++++++++++++++++++++ src/paperless/serialisers.py | 16 +++++ src/paperless/settings/__init__.py | 4 ++ src/paperless_ai/client.py | 16 ++++- src/paperless_ai/embedding.py | 9 ++- src/paperless_ai/tests/test_client.py | 12 ++++ src/paperless_ai/tests/test_embedding.py | 14 ++++ 11 files changed, 192 insertions(+), 61 deletions(-) create mode 100644 src/paperless/network.py diff --git a/docs/configuration.md b/docs/configuration.md index ac148c68b..59bc56907 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1947,6 +1947,12 @@ current backend. If not supplied, defaults to "gpt-3.5-turbo" for OpenAI and "ll Defaults to None. +#### [`PAPERLESS_AI_LLM_ALLOW_INTERNAL_ENDPOINTS=`](#PAPERLESS_AI_LLM_ALLOW_INTERNAL_ENDPOINTS) {#PAPERLESS_AI_LLM_ALLOW_INTERNAL_ENDPOINTS} + +: If set to false, Paperless blocks AI endpoint URLs that resolve to non-public addresses (e.g., localhost, etc). + + Defaults to true, which allows internal endpoints. + #### [`PAPERLESS_AI_LLM_INDEX_TASK_CRON=`](#PAPERLESS_AI_LLM_INDEX_TASK_CRON) {#PAPERLESS_AI_LLM_INDEX_TASK_CRON} : Configures the schedule to update the AI embeddings of text content and metadata for all documents. Only performed if diff --git a/src/documents/tests/test_api_app_config.py b/src/documents/tests/test_api_app_config.py index 7717c3488..b946bc1bf 100644 --- a/src/documents/tests/test_api_app_config.py +++ b/src/documents/tests/test_api_app_config.py @@ -5,6 +5,7 @@ from unittest.mock import patch from django.contrib.auth.models import User from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import override_settings from rest_framework import status from rest_framework.test import APITestCase @@ -693,3 +694,17 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase): content_type="application/json", ) mock_update.assert_called_once() + + @override_settings(LLM_ALLOW_INTERNAL_ENDPOINTS=False) + def test_update_llm_endpoint_blocks_internal_endpoint_when_disallowed(self) -> None: + response = self.client.patch( + f"{self.ENDPOINT}1/", + json.dumps( + { + "llm_endpoint": "http://127.0.0.1:11434", + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("non-public address", str(response.data).lower()) diff --git a/src/documents/workflows/webhooks.py b/src/documents/workflows/webhooks.py index a0256873a..0c510a35d 100644 --- a/src/documents/workflows/webhooks.py +++ b/src/documents/workflows/webhooks.py @@ -1,12 +1,14 @@ -import ipaddress import logging -import socket -from urllib.parse import urlparse import httpx from celery import shared_task from django.conf import settings +from paperless.network import format_host_for_url +from paperless.network import is_public_ip +from paperless.network import resolve_hostname_ips +from paperless.network import validate_outbound_http_url + logger = logging.getLogger("paperless.workflows.webhooks") @@ -34,23 +36,19 @@ class WebhookTransport(httpx.HTTPTransport): raise httpx.ConnectError("No hostname in request URL") try: - addr_info = socket.getaddrinfo(hostname, None) - except socket.gaierror as e: - raise httpx.ConnectError(f"Could not resolve hostname: {hostname}") from e - - ips = [info[4][0] for info in addr_info if info and info[4]] - if not ips: - raise httpx.ConnectError(f"Could not resolve hostname: {hostname}") + ips = resolve_hostname_ips(hostname) + except ValueError as e: + raise httpx.ConnectError(str(e)) from e if not self.allow_internal: for ip_str in ips: - if not WebhookTransport.is_public_ip(ip_str): + if not is_public_ip(ip_str): raise httpx.ConnectError( f"Connection blocked: {hostname} resolves to a non-public address", ) ip_str = ips[0] - formatted_ip = self._format_ip_for_url(ip_str) + formatted_ip = format_host_for_url(ip_str) new_headers = httpx.Headers(request.headers) if "host" in new_headers: @@ -69,40 +67,6 @@ class WebhookTransport(httpx.HTTPTransport): return super().handle_request(request) - def _format_ip_for_url(self, ip: str) -> str: - """ - Format IP address for use in URL (wrap IPv6 in brackets) - """ - try: - ip_obj = ipaddress.ip_address(ip) - if ip_obj.version == 6: - return f"[{ip}]" - return ip - except ValueError: - return ip - - @staticmethod - def is_public_ip(ip: str | int) -> bool: - try: - obj = ipaddress.ip_address(ip) - return not ( - obj.is_private - or obj.is_loopback - or obj.is_link_local - or obj.is_multicast - or obj.is_unspecified - ) - except ValueError: # pragma: no cover - return False - - @staticmethod - def resolve_first_ip(host: str) -> str | None: - try: - info = socket.getaddrinfo(host, None) - return info[0][4][0] if info else None - except Exception: # pragma: no cover - return None - @shared_task( retry_backoff=True, @@ -118,21 +82,24 @@ def send_webhook( *, as_json: bool = False, ): - p = urlparse(url) - if p.scheme.lower() not in settings.WEBHOOKS_ALLOWED_SCHEMES or not p.hostname: - logger.warning("Webhook blocked: invalid scheme/hostname") + try: + parsed = validate_outbound_http_url( + url, + allowed_schemes=settings.WEBHOOKS_ALLOWED_SCHEMES, + allowed_ports=settings.WEBHOOKS_ALLOWED_PORTS, + # Internal-address checks happen in transport to preserve ConnectError behavior. + allow_internal=True, + ) + except ValueError as e: + logger.warning("Webhook blocked: %s", e) + raise + + hostname = parsed.hostname + if hostname is None: # pragma: no cover raise ValueError("Invalid URL scheme or hostname.") - port = p.port or (443 if p.scheme == "https" else 80) - if ( - len(settings.WEBHOOKS_ALLOWED_PORTS) > 0 - and port not in settings.WEBHOOKS_ALLOWED_PORTS - ): - logger.warning("Webhook blocked: port not permitted") - raise ValueError("Destination port not permitted.") - transport = WebhookTransport( - hostname=p.hostname, + hostname=hostname, allow_internal=settings.WEBHOOKS_ALLOW_INTERNAL_REQUESTS, ) diff --git a/src/paperless/config.py b/src/paperless/config.py index 65d790ff4..fd2c1620d 100644 --- a/src/paperless/config.py +++ b/src/paperless/config.py @@ -188,6 +188,7 @@ class AIConfig(BaseConfig): llm_model: str = dataclasses.field(init=False) llm_api_key: str = dataclasses.field(init=False) llm_endpoint: str = dataclasses.field(init=False) + llm_allow_internal_endpoints: bool = dataclasses.field(init=False) def __post_init__(self) -> None: app_config = self._get_config_instance() @@ -203,6 +204,7 @@ class AIConfig(BaseConfig): self.llm_model = app_config.llm_model or settings.LLM_MODEL self.llm_api_key = app_config.llm_api_key or settings.LLM_API_KEY self.llm_endpoint = app_config.llm_endpoint or settings.LLM_ENDPOINT + self.llm_allow_internal_endpoints = settings.LLM_ALLOW_INTERNAL_ENDPOINTS @property def llm_index_enabled(self) -> bool: diff --git a/src/paperless/network.py b/src/paperless/network.py new file mode 100644 index 000000000..4af00052f --- /dev/null +++ b/src/paperless/network.py @@ -0,0 +1,76 @@ +import ipaddress +import socket +from collections.abc import Collection +from urllib.parse import ParseResult +from urllib.parse import urlparse + + +def is_public_ip(ip: str | int) -> bool: + try: + obj = ipaddress.ip_address(ip) + return not ( + obj.is_private + or obj.is_loopback + or obj.is_link_local + or obj.is_multicast + or obj.is_unspecified + ) + except ValueError: # pragma: no cover + return False + + +def resolve_hostname_ips(hostname: str) -> list[str]: + try: + addr_info = socket.getaddrinfo(hostname, None) + except socket.gaierror as e: + raise ValueError(f"Could not resolve hostname: {hostname}") from e + + ips = [info[4][0] for info in addr_info if info and info[4]] + if not ips: + raise ValueError(f"Could not resolve hostname: {hostname}") + return ips + + +def format_host_for_url(host: str) -> str: + """ + Format IP address for URL use (wrap IPv6 in brackets). + """ + try: + ip_obj = ipaddress.ip_address(host) + if ip_obj.version == 6: + return f"[{host}]" + return host + except ValueError: + return host + + +def validate_outbound_http_url( + url: str, + *, + allowed_schemes: Collection[str] = ("http", "https"), + allowed_ports: Collection[int] | None = None, + allow_internal: bool = False, +) -> ParseResult: + parsed = urlparse(url) + scheme = parsed.scheme.lower() + + if scheme not in allowed_schemes or not parsed.hostname: + raise ValueError("Invalid URL scheme or hostname.") + + default_port = 443 if scheme == "https" else 80 + try: + port = parsed.port or default_port + except ValueError as e: + raise ValueError("Invalid URL scheme or hostname.") from e + + if allowed_ports and port not in allowed_ports: + raise ValueError("Destination port not permitted.") + + if not allow_internal: + for ip_str in resolve_hostname_ips(parsed.hostname): + if not is_public_ip(ip_str): + raise ValueError( + f"Connection blocked: {parsed.hostname} resolves to a non-public address", + ) + + return parsed diff --git a/src/paperless/serialisers.py b/src/paperless/serialisers.py index 97a2bee7e..495ae018a 100644 --- a/src/paperless/serialisers.py +++ b/src/paperless/serialisers.py @@ -6,6 +6,7 @@ from allauth.mfa.models import Authenticator from allauth.mfa.totp.internal.auth import TOTP from allauth.socialaccount.models import SocialAccount from allauth.socialaccount.models import SocialApp +from django.conf import settings from django.contrib.auth.models import Group from django.contrib.auth.models import Permission from django.contrib.auth.models import User @@ -15,6 +16,7 @@ from rest_framework import serializers from rest_framework.authtoken.serializers import AuthTokenSerializer from paperless.models import ApplicationConfiguration +from paperless.network import validate_outbound_http_url from paperless.validators import reject_dangerous_svg from paperless_mail.serialisers import ObfuscatedPasswordField @@ -236,6 +238,20 @@ class ApplicationConfigurationSerializer(serializers.ModelSerializer): reject_dangerous_svg(file) return file + def validate_llm_endpoint(self, value: str | None) -> str | None: + if not value: + return value + + try: + validate_outbound_http_url( + value, + allow_internal=settings.LLM_ALLOW_INTERNAL_ENDPOINTS, + ) + except ValueError as e: + raise serializers.ValidationError(str(e)) from e + + return value + class Meta: model = ApplicationConfiguration fields = "__all__" diff --git a/src/paperless/settings/__init__.py b/src/paperless/settings/__init__.py index 34d163dd7..011f776b5 100644 --- a/src/paperless/settings/__init__.py +++ b/src/paperless/settings/__init__.py @@ -1112,3 +1112,7 @@ LLM_BACKEND = os.getenv("PAPERLESS_AI_LLM_BACKEND") # "ollama" or "openai" LLM_MODEL = os.getenv("PAPERLESS_AI_LLM_MODEL") LLM_API_KEY = os.getenv("PAPERLESS_AI_LLM_API_KEY") LLM_ENDPOINT = os.getenv("PAPERLESS_AI_LLM_ENDPOINT") +LLM_ALLOW_INTERNAL_ENDPOINTS = get_bool_from_env( + "PAPERLESS_AI_LLM_ALLOW_INTERNAL_ENDPOINTS", + "true", +) diff --git a/src/paperless_ai/client.py b/src/paperless_ai/client.py index d70fe57b5..95b61b508 100644 --- a/src/paperless_ai/client.py +++ b/src/paperless_ai/client.py @@ -7,6 +7,7 @@ if TYPE_CHECKING: from llama_index.llms.openai import OpenAI from paperless.config import AIConfig +from paperless.network import validate_outbound_http_url from paperless_ai.base_model import DocumentClassifierSchema logger = logging.getLogger("paperless_ai.client") @@ -25,17 +26,28 @@ class AIClient: if self.settings.llm_backend == "ollama": from llama_index.llms.ollama import Ollama + endpoint = self.settings.llm_endpoint or "http://localhost:11434" + validate_outbound_http_url( + endpoint, + allow_internal=self.settings.llm_allow_internal_endpoints, + ) return Ollama( model=self.settings.llm_model or "llama3.1", - base_url=self.settings.llm_endpoint or "http://localhost:11434", + base_url=endpoint, request_timeout=120, ) elif self.settings.llm_backend == "openai": from llama_index.llms.openai import OpenAI + endpoint = self.settings.llm_endpoint or None + if endpoint: + validate_outbound_http_url( + endpoint, + allow_internal=self.settings.llm_allow_internal_endpoints, + ) return OpenAI( model=self.settings.llm_model or "gpt-3.5-turbo", - api_base=self.settings.llm_endpoint or None, + api_base=endpoint, api_key=self.settings.llm_api_key, ) else: diff --git a/src/paperless_ai/embedding.py b/src/paperless_ai/embedding.py index f7193a8db..72ad8e14a 100644 --- a/src/paperless_ai/embedding.py +++ b/src/paperless_ai/embedding.py @@ -12,6 +12,7 @@ from documents.models import Document from documents.models import Note from paperless.config import AIConfig from paperless.models import LLMEmbeddingBackend +from paperless.network import validate_outbound_http_url def get_embedding_model() -> "BaseEmbedding": @@ -21,10 +22,16 @@ def get_embedding_model() -> "BaseEmbedding": case LLMEmbeddingBackend.OPENAI: from llama_index.embeddings.openai import OpenAIEmbedding + endpoint = config.llm_endpoint or None + if endpoint: + validate_outbound_http_url( + endpoint, + allow_internal=config.llm_allow_internal_endpoints, + ) return OpenAIEmbedding( model=config.llm_embedding_model or "text-embedding-3-small", api_key=config.llm_api_key, - api_base=config.llm_endpoint or None, + api_base=endpoint, ) case LLMEmbeddingBackend.HUGGINGFACE: from llama_index.embeddings.huggingface import HuggingFaceEmbedding diff --git a/src/paperless_ai/tests/test_client.py b/src/paperless_ai/tests/test_client.py index 08582cc71..e1325b107 100644 --- a/src/paperless_ai/tests/test_client.py +++ b/src/paperless_ai/tests/test_client.py @@ -12,6 +12,7 @@ from paperless_ai.client import AIClient def mock_ai_config(): with patch("paperless_ai.client.AIConfig") as MockAIConfig: mock_config = MagicMock() + mock_config.llm_allow_internal_endpoints = True MockAIConfig.return_value = mock_config yield mock_config @@ -59,6 +60,17 @@ def test_get_llm_openai(mock_ai_config, mock_openai_llm): assert client.llm == mock_openai_llm.return_value +def test_get_llm_openai_blocks_internal_endpoint_when_disallowed(mock_ai_config): + mock_ai_config.llm_backend = "openai" + mock_ai_config.llm_model = "test_model" + mock_ai_config.llm_api_key = "test_api_key" + mock_ai_config.llm_endpoint = "http://127.0.0.1:1234" + mock_ai_config.llm_allow_internal_endpoints = False + + with pytest.raises(ValueError, match="non-public address"): + AIClient() + + def test_get_llm_unsupported_backend(mock_ai_config): mock_ai_config.llm_backend = "unsupported" diff --git a/src/paperless_ai/tests/test_embedding.py b/src/paperless_ai/tests/test_embedding.py index 98da6e410..f9dbfd85d 100644 --- a/src/paperless_ai/tests/test_embedding.py +++ b/src/paperless_ai/tests/test_embedding.py @@ -15,6 +15,7 @@ from paperless_ai.embedding import get_embedding_model @pytest.fixture def mock_ai_config(): with patch("paperless_ai.embedding.AIConfig") as MockAIConfig: + MockAIConfig.return_value.llm_allow_internal_endpoints = True yield MockAIConfig @@ -77,6 +78,19 @@ def test_get_embedding_model_openai(mock_ai_config): assert model == MockOpenAIEmbedding.return_value +def test_get_embedding_model_openai_blocks_internal_endpoint_when_disallowed( + mock_ai_config, +): + mock_ai_config.return_value.llm_embedding_backend = LLMEmbeddingBackend.OPENAI + mock_ai_config.return_value.llm_embedding_model = "text-embedding-3-small" + mock_ai_config.return_value.llm_api_key = "test_api_key" + mock_ai_config.return_value.llm_endpoint = "http://127.0.0.1:11434" + mock_ai_config.return_value.llm_allow_internal_endpoints = False + + with pytest.raises(ValueError, match="non-public address"): + get_embedding_model() + + def test_get_embedding_model_huggingface(mock_ai_config): mock_ai_config.return_value.llm_embedding_backend = LLMEmbeddingBackend.HUGGINGFACE mock_ai_config.return_value.llm_embedding_model = ( From ca5879a54ed7d7357b9bab202166f007e6d99977 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 16 Mar 2026 23:03:31 -0700 Subject: [PATCH 21/38] Fix one test with explicit override --- src/documents/tests/test_consumer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/documents/tests/test_consumer.py b/src/documents/tests/test_consumer.py index 2acfb3c7d..a59c7d676 100644 --- a/src/documents/tests/test_consumer.py +++ b/src/documents/tests/test_consumer.py @@ -642,6 +642,7 @@ class TestConsumer( self._assert_first_last_send_progress() @mock.patch("documents.consumer.generate_unique_filename") + @override_settings(FILENAME_FORMAT="{pk}") def testFilenameHandlingFallsBackWhenGeneratedPathExceedsDbLimit(self, m): m.side_effect = lambda doc, archive_filename=False: Path( ("a" * 1100 + ".pdf") if not archive_filename else ("b" * 1100 + ".pdf"), From 736b08ad09d4f7205cdcb9b4e973e242e91020dc Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 17 Mar 2026 08:23:08 -0700 Subject: [PATCH 22/38] Tweak: use cancel instead of discard for app config button --- src-ui/src/app/components/admin/config/config.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/admin/config/config.component.html b/src-ui/src/app/components/admin/config/config.component.html index fcb50183a..38ddbf3a7 100644 --- a/src-ui/src/app/components/admin/config/config.component.html +++ b/src-ui/src/app/components/admin/config/config.component.html @@ -59,7 +59,7 @@