diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-llmindex-migrate b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-complete/dependencies.d/init-llmindex-migrate
new file mode 100644
index 000000000..e69de29bb
diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-llmindex-migrate/dependencies.d/init-migrations b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-llmindex-migrate/dependencies.d/init-migrations
new file mode 100644
index 000000000..e69de29bb
diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-llmindex-migrate/run b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-llmindex-migrate/run
new file mode 100755
index 000000000..50fa691c2
--- /dev/null
+++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-llmindex-migrate/run
@@ -0,0 +1,12 @@
+#!/command/with-contenv /usr/bin/bash
+# shellcheck shell=bash
+
+declare -r log_prefix="[init-llmindex-migrate]"
+
+echo "${log_prefix} Checking for pending LLM index migrations..."
+cd "${PAPERLESS_SRC_DIR}"
+if [[ -n "${USER_IS_NON_ROOT}" ]]; then
+ python3 manage.py document_llmindex migrate
+else
+ s6-setuidgid paperless python3 manage.py document_llmindex migrate
+fi
diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-llmindex-migrate/type b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-llmindex-migrate/type
new file mode 100644
index 000000000..bdd22a185
--- /dev/null
+++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-llmindex-migrate/type
@@ -0,0 +1 @@
+oneshot
diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/init-llmindex-migrate/up b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-llmindex-migrate/up
new file mode 100644
index 000000000..c2016d47a
--- /dev/null
+++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/init-llmindex-migrate/up
@@ -0,0 +1 @@
+/etc/s6-overlay/s6-rc.d/init-llmindex-migrate/run
diff --git a/docs/administration.md b/docs/administration.md
index 8e27df982..fbc636935 100644
--- a/docs/administration.md
+++ b/docs/administration.md
@@ -212,6 +212,16 @@ following:
This is a no-op if the index is already up to date, so it is safe to
run on every upgrade.
+5. Migrate the LLM index if needed.
+
+ ```shell-session
+ cd src
+ python3 manage.py document_llmindex migrate
+ ```
+
+ This is a no-op if the index schema is already current, so it is safe
+ to run on every upgrade.
+
### Database Upgrades
Paperless-ngx is compatible with Django-supported versions of PostgreSQL and MariaDB and it is generally
@@ -532,7 +542,7 @@ index is updated automatically on the schedule set by
can manage it manually:
```
-document_llmindex {rebuild,update,compact}
+document_llmindex {rebuild,update,compact,migrate}
```
Specify `rebuild` to build the index from scratch from all documents in the database. Use
diff --git a/docs/usage.md b/docs/usage.md
index e4009cdff..bd03c9a51 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -620,6 +620,34 @@ no other workflow will be executed on the document.
If a "Move to Trash" action is executed in a consume pipeline, the consumption
will be aborted and the file will be deleted.
+##### Password Removal {#workflow-action-password-removal}
+
+"Password Removal" actions attempt to remove password protection from encrypted PDF documents. You can specify:
+
+- One or more passwords to try, separated by commas or new lines
+- Each password is tried in order until one successfully unlocks the document
+
+Password removal never modifies a file in place. Instead, once a working password is found, the
+decrypted content is consumed as a new [document version](#document-file-versions), leaving the
+original (still encrypted) version in the document's version history.
+
+**Consumption Started**: because this trigger fires before the document exists yet, the password
+removal itself is deferred until after the initial consumption of the encrypted file has completed.
+OCR engines cannot process an encrypted PDF, so this first version is typically stored with no
+extracted text (unless the file already contained extractable text outside of OCR). Immediately
+afterwards, the password is removed and the decrypted file is automatically re-consumed as a second,
+new version of the same document, this time with normal OCR/text extraction applied. In other words,
+a password-protected file added with this trigger will briefly exist as an un-OCR'd version before
+the properly processed version is created.
+
+**Document Added**, **Document Updated**, **Scheduled**: these triggers run against a document that
+already exists, so password removal happens immediately: the decrypted content is queued for
+consumption as a new version right away. Note that if the document's initial consumption also
+happened while it was still encrypted, that original version will likewise be missing OCR text.
+
+**Current limitation**: Passwords are stored as a simple list without descriptions. To handle
+multiple PDF types with different passwords, create separate workflows for each use case.
+
#### Workflow placeholders
Titles and webhook payloads can be generated by workflows using [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/).
diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf
index 9e98563f0..4a0607abb 100644
--- a/src-ui/messages.xlf
+++ b/src-ui/messages.xlf
@@ -659,7 +659,7 @@
src/app/components/document-detail/document-detail.component.html
- 445
+ 449src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.html
@@ -831,7 +831,7 @@
src/app/components/document-detail/document-detail.component.html
- 464
+ 468src/app/components/document-list/document-list.component.html
@@ -1355,7 +1355,7 @@
src/app/components/document-detail/document-detail.component.html
- 390
+ 394src/app/components/document-list/bulk-editor/bulk-editor.component.html
@@ -1596,7 +1596,7 @@
src/app/components/document-detail/document-detail.component.html
- 165
+ 169
@@ -1607,7 +1607,7 @@
src/app/components/document-detail/document-detail.component.html
- 170
+ 174src/app/components/document-list/bulk-editor/bulk-editor.component.html
@@ -1638,7 +1638,7 @@
src/app/components/document-detail/document-detail.component.html
- 174
+ 178src/app/components/document-list/bulk-editor/bulk-editor.component.html
@@ -1669,7 +1669,7 @@
src/app/components/document-detail/document-detail.component.html
- 178
+ 182src/app/components/document-list/bulk-editor/bulk-editor.component.html
@@ -1849,7 +1849,7 @@
src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html
- 154
+ 159src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
@@ -3972,11 +3972,11 @@
src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html
- 96
+ 101src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html
- 102
+ 107
@@ -3987,11 +3987,11 @@
src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html
- 97
+ 102src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html
- 103
+ 108
@@ -4002,14 +4002,14 @@
src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html
- 119
+ 124Anysrc/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html
- 152
+ 157src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
@@ -4020,21 +4020,21 @@
Notsrc/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html
- 157
+ 162Add querysrc/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html
- 176
+ 181Add expressionsrc/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html
- 179
+ 184
@@ -4330,14 +4330,14 @@
Create new itemsrc/app/components/common/edit-dialog/edit-dialog.component.ts
- 119
+ 123Edit itemsrc/app/components/common/edit-dialog/edit-dialog.component.ts
- 123
+ 127
@@ -4914,7 +4914,7 @@
src/app/components/document-detail/document-detail.component.html
- 356
+ 360
@@ -7760,14 +7760,14 @@
Detailssrc/app/components/document-detail/document-detail.component.html
- 160
+ 164Titlesrc/app/components/document-detail/document-detail.component.html
- 163
+ 167src/app/components/document-list/document-list.component.html
@@ -7790,14 +7790,14 @@
Date createdsrc/app/components/document-detail/document-detail.component.html
- 167
+ 171Defaultsrc/app/components/document-detail/document-detail.component.html
- 179
+ 183src/app/components/manage/saved-views/saved-views.component.html
@@ -7808,14 +7808,14 @@
Contentsrc/app/components/document-detail/document-detail.component.html
- 286
+ 290Metadatasrc/app/components/document-detail/document-detail.component.html
- 295
+ 299src/app/components/document-detail/metadata-collapse/metadata-collapse.component.ts
@@ -7826,147 +7826,147 @@
Date modifiedsrc/app/components/document-detail/document-detail.component.html
- 302
+ 306Date addedsrc/app/components/document-detail/document-detail.component.html
- 306
+ 310Media filenamesrc/app/components/document-detail/document-detail.component.html
- 310
+ 314Original filenamesrc/app/components/document-detail/document-detail.component.html
- 314
+ 318Original SHA256 checksumsrc/app/components/document-detail/document-detail.component.html
- 318
+ 322Original file sizesrc/app/components/document-detail/document-detail.component.html
- 322
+ 326Original mime typesrc/app/components/document-detail/document-detail.component.html
- 326
+ 330Archive SHA256 checksumsrc/app/components/document-detail/document-detail.component.html
- 331
+ 335Archive file sizesrc/app/components/document-detail/document-detail.component.html
- 337
+ 341Original document metadatasrc/app/components/document-detail/document-detail.component.html
- 346
+ 350Archived document metadatasrc/app/components/document-detail/document-detail.component.html
- 349
+ 353Notes src/app/components/document-detail/document-detail.component.html
- 368,371
+ 372,375Historysrc/app/components/document-detail/document-detail.component.html
- 379
+ 383 Duplicates src/app/components/document-detail/document-detail.component.html
- 401,405
+ 405,409Duplicate documents detected:src/app/components/document-detail/document-detail.component.html
- 407
+ 411In trashsrc/app/components/document-detail/document-detail.component.html
- 418
+ 422Save & nextsrc/app/components/document-detail/document-detail.component.html
- 447
+ 451Save & closesrc/app/components/document-detail/document-detail.component.html
- 449
+ 453Discardsrc/app/components/document-detail/document-detail.component.html
- 451
+ 455Document loading...src/app/components/document-detail/document-detail.component.html
- 459
+ 463Enter Passwordsrc/app/components/document-detail/document-detail.component.html
- 513
+ 517
diff --git a/src-ui/src/app/components/admin/tasks/tasks.component.html b/src-ui/src/app/components/admin/tasks/tasks.component.html
index 7fae03a7f..f1220e4b5 100644
--- a/src-ui/src/app/components/admin/tasks/tasks.component.html
+++ b/src-ui/src/app/components/admin/tasks/tasks.component.html
@@ -21,7 +21,7 @@
-@if (!tasksService.completedFileTasks && tasksService.loading) {
+@if (loading() && pagedTasks().length === 0) {