Compare commits

...
90 Commits
Author SHA1 Message Date
shamoon c4d1b0c732 Bump version to 3.3.1 2026-10-12 13:01:03 -07:00
shamoon 333cbf0ae6 Merge branch 'dev' 2026-10-12 13:00:15 -07:00
github-actions[bot]andCrowdin Bot 49a298894e New Crowdin translations by GitHub Action (#14368)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-10-12 12:59:46 -07:00
GitHub Actions 551ebad6f1 Auto translate strings 2026-10-12 19:13:03 +00:00
Trenton H 4171ecf166 Chore: share the authorizing owner rule between the id sets and the trash filter (#14425)
The trash list needs to show a version to whoever owns its root document, but
it deliberately shows only owned and unowned documents and ignores explicit
shares, so it cannot use permitted_object_ids. #14384 gave it its own
filter_queryset with a hand-built condition through the root, which describes
the same rule as the annotations in permitted_object_ids a second time.

Move those annotations into annotate_authorizing_fields. PermittedObjectsFilter
gains a parent_field attribute that its owner-only and granted paths both use,
and the trash filter shrinks to setting include_granted and parent_field.
2026-10-12 12:11:42 -07:00
GitHub Actions 8289dd0bae Auto translate strings 2026-10-12 18:12:14 +00:00
Trenton H b195423f1d Fix: use a root document's effective content for the LLM index and pr… (#14423)
* Fix: use a root document's effective content for the LLM index and prompts

The search index and the document views use the newest version's content as a
root document's content, but everything the LLM side reads used the root's own
content: the text embedded in the LLM index, the content in the classification
prompt, the context blocks from similar documents, and the text that is
searched for similar documents. After a document was replaced by a new
version the LLM kept answering from the old text.

Read get_effective_content() in those four places. The index update annotates
the content in the query like the search index does, and the similar-document
lookup annotates it for the documents it fetches, so no query is made per
document. Entries already in the LLM index keep the old text until the
document next changes or the index is rebuilt.

* Chore: test the LLM query text and bulk annotation use effective content

* Removes the query count testing.  It's flakey from local to CI and workers, doesn't really add much imo
2026-10-12 11:10:50 -07:00
Trenton H 9f2f809131 Fix: index document versions as their root document (#14422)
* Fix: index document versions as their root document

The search index and the LLM index hold root documents only, a root being
indexed with its newest version's content. Every write path therefore had to
remember to hand them the root. Several did not.

WriteBatch.add_or_update, WriteBatch.add_or_update_ids and
llm_index_add_or_update_document now resolve a version to its root themselves.
The reindex command and update_llm_index only walk root documents, and an
incremental LLM update is scoped to the roots of the given ids through
versioning.root_document_ids, which add_or_update_ids shares. more_like_id
returns the root's id for a version. The reprocess task no longer picks the root
for the indexes and only still clears the caches of both documents.

* Fix: rebuild old search indexes and look versions up as their root for similar documents

An index built before versions were indexed as their root can still hold
version entries under their own id and metadata, and only a rebuild clears
them. Bump the schema version so the index is rebuilt on upgrade.

more_like_this_ids looked the given id up in the index as is, so asking for
documents like a version, as the AI suggestions endpoint can, silently found
nothing. It now looks the root up instead.
2026-10-12 11:10:50 -07:00
GitHub Actions 96535991a3 Auto translate strings 2026-10-12 17:18:18 +00:00
Trenton H c2deb69ac5 Fix: authorize document versions by their root in the single-object permission check (#14421)
* Fix: authorize document versions by their root in the single-object permission check

has_perms_owner_aware judged a document by its own owner and grants, so each
endpoint that fetches a document itself had to remember to map a version to
its root document first, and one that forgot, like the more-like-this search
filter, authorized by a stale version owner.

The check now maps a Document to its root before looking at the owner and the
guardian grants, matching what permitted_document_ids does for id sets. The
eight call sites that mapped the document themselves pass it straight through.
The DRF object permission class needs no change because the document viewset
only ever serves root documents.
2026-10-12 17:17:00 +00:00
GitHub Actions 070f614929 Auto translate strings 2026-10-12 17:06:18 +00:00
Trenton H 6f496f741b Chore: New helper for JSON parsing during settings, to provide a better error message to users (#14419) 2026-10-12 17:04:56 +00:00
Sven Lankes 1fb463b55c Chore: silence gotenberg startup warning (#14405) 2026-10-12 16:54:47 +00:00
shamoon 9a136ecb62 Chore: drop PR size labeling 2026-10-12 09:41:48 -07:00
shamoon 192ddbe45e Documentation: tweak demo hosting attribution 2026-10-12 09:18:19 -07:00
shamoon aac5e49034 Merge branch 'main' into dev 2026-10-12 09:13:04 -07:00
shamoon 716d310eb3 Documentation: credit Fyra Stack for hosting the demo (#14399) 2026-10-12 09:10:40 -07:00
dependabot[bot] 71d2edb055 Chore(deps): Bump the npm_and_yarn group across 1 directory with 2 updates (#14417)
Bumps the npm_and_yarn group with 2 updates in the /src/paperless_mail/templates directory: [source-map-js](https://github.com/7rulnik/source-map-js) and [postcss-selector-parser](https://github.com/postcss/postcss-selector-parser).


Updates `source-map-js` from 1.2.1 to 1.2.2
- [Release notes](https://github.com/7rulnik/source-map-js/releases)
- [Changelog](https://github.com/7rulnik/source-map-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/7rulnik/source-map-js/compare/v1.2.1...v1.2.2)

Removes `postcss-selector-parser`

---
updated-dependencies:
- dependency-name: source-map-js
  dependency-version: 1.2.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: postcss-selector-parser
  dependency-version:
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-12 06:52:11 -07:00
shamoon 973af89f61 Chore: update frontend TS libs to ES2022 2026-10-11 15:39:56 -07:00
GitHub Actions 937bf2fa7a Auto translate strings 2026-10-10 22:05:15 +00:00
Trenton HandClaude Sonnet 5.5 2f2381578c Fix: authorize document versions by their root and speed up permission id sets (#14396)
* Fix: authorize document versions by their root and speed up permission id sets

permitted_document_ids judged a version by its own owner and grants, so a
version whose owner had drifted from its root's was visible to the wrong
people and hidden from the right ones. Callers patched this individually by
mapping each document to its root first. The query itself was also slow on
MariaDB: the guardian grants were a UNION cast to integers and tested with
IN inside an OR with the owner checks, which MariaDB cannot materialize, so it
re-scans the user's grants for every document. At 20k documents that took
seconds for a user with a couple of hundred grants.

permitted_object_ids now casts the row key to a string, as guardian stores
object_pk, and tests it against a single uncorrelated UNION ALL of the user's
and groups' grants. No integer cast is needed and the user's groups are
matched with an IN subquery rather than a join through the membership table.
Postgres and SQLite build the grant set once, and MariaDB probes guardian's
unique indexes per row, which is cheap. A correlated EXISTS per grant also
fixed MariaDB but was up to 3x slower than before on Postgres and SQLite.

permitted_object_ids also takes an optional parent_field naming a
self-referencing foreign key whose target authorizes the row, and
permitted_document_ids passes root_document, so a version is visible exactly
when its root is. The helper that mapped documents to their roots at the call
sites is no longer needed, so the email, selection data, share link bundle,
trash, bulk download and bulk edit checks use the id set directly.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-10 15:03:52 -07:00
shamoon e606dbffcd Chore: update the db options warning (#14398) 2026-10-10 07:17:54 -07:00
shamoon 138160cbda Fix: bulk reprocess latest version for root documents (#14385) 2026-10-08 18:50:44 +00:00
GitHub Actions 6d960c11d5 Auto translate strings 2026-10-08 18:40:22 +00:00
shamoon a35bd6e238 Fix: consistently use root doc for version action permissions (#14384) 2026-10-08 18:39:05 +00:00
Trenton H 474c630aa4 Fix: Don't attempt to index created dates which are not representable inside the FTS index (#14390) 2026-10-08 08:36:35 -07:00
Trenton H d7a9894400 Fix: retry a search index rebuild that was interrupted (#14379)
An interrupted rebuild left an empty index stamped as current, so the next
start reported it as up to date. Mark the rebuild as in progress and only
clear the marker once it completes.
2026-10-07 08:54:36 -07:00
shamoon f8876c55e3 Documentation: remove MARIADB_HOST from MariaDB compose files (#14378) 2026-10-07 07:32:37 -07:00
Trenton H e5b215f9d3 Chore: Updates the troubleshooting with 2 common database collation things (#14370) 2026-10-06 11:02:42 -07:00
GitHub Actions ee34a6598e Auto translate strings 2026-10-06 15:13:08 +00:00
Trenton H 3a3b3ef66a Chore: Upgrade runners to Ubuntu 26.04 (#14347)
* Moves runners to 26.04 and a few jobs to -slim variant

* Probably fixing the imagemagik 7 problems and maybe the frontend playwright thing?

* Compare thumbnails, but allow a little difference in the perceptual hash
2026-10-06 08:11:44 -07:00
github-actions[bot]andshamoon 94f10b8622 Documentation: Add v3.3.0 changelog (#14362)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-10-05 21:14:19 -07:00
shamoon 6f7ed1d4a7 Bump version to 3.3.0 2026-10-05 20:34:24 -07:00
shamoon 0428bf6955 Merge branch 'dev' 2026-10-05 20:33:36 -07:00
github-actions[bot]andCrowdin Bot 662a5237a3 New Crowdin translations by GitHub Action (#14361)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-10-06 03:29:47 +00:00
GitHub Actions 562c2d0985 Auto translate strings 2026-10-06 03:19:38 +00:00
shamoon 6cdb93cd59 Fix: dont overcount same title or date suggestions 2026-10-05 20:18:09 -07:00
GitHub Actions 85fcf66691 Auto translate strings 2026-10-06 03:05:53 +00:00
shamoon 73b2321e87 Fix create after ML-only 2026-10-05 20:04:35 -07:00
GitHub Actions 5eecd6971e Auto translate strings 2026-10-06 02:34:22 +00:00
shamoon bc92e4ff98 Fix suggestions dropdown counts and empty state 2026-10-05 19:32:56 -07:00
github-actions[bot]andCrowdin Bot dacc213917 New Crowdin translations by GitHub Action (#14214)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-10-06 02:09:40 +00:00
GitHub Actions c780586c72 Auto translate strings 2026-10-05 16:26:46 +00:00
jurassicparkicecreamandshamoon 62cc31fbdb Feature: store barcode contents, list and search them (#14276)
* Feature: store barcode contents, list and search them

New setting PAPERLESS_CONSUMER_STORE_BARCODE_VALUES (off by default)
stores all barcodes found during consumption with the document: page,
type and content. They are listed on the metadata tab with a copy
button, returned by the documents API and searchable with barcodes:
in the advanced search. Versions keep their own barcodes, reprocessing
reads them again. Refs #9898

* Tests: cover the remaining barcode branches

Covers unchanged and failing barcode reads on reprocessing, unsupported files and DocumentBarcode.__str__, and uses toHaveLength in the barcode list spec as suggested by SonarCloud.

* Address review: keep barcodes when they can't be read, format choices

- Reprocessing and new versions keep the stored barcodes when the file
  can't be scanned or the scan fails, and replace them atomically.
- Format is a TextChoices of the zxing-cpp formats, with a test.
- Shared scan code, latest_version helper, TypedDict, serializer reuse.
- Barcodes in the split manifest, export/import tests, pytest-style tests.

* Barcode tests: TIFF reprocess, format check both ways, fixtures

- Reprocessing a TIFF with TIFF support off keeps the stored barcodes.
- The format test also fails when zxing-cpp drops a format.
- Fixtures in place of the sample dir mixin, plugin-level disabled test.
- Format labels aren't translated, OpenAPI enum named BarcodeFormatEnum.

* Review: module-level zxing reader, shorter barcode docs

- read_barcodes_zxing is a module-level function used by scan_pdf.
- Drop the trivial __str__ test, mark it no cover.
- Shorten the barcode docs and remove the duplicate in configuration.md.

* Fix header

* Use utility class

* Return barcodes from the metadata endpoint only

Drop the barcodes field and its prefetches from the document serializer,
as agreed in the review. Also remove the now empty component stylesheet.

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-10-05 16:25:19 +00:00
Trenton H 312684aef3 Fix: Set the ProcessedMail owner based on the rule owner in all cases (#14356) 2026-10-05 05:57:41 -07:00
dependabot[bot]andTrenton H 1d42d372a2 Chore(deps): Bump django-filter from 25.2 to 26.1 (#14337)
* Chore(deps): Bump django-filter from 25.2 to 26.1

Bumps [django-filter](https://github.com/carltongibson/django-filter) from 25.2 to 26.1.
- [Release notes](https://github.com/carltongibson/django-filter/releases)
- [Changelog](https://github.com/carltongibson/django-filter/blob/main/CHANGES.rst)
- [Commits](https://github.com/carltongibson/django-filter/compare/25.2...26.1)

---
updated-dependencies:
- dependency-name: django-filter
  dependency-version: '26.1'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Linting

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Trenton H <797416+stumpylog@users.noreply.github.com>
2026-10-04 22:24:13 +00:00
dependabot[bot] 08c4f3d3aa Chore(deps): Bump the utilities-patch group across 1 directory with 2 updates (#14338)
Bumps the utilities-patch group with 2 updates in the / directory: [psycopg-pool](https://github.com/psycopg/psycopg) and [ruff](https://github.com/astral-sh/ruff).


Updates `psycopg-pool` from 3.3.2 to 3.3.3
- [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst)
- [Commits](https://github.com/psycopg/psycopg/compare/3.3.2...3.3.3)

Updates `ruff` from 0.16.8 to 0.16.9
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.16.8...0.16.9)

---
updated-dependencies:
- dependency-name: psycopg-pool
  dependency-version: 3.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: ruff
  dependency-version: 0.16.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-04 13:54:08 -07:00
dependabot[bot] 8adbff1423 Chore(deps): Bump the pre-commit-dependencies group across 1 directory with 3 updates (#14351)
Bumps the pre-commit-dependencies group with 3 updates in the / directory: [https://github.com/rbubley/mirrors-prettier](https://github.com/rbubley/mirrors-prettier), prettier and [https://github.com/astral-sh/ruff-pre-commit](https://github.com/astral-sh/ruff-pre-commit).


Updates `https://github.com/rbubley/mirrors-prettier` from v3.9.8 to 3.9.9
- [Commits](https://github.com/rbubley/mirrors-prettier/compare/v3.9.8...v3.9.9)

Updates `prettier` from 3.9.8 to 3.9.9

Updates `https://github.com/astral-sh/ruff-pre-commit` from v0.16.8 to 0.16.9
- [Release notes](https://github.com/astral-sh/ruff-pre-commit/releases)
- [Commits](https://github.com/astral-sh/ruff-pre-commit/compare/v0.16.8...v0.16.9)

---
updated-dependencies:
- dependency-name: https://github.com/rbubley/mirrors-prettier
  dependency-version: 3.9.9
  dependency-type: direct:production
  dependency-group: pre-commit-dependencies
- dependency-name: prettier
  dependency-version: 3.9.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: pre-commit-dependencies
- dependency-name: https://github.com/astral-sh/ruff-pre-commit
  dependency-version: 0.16.9
  dependency-type: direct:production
  dependency-group: pre-commit-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-03 13:58:23 -07:00
dependabot[bot] 7c11be81ca Chore(deps-dev): Bump types-channels (#14335)
Bumps [types-channels](https://github.com/python/typeshed) from 4.3.0.20260408 to 4.3.0.20260518.
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-channels
  dependency-version: 4.3.0.20260518
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-03 20:04:48 +00:00
dependabot[bot] 3e46186440 docker(deps): Bump astral-sh/uv (#14327)
Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.12.20-python3.14-trixie-slim to 0.12.23-python3.14-trixie-slim.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.12.20...0.12.23)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-version: 0.12.21-python3.14-trixie-slim
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-03 12:52:54 -07:00
dependabot[bot] 6ce43be400 Chore(deps): Bump the actions group across 1 directory with 4 updates (#14332)
Bumps the actions group with 4 updates in the / directory: [github/codeql-action/upload-sarif](https://github.com/github/codeql-action), [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [crowdin/github-action](https://github.com/crowdin/github-action).


Updates `github/codeql-action/upload-sarif` from 4.38.1 to 4.38.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/1c5b675653bb5c22dbe9b12b556ec555138e09fd...2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2)

Updates `github/codeql-action/init` from 4.38.1 to 4.38.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/1c5b675653bb5c22dbe9b12b556ec555138e09fd...2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2)

Updates `github/codeql-action/analyze` from 4.38.1 to 4.38.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/1c5b675653bb5c22dbe9b12b556ec555138e09fd...2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2)

Updates `crowdin/github-action` from 3.2.0 to 3.3.0
- [Release notes](https://github.com/crowdin/github-action/releases)
- [Commits](https://github.com/crowdin/github-action/compare/df474cdfb9f41d6ae777118749477c2cdf7cacc8...9c23991700c0ec5256fd41089b9d9d7d540e424e)

---
updated-dependencies:
- dependency-name: crowdin/github-action
  dependency-version: 3.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.38.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 20:42:36 -07:00
dependabot[bot] 0204009b66 Chore(deps): Bump the uv group across 1 directory with 2 updates (#14325)
Bumps the uv group with 2 updates in the / directory: [django](https://github.com/django/django) and [tornado](https://github.com/tornadoweb/tornado).


Updates `django` from 5.2.16 to 5.2.17
- [Commits](https://github.com/django/django/compare/5.2.16...5.2.17)

Updates `tornado` from 6.5.8 to 6.5.9
- [Changelog](https://github.com/tornadoweb/tornado/blob/master/docs/releases.rst)
- [Commits](https://github.com/tornadoweb/tornado/compare/v6.5.8...v6.5.9)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 5.2.17
  dependency-type: direct:production
- dependency-name: tornado
  dependency-version: 6.5.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-03 01:07:57 +00:00
GitHub Actions cec0b99d2e Auto translate strings 2026-10-02 23:10:15 +00:00
Trenton H 3615a5a2a5 Fix: Ensure log rotation settings are converted to integers (#14343)
* Fix: Ensure the log rotatation options are integers

* This might have the same problem

* Just in case, transition these to the helper too
2026-10-02 23:08:48 +00:00
Trenton H 56687614df Fix: Wrap apt calls into a retry so we can ideally jump a slow mirror (#14344)
* Fix: Wrap apt calls into a retry so we can ideally jump from a slow mirror to a faster one

* Use the different synxtax for an in-repo usage
2026-10-02 15:55:54 -07:00
GitHub Actions f1ae9c00ac Auto translate strings 2026-10-02 05:59:48 +00:00
dependabot[bot] bc2ff953ed Chore(deps): Bump the frontend-angular-dependencies group (#14329)
Bumps the frontend-angular-dependencies group in /src-ui with 13 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/cdk](https://github.com/angular/components) | `22.1.7` | `22.2.0` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `22.1.7` | `22.2.0` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `22.1.7` | `22.2.0` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `22.1.7` | `22.2.0` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `22.1.7` | `22.2.0` |
| [@angular/localize](https://github.com/angular/angular) | `22.1.7` | `22.2.0` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `22.1.7` | `22.2.0` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `22.1.7` | `22.2.0` |
| [@angular-devkit/core](https://github.com/angular/angular-cli) | `22.1.8` | `22.2.0` |
| [@angular-devkit/schematics](https://github.com/angular/angular-cli) | `22.1.8` | `22.2.0` |
| [@angular/build](https://github.com/angular/angular-cli) | `22.1.8` | `22.2.0` |
| [@angular/cli](https://github.com/angular/angular-cli) | `22.1.8` | `22.2.0` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `22.1.7` | `22.2.0` |


Updates `@angular/cdk` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/components/compare/v22.1.7...v22.2.0)

Updates `@angular/common` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.0/packages/common)

Updates `@angular/compiler` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.0/packages/compiler)

Updates `@angular/core` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.0/packages/core)

Updates `@angular/forms` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.0/packages/forms)

Updates `@angular/localize` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/compare/v22.1.7...v22.2.0)

Updates `@angular/platform-browser` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.0/packages/platform-browser)

Updates `@angular/router` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.0/packages/router)

Updates `@angular-devkit/core` from 22.1.8 to 22.2.0
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v22.1.8...v22.2.0)

Updates `@angular-devkit/schematics` from 22.1.8 to 22.2.0
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v22.1.8...v22.2.0)

Updates `@angular/build` from 22.1.8 to 22.2.0
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v22.1.8...v22.2.0)

Updates `@angular/cli` from 22.1.8 to 22.2.0
- [Release notes](https://github.com/angular/angular-cli/releases)
- [Changelog](https://github.com/angular/angular-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/angular-cli/compare/v22.1.8...v22.2.0)

Updates `@angular/compiler-cli` from 22.1.7 to 22.2.0
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.2.0/packages/compiler-cli)

---
updated-dependencies:
- dependency-name: "@angular/cdk"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/common"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/core"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/forms"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/localize"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/router"
  dependency-version: 22.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-devkit/core"
  dependency-version: 22.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular-devkit/schematics"
  dependency-version: 22.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/build"
  dependency-version: 22.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/cli"
  dependency-version: 22.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler-cli"
  dependency-version: 22.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-angular-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 05:58:14 +00:00
dependabot[bot] e17689a91b Chore(deps-dev): Bump prettier from 3.9.8 to 3.9.9 in /src-ui (#14331)
Bumps [prettier](https://github.com/prettier/prettier) from 3.9.8 to 3.9.9.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.9.8...3.9.9)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.9.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 05:42:48 +00:00
dependabot[bot] 764d1a492d Chore(deps-dev): Bump the frontend-eslint-dependencies group (#14330)
Bumps the frontend-eslint-dependencies group in /src-ui with 3 updates: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin), [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) and [@typescript-eslint/utils](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/utils).


Updates `@typescript-eslint/eslint-plugin` from 8.70.0 to 8.70.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 8.70.0 to 8.70.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.1/packages/parser)

Updates `@typescript-eslint/utils` from 8.70.0 to 8.70.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/utils/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.1/packages/utils)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-version: 8.70.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-eslint-dependencies
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.70.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-eslint-dependencies
- dependency-name: "@typescript-eslint/utils"
  dependency-version: 8.70.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-eslint-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-02 05:31:40 +00:00
9d50a3b261 Fix: ship pdf.js CMaps so CJK documents render in the viewer (#14318)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-10-02 03:27:15 +00:00
dependabot[bot] da3de299fc Chore(deps-dev): Bump zensical (#14326)
Bumps the development group with 1 update: [zensical](https://github.com/zensical/zensical).


Updates `zensical` from 0.0.64 to 0.0.65
- [Release notes](https://github.com/zensical/zensical/releases)
- [Commits](https://github.com/zensical/zensical/compare/v0.0.64...v0.0.65)

---
updated-dependencies:
- dependency-name: zensical
  dependency-version: 0.0.65
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 20:32:40 +00:00
GitHub Actions 3a022e2dc4 Auto translate strings 2026-10-01 20:02:19 +00:00
shamoon 58bda48682 Enhancement: more control over suggestion requests (#14258) 2026-10-01 20:00:52 +00:00
dependabot[bot] b2a2e15ead Chore(deps): Bump the uv group across 1 directory with 2 updates (#14314)
Bumps the uv group with 2 updates in the / directory: [pyjwt](https://github.com/jpadilla/pyjwt) and [urllib3](https://github.com/urllib3/urllib3).


Updates `pyjwt` from 2.13.0 to 2.15.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.13.0...2.15.0)

Updates `urllib3` from 2.7.0 to 2.8.0
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.7.0...2.8.0)

---
updated-dependencies:
- dependency-name: pyjwt
  dependency-version: 2.15.0
  dependency-type: indirect
- dependency-name: urllib3
  dependency-version: 2.8.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-01 17:14:42 +00:00
shamoon e865185425 Chore: anchor admin url pattern (#14316) 2026-10-01 01:22:06 -07:00
shamoon d02c19a09e Chorehancement: set manifest CORS for credentials (#14307) 2026-09-30 08:03:04 -07:00
dependabot[bot]andstumpylog 4a106454ce Chore(deps): Bump the utilities-minor group across 1 directory with 7 updates (#14305)
* Chore(deps): Bump the utilities-minor group across 1 directory with 7 updates

Bumps the utilities-minor group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [django-guardian](https://github.com/django-guardian/django-guardian) | `3.4.1` | `3.5.0` |
| [flower](https://github.com/mher/flower) | `2.1.0` | `2.2.0` |
| [sentence-transformers](https://github.com/huggingface/sentence-transformers) | `6.0.1` | `6.1.0` |
| [watchfiles](https://github.com/samuelcolvin/watchfiles) | `1.2.0` | `1.3.0` |
| [mysqlclient](https://github.com/PyMySQL/mysqlclient) | `2.2.8` | `2.3.0` |
| [faker](https://github.com/joke2k/faker) | `40.38.0` | `40.39.0` |
| [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) | `16.6.1` | `16.7` |



Updates `django-guardian` from 3.4.1 to 3.5.0
- [Release notes](https://github.com/django-guardian/django-guardian/releases)
- [Commits](https://github.com/django-guardian/django-guardian/compare/3.4.1...3.5.0)

Updates `flower` from 2.1.0 to 2.2.0
- [Release notes](https://github.com/mher/flower/releases)
- [Commits](https://github.com/mher/flower/compare/v2.1.0...v2.2.0)

Updates `sentence-transformers` from 6.0.1 to 6.1.0
- [Release notes](https://github.com/huggingface/sentence-transformers/releases)
- [Commits](https://github.com/huggingface/sentence-transformers/compare/v6.0.1...v6.1.0)

Updates `watchfiles` from 1.2.0 to 1.3.0
- [Release notes](https://github.com/samuelcolvin/watchfiles/releases)
- [Commits](https://github.com/samuelcolvin/watchfiles/compare/v1.2.0...v1.3.0)

Updates `mysqlclient` from 2.2.8 to 2.3.0
- [Release notes](https://github.com/PyMySQL/mysqlclient/releases)
- [Changelog](https://github.com/PyMySQL/mysqlclient/blob/main/HISTORY.rst)
- [Commits](https://github.com/PyMySQL/mysqlclient/compare/v2.2.8...v2.3.0)

Updates `faker` from 40.38.0 to 40.39.0
- [Release notes](https://github.com/joke2k/faker/releases)
- [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md)
- [Commits](https://github.com/joke2k/faker/compare/v40.38.0...v40.39.0)

Updates `pytest-rerunfailures` from 16.6.1 to 16.7
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/16.6.1...16.7)

---
updated-dependencies:
- dependency-name: django-guardian
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: faker
  dependency-version: 40.39.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: flower
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: mysqlclient
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: pytest-rerunfailures
  dependency-version: '16.7'
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: sentence-transformers
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
- dependency-name: watchfiles
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: utilities-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Linting

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: stumpylog <797416+stumpylog@users.noreply.github.com>
2026-09-29 13:28:35 -07:00
dependabot[bot] 4254100fae docker-compose(deps): bump greenmail/standalone in /docker/compose (#14281)
Bumps greenmail/standalone from 2.1.13 to 2.1.14.

---
updated-dependencies:
- dependency-name: greenmail/standalone
  dependency-version: 2.1.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-29 11:31:43 -07:00
dependabot[bot] 74f727b1f3 docker(deps): Bump astral-sh/uv (#14282)
Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.12.16-python3.14-trixie-slim to 0.12.20-python3.14-trixie-slim.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.12.16...0.12.20)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-version: 0.12.19-python3.14-trixie-slim
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-29 10:08:39 -07:00
dependabot[bot] fb6fad952e Chore(deps): Bump the pre-commit-dependencies group across 1 directory with 3 updates (#14283)
Bumps the pre-commit-dependencies group with 3 updates in the / directory: [https://github.com/rbubley/mirrors-prettier](https://github.com/rbubley/mirrors-prettier), prettier and [https://github.com/astral-sh/ruff-pre-commit](https://github.com/astral-sh/ruff-pre-commit).


Updates `https://github.com/rbubley/mirrors-prettier` from v3.9.6 to 3.9.8
- [Commits](https://github.com/rbubley/mirrors-prettier/compare/v3.9.6...v3.9.8)

Updates `prettier` from 3.9.6 to 3.9.8

Updates `https://github.com/astral-sh/ruff-pre-commit` from v0.16.7 to 0.16.8
- [Release notes](https://github.com/astral-sh/ruff-pre-commit/releases)
- [Commits](https://github.com/astral-sh/ruff-pre-commit/compare/v0.16.7...v0.16.8)

---
updated-dependencies:
- dependency-name: https://github.com/astral-sh/ruff-pre-commit
  dependency-version: 0.16.8
  dependency-type: direct:production
  dependency-group: pre-commit-dependencies
- dependency-name: https://github.com/rbubley/mirrors-prettier
  dependency-version: 3.9.8
  dependency-type: direct:production
  dependency-group: pre-commit-dependencies
- dependency-name: prettier
  dependency-version: 3.9.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: pre-commit-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-29 09:33:49 -07:00
dependabot[bot] f9b4e0c8e0 Chore(deps): Bump the utilities-patch group across 1 directory with 6 updates (#14297)
Bumps the utilities-patch group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [llama-index-core](https://github.com/run-llama/llama_index) | `0.14.24` | `0.14.25` |
| [tantivy](https://github.com/quickwit-oss/tantivy-py) | `0.26.0` | `0.26.2` |
| [psycopg-pool](https://github.com/psycopg/psycopg) | `3.3.1` | `3.3.2` |
| [zensical](https://github.com/zensical/zensical) | `0.0.60` | `0.0.64` |
| [prek](https://github.com/j178/prek) | `0.5.2` | `0.5.3` |
| [ruff](https://github.com/astral-sh/ruff) | `0.16.7` | `0.16.8` |



Updates `llama-index-core` from 0.14.24 to 0.14.25
- [Release notes](https://github.com/run-llama/llama_index/releases)
- [Changelog](https://github.com/run-llama/llama_index/blob/main/CHANGELOG.md)
- [Commits](https://github.com/run-llama/llama_index/compare/v0.14.24...v0.14.25)

Updates `tantivy` from 0.26.0 to 0.26.2
- [Release notes](https://github.com/quickwit-oss/tantivy-py/releases)
- [Commits](https://github.com/quickwit-oss/tantivy-py/compare/0.26.0...0.26.2)

Updates `psycopg-pool` from 3.3.1 to 3.3.2
- [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst)
- [Commits](https://github.com/psycopg/psycopg/compare/3.3.1...3.3.2)

Updates `zensical` from 0.0.60 to 0.0.64
- [Release notes](https://github.com/zensical/zensical/releases)
- [Commits](https://github.com/zensical/zensical/compare/v0.0.60...v0.0.64)

Updates `prek` from 0.5.2 to 0.5.3
- [Release notes](https://github.com/j178/prek/releases)
- [Changelog](https://github.com/j178/prek/blob/master/CHANGELOG.md)
- [Commits](https://github.com/j178/prek/compare/v0.5.2...v0.5.3)

Updates `ruff` from 0.16.7 to 0.16.8
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.16.7...0.16.8)

---
updated-dependencies:
- dependency-name: llama-index-core
  dependency-version: 0.14.25
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: prek
  dependency-version: 0.5.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: psycopg-pool
  dependency-version: 3.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: ruff
  dependency-version: 0.16.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: tantivy
  dependency-version: 0.26.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
- dependency-name: zensical
  dependency-version: 0.0.63
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: utilities-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-29 08:52:09 -07:00
dependabot[bot] e5041097af Chore(deps): Bump the actions group across 1 directory with 10 updates (#14301)
Bumps the actions group with 10 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) | `10.1.0` | `10.2.0` |
| [codecov/codecov-action](https://github.com/codecov/codecov-action) | `7.0.0` | `7.1.1` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `4.3.0` | `4.4.1` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | `7.3.0` | `7.4.0` |
| [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.38.0` | `4.38.1` |
| [stumpylog/image-cleaner-action/ephemeral](https://github.com/stumpylog/image-cleaner-action) | `0.12.0` | `0.13.0` |
| [stumpylog/image-cleaner-action/untagged](https://github.com/stumpylog/image-cleaner-action) | `0.12.0` | `0.13.0` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.38.0` | `4.38.1` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.38.0` | `4.38.1` |
| [crowdin/github-action](https://github.com/crowdin/github-action) | `3.0.2` | `3.2.0` |



Updates `astral-sh/setup-uv` from 10.1.0 to 10.2.0
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/bec219d24cd3e171d82865faccec33120bb574f4...c18668ad3cf93ea998bef934396af7bb5c839dc7)

Updates `codecov/codecov-action` from 7.0.0 to 7.1.1
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/fb8b3582c8e4def4969c97caa2f19720cb33a72f...303a32d7a59b442fa8d48b6a1cc6825c09c847a5)

Updates `docker/setup-buildx-action` from 4.3.0 to 4.4.1
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/37fe631027851001ddb9b187196cc803df7f5f0e...f87e5991a6d7451dcb8d9637bfbc97413f497069)

Updates `docker/build-push-action` from 7.3.0 to 7.4.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/53b7df96c91f9c12dcc8a07bcb9ccacbed38856a...c3c9e263c25d99ce0380d002d59b67737d91b0dc)

Updates `github/codeql-action/upload-sarif` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `stumpylog/image-cleaner-action/ephemeral` from 0.12.0 to 0.13.0
- [Release notes](https://github.com/stumpylog/image-cleaner-action/releases)
- [Changelog](https://github.com/stumpylog/image-cleaner-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stumpylog/image-cleaner-action/compare/4fe057d991d63b8f6d5d22c40f17c1bca2226537...21f875bab2376314e0525c614e923c669434f0e4)

Updates `stumpylog/image-cleaner-action/untagged` from 0.12.0 to 0.13.0
- [Release notes](https://github.com/stumpylog/image-cleaner-action/releases)
- [Changelog](https://github.com/stumpylog/image-cleaner-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stumpylog/image-cleaner-action/compare/4fe057d991d63b8f6d5d22c40f17c1bca2226537...21f875bab2376314e0525c614e923c669434f0e4)

Updates `github/codeql-action/init` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `github/codeql-action/analyze` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `crowdin/github-action` from 3.0.2 to 3.2.0
- [Release notes](https://github.com/crowdin/github-action/releases)
- [Commits](https://github.com/crowdin/github-action/compare/0d5670f539973aea2f01abce61a8989934df0025...df474cdfb9f41d6ae777118749477c2cdf7cacc8)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: codecov/codecov-action
  dependency-version: 7.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: docker/build-push-action
  dependency-version: 7.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: stumpylog/image-cleaner-action/ephemeral
  dependency-version: 0.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: stumpylog/image-cleaner-action/untagged
  dependency-version: 0.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: crowdin/github-action
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-29 08:18:28 -07:00
Trenton H 61f36eb253 Chore: Disallow prerelease resolution in uv (#14296)
uv's default "if-necessary" prerelease policy will silently select a
prerelease package when no stable release satisfies the combined
constraint set, even without an explicit opt-in.  Disallow pre-releases
instead
2026-09-28 18:49:21 +00:00
Trenton H e50bcc2efb Chore: Updates typing baseline files (#14295)
* Chore: Refresh the mypy and pyrefly baselines

* Chore: Type factory calls as the model they build for mypy

factory-boy leaves Factory() unannotated, so mypy took UserFactory() to
be a factory instance rather than a User. A shared TypedModelFactory
base declares the call as returning the model for type checking only.

* Refresh again after rebase
2026-09-28 11:32:05 -07:00
dependabot[bot] 88000aab45 Chore(deps-dev): Bump the frontend-jest-dependencies group (#14286)
Bumps the frontend-jest-dependencies group in /src-ui with 2 updates: [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) and [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom).


Updates `jest` from 30.5.1 to 30.5.2
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.5.2/packages/jest)

Updates `jest-environment-jsdom` from 30.5.1 to 30.5.2
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.5.2/packages/jest-environment-jsdom)

---
updated-dependencies:
- dependency-name: jest
  dependency-version: 30.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-jest-dependencies
- dependency-name: jest-environment-jsdom
  dependency-version: 30.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-jest-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 09:22:21 +00:00
dependabot[bot] 01b4ab392c Chore(deps-dev): Bump eslint (#14287)
Bumps the frontend-eslint-dependencies group in /src-ui with 1 update: [eslint](https://github.com/eslint/eslint).


Updates `eslint` from 10.10.0 to 10.11.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.10.0...v10.11.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 10.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: frontend-eslint-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 09:11:55 +00:00
dependabot[bot] 52aa8758e3 Chore(deps-dev): Bump @types/node from 26.5.0 to 26.6.2 in /src-ui (#14288)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 26.5.0 to 26.6.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.6.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 09:00:39 +00:00
dependabot[bot] 4b7cc1c0c3 Chore(deps-dev): Bump prettier from 3.9.6 to 3.9.8 in /src-ui (#14289)
Bumps [prettier](https://github.com/prettier/prettier) from 3.9.6 to 3.9.8.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.9.6...3.9.8)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.9.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 08:48:03 +00:00
GitHub Actions f02d7455be Auto translate strings 2026-09-28 08:32:00 +00:00
dependabot[bot] a61c35fcd8 Chore(deps): Bump the frontend-angular-dependencies group (#14285)
Bumps the frontend-angular-dependencies group in /src-ui with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [@angular/cdk](https://github.com/angular/components) | `22.1.6` | `22.1.7` |
| [@angular/common](https://github.com/angular/angular/tree/HEAD/packages/common) | `22.1.6` | `22.1.7` |
| [@angular/compiler](https://github.com/angular/angular/tree/HEAD/packages/compiler) | `22.1.6` | `22.1.7` |
| [@angular/core](https://github.com/angular/angular/tree/HEAD/packages/core) | `22.1.6` | `22.1.7` |
| [@angular/forms](https://github.com/angular/angular/tree/HEAD/packages/forms) | `22.1.6` | `22.1.7` |
| [@angular/localize](https://github.com/angular/angular) | `22.1.6` | `22.1.7` |
| [@angular/platform-browser](https://github.com/angular/angular/tree/HEAD/packages/platform-browser) | `22.1.6` | `22.1.7` |
| [@angular/router](https://github.com/angular/angular/tree/HEAD/packages/router) | `22.1.6` | `22.1.7` |
| [@ng-select/ng-select](https://github.com/ng-select/ng-select) | `24.1.1` | `24.1.2` |
| [@angular/compiler-cli](https://github.com/angular/angular/tree/HEAD/packages/compiler-cli) | `22.1.6` | `22.1.7` |


Updates `@angular/cdk` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/components/releases)
- [Changelog](https://github.com/angular/components/blob/main/CHANGELOG.md)
- [Commits](https://github.com/angular/components/compare/v22.1.6...v22.1.7)

Updates `@angular/common` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.1.7/packages/common)

Updates `@angular/compiler` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.1.7/packages/compiler)

Updates `@angular/core` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.1.7/packages/core)

Updates `@angular/forms` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.1.7/packages/forms)

Updates `@angular/localize` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/compare/v22.1.6...v22.1.7)

Updates `@angular/platform-browser` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.1.7/packages/platform-browser)

Updates `@angular/router` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.1.7/packages/router)

Updates `@ng-select/ng-select` from 24.1.1 to 24.1.2
- [Release notes](https://github.com/ng-select/ng-select/releases)
- [Commits](https://github.com/ng-select/ng-select/compare/v24.1.1...v24.1.2)

Updates `@angular/compiler-cli` from 22.1.6 to 22.1.7
- [Release notes](https://github.com/angular/angular/releases)
- [Commits](https://github.com/angular/angular/commits/v22.1.7/packages/compiler-cli)

---
updated-dependencies:
- dependency-name: "@angular/cdk"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/common"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/core"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/forms"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/localize"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/platform-browser"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/router"
  dependency-version: 22.1.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@ng-select/ng-select"
  dependency-version: 24.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
- dependency-name: "@angular/compiler-cli"
  dependency-version: 22.1.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: frontend-angular-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-28 08:30:31 +00:00
GitHub Actions 64552d5d5b Auto translate strings 2026-09-28 06:27:28 +00:00
shamoon a59edf3e5b Fix: use version page_count for versioned document (#14280) 2026-09-27 23:26:00 -07:00
Bernát Gábor 6a5d06ee26 Chore: replace bleach with turbohtml (#14269)
bleach's repository is archived, and it depends on html5lib, which has not had
a release since 2020. turbohtml provides a bleach-compatible clean and a native
linkify that runs in linear time, so the 2048-character cap on email
linkification from #13187 is no longer needed and long messages get their
email addresses linked again.
2026-09-27 16:50:19 -07:00
GitHub Actions 126ec414a8 Auto translate strings 2026-09-27 14:08:00 +00:00
shamoon 773744d04b Enhancement: include Django admin with 2FA (#14270) 2026-09-27 07:06:26 -07:00
shamoon 7b98085b31 Fix: prevent dev pdf viewer link flash (#14268) 2026-09-26 07:02:49 -07:00
shamoon b3bf220f56 Fix: rest of PDF link fix 2026-09-25 20:08:36 -07:00
shamoon d4429c3cc7 Fix: allow pointer events for pdf links in pngx viewer (#14264) 2026-09-25 18:35:21 -07:00
Trenton H 4f777d7438 Feature: propagate resolved secrets to interactive container shells (#14254)
docker exec bash bypasses s6, so *_FILE secrets resolved by init-env-file
into /run/s6/container_environment aren't visible even though s6 services
and management wrappers already see them via with-contenv.

Add /etc/profile.d/contenv.sh, sourced by both login and non-login
interactive bash shells, to export those resolved values into the shell.
2026-09-24 13:07:41 -07:00
shamoon c63afb47b2 Documentation: correct duplicates info (#14243) 2026-09-23 08:27:18 -07:00
228 changed files with 53727 additions and 52797 deletions

No files matched your search

@@ -66,7 +66,7 @@ services:
command: command:
- "gotenberg" - "gotenberg"
- "--chromium-disable-javascript=true" - "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*" - "--chromium-allow-list=^file:///tmp/.*"
tika: tika:
image: docker.io/apache/tika:3.3.1.0 image: docker.io/apache/tika:3.3.1.0
restart: unless-stopped restart: unless-stopped
+53
View File
@@ -0,0 +1,53 @@
name: Install apt packages
description: >
Installs apt packages with a per-attempt timeout and retries. The hosted runner's Azure apt mirror intermittently serves archives at a crawl, which apt's own timeout does not catch. A stuck attempt is killed and retried, which usually lands on a healthy mirror node.
inputs:
packages:
description: Space separated list of packages to install
required: true
attempts:
description: Maximum number of attempts
default: "3"
attempt-timeout:
description: Seconds before a single attempt is killed
default: "180"
runs:
using: composite
steps:
- name: Install ${{ inputs.packages }}
shell: bash
env:
PACKAGES: ${{ inputs.packages }}
ATTEMPTS: ${{ inputs.attempts }}
ATTEMPT_TIMEOUT: ${{ inputs.attempt-timeout }}
run: |
SUDO=""
if [ "$(id -u)" -ne 0 ]; then
SUDO="sudo"
fi
attempt_install() {
$SUDO apt-get update -q
# Intentionally unquoted, this is a list of package names
$SUDO apt-get install -q --yes --no-install-recommends $PACKAGES
}
export -f attempt_install
export SUDO PACKAGES
for attempt in $(seq 1 "${ATTEMPTS}"); do
echo "::group::apt install attempt ${attempt}/${ATTEMPTS}"
start=$(date +%s)
if timeout --kill-after=10 "${ATTEMPT_TIMEOUT}" bash -c attempt_install; then
echo "::endgroup::"
echo "Installed in $(( $(date +%s) - start ))s"
exit 0
fi
echo "::endgroup::"
echo "::warning::apt install attempt ${attempt}/${ATTEMPTS} failed or exceeded ${ATTEMPT_TIMEOUT}s"
# A killed attempt can leave dpkg half configured
$SUDO dpkg --configure -a || true
done
echo "::error::apt install failed after ${ATTEMPTS} attempts"
exit 1
-1
View File
@@ -59,7 +59,6 @@ updates:
- "drf-*" - "drf-*"
- "djangorestframework" - "djangorestframework"
- "whitenoise" - "whitenoise"
- "bleach"
- "jinja2" - "jinja2"
# Async, Task Queuing & Caching # Async, Task Queuing & Caching
async-tasks: async-tasks:
+11 -12
View File
@@ -80,7 +80,7 @@ jobs:
needs: changes needs: changes
if: needs.changes.outputs.backend_changed == 'true' if: needs.changes.outputs.backend_changed == 'true'
name: "Python ${{ matrix.python-version }}" name: "Python ${{ matrix.python-version }}"
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
contents: read contents: read
strategy: strategy:
@@ -102,20 +102,19 @@ jobs:
with: with:
python-version: "${{ matrix.python-version }}" python-version: "${{ matrix.python-version }}"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
python-version: ${{ steps.setup-python.outputs.python-version }} python-version: ${{ steps.setup-python.outputs.python-version }}
- name: Install system dependencies - name: Install system dependencies
timeout-minutes: 10 timeout-minutes: 12
run: | uses: $/.github/actions/apt-install
sudo apt-get update -qq with:
sudo apt-get install -qq --no-install-recommends \ packages: unpaper tesseract-ocr imagemagick ghostscript poppler-utils
unpaper tesseract-ocr imagemagick ghostscript poppler-utils
- name: Configure ImageMagick - name: Configure ImageMagick
run: | run: |
sudo cp docker/rootfs/etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml sudo cp docker/rootfs/etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-7/policy.xml
- name: Install Python dependencies - name: Install Python dependencies
env: env:
PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }} PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }}
@@ -139,13 +138,13 @@ jobs:
pytest pytest
- name: Upload test results to Codecov - name: Upload test results to Codecov
if: always() if: always()
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
flags: backend-python-${{ matrix.python-version }} flags: backend-python-${{ matrix.python-version }}
files: junit.xml files: junit.xml
report_type: test_results report_type: test_results
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
flags: backend-python-${{ matrix.python-version }} flags: backend-python-${{ matrix.python-version }}
files: coverage.xml files: coverage.xml
@@ -159,7 +158,7 @@ jobs:
needs: changes needs: changes
if: needs.changes.outputs.backend_changed == 'true' if: needs.changes.outputs.backend_changed == 'true'
name: Check project typing name: Check project typing
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
contents: read contents: read
env: env:
@@ -176,7 +175,7 @@ jobs:
with: with:
python-version: "${{ env.DEFAULT_PYTHON }}" python-version: "${{ env.DEFAULT_PYTHON }}"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
+6 -6
View File
@@ -24,10 +24,10 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- runner: ubuntu-24.04 - runner: ubuntu-26.04
arch: amd64 arch: amd64
platform: linux/amd64 platform: linux/amd64
- runner: ubuntu-24.04-arm - runner: ubuntu-26.04-arm
arch: arm64 arch: arm64
platform: linux/arm64 platform: linux/arm64
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
@@ -106,7 +106,7 @@ jobs:
echo "repository=${repo_name}" echo "repository=${repo_name}"
echo "name=${repo_name}" >> $GITHUB_OUTPUT echo "name=${repo_name}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
@@ -132,7 +132,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
- name: Build and push by digest - name: Build and push by digest
id: build id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
@@ -163,7 +163,7 @@ jobs:
archive: false archive: false
merge-and-push: merge-and-push:
name: Merge and Push Manifest name: Merge and Push Manifest
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
needs: build-arch needs: build-arch
if: needs.build-arch.outputs.should-push == 'true' if: needs.build-arch.outputs.should-push == 'true'
environment: image-publishing environment: image-publishing
@@ -182,7 +182,7 @@ jobs:
echo "Downloaded digests:" echo "Downloaded digests:"
ls -la /tmp/digests/ ls -la /tmp/digests/
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
+3 -3
View File
@@ -65,7 +65,7 @@ jobs:
needs: changes needs: changes
if: needs.changes.outputs.docs_changed == 'true' if: needs.changes.outputs.docs_changed == 'true'
name: Build Documentation name: Build Documentation
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
steps: steps:
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
- name: Checkout - name: Checkout
@@ -78,7 +78,7 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }} python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
@@ -102,7 +102,7 @@ jobs:
name: Deploy Documentation name: Deploy Documentation
needs: [changes, build] needs: [changes, build]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changes.outputs.docs_changed == 'true' if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.changes.outputs.docs_changed == 'true'
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
pages: write pages: write
id-token: write id-token: write
+13 -12
View File
@@ -72,7 +72,7 @@ jobs:
needs: changes needs: changes
if: needs.changes.outputs.frontend_changed == 'true' if: needs.changes.outputs.frontend_changed == 'true'
name: Install Dependencies name: Install Dependencies
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
contents: read contents: read
steps: steps:
@@ -104,7 +104,7 @@ jobs:
name: Lint name: Lint
needs: [changes, install-dependencies] needs: [changes, install-dependencies]
if: needs.changes.outputs.frontend_changed == 'true' if: needs.changes.outputs.frontend_changed == 'true'
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
contents: read contents: read
steps: steps:
@@ -137,7 +137,7 @@ jobs:
name: "Unit Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})" name: "Unit Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})"
needs: [changes, install-dependencies] needs: [changes, install-dependencies]
if: needs.changes.outputs.frontend_changed == 'true' if: needs.changes.outputs.frontend_changed == 'true'
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
contents: read contents: read
strategy: strategy:
@@ -174,13 +174,13 @@ jobs:
run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }} run: cd src-ui && pnpm run test --max-workers=2 --shard=${{ matrix.shard-index }}/${{ matrix.shard-count }}
- name: Upload test results to Codecov - name: Upload test results to Codecov
if: always() if: always()
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
flags: frontend-node-${{ matrix.node-version }} flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/ directory: src-ui/
report_type: test_results report_type: test_results
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
flags: frontend-node-${{ matrix.node-version }} flags: frontend-node-${{ matrix.node-version }}
directory: src-ui/coverage/ directory: src-ui/coverage/
@@ -188,10 +188,10 @@ jobs:
name: E2E Tests name: E2E Tests
needs: [changes, install-dependencies] needs: [changes, install-dependencies]
if: needs.changes.outputs.frontend_changed == 'true' if: needs.changes.outputs.frontend_changed == 'true'
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
contents: read contents: read
container: mcr.microsoft.com/playwright:v1.62.1-noble container: mcr.microsoft.com/playwright:v1.62.1-resolute
env: env:
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
@@ -216,7 +216,7 @@ jobs:
with: with:
python-version: '3.12' python-version: '3.12'
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
version: '0.12.x' version: '0.12.x'
enable-cache: false enable-cache: false
@@ -231,9 +231,10 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: cd src-ui && pnpm install --frozen-lockfile run: cd src-ui && pnpm install --frozen-lockfile
- name: Install backend system dependencies - name: Install backend system dependencies
run: | timeout-minutes: 12
apt-get update uses: $/.github/actions/apt-install
apt-get install --yes --quiet --no-install-recommends libmagic1 with:
packages: libmagic1
- name: Install backend dependencies - name: Install backend dependencies
env: env:
PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }} PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }}
@@ -245,7 +246,7 @@ jobs:
name: Frontend Build name: Frontend Build
needs: [changes, unit-tests, e2e-tests] needs: [changes, unit-tests, e2e-tests]
if: needs.changes.outputs.frontend_changed == 'true' if: needs.changes.outputs.frontend_changed == 'true'
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
contents: read contents: read
steps: steps:
+10 -9
View File
@@ -14,7 +14,7 @@ permissions: {}
jobs: jobs:
wait-for-docker: wait-for-docker:
name: Wait for Docker Build name: Wait for Docker Build
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
checks: read checks: read
statuses: read statuses: read
@@ -30,7 +30,7 @@ jobs:
build-release: build-release:
name: Build Release name: Build Release
needs: wait-for-docker needs: wait-for-docker
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
contents: read contents: read
steps: steps:
@@ -59,7 +59,7 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }} python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false enable-cache: false
@@ -70,9 +70,10 @@ jobs:
run: | run: |
uv sync --python "${PYTHON_VERSION}" --dev --frozen uv sync --python "${PYTHON_VERSION}" --dev --frozen
- name: Install system dependencies - name: Install system dependencies
run: | timeout-minutes: 12
sudo apt-get update -qq uses: $/.github/actions/apt-install
sudo apt-get install -qq --no-install-recommends gettext liblept5 with:
packages: gettext libleptonica6
# ---- Build Documentation ---- # ---- Build Documentation ----
- name: Build documentation - name: Build documentation
env: env:
@@ -144,7 +145,7 @@ jobs:
publish-release: publish-release:
name: Publish Release name: Publish Release
needs: build-release needs: build-release
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
@@ -196,7 +197,7 @@ jobs:
name: Append Changelog name: Append Changelog
needs: publish-release needs: publish-release
if: needs.publish-release.outputs.prerelease == 'false' if: needs.publish-release.outputs.prerelease == 'false'
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
@@ -212,7 +213,7 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }} python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false enable-cache: false
+3 -3
View File
@@ -15,7 +15,7 @@ permissions:
jobs: jobs:
zizmor: zizmor:
name: Run zizmor name: Run zizmor
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
contents: read contents: read
actions: read actions: read
@@ -29,7 +29,7 @@ jobs:
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
semgrep: semgrep:
name: Semgrep CE name: Semgrep CE
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
container: container:
image: semgrep/semgrep:1.155.0@sha256:cc869c685dcc0fe497c86258da9f205397d8108e56d21a86082ea4886e52784d image: semgrep/semgrep:1.155.0@sha256:cc869c685dcc0fe497c86258da9f205397d8108e56d21a86082ea4886e52784d
if: github.actor != 'dependabot[bot]' if: github.actor != 'dependabot[bot]'
@@ -44,7 +44,7 @@ jobs:
- name: Run Semgrep - name: Run Semgrep
run: semgrep scan --config auto --sarif-output results.sarif run: semgrep scan --config auto --sarif-output results.sarif
- name: Upload results to GitHub code scanning - name: Upload results to GitHub code scanning
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
if: always() if: always()
with: with:
sarif_file: results.sarif sarif_file: results.sarif
+4 -4
View File
@@ -17,7 +17,7 @@ jobs:
cleanup-images: cleanup-images:
name: Cleanup Image Tags for ${{ matrix.primary-name }} name: Cleanup Image Tags for ${{ matrix.primary-name }}
if: github.repository_owner == 'paperless-ngx' if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
environment: registry-maintenance environment: registry-maintenance
strategy: strategy:
fail-fast: false fail-fast: false
@@ -29,7 +29,7 @@ jobs:
steps: steps:
- name: Clean temporary images - name: Clean temporary images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@4fe057d991d63b8f6d5d22c40f17c1bca2226537 # v0.12.0 uses: stumpylog/image-cleaner-action/ephemeral@21f875bab2376314e0525c614e923c669434f0e4 # v0.13.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}" owner: "${{ github.repository_owner }}"
@@ -42,7 +42,7 @@ jobs:
cleanup-untagged-images: cleanup-untagged-images:
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }} name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
if: github.repository_owner == 'paperless-ngx' if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
needs: needs:
- cleanup-images - cleanup-images
environment: registry-maintenance environment: registry-maintenance
@@ -56,7 +56,7 @@ jobs:
steps: steps:
- name: Clean untagged images - name: Clean untagged images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@4fe057d991d63b8f6d5d22c40f17c1bca2226537 # v0.12.0 uses: stumpylog/image-cleaner-action/untagged@21f875bab2376314e0525c614e923c669434f0e4 # v0.13.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "${{ github.repository_owner }}" owner: "${{ github.repository_owner }}"
+3 -3
View File
@@ -21,7 +21,7 @@ on:
jobs: jobs:
analyze: analyze:
name: Analyze name: Analyze
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
actions: read actions: read
contents: read contents: read
@@ -39,7 +39,7 @@ jobs:
persist-credentials: false persist-credentials: false
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -47,4 +47,4 @@ jobs:
# Prefix the list here with "+" to use these queries and those in the config file. # Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main # queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
synchronize-with-crowdin: synchronize-with-crowdin:
name: Crowdin Sync name: Crowdin Sync
if: github.repository_owner == 'paperless-ngx' if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
environment: translation-sync environment: translation-sync
steps: steps:
- name: Checkout - name: Checkout
@@ -22,7 +22,7 @@ jobs:
token: ${{ secrets.PNGX_BOT_PAT }} token: ${{ secrets.PNGX_BOT_PAT }}
persist-credentials: false persist-credentials: false
- name: crowdin action - name: crowdin action
uses: crowdin/github-action@0d5670f539973aea2f01abce61a8989934df0025 # v3.0.2 uses: crowdin/github-action@9c23991700c0ec5256fd41089b9d9d7d540e424e # v3.3.0
with: with:
upload_translations: false upload_translations: false
download_translations: true download_translations: true
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
# Note: peakoss/anti-slop does not support the `issues` event yet (all of its # Note: peakoss/anti-slop does not support the `issues` event yet (all of its
# issue inputs are still commented out upstream), so the checks that the PR Bot # issue inputs are still commented out upstream), so the checks that the PR Bot
# workflow gets from the action are implemented manually here. # workflow gets from the action are implemented manually here.
runs-on: ubuntu-latest runs-on: ubuntu-slim
permissions: permissions:
issues: write issues: write
steps: steps:
+2 -12
View File
@@ -4,7 +4,7 @@ on:
types: [opened] types: [opened]
jobs: jobs:
Anti-slop: Anti-slop:
runs-on: ubuntu-latest runs-on: ubuntu-slim
permissions: permissions:
contents: read contents: read
issues: read issues: read
@@ -24,7 +24,7 @@ jobs:
ASLOP-PR-VERIFY ASLOP-PR-VERIFY
pr-bot: pr-bot:
name: Automated PR Bot name: Automated PR Bot
runs-on: ubuntu-latest runs-on: ubuntu-slim
# Runs after Anti-slop so the welcome comment can see whether the PR was closed # Runs after Anti-slop so the welcome comment can see whether the PR was closed
# instead of racing it. Still runs if that job fails, so labeling is not lost. # instead of racing it. Still runs if that job fails, so labeling is not lost.
needs: Anti-slop needs: Anti-slop
@@ -38,16 +38,6 @@ jobs:
uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0 uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Label by size
uses: Gascon1/pr-size-labeler@deff8ed00a76639a7c0f197525bafa3350ba4c36 # v1.3.0
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
xs_label: 'small-change'
xs_diff: '9'
s_label: 'non-trivial'
s_diff: '99999'
fail_if_xl: 'false'
excluded_files: /\.lock$/ /\.txt$/ ^src-ui/pnpm-lock\.yaml$ ^src-ui/messages\.xlf$ ^src/locale/en_US/LC_MESSAGES/django\.po$
- name: Label by PR title - name: Label by PR title
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with: with:
+1 -1
View File
@@ -12,7 +12,7 @@ permissions:
jobs: jobs:
pr_opened_or_reopened: pr_opened_or_reopened:
name: pr_opened_or_reopened name: pr_opened_or_reopened
runs-on: ubuntu-24.04 runs-on: ubuntu-slim
permissions: permissions:
# write permission is required for autolabeler # write permission is required for autolabeler
pull-requests: write pull-requests: write
+5 -5
View File
@@ -9,7 +9,7 @@ jobs:
stale: stale:
name: 'Stale' name: 'Stale'
if: github.repository_owner == 'paperless-ngx' if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
issues: write issues: write
pull-requests: write pull-requests: write
@@ -34,7 +34,7 @@ jobs:
lock-threads: lock-threads:
name: 'Lock Old Threads' name: 'Lock Old Threads'
if: github.repository_owner == 'paperless-ngx' if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04 runs-on: ubuntu-26.04
permissions: permissions:
issues: write issues: write
pull-requests: write pull-requests: write
@@ -58,7 +58,7 @@ jobs:
close-answered-discussions: close-answered-discussions:
name: 'Close Answered Discussions' name: 'Close Answered Discussions'
if: github.repository_owner == 'paperless-ngx' if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04 runs-on: ubuntu-slim
permissions: permissions:
discussions: write discussions: write
steps: steps:
@@ -117,7 +117,7 @@ jobs:
close-outdated-discussions: close-outdated-discussions:
name: 'Close Outdated Discussions' name: 'Close Outdated Discussions'
if: github.repository_owner == 'paperless-ngx' if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04 runs-on: ubuntu-slim
permissions: permissions:
discussions: write discussions: write
steps: steps:
@@ -211,7 +211,7 @@ jobs:
close-unsupported-feature-requests: close-unsupported-feature-requests:
name: 'Close Unsupported Feature Requests' name: 'Close Unsupported Feature Requests'
if: github.repository_owner == 'paperless-ngx' if: github.repository_owner == 'paperless-ngx'
runs-on: ubuntu-24.04 runs-on: ubuntu-slim
permissions: permissions:
discussions: write discussions: write
steps: steps:
+6 -5
View File
@@ -8,7 +8,7 @@ env:
jobs: jobs:
generate-translate-strings: generate-translate-strings:
name: Generate Translation Strings name: Generate Translation Strings
runs-on: ubuntu-latest runs-on: ubuntu-26.04
environment: translation-sync environment: translation-sync
permissions: permissions:
contents: write contents: write
@@ -25,11 +25,12 @@ jobs:
id: setup-python id: setup-python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- name: Install system dependencies - name: Install system dependencies
run: | timeout-minutes: 12
sudo apt-get update -qq uses: $/.github/actions/apt-install
sudo apt-get install -qq --no-install-recommends gettext with:
packages: gettext
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with: with:
version: ${{ env.DEFAULT_UV_VERSION }} version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true enable-cache: true
+706 -456
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -38,7 +38,7 @@ repos:
- json - json
# See https://github.com/prettier/prettier/issues/15742 for the fork reason # See https://github.com/prettier/prettier/issues/15742 for the fork reason
- repo: https://github.com/rbubley/mirrors-prettier - repo: https://github.com/rbubley/mirrors-prettier
rev: 'v3.9.6' rev: 'v3.9.9'
hooks: hooks:
- id: prettier - id: prettier
types_or: types_or:
@@ -46,11 +46,11 @@ repos:
- ts - ts
- markdown - markdown
additional_dependencies: additional_dependencies:
- prettier@3.9.6 - prettier@3.9.9
- 'prettier-plugin-organize-imports@4.3.0' - 'prettier-plugin-organize-imports@4.3.0'
# Python hooks # Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.7 rev: v0.16.9
hooks: hooks:
- id: ruff-check - id: ruff-check
- id: ruff-format - id: ruff-format
+4804 -12941
View File
File diff suppressed because it is too large. Load diff
+4 -13
View File
@@ -43,26 +43,17 @@ Once you have submitted a **P**ull **R**equest it will be reviewed, approved, an
Important: Pull requests that implement a new feature or enhancement _should almost always target an existing feature request_ with evidence of community interest and discussion. This is in order to balance the work of implementing and maintaining new features / enhancements. Instead of opening a PR which does not meet this requirement, please open a feature request instead, to gather feedback from both users and the project maintainers. Important: Pull requests that implement a new feature or enhancement _should almost always target an existing feature request_ with evidence of community interest and discussion. This is in order to balance the work of implementing and maintaining new features / enhancements. Instead of opening a PR which does not meet this requirement, please open a feature request instead, to gather feedback from both users and the project maintainers.
## Non-Trivial Requests ## PR Review Process
PRs deemed `non-trivial` will go through a stricter review process before being merged into `dev`. This is to ensure code quality and complete functionality (free of side effects). Our community review process for PRs is the following:
Examples of `non-trivial` PRs might include:
- Additional features
- Large changes to many distinct files
- Breaking or deprecation of existing features
Our community review process for `non-trivial` PRs is the following:
1. Must pass usual automated code tests and formatting checks. 1. Must pass usual automated code tests and formatting checks.
2. The PR will be assigned and pinged to the appropriately experienced team (i.e. @paperless-ngx/backend for backend changes). 2. The PR will be assigned and pinged to the appropriately experienced team (i.e. @paperless-ngx/backend for backend changes).
3. Development team will check and test code manually (possibly over several days). 3. Development team will check and test code manually (possibly over several days).
- You may be asked to make changes or rebase. - You may be asked to make changes or rebase.
- The team may ask for additional testing done by @paperless-ngx/test 4. One or more members of the team will approve and finally merge the request into `dev` 🎉.
4. **At least two** members of the team will approve and finally merge the request into `dev` 🎉.
This process might be slow as community members have different schedules and time to dedicate to the Paperless project. However it ensures community code reviews are as brilliantly thorough as they once were with @jonaswinkler. This process might be slow as community members have different schedules and time to dedicate to the Paperless project. However it ensures community code reviews are thorough and of high quality.
# Use of AI Tools # Use of AI Tools
+4 -2
View File
@@ -30,7 +30,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs # Purpose: Installs s6-overlay and rootfs
# Comments: # Comments:
# - Don't leave anything extra in here either # - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.12.16-python3.14-trixie-slim AS s6-overlay-base FROM ghcr.io/astral-sh/uv:0.12.23-python3.14-trixie-slim AS s6-overlay-base
WORKDIR /usr/src/s6 WORKDIR /usr/src/s6
@@ -171,7 +171,9 @@ RUN set -eux \
&& cp /etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml \ && cp /etc/ImageMagick-6/paperless-policy.xml /etc/ImageMagick-6/policy.xml \
&& echo "Cleaning up image layer" \ && echo "Cleaning up image layer" \
&& rm --force --verbose *.deb \ && rm --force --verbose *.deb \
&& rm --recursive --force --verbose /var/lib/apt/lists/* && rm --recursive --force --verbose /var/lib/apt/lists/* \
&& echo "Configuring interactive shells to source the s6 container environment" \
&& echo '. /etc/profile.d/contenv.sh' >> /etc/bash.bashrc
WORKDIR /usr/src/paperless/src/ WORKDIR /usr/src/paperless/src/
+4 -8
View File
@@ -21,7 +21,7 @@ Paperless-ngx is a document management system that transforms your physical docu
Paperless-ngx is the official successor to the original [Paperless](https://github.com/the-paperless-project/paperless) & [Paperless-ng](https://github.com/jonaswinkler/paperless-ng) projects and is designed to distribute the responsibility of advancing and supporting the project among a team of people. [Consider joining us!](#community-support) Paperless-ngx is the official successor to the original [Paperless](https://github.com/the-paperless-project/paperless) & [Paperless-ng](https://github.com/jonaswinkler/paperless-ng) projects and is designed to distribute the responsibility of advancing and supporting the project among a team of people. [Consider joining us!](#community-support)
Thanks to the generous folks at [DigitalOcean](https://m.do.co/c/8d70b916d462), a demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com) using login `demo` / `demo`. _Note: demo content is reset frequently and confidential information should not be uploaded._ Thanks to the generous folks at [Fyra Stack](https://fyrastack.com), a demo is available at [demo.paperless-ngx.com](https://demo.paperless-ngx.com) using login `demo` / `demo`. _Note: demo content is reset frequently and confidential information should not be uploaded._
- [Features](#features) - [Features](#features)
- [Getting started](#getting-started) - [Getting started](#getting-started)
@@ -33,13 +33,9 @@ Thanks to the generous folks at [DigitalOcean](https://m.do.co/c/8d70b916d462),
- [Related Projects](#related-projects) - [Related Projects](#related-projects)
- [Important Note](#important-note) - [Important Note](#important-note)
<p align="right">This project is supported by:<br/> <p align="right">Demo hosting provided by:<br/>
<a href="https://m.do.co/c/8d70b916d462" style="padding-top: 4px; display: block;"> <a href="https://fyrastack.com">
<picture> <img src="docs/assets/sponsors/fyra-stack.svg" alt="" width="28px" align="absmiddle"> Fyra Stack
<source media="(prefers-color-scheme: dark)" srcset="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_white.svg" width="140px">
<source media="(prefers-color-scheme: light)" srcset="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="140px">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_black_.svg" width="140px">
</picture>
</a> </a>
</p> </p>
+2 -2
View File
@@ -14,7 +14,7 @@ services:
command: command:
- "gotenberg" - "gotenberg"
- "--chromium-disable-javascript=true" - "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*" - "--chromium-allow-list=^file:///tmp/.*"
- "--log-level=warn" - "--log-level=warn"
- "--log-format=text" - "--log-format=text"
tika: tika:
@@ -24,7 +24,7 @@ services:
network_mode: host network_mode: host
restart: unless-stopped restart: unless-stopped
greenmail: greenmail:
image: docker.io/greenmail/standalone:2.1.13 image: docker.io/greenmail/standalone:2.1.14
hostname: greenmail hostname: greenmail
container_name: greenmail container_name: greenmail
environment: environment:
@@ -40,7 +40,6 @@ services:
volumes: volumes:
- dbdata:/var/lib/mysql - dbdata:/var/lib/mysql
environment: environment:
MARIADB_HOST: paperless
MARIADB_DATABASE: paperless MARIADB_DATABASE: paperless
MARIADB_USER: paperless MARIADB_USER: paperless
MARIADB_PASSWORD: paperless MARIADB_PASSWORD: paperless
@@ -79,7 +78,7 @@ services:
command: command:
- "gotenberg" - "gotenberg"
- "--chromium-disable-javascript=true" - "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*" - "--chromium-allow-list=^file:///tmp/.*"
tika: tika:
image: docker.io/apache/tika:3.3.1.0 image: docker.io/apache/tika:3.3.1.0
restart: unless-stopped restart: unless-stopped
@@ -36,7 +36,6 @@ services:
volumes: volumes:
- dbdata:/var/lib/mysql - dbdata:/var/lib/mysql
environment: environment:
MARIADB_HOST: paperless
MARIADB_DATABASE: paperless MARIADB_DATABASE: paperless
MARIADB_USER: paperless MARIADB_USER: paperless
MARIADB_PASSWORD: paperless MARIADB_PASSWORD: paperless
@@ -74,7 +74,7 @@ services:
command: command:
- "gotenberg" - "gotenberg"
- "--chromium-disable-javascript=true" - "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*" - "--chromium-allow-list=^file:///tmp/.*"
tika: tika:
image: docker.io/apache/tika:3.3.1.0 image: docker.io/apache/tika:3.3.1.0
restart: unless-stopped restart: unless-stopped
@@ -63,7 +63,7 @@ services:
command: command:
- "gotenberg" - "gotenberg"
- "--chromium-disable-javascript=true" - "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*" - "--chromium-allow-list=^file:///tmp/.*"
tika: tika:
image: docker.io/apache/tika:3.3.1.0 image: docker.io/apache/tika:3.3.1.0
restart: unless-stopped restart: unless-stopped
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
# Source s6 container environment for interactive shells.
# Ensures variables resolved from *_FILE secret injection are visible
# when using 'docker exec bash'. Does not affect s6 services (those
# use with-contenv directly). Has no effect in non-container contexts
# because the directory will not exist.
# Note: sh/dash shells opened via 'docker exec sh' are not covered;
# only bash-based sessions benefit from this file.
_pngx_contenv="/run/s6/container_environment"
if [ -d "${_pngx_contenv}" ]; then
for _pngx_f in "${_pngx_contenv}"/*; do
[ -f "${_pngx_f}" ] || continue
_pngx_name=$(basename "${_pngx_f}")
_pngx_val=$(cat "${_pngx_f}")
export "${_pngx_name}=${_pngx_val}"
done
fi
unset _pngx_contenv _pngx_f _pngx_name _pngx_val
+22 -7
View File
@@ -136,13 +136,15 @@ for suggested generation and embedding models.
### AI-assisted suggestions ### AI-assisted suggestions
With AI enabled, Paperless-ngx can suggest a title, tags, correspondent, document type, With AI enabled, Paperless-ngx can suggest a title, tags, correspondent, document type,
storage path and dates by sending the document to the LLM. This is **opt-in per request** storage path and dates by sending the document to the LLM using "Suggest" button on the document
and surfaces through the "Suggest" control on the document detail page, alongside the detail page. You can choose which type of suggestions are requested by default under Settings >
classic classifier-based suggestions — it does not disable them. Suggestions are requested Documents, either ML (classifier-based) suggestions, AI suggestions, or both. When both are requested
automatically when you open a document that carries an inbox tag unless "Automatically request the results are combined.
suggestions for inbox documents" under Settings > Documents is disabled. Suggestion output
language can be steered with Suggestions are requested automatically when you open a document that carries an inbox tag
[`PAPERLESS_AI_LLM_OUTPUT_LANGUAGE`](configuration.md#PAPERLESS_AI_LLM_OUTPUT_LANGUAGE) unless "Automatically request suggestions for inbox documents" under Settings > Documents is disabled.
Suggestion output language can be steered with [`PAPERLESS_AI_LLM_OUTPUT_LANGUAGE`](configuration.md#PAPERLESS_AI_LLM_OUTPUT_LANGUAGE)
(otherwise it follows the user's UI language). (otherwise it follows the user's UI language).
### The LLM index (RAG) and similar documents ### The LLM index (RAG) and similar documents
@@ -1008,6 +1010,19 @@ documents to both separate and categorize them in a single operation.
**Example:** A 6-page scan with TAG:invoice on page 3 and TAG:receipt on page 5 will create **Example:** A 6-page scan with TAG:invoice on page 3 and TAG:receipt on page 5 will create
three documents: pages 1-2 (no tags), pages 3-4 (tagged "invoice"), and pages 5-6 (tagged "receipt"). three documents: pages 1-2 (no tags), pages 3-4 (tagged "invoice"), and pages 5-6 (tagged "receipt").
### Barcode Contents {#barcode-contents}
By default, Paperless only uses barcodes for splitting, ASNs and tags. With
[`PAPERLESS_CONSUMER_STORE_BARCODE_VALUES`](configuration.md#PAPERLESS_CONSUMER_STORE_BARCODE_VALUES)
enabled, it stores the content of every barcode with the document, e.g. payment codes or QR codes.
- Barcodes are listed on the **Metadata** tab with page, type and content, and can be copied.
- The API returns them in the `barcodes` field of `/api/documents/{id}/metadata/`.
- They can be [searched](usage.md#searching-barcodes), e.g. `barcodes:DE89370400440532013000`.
- Only the first [`PAPERLESS_CONSUMER_BARCODE_MAX_PAGES`](configuration.md#PAPERLESS_CONSUMER_BARCODE_MAX_PAGES)
pages are scanned. Reprocessing reads the barcodes of existing documents.
- Each version keeps its own barcodes, and the newest version's are shown and searched.
## Automatic collation of double-sided documents {#collate} ## Automatic collation of double-sided documents {#collate}
!!! note !!! note
+29
View File
@@ -101,3 +101,32 @@ h4 code {
border-top: .05rem solid var(--md-default-fg-color--lightest); border-top: .05rem solid var(--md-default-fg-color--lightest);
} }
} }
.md-typeset .demo-sponsor {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: .35rem;
margin-top: -1rem;
margin-bottom: 2rem;
color: var(--md-default-fg-color--light);
font-size: .65rem;
}
.md-typeset .demo-sponsor a {
display: inline-flex;
align-items: center;
gap: .25rem;
color: inherit;
font-weight: 600;
}
.md-typeset .demo-sponsor a:hover {
color: var(--md-accent-fg-color);
}
.md-typeset .demo-sponsor img {
width: 1rem;
height: 1rem;
flex-shrink: 0;
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="666.667" height="666.667" viewBox="0 0 666.667 666.667"><g stroke-width="1.333"><path fill="#fef400" d="m332.66668 418.66668h-84.66667v-85.33334-85.33333h84.66667 84.66667v-85.33334-85.333334h85.33333 85.33334v85.333334 85.33334h-85.33334-85.33333v85.33333 85.33334z"/><path fill="#fe6565" d="m248.00001 589.33335h-170.666674v-170.66667-170.66667h85.333334 85.33334v-85.33334-85.333334h84.66667 84.66667v85.333334 85.33334h85.33333 85.33334v85.33333 85.33334h-84.66667-84.66667v85.33334 85.33333zm84.66667-170.66667h84.66667v-85.33334-85.33333h-84.66667-84.66667v85.33333 85.33334z"/></g></svg>

After

Width:  |  Height:  |  Size: 641 B

+125
View File
@@ -1,5 +1,130 @@
# Changelog # Changelog
## paperless-ngx 3.3.0
### Features / Enhancements
- Enhancement: more control over suggestion requests [@shamoon](https://github.com/shamoon) ([#14258](https://github.com/paperless-ngx/paperless-ngx/pull/14258))
- Chorehancement: set manifest CORS for credentials [@shamoon](https://github.com/shamoon) ([#14307](https://github.com/paperless-ngx/paperless-ngx/pull/14307))
- Enhancement: include Django admin with 2FA [@shamoon](https://github.com/shamoon) ([#14270](https://github.com/paperless-ngx/paperless-ngx/pull/14270))
- Feature: propagate resolved secrets to interactive container shells [@stumpylog](https://github.com/stumpylog) ([#14254](https://github.com/paperless-ngx/paperless-ngx/pull/14254))
- Enhancement: support separate embedding API key [@furkanural](https://github.com/furkanural) ([#14067](https://github.com/paperless-ngx/paperless-ngx/pull/14067))
- Enhancement: support passthrough extra params for LLMs [@shamoon](https://github.com/shamoon) ([#14202](https://github.com/paperless-ngx/paperless-ngx/pull/14202))
- Feature: store barcode contents, list and search them [@jurassicparkicecream](https://github.com/jurassicparkicecream) ([#14276](https://github.com/paperless-ngx/paperless-ngx/pull/14276))
### Bug Fixes
- Fix: Set the ProcessedMail owner based on the rule owner in all cases [@stumpylog](https://github.com/stumpylog) ([#14356](https://github.com/paperless-ngx/paperless-ngx/pull/14356))
- Fix: Ensure log rotation settings are converted to integers [@stumpylog](https://github.com/stumpylog) ([#14343](https://github.com/paperless-ngx/paperless-ngx/pull/14343))
- Fix: Wrap apt calls into a retry so we can ideally jump a slow mirror [@stumpylog](https://github.com/stumpylog) ([#14344](https://github.com/paperless-ngx/paperless-ngx/pull/14344))
- Fix: ship pdf.js CMaps so CJK documents render in the viewer [@MrOggy85](https://github.com/MrOggy85) ([#14318](https://github.com/paperless-ngx/paperless-ngx/pull/14318))
- Fix: use version page\_count for versioned document [@shamoon](https://github.com/shamoon) ([#14280](https://github.com/paperless-ngx/paperless-ngx/pull/14280))
- Fix: allow pointer events for pdf links in pngx viewer [@shamoon](https://github.com/shamoon) ([#14264](https://github.com/paperless-ngx/paperless-ngx/pull/14264))
- Fix: During a move to the trash directory, only attempt to copy metadata [@stumpylog](https://github.com/stumpylog) ([#14250](https://github.com/paperless-ngx/paperless-ngx/pull/14250))
- Fix: convert file mtime to the configured time zone directly [@stumpylog](https://github.com/stumpylog) ([#14249](https://github.com/paperless-ngx/paperless-ngx/pull/14249))
- Fix: ensure documentDeleted subscription is discarded [@shamoon](https://github.com/shamoon) ([#14247](https://github.com/paperless-ngx/paperless-ngx/pull/14247))
- Chore: Fix bugs in the test suite [@stumpylog](https://github.com/stumpylog) ([#14244](https://github.com/paperless-ngx/paperless-ngx/pull/14244))
- Fix: ensure bulk operations are checked against version root [@shamoon](https://github.com/shamoon) ([#14246](https://github.com/paperless-ngx/paperless-ngx/pull/14246))
- Fix: indexing after document-added workflows signal [@shamoon](https://github.com/shamoon) ([#14242](https://github.com/paperless-ngx/paperless-ngx/pull/14242))
- Fix: Record full tag and custom field lists in bulk edit audit log [@stumpylog](https://github.com/stumpylog) ([#14236](https://github.com/paperless-ngx/paperless-ngx/pull/14236))
- Chore: update pikepdf for ocrmypdf requirement [@shamoon](https://github.com/shamoon) ([#14235](https://github.com/paperless-ngx/paperless-ngx/pull/14235))
- Fix: handle legacy bulk edit split page range with missing page\_count [@shamoon](https://github.com/shamoon) ([#14212](https://github.com/paperless-ngx/paperless-ngx/pull/14212))
- Fix: ignore invalid EXIF orientation when generating image archives [@zhzy0077](https://github.com/zhzy0077) ([#14203](https://github.com/paperless-ngx/paperless-ngx/pull/14203))
### Documentation
- Documentation: correct duplicates info [@shamoon](https://github.com/shamoon) ([#14243](https://github.com/paperless-ngx/paperless-ngx/pull/14243))
### Maintenance
- Chore(deps): Bump the actions group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14332](https://github.com/paperless-ngx/paperless-ngx/pull/14332))
- Fix: Wrap apt calls into a retry so we can ideally jump a slow mirror [@stumpylog](https://github.com/stumpylog) ([#14344](https://github.com/paperless-ngx/paperless-ngx/pull/14344))
- Chore(deps): Bump the actions group across 1 directory with 10 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14301](https://github.com/paperless-ngx/paperless-ngx/pull/14301))
### Dependencies
<details>
<summary>27 changes</summary>
- Chore(deps): Bump django-filter from 25.2 to 26.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#14337](https://github.com/paperless-ngx/paperless-ngx/pull/14337))
- Chore(deps): Bump the utilities-patch group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14338](https://github.com/paperless-ngx/paperless-ngx/pull/14338))
- Chore(deps): Bump the pre-commit-dependencies group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14351](https://github.com/paperless-ngx/paperless-ngx/pull/14351))
- Chore(deps-dev): Bump types-channels from 4.3.0.20260408 to 4.3.0.20260518 @[dependabot[bot]](https://github.com/apps/dependabot) ([#14335](https://github.com/paperless-ngx/paperless-ngx/pull/14335))
- docker(deps): Bump astral-sh/uv from 0.12.20-python3.14-trixie-slim to 0.12.23-python3.14-trixie-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#14327](https://github.com/paperless-ngx/paperless-ngx/pull/14327))
- Chore(deps): Bump the actions group across 1 directory with 4 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14332](https://github.com/paperless-ngx/paperless-ngx/pull/14332))
- Chore(deps): Bump the uv group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14325](https://github.com/paperless-ngx/paperless-ngx/pull/14325))
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 13 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14329](https://github.com/paperless-ngx/paperless-ngx/pull/14329))
- Chore(deps-dev): Bump prettier from 3.9.8 to 3.9.9 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14331](https://github.com/paperless-ngx/paperless-ngx/pull/14331))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14330](https://github.com/paperless-ngx/paperless-ngx/pull/14330))
- Chore(deps-dev): Bump zensical from 0.0.64 to 0.0.65 in the development group @[dependabot[bot]](https://github.com/apps/dependabot) ([#14326](https://github.com/paperless-ngx/paperless-ngx/pull/14326))
- Chore(deps): Bump the uv group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14314](https://github.com/paperless-ngx/paperless-ngx/pull/14314))
- Chore(deps): Bump the utilities-minor group across 1 directory with 7 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14305](https://github.com/paperless-ngx/paperless-ngx/pull/14305))
- docker-compose(deps): bump greenmail/standalone from 2.1.13 to 2.1.14 in /docker/compose @[dependabot[bot]](https://github.com/apps/dependabot) ([#14281](https://github.com/paperless-ngx/paperless-ngx/pull/14281))
- docker(deps): Bump astral-sh/uv from 0.12.16-python3.14-trixie-slim to 0.12.20-python3.14-trixie-slim @[dependabot[bot]](https://github.com/apps/dependabot) ([#14282](https://github.com/paperless-ngx/paperless-ngx/pull/14282))
- Chore(deps): Bump the pre-commit-dependencies group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14283](https://github.com/paperless-ngx/paperless-ngx/pull/14283))
- Chore(deps): Bump the utilities-patch group across 1 directory with 6 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14297](https://github.com/paperless-ngx/paperless-ngx/pull/14297))
- Chore(deps): Bump the actions group across 1 directory with 10 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14301](https://github.com/paperless-ngx/paperless-ngx/pull/14301))
- Chore(deps-dev): Bump the frontend-jest-dependencies group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14286](https://github.com/paperless-ngx/paperless-ngx/pull/14286))
- Chore(deps-dev): Bump eslint from 10.10.0 to 10.11.0 in /src-ui in the frontend-eslint-dependencies group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#14287](https://github.com/paperless-ngx/paperless-ngx/pull/14287))
- Chore(deps-dev): Bump @types/node from 26.5.0 to 26.6.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14288](https://github.com/paperless-ngx/paperless-ngx/pull/14288))
- Chore(deps-dev): Bump prettier from 3.9.6 to 3.9.8 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14289](https://github.com/paperless-ngx/paperless-ngx/pull/14289))
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 10 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14285](https://github.com/paperless-ngx/paperless-ngx/pull/14285))
- Chore: replace bleach with turbohtml [@gaborbernat](https://github.com/gaborbernat) ([#14269](https://github.com/paperless-ngx/paperless-ngx/pull/14269))
- Chore(deps): Bump autobahn from 25.12.2 to 26.7.1 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#14231](https://github.com/paperless-ngx/paperless-ngx/pull/14231))
- Chore: update pikepdf for ocrmypdf requirement [@shamoon](https://github.com/shamoon) ([#14235](https://github.com/paperless-ngx/paperless-ngx/pull/14235))
- Chore(deps): Bump the pre-commit-dependencies group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14133](https://github.com/paperless-ngx/paperless-ngx/pull/14133))
</details>
### All App Changes
<details>
<summary>41 changes</summary>
- Feature: store barcode contents, list and search them [@jurassicparkicecream](https://github.com/jurassicparkicecream) ([#14276](https://github.com/paperless-ngx/paperless-ngx/pull/14276))
- Fix: Set the ProcessedMail owner based on the rule owner in all cases [@stumpylog](https://github.com/stumpylog) ([#14356](https://github.com/paperless-ngx/paperless-ngx/pull/14356))
- Chore(deps): Bump django-filter from 25.2 to 26.1 @[dependabot[bot]](https://github.com/apps/dependabot) ([#14337](https://github.com/paperless-ngx/paperless-ngx/pull/14337))
- Chore(deps): Bump the utilities-patch group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14338](https://github.com/paperless-ngx/paperless-ngx/pull/14338))
- Chore(deps-dev): Bump types-channels from 4.3.0.20260408 to 4.3.0.20260518 @[dependabot[bot]](https://github.com/apps/dependabot) ([#14335](https://github.com/paperless-ngx/paperless-ngx/pull/14335))
- Chore(deps): Bump the uv group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14325](https://github.com/paperless-ngx/paperless-ngx/pull/14325))
- Fix: Ensure log rotation settings are converted to integers [@stumpylog](https://github.com/stumpylog) ([#14343](https://github.com/paperless-ngx/paperless-ngx/pull/14343))
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 13 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14329](https://github.com/paperless-ngx/paperless-ngx/pull/14329))
- Chore(deps-dev): Bump prettier from 3.9.8 to 3.9.9 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14331](https://github.com/paperless-ngx/paperless-ngx/pull/14331))
- Chore(deps-dev): Bump the frontend-eslint-dependencies group across 1 directory with 3 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14330](https://github.com/paperless-ngx/paperless-ngx/pull/14330))
- Fix: ship pdf.js CMaps so CJK documents render in the viewer [@MrOggy85](https://github.com/MrOggy85) ([#14318](https://github.com/paperless-ngx/paperless-ngx/pull/14318))
- Chore(deps-dev): Bump zensical from 0.0.64 to 0.0.65 in the development group @[dependabot[bot]](https://github.com/apps/dependabot) ([#14326](https://github.com/paperless-ngx/paperless-ngx/pull/14326))
- Enhancement: more control over suggestion requests [@shamoon](https://github.com/shamoon) ([#14258](https://github.com/paperless-ngx/paperless-ngx/pull/14258))
- Chore(deps): Bump the uv group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14314](https://github.com/paperless-ngx/paperless-ngx/pull/14314))
- Chore: anchor admin url pattern [@shamoon](https://github.com/shamoon) ([#14316](https://github.com/paperless-ngx/paperless-ngx/pull/14316))
- Chorehancement: set manifest CORS for credentials [@shamoon](https://github.com/shamoon) ([#14307](https://github.com/paperless-ngx/paperless-ngx/pull/14307))
- Chore(deps): Bump the utilities-minor group across 1 directory with 7 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14305](https://github.com/paperless-ngx/paperless-ngx/pull/14305))
- Chore(deps): Bump the utilities-patch group across 1 directory with 6 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14297](https://github.com/paperless-ngx/paperless-ngx/pull/14297))
- Chore(deps-dev): Bump the frontend-jest-dependencies group across 1 directory with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14286](https://github.com/paperless-ngx/paperless-ngx/pull/14286))
- Chore(deps-dev): Bump eslint from 10.10.0 to 10.11.0 in /src-ui in the frontend-eslint-dependencies group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#14287](https://github.com/paperless-ngx/paperless-ngx/pull/14287))
- Chore(deps-dev): Bump @types/node from 26.5.0 to 26.6.2 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14288](https://github.com/paperless-ngx/paperless-ngx/pull/14288))
- Chore(deps-dev): Bump prettier from 3.9.6 to 3.9.8 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#14289](https://github.com/paperless-ngx/paperless-ngx/pull/14289))
- Chore(deps): Bump the frontend-angular-dependencies group across 1 directory with 10 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#14285](https://github.com/paperless-ngx/paperless-ngx/pull/14285))
- Fix: use version page\_count for versioned document [@shamoon](https://github.com/shamoon) ([#14280](https://github.com/paperless-ngx/paperless-ngx/pull/14280))
- Chore: replace bleach with turbohtml [@gaborbernat](https://github.com/gaborbernat) ([#14269](https://github.com/paperless-ngx/paperless-ngx/pull/14269))
- Enhancement: include Django admin with 2FA [@shamoon](https://github.com/shamoon) ([#14270](https://github.com/paperless-ngx/paperless-ngx/pull/14270))
- Fix: allow pointer events for pdf links in pngx viewer [@shamoon](https://github.com/shamoon) ([#14264](https://github.com/paperless-ngx/paperless-ngx/pull/14264))
- Feature: propagate resolved secrets to interactive container shells [@stumpylog](https://github.com/stumpylog) ([#14254](https://github.com/paperless-ngx/paperless-ngx/pull/14254))
- Fix: During a move to the trash directory, only attempt to copy metadata [@stumpylog](https://github.com/stumpylog) ([#14250](https://github.com/paperless-ngx/paperless-ngx/pull/14250))
- Fix: convert file mtime to the configured time zone directly [@stumpylog](https://github.com/stumpylog) ([#14249](https://github.com/paperless-ngx/paperless-ngx/pull/14249))
- Fix: ensure documentDeleted subscription is discarded [@shamoon](https://github.com/shamoon) ([#14247](https://github.com/paperless-ngx/paperless-ngx/pull/14247))
- Chore: Fix bugs in the test suite [@stumpylog](https://github.com/stumpylog) ([#14244](https://github.com/paperless-ngx/paperless-ngx/pull/14244))
- Fix: ensure bulk operations are checked against version root [@shamoon](https://github.com/shamoon) ([#14246](https://github.com/paperless-ngx/paperless-ngx/pull/14246))
- Enhancement: support separate embedding API key [@furkanural](https://github.com/furkanural) ([#14067](https://github.com/paperless-ngx/paperless-ngx/pull/14067))
- Enhancement: support passthrough extra params for LLMs [@shamoon](https://github.com/shamoon) ([#14202](https://github.com/paperless-ngx/paperless-ngx/pull/14202))
- Chore(deps): Bump autobahn from 25.12.2 to 26.7.1 in the uv group across 1 directory @[dependabot[bot]](https://github.com/apps/dependabot) ([#14231](https://github.com/paperless-ngx/paperless-ngx/pull/14231))
- Fix: indexing after document-added workflows signal [@shamoon](https://github.com/shamoon) ([#14242](https://github.com/paperless-ngx/paperless-ngx/pull/14242))
- Fix: Record full tag and custom field lists in bulk edit audit log [@stumpylog](https://github.com/stumpylog) ([#14236](https://github.com/paperless-ngx/paperless-ngx/pull/14236))
- Chore: update pikepdf for ocrmypdf requirement [@shamoon](https://github.com/shamoon) ([#14235](https://github.com/paperless-ngx/paperless-ngx/pull/14235))
- Fix: handle legacy bulk edit split page range with missing page\_count [@shamoon](https://github.com/shamoon) ([#14212](https://github.com/paperless-ngx/paperless-ngx/pull/14212))
- Fix: ignore invalid EXIF orientation when generating image archives [@zhzy0077](https://github.com/zhzy0077) ([#14203](https://github.com/paperless-ngx/paperless-ngx/pull/14203))
</details>
## paperless-ngx 3.2.1 ## paperless-ngx 3.2.1
### Bug Fixes ### Bug Fixes
+7
View File
@@ -1796,6 +1796,13 @@ assigns or creates tags if a properly formatted barcode is detected.
Defaults to false. Defaults to false.
#### [`PAPERLESS_CONSUMER_STORE_BARCODE_VALUES=<bool>`](#PAPERLESS_CONSUMER_STORE_BARCODE_VALUES) {#PAPERLESS_CONSUMER_STORE_BARCODE_VALUES}
: Stores the content of every barcode found during consumption, see
[Barcode Contents](advanced_usage.md#barcode-contents).
Defaults to false.
## Audit Trail ## Audit Trail
#### [`PAPERLESS_AUDIT_LOG_ENABLED=<bool>`](#PAPERLESS_AUDIT_LOG_ENABLED) {#PAPERLESS_AUDIT_LOG_ENABLED} #### [`PAPERLESS_AUDIT_LOG_ENABLED=<bool>`](#PAPERLESS_AUDIT_LOG_ENABLED) {#PAPERLESS_AUDIT_LOG_ENABLED}
+3 -1
View File
@@ -76,7 +76,9 @@ is not supported by any of the available parsers.
**A:** Not by default. As of v3, a file whose contents match an existing document is still **A:** Not by default. As of v3, a file whose contents match an existing document is still
consumed, and the duplicate is flagged in the UI — open the document and check the consumed, and the duplicate is flagged in the UI — open the document and check the
**Duplicates** tab to review documents that share the same content. If you prefer the old **Duplicates** tab to review documents that share the same content, or filter the document
list by **Duplicates** to find all of them (see
[Duplicate documents](usage.md#duplicate-documents)). If you prefer the old
behavior of rejecting duplicates during consumption, set behavior of rejecting duplicates during consumption, set
[`PAPERLESS_CONSUMER_DELETE_DUPLICATES`](configuration.md#PAPERLESS_CONSUMER_DELETE_DUPLICATES) [`PAPERLESS_CONSUMER_DELETE_DUPLICATES`](configuration.md#PAPERLESS_CONSUMER_DELETE_DUPLICATES)
to `true`. to `true`.
+4 -4
View File
@@ -12,10 +12,10 @@ physical documents into a searchable online archive so you can keep, well, _less
[Get started](setup.md){ .md-button .md-button--primary .index-callout } [Get started](setup.md){ .md-button .md-button--primary .index-callout }
[Demo](https://demo.paperless-ngx.com){ .md-button .md-button--secondary target=\_blank } [Demo](https://demo.paperless-ngx.com){ .md-button .md-button--secondary target=\_blank }
<div style="display: flex; justify-content: end; margin-top: -1.5rem;"> <div class="demo-sponsor">
<a href="https://m.do.co/c/8d70b916d462" target="_blank"> <span>Demo hosting by</span>
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_white.svg#only-dark" class="no-lightbox" width="150px"> <a href="https://fyrastack.com" target="_blank" rel="noopener">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_black.svg#only-light" class="no-lightbox" width="150px"> <img src="assets/sponsors/fyra-stack.svg" alt="" class="no-lightbox" width="20" height="20"> Fyra Stack
</a> </a>
</div> </div>
+60 -1
View File
@@ -129,7 +129,7 @@ change in the `docker-compose.yml` file:
command: command:
- 'gotenberg' - 'gotenberg'
- '--chromium-disable-javascript=true' - '--chromium-disable-javascript=true'
- '--chromium-allow-list=file:///tmp/.*' - '--chromium-allow-list=^file:///tmp/.*'
- '--api-timeout=60s' - '--api-timeout=60s'
``` ```
@@ -272,6 +272,65 @@ This error can occur in installations which have upgraded from a version of Pape
$ python3 manage.py convert_mariadb_uuid $ python3 manage.py convert_mariadb_uuid
``` ```
## MariaDB/MySQL error "Illegal mix of collations"
Consumption or other operations fail with an error like:
```
(1267, "Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and
(utf8mb4_unicode_ci,IMPLICIT) for operation '='")
```
This happens when the tables in your database do not all use the same
collation. It is most often seen on databases that existed before a
MariaDB/MySQL upgrade: older tables keep their original collation, while
tables created afterwards use the new server default.
To work around it, set the collation your existing tables use (the one in the error
that is not `utf8mb4_unicode_ci`) with
[`PAPERLESS_DB_OPTIONS`](configuration.md#PAPERLESS_DB_OPTIONS):
```bash
PAPERLESS_DB_OPTIONS="collation=utf8mb4_general_ci"
```
To fix it permanently, back up your database, then convert the database and
each table to a single collation and remove the override:
```sql
ALTER DATABASE paperless CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE <table_name> CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
## PostgreSQL warns about a "collation version mismatch"
The PostgreSQL log shows a warning like:
```
WARNING: database "paperless" has a collation version mismatch
DETAIL: The database was created using collation version 2.36, but the operating system provides version 2.41.
HINT: Rebuild all objects in this database that use the default collation and run ALTER DATABASE paperless REFRESH COLLATION VERSION, or build PostgreSQL with the right library version.
```
This comes from PostgreSQL, not Paperless-ngx. The `glibc` version that PostgreSQL
runs against changed, and the existing database was created with an older one. With
Docker, `glibc` comes from the PostgreSQL image's Debian base, not the host, so this
commonly happens when a new image is pulled after its base Debian release changed.
If PostgreSQL is installed directly on a host, it uses the host's `glibc` instead.
The warning is not an error and Paperless-ngx keeps working, but the database's text
indexes may be built with outdated sorting rules. To resolve it, back up your
database, then connect to it (for example with `psql -U paperless -d paperless`
inside the database container) and run:
```sql
REINDEX DATABASE paperless;
ALTER DATABASE paperless REFRESH COLLATION VERSION;
```
To avoid this in the future, pin your PostgreSQL image to a specific Debian release,
for example `postgres:18-trixie`, rather than `postgres:18`.
## Platform-Specific Deployment Troubleshooting ## Platform-Specific Deployment Troubleshooting
A user-maintained wiki page is available to help troubleshoot issues that may arise when trying to deploy Paperless-ngx on specific platforms, for example SELinux. Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Platform%E2%80%90Specific-Troubleshooting). A user-maintained wiki page is available to help troubleshoot issues that may arise when trying to deploy Paperless-ngx on specific platforms, for example SELinux. Please see [the wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Platform%E2%80%90Specific-Troubleshooting).
+21 -8
View File
@@ -299,19 +299,18 @@ for details.
### Duplicate documents ### Duplicate documents
By default, Paperless-ngx **does not reject duplicates**. If you consume a file whose By default, Paperless-ngx **does not reject duplicates**. If you consume a file whose
contents exactly match an existing document (same checksum), the new copy is still contents match an existing document (same original or archive checksum), the new copy is
consumed and a warning is logged. The task entry for the upload also flags that a still consumed and a warning is logged.
duplicate was detected and links to the existing document(s).
To review duplicates, open a document and switch to the **Duplicates** tab on the When a document has duplicates, a **Duplicates** tab appears on its detail page, listing
document detail page. It lists other documents that share the same content, including any the other documents you can view that share the same content (including any in the trash).
that are in the trash (shown with a badge), and links to each so you can decide which to To find all documents with duplicates, choose **Duplicates** in the document list's text
keep. filter dropdown, or use `has_duplicates=true` in the REST API.
If you would rather reject duplicates at consumption time (the pre-v3 behavior), set If you would rather reject duplicates at consumption time (the pre-v3 behavior), set
[`PAPERLESS_CONSUMER_DELETE_DUPLICATES`](configuration.md#PAPERLESS_CONSUMER_DELETE_DUPLICATES) [`PAPERLESS_CONSUMER_DELETE_DUPLICATES`](configuration.md#PAPERLESS_CONSUMER_DELETE_DUPLICATES)
to `true`. The duplicate file is then deleted instead of consumed, and the task fails with to `true`. The duplicate file is then deleted instead of consumed, and the task fails with
a "document already exists" message. a "Document already exists" message linking to the existing document.
## Document Suggestions ## Document Suggestions
@@ -1061,6 +1060,20 @@ notes.user:alice notes.note:insurance
The bare `notes:` prefix is shorthand for `notes.note:`. The bare `notes:` prefix is shorthand for `notes.note:`.
#### Searching barcodes
If [barcode contents are stored](advanced_usage.md#barcode-contents), they can be searched by
content or type, but only with a field name:
```
barcodes.value:DE89370400440532013000
barcodes.format:qrcode
barcodes:wifi barcodes:guest
```
`barcodes:` is shorthand for `barcodes.value:`. Separators are stripped, so each part of e.g.
`WIFI:S:Guest;P:secret;;` can be searched on its own.
All of these can be combined. Syntax not described here may not work as expected, and an unknown field name is searched as ordinary text. All of these can be combined. Syntax not described here may not work as expected, and an unknown field name is searched as ordinary text.
!!! note !!! note
+9 -9
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "paperless-ngx" name = "paperless-ngx"
version = "3.2.1" version = "3.3.1"
description = """\ description = """\
A community-supported supercharged document management system: scan, index and archive all your physical documents\ A community-supported supercharged document management system: scan, index and archive all your physical documents\
""" """
@@ -19,7 +19,6 @@ classifiers = [
dependencies = [ dependencies = [
"azure-ai-documentintelligence>=1.0.2", "azure-ai-documentintelligence>=1.0.2",
"babel>=2.17", "babel>=2.17",
"bleach~=6.4.0",
"celery[redis]~=5.6.2", "celery[redis]~=5.6.2",
"channels~=4.2", "channels~=4.2",
"channels-redis~=4.2", "channels-redis~=4.2",
@@ -34,8 +33,8 @@ dependencies = [
"django-compression-middleware~=0.5.0", "django-compression-middleware~=0.5.0",
"django-cors-headers~=4.9.0", "django-cors-headers~=4.9.0",
"django-extensions~=4.1", "django-extensions~=4.1",
"django-filter~=25.1", "django-filter>=25.1,<27",
"django-guardian>=3.3.3,<3.5", "django-guardian>=3.3.3,<3.6",
"django-multiselectfield~=1.0.1", "django-multiselectfield~=1.0.1",
"django-rich~=2.2.0", "django-rich~=2.2.0",
"django-soft-delete~=1.0.18", "django-soft-delete~=1.0.18",
@@ -45,7 +44,7 @@ dependencies = [
"drf-spectacular-sidecar>=2026.7.1,<2026.10", "drf-spectacular-sidecar>=2026.7.1,<2026.10",
"drf-writable-nested~=0.7.1", "drf-writable-nested~=0.7.1",
"filelock~=3.32.0", "filelock~=3.32.0",
"flower>=2.0.1,<2.2", "flower>=2.0.1,<2.3",
"gotenberg-client[httpx]~=1.0", "gotenberg-client[httpx]~=1.0",
"httpx-oauth~=0.17", "httpx-oauth~=0.17",
"ijson>=3.5.1", "ijson>=3.5.1",
@@ -77,6 +76,7 @@ dependencies = [
"tantivy~=0.26.0", "tantivy~=0.26.0",
"tika-client[httpx]~=1.0", "tika-client[httpx]~=1.0",
"torch>=2.13,<2.15", "torch>=2.13,<2.15",
"turbohtml~=1.10.0",
"watchfiles>=1.2", "watchfiles>=1.2",
"whitenoise~=6.11", "whitenoise~=6.11",
"whoosh-compat[tantivy]==0.3", "whoosh-compat[tantivy]==0.3",
@@ -84,13 +84,13 @@ dependencies = [
] ]
[project.optional-dependencies] [project.optional-dependencies]
mariadb = [ mariadb = [
"mysqlclient~=2.2.7", "mysqlclient>=2.2.7,<2.4",
] ]
postgres = [ postgres = [
"psycopg[c,pool]==3.3.4", "psycopg[c,pool]==3.3.4",
# Direct dependency for proper resolution of the pre-built wheels # Direct dependency for proper resolution of the pre-built wheels
"psycopg-c==3.3.4", "psycopg-c==3.3.4",
"psycopg-pool==3.3.1", "psycopg-pool==3.3.3",
] ]
webserver = [ webserver = [
"granian[uvloop]>=2.7,<2.9", "granian[uvloop]>=2.7,<2.9",
@@ -112,7 +112,7 @@ lint = [
testing = [ testing = [
"daphne", "daphne",
"factory-boy~=3.3.1", "factory-boy~=3.3.1",
"faker>=40.36,<40.39", "faker>=40.36,<40.40",
"imagehash", "imagehash",
"pytest~=9.1.1", "pytest~=9.1.1",
"pytest-cov~=7.1.0", "pytest-cov~=7.1.0",
@@ -136,7 +136,6 @@ typing = [
"mypy", "mypy",
"mypy-baseline", "mypy-baseline",
"pyrefly", "pyrefly",
"types-bleach",
"types-channels", "types-channels",
"types-colorama", "types-colorama",
"types-dateparser", "types-dateparser",
@@ -151,6 +150,7 @@ typing = [
[tool.uv] [tool.uv]
required-version = ">=0.9.0" required-version = ">=0.9.0"
prerelease = "disallow"
environments = [ environments = [
"sys_platform == 'darwin'", "sys_platform == 'darwin'",
"sys_platform == 'linux'", "sys_platform == 'linux'",
+1 -1
View File
@@ -2,5 +2,5 @@
docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -v paperless_pgdata:/var/lib/postgresql/data -d postgres:15 docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -v paperless_pgdata:/var/lib/postgresql/data -d postgres:15
docker run -d -p 6379:6379 redis:latest docker run -d -p 6379:6379 redis:latest
docker run -p 3000:3000 -d gotenberg/gotenberg:7.8 gotenberg --chromium-disable-javascript=true --chromium-allow-list="file:///tmp/.*" docker run -p 3000:3000 -d gotenberg/gotenberg:7.8 gotenberg --chromium-disable-javascript=true --chromium-allow-list="^file:///tmp/.*"
docker run -p 9998:9998 -d docker.io/apache/tika:latest docker run -p 9998:9998 -d docker.io/apache/tika:latest
+5
View File
@@ -91,6 +91,11 @@
"glob": "**/*", "glob": "**/*",
"input": "node_modules/pdfjs-dist/iccs/", "input": "node_modules/pdfjs-dist/iccs/",
"output": "/assets/iccs/" "output": "/assets/iccs/"
},
{
"glob": "**/*",
"input": "node_modules/pdfjs-dist/cmaps/",
"output": "/assets/cmaps/"
} }
], ],
"styles": [ "styles": [
+342 -242
View File
File diff suppressed because it is too large. Load diff
+23 -23
View File
@@ -1,6 +1,6 @@
{ {
"name": "paperless-ngx-ui", "name": "paperless-ngx-ui",
"version": "3.2.1", "version": "3.3.1",
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"ng": "ng", "ng": "ng",
@@ -15,16 +15,16 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "^22.1.6", "@angular/cdk": "^22.2.0",
"@angular/common": "~22.1.6", "@angular/common": "~22.2.0",
"@angular/compiler": "~22.1.6", "@angular/compiler": "~22.2.0",
"@angular/core": "~22.1.6", "@angular/core": "~22.2.0",
"@angular/forms": "~22.1.6", "@angular/forms": "~22.2.0",
"@angular/localize": "~22.1.6", "@angular/localize": "~22.2.0",
"@angular/platform-browser": "~22.1.6", "@angular/platform-browser": "~22.2.0",
"@angular/router": "~22.1.6", "@angular/router": "~22.2.0",
"@ng-bootstrap/ng-bootstrap": "^21.0.0", "@ng-bootstrap/ng-bootstrap": "^21.0.0",
"@ng-select/ng-select": "~24.1.1", "@ng-select/ng-select": "~24.1.2",
"@ngneat/dirty-check-forms": "^3.0.3", "@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
@@ -45,29 +45,29 @@
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/jest": "^22.0.1", "@angular-builders/jest": "^22.0.1",
"@angular-devkit/core": "^22.1.8", "@angular-devkit/core": "^22.2.0",
"@angular-devkit/schematics": "^22.1.8", "@angular-devkit/schematics": "^22.2.0",
"@angular-eslint/builder": "22.5.0", "@angular-eslint/builder": "22.5.0",
"@angular-eslint/eslint-plugin": "22.5.0", "@angular-eslint/eslint-plugin": "22.5.0",
"@angular-eslint/eslint-plugin-template": "22.5.0", "@angular-eslint/eslint-plugin-template": "22.5.0",
"@angular-eslint/schematics": "22.5.0", "@angular-eslint/schematics": "22.5.0",
"@angular-eslint/template-parser": "22.5.0", "@angular-eslint/template-parser": "22.5.0",
"@angular/build": "22.1.8", "@angular/build": "22.2.0",
"@angular/cli": "22.1.8", "@angular/cli": "22.2.0",
"@angular/compiler-cli": "~22.1.6", "@angular/compiler-cli": "~22.2.0",
"@playwright/test": "^1.62.1", "@playwright/test": "^1.62.1",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/node": "^26.5.0", "@types/node": "^26.6.2",
"@typescript-eslint/eslint-plugin": "^8.70.0", "@typescript-eslint/eslint-plugin": "^8.70.1",
"@typescript-eslint/parser": "^8.70.0", "@typescript-eslint/parser": "^8.70.1",
"@typescript-eslint/utils": "^8.70.0", "@typescript-eslint/utils": "^8.70.1",
"eslint": "^10.10.0", "eslint": "^10.11.0",
"jest": "30.5.1", "jest": "30.5.2",
"jest-environment-jsdom": "^30.5.1", "jest-environment-jsdom": "^30.5.2",
"jest-junit": "^17.0.0", "jest-junit": "^17.0.0",
"jest-preset-angular": "^17.0.0", "jest-preset-angular": "^17.0.0",
"jest-websocket-mock": "^2.5.0", "jest-websocket-mock": "^2.5.0",
"prettier": "^3.9.6", "prettier": "^3.9.9",
"prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-organize-imports": "^4.3.0",
"ts-node": "~10.9.2", "ts-node": "~10.9.2",
"typescript": "^6.0.3" "typescript": "^6.0.3"
+1312 -1960
View File
File diff suppressed because it is too large. Load diff
@@ -253,6 +253,24 @@
</div> </div>
</div> </div>
@if (aiEnabled) {
<div class="row mb-3">
<div class="col-md-3 col-form-label pt-0">
<span i18n>Suggestions default to</span>
</div>
<div class="col">
<fieldset class="btn-group btn-group-sm">
<input type="radio" class="btn-check" id="suggestionSourceBoth" [value]="SuggestionSource.Both" formControlName="documentEditingSuggestionSource">
<label class="btn btn-outline-primary" for="suggestionSourceBoth"><ng-container i18n>Both</ng-container></label>
<input type="radio" class="btn-check" id="suggestionSourceML" [value]="SuggestionSource.ML" formControlName="documentEditingSuggestionSource">
<label class="btn btn-outline-primary" for="suggestionSourceML"><i-bs class="me-1" name="cpu"></i-bs><ng-container i18n>ML only</ng-container></label>
<input type="radio" class="btn-check" id="suggestionSourceAI" [value]="SuggestionSource.AI" formControlName="documentEditingSuggestionSource">
<label class="btn btn-outline-primary" for="suggestionSourceAI"><i-bs class="me-1" name="stars"></i-bs><ng-container i18n>AI only</ng-container></label>
</fieldset>
</div>
</div>
}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<pngx-input-check i18n-title title="Automatically request suggestions for inbox documents" i18n-hint hint="If un-checked, suggestions must be requested via the Suggest button." formControlName="documentEditingAutoSuggest"></pngx-input-check> <pngx-input-check i18n-title title="Automatically request suggestions for inbox documents" i18n-hint hint="If un-checked, suggestions must be requested via the Suggest button." formControlName="documentEditingAutoSuggest"></pngx-input-check>
@@ -307,7 +307,7 @@ describe('SettingsComponent', () => {
expect(toastErrorSpy).toHaveBeenCalled() expect(toastErrorSpy).toHaveBeenCalled()
expect(storeSpy).toHaveBeenCalled() expect(storeSpy).toHaveBeenCalled()
expect(appearanceSettingsSpy).not.toHaveBeenCalled() expect(appearanceSettingsSpy).not.toHaveBeenCalled()
expect(setSpy).toHaveBeenCalledTimes(34) expect(setSpy).toHaveBeenCalledTimes(35)
expect(setSpy).toHaveBeenCalledWith(SETTINGS_KEYS.SIDEBAR_HIDDEN_ITEMS, [ expect(setSpy).toHaveBeenCalledWith(SETTINGS_KEYS.SIDEBAR_HIDDEN_ITEMS, [
HideableSidebarItemID.Workflows, HideableSidebarItemID.Workflows,
]) ])
@@ -44,6 +44,7 @@ import {
HIDEABLE_SIDEBAR_ITEM_IDS, HIDEABLE_SIDEBAR_ITEM_IDS,
HideableSidebarItemID, HideableSidebarItemID,
SETTINGS_KEYS, SETTINGS_KEYS,
SuggestionSource,
} from 'src/app/data/ui-settings' } from 'src/app/data/ui-settings'
import { User } from 'src/app/data/user' import { User } from 'src/app/data/user'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
@@ -184,6 +185,7 @@ export class SettingsComponent
documentEditingRemoveInboxTags: new FormControl(null), documentEditingRemoveInboxTags: new FormControl(null),
documentEditingOverlayThumbnail: new FormControl(null), documentEditingOverlayThumbnail: new FormControl(null),
documentEditingAutoSuggest: new FormControl(null), documentEditingAutoSuggest: new FormControl(null),
documentEditingSuggestionSource: new FormControl(null),
documentDetailsHiddenFields: new FormControl([]), documentDetailsHiddenFields: new FormControl([]),
searchDbOnly: new FormControl(null), searchDbOnly: new FormControl(null),
searchLink: new FormControl(null), searchLink: new FormControl(null),
@@ -217,6 +219,11 @@ export class SettingsComponent
public readonly PdfZoomScale = PdfZoomScale public readonly PdfZoomScale = PdfZoomScale
public readonly PdfEditorEditMode = PdfEditorEditMode public readonly PdfEditorEditMode = PdfEditorEditMode
public readonly SuggestionSource = SuggestionSource
get aiEnabled(): boolean {
return this.settings.get(SETTINGS_KEYS.AI_ENABLED)
}
public readonly documentDetailFieldOptions = documentDetailFieldOptions public readonly documentDetailFieldOptions = documentDetailFieldOptions
public readonly sidebarItemOptions = HIDEABLE_SIDEBAR_ITEM_IDS.map((id) => ({ public readonly sidebarItemOptions = HIDEABLE_SIDEBAR_ITEM_IDS.map((id) => ({
@@ -404,6 +411,9 @@ export class SettingsComponent
documentEditingAutoSuggest: this.settings.get( documentEditingAutoSuggest: this.settings.get(
SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST
), ),
documentEditingSuggestionSource: this.settings.get(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE
),
documentDetailsHiddenFields: this.settings.get( documentDetailsHiddenFields: this.settings.get(
SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS
), ),
@@ -625,6 +635,10 @@ export class SettingsComponent
SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST, SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST,
this.settingsForm.value.documentEditingAutoSuggest this.settingsForm.value.documentEditingAutoSuggest
) )
this.settings.set(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE,
this.settingsForm.value.documentEditingSuggestionSource
)
this.settings.set( this.settings.set(
SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS, SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS,
this.settingsForm.value.documentDetailsHiddenFields this.settingsForm.value.documentDetailsHiddenFields
@@ -154,11 +154,29 @@
& section { & section {
position: absolute; position: absolute;
text-align: initial; text-align: initial;
pointer-events: auto;
box-sizing: border-box; box-sizing: border-box;
transform-origin: 0 0; transform-origin: 0 0;
} }
& :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a {
position: absolute;
inset: 0;
font-size: 1em;
transition: none;
}
& :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton):not(.hasBorder)
> a:hover {
opacity: 0.2;
background-color: rgb(255 255 0);
}
& .annotationTextContent { & .annotationTextContent {
opacity: 0; opacity: 0;
} }
} }
:host ::ng-deep .textLayer.selecting ~ .annotationLayer section {
pointer-events: none;
}
@@ -1,7 +1,11 @@
import { SimpleChange } from '@angular/core' import { SimpleChange } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs' import * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs'
import { PDFSinglePageViewer, PDFViewer } from 'pdfjs-dist/web/pdf_viewer.mjs' import {
LinkTarget,
PDFSinglePageViewer,
PDFViewer,
} from 'pdfjs-dist/web/pdf_viewer.mjs'
import { PngxPdfViewerComponent } from './pdf-viewer.component' import { PngxPdfViewerComponent } from './pdf-viewer.component'
import { PdfRenderMode, PdfZoomLevel, PdfZoomScale } from './pdf-viewer.types' import { PdfRenderMode, PdfZoomLevel, PdfZoomScale } from './pdf-viewer.types'
@@ -58,6 +62,16 @@ describe('PngxPdfViewerComponent', () => {
expect((component as any).pdfViewer).toBeInstanceOf(PDFViewer) expect((component as any).pdfViewer).toBeInstanceOf(PDFViewer)
}) })
it('opens external links in a new tab', () => {
const linkService = (component as any).linkService
expect(linkService.options).toEqual(
expect.objectContaining({
externalLinkTarget: LinkTarget.BLANK,
externalLinkRel: 'noopener noreferrer nofollow',
})
)
})
it('resolves the worker source relative to the document base URI', async () => { it('resolves the worker source relative to the document base URI', async () => {
setBaseHref('/paperless/') setBaseHref('/paperless/')
const getDocumentSpy = jest.spyOn(pdfjs, 'getDocument') const getDocumentSpy = jest.spyOn(pdfjs, 'getDocument')
@@ -76,6 +90,7 @@ describe('PngxPdfViewerComponent', () => {
withCredentials: true, withCredentials: true,
wasmUrl: expect.stringContaining('/paperless/assets/wasm/'), wasmUrl: expect.stringContaining('/paperless/assets/wasm/'),
iccUrl: expect.stringContaining('/paperless/assets/iccs/'), iccUrl: expect.stringContaining('/paperless/assets/iccs/'),
cMapUrl: expect.stringContaining('/paperless/assets/cmaps/'),
}) })
}) })
@@ -21,6 +21,7 @@ import {
} from 'pdfjs-dist/legacy/build/pdf.mjs' } from 'pdfjs-dist/legacy/build/pdf.mjs'
import { import {
EventBus, EventBus,
LinkTarget,
PDFFindController, PDFFindController,
PDFLinkService, PDFLinkService,
PDFSinglePageViewer, PDFSinglePageViewer,
@@ -75,7 +76,11 @@ export class PngxPdfViewerComponent
private lastViewerPage?: number private lastViewerPage?: number
private readonly eventBus = new EventBus() private readonly eventBus = new EventBus()
private readonly linkService = new PDFLinkService({ eventBus: this.eventBus }) private readonly linkService = new PDFLinkService({
eventBus: this.eventBus,
externalLinkTarget: LinkTarget.BLANK,
externalLinkRel: 'noopener noreferrer nofollow',
})
private readonly findController = new PDFFindController({ private readonly findController = new PDFFindController({
eventBus: this.eventBus, eventBus: this.eventBus,
linkService: this.linkService, linkService: this.linkService,
@@ -185,6 +190,7 @@ export class PngxPdfViewerComponent
withCredentials: true, withCredentials: true,
wasmUrl: new URL('assets/wasm/', this.document.baseURI).toString(), wasmUrl: new URL('assets/wasm/', this.document.baseURI).toString(),
iccUrl: new URL('assets/iccs/', this.document.baseURI).toString(), iccUrl: new URL('assets/iccs/', this.document.baseURI).toString(),
cMapUrl: new URL('assets/cmaps/', this.document.baseURI).toString(),
} }
this.loadingTask = getDocument(initOptions) this.loadingTask = getDocument(initOptions)
try { try {
@@ -1,3 +1,4 @@
<div class="d-flex align-items-center">
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-primary" (click)="clickSuggest()" [disabled]="disabled() || loading() || (suggestions() && !aiEnabled())" [aria-label]="noSuggestions ? 'No suggestions' : 'Suggest'" i18n-aria-label> <button type="button" class="btn btn-sm btn-outline-primary" (click)="clickSuggest()" [disabled]="disabled() || loading() || (suggestions() && !aiEnabled())" [aria-label]="noSuggestions ? 'No suggestions' : 'Suggest'" i18n-aria-label>
@if (loading()) { @if (loading()) {
@@ -5,7 +6,7 @@
} @else if (noSuggestions) { } @else if (noSuggestions) {
<i-bs width="1.2em" height="1.2em" name="check-circle"></i-bs> <i-bs width="1.2em" height="1.2em" name="check-circle"></i-bs>
} @else { } @else {
<i-bs width="1.2em" height="1.2em" name="stars"></i-bs> <i-bs width="1.2em" height="1.2em" name="lightbulb"></i-bs>
} }
@if (noSuggestions) { @if (noSuggestions) {
<span class="d-none d-lg-inline ps-1" i18n>No suggestions</span> <span class="d-none d-lg-inline ps-1" i18n>No suggestions</span>
@@ -25,7 +26,7 @@
<div ngbDropdownMenu aria-labelledby="suggestionsDropdown" class="shadow suggestions-dropdown"> <div ngbDropdownMenu aria-labelledby="suggestionsDropdown" class="shadow suggestions-dropdown">
<div class="list-group list-group-flush small pb-0"> <div class="list-group list-group-flush small pb-0">
@if (novelSuggestions === 0 && reusableSuggestions === 0) { @if (novelSuggestions === 0 && fieldSuggestions === 0) {
<div class="list-group-item text-muted fst-italic"> <div class="list-group-item text-muted fst-italic">
<small class="text-muted small fst-italic" i18n>No novel suggestions</small> <small class="text-muted small fst-italic" i18n>No novel suggestions</small>
</div> </div>
@@ -48,9 +49,9 @@
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addCorrespondent.emit(correspondent)">{{ correspondent }}</button> <button type="button" class="list-group-item list-group-item-action bg-light" (click)="addCorrespondent.emit(correspondent)">{{ correspondent }}</button>
} }
} }
@if (reusableSuggestions > 0) { @if (fieldSuggestions > 0) {
<div class="list-group-item text-muted fst-italic"> <div class="list-group-item text-muted fst-italic">
<small class="text-muted small fst-italic" i18n>{reusableSuggestions, plural, =1 {1 existing value suggested below} other {{{reusableSuggestions}} existing values suggested below}}</small> <small class="text-muted small fst-italic" i18n>{fieldSuggestions, plural, =1 {1 suggestion available below} other {{{fieldSuggestions}} suggestions available below}}</small>
</div> </div>
} }
</div> </div>
@@ -58,3 +59,28 @@
</div> </div>
} }
</div> </div>
@if (aiEnabled()) {
<div ngbDropdown autoClose="outside" placement="bottom-end" [popperOptions]="popperOptions">
<button type="button" class="btn btn-sm btn-link position-relative" ngbDropdownToggle [disabled]="disabled() || loading()" i18n-title title="Suggestion options">
<i-bs name="three-dots"></i-bs>
@if (source() !== defaultSource()) {
<span class="position-absolute top-0 start-100 translate-middle p-1 bg-primary border border-light rounded-circle">
<span class="visually-hidden" i18n>Not using default</span>
</span>
}
</button>
<div ngbDropdownMenu class="shadow p-3">
<div class="small text-muted mb-2" i18n>Suggest using:</div>
<div class="form-check small">
<input class="form-check-input" type="checkbox" id="suggestionSourceML" [checked]="useML" [disabled]="useML && !useAI" (change)="setSources($event.target.checked, useAI)">
<label class="form-check-label d-inline-flex align-items-center gap-1" for="suggestionSourceML"><i-bs name="cpu"></i-bs><ng-container i18n>ML</ng-container></label>
</div>
<div class="form-check small">
<input class="form-check-input" type="checkbox" id="suggestionSourceAI" [checked]="useAI" [disabled]="useAI && !useML" (change)="setSources(useML, $event.target.checked)">
<label class="form-check-label d-inline-flex align-items-center gap-1" for="suggestionSourceAI"><i-bs name="stars"></i-bs><ng-container i18n>AI</ng-container></label>
</div>
</div>
</div>
}
</div>
@@ -1,3 +1,7 @@
.suggestions-dropdown { .suggestions-dropdown {
min-width: 250px; min-width: 250px;
} }
.btn-link.dropdown-toggle::after {
display: none;
}
@@ -1,6 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing' import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { SuggestionSource } from 'src/app/data/ui-settings'
import { SuggestionsDropdownComponent } from './suggestions-dropdown.component' import { SuggestionsDropdownComponent } from './suggestions-dropdown.component'
describe('SuggestionsDropdownComponent', () => { describe('SuggestionsDropdownComponent', () => {
@@ -83,7 +84,7 @@ describe('SuggestionsDropdownComponent', () => {
fixture.detectChanges() fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain( expect(fixture.nativeElement.textContent).toContain(
'2 existing values suggested below' '2 suggestions available below'
) )
expect(fixture.nativeElement.textContent).not.toContain( expect(fixture.nativeElement.textContent).not.toContain(
'No novel suggestions' 'No novel suggestions'
@@ -107,7 +108,7 @@ describe('SuggestionsDropdownComponent', () => {
expect(component.totalSuggestions).toBe(4) expect(component.totalSuggestions).toBe(4)
expect(fixture.nativeElement.textContent).toContain('Arbitration') expect(fixture.nativeElement.textContent).toContain('Arbitration')
expect(fixture.nativeElement.textContent).toContain( expect(fixture.nativeElement.textContent).toContain(
'2 existing values suggested below' '2 suggestions available below'
) )
}) })
@@ -124,10 +125,46 @@ describe('SuggestionsDropdownComponent', () => {
}) })
expect(component.novelSuggestions).toBe(0) expect(component.novelSuggestions).toBe(0)
expect(component.totalSuggestions).toBe(5) expect(component.totalSuggestions).toBe(6)
fixture.componentRef.setInput('appliedStoragePath', 7) fixture.componentRef.setInput('appliedStoragePath', 7)
expect(component.totalSuggestions).toBe(4) expect(component.totalSuggestions).toBe(5)
})
it('should count title and dates as field suggestions without calling them existing values', () => {
fixture.componentRef.setInput('aiEnabled', true)
fixture.componentRef.setInput('fetchedSources', [SuggestionSource.ML])
fixture.componentRef.setInput('suggestions', {
title: 'Suggested title',
dates: ['2026-01-04', '2026-02-01', '2026-03-01'],
correspondents: [1, 2, 3, 4],
document_types: [1, 2, 3, 4],
tags: [1, 2, 3, 4, 5, 6, 7, 8],
})
fixture.detectChanges()
component.clickSuggest()
fixture.detectChanges()
expect(component.reusableSuggestions).toBe(16)
expect(component.fieldSuggestions).toBe(20)
expect(component.totalSuggestions).toBe(20)
expect(fixture.nativeElement.textContent).toContain(
'20 suggestions available below'
)
expect(fixture.nativeElement.textContent).not.toContain('existing value')
})
it('should not count title or date suggestions matching the current values', () => {
fixture.componentRef.setInput('suggestions', {
title: 'Current title',
dates: ['2026-01-04', '2026-02-01'],
})
expect(component.fieldSuggestions).toBe(3)
fixture.componentRef.setInput('appliedTitle', 'Current title')
fixture.componentRef.setInput('appliedCreated', '2026-01-04')
expect(component.fieldSuggestions).toBe(1)
expect(component.totalSuggestions).toBe(1)
}) })
it('should show when a completed request returned no suggestions', () => { it('should show when a completed request returned no suggestions', () => {
@@ -144,6 +181,28 @@ describe('SuggestionsDropdownComponent', () => {
expect(fixture.nativeElement.textContent).toContain('No suggestions') expect(fixture.nativeElement.textContent).toContain('No suggestions')
}) })
it('should wait for all pending responses before showing the empty state', () => {
fixture.componentRef.setInput('aiEnabled', true)
fixture.componentRef.setInput('source', SuggestionSource.Both)
fixture.componentRef.setInput('fetchedSources', [SuggestionSource.ML])
fixture.componentRef.setInput('suggestions', { tags: [] })
fixture.componentRef.setInput('loading', true)
fixture.detectChanges()
expect(component.noSuggestions).toBeFalsy()
expect(fixture.nativeElement.textContent).not.toContain('No suggestions')
expect(
fixture.nativeElement.querySelector('[role="status"]')
).not.toBeNull()
fixture.componentRef.setInput('loading', false)
fixture.detectChanges()
expect(component.noSuggestions).toBeTruthy()
expect(fixture.nativeElement.textContent).toContain('No suggestions')
expect(fixture.nativeElement.querySelector('[role="status"]')).toBeNull()
})
it('should not show the empty state before a request or with suggestions', () => { it('should not show the empty state before a request or with suggestions', () => {
expect(component.noSuggestions).toBeFalsy() expect(component.noSuggestions).toBeFalsy()
@@ -179,14 +238,71 @@ describe('SuggestionsDropdownComponent', () => {
it('should toggle dropdown when clickSuggest is called and suggestions are not null', () => { it('should toggle dropdown when clickSuggest is called and suggestions are not null', () => {
fixture.componentRef.setInput('aiEnabled', true) fixture.componentRef.setInput('aiEnabled', true)
fixture.componentRef.setInput('fetchedSources', [SuggestionSource.ML])
fixture.detectChanges() fixture.detectChanges()
fixture.componentRef.setInput('suggestions', { fixture.componentRef.setInput('suggestions', {
suggested_correspondents: [], suggested_correspondents: [],
suggested_tags: [], suggested_tags: [],
suggested_document_types: [], suggested_document_types: [],
}) })
fixture.detectChanges()
component.clickSuggest() component.clickSuggest()
expect(component.dropdown.open).toBeTruthy() expect(component.dropdown.isOpen()).toBeTruthy()
expect(fixture.nativeElement.textContent).toContain('No novel suggestions') expect(fixture.nativeElement.textContent).toContain('No novel suggestions')
}) })
it('should fetch unfetched sources and show existing suggestions', () => {
jest.spyOn(component.getSuggestions, 'emit')
fixture.componentRef.setInput('aiEnabled', true)
fixture.componentRef.setInput('source', SuggestionSource.Both)
fixture.componentRef.setInput('fetchedSources', [SuggestionSource.ML])
fixture.componentRef.setInput('suggestions', { tags: [1] })
fixture.detectChanges()
component.clickSuggest()
expect(component.getSuggestions.emit).toHaveBeenCalledWith(
SuggestionSource.Both
)
expect(component.dropdown.isOpen()).toBeTruthy()
})
it('should only show source options when AI is enabled', () => {
expect(
fixture.nativeElement.querySelector('#suggestionSourceML')
).toBeNull()
fixture.componentRef.setInput('aiEnabled', true)
fixture.detectChanges()
fixture.nativeElement
.querySelector('button[title="Suggestion options"]')
.click()
fixture.detectChanges()
expect(
fixture.nativeElement.querySelector('#suggestionSourceML')
).not.toBeNull()
})
it('should emit source changes and never allow no source', () => {
const emitSpy = jest.spyOn(component.sourceChange, 'emit')
component.setSources(true, true)
expect(emitSpy).toHaveBeenCalledWith(SuggestionSource.Both)
component.setSources(true, false)
expect(emitSpy).toHaveBeenCalledWith(SuggestionSource.ML)
component.setSources(false, true)
expect(emitSpy).toHaveBeenCalledWith(SuggestionSource.AI)
emitSpy.mockClear()
component.setSources(false, false)
expect(emitSpy).not.toHaveBeenCalled()
})
it('should indicate a non-default source', () => {
fixture.componentRef.setInput('aiEnabled', true)
fixture.componentRef.setInput('source', SuggestionSource.AI)
fixture.componentRef.setInput('defaultSource', SuggestionSource.AI)
fixture.detectChanges()
expect(fixture.nativeElement.textContent).not.toContain('Not using default')
fixture.componentRef.setInput('source', SuggestionSource.Both)
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('Not using default')
})
}) })
@@ -8,6 +8,7 @@ import {
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { DocumentSuggestions } from 'src/app/data/document-suggestions' import { DocumentSuggestions } from 'src/app/data/document-suggestions'
import { SuggestionSource } from 'src/app/data/ui-settings'
import { pngxPopperOptions } from 'src/app/utils/popper-options' import { pngxPopperOptions } from 'src/app/utils/popper-options'
@Component({ @Component({
@@ -18,21 +19,29 @@ import { pngxPopperOptions } from 'src/app/utils/popper-options'
}) })
export class SuggestionsDropdownComponent { export class SuggestionsDropdownComponent {
public popperOptions = pngxPopperOptions public popperOptions = pngxPopperOptions
public readonly SuggestionSource = SuggestionSource
@ViewChild('dropdown') dropdown: NgbDropdown @ViewChild('dropdown') dropdown: NgbDropdown
readonly suggestions = input<DocumentSuggestions>(null) readonly suggestions = input<DocumentSuggestions>(null)
readonly aiEnabled = input(false) readonly aiEnabled = input(false)
readonly loading = input(false) readonly loading = input(false)
readonly disabled = input(false) readonly disabled = input(false)
readonly source = input<SuggestionSource>(SuggestionSource.ML)
readonly defaultSource = input<SuggestionSource>(SuggestionSource.ML)
readonly fetchedSources = input<SuggestionSource[]>([])
readonly appliedTags = input<number[]>([]) readonly appliedTags = input<number[]>([])
readonly appliedCorrespondent = input<number>(null) readonly appliedCorrespondent = input<number>(null)
readonly appliedDocumentType = input<number>(null) readonly appliedDocumentType = input<number>(null)
readonly appliedStoragePath = input<number>(null) readonly appliedStoragePath = input<number>(null)
readonly appliedTitle = input<string>(null)
readonly appliedCreated = input<string>(null)
@Output() @Output()
getSuggestions: EventEmitter<SuggestionsDropdownComponent> = getSuggestions: EventEmitter<SuggestionSource> = new EventEmitter()
new EventEmitter()
@Output()
sourceChange: EventEmitter<SuggestionSource> = new EventEmitter()
@Output() @Output()
addTag: EventEmitter<string> = new EventEmitter() addTag: EventEmitter<string> = new EventEmitter()
@@ -53,12 +62,42 @@ export class SuggestionsDropdownComponent {
} }
if (!this.suggestions()) { if (!this.suggestions()) {
this.getSuggestions.emit(this) this.getSuggestions.emit(this.source())
} else if (this.hasUnfetchedSources) {
// sources changed, fetch the rest and show what we have meanwhile
this.getSuggestions.emit(this.source())
this.dropdown?.open()
} else { } else {
this.dropdown?.toggle() this.dropdown?.toggle()
} }
} }
get useML(): boolean {
return this.source() !== SuggestionSource.AI
}
get useAI(): boolean {
return this.source() !== SuggestionSource.ML
}
get hasUnfetchedSources(): boolean {
const fetched = this.fetchedSources()
return (
(this.useML && !fetched.includes(SuggestionSource.ML)) ||
(this.useAI && !fetched.includes(SuggestionSource.AI))
)
}
public setSources(ml: boolean, ai: boolean) {
if (ml && ai) {
this.sourceChange.emit(SuggestionSource.Both)
} else if (ml) {
this.sourceChange.emit(SuggestionSource.ML)
} else if (ai) {
this.sourceChange.emit(SuggestionSource.AI)
}
}
get novelSuggestions(): number { get novelSuggestions(): number {
return ( return (
(this.suggestions()?.suggested_correspondents?.length ?? 0) + (this.suggestions()?.suggested_correspondents?.length ?? 0) +
@@ -91,7 +130,28 @@ export class SuggestionsDropdownComponent {
} }
get totalSuggestions(): number { get totalSuggestions(): number {
return this.novelSuggestions + this.reusableSuggestions return this.novelSuggestions + this.fieldSuggestions
}
get fieldSuggestions(): number {
return (
this.reusableSuggestions +
this.unappliedTitleSuggestions +
this.unappliedDateSuggestions
)
}
// hide a title or date suggestion equal to the current value
private get unappliedTitleSuggestions(): number {
const title = this.suggestions()?.title
return title && title !== this.appliedTitle() ? 1 : 0
}
private get unappliedDateSuggestions(): number {
const created = this.appliedCreated()
return (this.suggestions()?.dates ?? []).filter(
(date) => !created || date !== created
).length
} }
private countUnapplied(suggested: number[], applied: number[]): number { private countUnapplied(suggested: number[], applied: number[]): number {
@@ -102,6 +162,7 @@ export class SuggestionsDropdownComponent {
get noSuggestions(): boolean { get noSuggestions(): boolean {
const suggestions = this.suggestions() const suggestions = this.suggestions()
return ( return (
!this.loading() &&
suggestions != null && suggestions != null &&
!suggestions.title && !suggestions.title &&
!suggestions.tags?.length && !suggestions.tags?.length &&
@@ -0,0 +1,46 @@
<table class="table table-borderless align-baseline">
<thead>
<tr>
<th i18n>Page</th>
<th i18n>Type</th>
<th i18n>Content</th>
<th></th>
</tr>
</thead>
<tbody>
@for (barcode of barcodes(); track $index) {
<tr>
<td>{{ barcode.page }}</td>
<td class="text-nowrap">{{ barcode.format }}</td>
<td class="text-break">
@if (isLink(barcode.value)) {
<a
[href]="barcode.value"
target="_blank"
rel="noopener noreferrer nofollow"
>{{ barcode.value }}</a
>
} @else {
{{ barcode.value }}
}
</td>
<td class="text-end">
<button
type="button"
class="btn btn-sm btn-outline-primary"
(click)="copy($index)"
title="Copy content"
i18n-title
>
@if (copiedIndex() === $index) {
<i-bs name="clipboard-check"></i-bs>
} @else {
<i-bs name="clipboard"></i-bs>
}
<span class="visually-hidden" i18n>Copy content</span>
</button>
</td>
</tr>
}
</tbody>
</table>
@@ -0,0 +1,73 @@
import { Clipboard } from '@angular/cdk/clipboard'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { DocumentBarcodesComponent } from './document-barcodes.component'
const barcodes = [
{ page: 1, value: 'ASN00123', format: 'Code128' },
{ page: 2, value: 'https://example.com/invoice/4711', format: 'QRCode' },
{ page: 2, value: 'javascript:alert(1)', format: 'QRCode' },
]
describe('DocumentBarcodesComponent', () => {
let component: DocumentBarcodesComponent
let fixture: ComponentFixture<DocumentBarcodesComponent>
let clipboard: Clipboard
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [
DocumentBarcodesComponent,
NgxBootstrapIconsModule.pick(allIcons),
],
}).compileComponents()
fixture = TestBed.createComponent(DocumentBarcodesComponent)
component = fixture.componentInstance
clipboard = TestBed.inject(Clipboard)
fixture.componentRef.setInput('barcodes', barcodes)
fixture.detectChanges()
})
it('should display all barcodes', () => {
const rows = fixture.nativeElement.querySelectorAll('tbody tr')
expect(rows).toHaveLength(3)
expect(rows[0].textContent).toContain('ASN00123')
expect(rows[0].textContent).toContain('Code128')
})
it('should only link http(s) values', () => {
const links = fixture.nativeElement.querySelectorAll('tbody a')
expect(links).toHaveLength(1)
expect(links[0].getAttribute('href')).toEqual(
'https://example.com/invoice/4711'
)
expect(links[0].getAttribute('target')).toEqual('_blank')
})
it('should copy a value and show feedback', () => {
jest.useFakeTimers()
const copySpy = jest.spyOn(clipboard, 'copy').mockReturnValue(true)
const buttons = fixture.nativeElement.querySelectorAll('tbody button')
buttons[0].click()
fixture.detectChanges()
expect(copySpy).toHaveBeenCalledWith('ASN00123')
expect(component.copiedIndex()).toEqual(0)
expect(buttons[0].querySelector('i-bs').getAttribute('name')).toEqual(
'clipboard-check'
)
jest.advanceTimersByTime(3000)
fixture.detectChanges()
expect(component.copiedIndex()).toBeNull()
expect(buttons[0].querySelector('i-bs').getAttribute('name')).toEqual(
'clipboard'
)
jest.useRealTimers()
})
it('should not show feedback if copying failed', () => {
jest.spyOn(clipboard, 'copy').mockReturnValue(false)
component.copy(1)
expect(component.copiedIndex()).toBeNull()
})
})
@@ -0,0 +1,38 @@
import { Clipboard } from '@angular/cdk/clipboard'
import { Component, inject, input, OnDestroy, signal } from '@angular/core'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { DocumentBarcode } from 'src/app/data/document-barcode'
@Component({
selector: 'pngx-document-barcodes',
templateUrl: './document-barcodes.component.html',
imports: [NgxBootstrapIconsModule],
})
export class DocumentBarcodesComponent implements OnDestroy {
private readonly clipboard = inject(Clipboard)
readonly barcodes = input<DocumentBarcode[]>([])
readonly copiedIndex = signal<number>(null)
private copyTimeout: ReturnType<typeof setTimeout>
public isLink(value: string): boolean {
try {
const url = new URL(value.trim())
return ['http:', 'https:'].includes(url.protocol) && !!url.host
} catch {
return false
}
}
public copy(index: number) {
if (!this.clipboard.copy(this.barcodes()[index].value)) return
this.copiedIndex.set(index)
clearTimeout(this.copyTimeout)
this.copyTimeout = setTimeout(() => this.copiedIndex.set(null), 3000)
}
ngOnDestroy(): void {
clearTimeout(this.copyTimeout)
}
}
@@ -134,11 +134,17 @@
[loading]="suggestionsLoading()" [loading]="suggestionsLoading()"
[suggestions]="suggestions()" [suggestions]="suggestions()"
[aiEnabled]="aiEnabled" [aiEnabled]="aiEnabled"
[source]="suggestionSource"
[defaultSource]="defaultSuggestionSource"
[fetchedSources]="fetchedSuggestionSources()"
[appliedTags]="documentForm.value.tags" [appliedTags]="documentForm.value.tags"
[appliedCorrespondent]="documentForm.value.correspondent" [appliedCorrespondent]="documentForm.value.correspondent"
[appliedDocumentType]="documentForm.value.document_type" [appliedDocumentType]="documentForm.value.document_type"
[appliedStoragePath]="documentForm.value.storage_path" [appliedStoragePath]="documentForm.value.storage_path"
(getSuggestions)="getSuggestions()" [appliedTitle]="documentForm.value.title"
[appliedCreated]="documentForm.value.created"
(getSuggestions)="getSuggestions($event)"
(sourceChange)="suggestionSourceOverride.set($event)"
(addTag)="createTag($event)" (addTag)="createTag($event)"
(addDocumentType)="createDocumentType($event)" (addDocumentType)="createDocumentType($event)"
(addCorrespondent)="createCorrespondent($event)"> (addCorrespondent)="createCorrespondent($event)">
@@ -350,6 +356,10 @@
</table> </table>
} }
@if (metadata()?.barcodes?.length > 0) {
<h6 i18n>Barcodes</h6>
<pngx-document-barcodes [barcodes]="metadata().barcodes"></pngx-document-barcodes>
}
@if (metadata()?.original_metadata?.length > 0) { @if (metadata()?.original_metadata?.length > 0) {
<pngx-metadata-collapse i18n-title title="Original document metadata" [metadata]="metadata()?.original_metadata"></pngx-metadata-collapse> <pngx-metadata-collapse i18n-title title="Original document metadata" [metadata]="metadata()?.original_metadata"></pngx-metadata-collapse>
} }
@@ -43,7 +43,7 @@ import {
} from 'src/app/data/filter-rule-type' } from 'src/app/data/filter-rule-type'
import { StoragePath } from 'src/app/data/storage-path' import { StoragePath } from 'src/app/data/storage-path'
import { Tag } from 'src/app/data/tag' import { Tag } from 'src/app/data/tag'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS, SuggestionSource } from 'src/app/data/ui-settings'
import { PermissionsGuard } from 'src/app/guards/permissions.guard' import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe' import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
@@ -662,6 +662,45 @@ describe('DocumentDetailComponent', () => {
) )
}) })
it.each([
['tag', 'createTag', 'tags', 'suggested_tags'],
[
'document type',
'createDocumentType',
'document_type',
'suggested_document_types',
],
[
'correspondent',
'createCorrespondent',
'correspondent',
'suggested_correspondents',
],
])(
'should create a %s after ML-only suggestions',
(_, method, field, suggestedField) => {
initNormally()
component.suggestions.set({ tags: [1] })
let openModal: NgbModalRef
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
component[method]('New value')
openModal.componentInstance.succeeded.next({
id: 12,
name: 'New value',
is_inbox_tag: false,
color: '#ff0000',
text_color: '#000000',
})
if (field === 'tags') {
expect(component.tagsInput.value).toContain(12)
} else {
expect(component.documentForm.get(field).value).toBe(12)
}
expect(component.suggestions()[suggestedField]).toEqual([])
}
)
it('should support creating storage path', () => { it('should support creating storage path', () => {
initNormally() initNormally()
let openModal: NgbModalRef let openModal: NgbModalRef
@@ -1528,6 +1567,113 @@ describe('DocumentDetailComponent', () => {
expect(component.suggestionsLoading()).toBeFalsy() expect(component.suggestionsLoading()).toBeFalsy()
}) })
it('should get and merge ML and AI suggestions when source is both', () => {
settingsService.set(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE,
SuggestionSource.Both
)
const getSetting = settingsService.get.bind(settingsService)
jest
.spyOn(settingsService, 'get')
.mockImplementation((key) =>
key === SETTINGS_KEYS.AI_ENABLED ? true : getSetting(key)
)
const suggestionsSpy = jest
.spyOn(documentService, 'getSuggestions')
.mockReturnValue(of({ tags: [42], dates: ['2024-01-01'] }))
const aiSuggestionsSpy = jest
.spyOn(documentService, 'getAiSuggestions')
.mockReturnValue(
of({ title: 'AI title', tags: [42, 43], suggested_tags: ['New'] })
)
initNormally()
expect(suggestionsSpy).toHaveBeenCalled()
expect(aiSuggestionsSpy).toHaveBeenCalled()
expect(component.suggestions().title).toEqual('AI title')
expect(component.suggestions().tags).toEqual([42, 43])
expect(component.suggestions().suggested_tags).toEqual(['New'])
expect(component.suggestions().dates).toEqual(['2024-01-01'])
})
it('should only fetch sources not yet fetched for the document', () => {
settingsService.set(SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST, false)
settingsService.set(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE,
SuggestionSource.ML
)
const getSetting = settingsService.get.bind(settingsService)
jest
.spyOn(settingsService, 'get')
.mockImplementation((key) =>
key === SETTINGS_KEYS.AI_ENABLED ? true : getSetting(key)
)
const suggestionsSpy = jest
.spyOn(documentService, 'getSuggestions')
.mockReturnValue(of({ tags: [42] }))
const aiSuggestionsSpy = jest
.spyOn(documentService, 'getAiSuggestions')
.mockReturnValue(of({ tags: [43] }))
initNormally()
component.getSuggestions()
expect(suggestionsSpy).toHaveBeenCalledTimes(1)
expect(aiSuggestionsSpy).not.toHaveBeenCalled()
component.getSuggestions(SuggestionSource.Both)
expect(suggestionsSpy).toHaveBeenCalledTimes(1)
expect(aiSuggestionsSpy).toHaveBeenCalledTimes(1)
expect(component.suggestions().tags).toEqual([42, 43])
component.getSuggestions(SuggestionSource.Both)
expect(suggestionsSpy).toHaveBeenCalledTimes(1)
expect(aiSuggestionsSpy).toHaveBeenCalledTimes(1)
})
it('should use the per-document source override and reset it on document change', () => {
settingsService.set(SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST, false)
const getSetting = settingsService.get.bind(settingsService)
jest
.spyOn(settingsService, 'get')
.mockImplementation((key) =>
key === SETTINGS_KEYS.AI_ENABLED ? true : getSetting(key)
)
initNormally()
expect(component.suggestionSource).toEqual(SuggestionSource.AI)
component.suggestionSourceOverride.set(SuggestionSource.ML)
expect(component.suggestionSource).toEqual(SuggestionSource.ML)
jest
.spyOn(documentService, 'get')
.mockReturnValueOnce(of(Object.assign({}, doc)))
;(component as any).loadDocument(doc.id, true)
expect(component.suggestionSourceOverride()).toBeNull()
expect(component.fetchedSuggestionSources()).toEqual([])
})
it('should keep suggestions from one source if the other fails', () => {
settingsService.set(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE,
SuggestionSource.Both
)
const getSetting = settingsService.get.bind(settingsService)
jest
.spyOn(settingsService, 'get')
.mockImplementation((key) =>
key === SETTINGS_KEYS.AI_ENABLED ? true : getSetting(key)
)
const errorSpy = jest.spyOn(toastService, 'showError')
jest
.spyOn(documentService, 'getSuggestions')
.mockReturnValue(of({ tags: [42] }))
jest
.spyOn(documentService, 'getAiSuggestions')
.mockReturnValue(throwError(() => new Error('failed')))
initNormally()
expect(errorSpy).toHaveBeenCalled()
expect(component.suggestions().tags).toEqual([42])
expect(component.fetchedSuggestionSources()).toEqual([SuggestionSource.ML])
})
it('should show error if needed for get suggestions', () => { it('should show error if needed for get suggestions', () => {
const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions') const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions')
const errorSpy = jest.spyOn(toastService, 'showError') const errorSpy = jest.spyOn(toastService, 'showError')
@@ -28,7 +28,7 @@ import {
import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { DeviceDetectorService } from 'ngx-device-detector' import { DeviceDetectorService } from 'ngx-device-detector'
import { BehaviorSubject, Observable, of, Subject, timer } from 'rxjs' import { BehaviorSubject, merge, Observable, of, Subject, timer } from 'rxjs'
import { import {
catchError, catchError,
debounceTime, debounceTime,
@@ -48,7 +48,10 @@ import { DataType } from 'src/app/data/datatype'
import { Document, DocumentVersionInfo } from 'src/app/data/document' import { Document, DocumentVersionInfo } from 'src/app/data/document'
import { DocumentMetadata } from 'src/app/data/document-metadata' import { DocumentMetadata } from 'src/app/data/document-metadata'
import { DocumentNote } from 'src/app/data/document-note' import { DocumentNote } from 'src/app/data/document-note'
import { DocumentSuggestions } from 'src/app/data/document-suggestions' import {
DocumentSuggestions,
mergeSuggestions,
} from 'src/app/data/document-suggestions'
import { DocumentType } from 'src/app/data/document-type' import { DocumentType } from 'src/app/data/document-type'
import { FilterRule } from 'src/app/data/filter-rule' import { FilterRule } from 'src/app/data/filter-rule'
import { import {
@@ -63,7 +66,7 @@ import {
import { ObjectWithId } from 'src/app/data/object-with-id' import { ObjectWithId } from 'src/app/data/object-with-id'
import { StoragePath } from 'src/app/data/storage-path' import { StoragePath } from 'src/app/data/storage-path'
import { Tag } from 'src/app/data/tag' import { Tag } from 'src/app/data/tag'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS, SuggestionSource } from 'src/app/data/ui-settings'
import { User } from 'src/app/data/user' import { User } from 'src/app/data/user'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe' import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
@@ -132,6 +135,7 @@ import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-li
import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component' import { SuggestionsDropdownComponent } from '../common/suggestions-dropdown/suggestions-dropdown.component'
import { DocumentNotesComponent } from '../document-notes/document-notes.component' import { DocumentNotesComponent } from '../document-notes/document-notes.component'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component' import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import { DocumentBarcodesComponent } from './document-barcodes/document-barcodes.component'
import { DocumentHistoryComponent } from './document-history/document-history.component' import { DocumentHistoryComponent } from './document-history/document-history.component'
import { DocumentVersionDropdownComponent } from './document-version-dropdown/document-version-dropdown.component' import { DocumentVersionDropdownComponent } from './document-version-dropdown/document-version-dropdown.component'
import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component' import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component'
@@ -174,6 +178,7 @@ interface IncomingDocumentUpdate {
DateComponent, DateComponent,
DocumentLinkComponent, DocumentLinkComponent,
MetadataCollapseComponent, MetadataCollapseComponent,
DocumentBarcodesComponent,
PermissionsFormComponent, PermissionsFormComponent,
SelectComponent, SelectComponent,
TagsComponent, TagsComponent,
@@ -240,6 +245,10 @@ export class DocumentDetailComponent
private readonly autoSuggestSetting = this.settings.getSignal<boolean>( private readonly autoSuggestSetting = this.settings.getSignal<boolean>(
SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST SETTINGS_KEYS.DOCUMENT_EDITING_AUTO_SUGGEST
) )
private readonly suggestionSourceSetting =
this.settings.getSignal<SuggestionSource>(
SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE
)
private readonly hiddenFieldsSetting = this.settings.getSignal< private readonly hiddenFieldsSetting = this.settings.getSignal<
DocumentDetailFieldID[] DocumentDetailFieldID[]
>(SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS) >(SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS)
@@ -261,6 +270,9 @@ export class DocumentDetailComponent
readonly metadata = signal<DocumentMetadata>(undefined) readonly metadata = signal<DocumentMetadata>(undefined)
readonly suggestions = signal<DocumentSuggestions>(undefined) readonly suggestions = signal<DocumentSuggestions>(undefined)
readonly suggestionsLoading = signal(false) readonly suggestionsLoading = signal(false)
// per-document, resets on navigation
readonly suggestionSourceOverride = signal<SuggestionSource>(null)
readonly fetchedSuggestionSources = signal<SuggestionSource[]>([])
readonly users = signal<User[]>(undefined) readonly users = signal<User[]>(undefined)
readonly title = signal<string>(undefined) readonly title = signal<string>(undefined)
@@ -365,6 +377,15 @@ export class DocumentDetailComponent
return this.autoSuggestSetting() return this.autoSuggestSetting()
} }
get defaultSuggestionSource(): SuggestionSource {
return this.aiEnabled ? this.suggestionSourceSetting() : SuggestionSource.ML
}
get suggestionSource(): SuggestionSource {
if (!this.aiEnabled) return SuggestionSource.ML
return this.suggestionSourceOverride() ?? this.defaultSuggestionSource
}
get archiveContentRenderType(): ContentRenderType { get archiveContentRenderType(): ContentRenderType {
const hasArchiveVersion = const hasArchiveVersion =
this.metadata()?.has_archive_version ?? this.metadata()?.has_archive_version ??
@@ -590,6 +611,8 @@ export class DocumentDetailComponent
} }
this.documentId.set(doc.id) this.documentId.set(doc.id)
this.suggestions.set(null) this.suggestions.set(null)
this.suggestionSourceOverride.set(null)
this.fetchedSuggestionSources.set([])
const openDocument = this.openDocumentService.getOpenDocument( const openDocument = this.openDocumentService.getOpenDocument(
this.documentId() this.documentId()
) )
@@ -1077,29 +1100,44 @@ export class DocumentDetailComponent
return this.documentForm.get('custom_fields') as FormArray return this.documentForm.get('custom_fields') as FormArray
} }
getSuggestions() { getSuggestions(source: SuggestionSource = this.suggestionSource) {
const sources = (
source === SuggestionSource.Both
? [SuggestionSource.ML, SuggestionSource.AI]
: [source]
).filter((s) => !this.fetchedSuggestionSources().includes(s))
if (!sources.length) return
this.suggestionsLoading.set(true) this.suggestionsLoading.set(true)
const suggestionsObservable = this.aiEnabled merge(
...sources.map((s) =>
(s === SuggestionSource.AI
? this.documentsService.getAiSuggestions(this.documentId()) ? this.documentsService.getAiSuggestions(this.documentId())
: this.documentsService.getSuggestions(this.documentId()) : this.documentsService.getSuggestions(this.documentId())
suggestionsObservable ).pipe(
.pipe(
first(), first(),
takeUntil(this.unsubscribeNotifier), map((result) => ({ source: s, result })),
takeUntil(this.docChangeNotifier), catchError((error) => {
finalize(() => this.suggestionsLoading.set(false))
)
.subscribe({
next: (result) => {
this.suggestions.set(result)
},
error: (error) => {
this.suggestions.set(null)
this.toastService.showError( this.toastService.showError(
$localize`Error retrieving suggestions.`, $localize`Error retrieving suggestions.`,
error error
) )
}, return of(null)
})
)
)
)
.pipe(
takeUntil(this.unsubscribeNotifier),
takeUntil(this.docChangeNotifier),
finalize(() => this.suggestionsLoading.set(false))
)
.subscribe((response) => {
if (!response) return
this.fetchedSuggestionSources.update((f) => [...f, response.source])
this.suggestions.set(
mergeSuggestions(this.suggestions(), response.result)
)
}) })
} }
@@ -1116,7 +1154,7 @@ export class DocumentDetailComponent
if (this.suggestions()) { if (this.suggestions()) {
this.suggestions.set({ this.suggestions.set({
...this.suggestions(), ...this.suggestions(),
suggested_tags: this.suggestions().suggested_tags.filter( suggested_tags: (this.suggestions().suggested_tags ?? []).filter(
(tag) => tag !== newTag.name (tag) => tag !== newTag.name
), ),
}) })
@@ -1155,10 +1193,12 @@ export class DocumentDetailComponent
this.documentForm.get('document_type').setValue(newDocumentType.id) this.documentForm.get('document_type').setValue(newDocumentType.id)
this.documentForm.get('document_type').markAsDirty() this.documentForm.get('document_type').markAsDirty()
if (this.suggestions()) { if (this.suggestions()) {
this.suggestions().suggested_document_types = this.suggestions.set({
this.suggestions().suggested_document_types.filter( ...this.suggestions(),
(dt) => dt !== newName suggested_document_types: (
) this.suggestions().suggested_document_types ?? []
).filter((dt) => dt !== newName),
})
} }
}) })
} }
@@ -1185,10 +1225,12 @@ export class DocumentDetailComponent
this.documentForm.get('correspondent').setValue(newCorrespondent.id) this.documentForm.get('correspondent').setValue(newCorrespondent.id)
this.documentForm.get('correspondent').markAsDirty() this.documentForm.get('correspondent').markAsDirty()
if (this.suggestions()) { if (this.suggestions()) {
this.suggestions().suggested_correspondents = this.suggestions.set({
this.suggestions().suggested_correspondents.filter( ...this.suggestions(),
(c) => c !== newName suggested_correspondents: (
) this.suggestions().suggested_correspondents ?? []
).filter((c) => c !== newName),
})
} }
}) })
} }
+7
View File
@@ -0,0 +1,7 @@
export interface DocumentBarcode {
page: number
value: string
format: string
}
+4
View File
@@ -1,3 +1,5 @@
import { DocumentBarcode } from './document-barcode'
export interface DocumentMetadata { export interface DocumentMetadata {
original_checksum?: string original_checksum?: string
@@ -12,4 +14,6 @@ export interface DocumentMetadata {
has_archive_version?: boolean has_archive_version?: boolean
lang?: string lang?: string
barcodes?: DocumentBarcode[]
} }
@@ -15,3 +15,33 @@ export interface DocumentSuggestions {
dates?: string[] // ISO-formatted date string e.g. 2022-11-03 dates?: string[] // ISO-formatted date string e.g. 2022-11-03
} }
const union = <T>(a: T[] = [], b: T[] = []): T[] => [...new Set([...a, ...b])]
export function mergeSuggestions(
a: DocumentSuggestions,
b: DocumentSuggestions
): DocumentSuggestions {
if (!a) return b
return {
title: a.title || b.title,
tags: union(a.tags, b.tags),
suggested_tags: union(a.suggested_tags, b.suggested_tags),
correspondents: union(a.correspondents, b.correspondents),
suggested_correspondents: union(
a.suggested_correspondents,
b.suggested_correspondents
),
document_types: union(a.document_types, b.document_types),
suggested_document_types: union(
a.suggested_document_types,
b.suggested_document_types
),
storage_paths: union(a.storage_paths, b.storage_paths),
suggested_storage_paths: union(
a.suggested_storage_paths,
b.suggested_storage_paths
),
dates: union(a.dates, b.dates),
}
}
+8
View File
@@ -330,6 +330,13 @@ export const PaperlessConfigOptions: ConfigOption[] = [
config_key: 'PAPERLESS_CONSUMER_TAG_BARCODE_SPLIT', config_key: 'PAPERLESS_CONSUMER_TAG_BARCODE_SPLIT',
category: ConfigCategory.Barcode, category: ConfigCategory.Barcode,
}, },
{
key: 'barcode_store_values',
title: $localize`Store Barcode Contents`,
type: ConfigOptionType.Boolean,
config_key: 'PAPERLESS_CONSUMER_STORE_BARCODE_VALUES',
category: ConfigCategory.Barcode,
},
{ {
key: 'ai_enabled', key: 'ai_enabled',
title: $localize`AI Enabled`, title: $localize`AI Enabled`,
@@ -458,6 +465,7 @@ export interface PaperlessConfig extends ObjectWithId {
barcode_enable_tag: boolean barcode_enable_tag: boolean
barcode_tag_mapping: object barcode_tag_mapping: object
barcode_tag_split: boolean barcode_tag_split: boolean
barcode_store_values: boolean
remote_ocr_engine: string remote_ocr_engine: string
remote_ocr_api_key: string remote_ocr_api_key: string
remote_ocr_endpoint: string remote_ocr_endpoint: string
+13
View File
@@ -20,6 +20,12 @@ export enum GlobalSearchType {
TITLE_CONTENT = 'title-content', TITLE_CONTENT = 'title-content',
} }
export enum SuggestionSource {
ML = 'ml',
AI = 'ai',
Both = 'both',
}
export enum CollapsibleSection { export enum CollapsibleSection {
ATTRIBUTES = 'attributes', ATTRIBUTES = 'attributes',
} }
@@ -98,6 +104,8 @@ export const SETTINGS_KEYS = {
'general-settings:document-editing:overlay-thumbnail', 'general-settings:document-editing:overlay-thumbnail',
DOCUMENT_EDITING_AUTO_SUGGEST: DOCUMENT_EDITING_AUTO_SUGGEST:
'general-settings:document-editing:auto-suggest', 'general-settings:document-editing:auto-suggest',
DOCUMENT_EDITING_SUGGESTION_SOURCE:
'general-settings:document-editing:suggestion-source',
DOCUMENT_DETAILS_HIDDEN_FIELDS: DOCUMENT_DETAILS_HIDDEN_FIELDS:
'general-settings:document-details:hidden-fields', 'general-settings:document-details:hidden-fields',
SEARCH_DB_ONLY: 'general-settings:search:db-only', SEARCH_DB_ONLY: 'general-settings:search:db-only',
@@ -326,6 +334,11 @@ export const SETTINGS: UiSetting[] = [
type: 'boolean', type: 'boolean',
default: true, default: true,
}, },
{
key: SETTINGS_KEYS.DOCUMENT_EDITING_SUGGESTION_SOURCE,
type: 'string',
default: SuggestionSource.AI,
},
{ {
key: SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS, key: SETTINGS_KEYS.DOCUMENT_DETAILS_HIDDEN_FIELDS,
type: 'array', type: 'array',
+1 -1
View File
@@ -8,7 +8,7 @@ export const environment = {
apiVersion: '10', // match src/paperless/settings.py apiVersion: '10', // match src/paperless/settings.py
appTitle: DEFAULT_APP_TITLE, appTitle: DEFAULT_APP_TITLE,
tag: 'prod', tag: 'prod',
version: '3.2.1', version: '3.3.1',
webSocketHost: window.location.host, webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/', webSocketBaseUrl: base_url.pathname + 'ws/',
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Loaded 100 of 228 files, more files were not shown because too many files have changed in this diff. Show more