Compare commits

..

1 Commits

Author SHA1 Message Date
Crowdin Bot eae75e8798 New Crowdin translations by GitHub Action 2026-04-16 01:00:16 +00:00
332 changed files with 54719 additions and 69728 deletions
+4 -5
View File
@@ -11,7 +11,7 @@ concurrency:
group: backend-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
DEFAULT_UV_VERSION: "0.11.x"
DEFAULT_UV_VERSION: "0.10.x"
NLTK_DATA: "/usr/share/nltk_data"
permissions: {}
jobs:
@@ -100,13 +100,12 @@ jobs:
with:
python-version: "${{ matrix.python-version }}"
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
python-version: ${{ steps.setup-python.outputs.python-version }}
- name: Install system dependencies
timeout-minutes: 10
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends \
@@ -178,7 +177,7 @@ jobs:
with:
python-version: "${{ env.DEFAULT_PYTHON }}"
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
@@ -202,7 +201,7 @@ jobs:
check \
src/
- name: Cache Mypy
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .mypy_cache
# Keyed by OS, Python version, and dependency hashes
+6 -6
View File
@@ -108,7 +108,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -132,7 +132,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
- name: Build and push by digest
id: build
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
file: ./Dockerfile
@@ -154,7 +154,7 @@ jobs:
echo "${digest}" > "/tmp/digests/digest-${{ matrix.arch }}.txt"
- name: Upload digest
if: steps.check-push.outputs.should-push == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: digests-${{ matrix.arch }}
path: /tmp/digests/digest-${{ matrix.arch }}.txt
@@ -184,20 +184,20 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
if: needs.build-arch.outputs.push-external == 'true'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Quay.io
if: needs.build-arch.outputs.push-external == 'true'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
+3 -3
View File
@@ -11,7 +11,7 @@ concurrency:
permissions:
contents: read
env:
DEFAULT_UV_VERSION: "0.11.x"
DEFAULT_UV_VERSION: "0.10.x"
DEFAULT_PYTHON_VERSION: "3.12"
jobs:
changes:
@@ -78,7 +78,7 @@ jobs:
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
@@ -94,7 +94,7 @@ jobs:
--frozen \
zensical build --clean
- name: Upload GitHub Pages artifact
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
with:
path: site
name: github-pages-${{ github.run_id }}-${{ github.run_attempt }}
+16 -16
View File
@@ -81,18 +81,18 @@ jobs:
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.pnpm-store
@@ -113,17 +113,17 @@ jobs:
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.pnpm-store
@@ -152,17 +152,17 @@ jobs:
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.pnpm-store
@@ -191,7 +191,7 @@ jobs:
runs-on: ubuntu-24.04
permissions:
contents: read
container: mcr.microsoft.com/playwright:v1.59.1-noble
container: mcr.microsoft.com/playwright:v1.59.0-noble
env:
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
@@ -207,17 +207,17 @@ jobs:
with:
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.pnpm-store
@@ -244,17 +244,17 @@ jobs:
fetch-depth: 2
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.pnpm-store
+1 -1
View File
@@ -25,4 +25,4 @@ jobs:
with:
python-version: "3.14"
- name: Run prek
uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2
uses: j178/prek-action@53276d8b0d10f8b6672aa85b4588c6921d0370cc # v2.0.1
+12 -12
View File
@@ -8,7 +8,7 @@ concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
env:
DEFAULT_UV_VERSION: "0.11.x"
DEFAULT_UV_VERSION: "0.10.x"
DEFAULT_PYTHON_VERSION: "3.12"
permissions: {}
jobs:
@@ -20,10 +20,10 @@ jobs:
statuses: read
steps:
- name: Wait for Docker build
uses: lewagon/wait-on-check-action@9312864dfbc9fd208e9c0417843430751c042800 # v1.7.0
uses: lewagon/wait-on-check-action@74049309dfeff245fe8009a0137eacf28136cb3c # v1.5.0
with:
ref: ${{ github.sha }}
check-name: 'Merge and Push Manifest'
check-name: 'Build Docker Image'
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 60
build-release:
@@ -39,11 +39,11 @@ jobs:
persist-credentials: false
# ---- Frontend Build ----
- name: Install pnpm
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24.x
package-manager-cache: false
@@ -58,7 +58,7 @@ jobs:
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false
@@ -135,7 +135,7 @@ jobs:
sudo chown -R 1000:1000 paperless-ngx/
tar -cJf paperless-ngx.tar.xz paperless-ngx/
- name: Upload release artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: release
path: dist/paperless-ngx.tar.xz
@@ -170,18 +170,18 @@ jobs:
fi
- name: Create release and changelog
id: create-release
uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0
uses: release-drafter/release-drafter@139054aeaa9adc52ab36ddf67437541f039b88e2 # v7.1.1
with:
name: Paperless-ngx ${{ steps.get-version.outputs.version }}
tag: ${{ steps.get-version.outputs.version }}
version: ${{ steps.get-version.outputs.version }}
prerelease: ${{ steps.get-version.outputs.prerelease }}
publish: true
commitish: ${{ steps.get-version.outputs.prerelease == 'true' && 'dev' || 'main' }}
commitish: main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload release archive
uses: shogo82148/actions-upload-release-asset@ee2ae851dc5d938b90075b3ef12c540abfd1ee72 # v1.10.1
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1.10.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
upload_url: ${{ steps.create-release.outputs.upload_url }}
@@ -211,7 +211,7 @@ jobs:
with:
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: false
@@ -250,7 +250,7 @@ jobs:
git commit -am "Changelog ${VERSION} - GHA"
git push origin "${branch_name}"
- name: Create pull request
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
VERSION: ${{ needs.publish-release.outputs.version }}
with:
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
semgrep:
name: Semgrep CE
runs-on: ubuntu-24.04
@@ -44,7 +44,7 @@ jobs:
- name: Run Semgrep
run: semgrep scan --config auto --sarif-output results.sarif
- name: Upload results to GitHub code scanning
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
if: always()
with:
sarif_file: results.sarif
+2 -2
View File
@@ -39,7 +39,7 @@ jobs:
persist-credentials: false
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
languages: ${{ matrix.language }}
# 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.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
token: ${{ secrets.PNGX_BOT_PAT }}
persist-credentials: false
- name: crowdin action
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
uses: crowdin/github-action@7ca9c452bfe9197d3bb7fa83a4d7e2b0c9ae835d # v2.16.0
with:
upload_translations: false
download_translations: true
+5 -6
View File
@@ -3,18 +3,17 @@ on:
pull_request_target:
types: [opened]
jobs:
Anti-slop:
anti-slop:
runs-on: ubuntu-latest
permissions:
contents: read
issues: read
pull-requests: write
steps:
- uses: peakoss/anti-slop@57858eead489d08b255fab2af45a506c2ca6eab2 # v0.3.0
- uses: peakoss/anti-slop@85daca1880e9e1af197fc06ea03349daf08f4202 # v0.2.1
with:
max-failures: 4
failure-add-pr-labels: 'ai'
require-pr-template: true
pr-bot:
name: Automated PR Bot
runs-on: ubuntu-latest
@@ -38,7 +37,7 @@ jobs:
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
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const pr = context.payload.pull_request;
@@ -64,7 +63,7 @@ jobs:
}
- name: Label bot-generated PRs
if: ${{ contains(github.actor, 'dependabot') || contains(github.actor, 'crowdin-bot') }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const pr = context.payload.pull_request;
@@ -89,7 +88,7 @@ jobs:
}
- name: Welcome comment
if: ${{ !contains(github.actor, 'bot') }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const pr = context.payload.pull_request;
+1 -1
View File
@@ -19,6 +19,6 @@ jobs:
if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login != 'dependabot'
steps:
- name: Label PR with release-drafter
uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0
uses: release-drafter/release-drafter@139054aeaa9adc52ab36ddf67437541f039b88e2 # v7.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3 -3
View File
@@ -62,7 +62,7 @@ jobs:
permissions:
discussions: write
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
function sleep(ms) {
@@ -121,7 +121,7 @@ jobs:
permissions:
discussions: write
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
function sleep(ms) {
@@ -215,7 +215,7 @@ jobs:
permissions:
discussions: write
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
function sleep(ms) {
+4 -7
View File
@@ -3,8 +3,6 @@ on:
push:
branches:
- dev
env:
DEFAULT_UV_VERSION: "0.11.x"
jobs:
generate-translate-strings:
name: Generate Translation Strings
@@ -29,9 +27,8 @@ jobs:
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends gettext
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
version: ${{ env.DEFAULT_UV_VERSION }}
enable-cache: true
- name: Install backend python dependencies
run: |
@@ -43,18 +40,18 @@ jobs:
PAPERLESS_SECRET_KEY: "ci-translate-not-a-real-secret"
run: cd src/ && uv run manage.py makemessages -l en_US -i "samples*"
- name: Install pnpm
uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
with:
version: 10
- name: Use Node.js 24
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24.x
cache: 'pnpm'
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
- name: Cache frontend dependencies
id: cache-frontend-deps
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.pnpm-store
+606 -301
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -38,7 +38,7 @@ repos:
- json
# See https://github.com/prettier/prettier/issues/15742 for the fork reason
- repo: https://github.com/rbubley/mirrors-prettier
rev: 'v3.8.3'
rev: 'v3.8.1'
hooks:
- id: prettier
types_or:
@@ -46,16 +46,16 @@ repos:
- ts
- markdown
additional_dependencies:
- prettier@3.8.3
- prettier@3.8.1
- 'prettier-plugin-organize-imports@4.3.0'
# Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.12
rev: v0.15.8
hooks:
- id: ruff-check
- id: ruff-format
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "v2.21.1"
rev: "v2.21.0"
hooks:
- id: pyproject-fmt
# Dockerfile hooks
+5250 -3762
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -30,7 +30,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs
# Comments:
# - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.11.6-python3.12-trixie-slim AS s6-overlay-base
FROM ghcr.io/astral-sh/uv:0.10.9-python3.12-trixie-slim AS s6-overlay-base
WORKDIR /usr/src/s6
@@ -104,6 +104,8 @@ ARG JBIG2ENC_VERSION=0.30
# Set Python environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
# Ignore warning from Whitenoise about async iterators
PYTHONWARNINGS="ignore:::django.http.response:517" \
PNGX_CONTAINERIZED=1 \
# https://docs.astral.sh/uv/reference/settings/#link-mode
UV_LINK_MODE=copy
@@ -234,10 +236,8 @@ RUN set -eux \
&& mkdir -m700 --verbose /usr/src/paperless/.gnupg \
&& echo "Adjusting all permissions" \
&& chown --from root:root --changes --recursive paperless:paperless /usr/src/paperless \
&& echo "Making fontconfig cache writable for arbitrary container UIDs" \
&& chmod 1777 /var/cache/fontconfig \
&& echo "Collecting static files" \
&& PAPERLESS_SECRET_KEY=build-time-dummy s6-setuidgid paperless python3 manage.py collectstatic --clear --no-input \
&& PAPERLESS_SECRET_KEY=build-time-dummy s6-setuidgid paperless python3 manage.py collectstatic --clear --no-input --link \
&& PAPERLESS_SECRET_KEY=build-time-dummy s6-setuidgid paperless python3 manage.py compilemessages \
&& /usr/local/bin/deduplicate.py --verbose /usr/src/paperless/static/
+3 -3
View File
@@ -7,9 +7,9 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/paperless-ngx/paperless-ngx/blob/main/docs/assets/logo_full_white.png" width="50%">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/paperless-ngx/paperless-ngx/blob/main/docs/assets/logo_full_black.png" width="50%">
<img src="https://github.com/paperless-ngx/paperless-ngx/blob/main/docs/assets/logo_full_black.png" width="50%">
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/paperless-ngx/paperless-ngx/blob/main/resources/logo/web/png/White%20logo%20-%20no%20background.png" width="50%">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/Black%20logo%20-%20no%20background.png" width="50%">
<img src="https://github.com/paperless-ngx/paperless-ngx/raw/main/resources/logo/web/png/Black%20logo%20-%20no%20background.png" width="50%">
</picture>
</p>
-1
View File
@@ -57,7 +57,6 @@ We may close reports that are:
The following are not generally considered vulnerabilities unless accompanied by a concrete, reproducible impact in Paperless-ngx:
- large uploads or resource usage that do not bypass documented limits or privileges
- IDOR / access control claims regarding the ability to attach an un-viewable object to a document. This is expected behavior.
- claims based solely on the presence of a library, framework feature or code pattern without a working exploit
- reports that rely on admin-level access, workflow-editing privileges, shell access, or other high-trust roles unless they demonstrate an unintended privilege boundary bypass
- optional webhook, mail, AI, OCR, or integration behavior described without a product-level vulnerability
@@ -8,13 +8,6 @@ export GRANIAN_HOST=${GRANIAN_HOST:-${PAPERLESS_BIND_ADDR:-"::"}}
export GRANIAN_PORT=${GRANIAN_PORT:-${PAPERLESS_PORT:-8000}}
export GRANIAN_WORKERS=${GRANIAN_WORKERS:-${PAPERLESS_WEBSERVER_WORKERS:-1}}
# Static file serving: Granian matches against the raw URI path (before any
# SCRIPT_NAME stripping), so the route must include the subpath prefix.
_static_dir="${PAPERLESS_STATICDIR:-/usr/src/paperless/static}"
_static_route="${PAPERLESS_FORCE_SCRIPT_NAME}/static"
export GRANIAN_STATIC_PATH_MOUNT=${GRANIAN_STATIC_PATH_MOUNT:-${_static_dir}}
export GRANIAN_STATIC_PATH_ROUTE=${GRANIAN_STATIC_PATH_ROUTE:-${_static_route:-/static}}
# Only set GRANIAN_URL_PATH_PREFIX if PAPERLESS_FORCE_SCRIPT_NAME is set
if [[ -n "${PAPERLESS_FORCE_SCRIPT_NAME}" ]]; then
export GRANIAN_URL_PATH_PREFIX=${PAPERLESS_FORCE_SCRIPT_NAME}
+6 -7
View File
@@ -357,13 +357,12 @@ and the script does the rest of the work:
document_importer source
```
| Option | Required | Default | Description |
| ------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------ |
| source | Yes | N/A | The directory containing an export |
| `--no-progress-bar` | No | False | If provided, the progress bar will be hidden |
| `--data-only` | No | False | If provided, only import data, do not import document files or thumbnails |
| `--passphrase` | No | N/A | If your export was encrypted with a passphrase, must be provided |
| `--batch-size` | No | 500 | Number of database records inserted per batch. Lower values reduce peak memory usage on very large installs. |
| Option | Required | Default | Description |
| ------------------- | -------- | ------- | ------------------------------------------------------------------------- |
| source | Yes | N/A | The directory containing an export |
| `--no-progress-bar` | No | False | If provided, the progress bar will be hidden |
| `--data-only` | No | False | If provided, only import data, do not import document files or thumbnails |
| `--passphrase` | No | N/A | If your export was encrypted with a passphrase, must be provided |
When you use the provided docker compose script, put the export inside
the `export` folder in your paperless source directory. Specify
Binary file not shown.

Before

Width:  |  Height:  |  Size: 748 B

After

Width:  |  Height:  |  Size: 768 B

+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<path class="st0" d="M299,891.7c-4.2-19.8-12.5-59.6-13.6-59.6c-176.7-105.7-155.8-288.7-97.3-393.4
c12.5,131.8,245.8,222.8,109.8,383.9c-1.1,2,6.2,27.2,12.5,50.2c27.2-46,68-101.4,65.8-106.7C208.9,358.2,731.9,326.9,840.6,73.7
c49.1,244.8-25.1,623.5-445.5,719.7c-2,1.1-76.3,131.8-79.5,132.9c0-2-31.4-1.1-27.2-11.5C290.7,908.4,294.8,900.1,299,891.7
L299,891.7z M293.8,793.4c53.3-61.8-9.4-167.4-47.1-201.9C310.5,701.3,306.3,765.1,293.8,793.4L293.8,793.4z"/>
</svg>

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 57 KiB

+67 -18
View File
@@ -1,19 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 2670 860">
<path id="leaf" style="fill:#005616;" d="M2227.4,821.2c-6.1-17.8-18.1-53.6-19.2-53.4-174.7-77.8-159.8-201.2-117.5-304.2,26.3,120.1,235.3,130.3,128,294.1-.7,2,8.8,24.3,17.1,44.9,19.9-45.4,51.3-101.1,48.8-105.7-199.9-357.4,278.8-444.7,350.7-690.2,72.6,220.1,46.5,577.5-330.4,713.3-1.8,1.2-55.6,130-58.5,131.4-.2-1.9-29.1,2.5-26.4-7.6,1.4-6.2,4.2-14.2,7.2-22.4h0v-.2h.2,0ZM2211.7,731.2c42.3-62.9-11.1-105.7-49.8-133.2,71,94,58.1,105.7,49.8,133.2h0Z"/>
<g id="text" style="fill: #000;">
<path class="st1" d="M654.6,393.2l-.7,137.7h-85.5V188.7h85.4c.4,11.3-.3,21.7,1.3,33.8,23.1-34.1,62.3-50,101.1-38.3,16.5,5,29.6,16.4,39.7,30,34.4,46.5,35.1,134,3.6,182.2-10.1,14.4-22.5,26.9-39,33.4-39.5,15.7-81,1.1-105.9-36.6h0ZM721,362.2c21-26.1,21-82.7-.4-108.4-13.2-15.9-36.4-16.1-49.9-.4-22.2,25.8-21.7,85.3.5,110.1,13.6,15.2,36.6,15,49.7-1.3h.1Z"/>
<path class="st1" d="M164,301l-72.8.7v126.1H3.4V98.1l159.7.5c31.3,0,58.9,13.6,79.4,36.1,30.8,37.6,30.9,91.7.6,129.6-20.1,22.8-47.6,36.5-79,36.8h-.1ZM176.8,199.8c0-20.8-15.1-35-34.7-35l-51,.2v69.5l53.6-.2c18.5,0,32-15.8,32.2-34.5h-.1Z"/>
<polygon class="st1" points="1338.2 427.8 1338 366 1412.4 365.8 1412.5 139.3 1338.1 139.1 1338.1 77.4 1498.1 77.4 1498.1 365.7 1572.3 365.9 1572.5 427.7 1338.2 427.8"/>
<path class="st1" d="M1741.8,364.3c9.1-8.6,14-18.1,17.7-30.3l68.4,13.3c-10.5,45.2-46.5,79.2-92.3,86.7-59.2,9.6-118.7-14.2-138.6-73.7-10.9-32.7-10.7-68.6.6-100.9,17.7-50.6,64.3-80.5,117.1-79.1,76.5,2,113.4,65.4,111.1,136.1h-155.4c-.7,12.5,3,25,9.7,35.9,13.2,21.3,40.9,26.9,61.5,12h.2ZM1749.4,273.1c-2.4-10.8-6.9-18-13.9-24.6-12.8-8.3-30.1-9.5-43.4-1.1-9.3,5.8-14.6,15.1-18,25.7h75.3Z"/>
<path class="st1" d="M1010.3,364.3c9.1-8.5,13.9-18.1,17.7-30.3l68.4,13.3c-10.4,45.2-46.5,79.2-92.3,86.7-59.3,9.6-118.8-14.2-138.7-73.9-10.8-32.3-10.6-67.4.2-99.3,17.3-51.2,64.2-81.8,117.6-80.4,76.6,2,113.5,65.3,111.1,136.1h-155.6c-.2,12.7,3.2,25.1,9.9,35.9,13.2,21.3,40.9,27,61.5,12h.2ZM1018,273.2c-2.4-9.4-6.3-18.5-14.2-24.4-12.3-9.1-30.4-9.4-43.3-1.3-9.3,5.9-14.4,15.1-17.9,25.6h75.4Z"/>
<path class="st1" d="M424.3,376.9c-7.1,13.6-12.5,25.7-23.2,35.5-14.3,13.3-32.6,19.3-52.3,19.4-40.4.2-75.6-23.1-73.6-65.7.9-20.1,9.7-37.2,26.5-49.2,30.5-21.8,55.8-22.4,87.8-40.6,8.1-4.6,18.2-15.3,12.4-22.2s-5-3-8-3.7h-96.3v-61.8h109.6c14.7.6,28.1,2.2,41.7,7.2,23.7,8.8,39.6,29.5,39.8,55.2l.7,90.6c0,13.5,11,23,23.7,23.9l10.1.7v61.3h-29.9c-13.1,0-25.9-3-37.3-8.6-16.9-8.2-26.9-22.2-31.6-42.2h0v.2h-.1ZM364.9,370.1c6.8,5.9,16.2,6.5,24.8,2.7,18.1-7.9,16.5-38.3,16.1-55-3.6,4.3-7.4,9-12.5,11.2l-21.1,9.3c-5.8,2.5-10.6,8-11.8,13s-1,13.8,4.7,18.7h-.2Z"/>
<path class="st1" d="M1943,430.1c-33.5-8.9-68.5-33.6-78.9-68.9l66.6-27.2c11.8,22.1,31.6,42.1,57.2,39.8,4.3-.4,9.3-3.1,11.2-6,7.8-12.5-4.3-24.3-16.2-30.7l-47.3-25.2c-32.2-17.1-57.7-50.7-41.6-87.4,11.9-27,48.1-35,75.3-36h99.2v61.8h-88.6c-2.5.4-6.2,2.3-7,4.2s.7,7,2.7,8.2c31.6,18.6,88.3,38.3,103.8,72,10.4,22.6,6.7,50-9.2,69.1-29.5,35.7-86.1,36.9-127,26.1v.2h-.2,0Z"/>
<path class="st1" d="M1318.2,264.3l-68.5.2c-19.4,0-30.1,10.8-31.6,30.2v133.1h-85.7v-239h85.6l1,58.9,11.9-25.1c14.3-30.5,56.9-36.5,87.4-33.6v75.4h-.1Z"/>
<path class="st1" d="M2232.8,374.2c-26,1.2-44.6-18.4-56.5-40.1l-66.5,27.3c10.8,35.9,46.2,60.4,80.3,69.2h0c10.6,2.6,22,4.5,33.7,5.2,3.2-7.9,6.8-15.6,10.8-23.4,18.5-35.9,44.3-68.4,73.8-98.8-23.6-21.1-62.6-36.7-87-50.6-2.2-1.2-3.6-6.7-2.7-8.7.9-2,4.5-3.5,7.4-3.9h88.2v-61.8h-97.4c-27,.7-63.8,8.2-76.5,34.8-8.3,17.5-6.8,38.5,3.5,54.9,9.3,14.9,22.2,25.8,37.7,33.9l45.8,24.3c11.5,6.1,24.7,17,17.9,30.5-2.1,4.1-7.4,6.5-12.6,7.2h.1Z"/>
<path class="st1" d="M1547.6,801.6h81.2c11.6-.2,23.2-3.8,31.9-11.2,7.3-6.2,11.7-15.4,13.9-24.8l16.8-72.7c-7.2,9-12.8,16.9-20.7,24.2-18.3,16.8-42.3,23.8-66.9,19.5-32.5-5.7-46.7-34.7-47-65.6-.5-44,18.9-93.6,57.6-117.1,18-10.9,39.5-13.9,60-9.6,12.4,2.6,22.1,9.9,29.1,20,5.8,8.4,7.8,17.2,10.8,27.8l10.7-45.4,15.6.3-50.6,219.5c-2.9,12.6-8.9,24.6-18.4,32.9-12,10.4-28.1,15.1-44,15.2l-82.9.2,2.7-13.1h.2ZM1691.8,673.5c12.9-26.3,20.1-60.3,11-88.6-5.1-15.8-17.9-26.5-34.2-28.8-20.7-2.9-40.3,2.9-55.9,16.8-13.6,12.1-23.5,26.7-30.3,43.7-9.8,24.4-14.8,56.5-4.6,81.1,5,12.1,14.7,21.3,27.6,24.7,39,10.3,70.1-16,86.4-49h0Z"/>
<path class="st1" d="M1441.6,556.8c-43.6-8.7-84.4,29.7-93.8,70l-24.8,106.6h-15.7l43.1-186.4,15.6-.2-8.6,39.5c22.3-28.9,53.9-49.3,90.7-42.5,16.8,3.1,29.1,15.6,32.1,32.4,2.1,11.6,1.6,23.4-1.1,35.3l-28.1,122.2h-15.6c0,0,27.5-119.9,27.5-119.9,4.7-20.6,5.9-51.3-21.2-56.7v-.3Z"/>
<path class="st1" d="M1958.9,733.3h-16.2l-38.2-90.1-79.8,90.3-19.3-.2,77.6-87.2c5.1-5.7,11-10.1,17.2-14.5-4.6-4.7-8.5-9.6-11.3-15.3l-33.9-69.3,16.2-.2,35.3,74.1,69-73.9c6.6-.3,12.7-.3,19.6.2l-63.1,66.6c-6.4,6.8-13.4,12.5-20.9,18,3.4,3.4,7.5,7.5,9.6,12.4l38.3,89.2h-.1Z"/>
<path class="st1" d="M1224.4,635.4H3.4c1.1-5.6,1.9-9.5,3.1-13.9h1220.9l-2.9,13.9h0Z"/>
</g>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 2962.2 860.2" style="enable-background:new 0 0 2962.2 860.2;" xml:space="preserve">
<style type="text/css">
.st0{fill:#17541F;stroke:#000000;stroke-miterlimit:10;}
</style>
<path d="M1055.6,639.7v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3v249.6L1055.6,639.7L1055.6,639.7z M1059.1,514.9c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
c16.8,0,30.3-5.9,40.6-17.7C1054,546.9,1059.1,532.3,1059.1,514.9z"/>
<path d="M1417.8,398.2c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V390.2h85.3v20.6
c18-20,43.1-30.1,75.4-30.1C1379.2,380.7,1399.5,386.6,1417.8,398.2z M1389.5,514.9c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7S1389.5,532.3,1389.5,514.9z"/>
<path d="M1713.6,555.3l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2s37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8c3.6,11.4,10.5,20.7,20.9,28.1
c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1695.8,570.1,1704.9,563.7,1713.6,555.3z M1596.9,486.2h92.9
c-2.1-12.3-7.5-22.1-16.2-29.4s-18.7-11-30.1-11s-21.5,3.7-30.3,11S1599,473.9,1596.9,486.2z"/>
<path d="M1908.8,418.4c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7c-8.4-2.1-15.7-3.1-22-3.1
c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V390.2h85.3V418.4L1908.8,418.4z"/>
<path d="M2113,258.2v381.5h-85.3V258.2H2113z"/>
<path d="M2360.8,555.3l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2343.1,570.1,2352.1,563.7,2360.8,555.3z
M2244.1,486.2h92.9c-2.1-12.3-7.5-22.1-16.2-29.4s-18.7-11-30.1-11s-21.5,3.7-30.3,11C2251.7,464.1,2246.2,473.9,2244.1,486.2z"/>
<path d="M2565.9,446.3c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2590.5,448.7,2577.6,446.3,2565.9,446.3z"/>
<path d="M2817.3,446.3c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2841.8,448.7,2828.9,446.3,2817.3,446.3z"/>
<g>
<path d="M2508,724h60.2v17.3H2508V724z"/>
<path d="M2629.2,694.4c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6v-52.6
c0-9.3-1.7-16.2-5.1-20.7c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2620.2,699.4,2624.4,696.4,2629.2,694.4z"/>
<path d="M2790.3,833.2c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2s-6.6-11.9-7.1-19.6h19.6
c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9v-24.7c-3.6,3.4-8,6.2-13.3,8.2
c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3c-4-7.3-6-15.5-6-24.6s2-17.3,6-24.7
s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8h19.4v107.8
C2803.2,815.9,2798.9,826.4,2790.3,833.2z M2782.2,755.7c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16c-2.6-4.8-6.1-8.5-10.5-11.1
c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3
c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1C2776.1,764.1,2779.6,760.4,2782.2,755.7z"/>
<path d="M2843.5,788.4h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35L2843.5,788.4z
"/>
</g>
<path d="M835.8,319.2c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2H647v-135h72.7c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7c11.5-18.9,17.3-39.5,17.3-61.9
C853.1,358.9,847.4,338.1,835.8,319.2z M747,416.6c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C761.1,396.6,756.4,407.7,747,416.6z"/>
<path class="st0" d="M164.7,698.7c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4C82.8,431.4,277,507.1,163.8,641.2
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C89.7,254.7,525,228.6,615.5,17.9c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C157.8,712.6,161.2,705.7,164.7,698.7L164.7,698.7z M160.4,616.9
c44.4-51.4-7.8-139.3-39.2-168C174.3,540.2,170.8,593.3,160.4,616.9L160.4,616.9z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

-19
View File
@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 2670 860">
<path id="leaf" style="fill:#005616;" d="M2227.4,821.2c-6.1-17.8-18.1-53.6-19.2-53.4-174.7-77.8-159.8-201.2-117.5-304.2,26.3,120.1,235.3,130.3,128,294.1-.7,2,8.8,24.3,17.1,44.9,19.9-45.4,51.3-101.1,48.8-105.7-199.9-357.4,278.8-444.7,350.7-690.2,72.6,220.1,46.5,577.5-330.4,713.3-1.8,1.2-55.6,130-58.5,131.4-.2-1.9-29.1,2.5-26.4-7.6,1.4-6.2,4.2-14.2,7.2-22.4h0v-.2h.2,0ZM2211.7,731.2c42.3-62.9-11.1-105.7-49.8-133.2,71,94,58.1,105.7,49.8,133.2h0Z"/>
<g id="text" style="fill: #eee;">
<path class="st1" d="M654.6,393.2l-.7,137.7h-85.5V188.7h85.4c.4,11.3-.3,21.7,1.3,33.8,23.1-34.1,62.3-50,101.1-38.3,16.5,5,29.6,16.4,39.7,30,34.4,46.5,35.1,134,3.6,182.2-10.1,14.4-22.5,26.9-39,33.4-39.5,15.7-81,1.1-105.9-36.6h0ZM721,362.2c21-26.1,21-82.7-.4-108.4-13.2-15.9-36.4-16.1-49.9-.4-22.2,25.8-21.7,85.3.5,110.1,13.6,15.2,36.6,15,49.7-1.3h.1Z"/>
<path class="st1" d="M164,301l-72.8.7v126.1H3.4V98.1l159.7.5c31.3,0,58.9,13.6,79.4,36.1,30.8,37.6,30.9,91.7.6,129.6-20.1,22.8-47.6,36.5-79,36.8h-.1ZM176.8,199.8c0-20.8-15.1-35-34.7-35l-51,.2v69.5l53.6-.2c18.5,0,32-15.8,32.2-34.5h-.1Z"/>
<polygon class="st1" points="1338.2 427.8 1338 366 1412.4 365.8 1412.5 139.3 1338.1 139.1 1338.1 77.4 1498.1 77.4 1498.1 365.7 1572.3 365.9 1572.5 427.7 1338.2 427.8"/>
<path class="st1" d="M1741.8,364.3c9.1-8.6,14-18.1,17.7-30.3l68.4,13.3c-10.5,45.2-46.5,79.2-92.3,86.7-59.2,9.6-118.7-14.2-138.6-73.7-10.9-32.7-10.7-68.6.6-100.9,17.7-50.6,64.3-80.5,117.1-79.1,76.5,2,113.4,65.4,111.1,136.1h-155.4c-.7,12.5,3,25,9.7,35.9,13.2,21.3,40.9,26.9,61.5,12h.2ZM1749.4,273.1c-2.4-10.8-6.9-18-13.9-24.6-12.8-8.3-30.1-9.5-43.4-1.1-9.3,5.8-14.6,15.1-18,25.7h75.3Z"/>
<path class="st1" d="M1010.3,364.3c9.1-8.5,13.9-18.1,17.7-30.3l68.4,13.3c-10.4,45.2-46.5,79.2-92.3,86.7-59.3,9.6-118.8-14.2-138.7-73.9-10.8-32.3-10.6-67.4.2-99.3,17.3-51.2,64.2-81.8,117.6-80.4,76.6,2,113.5,65.3,111.1,136.1h-155.6c-.2,12.7,3.2,25.1,9.9,35.9,13.2,21.3,40.9,27,61.5,12h.2ZM1018,273.2c-2.4-9.4-6.3-18.5-14.2-24.4-12.3-9.1-30.4-9.4-43.3-1.3-9.3,5.9-14.4,15.1-17.9,25.6h75.4Z"/>
<path class="st1" d="M424.3,376.9c-7.1,13.6-12.5,25.7-23.2,35.5-14.3,13.3-32.6,19.3-52.3,19.4-40.4.2-75.6-23.1-73.6-65.7.9-20.1,9.7-37.2,26.5-49.2,30.5-21.8,55.8-22.4,87.8-40.6,8.1-4.6,18.2-15.3,12.4-22.2s-5-3-8-3.7h-96.3v-61.8h109.6c14.7.6,28.1,2.2,41.7,7.2,23.7,8.8,39.6,29.5,39.8,55.2l.7,90.6c0,13.5,11,23,23.7,23.9l10.1.7v61.3h-29.9c-13.1,0-25.9-3-37.3-8.6-16.9-8.2-26.9-22.2-31.6-42.2h0v.2h-.1ZM364.9,370.1c6.8,5.9,16.2,6.5,24.8,2.7,18.1-7.9,16.5-38.3,16.1-55-3.6,4.3-7.4,9-12.5,11.2l-21.1,9.3c-5.8,2.5-10.6,8-11.8,13s-1,13.8,4.7,18.7h-.2Z"/>
<path class="st1" d="M1943,430.1c-33.5-8.9-68.5-33.6-78.9-68.9l66.6-27.2c11.8,22.1,31.6,42.1,57.2,39.8,4.3-.4,9.3-3.1,11.2-6,7.8-12.5-4.3-24.3-16.2-30.7l-47.3-25.2c-32.2-17.1-57.7-50.7-41.6-87.4,11.9-27,48.1-35,75.3-36h99.2v61.8h-88.6c-2.5.4-6.2,2.3-7,4.2s.7,7,2.7,8.2c31.6,18.6,88.3,38.3,103.8,72,10.4,22.6,6.7,50-9.2,69.1-29.5,35.7-86.1,36.9-127,26.1v.2h-.2,0Z"/>
<path class="st1" d="M1318.2,264.3l-68.5.2c-19.4,0-30.1,10.8-31.6,30.2v133.1h-85.7v-239h85.6l1,58.9,11.9-25.1c14.3-30.5,56.9-36.5,87.4-33.6v75.4h-.1Z"/>
<path class="st1" d="M2232.8,374.2c-26,1.2-44.6-18.4-56.5-40.1l-66.5,27.3c10.8,35.9,46.2,60.4,80.3,69.2h0c10.6,2.6,22,4.5,33.7,5.2,3.2-7.9,6.8-15.6,10.8-23.4,18.5-35.9,44.3-68.4,73.8-98.8-23.6-21.1-62.6-36.7-87-50.6-2.2-1.2-3.6-6.7-2.7-8.7.9-2,4.5-3.5,7.4-3.9h88.2v-61.8h-97.4c-27,.7-63.8,8.2-76.5,34.8-8.3,17.5-6.8,38.5,3.5,54.9,9.3,14.9,22.2,25.8,37.7,33.9l45.8,24.3c11.5,6.1,24.7,17,17.9,30.5-2.1,4.1-7.4,6.5-12.6,7.2h.1Z"/>
<path class="st1" d="M1547.6,801.6h81.2c11.6-.2,23.2-3.8,31.9-11.2,7.3-6.2,11.7-15.4,13.9-24.8l16.8-72.7c-7.2,9-12.8,16.9-20.7,24.2-18.3,16.8-42.3,23.8-66.9,19.5-32.5-5.7-46.7-34.7-47-65.6-.5-44,18.9-93.6,57.6-117.1,18-10.9,39.5-13.9,60-9.6,12.4,2.6,22.1,9.9,29.1,20,5.8,8.4,7.8,17.2,10.8,27.8l10.7-45.4,15.6.3-50.6,219.5c-2.9,12.6-8.9,24.6-18.4,32.9-12,10.4-28.1,15.1-44,15.2l-82.9.2,2.7-13.1h.2ZM1691.8,673.5c12.9-26.3,20.1-60.3,11-88.6-5.1-15.8-17.9-26.5-34.2-28.8-20.7-2.9-40.3,2.9-55.9,16.8-13.6,12.1-23.5,26.7-30.3,43.7-9.8,24.4-14.8,56.5-4.6,81.1,5,12.1,14.7,21.3,27.6,24.7,39,10.3,70.1-16,86.4-49h0Z"/>
<path class="st1" d="M1441.6,556.8c-43.6-8.7-84.4,29.7-93.8,70l-24.8,106.6h-15.7l43.1-186.4,15.6-.2-8.6,39.5c22.3-28.9,53.9-49.3,90.7-42.5,16.8,3.1,29.1,15.6,32.1,32.4,2.1,11.6,1.6,23.4-1.1,35.3l-28.1,122.2h-15.6c0,0,27.5-119.9,27.5-119.9,4.7-20.6,5.9-51.3-21.2-56.7v-.3Z"/>
<path class="st1" d="M1958.9,733.3h-16.2l-38.2-90.1-79.8,90.3-19.3-.2,77.6-87.2c5.1-5.7,11-10.1,17.2-14.5-4.6-4.7-8.5-9.6-11.3-15.3l-33.9-69.3,16.2-.2,35.3,74.1,69-73.9c6.6-.3,12.7-.3,19.6.2l-63.1,66.6c-6.4,6.8-13.4,12.5-20.9,18,3.4,3.4,7.5,7.5,9.6,12.4l38.3,89.2h-.1Z"/>
<path class="st1" d="M1224.4,635.4H3.4c1.1-5.6,1.9-9.5,3.1-13.9h1220.9l-2.9,13.9h0Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 61 KiB

+68 -18
View File
@@ -1,19 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 2670 860">
<path id="leaf" style="fill:#005616;" d="M2227.4,821.2c-6.1-17.8-18.1-53.6-19.2-53.4-174.7-77.8-159.8-201.2-117.5-304.2,26.3,120.1,235.3,130.3,128,294.1-.7,2,8.8,24.3,17.1,44.9,19.9-45.4,51.3-101.1,48.8-105.7-199.9-357.4,278.8-444.7,350.7-690.2,72.6,220.1,46.5,577.5-330.4,713.3-1.8,1.2-55.6,130-58.5,131.4-.2-1.9-29.1,2.5-26.4-7.6,1.4-6.2,4.2-14.2,7.2-22.4h0v-.2h.2,0ZM2211.7,731.2c42.3-62.9-11.1-105.7-49.8-133.2,71,94,58.1,105.7,49.8,133.2h0Z"/>
<g id="text" style="fill: #fff;">
<path class="st1" d="M654.6,393.2l-.7,137.7h-85.5V188.7h85.4c.4,11.3-.3,21.7,1.3,33.8,23.1-34.1,62.3-50,101.1-38.3,16.5,5,29.6,16.4,39.7,30,34.4,46.5,35.1,134,3.6,182.2-10.1,14.4-22.5,26.9-39,33.4-39.5,15.7-81,1.1-105.9-36.6h0ZM721,362.2c21-26.1,21-82.7-.4-108.4-13.2-15.9-36.4-16.1-49.9-.4-22.2,25.8-21.7,85.3.5,110.1,13.6,15.2,36.6,15,49.7-1.3h.1Z"/>
<path class="st1" d="M164,301l-72.8.7v126.1H3.4V98.1l159.7.5c31.3,0,58.9,13.6,79.4,36.1,30.8,37.6,30.9,91.7.6,129.6-20.1,22.8-47.6,36.5-79,36.8h-.1ZM176.8,199.8c0-20.8-15.1-35-34.7-35l-51,.2v69.5l53.6-.2c18.5,0,32-15.8,32.2-34.5h-.1Z"/>
<polygon class="st1" points="1338.2 427.8 1338 366 1412.4 365.8 1412.5 139.3 1338.1 139.1 1338.1 77.4 1498.1 77.4 1498.1 365.7 1572.3 365.9 1572.5 427.7 1338.2 427.8"/>
<path class="st1" d="M1741.8,364.3c9.1-8.6,14-18.1,17.7-30.3l68.4,13.3c-10.5,45.2-46.5,79.2-92.3,86.7-59.2,9.6-118.7-14.2-138.6-73.7-10.9-32.7-10.7-68.6.6-100.9,17.7-50.6,64.3-80.5,117.1-79.1,76.5,2,113.4,65.4,111.1,136.1h-155.4c-.7,12.5,3,25,9.7,35.9,13.2,21.3,40.9,26.9,61.5,12h.2ZM1749.4,273.1c-2.4-10.8-6.9-18-13.9-24.6-12.8-8.3-30.1-9.5-43.4-1.1-9.3,5.8-14.6,15.1-18,25.7h75.3Z"/>
<path class="st1" d="M1010.3,364.3c9.1-8.5,13.9-18.1,17.7-30.3l68.4,13.3c-10.4,45.2-46.5,79.2-92.3,86.7-59.3,9.6-118.8-14.2-138.7-73.9-10.8-32.3-10.6-67.4.2-99.3,17.3-51.2,64.2-81.8,117.6-80.4,76.6,2,113.5,65.3,111.1,136.1h-155.6c-.2,12.7,3.2,25.1,9.9,35.9,13.2,21.3,40.9,27,61.5,12h.2ZM1018,273.2c-2.4-9.4-6.3-18.5-14.2-24.4-12.3-9.1-30.4-9.4-43.3-1.3-9.3,5.9-14.4,15.1-17.9,25.6h75.4Z"/>
<path class="st1" d="M424.3,376.9c-7.1,13.6-12.5,25.7-23.2,35.5-14.3,13.3-32.6,19.3-52.3,19.4-40.4.2-75.6-23.1-73.6-65.7.9-20.1,9.7-37.2,26.5-49.2,30.5-21.8,55.8-22.4,87.8-40.6,8.1-4.6,18.2-15.3,12.4-22.2s-5-3-8-3.7h-96.3v-61.8h109.6c14.7.6,28.1,2.2,41.7,7.2,23.7,8.8,39.6,29.5,39.8,55.2l.7,90.6c0,13.5,11,23,23.7,23.9l10.1.7v61.3h-29.9c-13.1,0-25.9-3-37.3-8.6-16.9-8.2-26.9-22.2-31.6-42.2h0v.2h-.1ZM364.9,370.1c6.8,5.9,16.2,6.5,24.8,2.7,18.1-7.9,16.5-38.3,16.1-55-3.6,4.3-7.4,9-12.5,11.2l-21.1,9.3c-5.8,2.5-10.6,8-11.8,13s-1,13.8,4.7,18.7h-.2Z"/>
<path class="st1" d="M1943,430.1c-33.5-8.9-68.5-33.6-78.9-68.9l66.6-27.2c11.8,22.1,31.6,42.1,57.2,39.8,4.3-.4,9.3-3.1,11.2-6,7.8-12.5-4.3-24.3-16.2-30.7l-47.3-25.2c-32.2-17.1-57.7-50.7-41.6-87.4,11.9-27,48.1-35,75.3-36h99.2v61.8h-88.6c-2.5.4-6.2,2.3-7,4.2s.7,7,2.7,8.2c31.6,18.6,88.3,38.3,103.8,72,10.4,22.6,6.7,50-9.2,69.1-29.5,35.7-86.1,36.9-127,26.1v.2h-.2,0Z"/>
<path class="st1" d="M1318.2,264.3l-68.5.2c-19.4,0-30.1,10.8-31.6,30.2v133.1h-85.7v-239h85.6l1,58.9,11.9-25.1c14.3-30.5,56.9-36.5,87.4-33.6v75.4h-.1Z"/>
<path class="st1" d="M2232.8,374.2c-26,1.2-44.6-18.4-56.5-40.1l-66.5,27.3c10.8,35.9,46.2,60.4,80.3,69.2h0c10.6,2.6,22,4.5,33.7,5.2,3.2-7.9,6.8-15.6,10.8-23.4,18.5-35.9,44.3-68.4,73.8-98.8-23.6-21.1-62.6-36.7-87-50.6-2.2-1.2-3.6-6.7-2.7-8.7.9-2,4.5-3.5,7.4-3.9h88.2v-61.8h-97.4c-27,.7-63.8,8.2-76.5,34.8-8.3,17.5-6.8,38.5,3.5,54.9,9.3,14.9,22.2,25.8,37.7,33.9l45.8,24.3c11.5,6.1,24.7,17,17.9,30.5-2.1,4.1-7.4,6.5-12.6,7.2h.1Z"/>
<path class="st1" d="M1547.6,801.6h81.2c11.6-.2,23.2-3.8,31.9-11.2,7.3-6.2,11.7-15.4,13.9-24.8l16.8-72.7c-7.2,9-12.8,16.9-20.7,24.2-18.3,16.8-42.3,23.8-66.9,19.5-32.5-5.7-46.7-34.7-47-65.6-.5-44,18.9-93.6,57.6-117.1,18-10.9,39.5-13.9,60-9.6,12.4,2.6,22.1,9.9,29.1,20,5.8,8.4,7.8,17.2,10.8,27.8l10.7-45.4,15.6.3-50.6,219.5c-2.9,12.6-8.9,24.6-18.4,32.9-12,10.4-28.1,15.1-44,15.2l-82.9.2,2.7-13.1h.2ZM1691.8,673.5c12.9-26.3,20.1-60.3,11-88.6-5.1-15.8-17.9-26.5-34.2-28.8-20.7-2.9-40.3,2.9-55.9,16.8-13.6,12.1-23.5,26.7-30.3,43.7-9.8,24.4-14.8,56.5-4.6,81.1,5,12.1,14.7,21.3,27.6,24.7,39,10.3,70.1-16,86.4-49h0Z"/>
<path class="st1" d="M1441.6,556.8c-43.6-8.7-84.4,29.7-93.8,70l-24.8,106.6h-15.7l43.1-186.4,15.6-.2-8.6,39.5c22.3-28.9,53.9-49.3,90.7-42.5,16.8,3.1,29.1,15.6,32.1,32.4,2.1,11.6,1.6,23.4-1.1,35.3l-28.1,122.2h-15.6c0,0,27.5-119.9,27.5-119.9,4.7-20.6,5.9-51.3-21.2-56.7v-.3Z"/>
<path class="st1" d="M1958.9,733.3h-16.2l-38.2-90.1-79.8,90.3-19.3-.2,77.6-87.2c5.1-5.7,11-10.1,17.2-14.5-4.6-4.7-8.5-9.6-11.3-15.3l-33.9-69.3,16.2-.2,35.3,74.1,69-73.9c6.6-.3,12.7-.3,19.6.2l-63.1,66.6c-6.4,6.8-13.4,12.5-20.9,18,3.4,3.4,7.5,7.5,9.6,12.4l38.3,89.2h-.1Z"/>
<path class="st1" d="M1224.4,635.4H3.4c1.1-5.6,1.9-9.5,3.1-13.9h1220.9l-2.9,13.9h0Z"/>
</g>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 2962.2 860.2" style="enable-background:new 0 0 2962.2 860.2;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;stroke:#000000;stroke-miterlimit:10;}
.st1{fill:#17541F;stroke:#000000;stroke-miterlimit:10;}
</style>
<path class="st0" d="M1055.6,639.7v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3v249.6L1055.6,639.7L1055.6,639.7z M1059.1,514.9c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
c16.8,0,30.3-5.9,40.6-17.7C1054,546.9,1059.1,532.3,1059.1,514.9z"/>
<path class="st0" d="M1417.8,398.2c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V390.2h85.3v20.6
c18-20,43.1-30.1,75.4-30.1C1379.2,380.7,1399.5,386.6,1417.8,398.2z M1389.5,514.9c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7S1389.5,532.3,1389.5,514.9z"/>
<path class="st0" d="M1713.6,555.3l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2s37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8c3.6,11.4,10.5,20.7,20.9,28.1
c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1695.8,570.1,1704.9,563.7,1713.6,555.3z M1596.9,486.2h92.9
c-2.1-12.3-7.5-22.1-16.2-29.4s-18.7-11-30.1-11s-21.5,3.7-30.3,11S1599,473.9,1596.9,486.2z"/>
<path class="st0" d="M1908.8,418.4c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7
c-8.4-2.1-15.7-3.1-22-3.1c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V390.2h85.3V418.4L1908.8,418.4z"/>
<path class="st0" d="M2113,258.2v381.5h-85.3V258.2H2113z"/>
<path class="st0" d="M2360.8,555.3l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2343.1,570.1,2352.1,563.7,2360.8,555.3z
M2244.1,486.2h92.9c-2.1-12.3-7.5-22.1-16.2-29.4s-18.7-11-30.1-11s-21.5,3.7-30.3,11C2251.7,464.1,2246.2,473.9,2244.1,486.2z"/>
<path class="st0" d="M2565.9,446.3c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2590.5,448.7,2577.6,446.3,2565.9,446.3z"/>
<path class="st0" d="M2817.3,446.3c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2841.8,448.7,2828.9,446.3,2817.3,446.3z"/>
<g>
<path class="st0" d="M2508,724h60.2v17.3H2508V724z"/>
<path class="st0" d="M2629.2,694.4c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6v-52.6
c0-9.3-1.7-16.2-5.1-20.7c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2620.2,699.4,2624.4,696.4,2629.2,694.4z"/>
<path class="st0" d="M2790.3,833.2c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2s-6.6-11.9-7.1-19.6
h19.6c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9v-24.7
c-3.6,3.4-8,6.2-13.3,8.2c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3c-4-7.3-6-15.5-6-24.6
s2-17.3,6-24.7s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8h19.4v107.8
C2803.2,815.9,2798.9,826.4,2790.3,833.2z M2782.2,755.7c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16c-2.6-4.8-6.1-8.5-10.5-11.1
c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3
c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1C2776.1,764.1,2779.6,760.4,2782.2,755.7z"/>
<path class="st0" d="M2843.5,788.4h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35
L2843.5,788.4z"/>
</g>
<path class="st0" d="M835.8,319.2c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2H647v-135h72.7c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7c11.5-18.9,17.3-39.5,17.3-61.9
C853.1,358.9,847.4,338.1,835.8,319.2z M747,416.6c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C761.1,396.6,756.4,407.7,747,416.6z"/>
<path class="st1" d="M164.7,698.7c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4C82.8,431.4,277,507.1,163.8,641.2
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C89.7,254.7,525,228.6,615.5,17.9c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C157.8,712.6,161.2,705.7,164.7,698.7L164.7,698.7z M160.4,616.9
c44.4-51.4-7.8-139.3-39.2-168C174.3,540.2,170.8,593.3,160.4,616.9L160.4,616.9z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

-11
View File
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1000 1000">
<defs>
<style>
.st0 {
fill: #005616;
}
</style>
</defs>
<path class="st0" d="M341,949.1c-6.9-20.3-20.7-61.2-21.9-61-199.6-88.9-182.5-229.8-134.3-347.5,30,137.2,268.8,148.9,146.2,336-.9,2.2,10,27.8,19.5,51.3,22.7-51.9,58.6-115.5,55.8-120.8C178,398.7,724.9,299,807.1,18.5c83,251.5,53.1,659.8-377.4,814.9-2,1.4-63.5,148.6-66.9,150.2-.2-2.1-33.2,2.9-30.1-8.7,1.6-7,4.8-16.2,8.2-25.6h0v-.2h.1ZM323.1,846.2c48.3-71.9-12.7-120.8-56.9-152.2,81.2,107.4,66.4,120.8,56.9,152.2h0Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 644 B

-11
View File
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1000 1000">
<defs>
<style>
.st0 {
fill: #fff;
}
</style>
</defs>
<path class="st0" d="M341,949.1c-6.9-20.3-20.7-61.2-21.9-61-199.6-88.9-182.5-229.8-134.3-347.5,30,137.2,268.8,148.9,146.2,336-.9,2.2,10,27.8,19.5,51.3,22.7-51.9,58.6-115.5,55.8-120.8C178,398.7,724.9,299,807.1,18.5c83,251.5,53.1,659.8-377.4,814.9-2,1.4-63.5,148.6-66.9,150.2-.2-2.1-33.2,2.9-30.1-8.7,1.6-7,4.8-16.2,8.2-25.6h0v-.2h.1ZM323.1,846.2c48.3-71.9-12.7-120.8-56.9-152.2,81.2,107.4,66.4,120.8,56.9,152.2h0Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 641 B

-24
View File
@@ -1,29 +1,5 @@
# Changelog
## paperless-ngx 2.20.15
### Security
- Resolve [GHSA-96jx-fj7m-qh6x](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-8c6x-pfjq-9gr7)
### Bug Fixes
- Fix: use only allauth login/logout endpoints [@shamoon](https://github.com/shamoon) ([#12639](https://github.com/paperless-ngx/paperless-ngx/pull/12639))
- Fix: correctly scope mail account enumeration [@shamoon](https://github.com/shamoon) ([#12636](https://github.com/paperless-ngx/paperless-ngx/pull/12636))
- Fix: prevent intermediate change event when CustomFieldQueryAtom operator changes type [@ggouzi](https://github.com/ggouzi) ([#12597](https://github.com/paperless-ngx/paperless-ngx/pull/12597))
- Fix: reject invalid requests to API notes endpoint [@ggouzi](https://github.com/ggouzi) ([#12582](https://github.com/paperless-ngx/paperless-ngx/pull/12582))
### All App Changes
<details>
<summary>4 changes</summary>
- Fix: use only allauth login/logout endpoints [@shamoon](https://github.com/shamoon) ([#12639](https://github.com/paperless-ngx/paperless-ngx/pull/12639))
- Fix: correctly scope mail account enumeration [@shamoon](https://github.com/shamoon) ([#12636](https://github.com/paperless-ngx/paperless-ngx/pull/12636))
- Fix: prevent intermediate change event when CustomFieldQueryAtom operator changes type [@ggouzi](https://github.com/ggouzi) ([#12597](https://github.com/paperless-ngx/paperless-ngx/pull/12597))
- Fix: reject invalid requests to API notes endpoint [@ggouzi](https://github.com/ggouzi) ([#12582](https://github.com/paperless-ngx/paperless-ngx/pull/12582))
</details>
## paperless-ngx 2.20.14
### Bug Fixes
+14 -46
View File
@@ -518,25 +518,8 @@ do CORS calls. Set this to your public domain name.
fail2ban with log entries for failed authorization attempts. Value should be
IP address(es).
This setting also controls allauth's
[`ALLAUTH_TRUSTED_PROXY_COUNT`](https://docs.allauth.org/en/latest/account/configuration.html),
which is set to the number of proxies listed here. Without this,
allauth cannot determine the client IP address for rate limiting when
running behind a reverse proxy, resulting in a `403 Forbidden` on login.
Defaults to empty string.
#### [`PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER=<header-name>`](#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER) {#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER}
: Sets allauth's
[`ALLAUTH_TRUSTED_CLIENT_IP_HEADER`](https://docs.allauth.org/en/latest/account/configuration.html).
Use this when your reverse proxy sets a dedicated header for the real
client IP instead of `X-Forwarded-For`, for example `X-Real-IP` (nginx)
or `CF-Connecting-IP` (Cloudflare). When set, this takes precedence over
[`PAPERLESS_TRUSTED_PROXIES`](#PAPERLESS_TRUSTED_PROXIES).
Defaults to none.
#### [`PAPERLESS_FORCE_SCRIPT_NAME=<path>`](#PAPERLESS_FORCE_SCRIPT_NAME) {#PAPERLESS_FORCE_SCRIPT_NAME}
: To host paperless under a subpath url like example.com/paperless you
@@ -2031,64 +2014,49 @@ suggestions. This setting is required to be set to true in order to use the AI f
#### [`PAPERLESS_AI_LLM_EMBEDDING_BACKEND=<str>`](#PAPERLESS_AI_LLM_EMBEDDING_BACKEND) {#PAPERLESS_AI_LLM_EMBEDDING_BACKEND}
: The embedding backend to use for RAG. This can be "openai-like", "huggingface", or
"ollama". The "openai-like" backend uses an OpenAI-compatible embeddings API.
: The embedding backend to use for RAG. This can be either "openai" or "huggingface".
Defaults to None.
#### [`PAPERLESS_AI_LLM_EMBEDDING_MODEL=<str>`](#PAPERLESS_AI_LLM_EMBEDDING_MODEL) {#PAPERLESS_AI_LLM_EMBEDDING_MODEL}
: The model to use for the embedding backend for RAG. This can be set to any of the embedding
models supported by the current embedding backend. If not supplied, defaults to
"text-embedding-3-small" for the OpenAI-compatible backend,
"sentence-transformers/all-MiniLM-L6-v2" for Huggingface, and "embeddinggemma" for Ollama.
Defaults to None.
#### [`PAPERLESS_AI_LLM_EMBEDDING_ENDPOINT=<str>`](#PAPERLESS_AI_LLM_EMBEDDING_ENDPOINT) {#PAPERLESS_AI_LLM_EMBEDDING_ENDPOINT}
: The endpoint / url to use for the embedding backend. If not supplied, embeddings use
`PAPERLESS_AI_LLM_ENDPOINT`.
: The model to use for the embedding backend for RAG. This can be set to any of the embedding models supported by the current embedding backend. If not supplied, defaults to "text-embedding-3-small" for OpenAI and "sentence-transformers/all-MiniLM-L6-v2" for Huggingface.
Defaults to None.
#### [`PAPERLESS_AI_LLM_BACKEND=<str>`](#PAPERLESS_AI_LLM_BACKEND) {#PAPERLESS_AI_LLM_BACKEND}
: The AI backend to use. This can be either "openai-like" or "ollama". If set to "ollama", the AI
features will be run locally on your machine. If set to "openai-like", the AI features will use
an OpenAI-compatible API endpoint, including OpenAI itself and compatible providers. This
setting is required to be set to use the AI features.
: The AI backend to use. This can be either "openai" or "ollama". If set to "ollama", the AI
features will be run locally on your machine. If set to "openai", the AI features will be run
using the OpenAI API. This setting is required to be set to use the AI features.
Defaults to None.
!!! note
Remote AI providers may be paid services. If you use a hosted OpenAI-compatible API, you
are responsible for any usage charges incurred by Paperless-ngx features, and your
document data will be sent to the provider you configure.
The OpenAI API is a paid service. You will need to set up an OpenAI account and
will be charged for usage incurred by Paperless-ngx features and your document data
will (of course) be sent to the OpenAI API. Paperless-ngx does not endorse the use of the
OpenAI API in any way.
Paperless-ngx does not endorse any specific provider. Refer to your provider's terms of
service and privacy policy, and use at your own risk.
Refer to the OpenAI terms of service, and use at your own risk.
#### [`PAPERLESS_AI_LLM_MODEL=<str>`](#PAPERLESS_AI_LLM_MODEL) {#PAPERLESS_AI_LLM_MODEL}
: The model to use for the AI backend, i.e. "gpt-3.5-turbo", "gpt-4" or any of the models supported
by the current backend. If not supplied, defaults to "gpt-3.5-turbo" for the OpenAI-compatible
backend and "llama3.1" for Ollama.
: The model to use for the AI backend, i.e. "gpt-3.5-turbo", "gpt-4" or any of the models supported by the
current backend. If not supplied, defaults to "gpt-3.5-turbo" for OpenAI and "llama3.1" for Ollama.
Defaults to None.
#### [`PAPERLESS_AI_LLM_API_KEY=<str>`](#PAPERLESS_AI_LLM_API_KEY) {#PAPERLESS_AI_LLM_API_KEY}
: The API key to use for the AI backend. This is typically required for the OpenAI-compatible
backend (optional for others).
: The API key to use for the AI backend. This is required for the OpenAI backend (optional for others).
Defaults to None.
#### [`PAPERLESS_AI_LLM_ENDPOINT=<str>`](#PAPERLESS_AI_LLM_ENDPOINT) {#PAPERLESS_AI_LLM_ENDPOINT}
: The endpoint / url to use for the AI backend. This is required for the Ollama backend and may be
used with the OpenAI-compatible backend to target a custom provider or local gateway.
: The endpoint / url to use for the AI backend. This is required for the Ollama backend (optional for others).
Defaults to None.
+1 -1
View File
@@ -4,7 +4,7 @@ title: Home
<div class="grid-left" markdown>
![image](assets/logo_full_black.svg#only-light){.index-logo}
![image](assets/logo_full_eee.svg#only-dark){.index-logo}
![image](assets/logo_full_white.svg#only-dark){.index-logo}
**Paperless-ngx** is a _community-supported_ open-source document management system that transforms your
physical documents into a searchable online archive so you can keep, well, _less paper_.
-22
View File
@@ -1,9 +1,5 @@
# v3 Migration Guide
## Pre-Requisites
Upgrading to Paperless-ngx v3 can only be performed from version 2.20.15. If you are running an older version, please upgrade to v2.20.15 before proceeding with the v3 upgrade.
## Secret Key is Now Required
The `PAPERLESS_SECRET_KEY` environment variable is now required. This is a critical security setting used for cryptographic signing and should be set to a long, random value.
@@ -41,10 +37,6 @@ separating the directory ignore from the file ignore.
| `CONSUMER_IGNORE_PATTERNS` | [`CONSUMER_IGNORE_PATTERNS`](configuration.md#PAPERLESS_CONSUMER_IGNORE_PATTERNS) | **Now regex, not fnmatch**; user patterns are added to (not replacing) default ones |
| _New_ | [`CONSUMER_IGNORE_DIRS`](configuration.md#PAPERLESS_CONSUMER_IGNORE_DIRS) | Additional directories to ignore; user entries are added to (not replacing) defaults |
## Duplicate Handling Changes
Paperless-ngx v3 no longer rejects duplicate documents by default. Instead, it now allows duplicates but adds a way to identify them via the UI. To (re-)enable duplicate rejection, set `PAPERLESS_CONSUMER_DELETE_DUPLICATES=true` in your environment.
## Encryption Support
Document and thumbnail encryption is no longer supported. This was previously deprecated in [paperless-ng 0.9.3](https://github.com/paperless-ngx/paperless-ngx/blob/dev/docs/changelog.md#paperless-ng-093)
@@ -250,12 +242,6 @@ For example:
}
```
## Task History Cleared on Upgrade
The task tracking system has been redesigned in this release. All existing task history records are dropped from the database during the upgrade. Previously completed, failed, or acknowledged tasks will no longer appear in the task list after upgrading.
No user action is required.
## Consume Script Positional Arguments Removed
Pre- and post-consumption scripts no longer receive positional arguments. All information is
@@ -318,11 +304,3 @@ echo "Document ${DOCUMENT_ID} from ${DOCUMENT_CORRESPONDENT} tagged: ${DOCUMENT_
Update any pre- or post-consumption scripts that read `$1`, `$2`, etc. to use the
corresponding environment variables instead. Environment variables have been the preferred
option since v1.8.0.
## Reverse Proxy and Login Rate Limiting
Allauth changed how it determines the client IP address for login rate limiting. Users running
behind a reverse proxy may need to set
[`PAPERLESS_TRUSTED_PROXIES`](configuration.md#PAPERLESS_TRUSTED_PROXIES),
[`PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER`](configuration.md#PAPERLESS_ALLAUTH_TRUSTED_CLIENT_IP_HEADER),
or both, to avoid `403 Forbidden` errors on login.
+8 -15
View File
@@ -302,19 +302,13 @@ Paperless-ngx includes several features that use AI to enhance the document mana
!!! warning
Remember that Paperless-ngx will send document content to the AI provider you have configured,
so consider the privacy implications of using these features, especially if using a remote
model or API provider instead of the default local model.
Remember that Paperless-ngx will send document content to the AI provider you have configured, so consider the privacy implications of using these features, especially if using a remote model (e.g. OpenAI), instead of the default local model.
The AI features work by creating an embedding of the text content and metadata of documents, which is then used for various tasks such as similarity search and question answering. This uses the FAISS vector store.
### AI-Enhanced Suggestions
If enabled, Paperless-ngx can use an AI LLM model to suggest document titles, dates, tags,
correspondents and document types for documents. This feature will always be "opt-in" and does not
disable the existing classifier-based suggestion system. Currently, both remote
(via OpenAI-compatible APIs) and local (via Ollama) models are supported, see
[configuration](configuration.md#ai) for details.
If enabled, Paperless-ngx can use an AI LLM model to suggest document titles, dates, tags, correspondents and document types for documents. This feature will always be "opt-in" and does not disable the existing classifier-based suggestion system. Currently, both remote (via the OpenAI API) and local (via Ollama) models are supported, see [configuration](configuration.md#ai) for details.
### Document Chat
@@ -420,7 +414,7 @@ still have "object-level" permissions.
| SavedView | Add, edit, delete or view Saved Views. |
| ShareLink | Add, delete or view Share Links. |
| StoragePath | Add, edit, delete or view Storage Paths. |
| SystemMonitoring | View the system status dialog, tasks summary and their API endpoints. Admin users also retain system status access. |
| SystemStatus | View the system status dialog and corresponding API endpoint. Admin users also retain system status access. |
| Tag | Add, edit, delete or view Tags. |
| UISettings | Add, edit, delete or view the UI settings that are used by the web app.<br/>:warning: **Users that will access the web UI must be granted at least _View_ permissions.** |
| User | Add, edit, delete or view other user accounts via Settings > Users & Groups and `/api/users/`. These permissions are not needed for users to edit their own profile via "My Profile" or `/api/profile/`. |
@@ -861,14 +855,13 @@ Matching natural date keywords:
```
added:today
modified:yesterday
created:"previous week"
added:"previous month"
modified:"this year"
created:this_week
added:last_month
modified:this_year
```
Supported date keywords: `today`, `yesterday`, `previous week`,
`this month`, `previous month`, `this year`, `previous year`,
`previous quarter`.
Supported date keywords: `today`, `yesterday`, `this_week`, `last_week`,
`this_month`, `last_month`, `this_year`, `last_year`.
#### Searching custom fields
+2 -16
View File
@@ -30,25 +30,11 @@
"**/.idea": true,
"**/.venv": true,
"**/.coverage": true,
"**/coverage.json": true,
"htmlcov/": true,
"coverage.xml": true,
"junit.xml": true
"**/coverage.json": true
},
"python.languageServer": "Pylance",
"python.defaultInterpreterPath": "${workspaceFolder:paperless-ngx}/.venv/bin/python3",
"python.analysis.extraPaths": ["${workspaceFolder:paperless-ngx}/src"],
"python.defaultInterpreterPath": ".venv/bin/python3",
"python.analysis.inlayHints.pytestParameters": true,
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports.ruff": "explicit"
}
}
},
"extensions": {
"recommendations": ["ms-python.python", "charliermarsh.ruff", "editorconfig.editorconfig"],
+21 -25
View File
@@ -1,6 +1,6 @@
[project]
name = "paperless-ngx"
version = "3.0.0"
version = "2.20.14"
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
readme = "README.md"
requires-python = ">=3.11"
@@ -25,9 +25,10 @@ dependencies = [
# WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes.
"django~=5.2.13",
"django-allauth[mfa,socialaccount]~=65.16.0",
"django-allauth[mfa,socialaccount]~=65.15.0",
"django-auditlog~=3.4.1",
"django-cachalot~=2.9.0",
"django-celery-results~=2.6.0",
"django-compression-middleware~=0.5.0",
"django-cors-headers~=4.9.0",
"django-extensions~=4.1",
@@ -36,31 +37,30 @@ dependencies = [
"django-multiselectfield~=1.0.1",
"django-rich~=2.2.0",
"django-soft-delete~=1.0.18",
"django-treenode>=0.24",
"django-treenode>=0.23.2",
"djangorestframework~=3.16",
"djangorestframework-guardian~=0.4.0",
"drf-spectacular~=0.28",
"drf-spectacular-sidecar~=2026.5.1",
"drf-spectacular-sidecar~=2026.4.1",
"drf-writable-nested~=0.7.1",
"faiss-cpu>=1.10",
"filelock~=3.29.0",
"filelock~=3.25.2",
"flower~=2.0.1",
"gotenberg-client~=0.14.0",
"httpx-oauth~=0.16",
"ijson>=3.2",
"imap-tools~=1.12.1",
"imap-tools~=1.11.0",
"jinja2~=3.1.5",
"langdetect~=1.0.9",
"llama-index-core>=0.14.21",
"llama-index-core>=0.14.12",
"llama-index-embeddings-huggingface>=0.6.1",
"llama-index-embeddings-ollama>=0.9",
"llama-index-embeddings-openai-like>=0.2.2",
"llama-index-embeddings-openai>=0.5.1",
"llama-index-llms-ollama>=0.9.1",
"llama-index-llms-openai-like>=0.7.1",
"llama-index-llms-openai>=0.6.13",
"llama-index-vector-stores-faiss>=0.5.2",
"nltk~=3.9.1",
"ocrmypdf~=17.4.2",
"openai>=2.32",
"ocrmypdf~=17.4.0",
"openai>=1.76",
"pathvalidate~=3.3.1",
"pdf2image~=1.17.0",
"python-dateutil~=2.9.0",
@@ -68,13 +68,13 @@ dependencies = [
"python-gnupg~=0.5.4",
"python-ipware~=3.0.0",
"python-magic~=0.4.27",
"rapidfuzz~=3.14.5",
"rapidfuzz~=3.14.0",
"redis[hiredis]~=5.2.1",
"regex>=2026.4.4",
"regex>=2025.9.18",
"scikit-learn~=1.8.0",
"sentence-transformers>=5.4.1",
"sentence-transformers>=4.1",
"setproctitle~=1.3.4",
"tantivy~=0.26.0",
"tantivy>=0.25.1",
"tika-client~=0.11.0",
"torch~=2.11.0",
"watchfiles>=1.1.1",
@@ -102,16 +102,16 @@ dev = [
{ include-group = "testing" },
]
docs = [
"zensical>=0.0.36",
"zensical>=0.0.21",
]
lint = [
"prek~=0.3.10",
"ruff~=0.15.12",
"prek~=0.3.0",
"ruff~=0.15.0",
]
testing = [
"daphne",
"factory-boy~=3.3.1",
"faker~=40.15.0",
"faker~=40.12.0",
"imagehash",
"pytest~=9.0.3",
"pytest-cov~=7.1.0",
@@ -144,8 +144,7 @@ typing = [
"types-python-dateutil",
"types-pytz",
"types-redis",
"types-regex",
"types-setuptools"
"types-setuptools",
]
[tool.uv]
@@ -180,8 +179,6 @@ respect-gitignore = true
fix = true
show-fixes = true
output-format = "grouped"
[tool.ruff.format]
line-ending = "lf"
[tool.ruff.lint]
# https://docs.astral.sh/ruff/rules/
extend-select = [
@@ -315,7 +312,6 @@ markers = [
"date_parsing: Tests which cover date parsing from content or filename",
"management: Tests which cover management commands/functionality",
"search: Tests for the Tantivy search backend",
"api: Tests for REST API endpoints",
]
[tool.pytest_env]
+16
View File
@@ -0,0 +1,16 @@
9w
{@@N
Q@@@@H
G@@@@@@@\
SilN@@@@@@@
*Q *@@@@@@@@S /= = = = = = = = = = = = = = = = = =\
*@ B@@@@@@@@N || ||
N R$ A@@@@@@@@@@ || PAPERLESS-NGX ||
x@@ $U B@@@@@@@@@R || ||
N@@N^ @ N@@@@@@@@@* \= = = = = = = = = = = = = = = = = =/
|@@@u @ E@@@@@@@@l
Q@@@ \ Px@@@@@@P
1@@S` @@@o'
z$ ;
v
/
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,329 @@
%PDF-1.6 %âãÏÓ
3 0 obj <</Metadata 7 0 R/OCProperties<</D<</ON[8 0 R 22 0 R]/Order 23 0 R/RBGroups[]>>/OCGs[8 0 R 22 0 R]>>/Pages 4 0 R/Type/Catalog>> endobj 7 0 obj <</Length 8109/Subtype/XML/Type/Metadata>>stream
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 7.1-c000 79.a8731b9, 2021/09/09-00:37:38 ">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:xmpGImg="http://ns.adobe.com/xap/1.0/g/img/"
xmlns:pdf="http://ns.adobe.com/pdf/1.3/"
xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/"
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#"
xmlns:xmpG="http://ns.adobe.com/xap/1.0/g/"
xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:illustrator="http://ns.adobe.com/illustrator/1.0/">
<xmp:CreateDate>2018-12-29T21:47:38Z</xmp:CreateDate>
<xmp:CreatorTool>Chromium</xmp:CreatorTool>
<xmp:ModifyDate>2022-02-26T20:11:14-08:00</xmp:ModifyDate>
<xmp:MetadataDate>2022-02-26T20:11:14-08:00</xmp:MetadataDate>
<xmp:Thumbnails>
<rdf:Alt>
<rdf:li rdf:parseType="Resource">
<xmpGImg:width>256</xmpGImg:width>
<xmpGImg:height>76</xmpGImg:height>
<xmpGImg:format>JPEG</xmpGImg:format>
<xmpGImg:image>/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA&#xA;AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK&#xA;DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f&#xA;Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgATAEAAwER&#xA;AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA&#xA;AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB&#xA;UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE&#xA;1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ&#xA;qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy&#xA;obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp&#xA;0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo&#xA;+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYqlnmXzJ&#xA;pHlvR7jV9VmENpAP9k7n7MaD9pm7DBKQAssZSAFl5x+RvnjUfN2r+br+8+BWmtJLaCtVijZZUVB8&#xA;liFT3O+VYp8RLVhnxEvWsub3Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FWG+f/zU8seTLdlvJfrWqMKwaZCw9U1Gxc7iNfdvoByueQRa55BF8t+evzB8&#xA;w+c9T+t6pLxgjJFpYx1EMKn+Ud2PdjufltmJOZlzcOczI7vVP+cVYSbjzJNX4VSzSnuxmP8Axrl2&#xA;n6t2m6voLMlynYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FVk88FvBJPPIsMESl5ZZCFRVUVLMx2AA74q8A/Mv/nIqV2l0ryYeEYqkusuPiPY/V0Ybf67fQB1z&#xA;GyZugcXJn6B4TcXFxczyXFxK808rF5ZZGLuzHclmNSScxnGU8VfTH/OMOlNB5R1LUXFDfXnBPdII&#xA;wAf+CkYZl4Bs5enGz2TL3IdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirs&#xA;VdirsVYj5X/MGw13zf5i8vRFRJosiJDv8UqgcJ2/55zfD92QjOyQ1xnZIZdk2x8v/np+a1zr2qTe&#xA;XNJmMeh2MhjuHQ0+tTIaMWI6xow+EdD9rwpiZclmhycPNks0OTyPKGh2Kr4YZZ5khhQySysEjjUV&#xA;ZmY0AA8ScVfbnkXy2nlryjpeiinqWkAE5HQzPV5SPnIxzYQjQp2MI0KT3JMnYq7FXYq7FXYq7FXY&#xA;q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXw9oPm/WdE8zx+Y7OWl+szTSg14SCQ1kj&#xA;cd1etD/XNeJEG3XRkQbfUt9+Ytlqv5U6n5q0ZykqWcw9Ov7y3uuPHi3TdGYH3G/fMwzuNhzDkuNh&#xA;8f5guC7FXYq9q/5x5/LaXUNTTzdqUVNPsWP6NRx/e3A29QA/sxdj/N8jmRhhe7kYMdmy+ksynLdi&#xA;rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdir4a836BP5f8AM2pa&#xA;NOpVrOd40r+1HWsbivZkIYZr5CjTrpRo0y3ytqd6dCuLK3J9DzFYXmn3cP8ANe6dCLi3kUdjJG6R&#xA;e5qcnE7e9nE7e953lTU7FXrv5W/kPquuzQ6r5kiew0RSHS1eqXFyOwpsY4z3Y7n9nryF+PCTuW/H&#xA;hJ3PJ9NWlpbWltFa2sSwW0CiOGGMBURFFAqgdAMywHMAVcVdirsVdirsVdirsVdirsVdirsVdirs&#xA;VdirsVdirsVdirsVdirsVdirsVdirsVdiryr86/yjfzbbprOjKq69aJwaI0UXMQ3Ccugdf2SevQ9&#xA;qU5cfFuObRlxcW45vL/ym0y40i+vNc8xwvZaP5Raa5uopk4yPezxCGKDi1DypuPeleuU4xW56NOI&#xA;VueiJ8n/APOP2q+Z7WLXbq6j0XStQLT2tmqNNOsLMSgoxRQCv2TyO29MMcJO6Y4Cd3snk78mvI3l&#xA;Z0uLa0N7qKUIvrykrqw7otAifNVr75fHEA5EcUYs4yxsdirsVdirsVdirsVdirsVdirsVdirsVdi&#xA;rsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVeRedfy/wBa8z/mjBbyQyxeUJYra81aQlfSnntP&#xA;URUWhrUpIqUNNqnsMolAmXk0TgTLyeuIiIioihUUBVVRQADYAAZe3t4q7FXYq7FXYq7FXYq7FXYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq//2Q==</xmpGImg:image>
</rdf:li>
</rdf:Alt>
</xmp:Thumbnails>
<pdf:Producer>Skia/PDF m64</pdf:Producer>
<xmpTPg:NPages>1</xmpTPg:NPages>
<xmpTPg:HasVisibleTransparency>False</xmpTPg:HasVisibleTransparency>
<xmpTPg:HasVisibleOverprint>False</xmpTPg:HasVisibleOverprint>
<xmpTPg:MaxPageSize rdf:parseType="Resource">
<stDim:w>2409.000000</stDim:w>
<stDim:h>909.000000</stDim:h>
<stDim:unit>Pixels</stDim:unit>
</xmpTPg:MaxPageSize>
<xmpTPg:PlateNames>
<rdf:Seq>
<rdf:li>Cyan</rdf:li>
<rdf:li>Magenta</rdf:li>
<rdf:li>Yellow</rdf:li>
<rdf:li>Black</rdf:li>
</rdf:Seq>
</xmpTPg:PlateNames>
<xmpTPg:SwatchGroups>
<rdf:Seq>
<rdf:li rdf:parseType="Resource">
<xmpG:groupName>Default Swatch Group</xmpG:groupName>
<xmpG:groupType>0</xmpG:groupType>
</rdf:li>
</rdf:Seq>
</xmpTPg:SwatchGroups>
<xmpMM:InstanceID>uuid:e5f59418-0be8-dd42-a564-bc1f41615750</xmpMM:InstanceID>
<xmpMM:RenditionClass>proof:pdf</xmpMM:RenditionClass>
<xmpMM:DocumentID>uuid:c2483dfa-3a53-3149-80a7-6822614a9dee</xmpMM:DocumentID>
<dc:format>application/pdf</dc:format>
<illustrator:CreatorSubTool>Adobe Illustrator</illustrator:CreatorSubTool>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?> endstream endobj 4 0 obj <</Count 1/Kids[5 0 R]/Type/Pages>> endobj 5 0 obj <</ArtBox[152.941 154.947 2262.04 755.845]/BleedBox[0.0 0.0 2409.0 909.0]/Contents 24 0 R/CropBox[0.0 0.0 2409.0 909.0]/LastModified(D:20220226201114-08'00')/MediaBox[0 0 2409 909]/Parent 4 0 R/PieceInfo<</Illustrator 25 0 R>>/Resources<</ExtGState<</GS0 26 0 R>>/Properties<</MC0 22 0 R>>>>/Thumb 27 0 R/TrimBox[0.0 0.0 2409.0 909.0]/Type/Page>> endobj 24 0 obj <</Filter/FlateDecode/Length 3540>>stream
H‰ì—ÍŽ¹„ïýõU"™ü½Z|2 Ã?ÀÀ»{Øõûþ"ɪîI¼;ö° i²ø“ÌŒˆÌüð·Û‡¿~ ÛŸþüq»ý| ÛcK™ÿvýúå_·nÿ¾ÅMÿ~ùñöá/ÿÛÿ¹ýÌ0ð/n}ԣƴÙÈG¨£l/Ÿnú¢ÿc:j­ÛnGëeKv¤·=Æ#uÛ,?/·=v¾o9%—mOý¨ŒòÑjÛvf»ÕsÈâ’Ž1Òõ½¦£Ç¡Í¹¥moáH9?œÝ& ¿{ض÷xX—ak´rýÞÏokç~š½ŽÞïg¯»÷Óòbx€¿olßïÆëm¡ÝWÌ—ï§õË3WLÏíËüpYÜæŸ8‘«»butŒûtM´ÂÄØ^ñn9d+²®6;†ž3ÚÑ
Æ—vd³k,—q©õûŠÞÒ5¶£h>†8ßÕË8Jä’ÐqN«G Yf#Ëá5ÏD“sø’ÇÑkòM<»š<š¡=xÅru¯X~œGh)k†¹¾ÆºûsÝÂ%Íß";
þ’Ñ=\ŽXÚ†Ùâý-]&t‡¯eã±Yn[ÎÀµ·Ë[k¨Mwžß9¾Qw6öÍ`‹ÖªÇßÇ|m-›2çÏÆŠÕˆMÁ»‡wNütûáö÷öÅh툱~“ìzæ_á„ Ôô ,©_ ¬ãÅžh¸äèÊü“ê<ÐOÛ.ú­sŸé§‹ïô›vío ¦Ÿ^öH?=ü‘òÌ3ÿ›¯ò/á<Ÿn \Ü’€Ú‚^áL¹i!“P^À­F£`Â]°?Ç|Θ¯­‰wœ‹v|'8²˜']^|_ÌtƒïÌm\8îÔÝãù<»‡'mÈD%–ûŠ*q™uÕ‘vßÕGÑ_3‘4aèöëä}Ó­_˜»¨pnülb‘‰ î.H æiÚÉÄeû©…r¢öÿ]aDò¶Ç ¾¼%XêÚHŒrdE…ÕÁ*g§ÌÏ×[­„hl_ `F,B|Rä.¹\ȶ%Òf†tXÿú,tÑ-ÉÕÇ­ øk'² X;
u‹à]Ê{#ÔpsD™¬ë‰{!“Á"Nr
õ
T;ïˆÁt
ekìiÒdq>Ú8’¼}Ž_n+‡ø²øwPX´‘†&‚üà˜0ÖHÖ‰…%îÊäQ TVÝ¢“¾“qûð@^©¸àå4¡%¸¥Ì¡é]Ië=]3~å^a £ÁÝèçg’ÄJÆB5ÎÖ0;°¯‘\É‚).qf;ÆckÈŽŸév5^©ø±sˆIâàzŽk÷³ÑpQÇ8DZ%ËŠÁWx[ -6Ljz||š‚=¦ŠG^ÇúØ-J…†,%y›Ðò´kÌß<Jüš4™ªÂÃ@ËrÐÖü¿óþŠ›y`’·«ÞžÐXåyðnÜ.OÅîâpå Ž5œXíš(诈q2Ø‚ÀSÚÊ&wÉ¥1{¼ZÂQ㈭yØž~ßϘZ”ÛU¢e…_B~1µ4¤ã( ·á`ÐÎýØŽátóÒ“7FÈcªkA Xû3ޝçÈ#«d³Æà3@Ièr}V
F “Ôa½9xIéÉëØ¡ZŠaGx°¤9”v`R#þýšÀR³Š‹z„d79LKžùTôt¥+\7†Ò•
™¢ì VApﮡàcÄ/Ma}I*9ÂãÏ=\EH¬¼³Éæ ]¶k'©»/z‡ãCÂÀèòm% peJ—”Tpž¢ö›•´<)iùº’Bõ ¤ý%ý JjßIIíû)i{£¤í;)©ýO%Ío•´þ_Jê”äØìB*–KH‘ão éP]$™É¨Sù¨Tˆ)ÔUgRBÜÎ.“I¿_µj¢À—"¿$d'|í>Zò¥Êüœa½÷‚T1ÝËGpe>]­¬j.Ì“Œf¥¼ãÓüÏ2þkb]QÁ¾[¡[쎥1Èdî^®-…jTá.É Žà⤂}  qV®Ñå<ú–¾œÛ9\Ô“©lë¨Ò9dÕ`7Ëׄô¡)) #b5O‰¨B¬i^Ì+šËtW­³ÔЄÜ(E*^5|„Àj¾å}^ÖâÃS¥í¢Ê¿z³dòQi*«Eõ>hDƒ/:~Œj.mf>1µN[x»/T_ œéÏðD”]¡ÏþU.Ò'zÕT« ªÓ•úD,­WtaŸv¶kBš­žo+¥ç5ì$tóôyNdÏÌî—t5¡ë,ìO®DÖ¼óÌ’8äæ­LîzÒÌgÖšš²§Áî4xy ìêw¡fÞKé“VÄÙyrsqè}øÆ®/8ªw*^"H[JùY+Ám’Èœc¸g³=[3@TË!#aâÍC[’¬B˜½œ÷‰KE–R™?ý¾F¸:‚frÂOÎ*ϼté±ÍÖ¶™·¶
©‡ƒd2sôpƒý«7óÚ(†I÷½Þ8«9¶)) ¦5Ï„8_³ÔBC¢0;Œb,˜‘"ir}ØfŽ~–´IÙˆã7%­ý!i_”´ü,iù­¤åß«¤…º$­M
_#Q“ ~­¨¥w5%ûwÓ´þžšÖÞ_ÓâïUÓâ¯Ô´þŽ’Ö…SÒ«'I£‚³Ä e5-I•vOŽß£0ý¦çJA %/HÞ“J˜NÙ£fÞÝÐáuN^;¢4•«¯ŽÙOY<‡ÞD9~ð dU¸·žüPþ[Xç™ÙUNI¹†IÎŸÏ ’éü¨›m•°o'L"RÏ Ž ·¨N‹Ñ§~N’¡;n‚ÛÃÅÁôWXBÓ‘¶Æ™÷Ñ‹ªvo±‹ÊøÀ’ïÿôú„ÉYŒŸ XÖî#ìÕI²&x™š"EZò>ŸÁ£A˜Y[Þ I59¸$ÕiŸº0uo¸RÖðǾ’þ(õI:DÐ{Le<‘dÇõ¼м!x”–ÝA=ÎàDg5.¯I®Hy?×ðA.Ò ¸(fŒ©å³‚ªæÚàrJ£·²dCÅL L¿²¢†qJgbIcWœÝ%d Éd³zªì:ü;ö¶8¶sæÒ¸VÏÃbS¢—ܰèg‡êÈ?Ý‘],g'&Sò˜©>Î ]Fw°$ݵLßWÊÖK³ûÒgÚ¤ÈX`ÓÁ«ID"‹ÃEJ'‘õÓquô[Ê+¶PÛIΙäB³’%ìkoýä¢ÉÀêã¿8Ø<ch¦‹Q}ñ Í›GáARýÀÛÁ „–WªEì†ÊT3‹›ˆÓhó”c2uõL_dd÷½ÍqQ‰A,Zð¤©|žìšyõ¸ôu[Ô-ØÕ”ˆ¹ÍñÃRóŽ”ÏÐèíêlcñØ%2ìÊžå…mó±E÷ºÐôV­H3Êžé,g~R­†#¼ZÙ>OîÅ!:;ÈP„n I æ&øzszV7¡ „»¢3¦”ÈXŽªÅY>F=:ûê˜ecô‚ ¬ÔV^Òilc=Þ$^É$¯K’l•Rjþ’dI€ÇäNÉôòL‰JY˜š¤TYÀÅÝqLU¥Žaæzê¢HŽ$ô­ú(D/T›Û¯± î¿d—=rÛ0F{ŸÂÐ  YgRú*â"irÿ"ï-@HŠ{R$±ûí÷ã„6§©á¯g3[ý­~¢»i<o¯ÀƒÎfû5Ë266¢M‘³)t+
b÷ó¸ÚbJRT8kÝÊÛÚ®éÝb£Ø¦©«vAaÀùuôÞiVË&1Vµé9cDåJÞ {µ5pÁÇk’‡Uâ:/~¬fŠàh½¸k6y]gÓó¯™eM†~«=<Ç0>ñcí̵{4 UoÞüýßqz?5[ø§.í ²užIà&)ýTù¹ êTçR¹8µ/oð{í4†aý?ÁÏå8FŒRÏù>0%Ý'ó2pÿÞJ{€3J/nV†›•˜æšr7P?H™öA«ßÝNœº»Pþ¹ª‡Ïîm´€Ž"Fàr‹§ÇOØdš«e»겆Aš…¾Öf=*V;J<}ç]˘ä8x3ÊXá¼E®œÔÀÊ:G8û‘hÐñ`×¼©&gT<Õ‘ŸÖV7öÙ…}pËÛµH-Z3?þy~ØÖÆ51¨EjWeœ÷œìY63Ó»YSn£Ö³ä·i3ï¼?Bþ›:[@Å9L7¡u ´ÖÝ
̵^CA†I|T“0Êçk'Çœ&ã*Tü›)þß_ϳËsæÒójoNÙ℃Íá
ú.¸(‚ç¤êåy(Å@™7mÚ̘ʶPuÚÚªÚ­œ”P°¶åJçUÒÙÃgéˆ^À’}æÉ–t9jËjHÉ5t]î!:>H|H>@‰êDë8p€s:¿â‰ÜlSÀ¬Ì  ÷¨/^6“p=ëR [B°Ú ëIžfŽÝ-Љµ+‡®ªÆ\q²9EÂÊ$NÅ“ƒ:=á
aǸÇy§Ñ±œÝVö$8 (fóÉ~†/ç{²'<RdN87¸.O9ƒÛºû¶ÅWÜvås˜ÙvÁÖüS ²^ÚxŸÔùóëÇ'ÿþ 0B¸| endstream endobj 27 0 obj <</BitsPerComponent 8/ColorSpace 28 0 R/Filter[/ASCII85Decode/FlateDecode]/Height 39/Length 181/Width 105>>stream
8;Z]!\Ij?G$j8@t/k5dUU3<am*`9`037'CAm[R>[,!)9)1.3+''h?`o>gmB[ET8ck
@lE>-DRC'5>'BJ23HlQilI&Ga&7If\2VcDkpR`P&Ag+(rGsf,$]V4,Oi!oYPO?6G/
Ye+**Y]%)+Lu]7C/1+obTM<jR!k7bhp#"6_FO&2i!:ndgO8~> endstream endobj 28 0 obj [/Indexed/DeviceRGB 255 29 0 R] endobj 29 0 obj <</Filter[/ASCII85Decode/FlateDecode]/Length 428>>stream
8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0
b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup`
E1r!/,*0[*9.aFIR2&b-C#s<Xl5FH@[<=!#6V)uDBXnIr.F>oRZ7Dl%MLY\.?d>Mn
6%Q2oYfNRF$$+ON<+]RUJmC0I<jlL.oXisZ;SYU[/7#<&37rclQKqeJe#,UF7Rgb1
VNWFKf>nDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j<etJICj7e7nPMb=O6S7UOH<
PO7r\I.Hu&e0d&E<.')fERr/l+*W,)q^D*ai5<uuLX.7g/>$XKrcYp0n+Xl_nU*O(
l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 22 0 obj <</Intent 30 0 R/Name(Layer 1)/Type/OCG/Usage 31 0 R>> endobj 30 0 obj [/View/Design] endobj 31 0 obj <</CreatorInfo<</Creator(Adobe Illustrator 26.0)/Subtype/Artwork>>>> endobj 26 0 obj <</AIS false/BM/Normal/CA 1.0/OP false/OPM 1/SA true/SMask/None/Type/ExtGState/ca 1.0/op false>> endobj 25 0 obj <</LastModified(D:20220226201114-08'00')/Private 32 0 R>> endobj 32 0 obj <</AIMetaData 33 0 R/AIPDFPrivateData1 34 0 R/ContainerVersion 12/CreatorVersion 26/RoundtripStreamType 2/RoundtripVersion 26>> endobj 33 0 obj <</Length 1444>>stream
%!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 24.0 %%AI8_CreatorVersion: 26.0.3 %%For: (Michael Shamoon) () %%Title: (White logo - no background.pdf) %%CreationDate: 2/26/22 8:11 PM %%Canvassize: 16383 %%BoundingBox: 152 154 2263 756 %%HiResBoundingBox: 152.941359391029 154.946950299891 2262.04187549133 755.845102922764 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 14.0 %AI12_BuildNumber: 778 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 0 0 2409 909 %AI3_TemplateBox: 1203.5 454.5 1203.5 454.5 %AI3_TileBox: 826.5 166.5 1560.5 742.5 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI24_LargeCanvasScale: 1 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI17_Begin_Content_if_version_gt:24 4 %AI10_OpenToVie: -2651 3020 0.25059563884769 0 7787.44597860344 8164.54751330906 2548 1389 18 0 0 6 45 0 0 0 1 1 0 1 1 0 1 %AI17_Alternate_Content %AI9_OpenToView: -2651 3020 0.25059563884769 2548 1389 18 0 0 6 45 0 0 0 1 1 0 1 1 0 1 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 7 %AI17_Begin_Content_if_version_gt:24 4 %AI17_Alternate_Content %AI17_End_Versioned_Content %%PageOrigin:704 -46 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 34 0 obj <</Length 33953>>stream
%AI24_ZStandard_Data(µ/ýX<ž¿EZ-D¤™æ£Ù¥¶[ã*DÞJï§ënYÉwzOê°@Yþ= )œàr²
}€h>@>@$F"ôãr.)™ Kˆj…Ãq
E`ð è‡YXZª>Tü!ò¹T Õ¸A×À”ŸQìt€h4îRØdBFs2Êš´µdˆ:-¢Hæd”
â€àd”ÙÅŠKÑ™‡c4(Vš¦X‰b'S6 
“y8p+cÁa3á.±€_&ÔAõ!êl&\âeÑñáÀºPÀêpM´ ,0O´ˆbǵ@U»Œˆ:‰•Pm2ŸƒMK¢€Dv´X$+Q@ƒI±H"H¦”( Š £°:­Ïš€@ìHE;ٕΤ‚‚¡€G*ŠˆŒÃf4À±ŒÒÙŒv¤‚À*X—N«³Ù„É,¤€¸¤¤Èô¡ð0!i™],  ±A"˜ˆ"+2‰b'ƒÒpâ`"KDÂŽ
H*2‰@bƒ´A—ÖÀ à>)¹É® D‚Ë¢d¦€x!Ù)“`Ñp°/&‘¥³ ±TuY”•f µHÊñhiùÀRAl²ËDÔÙp@#j@!âUÂ5€…¥eD‰AK†¨³É®—¡(„t6ê°‰ø‰0Àˆ:­„ÎFÃ@s"Rél"² %Z%„¨\2xT2 )‰]«ÑÆAe±z„ c“ñ’‘Àép° m…“]‚BlF*‹„,¢ÓÙhˆX$š] ¢Ø!e'£ÌðÅÎAFgb© ²kJÉ“e>¤•AFg£±±9HP}è0eVˆ ê%ÔÙì«XVo (|6Ù•¡©i†ìZHX Ð0aàTÁHDà"j!mZÑ* ::T¡Ü–Ð+¤·-»F^ž ÁÛha°qzò²d D + =L```Š
¢Vv,NÁã˜B !­ÂȣİÉ.…Œ†ƒØ24 +bÀƒ ”†‡ÀB@J;ÛöÒÀɃ´à@­<˜ ³É.æhN®8¼³ÙPp >Ÿ’ƒø8" Ž:BŸƒç!SL§a£NüQ:%hÄ‡à‚“]Á­j= Xx(¾¤œ»Iˆ“+Íã¨ìCàÀ”ÆFBc<ˆ88 ¬xD'Ü'%³Ë¥Ñ±9Cf¡¡!"G¾ )XŒGv"» 4 O„ˆ„Æ %UÊ@:½™3ht6YI'ÄÒ’]ûÁ2Ze´
|IvÒ°]J6`úHqL x, hJ`8m<¢ËA
ËCÀÈ`G(¥  ”ŠuHm*J"ë)!­Àì:Ù@X8² ‘ñ YXZv˜(|ïµÙgc€!x ‚®d×Kv2Ê•ÕYP•xŒü™´ì8ÑTÙ5‚NcÁb¡€³0*ͳm™B!m[F@vQ|
øä¢W +®@¡o`İi}$GsÓñ`mÙ•€ ¡!«!@ÁlZ *Ö¶5˜²ÃdB \
˜¼lÙ•ÀÑœô, œdÅÈ(Ó9XR…ˆf5Œbò&/›Ë‚)#H0Ù5 p'ZNé‚Ùpd[JAÂ$³‹aÄhY¸P@±HŽ„LR »$X0HHH4JL":
‰Ñ™|¼‰ÕGƒ€‡V)"€r¢±Â¡’:øÈhD\h#Š G*È®d86.¶ .ÙI,T2±<Á#‰¨ÈÕ«´ÑTá+R€Hˆe$b¤¡/FÅq‚Vv…|F;ð¤hàã$A”¤þX¤X2‚Ç⸠I(³ c[6
^%4p1”!!-Ò 4+ž]Ò9`%T¬€tÉg°6 ¬MƒpÀ‘Âydq\v]t(”pTLn² Ù@ù ÙH² %úŽÁƒƒŒNÁɦjS´"¨áhNNlidó`1l[vYtÅÄA…Š@”'ŒlaÈ. ˆŽPŽ„tLa€O€C†rd£%SàN^¶ìZ©
XX<Yì‘€¬-»V´ñP>-’ídca =-»VÞFCJ L‰°°AV†£9iÒl ,†mcU œ„¼¬Z°–ÕÙe€u6/°‚] l+o£á‚¥‚xNž@½m¬Š 'V%m Ù?Œô䃢- ΀ÍSÒ­E,<¤•H7 Tüa†¢a“]ïbƒ´ Yu "P<Z `;(w6+PˆÉ£ÀqU ’…ì
e‹óÀñ=X´\„pJ,`”ÞÈcñbqñ‚qضf2ÊÎ †CŸ&"RrB"ãaBÂ…åAQ´¬€0­çUœŒ‡÷¼ @Œô‚]IE,X$]¬Žòè(Áð`FD)Ÿ-$5a&wAC@c|2NFÙ2áBiãJpx x޽8 °²qe >òŒ<$r‰@AùKh™)€±á ©Y2Eàqp2NNüÐâ!Ò%Œdp"-=¼Ä-Ī€Y]šÆSïAPÒæ„@%Aòá0hÑ@8¨ + P´È Y Ââ<lh"D'qä,Žcygã0q2JSJÅ3á™Ë‚Ê&#Y)œˆ ø`¸ðdב’XŒŠ‡ÅËzH+›–Âce2;!™"5lŒXD1Ñêp`Ðk±á$·V6 '9(?ñÎ&»0ͤ*i ‡[À®V´U CêTaJ‘°VJ;™H›èÓ
22ÃiEúDè§”S+¥™$L)ÕGâÓ"¡¨¶’'UR'Õ©EÚh¥T¡–ÆByáxyj±4ОP¬T)%‘Á²©ÐÚ<-ª-ô°l$MkUJeÑZj[)©R*HhÒ&z¤ M©N
õRO
…:R(-/+Z‹´i)Mtú¨P<š
©E"hÒ&ÚÕP§ŽçKʨX­H[Çê´•XÈ¥ J±¤^0Ö)E…i¬ EJR‹„­”^XXé„Z­HXëE5 …‘N˜V"¡V˜ª…™4-…½¨ØTX KH**©SëdÒ°”
KP+ìE5€Æby`.ÊH0 ÓP§,EÂTR'H,,¥­ca˜¶Ò´„i,m¥#9À°”
3i,„Jh*¤ (£UÂíô¢ h±H!ˆM…Ê–ÓÖ±qªØH¦©Åae¬Jx´µy>ÊbÙH*ÚF‡I”¡*áùd<EU@ÓP%Hc½h(”ª”:m(ͤj1­VÕb¡Z¬Õ
Õbm«•Ém+Õß­Â{Qß'Bƒm°2´”ZJºÒR¹­•:¹–Ò¾˜X+•[ ´R¥R$Ò©•"¹U´Öi«–[ ”zIq¬Í´­ Z±6ŠªÀê„*¹UZZ'É™¶’6ÕÊê„Jq¦­•ÒN,%
Åm¨ÒŠÊ¦r¦­”R9±4“CQ@QµR§MKP)šÉ±´¨¬ËÖ©…¢*pJ ÅÚT)©“¦BZ¥IÓP( ű´¨¤T­‹¥m«U‰¤¢¥kEZµ¨JŠr¬Õ¦™H+,ÇZÀm«
…”Ò@Z¹"¡6MUª´"€ÓP¤jUb9ÓVêèªVІ‚ê4RËÉm«Õú«ZT)Û
er­T
J@C9ÖŠQ^X®•JÙ\JŠÊm«Õ¦ pÚ@Ø(˜IKR4Pf#¥N(º±[À±V,––jP)-åX«MÅ­ ZqÛjUÚK‹Êªµi¤T%·
ÀŠ”2FP­ph)›–ÒpÊÀ°\ OéÂT)) Õ"¡PV'JcmšÚ–A¡H(”ª¤’"mš
iåPTK•:aª_jGr`¨Jx°U Ï µ:¡>Ê"PìÔJyÆ‘q82"0Õo±H3i¦MUÒ68h¬–Æby4
©µ©Vˆà´J'PcPT´ €†Zµ¨JØê„*¡H(˜
[j+uÚV›&€jµÂÒ,°(áÑÖ˪õ‚ÁX+D‚µ¼œLš k;…¾þ¯Tñµ©Åqb>R´ŠÝÛ­Çרs×ý‘R•4Žh¦”
¥}ž2Ä০»å†*©Ó
ë”"}hœZ$”¶‘®€åJ•i*¤©m-ê%$áaÉŒR*œ6ÕÈ7E«à@¡´w¤hºoS´ŠÕ…Ò>ÚÒX))¤–;톘»
K)Ru©¤NÐN°SZ+.¡o´R´ŠÖ)åÑJÑ*Fè%Ò
#]%åÑJ¤PkEå´á­˜ŠzCl**db:m¢¿Ì¢ÚZ,mSÙ´‹´©Z/LÂÃÂ"µ¨V¬ ¥*µ;­LBL&Òê1 36l€RJÛH+VŠ´µ°(áQÁZ/*–’*Ųp{tB‘°S+EzÚ
€ÖÂ´Ô ÕzQa›–‚%JJujP/-'¶i+Ò

ÓZ$”ŠuÂrÃÛT(U‰„Eg÷'€“„§uBPH­h˜&À¦±´-S•ðxHÚæY©¶RX­­¢EÕj©P2õ©gª‹©»Ò2ðÃÝ·®0B—ùZ+*'me€´Òö¢âÐJ'” …¥¥T)“ÆRR¡ô„¢ql$X`m$X`-C‹ªÅz)™°Rª%¤ ¤¡V-$Ö‚ ,­õ‚rÚZ-RJÛL¨ÓJ‰ÕÒ84
é´‘PRd"µ´–Õ­ø‚" ÒLª”‚fR¥P-0d4“ê¢Â„ÃÀ`ÒZ% ¥q°$2J¥‚Ixxp0 J¢h)Ú“L/ª‚F¦Vh…«€!JLQ˜Ùðdd0eÙHP+Ö ”ÁZ) Îó?a|3b©µ.2˜Jê´µ84“ªtJyp0¤BimžŒIv%`šM©N­K•‘ÁL²kCÕÒ6Z©“*EÒ68X@Ú6Ï)¥:µJ§Œc’]¤ “ÞÞëT‘q•2®K (Z«¥¡H¦T–¢8ØÅÀ¹L ˜bzm€`y™°$!€Y`©
`‰M`y ‚ååÁÓÇêõ½5£K,>зߋ »ŸëoGÿ\bþbÇþ׺û]^X^$½0°ÔP«–çJÀ´ÒÓ´P/¤ D s-`)¨Rœ‹¥¡hq´6OÌLÅ2W1Í\šÌeJE8LÃY)½¨:0ŽfÓL¶i*¤ÉSPpL£­H+˜)õ¢Zµ¨>ZëôÂmš)uJYi2@ZZŠ„Êh¥N¤Š”"¡4N,SJCA`+,”SÅJ©EǦjL˜§VI[±6­tZY6•
Eú´6Okó¬´H)ŒTJ¥ê@©TØ
ëeƒÀ6­Õ"•´k U:µ6RF ë„R¡´ À±R¡4R-*Ee0 ¦m$TH³:\Ó¨¤N%ÒÖe0M«¤@ÁÀLšN›ªUÒ>2—*%%±zQm¢ÔIå•a/.•Ô©•:m)Õ…Éꥥq¨¨Pª-#Ë.V+¥µ´¨>¦TD].Óâc²)­Õ"¡BÛ
]ßǸ17ª&tÛ76‘É·Öù ©“ÊiÃÐTHÙ@;}¸I31¥" µH©Mk¡<&¯Þ؃ŠEÈŠÁöÅ^ÆdVÿv×O!ÖÍùàGïvÿ±Äâû†ŸGO_—Xn2¦þV[ÐNªŽ”¶Â0I•ðdWc½ -LRN&­”ÖJ©>&©“ŠaòÊÚ´H-/°õ‚R±´–¦²‘:”„
µ6‰£4“Ê"Jx²K± E*qh*©Â¤W“][`µN)MC½ldvs´6R]` ÐN¹ËçO1wo!ê䮽1wœ«µH¨U‹Ö3b1{Y,©e¥Ú4$)Õ餢mÈ$¶!ÚȲ+±–JêÔjiš
©EÂä%<Ù…-h*$–‡ Kx² þc ›†ÒòraB^ LV%<еH-*¤¡N!—ðdK‰„‚À B¡´ŒÔJi*$ ,Eàƒ-°6Jõa±´’jËh¥´–—’
”eW¿ß›ê ûcÌÛj§Û·‹i„1nz|]»bqS#GgíèXÚÆi”e×óÇøíb튭+¶„JêdJ½¨ ²ìbÈO±M'$@ŠôɲË"S]€Ø.ç-g±­E¥T'MŠiåL©Õ¦ÂR"¡`ª¦€©TT(LkV Õ:udÙ¥É`š”JÕBZáÀPi¥t²È` ›*”e»to±…”êÄÚ´(áɲ+]/¦ÙÈlªik±^P( [ØT-’¶}²ìÂÒ±‚†¢2y820YÙ•±¡4"2* ’…QDJVd ¤<ž“Š&#%dìiPYp4^ÖÃÒá”al8³À)ÉdDØ øD€€~ (Zf—·´à‡ED(¤Ð'äã$¤Ñ$ÞÙ´Š…+‰w6™!M’xg“v$Þ1(øH†‡MLˆÈ8PûD¼ '³«€t²,R²'+""%ïl^ CÄöK±áÞÙ¨èïl4$(Mà ðΆóÑí€Ò;€¶;¸$„“Ù¥ÞÙd†I$âTðÂáQ"$"­ £å B#ÍBdŒ¨"¥”áÉ.‡‚Ø`xpÐl‡w6
­°™Œ’=4JªÞÙ@ ‡’r4¼³Ñ…’f„E”4¼³qåÂHó¡`¤ÎÔ*"Í®„ ”Œ „HU>+…ö% Ù¥PÚTlH­¨¡X9HÐä„|#±+‰¼gôÀ9' \êÄ$eQÒÀɃgÙ(
æ<# (Q'U!ÂPÂ"2XbˆÁÄâ< ˜>/»@6D$ŸˆD ‰€^>ÙëŒ "LšÝ8h8y6T²÷Ð9܆*P6º-¨,8Ùo”ôeÃâäIÈtlx41a€é=-*%Œñ¶Á’›Ãˆ[Š*°<h<¨´ÓÊ  2$<n Võp¡"ºàh\<$ Ëê!»T¤¤K˃{HyOL¨4m ©L)†+ã`¯¼:ð`<+AQUÃJÅé§B”(,U‰8©z‰Ð¯XV[œ`8ôæAä*Dª¹È®K,àT ˜qT”Â;…ME0•Dhf¡ãB@ 6ÓMJ8'ä" Qì¸sRa”ÐàœÜP-'…¥eÆ„¤%)b€§{Ta…E²FÊ"CpÂ10m0x˜3.=ïl|#šÈg™0„æƒñ˜Yœ„,>ž¡£A}´<»º@‹“. [2)©ÅÉ̃Š4KZ%{-,™‹ÅÉš0¦dQØÄ‚¢%,I€„…“
,mb
'U+;[‚ÂI%€ÂIDŠ&Ç…Ô¤”f·CAB8iuh¡ìÁ¨'4ŸT<8©ád‹ÛœŒ’@Ag³éðΆY8,8L ƒƒà‘ÁÉF'7Œ’Še5Di8q ´‘EepÒdÀ
Ov=dpòãáÀº!âäKÉjF@ !ÁÀ”NBTò•4”(†G…¡€9…OgÓ] ú‘pNkÁ#¥+2 —ƒV@K’–—É‚NvTTtÅ¡Ê01 mº"%FÞÁ±HÀT¬\0 &“L#È`bf!%]:­Õ‹ J+…‚²­¸h("((+#¬% x ÙBDR!šKÈJˆ2bÒÙ¬j$¤b1Ê–-#¤£ÊxaÉ XÉè¸|
XZ†HɆ’Qb’ÑÙ”Œ”„d¤>20(ÑJvµ´Šhd$„dÌÈÈ‚`5ó²/B ¶Ž
â ¢ð…ÒË–]"*[âJL". ðFa@Ãा, Ƴ*Ã;ɈBb ˆ˜ØÚõIGN D\&·L‹“. Mr\g$B7£„ÎF¡3¡{‘ÐÙhš“#ƒÑ‹†€ˆ\y +b€!`y°V\ˆ‡ƒ€Í¦ºèŒãZà4lÛGÛlÖ…ÄɄήìZølMv1H 0ô¶aLBÙURÀ£Ã3‚°á°ðÙ4
±‚æôɸH(OÅbò<{@â„f;(Ð( ²+¡À‰‡´¼Æ‹Â¦¢-%“õ°D2ž àppD Øxéð‡–ìÊ®ìú`áäÁ &“°mÌÄ"cb¦Ã Gh0Yò‘€Y¸ Á„œ+m0, ¦â€£€áˆQÀ,sÚ „+m0ÙõàJL¹Ð`^‹„"¤íJëXx>X¬;„P6ª
•lptEB”G’’ñ°d(2sàò±Ê®ìÊ®ì:€xÈ®UÈÆÁÇà Ç%õÀqBŠÈ®ìÊ.6™À” C*`46V. ˜Ì.Jò\ P%!ÕTKå
WtÕ€²Q‰– Gçmxt)è‹ b?
ŸìÚh˜ ñÛ–]Ù•±à°m ‚Î(»v@vµxFíZÙ•]$­è`¡%ÂÀB;<Rë@ E?% Íãá Îv‘’Xˆ‚L³R‰H³‹€ ³4ˆD PÇD•±ÞFR,ŸÏŠcðÒÙ DL†"»NZÄÂÃFÅI¨eBÔ1è08lbvm°L0<(IË$4@€s ]+
Z]ÙuÁ: 
±N- Çãò §ŒQÄHɧÓ]& /Œ¸D,<|HTYv‰öck‘ÐQyaL|²Ë´QC'ˆ¡‚ÖÆyÀÈ ½€
JPPŽB²E0¬:R.NÞÈ3`…ôJU‚gÂ7 Ð…FÉó«ÏEÄ€ˆMñ¡"<n ¼Œˆ³Kãå`E%0‹édjeÁa±TW,#šÅ `É‘Sf9&!NF ˜xt2
fåm4¬¼†ÍÊ€I$ÂH#—_Á¼pxX¶¨1d×6à£Ã³"rÚD°'ÃÑ qĤtBŠ@}x`eÒØ4dhiÂlÙFªƒ‡Í tú<¸„F"T²iÈpÀ(6eá”TCÊg;Ùp “•“ÒfWsŒDè–ÊV8..ä àa óÂá™x$@™ žì:q…H20 R ‘† t@am°4R@ êðdW… Á(Á$´‚ñN"0-“DÅǃöÁJ÷ hÊâ`X:L*@Ló mæÂ•6šŒÈ3a2IÀ„µÁ<dD6˜‹Žf7@ #šÅ¨Ú€'N #÷ŒN¢€ƒÉf׿ …‘/@ AÃ;Þpivé…w6Ri(5€‚(± '5'=#Õ \xgƒ¡¢! Ÿ „(»”Â;
Ϭè˜<,'ŸÑÇw6
õHPŠMC‡DóNn2JSfÕ¡¸øpà‰Þ,$œ\ äÄIpŸ”$Á<ì*¤³ÙŒ6.)š‘w6­€v0j™xgã2-Nf:CË-Ä;ÒŠÓá IÀŒ^6˜‰Ž&#r¡¡@0iÉÁ
qvq¼† „çá=.4»‘
@:©XpX ½ 8¡(çäY-NF–R„Av2àŒ†¨³qïl„ÂÓ :XY
5jyˆe•'³k31¡ÎãAPGp2J–€Š5ÐÅÎAƒ”Ê‚¤ ò:Í(äáÀ(.⠴؈P«8xˆ@,Ê’X>$Íx AU4@RçÁÂITv2JWAᤄ€ 1eV@,GÃÁ*| 44¡ '£t(œÌ®g`JRBÊ wxg³a*N %M ˆËê)O‡gEñ+ŸÏg¡Áã¥g#„“$€(ŒÐ[ù¤hJ4ÈFƒ~t#ÂI+ÐlKu80H'Y&É*
ds#ƒ“®"ƒ“/
œLpn>±E%ÁÉ(×CÄÉìjµÄÞ¸ACÄIN…ˆ“›GÃÁ–(„D%›‘†àd”šQveÅCÉ u*6áAÀBI„",,d2¦Ì.Lw8©Òé±úpà‘(<#ˆ Ç9÷Iyg“0 )#ïlXXZf䜌’5â‚“Q&˜€@lH[áxHÃÁRИ„FZ
H*!1Ê„^²ЦdÀ
«BÕâ­,ºÒ3¢‘RÀtP' ¦Ä€£€É`¥³ÁL 2 /!REZˆ°<œ4¤¼ÇåKëa­@,™]--ÒÒá!Å‚£áðPR‘Цä†1@RÉiÃ(H„X¡ÍH„0¨x…ˆmt°âÏ vƒÇÉA;yÙ@©Ì”-.ŒÅ&n>J"šìbX)ÈlCq1sñRKÇ
G+J;¨àH±dÕGÁÂ+ àl„<˜ ˆÍJ©ÁÓªð„D@-VCO*TCÃ@³ÙµzH±à€hØ0Ë„?`H¤ G°^¸  –”*Ã;“VYGGȃIY¼`€@
 `Ä–ÈJ(‚Ì' d,+"ŽŒ Å 2@2ŒŠÉÇCJ¼aŒL'…ƒÖn[k£9™&/ÛGJ_6ÕkãDlÛ¦‘±8Éì:á“lñ†I¦€””$Æ
Ij<D2ó¡0 _J>ÕÊLYayäÇI¢##[p䊆ÂH•”–Š4¹¨H…¨!ÊŠ“*” ŠP.(ˆHNˆˆT±œ:6Ù²Á `]: O‡Y`<z€1ØŒd˜”ѱáÕ3! Åy8mt:N8*<!ëA°øð£°àdB>­T³k´y,<ŒÄò°±% OJ&ºx'UîÄ"[5t^V^V<D8¼÷IQpŸ”ä|4¸ÌB
ˆd3É̈Bv=ˆd\F\F€  L>š’4'+ý|h$âó9HÉUÈ*D"ê|€ dçƒÀÂÊ. KK­À˜òó10åç€5&$M“]   îd”Ÿ– QKÆ£¸4ópàÌÂÆ‚Á&[<Ñâ 8»&Z&2:aà
˜B&œDÉJЬHHHh2¬…‡–&êlF£‘
«ÓTv¤rR²'«%{²/&i3 \Ah€Ä2ÈèsÐ! -ÕÂ&?Ìi@xÙ
˜ ³IÈh8Ø‚ yDDäÅÜA Ñ+™œ€.:#x(BéH¡8d4+¾ük=j0TŠÄˆ&Ñl|B†R@4Bà³ÐÁŒÖ†Ãh s°¦ÐH(»2DÐÛœ¨:86 Ž 4`ñQPGÃÈ["¨‡ jp`8<,!)!•‘–/!•'Ãíg0XQŽÈ€dX#
2 )!Ÿ³Pôm&›ÉÆeD´¬0 )
.#@ Ø éÇeD4"úøø0 ‰8HɃ”ƒ”¼BDŸÏÅáAÐeaiÊÂÒ’…¥¥æd”š’F±¢È<C±¢Ðh,8l4(²÷p`
ŠçeB'Z&Ø%¼LèDK¦
˜'Z4™…ή‰„— uh5œŒr¤+RR¢€ ÄŽT˜3Ê\@ VA2NÉÀ8´^:-î“"2°d6Lˆ‰PÐH„®B"J6 (Á(„D”@l": ÌD`A@ ™í%Ô’9ñŒ¬8HD‰Kªå¥DÓbùhF0j
$wPá0V>£4$¤K¶e£à"ã6œ¼l!šÎ.-.ÜÐVXÐT2™PŽ‹-`E ˆ„4Á[ •°Qp'/›Á€€¸°)k[y±‚Ýg”€™p¥ æ¡ÀJ¦£ÀJ&$NDÄ L {0% &»TXëÞÉØШ€„8Õ9Èxmt.8$%µð|,<T0H¸<h `Xò €¢aó«²øpÜ'%Y(¢MHH Å}RÒi@6Ï,tfa3 /%  ëš2a@S6š’«Uˆ¨ó2¡ŸŒ …1¡Îå åD³Q}ˆTªìò1ø¼TxôÃ
yôÃÂÒ<:R¢Î†äåád”Ÿ4!iiBÒòÅ$»0 /.±€_&Ôa¢eb]b¿L¨…:L´L P¨ÃË*0kB\³Â"ÙÉj 2-’Í8HˆÈ8H8ÉŽ²‹Õiu6ÙÕrhiGBg£;RauZÛ‹R©†ƒ-ˆ,"Ó„C«³±T* IÄ‚,+2m²+"…^@HQgâUBÈ*!$¢4±J‰PÉ®ˆ’ˆ„Õg$"•2fBZ 2:™ ŒL)3“— Ó€1 `˜^.Ô#A6“‘D(#ÀãŒ`²‹`B…µ-¤h€](lcD*!!½m+„+~ábØxÐ ÁضäM$JLjJBƃ¥BIh`DÓ,(™ONde0XT Mv¹ …æÃÒÙ`2L& ˜ ¦Ãd’€á(ùHÀ Ø%`”0, †£ÀJæ 62˜¬`÷°ÑÒr$Rˆ1 '],"Ua|P”%CbÁшšýÐN<8¼ºX(!ƒˆuŸ”t÷IIvŸÌ
Ä~B@B@ V?ä£á`?/³’Ùµ™l&¢o&Ù•YHɆƒu™…„ÌBJf—€ªÅ)-H$g1@² ù€ºŒˆ:.##¢Î—Š’nD$=Hp9H¹²ˆÎŽ
H .ËPSè0F†q¡c} »uÝLר1õ©óå_]_Ùu2¶®µ{SvT,êæ¿è0.öâoFøÜéÞ£b_tS!\øøã«íúÞ?ÇøÌ YãæBß·ÛÐi¾ØÑa\Ù£ûîS¨Œ=„ðŸï?uî¹Ø¾‡B~èx[~Ï·a:UçÕç÷ë²3.Œ¡¯¾çß§î=cʬ7¹ÓÅÏÅÚŸ/|½íê2_ÂþçÉìüj÷ã„®é<2Ö1n\†žÑýËÅþår+wö»ÍÆú¡ê®¿\ìmG…í`6w³ÿsêêº:ÄôõeŒð÷9í\ìõùcØ•ßa;å\,êËŒ1fûƒî¿ ×Y]Ô\O…î𺨭߻ëÌÅtýAÏ\NètëŒ úªc1&|ýw«¾süåv£^¥5óÙù6ÃÎÔfø^·óUÝocQá*'?dçš
5Ó9º¸ß÷õwÝñó†é¼î¹±îØ0½W£ÛŠýFùݺȽçã{QûÅÝ…qaÔ×üŽðÝTŒ¡sþòÃÔÞÑ%Œª¿Ý™y݃Q¯ì·—›ßwæ{t˜û‘ÛqŒ¿ª¿ðiï»|ÿŸû¿ïúýãÏ—ï!Œ©ºîÿßKítŸ5ß·?ü^.kº:Í}½/nÔørì_|›Î¾…ÑÙéBÅ:¡¶FÇ«Îñe\çpsóéF…î-Ô«åÅ}ïý0ö†Ü1s¡2TÈcn|Møvsß)C}«Ÿ¶ë…ÝüôÓûݽB…Ð>]&üVÝmý£³FáçÆ×øÑ)ü·ðõ‹f»þÉü¬2tï=ëÛ}O¡Óç ;ákê¢ÿ§ò«ª²;pcŒ sw¡Ó_½S5²{õy÷êCø¼ï½‹úÂ…îª2ìe†n¾÷Mý…ÏÓŸö:Uøû.o„ê8>»nj»7týÿ.ûï{uп};¡¯BuÜÿüòÓÍvp_cä¸úÚÕ=ý×
ß}¾Âì…ÎÛêb|ÝŸ²C×ñÁߎþÒy|ûºad}é.Æçìªÿ2=ºÏù wŒÙ=ýŽý™¹éîà~û¾Oï|ÖôÖütè¢?ÿèþŸŸF“ÛÁøŸÙßê«k†ßc„oÝ>Èþn6ï»…Ó¹ÕuCýŠÏú:ò?§ùZ°ø¾ÍoßF—Ð1øZ[»ý½ã÷4åªàïq¯ïg\NÇÞzUôs7„ßeä×ïç×ï•Ý'Ÿ»Ãÿèýê®Æç v|o\æèzÓŒü`÷Ëç|þëè¹êôáóºî–]õ-du¸¹nÛS“£óK_خʜþ\»+Æšüì¹±Uý=˜/FO—Ïû×_õoìX}{û¹?׫û£§vŒüùTÛû=—ïz}°ñY£køñc¾ußó÷²ÿ½èÊ]tèêúŒßÚ0B÷Ïõ*½mÖÍ|.º+ö>|¨/a;c˜ïùƒüN݃1c»÷v»SØî­Ý ·{¿çTæ¸nac‘7ßëî}çìŠé&»ç?çt°ÿWûÕÝ/·7÷‹:ýÆ4yáﯷËõÖèOUŸ•]¿Æ^fúf÷ªsõÏ2|»­ñês®b›
¿Õ£·;Ž Wÿ©Gã¾×;ú¿Œ¿Q7Õ½LoÛŸ{ázîrt¬ýé¿ûð±FW×~Ÿ½ñ=‡¬±Ûu7ÆÏ«Ïª.™ªÇÍç´{~ܸÐ=küï}úØ=n æç÷Ç×è¸YÝ÷9o:åÆö1s„é¿.û×üÜ#ÜþÐs]W<lŽ19õ;ëÕ‚¹sôÜøù¢kmŒsaú{WÊõÁâk\‡£Çö~ïüÖýcÅ<&3ôÏÈëÚƒqó_æsùX¯Ö¯“Ù¡;|.1†ÌÙž0Ù±³bû~v»»»oÆâ:ÇŸ¿v¯7>äìæ„îù9oÔVmΛ±_îWøÐAeÅâ÷ëw›1öe˜ÞùÐ¥ÆdU³7~f²nìÎøß ß÷ûü¸ï<®bý´!ìíWuï1öÍpÕóó]F~QŸ?˜W¶Ëª®ïÚ®Ÿ±ÈŸ‘£n»ÜnÿÎ}^Žn!wÜö|×Ïu6ƽÊú.·ÛUfkCÇ î¯{ÚßúÜ.»±}þ²Ó_»ÏVÌ_Ìç„ïuoÜÞ÷ç`dÅ Tñy3?êªGç¨ïÔ§® ¦oÆÇìÛûNŸ{Îueè»ß÷ÝsÖ}îoY]6ëUzáó&L·ËÌñyÿñ;NVl_·>Oîÿ¥{7Æ×‘sEcòÛýtûÞýF09®{_Å¢G}þýVÙ÷c|}Þé¼÷—£;ŒÿBN}o÷½wU÷Ⱥ¯Cw¨Ì‰ANϧêª[íëìÿØ!|
_rôO×ý†ßñyt¼«˜6|uÈ :5c·~dw•cüŒ±õe|ûºŸÚÐ=^Åâæö‹þÜÜcl¸Î{s ùÅ]VÏÕn÷]×;[W]Œ
Ý[c:þUŒú÷w¾ïOùÚï \}©Ì0Â÷ {Éz•:².»_VlÙ2\¸°9rþîKÇ‹îýÑó¹ºO^]FÖ«ó†ú˜{ó²>§Úo=²¾çú1ö¼¯¹ßœŽŸŸUá'¿wîûè›îyjŒßÜ ù]ôÔŽá{ÊOݿԎ7c|è®?US;ÂÍmv SãûÇÔw÷bª¿×}O3¡ƒ©ú`ç¶»ƒªºŸ¿p3~t¾ªûðŸ:älǬ³=BݧêžU?*ïF¸úýðásÑÛu«b­ü±·_„úZ¿¡:çõéì]˜XL÷÷ßÛÙ1öóŸìWö\Na|‡ÿ1»ßëÍøùÿÜG½>Ø‹¿²>O]^¸í §bÚ¯ýeo„¿9Æ÷vú©šüžÐE˜z•Xã§+³²cUÅ–!ÜöVUN}¨ÏÅUÅô¥ÇæÚ¹ìÙÙu3丌q>?~¸˜ö6?³¿s÷‹Oa¿n§¸ñiLæW±ßó|øâ{ÿ´ÛUø*ûsþ|•0ÿLÝnŽ“³Ù»_B…o×e®b¯ ßC†á§{Qõ*aï9*ÌW/jüìŽBU¬™óá&oçû|~v
U±÷`«Ì_:ŒûœN_õjÁÆ…¹ kTç§ïÞU/›ˆ>8<®”«âiLþ}XôLŽºúþêïöÇøB‡±÷ÝY#óÂÎtðÁ«´Ú±u×7ÙÅflõ[õÁ÷Ú÷7|‡ÝëŽ3f|ªÛÇ¿»ù"„ ]>cïå—ï9Û)䫤ê»Ñ÷>3¿äu®sõ¸»ûËLîè z?}Ù ÿ±Bÿý÷¯í¯_|ýý6¾c~í—±=êsÜð*5'û·¿ÿ;wˆñS÷nõÇÐAn,B¨Ýù¢rtÿ Æñ£Âto}®¹1æW?eèþáUÂb§æ{ózt¾}C¸yã²s1N^èÙšqÝkcÑ3®*÷Ðm„dUç\v~÷²î&CõȘïGÕç^„Ó‹åÁâ•rU¨v璘«ãä§¾þîêžÇ«ƒœïUă®a0Ž­¯£Foèâk 6?¸ï廆½·šÑc7§S¯R|oý­ïówãèÝÌÜìîacüسa~¼J̧êïßkºøýpÿ=jjºËo,æ?˜QÝÛ)|L_„q¿]á;èýGØË/z§ÓçLØëºÑ¡ãîîü¨ áãèà6özê;LO÷ü1oöO~ÿ·nµ;òƒð_ŒŽ£¾ÜÜÎ|ûΟ1Ž“õŸ¿uëõû¾Â×qÕEgì½ØÐ!'üèÜÿ*a®OWëçºnÆ4ò¿¦6lwüƒþò³6\—±Wß+|¨êô[föçî•1èþ²'\w
ÿ*ašÐaÿƒ.&cóõ³¾×ñ1o|Íew™ŒíÓNÕÝÞt0aÆ}ÿù¡Û…WIcÇí×øê"Cì;>}3•Ý‹‹ENM¨0f|nÄ`ûkôÜ÷Ø3&Ô}}Ñ%„WÆ«0£r箃ڂùòcV¸¾ë8[nøö}û¥ëKLY¬õzA¸4@°¼<X^&ÙÅàÑÚ<&‰iÔ[À@ œV„Ë+UiË\(XbéXbî‹ÁU,¾nÚ¤
S*ÂDõê €•²ðŽ·´8I’]ÙE‘]îã^±¬.0¡BúŒ.ÞÈ#âJ¶‡ŽÐi£³mÙÅz00e.gBÂn–½³m> =bÎÉ(³k•s2Êäp8NçC!`¼MÈ®Là$`8^¶LFãNF™p’]šš aÀƒË®FÃá°Ä&axá`6$2Û@Bðp]-6a‘¸ˆåX$€ Xa$BYT (QæäÉT"‡ÎÀ‡ÄÌŽ"
>›ˆS‚惵žÏFHå¬Ý6VņH»ðΆ³°éÊ8l²KáÂ;W²¹@hp˜ÈÏ*A…D硳Ê> Jq’ÊÍPxg³a`#ïdœ‘+©s‰üÀRAäÄv
Œt³àšvÇr™ÌVAÆÀ‰U²aQeh:\F@Œ^‹óê[辬ýòþbÌŸ²ÿûæŽýbô^øœÃÅœý¡Gèÿ9z„­pÕ×u?+³ºMŽÏÙ/£ë^lã.üþý~#cœ›7áKv¿úº·½1]íîè2;üÏæ|×ìXÌ| »_?;øŠuú?ü百Ñm+];¾¹]>Æú½®CÝèë’õú¨.k·çcvÏ«ÌýïUctÏ‹½Ž¿‘Âuû‹µ.G˜
YÓqïÕ‚qú¿Â„¿.ßbp[WÝyïEÁ`LïߟëªKl!ôü~¯ÚqÙ%ÆÏ™.±}ú±!Ëùö]Ó!Ìl—G¨ûŸ#t‡.U¯öÖýcszö¦ºýÆþy“×½X|ÝáòBu ëß„u7>#'ìæ3¾‹OßCØú<s;:—]ßCÿa½œù½óëóu¸°µÕ½ñ¥þ¿c‘_Œé.×¹£w¾ø–ŸcWưû½ý×ð¯C†Üíx½¢z· ÿó¢jüØî±ÝöÃÞ‹›0f~ü¸ÏuctwìS…ÿ÷ÛÏø¡»}ïjÁ>öGV¼Ïùk¬¡¿æÈ·]|^)×ökBøÜß»]Ç¢óBèª{Ÿût ng¾ç|øØ×US#„/ß‹ü1êo*ôw¯s~0_ô¡»èiv?ÿö_,¾«~ÜN÷º]ÝrcÑÛW“9r;ÝÏÖw•tcŒîXû?r2|/ßgcÑ¡ë'ëS÷NßUù&t§ÙÏüø¹ýªÀúé;Œ1a¼>˜æß{cÍcÌèñ­SvÌ_û¦óÛ×½áºËvÌzº†1;äTèþï`{Ç„êblÌÓóutîŽÅŽß­Î×1úüÚðß:øƒûôéw¿¿[w ²f¿çÛÝ¿_Ͱû¡Ç—Ï1ôëã?wûa› Õ÷ùÁ÷°õäe… Çv·úÃÖw ßò{Õ—.>ÇØ]_¾øPÛÅø0o}eýlO§Ïãs­qö{p]ºc»þ^|Î÷¯ì5¾çÎåu÷Ø1Ï‘=ÿ_ºæ¿@ÃÇFígv×?ê.dçêUÙ9ooçGèqŸºÂ§¾ÊÜ.òKͨ®¶ïg»K‡¾Uß+tÍëP]£®ç{Ãöºœïå&ŒËî£~ô3c7lÞøñ=|è>çkjä.î&»ÏÝ«"ò3Ô|_÷jñîoÏ\ý8ôä×cî»Ø°õu|þ¼“#³wçCÝl~.5êkÞníù~£Â.Ƹ0ŸûäoåÝåå÷ØÏ}rÃ÷ÏŶã¾óg\÷Ç
ßóç ÷Ótœ »õÅ—ºÌÎæ¹ÓéfïÆå¸Ð­.öžGè^GØ/æƒÊì á¿tuQýáròs]÷v»³WŸ§÷‹úÒ¥:üü<ß!gô¸žýzuÿA¸ü ÇõäUçÍd~Íó!ŒQ{—ã{öÅ:•£¾?U÷¾óƒÿ<ÝúbïÓãëF÷ÁÜ}˜.uá:nŽéê`3¯fì×ÿ^›?ÓmGMøî1f;È‹íƒïúÖŸ;\ýÔvÍû¯cÜŒ ûåc…2|íÙõ=ϸìÜ×ßö;ö•á6»ÝÅ`BOßáwÔð_ú»CS¯”듹ýwÙaGå÷Ý ûu*{¾ý!lد­Ð™ùaîGÏœñyC… ݽ/Ö°£¿†ðá/³Bèö-W½Ùõ×=Sßî¾gÍç «¾ü̸©ÐùÛÿ×}_ÜÈU£~dvå÷îÛì¦'L÷©í ¶ƒíðµò{tÛ‹íöƒÐ_Gì!Ôçž¹ÿîñbÑûßÂÜ9êã}é.y1˜ºÉùUáÛ·®w1w~¯ŸþöØêvdøà:»wŒîVsÛ»×iêb¬ùÿ“³Ý+|0¶ªëØ™ÿÙÐÓ-»ëC˜ì`ö¾úý2»£þ÷g¶cÕŞ·ÿâ‡Ð[9ýy>UUUö[—¯«Ð]¯6'Ç~ÈëÏ%ëb0¾Fuþ\ßB×w½îÉÛ°cºÓ¸ ¿—c'w>÷ Õ7Õ#ìþßè`T¬=!ÜüT}
ߣË\ B]l}ù½^þ|‡Î¡C~ÊÍÌ.¾bÌ !|Fu뺘¦û÷»~Åâ³'?t†N_±§º1ù9º{­A}Ö3îSíLŽ Yßr>×v;TVYõûßcøúñÿ~?o޽P9›ÝÙýSLõ¡ƒ«›ÉoõÝÁ§ØÛçgüÚu¿jïkŽ›°_;B]ǯØç:ä\Vu¯ÚþV7û¹÷úbìOç÷AØþܵªû{c'L§P±—ùvuaܘÙñ3:Å⃑Ý9{O#o¹—ݦf¾TeVèÞÓ×ÎSëð)§gC·ïAõVwlß{ûåï¿‹®ÇÈ}ÓŸ[ÕÌÇù—cëSf÷ªùùBL#|û®ÆN†1>×~
ݯîõéíÜu1û0êò{n[n+o»wÅà{¨0z¯»tMØê›üìö)¶Ûãû†ïžëî~Þû+ûF§o1Ïüè¬Êéï÷»øF÷tÝ:ÔÔøÝ›éÞ£BýïíKsÓ3ÝÓÜemö¨°ßcõfÇûï9á?y1»¿.9s‡¼­©îîº=aä55v:ØÛž19ºæ\ìékv~lm¨ ãkÝsÇÇQõAýU~uÛ¹×>¯ÓŸ{ý¶ý%¿¾]ýõ˜Ùn=k¸Ïß{èúœúbÿVUùuküíÍf{1mÌ0}±ým„ùúaÿ2«fºÓ—‹-ä}¾u1ö˜PÇí_…Êûë˜7{=²?¿Ûǹ £ë¨¿ìùºãsóó1|aº×‹5|]ÿs1VØüfÆuì‹iÆÏæþ5F^}±Çaº|×åçÿÛ×üW…ÎÎ_.„Ùü:~Lº¿~ýp;ŸŸ:õ\L¡®çf2|ÎßbÏa櫾ºï\ìµïÓèú-æñù:îÜ«So÷þ§¯®}Ÿ¿‡o]ÜŠý÷6¾Èzµ`ü"»Æ~}Ði/öÜnªGÈ ¡k¨0}]G…qÿ=#»Î]L™ÆW…ϰŸ¦kÝ×¹¯®Žwµ™yÛòjo¦Bm羓ckçî{¹÷ÿ½ºÇû ôNgîv» óÌÝw…P=¦³ãÖ×õ—êîƒ/r»S×}þÞaª>³êGOUW_ÃÔtºÿý0?ÓaÜ…ðãSw™ºžíÿïîSþ«:CøûúÍaüfã²nóÛT˜n[æ²7».+GæØÛýÓ݇ ÕÁ×e¸ÝùP3:‡¡+|›/_„Pjª;꿼é¼ÝGÝÕ‡ÿ.£îUâ·é¡?^Ïe÷žî®Ç×ìýÎèð=vÇ­«=>§¬ 2ä·üÒíê2¿«3äçZuyýS]Æ\,¶³îû+³ft/Æõ~“ß·?&\^S76业êzucç>¨p;]¶îgf¶þãÏFmÖןùþ^Ýæj{Ã×ýqÝî>˜úž:Èë0¶?¸ Óµ¯.~ô¸ì ï‹Ÿ°áëŒ.}ß¿ùÙ÷÷ö¾¨ýÏ5/ä˜þoŸëÝmØ®u»>Öfu››ÛüÞßÝC}ܪŸr;øtáûÝ̳u_¦»ºîïo²jtîºÍªîݽëBø.Ÿ.dqcjŒŽŸîrì]^Ÿ.ÿ¾Cn÷^·ä™ùytšÌ1ûß×m7¶Ñ_³S}ð½?~öös#;Ý\¬ó_j{Ըι_v„°Fü.™ùu²«Ëν*0…ÿð;õe:_v…ÍíöæãWýOèþ5öÊÛíçbo3asÃíuü9\O~Ÿº~¹{ï?aBçȯŽ=Wûu£.t k˜Ýí"çbð!§‹°±¸»Ü¾ªï|s9#ÜݨÎ.5÷*­FfÖýÇŽ£²¾§ÝÿT½ÝÁ»_½ágŒ¾Þ©®Ðãgk:ÞÑa¾tþ"ÔþüØÑñk jÜíUÓéFŘ»›c§w;ØÊÛÎþ¼ŸƒËÿb„Ž•W_¦¿ãlìÅîøV¡º{þ4a¶?…îz£bÛõ¹BWu2ö>ò2Œñ‹ð½M¸Où·ãzlÐEÎø4ÓÅgìÿy|í¯ÝÅŽÚñ_÷ëÂtÑ£bÝÿýÐí3ö²ýíC¸ïò£ú²*Œ [õÁv=ª;øûä3ªóȼ¿
û9ÔäŒnò+ëËn~øûõ±èší1_kì˜Êʬên5ãÇ16ÇÍtùüqC®ûéžÃýajÆíog[sóîÇøòÝgîUÒ×ûœó?uNõ…Üê‹p÷uÙ1ŒŠ9„›ïfd÷š¹ãf»ý£»ûFþ›ÍðÙ]¾¨ØÇøÔù½‡ù:º~~|v„ð½ý9=_osäø :+„Ð=¾;òsÊ‘75ê‹ïÛå2ö õÿÁT÷ƒ™ó¨t¡X”å0ŽcJA†fˆÆ à (Iæòñ¶Æ]D(F<B(,‘‰òX0$9¢0Š£¤œbÌ¡3
C“¸dv)1UµÜÈ䖬܅ùAص4CíòN׎²_÷J}µ®ºf'hy¶Ø=Å̱/‰Ç«÷?¯Ç‘È̘„¥>4”Å<'s¦v…íÚvó|à‚AÁRd¢ÄD¯ë^m ˜íÒ§‹Þ=ÝZ­Þ¨¢{À÷w;ÂvXhž‰ˆÖ^“M‹t ôÂYc±±ž)^NekÓá÷'F¡®¬™<ƒIf#ÞÃWãKô©JùG]xkŠ…ô‘…ñFÖÃèEPW”"d<º›¤èó$® `Ûš¥RÚ&1ãá÷$cAѶ€Uâòº|A
9Ö–OýÀªG‘ðÖʲônbNŠÒQ3ªzJÜŸÉ×F;ðIf=Æ$ÎÄY¸IÆWÔýN×s·a&»…°}D·¬ã}DÔî.m(/Uù¯à6klå6¤žÃ’<,ø¯À–íÄÓª`(ä æø¢Ek—±‹Ö¹²…Â裿U/“›SнÓý§š+hwfbwé3P£l ¹´6žaQRÒõ÷?DÆ¡í-“‘þ& %Áai¯IÏqÃB 7<j
ÎÑ9´>õ
L h悸àåTJTã)F9¾R/"(¨Ã«ë˜ŽŸ‚2g޶E5­JþSq¤°ù|«.ÈÙ7UˆÓ>U|cuœ1mBA_¾Ë’.ùe]hŽ8"ï©þÀÎásÀQãË’6K}SÕš×BÉl
ЦQEîómš¦Sbªri«”0‡ÕnïáóÙ^ bÆ>þ’ðà$¿W--7õá–<bqwvIªƒÒ¾ßfºêÆkEMý‹A“DRÉCåRÈ1504mq>÷róé”í&w­KDþ|ˆ©ú@°¯‰ù`_Ó€$l +™(¶9õˆŠT7ðˬ.ý2ÛÊ!æáþ«öÖrE¦[:§]T¿ïæâ:^ÊÒý
ÓÑ-[Akÿ”¦Ð¸©¥èï2>]”ÝzcóúJàˆÞ‘×F‚êöhfcQìµ£Þ³žQ…Ä™¯¼ÐENVÜDÈVú=ïæ8Qìδ+E³ñ4낪¢ŸUÁûYrzÔ„ËŠ=J!AK5(Rraé¾¥¬ûŸÇõ§cElâ³4M ˜“\cÞJ5±j€Ã±C?™Á*M®ÚW½Xni0 .O:cÿ @xŸk@ô¢YO¡ßur
ˆ(ËKY4¯Ô zaY¿W¸”–&æ¿< |{¡.Ά¾˜W}|AHîCÇÞcNÆ-Ðy
$œ”Â?£wNh TS >QÔë!=6ÏTSjIL£„W-òR£ÑF£ZŒÞ^xŸQ²¶ã”¾ÒÚ®U@ {ÆÀ¨ÂY—5Xìa_øç™ƒ”ïùžãØwï¦êìZÒD…ö~šÚÏÓ?Njs÷;ûs¢fV%n
*ÚÐûéÜ2BrH˜—DѺhŠÓGƒá
çYþ4ä›7Ît¬ÞIB>¹lT¢SÁcå^UŒK„Ÿšú«àŠÒ«©ÅÚ¦^§^eòBÞyÆ¢¢‡ècýV:Ü™C„£žÎêÕLæÙ©›ÚYž%2©|eaŽÝBÙùŠá¯HÈ3%ÝÁ&%ÉüK#ì· CqR ¼ÖL-ýÀâΨIƒ#Z«ÝGÞA€ÅÀ%¡‹ñçÀ*͹†Àq¤u lÔ¡}0“¸äÆüJŽxP4gSÏÃD‰ü_“Í÷…¾"h¯è«bUÞ@æ÷jgy5£t¯¼Újô’ðÑóÔsÍࣘ{uð)©ôtw½•KB ¢âò"CY±*}ÎWì(Ðî¼ßšßWí€â¯ÆWÁé`Òj2£©¯¸%«ÑúWÁ.<MŸ$Æ_ö«fœ ü+©sžìÙÔJPœ³ ïWßòO¥øª^fø½º­]°V
újÅ
…ºDÊ“­˜eKI Ø8Ñž)ѵHÅ389ùÓøÙ#‹‹^j¹jExñÚX÷|h­s~¸ý3{€÷×µAþlmëæ•Ej©‹¡Øú]:ÐÛ>‹ œ¨‹°#'»Ë©e?™²&q¾h~`Ô_‹Ì%îhŠ—€»Æj'¼
ÚB³GjFõ׊£ãI…o¬ÂßQ
jÔ² !Ó\Äñ8¿3Igk¬bà=9Êm fuX ”N2=²Ït0è3¢l衱 à÷'x䆖Vi;©¦Ï=µ§ÒRâ CW¬íåÍæ[UO>Jkw×Óْٕû~'LÇ$8OÝ«ÓS•'ê4dê&€‚ð=÷ž+(³^^Åñ# ?ßfÑX•WT¾¿ÕÄ_eŽü#¡H¦ÿÖ(|MÝý÷Ȳ+ÎÊŸmÞSôlÒôYé‘X| Ê€ž†ÿ+@'àõ´Lû¼>FÃ>U=·:j&Ë1¨Z$2™“6뎧xÇFUÿ=µ§HSÕ╊ªI¢Fä/>UG\r!Ûþ!`‹½¶°¢œCGý,È?^ê*tT©V$ ¸:Q==ÂÀ.å;Ö;uRý¦3c¢V>rcj(³v|©Ã¨RFÙ—›êuL*Ýô¨¼$p´Q—[ òZT®.ÎeØá~¹¨¬Âî×ÛàŽQ{­¹»#f{Ž¢´úWÔ Áã0ë¨bM¾þ¿fm—›E<&ªNhæˆE?¢:TΩßêÝEÔÓ4“D«6ÔXb‡
„)ì7N²ÆÐ¹]œV:«Z #zäK|JijÚÍ-D¾òÜhpàúº½atsúêY±NÛ-Þ4£"û‡jGéß+|ÕngnåJ;äü3È2¥© 'ÖY?°8TØ2µqKõb î§äz/ö1^x—#ç¯40´»ÇSF¿ª6™Å¥"SNç½r|ô-•¨n›ø\¯Ä}¥mßpsó“Æø(æ'ìÑ2þÛÒ@@CÚ›’·úº™šÒÃj;à3À?DX,p`³±§—ˆÙÒƒN²¤V¬8­¹”š­Šø~Ú~'¢‘òšÊâY ÿ55èáâ %òAÑ.`w]²Sm“­»j¸'nM›×n:¼\Ùºeà@vY¿nÑ”W§¢Ú-ªGW¬.ªjÚhVÓý{‹Üº(ú$nJàO³œa„SëV kTüT')XŠ»F®XÀç@¯¨w‰,;áŒ#?Rˆ† # 㙿ég³=hDY2“ó—úAžëÑts¾G¶x*@ºîŒÐíò3CµC§˜Zgü˜%ª\Fp¥M™øšìyr”íp´c/CÞès4Öo翯1%ÿ¾ï°QŽkùh)d‹A—¨†X2²¯{ëpüNƒ´wïÀ’Lêø0øué*‘ó%7éš ™/Å`=Hí¬ëÂÞ~èÃn½ÖYÌK‰úšßE­ÏH‹F8$
É„†„õmB¶) ]U¯J.×dÞœÆ
±3=¢Ð¬–“Žé~2¤—OlPŸ¡×ã±?`€¡Íîrþ÷c\2ˆçÚrzï4ç j¶àCEbz1À‰QÐ3âX+PO—²6VzéÑ€¢÷¨ëSý~öŸ xÒÓÚ‚ƒÕVhtšÄ´oUĤcÿ= ”Ëxö¡Àâ?/Zò EÔËö¸C`)ˆoÌúoiCÍ÷ wyràtoÏ%>2åLòÕy&å÷¿CêéI(Ø…ÓÓ²ô€.3»ÜöLdvw×CݱN¯%»#@.µá¼ ©î•Œy„…ÊV¡Äšú€ôXŸ­Bº^x"šˆ@«S d 4¤§@¦r(A ”eÐòDzM1|?“"Ť$±?#ù´é˜„ËÓûc‚gÖâ*¦èîÚ¬ä¾(<Øg·YàÖtLÁÅŠÉ=.˲,jc½þt2')¦ws|#ùŠ ý˜¬’/³øÔ{ ÀQL.9PWÿ¿_’DÅÎé§l¢$Š|‘—­íÒ¥§ù_*õm)±Ã:„}—–Ȭ||߱ïo+ÏÎ`¿rV‡+_wÃÊ Á¸2œ¨Œ¨”#öLèJ¹+•œ ì$êíÉpKM¤šPœºº¯ˆ#P‰Ýß yVs•Ý…Fþ›ÞC©ŒýäÐEû?nÁ†…PÐ0³¦ƒ4ÔayjôZá‰7†ÿgK:–°À×NvÆUÅ•ŠÔ”0qXF^F´˜(ÝOxe0–£ýË;d–á L ҼݎXì™áÀ–sœðªPc܇ÖìØP¸9†5ûDdRâZìñªhëˆtð æ?å\OBˆ.=1Öê¤"¼ Œ¹íþÆ€—G­‚¿œQµOñd‚¿Å ¿úý ¼ó€Ð.óä{²ÊÔv<ZðO²Ï\/l7KïdÅVÑÈ<2ð¿V»€Ô(ªxp mIlcðãd7ûïÄø}ý\ÀøK !xz6ûéØð+³¥8+ÉÎYf+_(•~ïù—ŒWRœGsœPþ²Ïn´¬“
~Ä3øŠ9^ƒXpçæŠ‹F©Qík™ò#XÂg>[ºÀÛ»ŽÉ‹Ý½s#õ÷ˆ`‰4´{óê%n‰õŒŒåG á#Òþ˜3ï•Wò½#Jì´«NRHô ûª&†p <ÙË*FB—wSZ­Ä0íF995YzŠ亸©"»šö9óE^1i¾PÓ´³9'ª<¢ C¨$ËpyÌRÊòªýJÄ~—o»€Ã°œÂ"0×b秨 öT>Hh…œ*æ:”Kàœ¬É6“£^˜¸t,×¼CgÚWJ‘ie¦„GÚ>=Çãý£j­~Âûã8Øê|Qù_ÇÕb¼¿YF.x9~NÛø¼¾]0™®®f‘˜*†ÔaŽÌ¨Õ¥ÒÜv1žô%h Q°a':ZÐíº¤FÐAbm¦-ëg*"ÌóÀGÃOÉzmZ È#h¸ýN[?Ðî{ý”Ç@l×ÎÇá`u72Ößp7šmÔ<3œц[´ÀÓ£øº4ƒö·ê ‹¢U_³8t'šŽ=`TÙ,¦6¤'rJ´7BÄ6ý»ðZQˆ¦@ubI‚¿fžÔ+†Êqõ1a©ž­&á2sô²=ˆ±È'6ï§;#ŽÍ&ég>þß/²æú˜XÓYbK÷Í„wµñª_*„Òœ ú\。ΤŒWF{
“äé  ¶­ÍRðÁ-3où}¿ÞK·ÙÞܺ‚ ŸÀTÛ#â˲›l@Ï ½U…"dWYãþꞂƄ$gù¼8 žôÑo¶CcÛF:2£ôêm±ì\ÌîXFP›¿W$ØÜºà´9¿Z דl‹VJ{¤ö<íÅaA+þK˜I±ku8Bޯʽƒ¬ÛBndna|Øæ©>?üO2;ƒKVE1çªH‡Ež`ž0 ûªjz†6dT;ÿÉò oôVæ7¢B¶ÚeÎ=½9(ƒL%þÕÓšåÎÕ”L7 amJ®+êþä °p$s ÿè´w/ûZmÃS+Ïœ¯¯ÀÃ}‡P è½ m[¨•ô÷UñnmZ88ho؈‰^Ç`ã<!£mé1,ƒ±l0"ØU~¦ªßB­åžÖ1xÊÊËn¥´½ì¼MuÑÇ”¡b“&5ñ$Õ«Ô÷*ß…Ð- ˜—q ®!g"{m‘ÛL`‰XBOÛl6°¤ràµ?ˆõH†å/²Æ’Ø ³;å”8x JèåLþR†y(šþtžATL™f8o†^Z@®éÂJî8—\Ecìu¯‰ŒªÆ< —ÅìTÆbÐõ*9‘€K¼I Œ<Tj|q”f“hä}&¤š-NðÙQ£L‹ÿ«ZIÏXZvñ‘ÜIÒ)a¹0:Ê®éÙ®Ò—ÈX©*ïXN©aw±p[ЏQ„º9
Ë: Ÿ.Ëï=íy áX(Pód˜Náâ *ߘ+3ã/ýu*ã×4h”g‚•3:*-@hãÄÉD"´ )壘û·“åK2ÒsA©¶ÅØæ!ꔑ¿à÷ˆÊÅÓïC,ÓT•’ a¤÷ ˆ ã8Ñ ™øÖ³e3Á.í3Ãg–íj*3Œ&_º"`sQTD?)¡¾áƒ÷
NðZA±ÉŽ«Q] ¦Z€9*#ñ}Ä—6‘ü­ÝÑ’jN&5¬Ò)ÄÁÛ¾2O'Öý%¿&Ä…•¶õZü)5´)‹ŠÑ ½…AU‘ÓüéÊà©ð‹ì†ˆHnqÆÊ¥ÀfȾ
aýJ”r-Ì¢‡ÄH‹Â.”¹Ò6:HŠ…½Z±!ÃK±êTr‚YòFs«¾$¸xÉ‹.Fƒ‡Ö©ÇùÎ÷PëÏzãäy\ÿxpO[Z,›òàn'WotEK£düŸúc±r¼”qXi-ýõ.Â[šãö!õ'9=rüæ÷puÆúþO)÷Èjä~Òmr>ú-Ï©ôdHåJZþ;FIõÊVUQ¡á­‰—½™¹c˜aüÂæé¬p6BÉÖec’XÌ$Žð!sY·ôëÕ]€ëÔÔ»é€NoqÊ=¾Ó³ÜlrNŠlÃ|Kõw#¡÷h֋ǽÂ?—o<Ë©ê9?ê*AwwoyS,yC€ß†ŸÇþþÓ]äOìÆ—î•3 .4ï(1 Sîhéà ‹ùr| )ßý¨,‚³¦¿Iñ=3j¶†KbmFµBgîa|±pÁÓjú¥µ ŽÐöeøú"6*g
™?¨ôŽÌý±[‘Q¬Ì?“¼ªja®–A`OF±º`á^9¾TÙÅ–ÅsÑ^°Æ! bëÈ:8Š»Ùù)’n&’Šæç â˜è’ÇþL|'\g‹«hw+›äê+3:ߢ:ݧþ#• ¦7ü"ŸvÕ÷_W[„aÉ‘«à/±JkW!\íýêŽÆ•’£è¤¼H*jú¹ÇcÃÎ’iÒXY2{‚0{.%ÚDƒ)ˆ¯¯‡îà3Ä6áåŠð}b¾ø>~p†C£MBÂ_,÷×oçR±ü ¤­{±Q‹.š%ç¿™‡Lï»2*
è!ê­‰I¨pïækn±X%êÍ…º;U©]Î"('ºÎ+”Ý£‰Ò"‚Ž$š·€º ~k…uÉ+³a9C‡fHèÿ,`õ¤íƒ, úÆïØÜ -ïÞí¿Í¦¡È77sLl#
9~è™»§óá#[H1Ñ·dË¿ì{¸pæô»ZÞß<Nm-ä7_Á”ñD›ƒÜN9W‚¯ýÊH‚Üœlð®ñDºEüœØ|Öùr¢tzŸ’]û.ËÓäZª÷¢1à‚¿{~-ú;q.W·Ù×
*€¯+Ð?u/lÑSLXÈ#*—=Y:ì¬KȯHÖjFúÊØøòüUæßa¶&_
+˜Iœs`î¶ÇRÌÆ!1id@º“fˆê4Ü‘\ÃZ‰K³Ü ‘Àoñ¬jšf†õy@¼¤_v€ð~·œýM¬À~á9é)P G‚Áܾïô…ä"”Ǫ¤cy"{߃÷ïi5õcÑÍãj×Ì,ÂÐg*£G=ß°£¤¶™©íÑD¦x6KUÄ4xX±JPŒ¾§?½
ìZe¼ a–fL¦ãŒLÝ×+•Ÿp`ÛÛRú2ÛYq+ó>Çž€•.u{ü À{G*_†üø~ÜÒ’J)]U¢µþM¬¡!äe…0ó‰]Ò¶h¾YBÌ&( ³_<1Æ (#fÉ3¹ÐªšB½;
ÓOȾI7wàr„æýÝçÀmHÛ`È™ÿGe×2Ïwå|äuF&®ܨÈLßÛÌÎf Ü9Ž/Àk6r•Š‹rXèÔÁcl8Å–SÉ韻9‹&…wuT4Òiá§àsŽy¾±ãm ®ƒ›oF&ëyºÈ¿Fß× ˆ–#/b¹e_¤„¡á•ƒo/€oKµ“ÓTݱË}P’ë
÷5’[ô@¯Œ±€–¾ÐVîl„.v{†/ñtÜC|%¸V.ý›({7_ß—b= ´btfqÅž§p7†u¬¹„t.`zŒë÷‚ñ~ àX§BPVvuW™™Ò^[ ) ‰Û^¤,­ô–r¨ä!¢¸’ÿ B±êkôÌôïÙcжÎåmú›¥+ÞL!Ì“#óÄ÷x”EåÍ{Üõ¹
©ð®`ràp µýZj¼¦ì÷ âba²²ö0ð3L×Å5 …¹_l5üP<‰Ó­éD<±þ|¢Äƒ¦=0r ó0€ŽŸP<Ñ›Ðñ’A¼2kôPÅ“›>¨0íÁáM½kî_ö4À& j`ý@EÑ9«'è\)žL¯@š93ZàZ«±ÑÒÆïA…4Æ~‚(¢ˆ9íÜíß¾6ݺe4a{ëPªyNáSº'õŒØÇÙ‡ˆ5Zò¸›G-§¥H›§µ}k Ÿì¢Q¦¡8
î:*þ¡ðöV¬'O‡Rœáz-–ÛJg§»%YSè`Ö€Gð7PüZÔʇ#‘%£ P/Ç^êÈÌ΋Z²˺-*­‡¹{V­z+PÍϰd0-?ŠŽù|5 Üžï›KN1qH×Ûz=ósZ\$îËÌC+øBfáñ D‡’R*“ÚÕÂ%B6Mú×(†‡HÈ¡^QÆIç‡÷®G`UáI3ź“ $Ò¹%I=ncÐpL"µŸn©–üçs™¤øèÊc:‹Ü"?ˆÍ¯x¥1²ÎÉÃob˜ôI[Ü_î¨xJ2sˆ…¯¸ò©¾cææÍÝ.`Ûý̜ۙOHjžÑóJ<QÅ!â ųøÝð}íó
h¬'«ñÄo]vCCK¤© «W¢ à8›ê hÖä§òÚÅ26Ôx"x<™fvEÕx b|·;ÈÇ“VM@ÿ²Æ“ÝwíÝJð…aœÆS0 ©\}6'YW±])ý¤­Fh´}b®fŸâèå‰Ì¦ï§Þ2™µJ„%§NeËrèì¦×ÏÌÏSÈB\EdÏMq
ô~D{y“ 38™I߯oýq‰ÂGkqz{+HJï@³­4=)ð0¦Õ 9C¥ «¤ú´´ãÝç=ÈRu&Õ'cB¯aQ 3¼L»ß ŵԉÝìîÅfT‹“ îŽ-ÄFµ´²Ž÷E8Núó1³S®¨µ" Q©w/7þeW{zðSµ$óX^ïwÉNfˆM7ØÿívSl~ìm*—¡ @ºòÁ-ŒÔÅMžþ3¯n-ÌSZÊKþ²k%Áiˆ’èÛîÅÁ[4" çK«ßÜ
ûDð§Áo;
9ÔÎq³ñ­\[Á±‰VQ‹.øñ.Gð¤$%Þ?×h#¨ÌtòæSŸÊ”Œž{ýng‡›èeÎ
Òóñ-NBö\¬ÞÍ»[Ëþ¦…ŠÖÏühؾ&rU)å8#yÒ/”tû±žNzÊÖÊæ÷+wþªÑÏ-A¦¸–=Èå'0m]¡ 6f*² ¬(î›
ÙbMJáðl™Ž*)(©Ü­ìÉÌ“IºÚ*ªÅH1¡þ&mëä઀Øl*ÍE÷× bÕè÷'jÄklà2`ïÙ ÃÖYªÎ݈Oð 8<ŒØ*”h4R š@¦ÐC ò/vœü¡• ÷†J+¯’ôW„YkdPÎi/è'ëRy$V^K>I³I/Ã->bG1Ä×ûd.„ËŒs£$¬ îÔ66]`À€JM »Ï—ä~ýðø'§ÂñgRMÏ=9¹%hc̽ L9¡Øò®Aú9_TÇ ëžêš4Ì\yw?ÍS—¯7ÂðˆL2mìQ38r¯îÕaÙŸýOÐ(C‚ž²@³5?ø@;cen2«´ið»~zÐuQE üë €‰Š«Y•ø·s …öôï7M¬ìlPs“÷ž"Õá_§@è#§Éªÿïâ_n6¨R¼qÑ¿ÀP ýe
þmn)ˆf–­×'ÍßÜ®ìFÅÛFöî/ý;2™fðožŸ³äè_Œ`:c…þM‰0\
­ŒRÇ¿ZÖÿ­ã£ê 3-„ô¯Ý›¯ ÒWÿ»DFHx3'Ø‹­A
¹ÿº7pô/ ŒAig|äÜðo@ž¹ð&¿+Fÿ63e—ÿF!¢uÅQm\Æ«•³þVÖwÚ^ •,MÊ´ÿ^5æÈ"$?™›Ä ¦ &‘•³m¹½[È+u§¶À­PMî4W) ¥%:¶Eßi²€yøˆÿIY´[(´Ý®ž]©9Œ‰U3o¨ýWR?Óº@ˆk«É-ì–vÇ9IigW¬øßCˆy‚
®§ÃÂIphôKö5'5” i• àTÔÝ/$Å{¤/©T ¹›¿ýØ©Õê4]‰½4'±—+îNs/¤Çîn˜xu©ø!X„³JQÜDÂOÅÌ—w!ÙX”Ö^åwx®jùy?!¢¯ËnÕ²àiB/ pþšým÷ëÏãï
žY,wÙ¦¥Û'__qGæ‚ð&Y·Y`P˽—ߤää;–ŸÚ…e'-€Ù\ñެÊã¯õÃY0ðeØ5Wkj* š=SQmÜ<iQˆm¹2ä'4Øn¸ŠÍƒz|(ˆ#ÿ|®ðB“ë.ÌXà ¾šËתPk|+lš…EÀ:›â€ ™eœ…W¡×ëDéæI¾Ò  Cô£ôÀHsŸÌ·«×®ß†Û¡¾¹a¸ÍÏ´e­ÈUøËý+„ ×@ϧ&åñƒ¯á¢Ð—ž ’Cq%õ¥´88İB h|Ã4êÓ²–õÚOº™Ž…zË&¦¦èI©‘-­‹ê¬ ]y_¬SŽd,Ò†zIkÎûk7™ õöˆ¼ÇN5¼"+õ¶HPÙ»¦h¥€Í-ššÔµF3#8Ø„x¤8ŠâCJÞ^Q^CÖ1WøØ§Ú縶 ÕÙÕ…ŽäLq7S,$›¹l…q9(-Êd¢.!×̶XS%¨oeÐnÓ‰í¼h.'ëþí”ÌL—yX騡?HДÄZtª'«£¡©}fX½!otª‡¾‹ÖvËÐor§‘zOÀºr%r*ÎÏz @Å·ˆÌh2OýÔä·¦+Aí)NˆsZ‘cÆ­:ïCñIü+$°u¼ÆÔr1¦¸1”Éy¬äÒó­±LŵÊïõ<›µ,r³r°ÇÜm)¯%ãQ3BíQû‰N©×O‚^mÁ‚o£íÎááé2õæ'Kþ5Ê·Ó›+¼Ç.ý0ÿÐÙ00jO¸â©Ý›i½Üv>ß_вz1¢7¡«NpE ¿„bY¾÷0Ñià} ‘”žtó²<PxTƒsæ¿z¥žiUïÔAñ¸PødI¿z'ã£S§B¿Òšp_½¶øæU¯7ª'µÒª¯›îÔTL©z ¥ànÝû5®^!BèVBƒgvE‹G•š/YÚŠß«4çfzõ²KïûÌä«Þ‘shv#v"tõnÏŸ+¬CJdâßrÀ^m‚ž®zMÚØ×¯q¼]·Q¨’Üšƒr© ¹ša4."¹¸’üÐD8`U*ªÛôȈY©”ž|zšˆ—4
ôH,UÄ,´dLÔZ‰Ê¶<™Dkª‘”o¹/„ì$¡k™ùºz=†x;²±rv‹xÒ LV¯šhÀsm±‡åiÀ„[ÿÈMü]† êÒŒ²D§ˆ2ž§×©·³»—X‰¾Ý¤De=¶äEdHp¦Í+`j%¬_H™1_ÊYc5;½¡òkxéeì̘$@¼šg/Lèºgd¸~VAs2Ï-»àŠFïÉEr¼r,Éôå›ý¨vÝ ¨žÓ+iÎF3ãAúà ԛ˨²¬hɆJhÞìÿvÆfè-ß©ëשáJ.=ŒÝå»÷;á OŽe^_V4NÝ7o–“QC¥ÆxxBýé¬ÑŠAÑ—ðžóyu·$“¹`IMA¶;™2zwµÆŠOÇ+™Ap™©šo|wG„€/ÁIÈoÁõ.2ª´ÕX7ÖP3SY­ôcAijs ¢ò¯¬h•?h®OA~n¼›Làæ×¼»ûŒ¡dø£°w·/Ü*Z¼ëÔ=K…mOd‹»x7¬i¬µ‘\|wǸ‡ÆöUÞÿÚïnÌ¿+Ê%øQ õîÂÞ²ð%h}ytc³µjFÝÄ»’4dÌÊ»»é"5„&s¬[ÚÞ9Û$Vµê-³ Æþ%ÅÞ,h³8BdHêųÞÜû{ŽÉ-’n=ƒpë·•²pø;ñjÑãнÃ.ð·6ùqP0öÏXo©‹¡ýé²}#_9I*³¹³-WNµKV:³7HSœïg«™¸’µ&ïÞ’M¾¸ÑϺñ/!¸Ì¿\±ñËòs‰g­ÔåÅÌÕó0ÌáW’I2(‹ÎP‘!Cv“Z£È’Âô)/{赢ݰ¼8⤑;±€
®Hlõå„cqÚÛôÇx\1Yn[‚Á‰Ïòɯõ°*H•Öñ^·Ï&ö]—©p9UÓ$tcÞ‘Ÿ`+¦ÔKØáSr¹Úµm¡?Ob¶6{bx͑ܬQºI*^ËË1ÖC:AÁ7V´Þ“aà°ÿ7fûR7áTP@_ ÞláÎ.³Âìkµ‹(tÇ¢´(RIÓ;1Öñ‚æDTÿqÂ5¬ªþ}DÞhÈåÛï:xCÍè  ¢4 ˜€zoæø´¤±ža°SÚþ°k F-LY‡ã£w>ÏñR:Å춆.å–[IÆ=ÙÇ» “”pÇn
'52ŒOíœ.qr­` Æ0Cûå`À.”ú• 4¦í´Sð@Öäzhvº r%ÄÛƒ [u"#ì…%¤îþ¨6Çënï$àÀØî×Ñ_ù¬ 4ÜŠ)^–åÍ`z‡Þ±p©|y^ô¿Éý(éÕ“ð†Š1‰i£÷Sª7~¡¿†D1û¢’ ÁF|™ÝEŬ%´àldªE·]Õ½ÝSEj:º4cÂ&»,«8º5§³Ýß=ºV;“=<ºà}ŸÞ Yl}AÒfwŠ®xtß0e¬ð ›£Ë…—…®¡wx¾Š2æ,±µŽ„ùÎá Pk·H×#æµ @^3v€’!Ë")FNl$5‹½§£^ “ô±¼PŒ\±üK›÷Á”娤8CEŽâˆ{ ŠãÛ_GlàVþŠ´J…÷ùY(Â,¾6Û=ßZƒ½ó­gø=ù{œ²aã“QÜΫÙäÛ¼H¼Haœ×ÚPQÍD²Hº(H‰1Ì‹áüZÎ˨-î3ÇȬë—M&¤.ôùòæ÷}Š-
ÁŒÍµzF\Î$…$a±–ËUß­*tf„Ê— íKyúl® ÂÁ£nYäfüi ©èÕ'Ò-%–©G£Ó¼æIÉ®—øÚ./jzÍéÔeÂÜßÈ¢!#·bó³<ŸÙÿ8Û{`}ô9lw7Ùd¾·ç×i7¹ ܈ŠäL¸vä’ï§çö$‰Š¸Z¼‘FÉ&¬ýÃ[$c¤²UGñ–h#þÅ=¥Ÿ%Øý¶j·D™¨é®Ïóˆ³ü½»“ä|$\D»¶ý žßCiX¢q™÷^_'©3P¨t2ÓÍüTlšpsLW~´¥]šá9 äÁt3ágc— )]}
QÿAÜ’r˜®ÿYä_é*”0Q2]-çk±ø’®*Ó…ˆËÇÙe·Z1Èt«ëK:ÎtÏVM›ø„O¾ä\-ÌÌë{¸ÝMs=S”Â@9о2 Þ
â'=RÄ\:dÅêÊÈr°g¦2ªÄ÷pîÝ&JÝð×"¡î
6SZ%uºÂ#® vï¼Ö_856XÄœa¬}Ïà&“Á—uå(Qpæ“…r{à}C×ÙDé.r*Ã7}Ïd£9o»Ú+fÀÔR­.`͉GJG:>•¼"ði,¹¿&Ëa%#[ß›Ž):xÚ&A*î® r—¤,POp@l]ÚwJ}І†ò’¶"¤pNWcÈoz‰†«5Næ½)MÙ¿PBŸiŽã€ðšÄvAÂÌÿI]2×1æÕ“Gd„ CzÛTÌåÊ.·Œ‰»‚í] Èý­~Ö^¨¹ÆÂ1}Ô‰b ì1ñ*_ ç”a .,¹ …œ_Ìj7p§ÈL±…«3Ã<‡Ã?zës+B%JW3Ø
¿ nÑ™–Àp¨øIóÚ§jƒ¨°ÇQÐÏê4ˆ^¤Ô ÅÈ%„ýÇ7AÆ­¿ÝLdqQ©ÂÆøö×ýaNÿ{rX(ý`¿Å»Lt¿“úËÔ8/Ùáϲ]Ê÷.Ïñê;é’úv ± ›ø‹Š¶ñ5ä:qš&²‡POc±ÆÂ0^£Ìû>Ðt1(Md”).9n>Ü:^ï]ùþ°žÉIÏeˆ0—{ß‘´ûg×=½Äƒp—ØýO‹³»`R)¤Œ¦j{òôUmRç‰$Ïu”ar”!“ô9d×)H•w,3¸EÿÙ¹LcðЦ›) BÓš»LGÿH›-†q=ØT½ÿ˜
¯·¤™u‹¤¸í´=®êÐr—?‘÷©ï;ÃøÆB7³fü´Õ7míé RþXfžäi1—.o4¤7cd
‰!^`ÏV?ÚAºû=GS#ïâzÆs«+ÿPík蟪Kq©¡®LüCÒ2m½¥$cÇm\Þ}ÂV¯œ¨m<†Ø˜Î}Ø’Ö…ù­÷e߯ €/ï—gi6.®
¾M\<uGU]ow‰å‹3H£¾Ò÷lMñ)o˜ÿiYdÂí`ôò·––ù´«ý¶_@IÇAc Û•=€G
Ø}÷×͈Xd£Ý€]Î~¨ìÃ/îÛXŽþÌ„êj‹ç»®›ìª¹"×_ìˬÛ^2~ì;ŸüÕúCH±áu®ïXXŸ})Ë'¼†øvöPáíÿ$ö§Û—ÿWdUJ<¾\Fk\W´›Ìµ¢Ú #¬²+]À‹Ö©P7‹Bii‹¡Å_!í…Íòÿ¥„g‡j p5¨_"Q8% dKáÜ »ÏPÿ़GGŸNFNºãyN‘²‘ÆG8XS8UÙõ™ð5ú_#›å޽¦ ÐT®òE;¸áC¤ž€8ßš.ÑM\+_€ë°á?o줎Îò5ëõÝÔ$cšâ«¡*l4L¶¶A` /0y+FOx9¶«eÄõÛñ{'Û¯H™"€!*…”{6pj‡ê![14¶XTÌ´ÏÈ>GÄÃ>I±pʨôÃÒ;¥Ã_ó9T/ý"Y=hš>¦§°‰dw í•_”kcë1cxID Ö[Z"^¬/‰áãsÊŠñs–ëÉçÀrkÛÆ'ߊ/'~¡–iP.·ÑQÑ-Ï& g
&[ºÿËT&|¬ÂÇ{ÅÀ2ži:€MæŠÂmRPѨSù_'@2¾¬ ß”]ªh}P+jMŠó/­ew†tàÒ?÷Ù])˰Ü_ë\¯êÇ5Í»5Ù ›TÔÇ ŒÃ
^äPq¡Bæ¸1D“,|,ª´èNI|³ ŸÕÊìR¼üz½F>®ï6Õe¡L«ñê~è›s¯[¹pņŠuaÉb©†±ƒó&OJ5­¿ ³]Tcð³®ïi~ÿà'&Ãä4z`ŠÇ”¡ã›ÓB#+c-Ò7°«—÷‹nÀBèŸÁ†déKDˉ×öK5rjF/›Çí9Þ½1Í\@#WÂO”â²|­VwH>À'š«]fâ$ŠhÞp®´¯öo¨7¸<)%õ³z¾û`†hµ}ªT)ä… ÞðéÓ“ßÝji¸ˆÖ.ìæT‰p<cëètŽ÷þš]=ª)Ž)Ú÷~ÞÈÞZ;ú• 08›×õs\q÷I˜#ͶYÉ%0yåÿ&×%ƒ™uŸd…ãö¤ú­F)"ÞÒwòRѼa&©U7½ÅN{Ãf¸"˜_ b÷'N#ŒèN
).¼—Å1wä_Ž™×ÃWÖý «–êõêi‰Z1âõ×ý˜uöûFœ0ƒsr8”eÔˆ´'¯n¦‘=ƒNóôw ñ“1ÌbI²pXfaVaDHW#YMAë½õÚ†oåÞsI pk¹È4ÿzGh?#Otý§ óùêÒ†®V%#o`ó ^
Xßy®ßN¨Ec~tHÊþ ô¹áFšQåºa«ø)g[@!ñPnÍ»&[Ù´ûïZI@ÖËÒvÃDAnÜzXÔ¶ˆo’—Z¢/V,ù¥õ°"âqƒÊ"F:Æ<là”èÑ¢3”PÀÛµÉʇŒ&œ¸ÐÉâ —‘ 3?]ª½þáÊ€q¡Ùûw­ù%LšøOàHôË%챌Ñ"ðó¼Bƒ õÔ¼‚Ðÿ²„¤de2\?á€B*³\OÏ4)HYî[næL©–JsóÆÒ¼€F¶±–™Êܹ¥µW/‚V«FÄþæ.& Ô=B–ݵxmóPP©U«™¬Múå°­škFÛ%I\ø,£?‡eGTv!æø<Ù ÞÓñy¾…ÀœÐá\Y§¼b„÷ë»2QGºÊi5[Õx©‡å×Ö±‹NѦåþ6Û¨ªQ 7 `f<`(ýû>åp
¯´7¦«öYíë÷R£ 7
ÞæŒOÊœT5}/lÍ’aå£ÕuŒñ SmGwͱNðÇ`رp33x¿·¸´´ýí/Ýk-³+먡$½æ°÷JSíîÛ/º®—&©±½â
·“'936Ê(müu ÓHäÐŒ.Íñø Óßmé¯%¾VK'ª§ZBgà‰ƒ„oö»½w!}+ÔFÖÒ%O^>ü<(X|&œã9sBæ»üeBÄl#7Ë€Ÿt2Q
q‘OeåFNú’Ý—Ú_ahýÉ&pÝ
Ñ8ô¹hÝe÷ *ÜÃCûf‡sx™ÖÊÞQÔåðÆ•‡bŒÍœèl‚€“ÿR˜6g-öJ©Ø ÿ
æúèñ–ò¶Ï*ªÑr™žèpæ ÉlMðM>&¶Ì9¥õOš åøé›cðbdX"Y¯!Z¨K‡É0t ¾‚V»wXH´ÚNDÎØ¾í#.¦Cyº\S¸vÆvL^ÁŠé*£ó~{³>ÜM1õ2@fî øÌòÐ*ÑøôEžm¹‰`õ¹ÍúI?ßws·ÚŠ0®‘ 6mî¢9QëBØp"à}‡¼‡AÕ—73œãIäÛW†•Ç[_;ó½³×ƒŒé7žº("s.ÖÖx·Gõ6`ˆð™YeÇh¥@gÕ¥.ÿ’"ZÛxÄ¢2µ|…”=õ‘Ü`a¬ÃQ@S‘–eCw¡ô~B2½—þ_ôb¯ÔPêa¡-|²ÜÔ]¬‰‘ˆ/1ïi#õ{Ö;ÀÝ *åü2ì†t"9szè}¼|ÈÝnçðÒ¿~JÇ
:>9†œ¤Iöå? ZcDŽÛ7zgÁ5ÊÝDVâ³M*#Å2bÈoHJâr Ê /^´ú?Z•ô™Åax¯ae™L¦†&ù°¦JèŒ5‘›È,°hÓƒ·ÉYí 9>Å|Òäd åÛ7æÊ“çV^ ³®¦–ŽU¾ä>ত/Þ:žà³¯ÿ›U޵e-îdÙ¾2ÖTãÅ) ý3êv§·¶Þ©ãÃÅ´v⯓luzc¼L&x0Üaø9™Ûd¨Àö‰z¿¯óoÙ3üœÕÍ}oƒà ÓA›•a
Q<ž")ÐÉÝ1ÈœZ·oíò¼dpHÄ “10¸¼—?:ßdzn²9$\­ƒ¶´mu¼Z%£Y)rpý(ÞØ0EÍÍë„ Û§¸Ÿ‰:{ è/9dÉRõ2•~ʦ0¿]²X˜2;T³Õ«É7¶7œ£Ã„,BX…;þ<vº,ǯ`¥èt€·Ðx£km}cÀ£À
eÍ«€ýaÓ~ ¾Ã‡€ï!çÖ7šUº|üåÖ×âlQ Í 8¦<8(8×`|{)#NA¨øb$ ÖüÐò÷¸Kߥ`7ø‡&ZÓ›QÃäiDšÇl?Æ5´â¯øbĤÓ7dªŽ ëÙÈËNNª1òœvœðG¾ßT]wõ‡x39e«ECxa%×Ä
¢-[= åÿ”’ÕöFDåºO?5a™mŸ­|.ùvc¶xàN(²U$ã|±'ÌÔåÂHðŸMVÆè¿Ò¯žF«ä€ÙèUozº¿n¾º!4¢ü-Þ¹¡œyo¡‹~!’7½HIc&ŸÂÊZlúÀ¹
µ#”í¢®9Èæöy ®;£ot3ƒÊpOÆ¢éE¸0JüU)‡ÓQ;œ±¤š%X]8UǬ&zÿøÞ?w õo¦P—Ã
H°ö-ú_:±+MOð 3Ū/~wob8AôÇÝ^÷ÁøïHL•þ?YKÚë[¦éîHÊ@&J„øMeDóéTJ àÎÒd’åy‚ê0“öóÉ0:¸´Š£).)ÿ§§núLÑdÁ*d#$—Ë~ëA{°ÄUp½3µ0Œid÷6*d?@d¸Keho-Í`J5ÒÑe»p)ʱí§ƒ"­UN¹óùNV[ÓKÙ%i^ÃnMßnêÛŽZBÅ©K0a^™¸ùJ¨sõ–~Ê¢dʦ¥vÏÃk° §P-í[ukYs¼P2~îxIÃbA8¾°F£ýŸò}üµŸ²Ê9™² ?KM*$Ó¦,
Ù]Ú_=´U@§DHÖõaø}Zü@â8™óÓéhÙ!\פ`Ƶê++PÔà(Mt<˜Næ,[„ aY&-Iü%î§p«ç ¯¾@zÛ}Pì¤Å–Ä9“ÏÒ÷‰ãÞPQQ¾Å|ê*A<ÀäS™òíhcWÍ
¬8Õªw˜¢%}ž£aÿü7ï8l1¤}qIÙÝq¦hΩõ«ŒÊ›ñÛ
Ä|]L' :3’Œ¥7àLCíCdÚd ¾¨ßo5žÞ3;=¢L!Zí×|©Ñç–†ùÅaO©lÍÒ¬Ì2€Á¤ÁÛ¹±œy%Ï$¶1O68•ú¡ë,æõÊ·y€C½ˆo—ñ'ÙA£õÁþ¶ç¹„4fsB&°µИ!xÓü˜ˆ=û‹÷‚{Þ<ƒ‰4Ê‘/Ý´h·m®+˜ ¨QN6œ¼`v¢E¼aëŸ4£J6qQKqÈM Â]ãÈxÜäLO… o‚Õ.¾m=Ú‘Žê$zÙt·o逩º'‡°’êGÔrþ0,_åfyž›œ¸èEñà„"ÀÔnHâx†&¥K6ï˵W ï_êœ6M3ýi/S`n+ã%…ƒ¼LÝF5§“¤×~}Œâa±Å’˜:U¤r“Á,bþ1Wn„,-m×|÷ù—Kr­å‰œ
n×1ø÷X ŽDÕŒU6é©ùaô´3ÙVƒ‚sã…‚K¡2#ç±ÅaÖ¨zúHû.½vø„ ›Ìª8÷G9Ò†Bl •+5:½ð9`ŒY¨WŠÁ:}!u K4ØEÇ—¸k >sRl,NÖ\POyhó"YÐÀÙRi"ØÏ¬ÑXðz94O”’Å‹-eËŽ€•^û_ãJoõ1 “ó
x6zOÈLë+k€é"賈Ͳ(n܉€ !õ 3rhœôÁ/-lcªU´š*Ɀßp˵·þ0Å¿”¦^—®©TwYdz{³£¾ŠºÏï“ÍJ“¼jZ Båÿ%÷æ¡ò¼â©¢ûëK¹—ëUûÌâhŽit«æÝÞ–t-‹7):”º˜)æÕ{ºìµï)ÞkxbÀ~¤æó«}‘·Íõ(ä†Q* mÖ< ÒꌶùP™‹—‚oq"<¤4/D ¬BþþºU)¹=Ú[RÓ_:1z»@òqó@š”–­äFKr$æ9ɺ^6@-˜¦x¶J,q =µ8¦¼{ÍiÍ[™xŠ£ë‚›7 Ëd®*TáæÔf=‹[Þ‚¿Pƒn³\Q1ŒæŽ³À$öM†€²"¡2؉ò—Á(xu>ôìÚÛ¹_—‚çÓ?Xœ NÕ?ñõºÙCÒþ½\GûPe Fä6FÙ‚FʈšôÌëÏ…9åþ^Ðh•XQ]hÜÇÓ"NË1;D°êÜú qÓa*[eß@]…ü{bL@KûJ"è¸Y«©yB±ïJ!;øP?šTedÅ@+'ÐáH÷sòSN§jjYÄò³HsÊ ‰cߌxÄô„£
¾Á
í~öãBý]luú4Àð~Ü0u™h#)Î}R·VL0d¶@¥Ï„°ÈR[ *Gø%Òw âQ´Y*››¸dĪ'·}‚¦²«úóø{›È}ëd£$NÍrˆ%!R|K3jÍšÐ-`ÛÏGÎ iܾ?umLð†ú î¹wIµ„à•ϸ˜` Þq8{"p£¸ƒo‰"õ¸±­ ÉuÞ ÕÌA·ÙŒ3"'~¤Ä&ûÁü˜~`Ú^ˆ`¡X‡r20Éä/A!úˆóÐÇñüÍÿN@ì ç~}>>É9ðþ·êl‹ú¥ i®†á„;WÀëâ½hô¯µ+øÕÀ£h"S·¬­Æ2J‹êð¾ÚÆôe…¡+ØŒ›nK«Õœ‰üõÇ -—æ¿?àÿMÎ-ö„FX?$rBÊUn „÷h{u£-Ĩ.'Œ&µk %§@Œö(SÈD ó8™§æ{íóNpdn,JôVµòbÎeX†õáK²€Ê¡e‘ã–üóÓðæäŒ²æ%YXƒ³‚W{wZéJ£»$t¢¸y_R!”â+š}no EÀE8…MVÚ£¨jÁ×ÿ•ߢû¶KöÈ2ÃEÇŒQ¨¶b*‚¹Oôæ¡ùù—@uçhÛ/úÖò©÷ÀŸM?¹°U/9t¦z2#«w8$|ðQÏ\qc q{aÀú¥8r½y+*•Z£eà¶¿W`sƒ#{x4É|¼{gè¨8ÍS÷Åôúð½s/Ä9äb´p§EðTþÄuÎv¨kr²áŒé\×ñ£Y+½þÇô9ܳ¦!c«¨yK§vª<•ÊêðœDT£w?ÒÿER<T9i°EZ`ÓhÜOБžÚðRÃKš¢#àdá¥t¼)MذvÖ9› =ŒÉEvîæ‡†QaR¤¤Âuzå¯yÖ©‘wƒ(êH{)Ù é\«•¹Ž·£ÌeÆ. ìdljÑ_Qåa¤HüT&ø‡ÞmÍß»wÁ;( ÿv’Lµ%Èá¶Ž$6ÅÌ"ºÌN6Ò„@èÛ$"ƒeäEAÊ$,…¼Ù^£˜}%ZkÙj6³ïå€þâCÑV.sAËÈÿÒêU-XïÍffžð¬Õ7cTOPO÷ó¿cýâÛo·ù47_2äÌö?ò·cèÔìg¸ï_t599¾{³{ÌlÕw‡ï=v›Ñ#¿ßVÇÌØ¾Ç÷Ïq\¬Ÿ²+„Ï¡»[¨™ÙÊ»ºÝñ}±×ü†¯ñEÙ÷7Ó÷õßÓ‘a¿öww›|}°¨êB•±£z¾¦Ç×]…«>cïï9¶ƒîX„_Œ0áÃÞÓ¿ç»>Øë¸/þKõwý{µ`«Ð¿ò‹6låvÛ{¥\þ8÷­~FNŒy¿¡>‹š 9ræS×}ÿø dwÙ;~n²{ñáBwñáîË¿]Ý]6·3üW#"˲ìÒTH+¨VX˜¶©Z,mSÁ4ÒJk0MKåÚ4Ô‰´u((Tt¦{f웡fºô}Ñ¿c{lw뙹 [WÕm7;t¼Ïûaîò‹ìïçKvøxÝ;c‘—< !
£È Gƒ˜®"øOâ$'-@é¾0ÛHñt® ^W´Ã$9’Si¼HÕºvftsɾÈtìÜÃ5ûUè‹oµ â0Qž`&4.^í€TÒÓ
m¡¥ ’ŸŸ¬UsXnîp¥ÓŽ¢Ù@½çÆ¡rÃ`í†_|  cD£^Ü÷Ví´[ l?Þá±h…´Ö?C^;Ä3¸µøŸZ
I|Æ~1éi¤H¤%·ÀËms2 endstream endobj 8 0 obj <</Intent 16 0 R/Name(Layer 1)/Type/OCG/Usage 17 0 R>> endobj 16 0 obj [/View/Design] endobj 17 0 obj <</CreatorInfo<</Creator(Adobe Illustrator 26.0)/Subtype/Artwork>>>> endobj 23 0 obj [22 0 R] endobj 35 0 obj <</CreationDate(D:20181229214738Z)/Creator(Chromium)/ModDate(D:20220226201114-08'00')/Producer(Skia/PDF m64)>> endobj xref
0 36
0000000001 65535 f
0000000002 00000 f
0000000006 00000 f
0000000016 00000 n
0000008344 00000 n
0000008395 00000 n
0000000000 00000 f
0000000159 00000 n
0000049269 00000 n
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000049339 00000 n
0000049370 00000 n
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000000000 00000 f
0000013251 00000 n
0000049455 00000 n
0000008757 00000 n
0000013551 00000 n
0000013438 00000 n
0000012367 00000 n
0000012690 00000 n
0000012738 00000 n
0000013322 00000 n
0000013353 00000 n
0000013625 00000 n
0000013769 00000 n
0000015264 00000 n
0000049480 00000 n
trailer <</Size 36/Root 3 0 R/Info 35 0 R/ID[<0637B01B244A4B52B4C4B4C4E2A49950><5DCE82ED017C4F1688E44790ACB41DBA>]>> startxref 49607 %%EOF
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 3212.8 1212.8" style="enable-background:new 0 0 3212.8 1212.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#17541F;}
</style>
<path d="M1180.9,847.9v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3V848L1180.9,847.9L1180.9,847.9z M1184.4,723.1c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
s30.3-5.9,40.6-17.7C1179.3,755.1,1184.4,740.5,1184.4,723.1z"/>
<path d="M1543.1,606.4c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V598.4h85.3V619
c18-20,43.1-30.1,75.4-30.1C1504.5,588.9,1524.8,594.8,1543.1,606.4z M1514.8,723.1c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7C1509.7,755.1,1514.8,740.5,1514.8,723.1z"/>
<path d="M1838.9,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1821.1,778.3,1830.2,771.9,1838.9,763.5z
M1722.2,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11S1724.3,682.1,1722.2,694.4z"/>
<path d="M2034.1,626.6c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7c-8.4-2.1-15.7-3.1-22-3.1
c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V598.4h85.3V626.6L2034.1,626.6z"/>
<path d="M2238.3,466.4v381.5H2153V466.4H2238.3z"/>
<path d="M2486.1,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2468.4,778.3,2477.4,771.9,2486.1,763.5z
M2369.4,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11
C2377,672.3,2371.5,682.1,2369.4,694.4z"/>
<path d="M2691.2,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2715.8,656.9,2702.9,654.5,2691.2,654.5z"/>
<path d="M2942.6,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1s-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9c23.3,0,44.4,3.6,63.3,10.8
c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6C2967.1,656.9,2954.2,654.5,2942.6,654.5z"/>
<g>
<path d="M2633.3,932.2h60.2v17.3h-60.2V932.2z"/>
<path d="M2754.5,902.6c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6V944c0-9.3-1.7-16.2-5.1-20.7
c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2745.5,907.6,2749.7,904.6,2754.5,902.6z"/>
<path d="M2915.6,1041.4c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2c-4.2-5.4-6.6-11.9-7.1-19.6
h19.6c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9v-24.7
c-3.6,3.4-8,6.2-13.3,8.2c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3c-4-7.3-6-15.5-6-24.6
s2-17.3,6-24.7s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8h19.4v107.8
C2928.5,1024.1,2924.2,1034.6,2915.6,1041.4z M2907.5,963.9c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16c-2.6-4.8-6.1-8.5-10.5-11.1
c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3
c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1C2901.4,972.3,2904.9,968.6,2907.5,963.9z"/>
<path d="M2968.8,996.6h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35L2968.8,996.6z
"/>
</g>
<path d="M961.1,527.4c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2h89.8v-135H845c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7s17.3-39.5,17.3-61.9
C978.4,567.1,972.7,546.3,961.1,527.4z M872.3,624.8c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C886.4,604.8,881.7,615.9,872.3,624.8z"/>
<path class="st0" d="M290,906.9c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4c10.4,109.7,204.6,185.4,91.4,319.5
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C215,462.9,650.3,436.8,740.8,226.1c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C283.1,920.8,286.5,913.9,290,906.9L290,906.9z M285.7,825.1
c44.4-51.4-7.8-139.3-39.2-168C299.6,748.4,296.1,801.5,285.7,825.1L285.7,825.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 3212.8 1212.8" style="enable-background:new 0 0 3212.8 1212.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#17541F;}
</style>
<path d="M1180.9,847.9v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3V848L1180.9,847.9L1180.9,847.9z M1184.4,723.1c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
s30.3-5.9,40.6-17.7C1179.3,755.1,1184.4,740.5,1184.4,723.1z"/>
<path d="M1543.1,606.4c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V598.4h85.3V619
c18-20,43.1-30.1,75.4-30.1C1504.5,588.9,1524.8,594.8,1543.1,606.4z M1514.8,723.1c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7C1509.7,755.1,1514.8,740.5,1514.8,723.1z"/>
<path d="M1838.9,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1821.1,778.3,1830.2,771.9,1838.9,763.5z
M1722.2,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11S1724.3,682.1,1722.2,694.4z"/>
<path d="M2034.1,626.6c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7c-8.4-2.1-15.7-3.1-22-3.1
c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V598.4h85.3V626.6L2034.1,626.6z"/>
<path d="M2238.3,466.4v381.5H2153V466.4H2238.3z"/>
<path d="M2486.1,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2468.4,778.3,2477.4,771.9,2486.1,763.5z
M2369.4,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11
C2377,672.3,2371.5,682.1,2369.4,694.4z"/>
<path d="M2691.2,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2715.8,656.9,2702.9,654.5,2691.2,654.5z"/>
<path d="M2942.6,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1s-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9c23.3,0,44.4,3.6,63.3,10.8
c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6C2967.1,656.9,2954.2,654.5,2942.6,654.5z"/>
<g>
<path d="M2633.3,932.2h60.2v17.3h-60.2V932.2z"/>
<path d="M2754.5,902.6c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6V944c0-9.3-1.7-16.2-5.1-20.7
c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2745.5,907.6,2749.7,904.6,2754.5,902.6z"/>
<path d="M2915.6,1041.4c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2c-4.2-5.4-6.6-11.9-7.1-19.6
h19.6c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9v-24.7
c-3.6,3.4-8,6.2-13.3,8.2c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3c-4-7.3-6-15.5-6-24.6
s2-17.3,6-24.7s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8h19.4v107.8
C2928.5,1024.1,2924.2,1034.6,2915.6,1041.4z M2907.5,963.9c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16c-2.6-4.8-6.1-8.5-10.5-11.1
c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3
c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1C2901.4,972.3,2904.9,968.6,2907.5,963.9z"/>
<path d="M2968.8,996.6h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35L2968.8,996.6z
"/>
</g>
<path d="M961.1,527.4c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2h89.8v-135H845c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7s17.3-39.5,17.3-61.9
C978.4,567.1,972.7,546.3,961.1,527.4z M872.3,624.8c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C886.4,604.8,881.7,615.9,872.3,624.8z"/>
<path class="st0" d="M290,906.9c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4c10.4,109.7,204.6,185.4,91.4,319.5
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C215,462.9,650.3,436.8,740.8,226.1c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C283.1,920.8,286.5,913.9,290,906.9L290,906.9z M285.7,825.1
c44.4-51.4-7.8-139.3-39.2-168C299.6,748.4,296.1,801.5,285.7,825.1L285.7,825.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 3212.8 1212.8" style="enable-background:new 0 0 3212.8 1212.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#17541F;}
</style>
<rect class="st0" width="3212.8" height="1212.8"/>
<path d="M1180.9,847.9v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3V848L1180.9,847.9L1180.9,847.9z M1184.4,723.1c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
s30.3-5.9,40.6-17.7C1179.3,755.1,1184.4,740.5,1184.4,723.1z"/>
<path d="M1543.1,606.4c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V598.4h85.3V619
c18-20,43.1-30.1,75.4-30.1C1504.5,588.9,1524.8,594.8,1543.1,606.4z M1514.8,723.1c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7C1509.7,755.1,1514.8,740.5,1514.8,723.1z"/>
<path d="M1838.9,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1821.1,778.3,1830.2,771.9,1838.9,763.5z
M1722.2,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11S1724.3,682.1,1722.2,694.4z"/>
<path d="M2034.1,626.6c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7c-8.4-2.1-15.7-3.1-22-3.1
c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V598.4h85.3V626.6L2034.1,626.6z"/>
<path d="M2238.3,466.4v381.5H2153V466.4H2238.3z"/>
<path d="M2486.1,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2468.4,778.3,2477.4,771.9,2486.1,763.5z
M2369.4,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11
C2377,672.3,2371.5,682.1,2369.4,694.4z"/>
<path d="M2691.2,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2715.8,656.9,2702.9,654.5,2691.2,654.5z"/>
<path d="M2942.6,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1s-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9c23.3,0,44.4,3.6,63.3,10.8
c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6C2967.1,656.9,2954.2,654.5,2942.6,654.5z"/>
<g>
<path d="M2633.3,932.2h60.2v17.3h-60.2V932.2z"/>
<path d="M2754.5,902.6c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6V944c0-9.3-1.7-16.2-5.1-20.7
c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2745.5,907.6,2749.7,904.6,2754.5,902.6z"/>
<path d="M2915.6,1041.4c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2c-4.2-5.4-6.6-11.9-7.1-19.6
h19.6c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9v-24.7
c-3.6,3.4-8,6.2-13.3,8.2c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3c-4-7.3-6-15.5-6-24.6
s2-17.3,6-24.7s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8h19.4v107.8
C2928.5,1024.1,2924.2,1034.6,2915.6,1041.4z M2907.5,963.9c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16c-2.6-4.8-6.1-8.5-10.5-11.1
c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3
c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1C2901.4,972.3,2904.9,968.6,2907.5,963.9z"/>
<path d="M2968.8,996.6h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35L2968.8,996.6z
"/>
</g>
<path d="M961.1,527.4c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2h89.8v-135H845c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7s17.3-39.5,17.3-61.9
C978.4,567.1,972.7,546.3,961.1,527.4z M872.3,624.8c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C886.4,604.8,881.7,615.9,872.3,624.8z"/>
<path class="st1" d="M290,906.9c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4c10.4,109.7,204.6,185.4,91.4,319.5
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C215,462.9,650.3,436.8,740.8,226.1c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C283.1,920.8,286.5,913.9,290,906.9L290,906.9z M285.7,825.1
c44.4-51.4-7.8-139.3-39.2-168C299.6,748.4,296.1,801.5,285.7,825.1L285.7,825.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 3212.8 1212.8" style="enable-background:new 0 0 3212.8 1212.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#17541F;}
</style>
<path class="st0" d="M1180.9,847.9v-20.6c-18,20-43.1,30.1-75.4,30.1c-22.4,0-42.8-5.8-61-17.5c-18.3-11.7-32.5-27.8-42.9-48.3
c-10.3-20.5-15.5-43.3-15.5-68.4c0-25.1,5.2-48,15.5-68.5s24.6-36.6,42.9-48.3s38.6-17.5,61-17.5c32.3,0,57.5,10,75.4,30.1v-20.6
h85.3V848L1180.9,847.9L1180.9,847.9z M1184.4,723.1c0-17.4-5.2-31.9-15.5-43.8c-10.3-11.8-23.9-17.7-40.6-17.7
c-16.8,0-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8c10.2,11.8,23.6,17.7,40.4,17.7
s30.3-5.9,40.6-17.7C1179.3,755.1,1184.4,740.5,1184.4,723.1z"/>
<path class="st0" d="M1543.1,606.4c18.3,11.7,32.5,27.8,42.9,48.3c10.3,20.5,15.5,43.3,15.5,68.5c0,25.1-5.2,48-15.5,68.4
c-10.3,20.5-24.6,36.6-42.9,48.3s-38.6,17.5-61,17.5c-32.3,0-57.5-10-75.4-30.1v165.6h-85.3V598.4h85.3V619
c18-20,43.1-30.1,75.4-30.1C1504.5,588.9,1524.8,594.8,1543.1,606.4z M1514.8,723.1c0-17.4-5.1-31.9-15.3-43.8
c-10.2-11.8-23.6-17.7-40.4-17.7s-30.2,5.9-40.4,17.7c-10.2,11.8-15.3,26.4-15.3,43.8c0,17.4,5.1,31.9,15.3,43.8
c10.2,11.8,23.6,17.7,40.4,17.7s30.2-5.9,40.4-17.7C1509.7,755.1,1514.8,740.5,1514.8,723.1z"/>
<path class="st0" d="M1838.9,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68c11.8-20.5,28.1-36.7,48.7-48.5s43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C1821.1,778.3,1830.2,771.9,1838.9,763.5z
M1722.2,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11S1724.3,682.1,1722.2,694.4z"/>
<path class="st0" d="M2034.1,626.6c7.8-10.8,17.2-19,28.3-24.7s22-8.5,32.8-8.5c11.4,0,20,1.6,26,4.9l-10.8,72.7
c-8.4-2.1-15.7-3.1-22-3.1c-17.1,0-30.4,4.3-39.9,12.8c-9.6,8.5-14.4,24.2-14.4,46.9v120.3h-85.3V598.4h85.3V626.6L2034.1,626.6z"/>
<path class="st0" d="M2238.3,466.4v381.5H2153V466.4H2238.3z"/>
<path class="st0" d="M2486.1,763.5l53,49.4c-28.1,29.6-66.7,44.4-115.8,44.4c-28.1,0-53-5.8-74.5-17.5s-38.2-27.7-49.8-48
c-11.7-20.3-17.7-43.2-18-68.7c0-24.8,5.9-47.5,17.7-68s28.1-36.7,48.7-48.5c20.6-11.8,43.5-17.7,68.7-17.7
c24.8,0,47.6,6.1,68.2,18.2c20.6,12.1,37,29.5,49.1,52.3c12.1,22.7,18.2,49.1,18.2,79l-0.4,11.7h-181.8
c3.6,11.4,10.5,20.7,20.9,28.1c10.3,7.3,21.3,11,33,11c14.4,0,26.3-2.2,35.7-6.5C2468.4,778.3,2477.4,771.9,2486.1,763.5z
M2369.4,694.4h92.9c-2.1-12.3-7.5-22.1-16.2-29.4c-8.7-7.3-18.7-11-30.1-11s-21.5,3.7-30.3,11
C2377,672.3,2371.5,682.1,2369.4,694.4z"/>
<path class="st0" d="M2691.2,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.7,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1c-9.7-10.9-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9
c23.3,0,44.4,3.6,63.3,10.8c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6
C2715.8,656.9,2702.9,654.5,2691.2,654.5z"/>
<path class="st0" d="M2942.6,654.5c-9.9,0-17.1,1.1-21.5,3.4c-4.5,2.2-6.7,5.9-6.7,11s3.4,8.8,10.3,11.2c6.9,2.4,18,4.9,33.2,7.6
c20,3,37,6.7,50.9,11.2s26,12.1,36.1,22.9c10.2,10.8,15.3,25.9,15.3,45.3c0,29.9-10.9,52.4-32.8,67.6
c-21.8,15.1-50.3,22.7-85.3,22.7c-25.7,0-49.5-3.7-71.4-11c-21.8-7.3-37.4-14.7-46.7-22.2l33.7-60.6c10.2,9,23.4,15.8,39.7,20.4
c16.3,4.6,31.3,7,45.1,7c19.8,0,29.6-5.2,29.6-15.7c0-5.4-3.3-9.4-9.9-11.9c-6.6-2.5-17.2-5.2-31.9-7.9c-18.9-3.3-34.9-7.2-48-11.7
c-13.2-4.5-24.6-12.2-34.3-23.1s-14.6-26-14.6-45.1c0-27.2,9.7-48.5,29-63.7c19.3-15.3,46-22.9,80.1-22.9c23.3,0,44.4,3.6,63.3,10.8
c18.9,7.2,34,14.5,45.3,22l-32.8,58.8c-10.8-7.5-23.2-13.7-37.3-18.6C2967.1,656.9,2954.2,654.5,2942.6,654.5z"/>
<g>
<path class="st0" d="M2633.3,932.2h60.2v17.3h-60.2V932.2z"/>
<path class="st0" d="M2754.5,902.6c4.9-2,10.2-3.1,16-3.1c10.9,0,19.5,3.4,25.9,10.2s9.6,16.7,9.6,29.6v57.3h-19.6V944
c0-9.3-1.7-16.2-5.1-20.7c-3.4-4.5-9.1-6.7-17-6.7c-6.5,0-11.8,2.4-16.1,7.1c-4.3,4.8-6.4,11.5-6.4,20.2v52.6h-19.6v-94.6h19.6v9.5
C2745.5,907.6,2749.7,904.6,2754.5,902.6z"/>
<path class="st0" d="M2915.6,1041.4c-8.6,6.8-19.4,10.2-32.3,10.2c-7.9,0-15.2-1.4-21.9-4.1s-12.1-6.8-16.3-12.2
c-4.2-5.4-6.6-11.9-7.1-19.6h19.6c0.7,6.1,3.5,10.8,8.4,13.9c4.9,3.2,10.7,4.8,17.4,4.8c7,0,13.1-2,18.2-6c5.1-4,7.7-10.3,7.7-18.9
v-24.7c-3.6,3.4-8,6.2-13.3,8.2c-5.2,2.1-10.7,3.1-16.3,3.1c-8.7,0-16.6-2.1-23.7-6.4c-7.1-4.3-12.6-10-16.7-17.3
c-4-7.3-6-15.5-6-24.6s2-17.3,6-24.7s9.6-13.2,16.7-17.4c7.1-4.3,15-6.4,23.7-6.4c5.7,0,11.1,1,16.3,3.1s9.6,4.8,13.3,8.2v-8.8
h19.4v107.8C2928.5,1024.1,2924.2,1034.6,2915.6,1041.4z M2907.5,963.9c2.6-4.7,3.8-10,3.8-15.9s-1.3-11.2-3.8-16
c-2.6-4.8-6.1-8.5-10.5-11.1c-4.5-2.7-9.5-4-15.1-4c-5.8,0-10.9,1.4-15.4,4.3c-4.5,2.8-7.9,6.6-10.3,11.4
c-2.4,4.8-3.6,9.9-3.6,15.5c0,5.4,1.2,10.5,3.6,15.3c2.4,4.8,5.8,8.6,10.3,11.5s9.6,4.3,15.4,4.3c5.6,0,10.6-1.4,15.1-4.1
C2901.4,972.3,2904.9,968.6,2907.5,963.9z"/>
<path class="st0" d="M2968.8,996.6h-21.6l37.9-48l-36.4-46.6h22.6l25.7,33.3l25.8-33.3h21.6l-36.2,45.9l37.9,48.6h-22.6l-27.4-35
L2968.8,996.6z"/>
</g>
<path class="st0" d="M961.1,527.4c-11.5-18.9-27.4-33.7-47.6-44.7c-20.2-10.9-43-16.4-68.5-16.4h-90.6c-8.6,39.6-21.3,77.2-38,112.4
c-10,21-21.3,41-33.9,59.9v209.2h89.8v-135H845c25.4,0,48.3-5.5,68.5-16.4s36.1-25.8,47.6-44.7s17.3-39.5,17.3-61.9
C978.4,567.1,972.7,546.3,961.1,527.4z M872.3,624.8c-9.4,9-21.8,13.5-37,13.5l-62.8,0.4v-93.4l62.8-0.4c15.3,0,27.6,4.5,37,13.5
s14.1,20,14.1,33.2C886.4,604.8,881.7,615.9,872.3,624.8z"/>
<path class="st1" d="M290,906.9c-3.5-16.5-10.4-49.6-11.3-49.6c-147.1-88-129.7-240.3-81-327.4c10.4,109.7,204.6,185.4,91.4,319.5
c-0.9,1.7,5.2,22.6,10.4,41.8c22.6-38.3,56.6-84.4,54.8-88.8C215,462.9,650.3,436.8,740.8,226.1c40.9,203.7-20.9,518.9-370.8,599
c-1.7,0.9-63.5,109.7-66.2,110.6c0-1.7-26.1-0.9-22.6-9.6C283.1,920.8,286.5,913.9,290,906.9L290,906.9z M285.7,825.1
c44.4-51.4-7.8-139.3-39.2-168C299.6,748.4,296.1,801.5,285.7,825.1L285.7,825.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

+82
View File
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="900"
height="900"
id="svg3923"
sodipodi:docname="square.svg"
inkscape:export-filename="/tmp/test.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata3929">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3927" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="3840"
inkscape:window-height="2096"
id="namedview3925"
showgrid="false"
inkscape:zoom="1.1360927"
inkscape:cx="635.07139"
inkscape:cy="606.383"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="g3921" />
<g
transform="matrix(10.638298,0,0,10.638298,106.38298,-206.38301)"
id="g3921">
<defs
id="SvgjsDefs1018" />
<g
id="SvgjsG1019"
featureKey="root"
style="fill:#ffffff" />
<g
id="SvgjsG1020"
featureKey="symbol1"
transform="matrix(0.10341565,0,0,0.10341565,-11.43874,18.048418)"
inkscape:export-filename="/tmp/test.png"
inkscape:export-xdpi="116.02285"
inkscape:export-ydpi="116.02285"
style="fill:#17541f">
<defs
id="defs3911" />
<g
id="g3915">
<path
d="M 231,798 C 227,779 219,741 218,741 49,640 69,465 125,365 c 12,126 235,213 105,367 -1,2 6,26 12,48 26,-44 65,-97 63,-102 C 145,288 645,258 749,16 c 47,234 -24,596 -426,688 -2,1 -73,126 -76,127 0,-2 -30,-1 -26,-11 2,-6 6,-14 10,-22 z M 330,625 C 267,476 452,312 544,271 356,439 324,564 330,625 Z m -104,79 c 51,-59 -9,-160 -45,-193 61,105 57,166 45,193 z"
style="fill:#17541f"
id="path3913"
inkscape:connector-curvature="0" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+1 -5
View File
@@ -23,11 +23,7 @@ ExecStart=/bin/sh -c '\
[ -n "$PAPERLESS_WEBSERVER_WORKERS" ] && export GRANIAN_WORKERS=$PAPERLESS_WEBSERVER_WORKERS; \
# URL path prefix: only set if PAPERLESS_FORCE_SCRIPT_NAME exists \
[ -n "$PAPERLESS_FORCE_SCRIPT_NAME" ] && export GRANIAN_URL_PATH_PREFIX=$PAPERLESS_FORCE_SCRIPT_NAME; \
# Static file serving: Granian matches the raw URI path (before SCRIPT_NAME stripping), \
# so the route must include any subpath prefix. \
[ -z "$GRANIAN_STATIC_PATH_MOUNT" ] && export GRANIAN_STATIC_PATH_MOUNT=${PAPERLESS_STATICDIR:-/opt/paperless/static}; \
[ -z "$GRANIAN_STATIC_PATH_ROUTE" ] && export GRANIAN_STATIC_PATH_ROUTE="${PAPERLESS_FORCE_SCRIPT_NAME}/static"; \
exec granian --interface asginl --ws --loop uvloop "paperless.asgi:application"'
exec granian --interface asginl --ws "paperless.asgi:application"'
[Install]
WantedBy=multi-user.target
+367 -558
View File
File diff suppressed because it is too large Load Diff
+27 -27
View File
@@ -1,6 +1,6 @@
{
"name": "paperless-ngx-ui",
"version": "3.0.0",
"version": "2.20.14",
"scripts": {
"preinstall": "npx only-allow pnpm",
"ng": "ng",
@@ -11,17 +11,17 @@
},
"private": true,
"dependencies": {
"@angular/cdk": "^21.2.8",
"@angular/common": "~21.2.10",
"@angular/compiler": "~21.2.10",
"@angular/core": "~21.2.10",
"@angular/forms": "~21.2.10",
"@angular/localize": "~21.2.10",
"@angular/platform-browser": "~21.2.10",
"@angular/platform-browser-dynamic": "~21.2.10",
"@angular/router": "~21.2.10",
"@angular/cdk": "^21.2.4",
"@angular/common": "~21.2.6",
"@angular/compiler": "~21.2.6",
"@angular/core": "~21.2.6",
"@angular/forms": "~21.2.6",
"@angular/localize": "~21.2.6",
"@angular/platform-browser": "~21.2.6",
"@angular/platform-browser-dynamic": "~21.2.6",
"@angular/router": "~21.2.6",
"@ng-bootstrap/ng-bootstrap": "^20.0.0",
"@ng-select/ng-select": "^21.8.0",
"@ng-select/ng-select": "^21.7.0",
"@ngneat/dirty-check-forms": "^3.0.3",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.8",
@@ -36,39 +36,39 @@
"rxjs": "^7.8.2",
"tslib": "^2.8.1",
"utif": "^3.1.0",
"uuid": "^14.0.0",
"uuid": "^13.0.0",
"zone.js": "^0.16.1"
},
"devDependencies": {
"@angular-builders/custom-webpack": "^21.0.3",
"@angular-builders/jest": "^21.0.3",
"@angular-devkit/core": "^21.2.8",
"@angular-devkit/schematics": "^21.2.8",
"@angular-devkit/core": "^21.2.6",
"@angular-devkit/schematics": "^21.2.6",
"@angular-eslint/builder": "21.3.1",
"@angular-eslint/eslint-plugin": "21.3.1",
"@angular-eslint/eslint-plugin-template": "21.3.1",
"@angular-eslint/schematics": "21.3.1",
"@angular-eslint/template-parser": "21.3.1",
"@angular/build": "^21.2.8",
"@angular/cli": "~21.2.8",
"@angular/compiler-cli": "~21.2.10",
"@codecov/webpack-plugin": "^2.0.1",
"@playwright/test": "^1.59.1",
"@angular/build": "^21.2.6",
"@angular/cli": "~21.2.6",
"@angular/compiler-cli": "~21.2.6",
"@codecov/webpack-plugin": "^1.9.1",
"@playwright/test": "^1.59.0",
"@types/jest": "^30.0.0",
"@types/node": "^25.6.0",
"@typescript-eslint/eslint-plugin": "^8.59.1",
"@typescript-eslint/parser": "^8.59.1",
"@typescript-eslint/utils": "^8.59.1",
"eslint": "^10.2.1",
"@types/node": "^25.5.0",
"@typescript-eslint/eslint-plugin": "^8.58.0",
"@typescript-eslint/parser": "^8.58.0",
"@typescript-eslint/utils": "^8.58.0",
"eslint": "^10.1.0",
"jest": "30.3.0",
"jest-environment-jsdom": "^30.3.0",
"jest-junit": "^17.0.0",
"jest-preset-angular": "^16.1.4",
"jest-junit": "^16.0.0",
"jest-preset-angular": "^16.1.2",
"jest-websocket-mock": "^2.5.0",
"prettier-plugin-organize-imports": "^4.3.0",
"ts-node": "~10.9.1",
"typescript": "^5.9.3",
"webpack": "^5.106.2"
"webpack": "^5.105.3"
},
"packageManager": "pnpm@10.17.1",
"pnpm": {
+1047 -1073
View File
File diff suppressed because it is too large Load Diff
+2 -5
View File
@@ -41,10 +41,7 @@ export class AppComponent implements OnInit, OnDestroy {
constructor() {
let anyWindow = window as any
anyWindow.pdfWorkerSrc = new URL(
'assets/js/pdf.worker.min.mjs',
document.baseURI
).toString()
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.mjs'
this.settings.updateAppearanceSettings()
}
@@ -222,7 +219,7 @@ export class AppComponent implements OnInit, OnDestroy {
},
{
anchorId: 'tour.file-tasks',
content: $localize`Tasks helps you track background work, what needs attention, and what recently completed.`,
content: $localize`File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process.`,
route: '/tasks',
backdropConfig: {
offset: 0,
@@ -337,7 +337,7 @@ describe('SettingsComponent', () => {
.mockImplementation(
(action, type) =>
action === PermissionAction.View &&
type === PermissionType.SystemMonitoring
type === PermissionType.SystemStatus
)
completeSetup()
expect(component['systemStatus']).toEqual(status) // private
@@ -359,7 +359,7 @@ describe('SettingsComponent', () => {
.mockImplementation(
(action, type) =>
action === PermissionAction.View &&
type === PermissionType.SystemMonitoring
type === PermissionType.SystemStatus
)
completeSetup()
component.showSystemStatus()
@@ -652,7 +652,7 @@ export class SettingsComponent
this.permissionsService.isAdmin() ||
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.SystemMonitoring
PermissionType.SystemStatus
)
)
}
@@ -1,16 +1,41 @@
<pngx-page-header
title="Tasks"
title="File Tasks"
i18n-title
info="Tasks shows detailed information about document consumption and system tasks."
info="File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process."
i18n-info
>
<div class="btn-toolbar col col-md-auto align-items-center gap-2">
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Clear selection</ng-container>
</button>
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="visibleTasks.length === 0">
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="tasksService.total === 0">
<i-bs name="check2-all" class="me-1"></i-bs>{{dismissButtonText}}
</button>
<div class="form-inline d-flex align-items-center">
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
<span class="input-group-text text-muted" i18n>Filter by</span>
@if (filterTargets.length > 1) {
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{filterTargetName}}</button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
@for (t of filterTargets; track t.id) {
<button ngbDropdownItem [class.active]="filterTargetID === t.id" (click)="filterTargetID = t.id">{{t.name}}</button>
}
</div>
</div>
} @else {
<span class="input-group-text">{{filterTargetName}}</span>
}
@if (filterText?.length) {
<button class="btn btn-link btn-sm px-2 position-absolute top-0 end-0 z-10" (click)="resetFilter()">
<i-bs width="1em" height="1em" name="x"></i-bs>
</button>
}
<input #filterInput class="form-control form-control-sm" type="text"
(keyup)="filterInputKeyup($event)"
[(ngModel)]="filterText">
</div>
</div>
<div class="form-check form-switch mb-0 ms-2">
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
@@ -23,264 +48,139 @@
<div class="visually-hidden" i18n>Loading...</div>
}
<div class="task-controls mb-3 gap-3 btn-toolbar align-items-center" role="toolbar">
<div class="task-view-scope btn-group btn-group-sm" role="group">
<input
type="radio"
class="btn-check"
[checked]="selectedSection === TaskSection.All"
id="section-all"
(click)="setSection(TaskSection.All)"
(keydown)="setSection(TaskSection.All)" />
<label class="btn btn-outline-primary" for="section-all">
<ng-container i18n>All</ng-container>
</label>
@for (section of sections; track section) {
<input
type="radio"
class="btn-check"
[checked]="selectedSection === section"
id="section-{{section}}"
(click)="setSection(section)"
(keydown)="setSection(section)" />
<label class="btn btn-outline-primary d-flex flex-row align-items-center" for="section-{{section}}">
{{ sectionLabel(section) }}
@if (sectionCount(section) > 0) {
<span class="badge ms-2" [class.bg-danger]="section === TaskSection.NeedsAttention" [class.bg-secondary]="section !== TaskSection.NeedsAttention">{{sectionCount(section)}}</span>
<ng-template let-tasks="tasks" #tasksTemplate>
<table class="table table-striped align-middle border shadow-sm">
<thead>
<tr>
<th scope="col">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="all-tasks" [disabled]="currentTasks.length === 0" [(ngModel)]="togggleAll" (click)="toggleAll($event); $event.stopPropagation();">
<label class="form-check-label" for="all-tasks"></label>
</div>
</th>
<th scope="col" i18n>Name</th>
<th scope="col" class="d-none d-lg-table-cell" i18n>Created</th>
@if (activeTab !== 'started' && activeTab !== 'queued') {
<th scope="col" class="d-none d-lg-table-cell" i18n>Results</th>
}
</label>
}
</div>
<div class="d-flex align-items-center gap-2">
<div class="text-muted"><ng-container i18n>Filter by</ng-container>:</div>
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{selectedTaskTypeLabel}}</button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
<button ngbDropdownItem [class.active]="selectedTaskType === null" (click)="setTaskType(null)" i18n>All types</button>
@for (option of taskTypeOptions; track option.value) {
<button ngbDropdownItem [class.active]="selectedTaskType === option.value" [disabled]="isTaskTypeOptionDisabled(option.value)" (click)="setTaskType(option.value)">{{option.label}}</button>
}
</div>
</div>
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{selectedTriggerSourceLabel}}</button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
<button ngbDropdownItem [class.active]="selectedTriggerSource === null" (click)="setTriggerSource(null)" i18n>All sources</button>
@for (option of triggerSourceOptions; track option.value) {
<button ngbDropdownItem [class.active]="selectedTriggerSource === option.value" [disabled]="isTriggerSourceOptionDisabled(option.value)" (click)="setTriggerSource(option.value)">{{option.label}}</button>
}
</div>
</div>
</div>
<div class="form-inline d-flex align-items-center flex-grow-1 task-search">
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{filterTargetName}}</button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
@for (t of filterTargets; track t.id) {
<button ngbDropdownItem [class.active]="filterTargetID === t.id" (click)="filterTargetID = t.id">{{t.name}}</button>
}
</div>
</div>
@if (filterText?.length) {
<button class="btn btn-link btn-sm px-2 position-absolute top-0 end-0 z-10" (click)="resetFilter()">
<i-bs width="1em" height="1em" name="x"></i-bs>
</button>
}
<input #filterInput class="form-control form-control-sm" type="text"
(keyup)="filterInputKeyup($event)"
[(ngModel)]="filterText">
</div>
</div>
@if (isFiltered) {
<button class="btn btn-link py-0 ms-md-auto" (click)="resetFilters()">
<i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small>
</button>
}
<ngb-pagination
[pageSize]="pageSize"
[collectionSize]="totalTasks"
[page]="page"
[maxSize]="5"
[rotate]="true"
size="sm"
aria-label="Tasks pagination"
(pageChange)="setPage($event)">
</ngb-pagination>
</div>
<ng-template let-tasks="tasks" let-section="section" #tasksTemplate>
<div class="section-header d-flex align-items-center justify-content-between mb-2">
<div>
<h5 class="mb-0">{{ sectionLabel(section) }}</h5>
<div class="small text-muted">
<ng-container i18n>{tasks.length, plural, =1 {1 task} other {{{tasks.length}} tasks}}</ng-container>
</div>
</div>
</div>
<div class="card border table-responsive mb-3">
<table class="table table-striped align-middle shadow-sm mb-0 tasks-table">
<thead>
<tr>
<th scope="col" class="select-col">
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
<th scope="col" i18n>Actions</th>
</tr>
</thead>
<tbody>
@for (task of tasks | slice: (page-1) * pageSize : page * pageSize; track task.id) {
<tr (click)="toggleSelected(task, $event); $event.stopPropagation();">
<td>
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
[id]="'all-tasks-' + section"
[disabled]="tasks.length === 0"
[checked]="areAllSelected(tasks)"
(click)="toggleSection(section, $event); $event.stopPropagation();"
(keydown)="toggleSection(section, $event); $event.stopPropagation();" />
<label class="form-check-label" for="all-tasks-{{section}}"><span class="visually-hidden">Check all</span></label>
<input type="checkbox" class="form-check-input" id="task{{task.id}}" [checked]="selectedTasks.has(task.id)" (click)="toggleSelected(task, $event); $event.stopPropagation();">
<label class="form-check-label" for="task{{task.id}}"></label>
</div>
</th>
<th scope="col" class="name-col-header" i18n>Name</th>
<th scope="col" class="d-none d-lg-table-cell created-col" i18n>Created</th>
@if (sectionShowsResults(section)) {
<th scope="col" class="d-none d-lg-table-cell results-col" i18n>Results</th>
}
<th scope="col" class="d-table-cell d-lg-none" i18n>Info</th>
<th scope="col" class="actions-col" i18n>Actions</th>
</tr>
</thead>
<tbody>
@for (task of tasks; track task.id) {
<tr (click)="toggleSelected(task); $event.stopPropagation();" (keydown)="toggleSelected(task); $event.stopPropagation();">
<td class="select-col">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
id="task{{task.id}}"
[checked]="selectedTasks.has(task.id)"
(click)="toggleSelected(task); $event.stopPropagation();"
(keydown)="toggleSelected(task); $event.stopPropagation();" />
<label class="form-check-label" for="task{{task.id}}"></label>
</div>
</td>
<td class="overflow-auto name-col">
<div>{{ taskDisplayName(task) }}</div>
<div class="small text-muted">
@if (taskShowsSeparateTypeLabel(task)) {
<span>{{ task.task_type_display }}</span>
<span class="mx-1">&bull;</span>
}
<span>{{ task.trigger_source_display }}</span>
</div>
</td>
<td class="d-none d-lg-table-cell created-col">{{ task.date_created | customDate:'short' }}</td>
@if (sectionShowsResults(section)) {
<td class="d-none d-lg-table-cell results-col">
@if (taskHasLongResultMessage(task)) {
<div class="result" (click)="expandTask(task); $event.stopPropagation();"
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ taskResultPreview(task) }}</span>
</div>
}
@if (taskHasResultMessage(task) && !taskHasLongResultMessage(task)) {
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ taskResultMessage(task) }}</span>
}
@if (duplicateDocumentId(task)) {
<div class="small text-warning-emphasis d-flex align-items-center gap-1 mt-1">
<i-bs class="lh-1" width="1em" height="1em" name="exclamation-triangle"></i-bs>
<span>{{ duplicateTaskLabel(task) }}</span>
</div>
}
<ng-template #resultPopover>
<pre class="small mb-0">{{ taskResultPopoverMessage(task) }}@if (taskResultMessageOverflowsPopover(task)) {
&hellip;
}</pre>
@if (taskResultMessageOverflowsPopover(task)) {
<br/><em>(<ng-container i18n>click for full output</ng-container>)</em>
}
</ng-template>
</td>
}
<td class="d-lg-none">
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();">
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
</button>
</td>
<td scope="row" class="actions-col">
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
<i-bs name="check" class="me-1"></i-bs><ng-container i18n>Dismiss</ng-container>
</button>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
@if (task.related_document_ids?.[0]) {
<a class="btn btn-sm btn-outline-primary" [routerLink]="['/documents', task.related_document_ids[0]]" (click)="dismissTask(task)">
<i-bs name="file-text" class="me-1"></i-bs><ng-container i18n>Open Document</ng-container>
</a>
}
</ng-container>
</div>
</td>
</tr>
<tr>
<td class="px-2 py-0" [class.border-0]="expandedTask !== task.id" [attr.colspan]="sectionShowsResults(section) ? 5 : 4">
<div #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="task-detail-panel bg-darker small mb-0">
<div class="p-2 p-lg-3 ms-lg-3">
@if (taskHasResultMessage(task)) {
<div class="detail-section mb-3">
<div class="detail-label fs-7 fw-bold text-uppercase text-muted mb-1" i18n>Result message</div>
<pre class="detail-block border border-dark bg-body p-3 rounded-2 mb-0">{{ taskResultMessage(task) }}</pre>
</div>
}
@if (duplicateDocumentId(task); as duplicateDocumentId) {
<div class="detail-section mb-3">
<div class="detail-label fs-7 fw-bold text-uppercase text-muted mb-1" i18n>Duplicate</div>
<div class="detail-block border border-dark bg-body p-3 rounded-2 mb-0">
<div class="d-flex align-items-center justify-content-between gap-3">
<div class="text-break">{{ duplicateTaskLabel(task) }}</div>
<button
class="btn btn-sm btn-outline-primary"
type="button"
(click)="openDuplicateDocument(duplicateDocumentId)">
<ng-container i18n>Open</ng-container>
</button>
</div>
</div>
</div>
}
<div class="row g-3">
<div class="col-12 col-xl-6">
<div class="detail-section h-100">
<div class="detail-label fs-7 fw-bold text-uppercase text-muted mb-1" i18n>Input data</div>
<pre class="detail-block border border-dark bg-body p-3 rounded-2 mb-0">{{ task.input_data | json }}</pre>
</div>
</div>
<div class="col-12 col-xl-6">
<div class="detail-section h-100">
<div class="detail-label fs-7 fw-bold text-uppercase text-muted mb-1" i18n>Result data</div>
<pre class="detail-block border border-dark bg-body p-3 rounded-2 mb-0">{{ (task.result_data ?? {}) | json }}</pre>
</div>
</div>
</td>
<td class="overflow-auto name-col">{{ task.task_file_name }}</td>
<td class="d-none d-lg-table-cell">{{ task.date_created | customDate:'short' }}</td>
@if (activeTab !== 'started' && activeTab !== 'queued') {
<td class="d-none d-lg-table-cell">
@if (task.result?.length > 50) {
<div class="result" (click)="expandTask(task); $event.stopPropagation();"
[ngbPopover]="resultPopover" popoverClass="shadow small mobile" triggers="mouseenter:mouseleave" container="body">
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result | slice:0:50 }}&hellip;</span>
</div>
</div>
}
@if (task.result?.length <= 50) {
<span class="small d-none d-md-inline-block font-monospace text-muted">{{ task.result }}</span>
}
<ng-template #resultPopover>
<pre class="small mb-0">{{ task.result | slice:0:300 }}@if (task.result.length > 300) {
&hellip;
}</pre>
@if (task.result?.length > 300) {
<br/><em>(<ng-container i18n>click for full output</ng-container>)</em>
}
</ng-template>
@if (task.duplicate_documents?.length > 0) {
<div class="small text-warning-emphasis d-flex align-items-center gap-1">
<i-bs class="lh-1" width="1em" height="1em" name="exclamation-triangle"></i-bs>
<span i18n>Duplicate(s) detected</span>
</div>
}
</td>
}
<td class="d-lg-none">
<button class="btn btn-link" (click)="expandTask(task); $event.stopPropagation();">
<i-bs width="1.2em" height="1.2em" name="info-circle"></i-bs>
</button>
</td>
<td scope="row">
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-secondary" (click)="dismissTask(task); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }">
<i-bs name="check" class="me-1"></i-bs><ng-container i18n>Dismiss</ng-container>
</button>
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
@if (task.related_document) {
<button class="btn btn-sm btn-outline-primary" (click)="dismissAndGo(task); $event.stopPropagation();">
<i-bs name="file-text" class="me-1"></i-bs><ng-container i18n>Open Document</ng-container>
</button>
}
</ng-container>
</div>
</td>
</tr>
<tr>
<td class="p-0" [class.border-0]="expandedTask !== task.id" colspan="5">
<pre #collapse="ngbCollapse" [ngbCollapse]="expandedTask !== task.id" class="small mb-0"><div class="small p-1 p-lg-3 ms-lg-3">{{ task.result }}</div></pre>
</td>
</tr>
}
</tbody>
</table>
</tbody>
</table>
<div class="pb-3 d-sm-flex justify-content-between align-items-center">
@if (tasks.length > 0) {
<div class="pb-2 pb-sm-0">
<ng-container i18n>{tasks.length, plural, =1 {One {{this.activeTabLocalized}} task} other {{{tasks.length || 0}} total {{this.activeTabLocalized}} tasks}}</ng-container>
@if (selectedTasks.size > 0) {
<ng-container i18n>&nbsp;({{selectedTasks.size}} selected)</ng-container>
}
</div>
}
@if (tasks.length > pageSize) {
<ngb-pagination [(page)]="page" [pageSize]="pageSize" [collectionSize]="tasks.length" maxSize="8" size="sm"></ngb-pagination>
}
</div>
</ng-template>
@if (visibleSections.length > 0) {
@for (section of visibleSections; track section) {
<div class="mb-4">
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks: tasksForSection(section), section: section}"></ng-container>
</div>
}
} @else {
<div class="alert alert-secondary fst-italic" i18n>No tasks match the current filters.</div>
}
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs" (hidden)="duringTabChange()" (navChange)="beforeTabChange()">
<li ngbNavItem="failed">
<a ngbNavLink i18n>Failed@if (tasksService.failedFileTasks.length > 0) {
<span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="completed">
<a ngbNavLink i18n>Complete@if (tasksService.completedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.completedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="started">
<a ngbNavLink i18n>Started@if (tasksService.startedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.startedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
<li ngbNavItem="queued">
<a ngbNavLink i18n>Queued@if (tasksService.queuedFileTasks.length > 0) {
<span class="badge bg-secondary ms-2">{{tasksService.queuedFileTasks.length}}</span>
}</a>
<ng-template ngbNavContent>
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks:currentTasks}"></ng-container>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav"></div>
@@ -16,39 +16,6 @@ pre {
cursor: pointer;
}
.tasks-table {
width: 100%;
}
@media (min-width: 992px) {
.tasks-table {
table-layout: fixed;
}
.tasks-table .select-col {
width: 3rem;
}
.tasks-table .created-col {
width: 13rem;
white-space: nowrap;
}
.tasks-table .results-col {
width: 24%;
}
.tasks-table .actions-col {
width: 18rem;
white-space: nowrap;
}
.tasks-table .name-col,
.tasks-table .results-col {
overflow: hidden;
}
}
.btn .spinner-border-sm {
width: 0.8rem;
height: 0.8rem;
@@ -63,12 +30,10 @@ pre {
.input-group .dropdown .btn {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.z-10 {
z-index: 10;
}
tbody tr:nth-last-child(2) td {
border-bottom: none !important;
}
@@ -9,17 +9,21 @@ import { FormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser'
import { Router } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing'
import { NgbModal, NgbModalRef, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import {
NgbModal,
NgbModalRef,
NgbModule,
NgbNavItem,
} from '@ng-bootstrap/ng-bootstrap'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { throwError } from 'rxjs'
import { routes } from 'src/app/app-routing.module'
import {
PaperlessTask,
PaperlessTaskName,
PaperlessTaskStatus,
PaperlessTaskTriggerSource,
PaperlessTaskType,
} from 'src/app/data/paperless-task'
import { Results } from 'src/app/data/results'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
@@ -29,131 +33,90 @@ import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { TasksComponent, TaskSection } from './tasks.component'
import { TasksComponent, TaskTab } from './tasks.component'
const tasks: PaperlessTask[] = [
{
id: 467,
task_id: '11ca1a5b-9f81-442c-b2c8-7e4ae53657f1',
input_data: { filename: 'test.pdf' },
task_file_name: 'test.pdf',
date_created: new Date('2023-03-01T10:26:03.093116Z'),
date_done: new Date('2023-03-01T10:26:07.223048Z'),
task_type: PaperlessTaskType.ConsumeFile,
task_type_display: 'Consume File',
trigger_source: PaperlessTaskTriggerSource.FolderConsume,
trigger_source_display: 'Folder Consume',
status: PaperlessTaskStatus.Failure,
status_display: 'Failure',
result_data: {
error_message:
'test.pd: Not consuming test.pdf: It is a duplicate of test (#100)',
},
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
status: PaperlessTaskStatus.Failed,
result: 'test.pd: Not consuming test.pdf: It is a duplicate of test (#100)',
acknowledged: false,
related_document_ids: [],
related_document: null,
},
{
id: 466,
task_id: '10ca1a5b-3c08-442c-b2c8-7e4ae53657f1',
input_data: { filename: '191092.pdf' },
task_file_name: '191092.pdf',
date_created: new Date('2023-03-01T09:26:03.093116Z'),
date_done: new Date('2023-03-01T09:26:07.223048Z'),
task_type: PaperlessTaskType.ConsumeFile,
task_type_display: 'Consume File',
trigger_source: PaperlessTaskTriggerSource.FolderConsume,
trigger_source_display: 'Folder Consume',
status: PaperlessTaskStatus.Failure,
status_display: 'Failure',
result_data: { duplicate_of: 311 },
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
status: PaperlessTaskStatus.Failed,
result:
'191092.pd: Not consuming 191092.pdf: It is a duplicate of 191092 (#311)',
acknowledged: false,
related_document_ids: [],
related_document: null,
},
{
id: 465,
task_id: '3612d477-bb04-44e3-985b-ac580dd496d8',
input_data: { filename: 'Scan Jun 6, 2023 at 3.19 PM.pdf' },
task_file_name: 'Scan Jun 6, 2023 at 3.19 PM.pdf',
date_created: new Date('2023-06-06T15:22:05.722323-07:00'),
date_done: new Date('2023-06-06T15:22:14.564305-07:00'),
task_type: PaperlessTaskType.ConsumeFile,
task_type_display: 'Consume File',
trigger_source: PaperlessTaskTriggerSource.FolderConsume,
trigger_source_display: 'Folder Consume',
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
status: PaperlessTaskStatus.Pending,
status_display: 'Pending',
result_data: null,
result: null,
acknowledged: false,
related_document_ids: [],
related_document: null,
},
{
id: 464,
task_id: '2eac4716-2aa6-4dcd-9953-264e11656d7e',
input_data: { filename: 'paperless-mail-l4dkg8ir' },
task_file_name: 'paperless-mail-l4dkg8ir',
date_created: new Date('2023-06-04T11:24:32.898089-07:00'),
date_done: new Date('2023-06-04T11:24:44.678605-07:00'),
task_type: PaperlessTaskType.ConsumeFile,
task_type_display: 'Consume File',
trigger_source: PaperlessTaskTriggerSource.EmailConsume,
trigger_source_display: 'Email Consume',
status: PaperlessTaskStatus.Success,
status_display: 'Success',
result_data: { document_id: 422, duplicate_of: 99 },
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
status: PaperlessTaskStatus.Complete,
result: 'Success. New document id 422 created',
acknowledged: false,
related_document_ids: [422],
related_document: 422,
},
{
id: 463,
task_id: '28125528-1575-4d6b-99e6-168906e8fa5c',
input_data: { filename: 'onlinePaymentSummary.pdf' },
task_file_name: 'onlinePaymentSummary.pdf',
date_created: new Date('2023-06-01T13:49:51.631305-07:00'),
date_done: new Date('2023-06-01T13:49:54.190220-07:00'),
task_type: PaperlessTaskType.ConsumeFile,
task_type_display: 'Consume File',
trigger_source: PaperlessTaskTriggerSource.FolderConsume,
trigger_source_display: 'Folder Consume',
status: PaperlessTaskStatus.Success,
status_display: 'Success',
result_data: { document_id: 421 },
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
status: PaperlessTaskStatus.Complete,
result: 'Success. New document id 421 created',
acknowledged: false,
related_document_ids: [421],
related_document: 421,
},
{
id: 462,
task_id: 'a5b9ca47-0c8e-490f-a04c-6db5d5fc09e5',
input_data: { filename: 'paperless-mail-_rrpmqk6' },
task_file_name: 'paperless-mail-_rrpmqk6',
date_created: new Date('2023-06-07T02:54:35.694916Z'),
date_done: null,
task_type: PaperlessTaskType.ConsumeFile,
task_type_display: 'Consume File',
trigger_source: PaperlessTaskTriggerSource.EmailConsume,
trigger_source_display: 'Email Consume',
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
status: PaperlessTaskStatus.Started,
status_display: 'Started',
result_data: null,
result: null,
acknowledged: false,
related_document_ids: [],
},
{
id: 461,
task_id: 'bb79efb3-1e78-4f31-b4be-0966620b0ce1',
input_data: { dry_run: false, scope: 'global' },
date_created: new Date('2023-06-07T03:54:35.694916Z'),
date_done: null,
task_type: PaperlessTaskType.SanityCheck,
task_type_display: 'Sanity Check',
trigger_source: PaperlessTaskTriggerSource.System,
trigger_source_display: 'System',
status: PaperlessTaskStatus.Started,
status_display: 'Started',
result_data: { issues_found: 0 },
acknowledged: false,
related_document_ids: [],
related_document: null,
},
]
const paginatedTasks: Results<PaperlessTask> = {
count: tasks.length,
results: tasks,
}
describe('TasksComponent', () => {
let component: TasksComponent
let fixture: ComponentFixture<TasksComponent>
@@ -202,193 +165,60 @@ describe('TasksComponent', () => {
component = fixture.componentInstance
jest.useFakeTimers()
fixture.detectChanges()
httpTestingController
.expectOne(
(req) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
)
.flush(paginatedTasks)
httpTestingController
.expectOne(
(req) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '25' &&
req.params.get('page') === '1'
)
.flush(paginatedTasks)
.flush(tasks)
})
it('should display task sections with counts', () => {
expect(component.selectedSection).toBe(TaskSection.All)
expect(component.selectedTaskType).toBeNull()
expect(component.selectedTriggerSource).toBeNull()
it('should display file tasks in 4 tabs by status', () => {
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavItem))
let currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Failed
).length
component.activeTab = TaskTab.Failed
fixture.detectChanges()
expect(tabButtons[0].nativeElement.textContent).toEqual(
`Failed${currentTasksLength}`
)
expect(
fixture.debugElement.queryAll(By.css('table input[type="checkbox"]'))
).toHaveLength(currentTasksLength + 1)
const viewScope = fixture.debugElement.query(By.css('.task-view-scope'))
const text = viewScope.nativeElement.textContent
expect(text).toContain('All')
expect(text).toContain('Needs attention')
expect(text).toContain('2')
expect(text).toContain('In progress')
expect(text).toContain('3')
expect(text).toContain('Recently completed')
})
it('should filter visible sections by selected status', () => {
component.setSection(TaskSection.InProgress)
currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Complete
).length
component.activeTab = TaskTab.Completed
fixture.detectChanges()
expect(tabButtons[1].nativeElement.textContent).toEqual(
`Complete${currentTasksLength}`
)
expect(component.visibleSections).toEqual([TaskSection.InProgress])
expect(fixture.nativeElement.textContent).toContain('In progress')
expect(fixture.nativeElement.textContent).not.toContain('Recent completed')
})
currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Started
).length
component.activeTab = TaskTab.Started
fixture.detectChanges()
expect(tabButtons[2].nativeElement.textContent).toEqual(
`Started${currentTasksLength}`
)
it('should filter tasks by task type', () => {
component.setSection(TaskSection.InProgress)
component.setTaskType(PaperlessTaskType.SanityCheck)
expect(component.tasksForSection(TaskSection.InProgress)).toHaveLength(1)
expect(component.tasksForSection(TaskSection.InProgress)[0].task_type).toBe(
PaperlessTaskType.SanityCheck
currentTasksLength = tasks.filter(
(t) => t.status === PaperlessTaskStatus.Pending
).length
component.activeTab = TaskTab.Queued
fixture.detectChanges()
expect(tabButtons[3].nativeElement.textContent).toEqual(
`Queued${currentTasksLength}`
)
})
it('should filter tasks by trigger source', () => {
component.setSection(TaskSection.InProgress)
component.setTriggerSource(PaperlessTaskTriggerSource.EmailConsume)
expect(component.tasksForSection(TaskSection.InProgress)).toHaveLength(1)
expect(
component.tasksForSection(TaskSection.InProgress)[0].trigger_source
).toBe(PaperlessTaskTriggerSource.EmailConsume)
})
it('should reset all active filters together', () => {
component.setSection(TaskSection.InProgress)
component.setTaskType(PaperlessTaskType.SanityCheck)
component.setTriggerSource(PaperlessTaskTriggerSource.System)
component.filterText = 'system'
jest.advanceTimersByTime(150)
expect(component.isFiltered).toBe(true)
component.resetFilters()
expect(component.selectedSection).toBe(TaskSection.InProgress)
expect(component.selectedTaskType).toBeNull()
expect(component.selectedTriggerSource).toBeNull()
expect(component.filterText).toBe('')
expect(component.isFiltered).toBe(false)
})
it('should keep header controls focused on actions and auto refresh', () => {
fixture.detectChanges()
const header = fixture.debugElement.query(By.css('pngx-page-header'))
const headerText = header.nativeElement.textContent
expect(headerText).toContain('Dismiss visible')
expect(headerText).toContain('Auto refresh')
expect(headerText).not.toContain('All types')
expect(headerText).not.toContain('All sources')
expect(headerText).not.toContain('Reset filters')
})
it('should render the view scope row above the filter bar', () => {
fixture.detectChanges()
const controls = fixture.debugElement.query(By.css('.task-controls'))
const viewScope = controls.query(By.css('.task-view-scope'))
const search = controls.query(By.css('.task-search'))
expect(viewScope).not.toBeNull()
expect(search).not.toBeNull()
expect(
viewScope.nativeElement.compareDocumentPosition(search.nativeElement) &
Node.DOCUMENT_POSITION_FOLLOWING
).toBeTruthy()
})
it('should render pagination controls next to the task filter', () => {
fixture.detectChanges()
const controls = fixture.debugElement.query(By.css('.task-controls'))
const search = controls.query(By.css('.task-search'))
const pagination = controls.query(By.css('ngb-pagination'))
expect(search).not.toBeNull()
expect(pagination).not.toBeNull()
})
it('should load a different task page when pagination changes', () => {
component.setPage(2)
const pageTwoTasks = {
count: 30,
results: [tasks[0]],
}
httpTestingController
.expectOne(
(req) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '25' &&
req.params.get('page') === '2'
)
.flush(pageTwoTasks)
expect(component.page).toBe(2)
expect(component.totalTasks).toBe(30)
expect(component.pagedTasks).toEqual([tasks[0]])
})
it('should expose stable task type options and disable empty ones', () => {
expect(component.taskTypeOptions.map((option) => option.value)).toContain(
PaperlessTaskType.TrainClassifier
)
expect(
component.isTaskTypeOptionDisabled(PaperlessTaskType.TrainClassifier)
).toBe(true)
expect(
component.isTaskTypeOptionDisabled(PaperlessTaskType.ConsumeFile)
).toBe(false)
})
it('should fall back to the raw selected task type label when no option matches', () => {
component.selectedTaskType = 'unknown_task_type' as PaperlessTaskType
expect(component.selectedTaskTypeLabel).toBe('unknown_task_type')
})
it('should expose stable trigger source options and disable empty ones', () => {
expect(
component.triggerSourceOptions.map((option) => option.value)
).toContain(PaperlessTaskTriggerSource.ApiUpload)
expect(
component.isTriggerSourceOptionDisabled(
PaperlessTaskTriggerSource.ApiUpload
)
).toBe(true)
expect(
component.isTriggerSourceOptionDisabled(
PaperlessTaskTriggerSource.EmailConsume
)
).toBe(false)
})
it('should fall back to the raw selected trigger source label when no option matches', () => {
component.selectedTriggerSource =
'unknown_trigger_source' as PaperlessTaskTriggerSource
expect(component.selectedTriggerSourceLabel).toBe('unknown_trigger_source')
it('should to go page 1 between tab switch', () => {
component.page = 10
component.duringTabChange()
expect(component.page).toEqual(1)
})
it('should support expanding / collapsing one task at a time', () => {
@@ -400,31 +230,6 @@ describe('TasksComponent', () => {
expect(component.expandedTask).toBeUndefined()
})
it('should show structured task details when expanded', () => {
component.setSection(TaskSection.InProgress)
component.expandTask(tasks[6])
fixture.detectChanges()
const detailText = fixture.nativeElement.textContent
expect(detailText).toContain('Input data')
expect(detailText).toContain('Result data')
expect(detailText).toContain('"scope": "global"')
expect(detailText).toContain('"issues_found": 0')
})
it('should show duplicate warnings and duplicate details when present', () => {
component.setSection(TaskSection.Completed)
component.expandTask(tasks[3])
fixture.detectChanges()
const content = fixture.nativeElement.textContent
expect(content).toContain('Duplicate of document #99')
expect(content).toContain('Duplicate')
expect(content).toContain('Open')
})
it('should support dismiss single task', () => {
const dismissSpy = jest.spyOn(tasksService, 'dismissTasks')
component.dismissTask(tasks[0])
@@ -435,7 +240,7 @@ describe('TasksComponent', () => {
component.toggleSelected(tasks[0])
component.toggleSelected(tasks[1])
component.toggleSelected(tasks[3])
component.toggleSelected(tasks[3])
component.toggleSelected(tasks[3]) // uncheck, for coverage
const selected = new Set([tasks[0].id, tasks[1].id])
expect(component.selectedTasks).toEqual(selected)
let modal: NgbModalRef
@@ -484,70 +289,41 @@ describe('TasksComponent', () => {
expect(component.selectedTasks.size).toBe(0)
})
it('should support dismiss visible tasks', () => {
component.setSection(TaskSection.NeedsAttention)
it('should support dismiss all tasks', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const dismissSpy = jest.spyOn(tasksService, 'dismissTasks')
component.dismissTasks()
expect(modal).not.toBeUndefined()
modal.componentInstance.confirmClicked.emit()
expect(dismissSpy).toHaveBeenCalledWith(new Set([467, 466]))
expect(dismissSpy).toHaveBeenCalledWith(new Set(tasks.map((t) => t.id)))
})
it('should dismiss the currently visible scoped and filtered tasks', () => {
component.setSection(TaskSection.InProgress)
component.setTaskType(PaperlessTaskType.SanityCheck)
component.setTriggerSource(PaperlessTaskTriggerSource.System)
const dismissSpy = jest.spyOn(tasksService, 'dismissTasks')
component.dismissTasks()
expect(dismissSpy).toHaveBeenCalledWith(new Set([461]))
})
it('should support toggling a full section', () => {
component.setSection(TaskSection.NeedsAttention)
fixture.detectChanges()
it('should support toggle all tasks', () => {
const toggleCheck = fixture.debugElement.query(
By.css('#all-tasks-needs_attention')
By.css('table input[type=checkbox]')
)
toggleCheck.nativeElement.dispatchEvent(new MouseEvent('click'))
fixture.detectChanges()
expect(component.selectedTasks).toEqual(
new Set(
tasks
.filter((t) => t.status === PaperlessTaskStatus.Failed)
.map((t) => t.id)
)
)
expect(toggleCheck).not.toBeNull()
toggleCheck.nativeElement.dispatchEvent(new MouseEvent('click'))
fixture.detectChanges()
expect(component.selectedTasks).toEqual(new Set([467, 466]))
})
it('should remove a full section from selection when toggled off', () => {
component.setSection(TaskSection.NeedsAttention)
component.selectedTasks = new Set([467, 466])
component.toggleSection(TaskSection.NeedsAttention, {
target: { checked: false },
} as unknown as PointerEvent)
expect(component.selectedTasks).toEqual(new Set())
})
it('should support dismiss and open a document', () => {
const dismissSpy = jest.spyOn(component, 'dismissTask')
fixture.detectChanges()
const openDocumentLink = fixture.debugElement
.queryAll(By.css('a'))
.find((link) => link.nativeElement.textContent.includes('Open Document'))
expect(openDocumentLink).not.toBeNull()
openDocumentLink.triggerEventHandler(
'click',
new MouseEvent('click', { ctrlKey: true })
)
expect(dismissSpy).toHaveBeenCalledWith(tasks[3])
const routerSpy = jest.spyOn(router, 'navigate')
component.dismissAndGo(tasks[3])
expect(routerSpy).toHaveBeenCalledWith([
'documents',
tasks[3].related_document,
])
})
it('should auto refresh, allow toggle', () => {
@@ -560,127 +336,57 @@ describe('TasksComponent', () => {
})
it('should filter tasks by file name', () => {
fixture.detectChanges()
const input = fixture.debugElement.query(
By.css('.task-search input[type=text]')
By.css('pngx-page-header input[type=text]')
)
expect(input).not.toBeNull()
input.nativeElement.value = '191092'
input.nativeElement.dispatchEvent(new Event('input'))
jest.advanceTimersByTime(150)
jest.advanceTimersByTime(150) // debounce time
fixture.detectChanges()
expect(component.filterText).toEqual('191092')
expect(component.tasksForSection(TaskSection.NeedsAttention)).toHaveLength(
1
)
})
it('should match task type and source in name filtering', () => {
component.setSection(TaskSection.InProgress)
component.filterText = 'system'
jest.advanceTimersByTime(150)
expect(component.tasksForSection(TaskSection.InProgress)).toHaveLength(1)
expect(component.tasksForSection(TaskSection.InProgress)[0].task_type).toBe(
PaperlessTaskType.SanityCheck
)
})
it('should fall back to task type when filename is unavailable', () => {
component.setSection(TaskSection.InProgress)
fixture.detectChanges()
const nameColumn = fixture.debugElement.queryAll(
By.css('tbody td.name-col')
)
const sanityTaskRow = nameColumn.find((cell) =>
cell.nativeElement.textContent.includes('Sanity Check')
)
expect(sanityTaskRow.nativeElement.textContent).toContain('Sanity Check')
expect(sanityTaskRow.nativeElement.textContent).toContain('System')
expect(
fixture.debugElement.queryAll(By.css('table tbody tr')).length
).toEqual(2) // 1 task x 2 lines
})
it('should filter tasks by result', () => {
component.setSection(TaskSection.NeedsAttention)
component.filterTargetID = 1
component.activeTab = TaskTab.Failed
fixture.detectChanges()
component.filterTargetID = 1
const input = fixture.debugElement.query(
By.css('.task-search input[type=text]')
By.css('pngx-page-header input[type=text]')
)
expect(input).not.toBeNull()
input.nativeElement.value = 'duplicate'
input.nativeElement.dispatchEvent(new Event('input'))
jest.advanceTimersByTime(150)
jest.advanceTimersByTime(150) // debounce time
fixture.detectChanges()
expect(component.filterText).toEqual('duplicate')
expect(component.tasksForSection(TaskSection.NeedsAttention)).toHaveLength(
2
)
})
it('should prefer explicit reason in the result message', () => {
expect(
component.taskResultMessage({
...tasks[0],
result_data: { reason: 'Manual review required', duplicate_of: 311 },
})
).toBe('Manual review required')
})
it('should return null preview and popover text when there is no result message', () => {
expect(component.taskResultPreview(tasks[2])).toBeNull()
expect(component.taskResultPopoverMessage(tasks[2])).toBe('')
expect(component.taskResultMessageOverflowsPopover(tasks[2])).toBe(false)
})
it('should navigate to a duplicate document details page', () => {
const routerSpy = jest.spyOn(router, 'navigate')
component.openDuplicateDocument(99)
expect(routerSpy).toHaveBeenCalledWith(['documents', 99, 'details'])
})
it('should report when a result message overflows the popover limit', () => {
const longMessage = 'x'.repeat(350)
const task = {
...tasks[0],
result_data: { error_message: longMessage },
}
expect(component.taskResultPopoverMessage(task)).toBe(
longMessage.slice(0, 300)
)
expect(component.taskResultMessageOverflowsPopover(task)).toBe(true)
fixture.debugElement.queryAll(By.css('table tbody tr')).length
).toEqual(4) // 2 tasks x 2 lines
})
it('should support keyboard events for filtering', () => {
fixture.detectChanges()
const input = fixture.debugElement.query(
By.css('.task-search input[type=text]')
By.css('pngx-page-header input[type=text]')
)
expect(input).not.toBeNull()
input.nativeElement.value = '191092'
input.nativeElement.dispatchEvent(
new KeyboardEvent('keyup', { key: 'Enter' })
)
expect(component.filterText).toEqual('191092')
expect(component.filterText).toEqual('191092') // no debounce needed
input.nativeElement.dispatchEvent(
new KeyboardEvent('keyup', { key: 'Escape' })
)
expect(component.filterText).toEqual('')
})
it('should keep clearing selection independent from resetting filters', () => {
component.setTaskType(PaperlessTaskType.ConsumeFile)
component.toggleSelected(tasks[0])
expect(component.selectedTasks.size).toBe(1)
component.clearSelection()
expect(component.selectedTasks.size).toBe(0)
expect(component.selectedTaskType).toBe(PaperlessTaskType.ConsumeFile)
expect(component.isFiltered).toBe(true)
it('should reset filter and target on tab switch', () => {
component.filterText = '191092'
component.filterTargetID = 1
component.activeTab = TaskTab.Completed
component.beforeTabChange()
expect(component.filterText).toEqual('')
expect(component.filterTargetID).toEqual(0)
})
})
@@ -1,11 +1,12 @@
import { JsonPipe, NgTemplateOutlet } from '@angular/common'
import { NgTemplateOutlet, SlicePipe } from '@angular/common'
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { Router, RouterLink } from '@angular/router'
import { Router } from '@angular/router'
import {
NgbCollapseModule,
NgbDropdownModule,
NgbModal,
NgbNavModule,
NgbPaginationModule,
NgbPopoverModule,
} from '@ng-bootstrap/ng-bootstrap'
@@ -19,12 +20,7 @@ import {
takeUntil,
timer,
} from 'rxjs'
import {
PaperlessTask,
PaperlessTaskStatus,
PaperlessTaskTriggerSource,
PaperlessTaskType,
} from 'src/app/data/paperless-task'
import { PaperlessTask } from 'src/app/data/paperless-task'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { TasksService } from 'src/app/services/tasks.service'
@@ -33,11 +29,11 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
export enum TaskSection {
All = 'all',
NeedsAttention = 'needs_attention',
InProgress = 'in_progress',
export enum TaskTab {
Queued = 'queued',
Started = 'started',
Completed = 'completed',
Failed = 'failed',
}
enum TaskFilterTargetID {
@@ -50,82 +46,6 @@ const FILTER_TARGETS = [
{ id: TaskFilterTargetID.Result, name: $localize`Result` },
]
const SECTION_LABELS = {
[TaskSection.All]: $localize`All`,
[TaskSection.NeedsAttention]: $localize`Needs attention`,
[TaskSection.InProgress]: $localize`In progress`,
[TaskSection.Completed]: $localize`Recently completed`,
}
const TASK_TYPE_OPTIONS: Array<{
value: PaperlessTaskType
label: string
}> = [
{
value: PaperlessTaskType.ConsumeFile,
label: $localize`Consume File`,
},
{
value: PaperlessTaskType.TrainClassifier,
label: $localize`Train Classifier`,
},
{
value: PaperlessTaskType.SanityCheck,
label: $localize`Sanity Check`,
},
{ value: PaperlessTaskType.MailFetch, label: $localize`Mail Fetch` },
{ value: PaperlessTaskType.LlmIndex, label: $localize`LLM Index` },
{
value: PaperlessTaskType.EmptyTrash,
label: $localize`Empty Trash`,
},
{
value: PaperlessTaskType.CheckWorkflows,
label: $localize`Check Workflows`,
},
{
value: PaperlessTaskType.BulkUpdate,
label: $localize`Bulk Update`,
},
{
value: PaperlessTaskType.ReprocessDocument,
label: $localize`Reprocess Document`,
},
{
value: PaperlessTaskType.BuildShareLink,
label: $localize`Build Share Link`,
},
{
value: PaperlessTaskType.BulkDelete,
label: $localize`Bulk Delete`,
},
]
const TRIGGER_SOURCE_OPTIONS: Array<{
value: PaperlessTaskTriggerSource
label: string
}> = [
{
value: PaperlessTaskTriggerSource.Scheduled,
label: $localize`Scheduled`,
},
{ value: PaperlessTaskTriggerSource.WebUI, label: $localize`Web UI` },
{
value: PaperlessTaskTriggerSource.ApiUpload,
label: $localize`API Upload`,
},
{
value: PaperlessTaskTriggerSource.FolderConsume,
label: $localize`Folder Consume`,
},
{
value: PaperlessTaskTriggerSource.EmailConsume,
label: $localize`Email Consume`,
},
{ value: PaperlessTaskTriggerSource.System, label: $localize`System` },
{ value: PaperlessTaskTriggerSource.Manual, label: $localize`Manual` },
]
@Component({
selector: 'pngx-tasks',
templateUrl: './tasks.component.html',
@@ -134,13 +54,13 @@ const TRIGGER_SOURCE_OPTIONS: Array<{
PageHeaderComponent,
IfPermissionsDirective,
CustomDatePipe,
JsonPipe,
SlicePipe,
FormsModule,
ReactiveFormsModule,
NgTemplateOutlet,
RouterLink,
NgbCollapseModule,
NgbDropdownModule,
NgbNavModule,
NgbPaginationModule,
NgbPopoverModule,
NgxBootstrapIconsModule,
@@ -155,22 +75,15 @@ export class TasksComponent
private readonly router = inject(Router)
private readonly toastService = inject(ToastService)
readonly TaskSection = TaskSection
readonly sections = [
TaskSection.NeedsAttention,
TaskSection.InProgress,
TaskSection.Completed,
]
public activeTab: TaskTab
public selectedTasks: Set<number> = new Set()
public togggleAll: boolean = false
public expandedTask: number
public autoRefreshEnabled: boolean = true
public readonly pageSize = 25
public pageSize: number = 25
public page: number = 1
public totalTasks: number = 0
public pagedTasks: PaperlessTask[] = []
public selectedSection: TaskSection = TaskSection.All
public selectedTaskType: PaperlessTaskType | null = null
public selectedTriggerSource: PaperlessTaskTriggerSource | null = null
public autoRefreshEnabled: boolean = true
private _filterText: string = ''
get filterText() {
@@ -182,86 +95,24 @@ export class TasksComponent
public filterTargetID: TaskFilterTargetID = TaskFilterTargetID.Name
public get filterTargetName(): string {
return FILTER_TARGETS.find((t) => t.id == this.filterTargetID).name
return this.filterTargets.find((t) => t.id == this.filterTargetID).name
}
private filterDebounce: Subject<string> = new Subject<string>()
public get filterTargets(): Array<{ id: number; name: string }> {
return FILTER_TARGETS
}
public get taskTypeOptions(): Array<{
value: PaperlessTaskType
label: string
}> {
return TASK_TYPE_OPTIONS
}
public get triggerSourceOptions(): Array<{
value: PaperlessTaskTriggerSource
label: string
}> {
return TRIGGER_SOURCE_OPTIONS
}
public get selectedTaskTypeLabel(): string {
if (this.selectedTaskType === null) {
return $localize`All types`
}
return (
this.taskTypeOptions.find(
(option) => option.value === this.selectedTaskType
)?.label ?? this.selectedTaskType
)
}
public get selectedTriggerSourceLabel(): string {
if (this.selectedTriggerSource === null) {
return $localize`All sources`
}
return (
this.triggerSourceOptions.find(
(option) => option.value === this.selectedTriggerSource
)?.label ?? this.selectedTriggerSource
)
return [TaskTab.Failed, TaskTab.Completed].includes(this.activeTab)
? FILTER_TARGETS
: FILTER_TARGETS.slice(0, 1)
}
get dismissButtonText(): string {
return this.selectedTasks.size > 0
? $localize`Dismiss selected`
: $localize`Dismiss visible`
}
get visibleSections(): TaskSection[] {
const sections =
this.selectedSection === TaskSection.All
? this.sections
: [this.selectedSection]
return sections.filter(
(section) => this.tasksForSection(section).length > 0
)
}
get visibleTasks(): PaperlessTask[] {
return this.visibleSections.flatMap((section) =>
this.tasksForSection(section)
)
}
get isFiltered(): boolean {
return (
this.selectedTaskType !== null ||
this.selectedTriggerSource !== null ||
this._filterText.length > 0
)
: $localize`Dismiss all`
}
ngOnInit() {
this.tasksService.reload()
this.reloadPage()
timer(5000, 5000)
.pipe(
filter(() => this.autoRefreshEnabled),
@@ -269,7 +120,6 @@ export class TasksComponent
)
.subscribe(() => {
this.tasksService.reload()
this.reloadPage(false)
})
this.filterDebounce
@@ -279,10 +129,7 @@ export class TasksComponent
distinctUntilChanged(),
filter((query) => !query.length || query.length > 2)
)
.subscribe((query) => {
this._filterText = query
this.clearSelection()
})
.subscribe((query) => (this._filterText = query))
}
ngOnDestroy() {
@@ -296,25 +143,20 @@ export class TasksComponent
dismissTasks(task: PaperlessTask = undefined) {
let tasks = task ? new Set([task.id]) : new Set(this.selectedTasks.values())
if (!task && tasks.size == 0) {
tasks = new Set(this.visibleTasks.map((t) => t.id))
}
if (!task && tasks.size == 0)
tasks = new Set(this.tasksService.allFileTasks.map((t) => t.id))
if (tasks.size > 1) {
let modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.title = $localize`Confirm Dismiss`
modal.componentInstance.messageBold = $localize`Dismiss ${tasks.size} tasks?`
modal.componentInstance.title = $localize`Confirm Dismiss All`
modal.componentInstance.messageBold = $localize`Dismiss all ${tasks.size} tasks?`
modal.componentInstance.btnClass = 'btn-warning'
modal.componentInstance.btnCaption = $localize`Dismiss`
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
modal.componentInstance.buttonsEnabled = false
modal.close()
this.tasksService.dismissTasks(tasks).subscribe({
next: () => {
this.reloadPage(false)
},
error: (e) => {
this.toastService.showError($localize`Error dismissing tasks`, e)
modal.componentInstance.buttonsEnabled = true
@@ -322,11 +164,8 @@ export class TasksComponent
})
this.clearSelection()
})
} else if (tasks.size === 1) {
} else {
this.tasksService.dismissTasks(tasks).subscribe({
next: () => {
this.reloadPage(false)
},
error: (e) =>
this.toastService.showError($localize`Error dismissing task`, e),
})
@@ -334,6 +173,11 @@ export class TasksComponent
}
}
dismissAndGo(task: PaperlessTask) {
this.dismissTask(task)
this.router.navigate(['documents', task.related_document])
}
expandTask(task: PaperlessTask) {
this.expandedTask = this.expandedTask == task.id ? undefined : task.id
}
@@ -344,177 +188,75 @@ export class TasksComponent
: this.selectedTasks.add(task.id)
}
toggleSection(section: TaskSection, event: PointerEvent) {
const sectionTasks = this.tasksForSection(section)
get currentTasks(): PaperlessTask[] {
let tasks: PaperlessTask[] = []
switch (this.activeTab) {
case TaskTab.Queued:
tasks = this.tasksService.queuedFileTasks
break
case TaskTab.Started:
tasks = this.tasksService.startedFileTasks
break
case TaskTab.Completed:
tasks = this.tasksService.completedFileTasks
break
case TaskTab.Failed:
tasks = this.tasksService.failedFileTasks
break
}
if (this._filterText.length) {
tasks = tasks.filter((t) => {
if (this.filterTargetID == TaskFilterTargetID.Name) {
return t.task_file_name
.toLowerCase()
.includes(this._filterText.toLowerCase())
} else if (this.filterTargetID == TaskFilterTargetID.Result) {
return t.result.toLowerCase().includes(this._filterText.toLowerCase())
}
})
}
return tasks
}
toggleAll(event: PointerEvent) {
if ((event.target as HTMLInputElement).checked) {
sectionTasks.forEach((task) => this.selectedTasks.add(task.id))
this.selectedTasks = new Set(this.currentTasks.map((t) => t.id))
} else {
sectionTasks.forEach((task) => this.selectedTasks.delete(task.id))
this.clearSelection()
}
}
areAllSelected(tasks: PaperlessTask[]): boolean {
return (
tasks.length > 0 && tasks.every((task) => this.selectedTasks.has(task.id))
)
}
taskDisplayName(task: PaperlessTask): string {
return task.input_data?.filename?.toString() || task.task_type_display
}
taskShowsSeparateTypeLabel(task: PaperlessTask): boolean {
return this.taskDisplayName(task) !== task.task_type_display
}
taskResultMessage(task: PaperlessTask): string | null {
if (!task.result_data) {
return null
}
const documentId = task.result_data?.['document_id']
if (typeof documentId === 'number') {
return $localize`Success. New document id ${documentId} created`
}
const reason = task.result_data?.['reason']
if (typeof reason === 'string') {
return reason
}
const duplicateOf = task.result_data?.['duplicate_of']
if (typeof duplicateOf === 'number') {
return $localize`Duplicate of document #${duplicateOf}`
}
const errorMessage = task.result_data?.['error_message']
if (typeof errorMessage === 'string') {
return errorMessage
}
return null
}
taskResultPreview(task: PaperlessTask): string | null {
const message = this.taskResultMessage(task)
if (!message) {
return null
}
return message.length > 50 ? `${message.slice(0, 50)}...` : message
}
taskHasLongResultMessage(task: PaperlessTask): boolean {
return (this.taskResultMessage(task)?.length ?? 0) > 50
}
taskHasResultMessage(task: PaperlessTask): boolean {
return !!this.taskResultMessage(task)
}
duplicateDocumentId(task: PaperlessTask): number | null {
const duplicateOf = task.result_data?.['duplicate_of']
return typeof duplicateOf === 'number' ? duplicateOf : null
}
duplicateTaskLabel(task: PaperlessTask): string {
return $localize`Duplicate of document #${this.duplicateDocumentId(task)}`
}
openDuplicateDocument(documentId: number) {
this.router.navigate(['documents', documentId, 'details'])
}
taskResultPopoverMessage(task: PaperlessTask): string {
return this.taskResultMessage(task)?.slice(0, 300) ?? ''
}
taskResultMessageOverflowsPopover(task: PaperlessTask): boolean {
return (this.taskResultMessage(task)?.length ?? 0) > 300
}
tasksForSection(section: TaskSection): PaperlessTask[] {
let tasks = this.pagedTasks.filter((task) =>
this.taskBelongsToSection(task, section)
)
return tasks.filter((task) => this.taskMatchesCurrentFilters(task))
}
sectionLabel(section: TaskSection): string {
return SECTION_LABELS[section]
}
sectionCount(section: TaskSection): number {
return this.pagedTasks.filter((task) =>
this.taskBelongsToSection(task, section)
).length
}
sectionShowsResults(section: TaskSection): boolean {
return section !== TaskSection.InProgress
}
setSection(section: TaskSection) {
this.selectedSection = section
this.clearSelection()
}
setTaskType(taskType: PaperlessTaskType | null) {
this.selectedTaskType = taskType
this.clearSelection()
}
setTriggerSource(triggerSource: PaperlessTaskTriggerSource | null) {
this.selectedTriggerSource = triggerSource
this.clearSelection()
}
taskTypeOptionCount(taskType: PaperlessTaskType | null): number {
return this.tasksForOptionCounts({ taskType }).length
}
triggerSourceOptionCount(
triggerSource: PaperlessTaskTriggerSource | null
): number {
return this.tasksForOptionCounts({ triggerSource }).length
}
isTaskTypeOptionDisabled(taskType: PaperlessTaskType | null): boolean {
return this.taskTypeOptionCount(taskType) === 0
}
isTriggerSourceOptionDisabled(
triggerSource: PaperlessTaskTriggerSource | null
): boolean {
return this.triggerSourceOptionCount(triggerSource) === 0
}
clearSelection() {
this.togggleAll = false
this.selectedTasks.clear()
}
setPage(page: number) {
if (this.page === page) {
return
}
duringTabChange() {
this.page = 1
}
this.page = page
this.clearSelection()
this.reloadPage()
beforeTabChange() {
this.resetFilter()
this.filterTargetID = TaskFilterTargetID.Name
}
get activeTabLocalized(): string {
switch (this.activeTab) {
case TaskTab.Queued:
return $localize`queued`
case TaskTab.Started:
return $localize`started`
case TaskTab.Completed:
return $localize`completed`
case TaskTab.Failed:
return $localize`failed`
}
}
public resetFilter() {
this._filterText = ''
}
public resetFilters() {
this.selectedTaskType = null
this.selectedTriggerSource = null
this.resetFilter()
this.clearSelection()
}
filterInputKeyup(event: KeyboardEvent) {
if (event.key == 'Enter') {
this._filterText = (event.target as HTMLInputElement).value
@@ -522,116 +264,4 @@ export class TasksComponent
this.resetFilter()
}
}
private taskBelongsToSection(
task: PaperlessTask,
section: TaskSection
): boolean {
switch (section) {
case TaskSection.NeedsAttention:
return [
PaperlessTaskStatus.Failure,
PaperlessTaskStatus.Revoked,
].includes(task.status)
case TaskSection.InProgress:
return [
PaperlessTaskStatus.Pending,
PaperlessTaskStatus.Started,
].includes(task.status)
case TaskSection.Completed:
return task.status === PaperlessTaskStatus.Success
}
}
private taskMatchesCurrentFilters(task: PaperlessTask): boolean {
return this.taskMatchesFilters(task, {
taskType: this.selectedTaskType,
triggerSource: this.selectedTriggerSource,
})
}
private taskMatchesFilters(
task: PaperlessTask,
{
taskType,
triggerSource,
}: {
taskType: PaperlessTaskType | null
triggerSource: PaperlessTaskTriggerSource | null
}
): boolean {
if (taskType !== null && task.task_type !== taskType) {
return false
}
if (triggerSource !== null && task.trigger_source !== triggerSource) {
return false
}
if (!this._filterText.length) {
return true
}
const query = this._filterText.toLowerCase()
if (this.filterTargetID == TaskFilterTargetID.Name) {
return [
this.taskDisplayName(task),
task.task_type_display,
task.trigger_source_display,
]
.filter(Boolean)
.some((value) => value.toLowerCase().includes(query))
}
return this.taskResultMessage(task)?.toLowerCase().includes(query) ?? false
}
private tasksForOptionCounts({
taskType = this.selectedTaskType,
triggerSource = this.selectedTriggerSource,
}: {
taskType?: PaperlessTaskType | null
triggerSource?: PaperlessTaskTriggerSource | null
}): PaperlessTask[] {
const sections =
this.selectedSection === TaskSection.All
? this.sections
: [this.selectedSection]
return this.pagedTasks.filter(
(task) =>
sections.some((section) => this.taskBelongsToSection(task, section)) &&
this.taskMatchesFilters(task, { taskType, triggerSource })
)
}
private reloadPage(resetToFirstPage: boolean = false) {
if (resetToFirstPage) {
this.page = 1
}
this.loading = true
this.tasksService
.list(this.page, this.pageSize, { acknowledged: false })
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (result) => {
this.pagedTasks = result.results
this.totalTasks = result.count
this.loading = false
if (
this.page > 1 &&
this.pagedTasks.length === 0 &&
this.totalTasks > 0
) {
this.page -= 1
this.reloadPage()
}
},
error: () => {
this.loading = false
},
})
}
}
@@ -8,8 +8,10 @@
[ngClass]="{ 'slim': slimSidebarEnabled, 'col-auto col-md-3 col-lg-2 col-xxxl-1' : !slimSidebarEnabled, 'py-3' : !customAppTitle?.length || slimSidebarEnabled, 'py-2': customAppTitle?.length }"
routerLink="/dashboard"
tourAnchor="tour.intro">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" width="1.5em" height="1.5em" fill="currentColor">
<path d="M341,949.1c-6.9-20.3-20.7-61.2-21.9-61-199.6-88.9-182.5-229.8-134.3-347.5,30,137.2,268.8,148.9,146.2,336-.9,2.2,10,27.8,19.5,51.3,22.7-51.9,58.6-115.5,55.8-120.8C178,398.7,724.9,299,807.1,18.5c83,251.5,53.1,659.8-377.4,814.9-2,1.4-63.5,148.6-66.9,150.2-.2-2.1-33.2,2.9-30.1-8.7,1.6-7,4.8-16.2,8.2-25.6h0v-.2h.1ZM323.1,846.2c48.3-71.9-12.7-120.8-56.9-152.2,81.2,107.4,66.4,120.8,56.9,152.2h0Z"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" height="1.5em" fill="currentColor">
<path
d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z"
transform="translate(0 0)" />
</svg>
<div class="ms-2 ms-md-3 d-inline-block" [class.d-md-none]="slimSidebarEnabled">
@if (customAppTitle?.length) {
@@ -292,13 +294,13 @@
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.PaperlessTask }"
tourAnchor="tour.file-tasks">
<a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()"
ngbPopover="Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end"
container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<i-bs class="me-2" name="list-task"></i-bs><span><ng-container i18n>Tasks</ng-container>@if (tasksService.needsAttentionTasks.length > 0) {
<span><span class="badge bg-danger ms-2 d-inline">{{tasksService.needsAttentionTasks.length}}</span></span>
<i-bs class="me-2" name="list-task"></i-bs><span><ng-container i18n>File Tasks</ng-container>@if (tasksService.failedFileTasks.length > 0) {
<span><span class="badge bg-danger ms-2 d-inline">{{tasksService.failedFileTasks.length}}</span></span>
}</span>
@if (tasksService.needsAttentionTasks.length > 0 && slimSidebarEnabled) {
<span class="badge bg-danger position-absolute top-0 end-0 d-none d-md-block">{{tasksService.needsAttentionTasks.length}}</span>
@if (tasksService.failedFileTasks.length > 0 && slimSidebarEnabled) {
<span class="badge bg-danger position-absolute top-0 end-0 d-none d-md-block">{{tasksService.failedFileTasks.length}}</span>
}
</a>
</li>
@@ -94,18 +94,12 @@ main {
}
.sidebar.slim:not(.animating) {
transition: none;
li.nav-item span,
.sidebar-heading span {
display: none;
}
}
.sidebar.slim:not(.animating) ~ main.col-slim {
transition: none;
}
.sidebar.animating {
li.nav-item span,
.sidebar-heading span {
@@ -36,7 +36,6 @@ import { RemoteVersionService } from 'src/app/services/rest/remote-version.servi
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { SearchService } from 'src/app/services/rest/search.service'
import { SettingsService } from 'src/app/services/settings.service'
import { TasksService } from 'src/app/services/tasks.service'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
@@ -98,7 +97,6 @@ describe('AppFrameComponent', () => {
let savedViewSpy
let modalService: NgbModal
let maybeRefreshSpy
let tasksService: TasksService
beforeEach(async () => {
TestBed.configureTestingModule({
@@ -176,7 +174,6 @@ describe('AppFrameComponent', () => {
openDocumentsService = TestBed.inject(OpenDocumentsService)
modalService = TestBed.inject(NgbModal)
router = TestBed.inject(Router)
tasksService = TestBed.inject(TasksService)
jest
.spyOn(settingsService, 'displayName', 'get')
@@ -447,16 +444,6 @@ describe('AppFrameComponent', () => {
expect(maybeRefreshSpy).toHaveBeenCalled()
})
it('should show tasks badge for needs-attention tasks', () => {
jest
.spyOn(tasksService, 'needsAttentionTasks', 'get')
.mockReturnValue([{} as any, {} as any])
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('Tasks2')
})
it('should indicate attributes management availability when any permission is granted', () => {
jest
.spyOn(permissionsService, 'currentUserCan')
@@ -8,21 +8,10 @@
<div class="chat-messages font-monospace small">
@for (message of messages; track message) {
<div class="message d-flex flex-row small" [class.justify-content-end]="message.role === 'user'">
<div class="p-2 m-2" [class.bg-dark]="message.role === 'user'">
<span>
{{ message.content }}
@if (message.isStreaming) { <span class="blinking-cursor">|</span> }
</span>
@if (message.role === 'assistant' && message.references?.length) {
<div class="chat-references list-group mt-3">
@for (reference of message.references; track reference.id) {
<a class="list-group-item list-group-item-action text-primary" [routerLink]="['/documents', reference.id]">
<i-bs width="0.9em" height="0.9em" name="file-text" class="me-1"></i-bs><span>{{ reference.title }}</span>
</a>
}
</div>
}
</div>
<span class="p-2 m-2" [class.bg-dark]="message.role === 'user'">
{{ message.content }}
@if (message.isStreaming) { <span class="blinking-cursor">|</span> }
</span>
</div>
}
<div #scrollAnchor></div>
@@ -7,10 +7,6 @@
overflow-y: auto;
}
.chat-references {
font-family: var(--bs-font-sans-serif);
}
.dropdown-toggle::after {
display: none;
}
@@ -3,13 +3,9 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ElementRef } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NavigationEnd, Router } from '@angular/router'
import { RouterTestingModule } from '@angular/router/testing'
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject } from 'rxjs'
import {
CHAT_METADATA_DELIMITER,
ChatService,
} from 'src/app/services/chat.service'
import { ChatService } from 'src/app/services/chat.service'
import { ChatComponent } from './chat.component'
describe('ChatComponent', () => {
@@ -22,11 +18,7 @@ describe('ChatComponent', () => {
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [
NgxBootstrapIconsModule.pick(allIcons),
RouterTestingModule,
ChatComponent,
],
imports: [NgxBootstrapIconsModule.pick(allIcons), ChatComponent],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
@@ -92,57 +84,6 @@ describe('ChatComponent', () => {
expect(component.messages[1].isStreaming).toBe(false)
})
it('should parse references from the metadata trailer without showing it', () => {
component.input = 'Hello'
component.sendMessage()
mockStream$.next(
`Hi there${CHAT_METADATA_DELIMITER}{"references":[{"id":42,"title":"Bread Recipe"}]}`
)
jest.advanceTimersByTime(1000)
expect(component.messages[1].content).toBe('Hi there')
expect(component.messages[1].references).toEqual([
{ id: 42, title: 'Bread Recipe' },
])
})
it('should render document reference links under assistant messages', () => {
component.input = 'Hello'
component.sendMessage()
mockStream$.next(
`Hi there${CHAT_METADATA_DELIMITER}{"references":[{"id":42,"title":"Bread Recipe"}]}`
)
jest.advanceTimersByTime(1000)
fixture.detectChanges()
const link = fixture.nativeElement.querySelector('.chat-references a')
expect(link.textContent).toContain('Bread Recipe')
expect(link.getAttribute('href')).toContain('/documents/42')
})
it('should remove delimiter fragments that were already streamed', () => {
component.input = 'Hello'
component.sendMessage()
mockStream$.next(`Hi there${CHAT_METADATA_DELIMITER.slice(0, 8)}`)
jest.advanceTimersByTime(1000)
expect(component.messages[1].content).toBe(
`Hi there${CHAT_METADATA_DELIMITER.slice(0, 8)}`
)
mockStream$.next(
`Hi there${CHAT_METADATA_DELIMITER}{"references":[{"id":42,"title":"Bread Recipe"}]}`
)
jest.advanceTimersByTime(1000)
expect(component.messages[1].content).toBe('Hi there')
expect(component.messages[1].references).toEqual([
{ id: 42, title: 'Bread Recipe' },
])
})
it('should handle errors during streaming', () => {
component.input = 'Hello'
component.sendMessage()
@@ -1,21 +1,16 @@
import { Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NavigationEnd, Router, RouterModule } from '@angular/router'
import { NavigationEnd, Router } from '@angular/router'
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { filter, map } from 'rxjs'
import {
ChatMessage,
ChatService,
parseChatResponse,
} from 'src/app/services/chat.service'
import { ChatMessage, ChatService } from 'src/app/services/chat.service'
@Component({
selector: 'pngx-chat',
imports: [
FormsModule,
ReactiveFormsModule,
RouterModule,
NgxBootstrapIconsModule,
NgbDropdownModule,
],
@@ -75,24 +70,13 @@ export class ChatComponent implements OnInit {
this.messages.push(assistantMessage)
this.loading = true
let lastVisibleContent = ''
let lastPartialLength = 0
this.chatService.streamChat(this.documentId, this.input).subscribe({
next: (chunk) => {
const nextResponse = parseChatResponse(chunk)
if (nextResponse.content.length < lastVisibleContent.length) {
this.resetTypewriter(assistantMessage, nextResponse.content)
lastVisibleContent = nextResponse.content
} else {
const visibleDelta = nextResponse.content.substring(
lastVisibleContent.length
)
lastVisibleContent = nextResponse.content
this.enqueueTypewriter(visibleDelta, assistantMessage)
}
assistantMessage.references = nextResponse.references
const delta = chunk.substring(lastPartialLength)
lastPartialLength = chunk.length
this.enqueueTypewriter(delta, assistantMessage)
},
error: () => {
assistantMessage.content += '\n\n⚠️ Error receiving response.'
@@ -109,13 +93,6 @@ export class ChatComponent implements OnInit {
this.input = ''
}
private resetTypewriter(message: ChatMessage, content: string): void {
this.typewriterBuffer = []
this.typewriterActive = false
message.content = content
this.scrollToBottom()
}
enqueueTypewriter(chunk: string, message: ChatMessage): void {
if (!chunk) return
@@ -10,12 +10,12 @@
</div>
</div>
@for (field of filteredFields; track field.id) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addField(field)" #button>
<button class="list-group-item list-group-item-action bg-light" (click)="addField(field)" #button>
<small class="d-flex">{{field.name}} <small class="ms-auto text-muted">{{getDataTypeLabel(field.data_type)}}</small></small>
</button>
}
@if (!filterText?.length || filteredFields.length === 0) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="createField(filterText)" [disabled]="!canCreateFields" #button>
<button class="list-group-item list-group-item-action bg-light" (click)="createField(filterText)" [disabled]="!canCreateFields" #button>
<small>
<i-bs width=".9em" height=".9em" name="asterisk" class="me-1"></i-bs><ng-container i18n>Create new field</ng-container>
</small>
@@ -86,7 +86,7 @@
<div class="selected-icon">
@if (addedRelativeDate) {
<a class="text-light focus-variants" href="javascript:void(0)" (click)="clearAddedRelativeDate()">
<i-bs width="1em" height="1em" name="check" class="variant-unfocused text-dark"></i-bs>
<i-bs width="1em" height="1em" name="check" class="variant-unfocused"></i-bs>
<i-bs width="1em" height="1em" name="x" class="variant-focused text-primary"></i-bs>
</a>
}
@@ -1,22 +1,22 @@
@if (customLogo) {
<img src="{{customLogo}}" [class]="getClasses()" [attr.style]="'height:'+height" />
} @else {
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2670 860" [attr.style]="'height:'+height">
<path class="leaf" style="fill:#005616;" d="M2227.4,821.2c-6.1-17.8-18.1-53.6-19.2-53.4-174.7-77.8-159.8-201.2-117.5-304.2,26.3,120.1,235.3,130.3,128,294.1-.7,2,8.8,24.3,17.1,44.9,19.9-45.4,51.3-101.1,48.8-105.7-199.9-357.4,278.8-444.7,350.7-690.2,72.6,220.1,46.5,577.5-330.4,713.3-1.8,1.2-55.6,130-58.5,131.4-.2-1.9-29.1,2.5-26.4-7.6,1.4-6.2,4.2-14.2,7.2-22.4h0v-.2h.2,0ZM2211.7,731.2c42.3-62.9-11.1-105.7-49.8-133.2,71,94,58.1,105.7,49.8,133.2h0Z"/>
<g class="text" style="fill: #000;">
<path class="st1" d="M654.6,393.2l-.7,137.7h-85.5V188.7h85.4c.4,11.3-.3,21.7,1.3,33.8,23.1-34.1,62.3-50,101.1-38.3,16.5,5,29.6,16.4,39.7,30,34.4,46.5,35.1,134,3.6,182.2-10.1,14.4-22.5,26.9-39,33.4-39.5,15.7-81,1.1-105.9-36.6h0ZM721,362.2c21-26.1,21-82.7-.4-108.4-13.2-15.9-36.4-16.1-49.9-.4-22.2,25.8-21.7,85.3.5,110.1,13.6,15.2,36.6,15,49.7-1.3h.1Z"/>
<path class="st1" d="M164,301l-72.8.7v126.1H3.4V98.1l159.7.5c31.3,0,58.9,13.6,79.4,36.1,30.8,37.6,30.9,91.7.6,129.6-20.1,22.8-47.6,36.5-79,36.8h-.1ZM176.8,199.8c0-20.8-15.1-35-34.7-35l-51,.2v69.5l53.6-.2c18.5,0,32-15.8,32.2-34.5h-.1Z"/>
<polygon class="st1" points="1338.2 427.8 1338 366 1412.4 365.8 1412.5 139.3 1338.1 139.1 1338.1 77.4 1498.1 77.4 1498.1 365.7 1572.3 365.9 1572.5 427.7 1338.2 427.8"/>
<path class="st1" d="M1741.8,364.3c9.1-8.6,14-18.1,17.7-30.3l68.4,13.3c-10.5,45.2-46.5,79.2-92.3,86.7-59.2,9.6-118.7-14.2-138.6-73.7-10.9-32.7-10.7-68.6.6-100.9,17.7-50.6,64.3-80.5,117.1-79.1,76.5,2,113.4,65.4,111.1,136.1h-155.4c-.7,12.5,3,25,9.7,35.9,13.2,21.3,40.9,26.9,61.5,12h.2ZM1749.4,273.1c-2.4-10.8-6.9-18-13.9-24.6-12.8-8.3-30.1-9.5-43.4-1.1-9.3,5.8-14.6,15.1-18,25.7h75.3Z"/>
<path class="st1" d="M1010.3,364.3c9.1-8.5,13.9-18.1,17.7-30.3l68.4,13.3c-10.4,45.2-46.5,79.2-92.3,86.7-59.3,9.6-118.8-14.2-138.7-73.9-10.8-32.3-10.6-67.4.2-99.3,17.3-51.2,64.2-81.8,117.6-80.4,76.6,2,113.5,65.3,111.1,136.1h-155.6c-.2,12.7,3.2,25.1,9.9,35.9,13.2,21.3,40.9,27,61.5,12h.2ZM1018,273.2c-2.4-9.4-6.3-18.5-14.2-24.4-12.3-9.1-30.4-9.4-43.3-1.3-9.3,5.9-14.4,15.1-17.9,25.6h75.4Z"/>
<path class="st1" d="M424.3,376.9c-7.1,13.6-12.5,25.7-23.2,35.5-14.3,13.3-32.6,19.3-52.3,19.4-40.4.2-75.6-23.1-73.6-65.7.9-20.1,9.7-37.2,26.5-49.2,30.5-21.8,55.8-22.4,87.8-40.6,8.1-4.6,18.2-15.3,12.4-22.2s-5-3-8-3.7h-96.3v-61.8h109.6c14.7.6,28.1,2.2,41.7,7.2,23.7,8.8,39.6,29.5,39.8,55.2l.7,90.6c0,13.5,11,23,23.7,23.9l10.1.7v61.3h-29.9c-13.1,0-25.9-3-37.3-8.6-16.9-8.2-26.9-22.2-31.6-42.2h0v.2h-.1ZM364.9,370.1c6.8,5.9,16.2,6.5,24.8,2.7,18.1-7.9,16.5-38.3,16.1-55-3.6,4.3-7.4,9-12.5,11.2l-21.1,9.3c-5.8,2.5-10.6,8-11.8,13s-1,13.8,4.7,18.7h-.2Z"/>
<path class="st1" d="M1943,430.1c-33.5-8.9-68.5-33.6-78.9-68.9l66.6-27.2c11.8,22.1,31.6,42.1,57.2,39.8,4.3-.4,9.3-3.1,11.2-6,7.8-12.5-4.3-24.3-16.2-30.7l-47.3-25.2c-32.2-17.1-57.7-50.7-41.6-87.4,11.9-27,48.1-35,75.3-36h99.2v61.8h-88.6c-2.5.4-6.2,2.3-7,4.2s.7,7,2.7,8.2c31.6,18.6,88.3,38.3,103.8,72,10.4,22.6,6.7,50-9.2,69.1-29.5,35.7-86.1,36.9-127,26.1v.2h-.2,0Z"/>
<path class="st1" d="M1318.2,264.3l-68.5.2c-19.4,0-30.1,10.8-31.6,30.2v133.1h-85.7v-239h85.6l1,58.9,11.9-25.1c14.3-30.5,56.9-36.5,87.4-33.6v75.4h-.1Z"/>
<path class="st1" d="M2232.8,374.2c-26,1.2-44.6-18.4-56.5-40.1l-66.5,27.3c10.8,35.9,46.2,60.4,80.3,69.2h0c10.6,2.6,22,4.5,33.7,5.2,3.2-7.9,6.8-15.6,10.8-23.4,18.5-35.9,44.3-68.4,73.8-98.8-23.6-21.1-62.6-36.7-87-50.6-2.2-1.2-3.6-6.7-2.7-8.7.9-2,4.5-3.5,7.4-3.9h88.2v-61.8h-97.4c-27,.7-63.8,8.2-76.5,34.8-8.3,17.5-6.8,38.5,3.5,54.9,9.3,14.9,22.2,25.8,37.7,33.9l45.8,24.3c11.5,6.1,24.7,17,17.9,30.5-2.1,4.1-7.4,6.5-12.6,7.2h.1Z"/>
<path class="st1" d="M1547.6,801.6h81.2c11.6-.2,23.2-3.8,31.9-11.2,7.3-6.2,11.7-15.4,13.9-24.8l16.8-72.7c-7.2,9-12.8,16.9-20.7,24.2-18.3,16.8-42.3,23.8-66.9,19.5-32.5-5.7-46.7-34.7-47-65.6-.5-44,18.9-93.6,57.6-117.1,18-10.9,39.5-13.9,60-9.6,12.4,2.6,22.1,9.9,29.1,20,5.8,8.4,7.8,17.2,10.8,27.8l10.7-45.4,15.6.3-50.6,219.5c-2.9,12.6-8.9,24.6-18.4,32.9-12,10.4-28.1,15.1-44,15.2l-82.9.2,2.7-13.1h.2ZM1691.8,673.5c12.9-26.3,20.1-60.3,11-88.6-5.1-15.8-17.9-26.5-34.2-28.8-20.7-2.9-40.3,2.9-55.9,16.8-13.6,12.1-23.5,26.7-30.3,43.7-9.8,24.4-14.8,56.5-4.6,81.1,5,12.1,14.7,21.3,27.6,24.7,39,10.3,70.1-16,86.4-49h0Z"/>
<path class="st1" d="M1441.6,556.8c-43.6-8.7-84.4,29.7-93.8,70l-24.8,106.6h-15.7l43.1-186.4,15.6-.2-8.6,39.5c22.3-28.9,53.9-49.3,90.7-42.5,16.8,3.1,29.1,15.6,32.1,32.4,2.1,11.6,1.6,23.4-1.1,35.3l-28.1,122.2h-15.6c0,0,27.5-119.9,27.5-119.9,4.7-20.6,5.9-51.3-21.2-56.7v-.3Z"/>
<path class="st1" d="M1958.9,733.3h-16.2l-38.2-90.1-79.8,90.3-19.3-.2,77.6-87.2c5.1-5.7,11-10.1,17.2-14.5-4.6-4.7-8.5-9.6-11.3-15.3l-33.9-69.3,16.2-.2,35.3,74.1,69-73.9c6.6-.3,12.7-.3,19.6.2l-63.1,66.6c-6.4,6.8-13.4,12.5-20.9,18,3.4,3.4,7.5,7.5,9.6,12.4l38.3,89.2h-.1Z"/>
<path class="st1" d="M1224.4,635.4H3.4c1.1-5.6,1.9-9.5,3.1-13.9h1220.9l-2.9,13.9h0Z"/>
</g>
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" [attr.style]="'height:'+height">
<path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/>
<g class="text" style="fill:#000">
<path d="M1022.3,428.7c-17.8-19.9-42.7-29.8-74.7-29.8c-22.3,0-42.4,5.7-60.5,17.3c-18.1,11.6-32.3,27.5-42.5,47.8 s-15.3,42.9-15.3,67.8c0,24.9,5.1,47.5,15.3,67.8c10.3,20.3,24.4,36.2,42.5,47.8c18.1,11.5,38.3,17.3,60.5,17.3 c32,0,56.9-9.9,74.7-29.8v20.4v0.2h84.5V408.3h-84.5V428.7z M1010.5,575c-10.2,11.7-23.6,17.6-40.2,17.6s-29.9-5.9-40-17.6 s-15.1-26.1-15.1-43.3c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6c16.6,0,30,5.9,40.2,17.6s15.3,26.1,15.3,43.3 S1020.7,563.3,1010.5,575z" transform="translate(0)"/>
<path d="M1381,416.1c-18.1-11.5-38.3-17.3-60.5-17.4c-32,0-56.9,9.9-74.7,29.8v-20.4h-84.5v390.7h84.5v-164 c17.8,19.9,42.7,29.8,74.7,29.8c22.3,0,42.4-5.7,60.5-17.3s32.3-27.5,42.5-47.8c10.2-20.3,15.3-42.9,15.3-67.8s-5.1-47.5-15.3-67.8 C1413.2,443.6,1399.1,427.7,1381,416.1z M1337.9,575c-10.1,11.7-23.4,17.6-40,17.6s-29.9-5.9-40-17.6s-15.1-26.1-15.1-43.3 c0-17.1,5-31.6,15.1-43.3s23.4-17.6,40-17.6s29.9,5.9,40,17.6s15.1,26.1,15.1,43.3S1347.9,563.3,1337.9,575z" transform="translate(0)"/>
<path d="M1672.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6c-20.4,11.7-36.5,27.7-48.2,48s-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S1692.6,428.8,1672.2,416.8z M1558.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H1558.3z" transform="translate(0)"/>
<path d="M1895.3,411.7c-11,5.6-20.3,13.7-28,24.4h-0.1v-28h-84.5v247.3h84.5V536.3c0-22.6,4.7-38.1,14.2-46.5 c9.5-8.5,22.7-12.7,39.6-12.7c6.2,0,13.5,1,21.8,3.1l10.7-72c-5.9-3.3-14.5-4.9-25.8-4.9C1917.1,403.3,1906.3,406.1,1895.3,411.7z" transform="translate(0)"/>
<rect x="1985" y="277.4" width="84.5" height="377.8" transform="translate(0)"/>
<path d="M2313.2,416.8c-20.5-12-43-18-67.6-18c-24.9,0-47.6,5.9-68,17.6s-36.5,27.7-48.2,48c-11.7,20.3-17.6,42.7-17.6,67.3 c0.3,25.2,6.2,47.8,17.8,68c11.5,20.2,28,36,49.3,47.6c21.3,11.5,45.9,17.3,73.8,17.3c48.6,0,86.8-14.7,114.7-44l-52.5-48.9 c-8.6,8.3-17.6,14.6-26.7,19c-9.3,4.3-21.1,6.4-35.3,6.4c-11.6,0-22.5-3.6-32.7-10.9c-10.3-7.3-17.1-16.5-20.7-27.8h180l0.4-11.6 c0-29.6-6-55.7-18-78.2S2333.6,428.8,2313.2,416.8z M2199.3,503.2c2.1-12.1,7.5-21.8,16.2-29.1s18.7-10.9,30-10.9 s21.2,3.6,29.8,10.9c8.6,7.2,13.9,16.9,16,29.1H2199.3z" transform="translate(0)"/>
<path d="M2583.6,507.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9 c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8 c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7 c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6 c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9 c34.7,0,62.9-7.4,84.5-22.4c21.7-15,32.5-37.3,32.5-66.9c0-19.3-5-34.2-15.1-44.9S2597.4,512.1,2583.6,507.7z" transform="translate(0)"/>
<path d="M2883.4,575.3c0-19.3-5-34.2-15.1-44.9s-22-18.3-35.8-22.7c-13.8-4.4-30.6-8.1-50.5-11.1c-15.1-2.7-26.1-5.2-32.9-7.6 c-6.8-2.4-10.2-6.1-10.2-11.1s2.3-8.7,6.7-10.9c4.4-2.2,11.5-3.3,21.3-3.3c11.6,0,24.3,2.4,38.1,7.2c13.9,4.8,26.2,11,36.9,18.4 l32.4-58.2c-11.3-7.4-26.2-14.7-44.9-21.8c-18.7-7.1-39.6-10.7-62.7-10.7c-33.7,0-60.2,7.6-79.3,22.7 c-19.1,15.1-28.7,36.1-28.7,63.1c0,19,4.8,33.9,14.4,44.7c9.6,10.8,21,18.5,34,22.9c13.1,4.5,28.9,8.3,47.6,11.6 c14.6,2.7,25.1,5.3,31.6,7.8s9.8,6.5,9.8,11.8c0,10.4-9.7,15.6-29.3,15.6c-13.7,0-28.5-2.3-44.7-6.9c-16.1-4.6-29.2-11.3-39.3-20.2 l-33.3,60c9.2,7.4,24.6,14.7,46.2,22c21.7,7.3,45.2,10.9,70.7,10.9c34.7,0,62.9-7.4,84.5-22.4 C2872.6,627.2,2883.4,604.9,2883.4,575.3z" transform="translate(0)"/>
<rect x="2460.7" y="738.7" width="59.6" height="17.2" transform="translate(0)"/>
<path d="M2596.5,706.4c-5.7,0-11,1-15.8,3s-9,5-12.5,8.9v-9.4h-19.4v93.6h19.4v-52c0-8.6,2.1-15.3,6.3-20c4.2-4.7,9.5-7.1,15.9-7.1 c7.8,0,13.4,2.3,16.8,6.7c3.4,4.5,5.1,11.3,5.1,20.5v52h19.4v-56.8c0-12.8-3.2-22.6-9.5-29.3 C2615.8,709.8,2607.3,706.4,2596.5,706.4z" transform="translate(0)"/>
<path d="M2733.8,717.7c-3.6-3.4-7.9-6.1-13.1-8.2s-10.6-3.1-16.2-3.1c-8.7,0-16.5,2.1-23.5,6.3s-12.5,10-16.5,17.3 c-4,7.3-6,15.4-6,24.4c0,8.9,2,17.1,6,24.3c4,7.3,9.5,13,16.5,17.2s14.9,6.3,23.5,6.3c5.6,0,11-1,16.2-3.1 c5.1-2.1,9.5-4.8,13.1-8.2v24.4c0,8.5-2.5,14.8-7.6,18.7c-5,3.9-11,5.9-18,5.9c-6.7,0-12.4-1.6-17.3-4.7c-4.8-3.1-7.6-7.7-8.3-13.8 h-19.4c0.6,7.7,2.9,14.2,7.1,19.5s9.6,9.3,16.2,12c6.6,2.7,13.8,4,21.7,4c12.8,0,23.5-3.4,32-10.1c8.6-6.7,12.8-17.1,12.8-31.1 V708.9h-19.2V717.7z M2732.2,770.1c-2.5,4.7-6,8.3-10.4,11.2c-4.4,2.7-9.4,4-14.9,4c-5.7,0-10.8-1.4-15.2-4.3s-7.8-6.7-10.2-11.4 c-2.3-4.8-3.5-9.8-3.5-15.2c0-5.5,1.1-10.6,3.5-15.3s5.8-8.5,10.2-11.3s9.5-4.2,15.2-4.2c5.5,0,10.5,1.4,14.9,4s7.9,6.3,10.4,11 s3.8,10,3.8,15.8S2734.7,765.4,2732.2,770.1z" transform="translate(0)"/>
<polygon points="2867.9,708.9 2846.5,708.9 2820.9,741.9 2795.5,708.9 2773.1,708.9 2809.1,755 2771.5,802.5 2792.9,802.5 2820.1,767.9 2847.2,802.6 2869.6,802.6 2832,754.4 " transform="translate(0)"/>
<path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/>
</g>
</svg>
}
@@ -9,15 +9,6 @@ describe('PngxPdfViewerComponent', () => {
let fixture: ComponentFixture<PngxPdfViewerComponent>
let component: PngxPdfViewerComponent
const setBaseHref = (href: string) => {
let base = document.querySelector('base')
if (!base) {
base = document.createElement('base')
document.head.appendChild(base)
}
base.setAttribute('href', href)
}
const initComponent = async (src = 'test.pdf') => {
component.src = src
fixture.detectChanges()
@@ -33,10 +24,6 @@ describe('PngxPdfViewerComponent', () => {
component = fixture.componentInstance
})
afterEach(() => {
setBaseHref('/')
})
it('loads a document and emits events', async () => {
const loadSpy = jest.fn()
const renderedSpy = jest.fn()
@@ -46,7 +33,7 @@ describe('PngxPdfViewerComponent', () => {
await initComponent()
expect(pdfjs.GlobalWorkerOptions.workerSrc).toBe(
new URL('assets/js/pdf.worker.min.mjs', document.baseURI).toString()
'/assets/js/pdf.worker.min.mjs'
)
const isVisible = (component as any).findController.onIsPageVisible as
| (() => boolean)
@@ -59,19 +46,6 @@ describe('PngxPdfViewerComponent', () => {
expect((component as any).pdfViewer).toBeInstanceOf(PDFViewer)
})
it('resolves the worker source relative to the document base URI', async () => {
setBaseHref('/paperless/')
await initComponent()
expect(pdfjs.GlobalWorkerOptions.workerSrc).toBe(
new URL('assets/js/pdf.worker.min.mjs', document.baseURI).toString()
)
expect(pdfjs.GlobalWorkerOptions.workerSrc).toContain(
'/paperless/assets/js/pdf.worker.min.mjs'
)
})
it('initializes single-page viewer and disables text layer', async () => {
component.renderMode = PdfRenderMode.Single
component.selectable = false
@@ -1,10 +1,8 @@
import {
AfterViewInit,
Component,
DOCUMENT,
ElementRef,
EventEmitter,
inject,
Input,
OnChanges,
OnDestroy,
@@ -41,8 +39,6 @@ import {
export class PngxPdfViewerComponent
implements AfterViewInit, OnChanges, OnDestroy
{
private readonly document = inject<Document>(DOCUMENT)
@Input() src!: PdfSource
@Input() page?: number
@Output() pageChange = new EventEmitter<number>()
@@ -170,10 +166,7 @@ export class PngxPdfViewerComponent
this.lastFindQuery = ''
this.loadingTask?.destroy()
GlobalWorkerOptions.workerSrc = new URL(
'assets/js/pdf.worker.min.mjs',
this.document.baseURI
).toString()
GlobalWorkerOptions.workerSrc = '/assets/js/pdf.worker.min.mjs'
this.loadingTask = getDocument(this.src)
try {
@@ -279,7 +272,7 @@ export class PngxPdfViewerComponent
if (!this.hasRenderedPage) {
return
}
const query = this.searchQuery?.trim()
const query = this.searchQuery.trim()
if (query === this.lastFindQuery) {
return
}
@@ -133,28 +133,28 @@ describe('PermissionsSelectComponent', () => {
expect(viewInput.nativeElement.disabled).toBeFalsy()
})
it('should treat system monitoring as view-only', () => {
it('should treat system status as view-only', () => {
component.ngOnInit()
fixture.detectChanges()
expect(
component.isActionSupported(
PermissionType.SystemMonitoring,
PermissionType.SystemStatus,
PermissionAction.View
)
).toBeTruthy()
expect(
component.isActionSupported(
PermissionType.SystemMonitoring,
PermissionType.SystemStatus,
PermissionAction.Change
)
).toBeFalsy()
const changeInput = fixture.debugElement.query(
By.css('input#SystemMonitoring_Change')
By.css('input#SystemStatus_Change')
)
const viewInput = fixture.debugElement.query(
By.css('input#SystemMonitoring_View')
By.css('input#SystemStatus_View')
)
expect(changeInput.nativeElement.disabled).toBeTruthy()
@@ -261,7 +261,7 @@ export class PermissionsSelectComponent
// Global statistics and system status only support view
if (
type === PermissionType.GlobalStatistics ||
type === PermissionType.SystemMonitoring
type === PermissionType.SystemStatus
) {
return action === PermissionAction.View
}
@@ -25,19 +25,19 @@
</div>
}
@if (suggestions?.suggested_tags.length > 0) {
<small class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="tags"></i-bs>Tags</small>
<small class="list-group-item text-uppercase text-muted small">Tags</small>
@for (tag of suggestions.suggested_tags; track tag) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addTag.emit(tag)" i18n>{{ tag }}</button>
}
}
@if (suggestions?.suggested_document_types.length > 0) {
<div class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="hash"></i-bs>Document Types</div>
<div class="list-group-item text-uppercase text-muted small">Document Types</div>
@for (type of suggestions.suggested_document_types; track type) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addDocumentType.emit(type)" i18n>{{ type }}</button>
}
}
@if (suggestions?.suggested_correspondents.length > 0) {
<div class="list-group-item text-uppercase text-muted small"><i-bs class="me-2" name="person"></i-bs>Correspondents</div>
<div class="list-group-item text-uppercase text-muted small">Correspondents</div>
@for (correspondent of suggestions.suggested_correspondents; track correspondent) {
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addCorrespondent.emit(correspondent)" i18n>{{ correspondent }}</button>
}
@@ -142,31 +142,6 @@
}
</ng-template>
</dd>
<dt i18n>Recent Task Activity <span class="small text-muted fw-light">({{status.tasks.summary.days}} days)</span></dt>
<dd class="mb-0">
@if (status.tasks.summary.total_count > 0) {
<ul class="list-group border-light mt-2">
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<span class="small"><ng-container i18n>Total</ng-container>:</span>
<span class="badge bg-light rounded-pill">{{status.tasks.summary.total_count}}</span>
</li>
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<span class="small"><ng-container i18n>Successful</ng-container>:</span>
<span class="badge bg-primary rounded-pill">{{status.tasks.summary.success_count}}</span>
</li>
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<span class="small"><ng-container i18n>Failed</ng-container>:</span>
<span class="badge bg-danger rounded-pill">{{status.tasks.summary.failure_count}}</span>
</li>
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<span class="small"><ng-container i18n>Pending</ng-container>:</span>
<span class="badge bg-warning rounded-pill">{{status.tasks.summary.pending_count}}</span>
</li>
</ul>
} @else {
<span class="small text-muted" i18n>No recent tasks</span>
}
</dd>
</dl>
</div>
</div>
@@ -184,11 +159,25 @@
<button class="btn btn-sm d-flex align-items-center btn-dark text-uppercase small" [ngbPopover]="indexStatus" triggers="click mouseenter:mouseleave">
{{status.tasks.index_status}}
@if (status.tasks.index_status === 'OK') {
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
@if (isStale(status.tasks.index_last_modified)) {
<i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1"></i-bs>
} @else {
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
}
} @else {
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
}
</button>
@if (currentUserIsSuperUser) {
@if (isRunning(PaperlessTaskName.IndexOptimize)) {
<div class="spinner-border spinner-border-sm ms-2" role="status"></div>
} @else {
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskName.IndexOptimize)">
<i-bs name="play-fill" class="me-1"></i-bs>
<ng-container i18n>Run Task</ng-container>
</button>
}
}
</dd>
<ng-template #indexStatus>
@if (status.tasks.index_status === 'OK') {
@@ -214,10 +203,10 @@
}
</button>
@if (currentUserIsSuperUser) {
@if (isRunning(PaperlessTaskType.TrainClassifier)) {
@if (isRunning(PaperlessTaskName.TrainClassifier)) {
<div class="spinner-border spinner-border-sm ms-2" role="status"></div>
} @else {
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskType.TrainClassifier)">
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskName.TrainClassifier)">
<i-bs name="play-fill" class="me-1"></i-bs>
<ng-container i18n>Run Task</ng-container>
</button>
@@ -248,10 +237,10 @@
}
</button>
@if (currentUserIsSuperUser) {
@if (isRunning(PaperlessTaskType.SanityCheck)) {
@if (isRunning(PaperlessTaskName.SanityCheck)) {
<div class="spinner-border spinner-border-sm ms-2" role="status"></div>
} @else {
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskType.SanityCheck)">
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskName.SanityCheck)">
<i-bs name="play-fill" class="me-1"></i-bs>
<ng-container i18n>Run Task</ng-container>
</button>
@@ -296,10 +285,10 @@
}
</button>
@if (currentUserIsSuperUser) {
@if (isRunning(PaperlessTaskType.LlmIndex)) {
@if (isRunning(PaperlessTaskName.LLMIndexUpdate)) {
<div class="spinner-border spinner-border-sm ms-2" role="status"></div>
} @else {
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskType.LlmIndex)">
<button class="btn btn-sm d-flex align-items-center btn-dark small ms-2" (click)="runTask(PaperlessTaskName.LLMIndexUpdate)">
<i-bs name="play-fill" class="me-1"></i-bs>
<ng-container i18n>Run Task</ng-container>
</button>
@@ -25,7 +25,7 @@ import {
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { Subject, of, throwError } from 'rxjs'
import { PaperlessTaskType } from 'src/app/data/paperless-task'
import { PaperlessTaskName } from 'src/app/data/paperless-task'
import {
InstallType,
SystemStatus,
@@ -71,13 +71,6 @@ const status: SystemStatus = {
llmindex_status: SystemStatusItemStatus.OK,
llmindex_last_modified: new Date().toISOString(),
llmindex_error: null,
summary: {
days: 30,
total_count: 12,
pending_count: 1,
success_count: 10,
failure_count: 1,
},
},
}
@@ -145,9 +138,9 @@ describe('SystemStatusDialogComponent', () => {
})
it('should check if task is running', () => {
component.runTask(PaperlessTaskType.SanityCheck)
expect(component.isRunning(PaperlessTaskType.SanityCheck)).toBeTruthy()
expect(component.isRunning(PaperlessTaskType.TrainClassifier)).toBeFalsy()
component.runTask(PaperlessTaskName.IndexOptimize)
expect(component.isRunning(PaperlessTaskName.IndexOptimize)).toBeTruthy()
expect(component.isRunning(PaperlessTaskName.SanityCheck)).toBeFalsy()
})
it('should support running tasks, refresh status and show toasts', () => {
@@ -158,22 +151,22 @@ describe('SystemStatusDialogComponent', () => {
// fail first
runSpy.mockReturnValue(throwError(() => new Error('error')))
component.runTask(PaperlessTaskType.SanityCheck)
expect(runSpy).toHaveBeenCalledWith(PaperlessTaskType.SanityCheck)
component.runTask(PaperlessTaskName.IndexOptimize)
expect(runSpy).toHaveBeenCalledWith(PaperlessTaskName.IndexOptimize)
expect(toastErrorSpy).toHaveBeenCalledWith(
`Failed to start task ${PaperlessTaskType.SanityCheck}, see the logs for more details`,
`Failed to start task ${PaperlessTaskName.IndexOptimize}, see the logs for more details`,
expect.any(Error)
)
// succeed
runSpy.mockReturnValue(of({}))
getStatusSpy.mockReturnValue(of(status))
component.runTask(PaperlessTaskType.SanityCheck)
expect(runSpy).toHaveBeenCalledWith(PaperlessTaskType.SanityCheck)
component.runTask(PaperlessTaskName.IndexOptimize)
expect(runSpy).toHaveBeenCalledWith(PaperlessTaskName.IndexOptimize)
expect(getStatusSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
`Task ${PaperlessTaskType.SanityCheck} started`
`Task ${PaperlessTaskName.IndexOptimize} started`
)
})
@@ -8,7 +8,7 @@ import {
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, takeUntil } from 'rxjs'
import { PaperlessTaskType } from 'src/app/data/paperless-task'
import { PaperlessTaskName } from 'src/app/data/paperless-task'
import {
SystemStatus,
SystemStatusItemStatus,
@@ -49,14 +49,14 @@ export class SystemStatusDialogComponent implements OnInit, OnDestroy {
private settingsService = inject(SettingsService)
public SystemStatusItemStatus = SystemStatusItemStatus
public PaperlessTaskType = PaperlessTaskType
public PaperlessTaskName = PaperlessTaskName
public status: SystemStatus
public frontendVersion: string = environment.version
public versionMismatch: boolean = false
public copied: boolean = false
private runningTasks: Set<PaperlessTaskType> = new Set()
private runningTasks: Set<PaperlessTaskName> = new Set()
private unsubscribeNotifier: Subject<any> = new Subject()
get currentUserIsSuperUser(): boolean {
@@ -107,11 +107,11 @@ export class SystemStatusDialogComponent implements OnInit, OnDestroy {
return now.getTime() - date.getTime() > hours * 60 * 60 * 1000
}
public isRunning(taskName: PaperlessTaskType): boolean {
public isRunning(taskName: PaperlessTaskName): boolean {
return this.runningTasks.has(taskName)
}
public runTask(taskName: PaperlessTaskType) {
public runTask(taskName: PaperlessTaskName) {
this.runningTasks.add(taskName)
this.toastService.showInfo(`Task ${taskName} started`)
this.tasksService.run(taskName).subscribe({
@@ -1,5 +1,5 @@
<pngx-page-header title="Dashboard" [subTitle]="subtitle" i18n-title tourAnchor="tour.dashboard">
<pngx-logo extra_classes="d-none d-md-block mt-n2" height="3rem"></pngx-logo>
<pngx-logo extra_classes="d-none d-md-block mt-n2" height="3.5rem"></pngx-logo>
</pngx-page-header>
<div class="row">
@@ -54,10 +54,10 @@
@if (isFinished(status)) {
<div>
@if (status.documentId) {
<a class="btn btn-sm btn-outline-primary btn-open" [routerLink]="['/documents', status.documentId]" (click)="dismiss(status)">
<button class="btn btn-sm btn-outline-primary btn-open" routerLink="/documents/{{status.documentId}}" (click)="dismiss(status)">
<small i18n>Open document</small>
<i-bs name="arrow-right-short"></i-bs>
</a>
</button>
}
</div>
}
@@ -1381,7 +1381,6 @@ describe('DocumentDetailComponent', () => {
it('should get suggestions', () => {
const suggestionsSpy = jest.spyOn(documentService, 'getSuggestions')
const aiSuggestionsSpy = jest.spyOn(documentService, 'getAiSuggestions')
suggestionsSpy.mockReturnValue(
of({
tags: [42, 43],
@@ -1392,35 +1391,6 @@ describe('DocumentDetailComponent', () => {
)
initNormally()
expect(suggestionsSpy).toHaveBeenCalled()
expect(aiSuggestionsSpy).not.toHaveBeenCalled()
expect(component.suggestions).toEqual({
tags: [42, 43],
suggested_tags: [],
suggested_document_types: [],
suggested_correspondents: [],
})
})
it('should get AI suggestions when AI is enabled', () => {
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')
const aiSuggestionsSpy = jest.spyOn(documentService, 'getAiSuggestions')
aiSuggestionsSpy.mockReturnValue(
of({
tags: [42, 43],
suggested_tags: [],
suggested_document_types: [],
suggested_correspondents: [],
})
)
initNormally()
expect(suggestionsSpy).not.toHaveBeenCalled()
expect(aiSuggestionsSpy).toHaveBeenCalled()
expect(component.suggestions).toEqual({
tags: [42, 43],
suggested_tags: [],
@@ -2221,20 +2191,10 @@ describe('DocumentDetailComponent', () => {
expect(createElementSpy).toHaveBeenCalledWith('iframe')
expect(appendChildSpy).toHaveBeenCalledWith(mockIframe)
expect(createObjectURLSpy).toHaveBeenCalledWith(blob)
expect(mockIframe.style).toEqual({
position: 'fixed',
right: '0',
bottom: '0',
width: '0',
height: '0',
border: '0',
visibility: 'hidden',
})
if (mockIframe.onload) {
mockIframe.onload({} as any)
}
tick()
expect(mockContentWindow.focus).toHaveBeenCalled()
expect(mockContentWindow.print).toHaveBeenCalled()
@@ -2347,7 +2307,6 @@ describe('DocumentDetailComponent', () => {
mockIframe.onload(new Event('load'))
}
tick()
tick(200)
if (expectToast) {
@@ -981,10 +981,8 @@ export class DocumentDetailComponent
getSuggestions() {
this.suggestionsLoading = true
const suggestionsObservable = this.aiEnabled
? this.documentsService.getAiSuggestions(this.documentId)
: this.documentsService.getSuggestions(this.documentId)
suggestionsObservable
this.documentsService
.getSuggestions(this.documentId)
.pipe(
first(),
takeUntil(this.unsubscribeNotifier),
@@ -1873,39 +1871,31 @@ export class DocumentDetailComponent
next: (blob) => {
const blobUrl = URL.createObjectURL(blob)
const iframe = document.createElement('iframe')
iframe.style.position = 'fixed'
iframe.style.right = '0'
iframe.style.bottom = '0'
iframe.style.width = '0'
iframe.style.height = '0'
iframe.style.border = '0'
iframe.style.visibility = 'hidden'
iframe.style.display = 'none'
iframe.src = blobUrl
document.body.appendChild(iframe)
iframe.onload = () => {
timer(0).subscribe(() => {
try {
iframe.contentWindow.focus()
iframe.contentWindow.print()
iframe.contentWindow.onafterprint = () => {
document.body.removeChild(iframe)
URL.revokeObjectURL(blobUrl)
}
} catch (err) {
// FF throws cross-origin error on onafterprint
const isCrossOriginAfterPrintError =
err instanceof DOMException &&
err.message.includes('onafterprint')
if (!isCrossOriginAfterPrintError) {
this.toastService.showError($localize`Print failed.`, err)
}
timer(100).subscribe(() => {
// delay to avoid FF print failure
document.body.removeChild(iframe)
URL.revokeObjectURL(blobUrl)
})
try {
iframe.contentWindow.focus()
iframe.contentWindow.print()
iframe.contentWindow.onafterprint = () => {
document.body.removeChild(iframe)
URL.revokeObjectURL(blobUrl)
}
})
} catch (err) {
// FF throws cross-origin error on onafterprint
const isCrossOriginAfterPrintError =
err instanceof DOMException &&
err.message.includes('onafterprint')
if (!isCrossOriginAfterPrintError) {
this.toastService.showError($localize`Print failed.`, err)
}
timer(100).subscribe(() => {
// delay to avoid FF print failure
document.body.removeChild(iframe)
URL.revokeObjectURL(blobUrl)
})
}
}
},
error: () => {
+2 -11
View File
@@ -55,13 +55,12 @@ export const ConfigCategory = {
}
export const LLMEmbeddingBackendConfig = {
OPENAI_LIKE: 'openai-like',
OPENAI: 'openai',
HUGGINGFACE: 'huggingface',
OLLAMA: 'ollama',
}
export const LLMBackendConfig = {
OPENAI_LIKE: 'openai-like',
OPENAI: 'openai',
OLLAMA: 'ollama',
}
@@ -302,13 +301,6 @@ export const PaperlessConfigOptions: ConfigOption[] = [
config_key: 'PAPERLESS_AI_LLM_EMBEDDING_MODEL',
category: ConfigCategory.AI,
},
{
key: 'llm_embedding_endpoint',
title: $localize`LLM Embedding Endpoint`,
type: ConfigOptionType.String,
config_key: 'PAPERLESS_AI_LLM_EMBEDDING_ENDPOINT',
category: ConfigCategory.AI,
},
{
key: 'llm_backend',
title: $localize`LLM Backend`,
@@ -371,7 +363,6 @@ export interface PaperlessConfig extends ObjectWithId {
ai_enabled: boolean
llm_embedding_backend: string
llm_embedding_model: string
llm_embedding_endpoint: string
llm_backend: string
llm_model: string
llm_api_key: string
+34 -51
View File
@@ -1,66 +1,49 @@
import { Document } from './document'
import { ObjectWithId } from './object-with-id'
export enum PaperlessTaskType {
ConsumeFile = 'consume_file',
TrainClassifier = 'train_classifier',
SanityCheck = 'sanity_check',
MailFetch = 'mail_fetch',
LlmIndex = 'llm_index',
EmptyTrash = 'empty_trash',
CheckWorkflows = 'check_workflows',
BulkUpdate = 'bulk_update',
ReprocessDocument = 'reprocess_document',
BuildShareLink = 'build_share_link',
BulkDelete = 'bulk_delete',
Auto = 'auto_task',
ScheduledTask = 'scheduled_task',
ManualTask = 'manual_task',
}
export enum PaperlessTaskTriggerSource {
Scheduled = 'scheduled',
WebUI = 'web_ui',
ApiUpload = 'api_upload',
FolderConsume = 'folder_consume',
EmailConsume = 'email_consume',
System = 'system',
Manual = 'manual',
export enum PaperlessTaskName {
ConsumeFile = 'consume_file',
TrainClassifier = 'train_classifier',
SanityCheck = 'check_sanity',
IndexOptimize = 'index_optimize',
LLMIndexUpdate = 'llmindex_update',
}
export enum PaperlessTaskStatus {
Pending = 'pending',
Started = 'started',
Success = 'success',
Failure = 'failure',
Revoked = 'revoked',
Pending = 'PENDING',
Started = 'STARTED',
Complete = 'SUCCESS',
Failed = 'FAILURE',
}
export interface PaperlessTask extends ObjectWithId {
task_id: string
task_type: PaperlessTaskType
task_type_display: string
trigger_source: PaperlessTaskTriggerSource
trigger_source_display: string
type: PaperlessTaskType
status: PaperlessTaskStatus
status_display: string
date_created: Date
date_started?: Date
date_done?: Date
duration_seconds?: number
wait_time_seconds?: number
input_data: Record<string, any>
result_data?: Record<string, any>
related_document_ids: number[]
acknowledged: boolean
task_id: string
task_file_name: string
task_name: PaperlessTaskName
date_created: Date
date_done?: Date
result?: string
related_document?: number
duplicate_documents?: Document[]
owner?: number
}
export interface PaperlessTaskSummary {
task_type: PaperlessTaskType
total_count: number
pending_count: number
success_count: number
failure_count: number
avg_duration_seconds: number | null
avg_wait_time_seconds: number | null
last_run: Date | null
last_success: Date | null
last_failure: Date | null
}
-7
View File
@@ -47,13 +47,6 @@ export interface SystemStatus {
llmindex_status: SystemStatusItemStatus
llmindex_last_modified: string // ISO date string
llmindex_error: string
summary: {
days: number
total_count: number
pending_count: number
success_count: number
failure_count: number
}
}
websocket_connected?: SystemStatusItemStatus // added client-side
}
+1 -23
View File
@@ -9,11 +9,7 @@ import {
} from '@angular/common/http/testing'
import { TestBed } from '@angular/core/testing'
import { environment } from 'src/environments/environment'
import {
CHAT_METADATA_DELIMITER,
ChatService,
parseChatResponse,
} from './chat.service'
import { ChatService } from './chat.service'
describe('ChatService', () => {
let service: ChatService
@@ -59,22 +55,4 @@ describe('ChatService', () => {
partialText: mockResponse,
} as any)
})
it('should parse chat references from the metadata trailer', () => {
const parsed = parseChatResponse(
`Answer text${CHAT_METADATA_DELIMITER}{"references":[{"id":1,"title":"Document 1"}]}`
)
expect(parsed.content).toBe('Answer text')
expect(parsed.references).toEqual([{ id: 1, title: 'Document 1' }])
})
it('should hide incomplete metadata trailer from the visible content', () => {
const parsed = parseChatResponse(
`Answer text${CHAT_METADATA_DELIMITER}{"references"`
)
expect(parsed.content).toBe('Answer text')
expect(parsed.references).toBeUndefined()
})
})
-40
View File
@@ -11,46 +11,6 @@ export interface ChatMessage {
role: 'user' | 'assistant'
content: string
isStreaming?: boolean
references?: ChatReference[]
}
export interface ChatReference {
id: number
title: string
}
export interface ParsedChatResponse {
content: string
references?: ChatReference[]
}
export const CHAT_METADATA_DELIMITER = '\n\n__PAPERLESS_CHAT_METADATA__'
export function parseChatResponse(response: string): ParsedChatResponse {
const delimiterIndex = response.indexOf(CHAT_METADATA_DELIMITER)
if (delimiterIndex === -1) {
return { content: response }
}
const metadataString = response.slice(
delimiterIndex + CHAT_METADATA_DELIMITER.length
)
try {
const metadata = JSON.parse(metadataString) as {
references?: ChatReference[]
}
return {
content: response.slice(0, delimiterIndex),
references: metadata.references ?? [],
}
} catch {
return {
content: response.slice(0, delimiterIndex),
}
}
}
@Injectable({
@@ -8,7 +8,7 @@ import {
const VIEW_ONLY_PERMISSION_TYPES = new Set<PermissionType>([
PermissionType.GlobalStatistics,
PermissionType.SystemMonitoring,
PermissionType.SystemStatus,
])
describe('PermissionsService', () => {
@@ -270,7 +270,7 @@ describe('PermissionsService', () => {
'delete_applicationconfiguration',
'view_applicationconfiguration',
'view_global_statistics',
'view_system_monitoring',
'view_system_status',
],
{
username: 'testuser',
@@ -30,7 +30,7 @@ export enum PermissionType {
Workflow = '%s_workflow',
ProcessedMail = '%s_processedmail',
GlobalStatistics = '%s_global_statistics',
SystemMonitoring = '%s_system_monitoring',
SystemStatus = '%s_system_status',
}
@Injectable({
@@ -193,14 +193,6 @@ describe(`DocumentService`, () => {
expect(req.request.method).toEqual('GET')
})
it('should call appropriate api endpoint for getting AI suggestions', () => {
subscription = service.getAiSuggestions(documents[0].id).subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/${documents[0].id}/ai_suggestions/`
)
expect(req.request.method).toEqual('GET')
})
it('should call appropriate api endpoint for bulk download', () => {
const ids = [1, 2, 3]
const content = 'both'
@@ -404,12 +404,6 @@ export class DocumentService extends AbstractPaperlessService<Document> {
)
}
getAiSuggestions(id: number): Observable<DocumentSuggestions> {
return this.http.get<DocumentSuggestions>(
this.getResourceUrl(id, 'ai_suggestions')
)
}
getHistory(id: number): Observable<AuditLogEntry[]> {
return this.http.get<AuditLogEntry[]>(this.getResourceUrl(id, 'history'))
}
+30 -104
View File
@@ -1,8 +1,4 @@
import {
HttpRequest,
provideHttpClient,
withInterceptorsFromDi,
} from '@angular/common/http'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import {
HttpTestingController,
provideHttpClientTesting,
@@ -10,8 +6,8 @@ import {
import { TestBed } from '@angular/core/testing'
import { environment } from 'src/environments/environment'
import {
PaperlessTaskName,
PaperlessTaskStatus,
PaperlessTaskTriggerSource,
PaperlessTaskType,
} from '../data/paperless-task'
import { TasksService } from './tasks.service'
@@ -41,21 +37,16 @@ describe('TasksService', () => {
it('calls tasks api endpoint on reload', () => {
tasksService.reload()
const req = httpTestingController.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
)
expect(req.request.method).toEqual('GET')
req.flush({ count: 0, results: [] })
})
it('does not call tasks api endpoint on reload if already loading', () => {
tasksService.loading = true
tasksService.reload()
httpTestingController.expectNone(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/`
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
)
})
@@ -72,79 +63,68 @@ describe('TasksService', () => {
// reload is then called
httpTestingController
.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
)
.flush({ count: 0, results: [] })
.flush([])
})
it('groups mixed task types by status when reloading', () => {
it('sorts tasks returned from api', () => {
expect(tasksService.total).toEqual(0)
const mockTasks = [
{
task_type: PaperlessTaskType.ConsumeFile,
trigger_source: PaperlessTaskTriggerSource.FolderConsume,
status: PaperlessTaskStatus.Success,
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
status: PaperlessTaskStatus.Complete,
acknowledged: false,
task_id: '1234',
input_data: { filename: 'file1.pdf' },
task_file_name: 'file1.pdf',
date_created: new Date(),
related_document_ids: [],
},
{
task_type: PaperlessTaskType.SanityCheck,
trigger_source: PaperlessTaskTriggerSource.System,
status: PaperlessTaskStatus.Failure,
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
status: PaperlessTaskStatus.Failed,
acknowledged: false,
task_id: '1235',
input_data: {},
task_file_name: 'file2.pdf',
date_created: new Date(),
related_document_ids: [],
},
{
task_type: PaperlessTaskType.MailFetch,
trigger_source: PaperlessTaskTriggerSource.Scheduled,
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
status: PaperlessTaskStatus.Pending,
acknowledged: false,
task_id: '1236',
input_data: {},
task_file_name: 'file3.pdf',
date_created: new Date(),
related_document_ids: [],
},
{
task_type: PaperlessTaskType.LlmIndex,
trigger_source: PaperlessTaskTriggerSource.WebUI,
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
status: PaperlessTaskStatus.Started,
acknowledged: false,
task_id: '1237',
input_data: {},
task_file_name: 'file4.pdf',
date_created: new Date(),
related_document_ids: [],
},
{
task_type: PaperlessTaskType.EmptyTrash,
trigger_source: PaperlessTaskTriggerSource.Manual,
status: PaperlessTaskStatus.Success,
type: PaperlessTaskType.Auto,
task_name: PaperlessTaskName.ConsumeFile,
status: PaperlessTaskStatus.Complete,
acknowledged: false,
task_id: '1238',
input_data: {},
task_file_name: 'file5.pdf',
date_created: new Date(),
related_document_ids: [],
},
]
tasksService.reload()
const req = httpTestingController.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
)
req.flush({ count: mockTasks.length, results: mockTasks })
req.flush(mockTasks)
expect(tasksService.allFileTasks).toHaveLength(5)
expect(tasksService.completedFileTasks).toHaveLength(2)
@@ -153,64 +133,10 @@ describe('TasksService', () => {
expect(tasksService.startedFileTasks).toHaveLength(1)
})
it('includes revoked tasks in needs attention', () => {
const mockTasks = [
{
task_type: PaperlessTaskType.SanityCheck,
trigger_source: PaperlessTaskTriggerSource.System,
status: PaperlessTaskStatus.Failure,
acknowledged: false,
task_id: '1235',
input_data: {},
date_created: new Date(),
related_document_ids: [],
},
{
task_type: PaperlessTaskType.MailFetch,
trigger_source: PaperlessTaskTriggerSource.Scheduled,
status: PaperlessTaskStatus.Revoked,
acknowledged: false,
task_id: '1236',
input_data: {},
date_created: new Date(),
related_document_ids: [],
},
{
task_type: PaperlessTaskType.EmptyTrash,
trigger_source: PaperlessTaskTriggerSource.Manual,
status: PaperlessTaskStatus.Success,
acknowledged: false,
task_id: '1238',
input_data: {},
date_created: new Date(),
related_document_ids: [],
},
]
tasksService.reload()
const req = httpTestingController.expectOne(
(req: HttpRequest<unknown>) =>
req.url === `${environment.apiBaseUrl}tasks/` &&
req.params.get('acknowledged') === 'false' &&
req.params.get('page_size') === '1000'
)
req.flush({ count: mockTasks.length, results: mockTasks })
expect(tasksService.needsAttentionTasks).toHaveLength(2)
expect(tasksService.needsAttentionTasks.map((task) => task.status)).toEqual(
expect.arrayContaining([
PaperlessTaskStatus.Failure,
PaperlessTaskStatus.Revoked,
])
)
})
it('supports running tasks', () => {
tasksService.run(PaperlessTaskType.SanityCheck).subscribe((res) => {
tasksService.run(PaperlessTaskName.SanityCheck).subscribe((res) => {
expect(res).toEqual({
task_id: 'abc-123',
result: 'success',
})
})
const req = httpTestingController.expectOne(
@@ -218,7 +144,7 @@ describe('TasksService', () => {
)
expect(req.request.method).toEqual('POST')
req.flush({
task_id: 'abc-123',
result: 'success',
})
})
})
+19 -52
View File
@@ -1,13 +1,12 @@
import { HttpClient } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
import { Observable, Subject } from 'rxjs'
import { first, map, takeUntil, tap } from 'rxjs/operators'
import { first, takeUntil, tap } from 'rxjs/operators'
import {
PaperlessTask,
PaperlessTaskName,
PaperlessTaskStatus,
PaperlessTaskType,
} from 'src/app/data/paperless-task'
import { Results } from 'src/app/data/results'
import { environment } from 'src/environments/environment'
@Injectable({
@@ -18,9 +17,8 @@ export class TasksService {
private baseUrl: string = environment.apiBaseUrl
private endpoint: string = 'tasks'
private readonly defaultReloadPageSize = 1000
public loading: boolean = false
public loading: boolean
private fileTasks: PaperlessTask[] = []
@@ -35,35 +33,21 @@ export class TasksService {
}
public get queuedFileTasks(): PaperlessTask[] {
return this.fileTasks.filter(
(t) => t.status === PaperlessTaskStatus.Pending
)
return this.fileTasks.filter((t) => t.status == PaperlessTaskStatus.Pending)
}
public get startedFileTasks(): PaperlessTask[] {
return this.fileTasks.filter(
(t) => t.status === PaperlessTaskStatus.Started
)
return this.fileTasks.filter((t) => t.status == PaperlessTaskStatus.Started)
}
public get completedFileTasks(): PaperlessTask[] {
return this.fileTasks.filter(
(t) => t.status === PaperlessTaskStatus.Success
(t) => t.status == PaperlessTaskStatus.Complete
)
}
public get failedFileTasks(): PaperlessTask[] {
return this.fileTasks.filter(
(t) => t.status === PaperlessTaskStatus.Failure
)
}
public get needsAttentionTasks(): PaperlessTask[] {
return this.fileTasks.filter((t) =>
[PaperlessTaskStatus.Failure, PaperlessTaskStatus.Revoked].includes(
t.status
)
)
return this.fileTasks.filter((t) => t.status == PaperlessTaskStatus.Failed)
}
public reload() {
@@ -71,38 +55,19 @@ export class TasksService {
this.loading = true
this.http
.get<Results<PaperlessTask>>(`${this.baseUrl}${this.endpoint}/`, {
params: {
acknowledged: 'false',
page_size: this.defaultReloadPageSize,
},
})
.pipe(map((r) => r.results))
.get<PaperlessTask[]>(
`${this.baseUrl}${this.endpoint}/?task_name=consume_file&acknowledged=false`
)
.pipe(takeUntil(this.unsubscribeNotifer), first())
.subscribe((r) => {
this.fileTasks = r
this.fileTasks = r.filter(
(t) => t.task_name == PaperlessTaskName.ConsumeFile
)
this.loading = false
})
}
public list(
page: number,
pageSize: number,
extraParams?: Record<string, string | number | boolean>
): Observable<Results<PaperlessTask>> {
return this.http.get<Results<PaperlessTask>>(
`${this.baseUrl}${this.endpoint}/`,
{
params: {
page,
page_size: pageSize,
...extraParams,
},
}
)
}
public dismissTasks(task_ids: Set<number>): Observable<any> {
public dismissTasks(task_ids: Set<number>) {
return this.http
.post(`${this.baseUrl}tasks/acknowledge/`, {
tasks: [...task_ids],
@@ -120,10 +85,12 @@ export class TasksService {
this.unsubscribeNotifer.next(true)
}
public run(taskType: PaperlessTaskType): Observable<{ task_id: string }> {
return this.http.post<{ task_id: string }>(
public run(taskName: PaperlessTaskName): Observable<any> {
return this.http.post<any>(
`${environment.apiBaseUrl}${this.endpoint}/run/`,
{ task_type: taskType }
{
task_name: taskName,
}
)
}
}
@@ -142,21 +142,6 @@ describe('CustomFieldQueryAtom', () => {
atom.value = [1, 3]
expect(changeSpy).toHaveBeenCalledTimes(1)
})
it('should emit one changed event when operator change coerces value', () => {
const atom = new CustomFieldQueryAtom([
1,
CustomFieldQueryOperator.In,
[1, 2],
])
const changeSpy = jest.fn()
atom.changed.subscribe(changeSpy)
atom.operator = CustomFieldQueryOperator.Exact
expect(changeSpy).toHaveBeenCalledTimes(1)
expect(atom.serialize()).toEqual([1, CustomFieldQueryOperator.Exact, ''])
})
})
describe('CustomFieldQueryExpression', () => {

Some files were not shown because too many files have changed in this diff Show More