Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e04e70db2e | ||
|
|
75517057d0 | ||
|
|
3efc91ff9a | ||
|
|
7c07c001cd | ||
|
|
f1f7f82e91 | ||
|
|
b3bd5d1fd1 | ||
|
|
ad9b368cb8 | ||
|
|
7c78b00df6 | ||
|
|
4dfb6e88af | ||
|
|
744797e59d | ||
|
|
689105c949 | ||
|
|
1332a30630 | ||
|
|
648e5608b7 | ||
|
|
42dffaa0c8 | ||
|
|
ec3359f1cb | ||
|
|
235e57bf77 | ||
|
|
630240971f | ||
|
|
44e312cfba | ||
|
|
67c158a0ab | ||
|
|
1554f0d439 | ||
|
|
3e250d7878 | ||
|
|
a6d63285b7 | ||
|
|
c1552140b0 | ||
|
|
238617e566 | ||
|
|
3f3671fbe5 | ||
|
|
01ba579766 | ||
|
|
e2864b6664 | ||
|
|
1756d70fda | ||
|
|
b12be9119f | ||
|
|
e66d5a70e7 | ||
|
|
1a81902a36 | ||
|
|
ec5b4eb939 | ||
|
|
3457823b5b | ||
|
|
ffbe3d1681 | ||
|
|
5c0f0b498a | ||
|
|
c46f748412 | ||
|
|
59be6583b8 | ||
|
|
aeefb962d4 | ||
|
|
f80be1030b | ||
|
|
7df744b684 | ||
|
|
c66634fcfd | ||
|
|
d92248c664 | ||
|
|
67152772d2 | ||
|
|
564b720bce | ||
|
|
6aaebd167d | ||
|
|
2d236024bc | ||
|
|
e09b352067 | ||
|
|
4c89b0f7df | ||
|
|
b020f12f41 | ||
|
|
43848a7d68 | ||
|
|
9e96fdc627 | ||
|
|
434b7efd66 | ||
|
|
814cc14f88 | ||
|
|
59bf69f1d5 | ||
|
|
9533e7c67a | ||
|
|
32b55555b5 | ||
|
|
c4c5b8ae3a | ||
|
|
743e555a1b | ||
|
|
c3f050d7c9 | ||
|
|
f64adb5b36 | ||
|
|
0207d48035 | ||
|
|
f3db8d8b59 | ||
|
|
8b288f9ee0 | ||
|
|
f3d91f3aea | ||
|
|
62361b67c4 | ||
|
|
9af9d35040 |
@@ -9,6 +9,8 @@ body:
|
||||
Stick to only a single feature per request. If you list multiple different features at once,
|
||||
your request will be closed.
|
||||
|
||||
Please read our [contributing guidelines](https://github.com/immich-app/immich/blob/main/CONTRIBUTING.md) before opening a feature request.
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: I have searched the existing feature requests, both open and closed, to make sure this is not a duplicate request.
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
name: Report an issue with Immich
|
||||
description: Report an issue with Immich
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please read our [contributing guidelines](https://github.com/immich-app/immich/blob/main/CONTRIBUTING.md) before opening an issue.
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: I have searched the existing issues, both open and closed, to make sure this is not a duplicate report.
|
||||
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
contents: read
|
||||
# Run on main branch or workflow_dispatch, or on PRs/other branches (build only, no upload)
|
||||
if: ${{ !github.event.pull_request.head.repo.fork && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
|
||||
runs-on: macos-26
|
||||
runs-on: xcode-27
|
||||
|
||||
steps:
|
||||
- id: token
|
||||
@@ -214,8 +214,8 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
permission-contents: read
|
||||
|
||||
- name: Select Xcode 26
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
|
||||
- name: Select Xcode 27.1
|
||||
run: sudo xcode-select -s /Applications/Xcode_27.1.app/Contents/Developer
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
working_directory: ./mobile
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@984c0c890880bbf811283d6f09c4607c62d210a4 # v1.323.0
|
||||
uses: ruby/setup-ruby@651ff0d62c5c79bed6d53ec518387aae20e72c1a # v1.329.0
|
||||
with:
|
||||
ruby-version: '3.4'
|
||||
bundler-cache: true # zizmor: ignore[cache-poisoning] release branches cannot read caches written by pull requests
|
||||
|
||||
@@ -200,8 +200,9 @@ jobs:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
||||
working-directory: 'deployment/modules/cloudflare/docs'
|
||||
shell: bash
|
||||
run: |
|
||||
mise run //deployment:tf output -- -json | jq -r '
|
||||
mise run //deployment:tf output -json | jq -r '
|
||||
"projectName=\(.pages_project_name.value)",
|
||||
"subdomain=\(.immich_app_branch_subdomain.value)"
|
||||
' >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
||||
working-directory: 'deployment/modules/cloudflare/docs'
|
||||
run: 'mise run //deployment:tf destroy -- -refresh=false'
|
||||
run: 'mise run //deployment:tf destroy -refresh=false'
|
||||
|
||||
- name: Comment
|
||||
uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
|
||||
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Generate a token
|
||||
id: generate_token
|
||||
if: ${{ inputs.skip != true }}
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
@@ -22,3 +22,4 @@ jobs:
|
||||
- uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0
|
||||
with:
|
||||
repo-token: ${{ steps.token.outputs.token }}
|
||||
sync-labels: true
|
||||
|
||||
+16
-26
@@ -1,43 +1,33 @@
|
||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||
|
||||
[[tools.opentofu]]
|
||||
version = "1.12.6"
|
||||
version = "1.13.1"
|
||||
backend = "aqua:opentofu/opentofu"
|
||||
|
||||
[tools.opentofu."platforms.linux-arm64"]
|
||||
checksum = "sha256:9bd0228a81bcd0c88f7045c74378f45a815779f19897191dff7d9efba9976b9e"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762688"
|
||||
|
||||
[tools.opentofu."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:9bd0228a81bcd0c88f7045c74378f45a815779f19897191dff7d9efba9976b9e"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762688"
|
||||
checksum = "sha256:9c1ef375aa1852db0b2888aa921b640c71f8140d4682aa4fec99378a64fa7dc3"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_linux_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708055"
|
||||
|
||||
[tools.opentofu."platforms.linux-x64"]
|
||||
checksum = "sha256:50a6106fa4de523d09c87af85f3db1dd47535fc005727fdca6852146476b88ec"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762723"
|
||||
|
||||
[tools.opentofu."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:50a6106fa4de523d09c87af85f3db1dd47535fc005727fdca6852146476b88ec"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762723"
|
||||
checksum = "sha256:378ada19d4bc70c43732004e8159be771b23b9a5afdf059e5f8a2b3fa2c70a69"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_linux_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708017"
|
||||
|
||||
[tools.opentofu."platforms.macos-arm64"]
|
||||
checksum = "sha256:f958ec5e511063be9feb180ca015a4cb7977566a9cf6a8550bba8c2a9b5aba74"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_darwin_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762710"
|
||||
checksum = "sha256:be78f659f04ef06a9dbd9b3934d46af95d787a3aa38396d459dea395261816a9"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_darwin_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708059"
|
||||
|
||||
[tools.opentofu."platforms.macos-x64"]
|
||||
checksum = "sha256:44bb1855f372f17f365fb94517906e78da5001da10f4c98de57a39bf982f3a92"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_darwin_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762655"
|
||||
checksum = "sha256:a73720443ba38712d7d96dc1e857add02c15a790919c653ad07492e9952f8c27"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_darwin_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708142"
|
||||
|
||||
[tools.opentofu."platforms.windows-x64"]
|
||||
checksum = "sha256:bcbcd9ca744af6c4053e2d5e75dae79ff33ad7dc38d45e1233e693d096284538"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_windows_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762708"
|
||||
checksum = "sha256:6825ae311e9a15747ce2ede7b7e97c8bb744a4d271fa8113259eeeee3d09438c"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_windows_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708102"
|
||||
|
||||
[[tools.terragrunt]]
|
||||
version = "1.0.3"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
[tools]
|
||||
terragrunt = "1.1.1"
|
||||
opentofu = "1.12.6"
|
||||
opentofu = "1.13.1"
|
||||
|
||||
[tasks."tg:fmt"]
|
||||
run = "terragrunt hclfmt"
|
||||
description = "Format terragrunt files"
|
||||
|
||||
[tasks.tf]
|
||||
run = "terragrunt run --all"
|
||||
run = "terragrunt run --all --"
|
||||
description = "Wrapper for terragrunt run-all"
|
||||
dir = "{{cwd}}"
|
||||
|
||||
@@ -16,5 +16,5 @@ run = "tofu fmt -recursive tf/"
|
||||
description = "Format terraform files"
|
||||
|
||||
[tasks."tf:init"]
|
||||
run = { task = "tf init -- -reconfigure" }
|
||||
run = { task = "tf init -reconfigure" }
|
||||
dir = "{{cwd}}"
|
||||
|
||||
@@ -97,7 +97,7 @@ services:
|
||||
command: ['./run.sh', '-disable-reporting']
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:12.4.11-ubuntu@sha256:23f2328b859c7677c7065c81b479600706ef9cc9004977af5bbd68b159b8ea60
|
||||
image: grafana/grafana:13.2.4-ubuntu@sha256:d838696907b4350d9232eb8da1503dc1d1010a0359d3ce4b3f7d2ff7db28eb75
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
@@ -209,6 +209,11 @@ The default configuration looks like this:
|
||||
"timeout": 30000,
|
||||
"tokenEndpointAuthMethod": "client_secret_post"
|
||||
},
|
||||
"passkey": {
|
||||
"enabled": true,
|
||||
"domain": null,
|
||||
"additionalDomains": []
|
||||
},
|
||||
"passwordLogin": {
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||
|
||||
[[tools.wrangler]]
|
||||
version = "4.137.0"
|
||||
version = "4.151.0"
|
||||
backend = "npm:wrangler"
|
||||
|
||||
[tools.wrangler.options]
|
||||
|
||||
+1
-1
@@ -28,4 +28,4 @@ run = "prettier --write ."
|
||||
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
|
||||
|
||||
[tools]
|
||||
wrangler = "4.137.0"
|
||||
wrangler = "4.151.0"
|
||||
|
||||
+2
-2
@@ -41,7 +41,7 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^75.0.0",
|
||||
"exiftool-vendored": "^35.0.0",
|
||||
"exiftool-vendored": "^39.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"luxon": "^3.4.4",
|
||||
"pg": "^8.11.3",
|
||||
@@ -55,6 +55,6 @@
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
||||
"typescript-eslint": "^8.28.0",
|
||||
"utimes": "^5.2.1",
|
||||
"vitest": "^4.0.0"
|
||||
"vitest": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('/admin/database-backups', () => {
|
||||
|
||||
// => action: restore database flow
|
||||
|
||||
describe.sequential('POST /start-restore', () => {
|
||||
describe('POST /start-restore', { concurrent: false }, () => {
|
||||
afterAll(async () => {
|
||||
await request(app).post('/admin/maintenance').set('cookie', cookie!).send({ action: 'end' });
|
||||
await utils.poll(
|
||||
@@ -104,14 +104,14 @@ describe('/admin/database-backups', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.sequential('should not work when the server is configured', async () => {
|
||||
it('should not work when the server is configured', { concurrent: false }, async () => {
|
||||
const { status, body } = await request(app).post('/admin/database-backups/start-restore').send();
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest('Admin setup is not available'));
|
||||
});
|
||||
|
||||
it.sequential('should enter maintenance mode in "database restore mode"', async () => {
|
||||
it('should enter maintenance mode in "database restore mode"', { concurrent: false }, async () => {
|
||||
await utils.resetDatabase(); // reset database before running this test
|
||||
|
||||
const { status, headers } = await request(app).post('/admin/database-backups/start-restore').send();
|
||||
@@ -145,7 +145,7 @@ describe('/admin/database-backups', () => {
|
||||
|
||||
// => action: restore database
|
||||
|
||||
describe.sequential('POST /backups/restore', () => {
|
||||
describe('POST /backups/restore', { concurrent: false }, () => {
|
||||
beforeAll(async () => {
|
||||
await utils.disconnectDatabase();
|
||||
});
|
||||
@@ -154,7 +154,7 @@ describe('/admin/database-backups', () => {
|
||||
await utils.connectDatabase();
|
||||
});
|
||||
|
||||
it.sequential('should restore a backup', { timeout: 60_000 }, async () => {
|
||||
it('should restore a backup', { timeout: 60_000, concurrent: false }, async () => {
|
||||
let filename = await utils.createBackup(admin.accessToken);
|
||||
|
||||
// work-around until test is running on released version
|
||||
@@ -212,7 +212,7 @@ describe('/admin/database-backups', () => {
|
||||
.toBeFalsy();
|
||||
});
|
||||
|
||||
it.sequential('fail to restore a corrupted backup', { timeout: 60_000 }, async () => {
|
||||
it('fail to restore a corrupted backup', { timeout: 60_000, concurrent: false }, async () => {
|
||||
await utils.prepareTestBackup('corrupted');
|
||||
|
||||
const { status, headers } = await request(app)
|
||||
@@ -283,7 +283,7 @@ describe('/admin/database-backups', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.sequential('rollback to restore point if backup is missing admin', { timeout: 60_000 }, async () => {
|
||||
it('rollback to restore point if backup is missing admin', { timeout: 60_000, concurrent: false }, async () => {
|
||||
await utils.prepareTestBackup('empty');
|
||||
|
||||
const { status, headers } = await request(app)
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('/admin/maintenance', () => {
|
||||
|
||||
// => enter maintenance mode
|
||||
|
||||
describe.sequential('POST /', () => {
|
||||
describe('POST /', { concurrent: false }, () => {
|
||||
it('should be a no-op if try to exit maintenance mode', async () => {
|
||||
const { status } = await request(app)
|
||||
.post('/admin/maintenance')
|
||||
@@ -87,7 +87,7 @@ describe('/admin/maintenance', () => {
|
||||
|
||||
// => in maintenance mode
|
||||
|
||||
describe.sequential('in maintenance mode', () => {
|
||||
describe('in maintenance mode', { concurrent: false }, () => {
|
||||
describe('GET ~/server/config', async () => {
|
||||
it('should indicate we are in maintenance mode', async () => {
|
||||
const { status, body } = await request(app).get('/server/config');
|
||||
@@ -152,7 +152,7 @@ describe('/admin/maintenance', () => {
|
||||
|
||||
// => exit maintenance mode
|
||||
|
||||
describe.sequential('POST /', () => {
|
||||
describe('POST /', { concurrent: false }, () => {
|
||||
it('should exit maintenance mode', async () => {
|
||||
const { status } = await request(app).post('/admin/maintenance').set('cookie', cookie!).send({
|
||||
action: 'end',
|
||||
|
||||
@@ -96,7 +96,7 @@ describe('/admin/integrity', () => {
|
||||
});
|
||||
|
||||
describe('POST /summary (& jobs)', async () => {
|
||||
it.sequential('reports no issues', async () => {
|
||||
it('reports no issues', { concurrent: false }, async () => {
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityUntrackedFiles,
|
||||
});
|
||||
@@ -130,7 +130,7 @@ describe('/admin/integrity', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.sequential('should detect an untracked file (job: check untracked files)', async () => {
|
||||
it('should detect an untracked file (job: check untracked files)', { concurrent: false }, async () => {
|
||||
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked1.png`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
@@ -152,31 +152,35 @@ describe('/admin/integrity', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.sequential('should detect outdated untracked file reports (job: refresh untracked files)', async () => {
|
||||
// these should not be detected:
|
||||
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked2.png`);
|
||||
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked3.png`);
|
||||
it(
|
||||
'should detect outdated untracked file reports (job: refresh untracked files)',
|
||||
{ concurrent: false },
|
||||
async () => {
|
||||
// these should not be detected:
|
||||
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked2.png`);
|
||||
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked3.png`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityUntrackedFilesRefresh,
|
||||
});
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityUntrackedFilesRefresh,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/admin/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
const { status, body } = await request(app)
|
||||
.get('/admin/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
untracked_file: 0,
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
untracked_file: 0,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.sequential('should delete untracked files (job: delete all untracked file reports)', async () => {
|
||||
it('should delete untracked files (job: delete all untracked file reports)', { concurrent: false }, async () => {
|
||||
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked1.png`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
@@ -204,30 +208,34 @@ describe('/admin/integrity', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.sequential('should detect a missing file and not a checksum mismatch (job: check missing files)', async () => {
|
||||
await utils.deleteFolder(`/data/upload/${admin.userId}`);
|
||||
it(
|
||||
'should detect a missing file and not a checksum mismatch (job: check missing files)',
|
||||
{ concurrent: false },
|
||||
async () => {
|
||||
await utils.deleteFolder(`/data/upload/${admin.userId}`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityMissingFiles,
|
||||
});
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityMissingFiles,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/admin/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
const { status, body } = await request(app)
|
||||
.get('/admin/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
missing_file: 1,
|
||||
checksum_mismatch: 0,
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
missing_file: 1,
|
||||
checksum_mismatch: 0,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.sequential('should detect outdated missing file reports (job: refresh missing files)', async () => {
|
||||
it('should detect outdated missing file reports (job: refresh missing files)', { concurrent: false }, async () => {
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityMissingFilesRefresh,
|
||||
});
|
||||
@@ -248,53 +256,57 @@ describe('/admin/integrity', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.sequential('should delete assets with missing files (job: delete all missing file reports)', async () => {
|
||||
await utils.deleteFolder(`/data/upload/${user1.userId}`);
|
||||
it(
|
||||
'should delete assets with missing files (job: delete all missing file reports)',
|
||||
{ concurrent: false },
|
||||
async () => {
|
||||
await utils.deleteFolder(`/data/upload/${user1.userId}`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityMissingFiles,
|
||||
});
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityMissingFiles,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
|
||||
|
||||
const { status: listStatus, body: listBody } = await request(app)
|
||||
.get('/admin/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
const { status: listStatus, body: listBody } = await request(app)
|
||||
.get('/admin/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
|
||||
expect(listStatus).toBe(200);
|
||||
expect(listBody).toEqual(
|
||||
expect.objectContaining({
|
||||
missing_file: 1,
|
||||
}),
|
||||
);
|
||||
expect(listStatus).toBe(200);
|
||||
expect(listBody).toEqual(
|
||||
expect.objectContaining({
|
||||
missing_file: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityMissingFilesDeleteAll,
|
||||
});
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityMissingFilesDeleteAll,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/admin/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
const { status, body } = await request(app)
|
||||
.get('/admin/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
missing_file: 0,
|
||||
}),
|
||||
);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
missing_file: 0,
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(utils.getAssetInfo(user1.accessToken, asset1.id)).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
isTrashed: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
await expect(utils.getAssetInfo(user1.accessToken, asset1.id)).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
isTrashed: true,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.sequential('should detect a checksum mismatch (job: check file checksums)', async () => {
|
||||
it('should detect a checksum mismatch (job: check file checksums)', { concurrent: false }, async () => {
|
||||
await utils.truncateFolder(`/data/upload/${admin.userId}`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
@@ -316,28 +328,33 @@ describe('/admin/integrity', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.sequential('should detect outdated checksum mismatch reports (job: refresh file checksums)', async () => {
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityChecksumMismatchRefresh,
|
||||
});
|
||||
it(
|
||||
'should detect outdated checksum mismatch reports (job: refresh file checksums)',
|
||||
{ concurrent: false },
|
||||
async () => {
|
||||
await utils.createJob(admin.accessToken, {
|
||||
name: ManualJobName.IntegrityChecksumMismatchRefresh,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
|
||||
await utils.waitForQueueFinish(admin.accessToken, QueueName.IntegrityCheck);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/admin/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
const { status, body } = await request(app)
|
||||
.get('/admin/integrity/summary')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
checksum_mismatch: 0,
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
checksum_mismatch: 0,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.sequential(
|
||||
it(
|
||||
'should delete assets with mismatched checksum (job: delete all checksum mismatch reports)',
|
||||
{ concurrent: false },
|
||||
async () => {
|
||||
await utils.truncateFolder(`/data/upload/${user2.userId}`);
|
||||
|
||||
@@ -387,7 +404,7 @@ describe('/admin/integrity', () => {
|
||||
});
|
||||
|
||||
describe('POST /report', async () => {
|
||||
it.sequential('reports untracked files', async () => {
|
||||
it('reports untracked files', { concurrent: false }, async () => {
|
||||
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked1.png`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
@@ -417,7 +434,7 @@ describe('/admin/integrity', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.sequential('reports missing files', async () => {
|
||||
it('reports missing files', { concurrent: false }, async () => {
|
||||
await utils.deleteFolder(`/data/upload/${admin.userId}`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
@@ -447,7 +464,7 @@ describe('/admin/integrity', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.sequential('reports checksum mismatched files', async () => {
|
||||
it('reports checksum mismatched files', { concurrent: false }, async () => {
|
||||
await utils.truncateFolder(`/data/upload/${admin.userId}`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
@@ -479,7 +496,7 @@ describe('/admin/integrity', () => {
|
||||
});
|
||||
|
||||
describe('DELETE /report/:id', async () => {
|
||||
it.sequential('delete untracked files', async () => {
|
||||
it('delete untracked files', { concurrent: false }, async () => {
|
||||
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked1.png`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
@@ -529,7 +546,7 @@ describe('/admin/integrity', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.sequential('delete assets missing files', async () => {
|
||||
it('delete assets missing files', { concurrent: false }, async () => {
|
||||
await utils.deleteFolder(`/data/upload/${admin.userId}`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
@@ -570,7 +587,7 @@ describe('/admin/integrity', () => {
|
||||
expect(listBody2.items.length).toBe(0);
|
||||
});
|
||||
|
||||
it.sequential('delete assets with failing checksum', async () => {
|
||||
it('delete assets with failing checksum', { concurrent: false }, async () => {
|
||||
await utils.truncateFolder(`/data/upload/${admin.userId}`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
@@ -613,7 +630,7 @@ describe('/admin/integrity', () => {
|
||||
});
|
||||
|
||||
describe('GET /report/:type/csv', () => {
|
||||
it.sequential('exports untracked files as csv', async () => {
|
||||
it('exports untracked files as csv', { concurrent: false }, async () => {
|
||||
await utils.putTextFile('untracked', `/data/upload/${admin.userId}/untracked1.png`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
@@ -637,7 +654,7 @@ describe('/admin/integrity', () => {
|
||||
});
|
||||
|
||||
describe('GET /report/:id/file', () => {
|
||||
it.sequential('downloads untracked file', async () => {
|
||||
it('downloads untracked file', { concurrent: false }, async () => {
|
||||
await utils.putTextFile('untracked-content', `/data/upload/${admin.userId}/untracked1.png`);
|
||||
|
||||
await utils.createJob(admin.accessToken, {
|
||||
|
||||
@@ -384,6 +384,27 @@ describe('/search', () => {
|
||||
expect(status).toBe(200);
|
||||
expect(body.assets.items).toEqual([expect.objectContaining({ id: asset.id })]);
|
||||
});
|
||||
|
||||
it('should strip metadata for a shared link without metadata', async () => {
|
||||
const asset = await utils.createAsset(admin.accessToken);
|
||||
const album = await utils.createAlbum(admin.accessToken, { albumName: 'foo', assetIds: [asset.id] });
|
||||
const sharedLink = await utils.createSharedLink(admin.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: album.id,
|
||||
showMetadata: false,
|
||||
});
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post(`/search/metadata?key=${sharedLink.key}`)
|
||||
.send({ albumIds: [album.id], withExif: true });
|
||||
expect(status).toBe(200);
|
||||
expect(body.assets.items).toHaveLength(1);
|
||||
expect(body.assets.items[0]).toEqual(expect.objectContaining({ id: asset.id, hasMetadata: false }));
|
||||
expect(body.assets.items[0]).not.toHaveProperty('exifInfo');
|
||||
expect(body.assets.items[0]).not.toHaveProperty('originalPath');
|
||||
expect(body.assets.items[0]).not.toHaveProperty('originalFileName');
|
||||
expect(body.assets.items[0]).not.toHaveProperty('ownerId');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /search/random', () => {
|
||||
@@ -546,7 +567,7 @@ describe('/search', () => {
|
||||
'New York',
|
||||
'Shanghai',
|
||||
'State of Berlin',
|
||||
'St.-Petersburg',
|
||||
'St. Petersburg',
|
||||
'Tbilisi',
|
||||
'Tokyo',
|
||||
'Virginia',
|
||||
@@ -570,7 +591,7 @@ describe('/search', () => {
|
||||
'New York',
|
||||
'Shanghai',
|
||||
'State of Berlin',
|
||||
'St.-Petersburg',
|
||||
'St. Petersburg',
|
||||
'Tbilisi',
|
||||
'Tokyo',
|
||||
'Virginia',
|
||||
|
||||
@@ -206,6 +206,13 @@ describe('/shared-links', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not return album users', async () => {
|
||||
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.album.albumUsers).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return unauthorized for incorrect shared link', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/shared-links/me')
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { LoginResponseDto, getMyUser, login } from '@immich/sdk';
|
||||
import { createUserDto } from 'src/fixtures.js';
|
||||
import { app, asBearerAuth, utils } from 'src/utils.js';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('/users', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let nonAdmin: LoginResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup({ onboarding: false });
|
||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user2);
|
||||
});
|
||||
|
||||
describe('PUT /users/me', () => {
|
||||
/** @deprecated */
|
||||
it('should allow a user to change their password (deprecated)', async () => {
|
||||
const user = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) });
|
||||
|
||||
expect(user.shouldChangePassword).toBe(true);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.put(`/users/me`)
|
||||
.send({ password: 'super-secret' })
|
||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({
|
||||
email: nonAdmin.userEmail,
|
||||
shouldChangePassword: false,
|
||||
});
|
||||
|
||||
const token = await login({ loginCredentialDto: { email: nonAdmin.userEmail, password: 'super-secret' } });
|
||||
|
||||
expect(token.accessToken).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -27,8 +27,8 @@ test.describe('Registration', () => {
|
||||
await expect(page).toHaveTitle(/Login/);
|
||||
await page.goto('/auth/login?autoLaunch=0');
|
||||
await page.getByLabel('Email').fill('admin@immich.app');
|
||||
await page.getByLabel('Password').fill('password');
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
await page.getByLabel('Password', { exact: true }).fill('password');
|
||||
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
||||
|
||||
// onboarding
|
||||
await expect(page).toHaveURL('/auth/onboarding');
|
||||
@@ -66,11 +66,12 @@ test.describe('Registration', () => {
|
||||
await page.goto('/auth/login?autoLaunch=0');
|
||||
await page.getByLabel('Email').fill('user@immich.cloud');
|
||||
await page.getByLabel('Password').fill('password');
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
||||
|
||||
// change password
|
||||
await expect(page.getByRole('heading')).toHaveText('Change Password');
|
||||
await expect(page).toHaveURL('/auth/change-password');
|
||||
await page.getByLabel('Password', { exact: true }).fill('password');
|
||||
await page.getByLabel('New Password').fill('new-password');
|
||||
await page.getByLabel('Confirm Password').fill('new-password');
|
||||
await page.getByRole('button', { name: 'Change password' }).click();
|
||||
@@ -79,7 +80,7 @@ test.describe('Registration', () => {
|
||||
await expect(page).toHaveURL('/auth/login?autoLaunch=0');
|
||||
await page.getByLabel('Email').fill('user@immich.cloud');
|
||||
await page.getByLabel('Password').fill('new-password');
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
||||
|
||||
// onboarding
|
||||
await expect(page).toHaveURL('/auth/onboarding');
|
||||
|
||||
@@ -80,7 +80,7 @@ test.describe('Shared Links', () => {
|
||||
const input = page.getByPlaceholder('Password');
|
||||
await input.fill('test-password');
|
||||
await page.getByRole('button', { name: 'Show password' }).click();
|
||||
// await page.getByText('test-password', { exact: true }).waitFor();
|
||||
// await page.getByText('test-password').waitFor();
|
||||
await expect(input).toHaveAttribute('type', 'text');
|
||||
});
|
||||
|
||||
|
||||
@@ -494,7 +494,7 @@ test.describe('Timeline', () => {
|
||||
force: false,
|
||||
ids: [assetToTrash.id],
|
||||
});
|
||||
await page.getByText('Trash', { exact: true }).click();
|
||||
await page.getByRole('link', { name: 'Trash' }).click();
|
||||
await thumbnailUtils.expectInViewport(page, assetToTrash.id);
|
||||
await thumbnailUtils.withAssetId(page, assetToTrash.id).hover();
|
||||
await thumbnailUtils.selectButton(page, assetToTrash.id).click();
|
||||
@@ -507,12 +507,12 @@ test.describe('Timeline', () => {
|
||||
json: { count: requestJson.ids.length },
|
||||
});
|
||||
});
|
||||
await page.getByText('Restore', { exact: true }).click();
|
||||
await page.getByText('Restore').click();
|
||||
await expect(restoreRequest).resolves.toEqual({
|
||||
ids: [assetToTrash.id],
|
||||
});
|
||||
await expect(thumbnailUtils.withAssetId(page, assetToTrash.id)).toHaveCount(0);
|
||||
await page.getByText('Photos', { exact: true }).click();
|
||||
await page.getByRole('link', { name: 'Photos' }).click();
|
||||
await thumbnailUtils.expectInViewport(page, assetToTrash.id);
|
||||
});
|
||||
test('open album, trash photo, open /trash, restore', async ({ page }) => {
|
||||
@@ -537,7 +537,7 @@ test.describe('Timeline', () => {
|
||||
ids: [assetToTrash.id],
|
||||
});
|
||||
await page.locator('#control-bar').getByLabel('Close').click();
|
||||
await page.getByText('Trash', { exact: true }).click();
|
||||
await page.getByRole('link', { name: 'Trash' }).click();
|
||||
await timelineUtils.waitForTimelineLoad(page);
|
||||
await thumbnailUtils.expectInViewport(page, assetToTrash.id);
|
||||
await thumbnailUtils.withAssetId(page, assetToTrash.id).hover();
|
||||
@@ -551,7 +551,7 @@ test.describe('Timeline', () => {
|
||||
json: { count: requestJson.ids.length },
|
||||
});
|
||||
});
|
||||
await page.getByText('Restore', { exact: true }).click();
|
||||
await page.getByText('Restore').click();
|
||||
await expect(restoreRequest).resolves.toEqual({
|
||||
ids: [assetToTrash.id],
|
||||
});
|
||||
@@ -603,7 +603,7 @@ test.describe('Timeline', () => {
|
||||
ids: [assetToArchive.id],
|
||||
});
|
||||
await expect(thumbnailUtils.withAssetId(page, assetToArchive.id)).toHaveCount(0);
|
||||
await page.getByText('Photos', { exact: true }).click();
|
||||
await page.getByRole('link', { name: 'Photos' }).click();
|
||||
await thumbnailUtils.expectInViewport(page, assetToArchive.id);
|
||||
});
|
||||
test('open /archive, favorite photo, unfavorite', async ({ page }) => {
|
||||
@@ -749,7 +749,7 @@ test.describe('Timeline', () => {
|
||||
ids: [assetToFavorite.id],
|
||||
});
|
||||
await expect(thumbnailUtils.withAssetId(page, assetToFavorite.id)).toHaveCount(0);
|
||||
await page.getByText('Photos', { exact: true }).click();
|
||||
await page.getByRole('link', { name: 'Photos' }).click();
|
||||
await thumbnailUtils.expectInViewport(page, assetToFavorite.id);
|
||||
});
|
||||
test.skip('open /favorites, archive photo, unarchive photo', async ({ page }) => {
|
||||
|
||||
+2
-2
@@ -657,13 +657,13 @@ export const utils = {
|
||||
isQueueEmpty: async (accessToken: string, queue: keyof QueuesResponseLegacyDto) => {
|
||||
const queues = await getQueuesLegacy({ headers: asBearerAuth(accessToken) });
|
||||
const jobCounts = queues[queue].jobCounts;
|
||||
return !jobCounts.active && !jobCounts.waiting;
|
||||
return !jobCounts.active && (!jobCounts.waiting || queues[queue].queueStatus.isPaused);
|
||||
},
|
||||
|
||||
waitForQueueFinish: (accessToken: string, queue: keyof QueuesResponseLegacyDto, ms?: number) => {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 10_000);
|
||||
const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 15_000);
|
||||
|
||||
while (true) {
|
||||
const done = await utils.isQueueEmpty(accessToken, queue);
|
||||
|
||||
@@ -324,6 +324,13 @@
|
||||
"oauth_timeout": "Request Timeout",
|
||||
"oauth_timeout_description": "Timeout for requests in milliseconds",
|
||||
"ocr_job_description": "Use machine learning to recognize text in images",
|
||||
"passkey_additional_domains": "Additional domains",
|
||||
"passkey_additional_domains_description": "Additional, trusted domains (related origin requests). These will only work if the passkey domain is also accessible.",
|
||||
"passkey_domain": "Passkey domain",
|
||||
"passkey_domain_description": "Domain used for passkeys (relying party). Leave blank to infer from the origin header. Changing this value will result in existing passkeys no longer working.",
|
||||
"passkey_enable_description": "Login with a passkey",
|
||||
"passkey_settings": "Passkey Login",
|
||||
"passkey_settings_description": "Manage passkey login settings",
|
||||
"password_enable_description": "Login with email and password",
|
||||
"password_settings": "Password Login",
|
||||
"password_settings_description": "Manage password login settings",
|
||||
@@ -874,6 +881,8 @@
|
||||
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
|
||||
"delete_local_dialog_ok_force": "Delete Anyway",
|
||||
"delete_others": "Delete others",
|
||||
"delete_passkey": "Delete passkey",
|
||||
"delete_passkey_prompt": "Are you sure you want to delete this passkey?",
|
||||
"delete_permanently": "Delete permanently",
|
||||
"delete_permanently_action_prompt": "{count} deleted permanently",
|
||||
"delete_shared_link": "Delete shared link",
|
||||
@@ -943,6 +952,7 @@
|
||||
"edit_location_action_prompt": "{count} location edited",
|
||||
"edit_location_dialog_title": "Location",
|
||||
"edit_name": "Edit name",
|
||||
"edit_passkey": "Edit passkey",
|
||||
"edit_people": "Edit people",
|
||||
"edit_person": "Edit person",
|
||||
"edit_tag": "Edit tag",
|
||||
@@ -1287,6 +1297,7 @@
|
||||
"last": "Last",
|
||||
"last_months": "{count, plural, one {Last month} other {Last # months}}",
|
||||
"last_seen": "Last seen",
|
||||
"last_used": "Last used",
|
||||
"latest_version": "Latest Version",
|
||||
"latitude": "Latitude",
|
||||
"leave": "Leave",
|
||||
@@ -1379,6 +1390,7 @@
|
||||
"login_has_been_disabled": "Login has been disabled",
|
||||
"login_password_changed_error": "There was an error updating your password",
|
||||
"login_password_changed_success": "Password updated successfully",
|
||||
"login_with_passkey": "Login with passkey",
|
||||
"logout_all_device_confirmation": "Are you sure you want to log out all devices?",
|
||||
"logout_this_device_confirmation": "Are you sure you want to log out this device?",
|
||||
"logs": "Logs",
|
||||
@@ -1427,6 +1439,7 @@
|
||||
"manage_your_api_keys": "Manage your API keys",
|
||||
"manage_your_devices": "Manage your logged-in devices",
|
||||
"manage_your_oauth_connection": "Manage your OAuth connection",
|
||||
"manage_your_passkeys": "Manage your passkeys",
|
||||
"map": "Map",
|
||||
"map_assets_in_bounds": "{count, plural, =0 {No photos in this area} one {# photo} other {# photos}}",
|
||||
"map_cannot_get_user_location": "Cannot get user's location",
|
||||
@@ -1546,6 +1559,7 @@
|
||||
"new_album": "New Album",
|
||||
"new_api_key": "New API Key",
|
||||
"new_feature": "New Feature",
|
||||
"new_passkey": "New passkey",
|
||||
"new_password": "New password",
|
||||
"new_person": "New person",
|
||||
"new_pin_code": "New PIN code",
|
||||
@@ -1658,6 +1672,8 @@
|
||||
"partner_sharing": "Partner Sharing",
|
||||
"partners": "Partners",
|
||||
"partners_description": "Share your photos and videos with other users",
|
||||
"passkeys": "{count, plural, one {Passkey} other {Passkeys}}",
|
||||
"passkeys_not_supported": "Passkeys are not supported by this browser",
|
||||
"password": "Password",
|
||||
"password_does_not_match": "Password does not match",
|
||||
"password_required": "Password Required",
|
||||
@@ -1842,6 +1858,7 @@
|
||||
"removed_from_favorites_count": "{count, plural, other {Removed #}} from favorites",
|
||||
"removed_from_stack": "Removed asset from stack",
|
||||
"removed_memory": "Removed memory",
|
||||
"removed_passkey": "Removed passkey: {name}",
|
||||
"removed_tagged_assets": "Removed tag from {count, plural, one {# asset} other {# assets}}",
|
||||
"rename": "Rename",
|
||||
"repository": "Repository",
|
||||
@@ -2025,6 +2042,7 @@
|
||||
"share_everyone": "Share everyone",
|
||||
"share_everyone_description": "Give {user} access to all of your people.",
|
||||
"share_link": "Share Link",
|
||||
"share_logs": "Share Logs",
|
||||
"share_original": "Use original (large)",
|
||||
"share_preview": "Use preview (small)",
|
||||
"share_quality_body": "Press and hold the share button to choose the image quality before you share.",
|
||||
@@ -2203,6 +2221,8 @@
|
||||
"theme": "Theme",
|
||||
"theme_selection": "Theme selection",
|
||||
"theme_selection_description": "Automatically set the theme to light or dark based on your browser's system preference",
|
||||
"theme_setting_asset_list_smooth_scrubbing_subtitle": "Scroll continuously when dragging the scrollbar instead of snapping to months",
|
||||
"theme_setting_asset_list_smooth_scrubbing_title": "Smooth timeline scrubbing",
|
||||
"theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles",
|
||||
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({count})",
|
||||
"theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.",
|
||||
|
||||
@@ -323,6 +323,13 @@
|
||||
"oauth_timeout": "Таймаут для запросов",
|
||||
"oauth_timeout_description": "Максимальное время, в течение которого ожидать ответа, в миллисекундах",
|
||||
"ocr_job_description": "Использование машинного обучения для распознавания текста на изображениях",
|
||||
"passkey_additional_domains": "Дополнительные домены",
|
||||
"passkey_additional_domains_description": "Дополнительные доверенные домены. Для их работы основной домен ключей доступа также должен быть доступен.",
|
||||
"passkey_domain": "Домен ключей доступа",
|
||||
"passkey_domain_description": "Домен для ключей доступа. Оставьте пустым для определения по адресу сайта. При изменении домена существующие ключи перестанут работать.",
|
||||
"passkey_enable_description": "Вход с помощью ключа доступа",
|
||||
"passkey_settings": "Вход по ключу доступа",
|
||||
"passkey_settings_description": "Настройки входа по ключу доступа",
|
||||
"password_enable_description": "Вход по электронной почте и паролю",
|
||||
"password_settings": "Настройки входа с паролем",
|
||||
"password_settings_description": "Управление настройками входа по паролю",
|
||||
@@ -873,6 +880,8 @@
|
||||
"delete_local_dialog_ok_backed_up_only": "Удалить только резервные копии",
|
||||
"delete_local_dialog_ok_force": "Все равно удалить",
|
||||
"delete_others": "Удалить остальные",
|
||||
"delete_passkey": "Удалить ключ доступа",
|
||||
"delete_passkey_prompt": "Удалить этот ключ доступа?",
|
||||
"delete_permanently": "Удалить навсегда",
|
||||
"delete_permanently_action_prompt": "Объекты удалены безвозвратно ({count} шт.)",
|
||||
"delete_shared_link": "Удалить публичную ссылку",
|
||||
@@ -942,6 +951,7 @@
|
||||
"edit_location_action_prompt": "Места изменены ({count} шт.)",
|
||||
"edit_location_dialog_title": "Местоположение",
|
||||
"edit_name": "Изменить имя",
|
||||
"edit_passkey": "Изменить ключ доступа",
|
||||
"edit_people": "Изменить людей",
|
||||
"edit_person": "Изменить человека",
|
||||
"edit_tag": "Изменить тег",
|
||||
@@ -1287,6 +1297,7 @@
|
||||
"last": "Последний",
|
||||
"last_months": "{count, plural, one {Последний месяц} many {# последних месяцев} other {# последних месяца}}",
|
||||
"last_seen": "Последний доступ",
|
||||
"last_used": "Последнее использование",
|
||||
"latest_version": "Последняя версия",
|
||||
"latitude": "Широта",
|
||||
"leave": "Покинуть",
|
||||
@@ -1379,6 +1390,7 @@
|
||||
"login_has_been_disabled": "Вход в систему был отключён",
|
||||
"login_password_changed_error": "Произошла ошибка при обновлении пароля",
|
||||
"login_password_changed_success": "Пароль успешно обновлен",
|
||||
"login_with_passkey": "Войти с ключом доступа",
|
||||
"logout_all_device_confirmation": "Вы действительно хотите завершить все сеансы, кроме текущего?",
|
||||
"logout_this_device_confirmation": "Вы действительно хотите завершить сеанс на этом устройстве?",
|
||||
"logs": "Журнал событий",
|
||||
@@ -1427,6 +1439,7 @@
|
||||
"manage_your_api_keys": "Управление API ключами для взаимодействия с другими программами",
|
||||
"manage_your_devices": "Управление устройствами, на которых вы входили в свой аккаунт",
|
||||
"manage_your_oauth_connection": "Настройки подключённого OAuth",
|
||||
"manage_your_passkeys": "Управление ключами доступа",
|
||||
"map": "Карта",
|
||||
"map_assets_in_bounds": "{count, plural, =0 {Нет фото в этой области} other {# фото}}",
|
||||
"map_cannot_get_user_location": "Невозможно получить местоположение пользователя",
|
||||
@@ -1546,6 +1559,7 @@
|
||||
"new_album": "Новый альбом",
|
||||
"new_api_key": "Новый API ключ",
|
||||
"new_feature": "Новая функциональность",
|
||||
"new_passkey": "Новый ключ доступа",
|
||||
"new_password": "Новый пароль",
|
||||
"new_person": "Новый человек",
|
||||
"new_pin_code": "Новый PIN-код",
|
||||
@@ -1658,6 +1672,8 @@
|
||||
"partner_sharing": "Совместное использование",
|
||||
"partners": "Партнёры",
|
||||
"partners_description": "Делитесь своими фото и видео с другими пользователями",
|
||||
"passkeys": "{count, plural, one {Ключ доступа} few {Ключи доступа} many {Ключи доступа} other {Ключи доступа}}",
|
||||
"passkeys_not_supported": "Этот браузер не поддерживает ключи доступа",
|
||||
"password": "Пароль",
|
||||
"password_does_not_match": "Пароли не совпадают",
|
||||
"password_required": "Требуется пароль",
|
||||
@@ -1842,6 +1858,7 @@
|
||||
"removed_from_favorites_count": "{count, plural, one {# объект удалён} many {# объектов удалено} other {# объекта удалено}} из избранного",
|
||||
"removed_from_stack": "Объект удалён из группы",
|
||||
"removed_memory": "Воспоминание удалено",
|
||||
"removed_passkey": "Удалён ключ доступа: {name}",
|
||||
"removed_tagged_assets": "Тег удалён у {count, plural, one {# объекта} other {# объектов}}",
|
||||
"rename": "Переименовать",
|
||||
"repository": "Репозиторий",
|
||||
@@ -2027,6 +2044,7 @@
|
||||
"share_everyone": "Поделиться со всеми",
|
||||
"share_everyone_description": "Предоставить пользователю {user} доступ всем своим людям.",
|
||||
"share_link": "Создать ссылку",
|
||||
"share_logs": "Поделиться журналами",
|
||||
"share_original": "Оригинал (большой размер)",
|
||||
"share_preview": "Предпросмотр (небольшой размер)",
|
||||
"share_quality_body": "Нажмите и удерживайте кнопку «Поделиться», чтобы выбрать качество изображения перед отправкой.",
|
||||
@@ -2206,6 +2224,8 @@
|
||||
"theme": "Тема",
|
||||
"theme_selection": "Выбор темы",
|
||||
"theme_selection_description": "Автоматически устанавливать светлую или тёмную тему в зависимости от настроек вашего браузера",
|
||||
"theme_setting_asset_list_smooth_scrubbing_subtitle": "Прокручивать ленту плавно при перетаскивании полосы прокрутки, без переходов по месяцам",
|
||||
"theme_setting_asset_list_smooth_scrubbing_title": "Плавная прокрутка ленты",
|
||||
"theme_setting_asset_list_storage_indicator_title": "Отображать индикатор хранилища на плитках объектов",
|
||||
"theme_setting_asset_list_tiles_per_row_title": "Количество объектов в строке ({count})",
|
||||
"theme_setting_colorful_interface_subtitle": "Добавить оттенок к фону.",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.13-slim-trixie@sha256:9d2e5553305c7c7b0097999bb17187c69b921ccd6bc9d40e4bb5ebe652c00285 AS builder-cpu
|
||||
FROM python:3.13-slim-trixie@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f AS builder-cpu
|
||||
|
||||
FROM python:3.13-slim-trixie@sha256:7c61056e61ac89e852de05f3dc6fa51a6dd2181797bceed46aa725dd7cb2cd3b AS builder-openvino
|
||||
FROM python:3.13-slim-trixie@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f AS builder-openvino
|
||||
|
||||
# renovate: datasource=github-releases depName=microsoft/onnxruntime
|
||||
ARG ONNXRUNTIME_VERSION="v1.30.0"
|
||||
@@ -59,7 +59,7 @@ RUN mkdir /opt/armnn && \
|
||||
cd /opt/ann && \
|
||||
sh build.sh
|
||||
|
||||
FROM python:3.12-slim-trixie@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS builder-rknn
|
||||
FROM python:3.12-slim-trixie@sha256:cb33f7f771ecec2ef1225cf39b159591a040391bd73e4c01f00fdfc4c47611d2 AS builder-rknn
|
||||
|
||||
# Warning: 25GiB+ disk space required to pull this image
|
||||
# TODO: find a way to reduce the image size
|
||||
@@ -81,11 +81,11 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy && \
|
||||
if [ "$DEVICE" = "openvino" ]; then uv pip install --no-deps /opt/onnxruntime_openvino-*.whl; fi
|
||||
|
||||
FROM python:3.13-slim-trixie@sha256:9d2e5553305c7c7b0097999bb17187c69b921ccd6bc9d40e4bb5ebe652c00285 AS prod-cpu
|
||||
FROM python:3.13-slim-trixie@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f AS prod-cpu
|
||||
|
||||
ENV MACHINE_LEARNING_MODEL_ARENA=false
|
||||
|
||||
FROM python:3.13-slim-trixie@sha256:9d2e5553305c7c7b0097999bb17187c69b921ccd6bc9d40e4bb5ebe652c00285 AS prod-openvino
|
||||
FROM python:3.13-slim-trixie@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f AS prod-openvino
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||
@@ -141,7 +141,7 @@ COPY --from=builder-armnn \
|
||||
/opt/ann/build.sh \
|
||||
/opt/armnn/
|
||||
|
||||
FROM python:3.12-slim-trixie@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS prod-rknn
|
||||
FROM python:3.12-slim-trixie@sha256:cb33f7f771ecec2ef1225cf39b159591a040391bd73e4c01f00fdfc4c47611d2 AS prod-rknn
|
||||
|
||||
# renovate: datasource=github-tags depName=airockchip/rknn-toolkit2
|
||||
ARG RKNN_TOOLKIT_VERSION="v2.3.0"
|
||||
@@ -150,7 +150,7 @@ ENV MACHINE_LEARNING_MODEL_ARENA=false
|
||||
|
||||
ADD --chmod=644 --checksum=sha256:73993ed4b440460825f21611731564503cc1d5a0c123746477da6cd574f34885 "https://github.com/airockchip/rknn-toolkit2/raw/refs/tags/${RKNN_TOOLKIT_VERSION}/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so" /usr/lib/
|
||||
|
||||
FROM python:3.13-slim-trixie@sha256:9d2e5553305c7c7b0097999bb17187c69b921ccd6bc9d40e4bb5ebe652c00285 AS mimalloc
|
||||
FROM python:3.13-slim-trixie@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f AS mimalloc
|
||||
RUN echo "deb http://deb.debian.org/debian trixie-backports main" > /etc/apt/sources.list.d/backports.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends libmimalloc3/trixie-backports && \
|
||||
|
||||
@@ -3,6 +3,7 @@ from collections.abc import Hashable
|
||||
from typing import Any
|
||||
|
||||
from immich_ml import allocator
|
||||
from immich_ml.config import clean_name
|
||||
from immich_ml.models.base import InferenceEntry, InferenceModel
|
||||
from immich_ml.schemas import Options
|
||||
|
||||
@@ -15,7 +16,8 @@ class ModelCache:
|
||||
self._expiries: dict[Key, asyncio.TimerHandle] = {}
|
||||
|
||||
def get[O: Options](self, entry: InferenceEntry[O], ttl: int | None = None) -> InferenceModel[O]:
|
||||
key = (entry.model, entry.name, entry.model.graph(entry.options))
|
||||
# names that differ only in what cleaning drops, like an `immich-app/` prefix, are the same model
|
||||
key = (entry.model, clean_name(entry.name), entry.model.graph(entry.options))
|
||||
if key not in self._models:
|
||||
self._models[key] = entry.model.create(entry.name, entry.options)
|
||||
elif key not in self._expiries:
|
||||
|
||||
@@ -1856,6 +1856,14 @@ class TestCache:
|
||||
assert len(model_cache._models) == 1
|
||||
model.create.assert_called_once()
|
||||
|
||||
async def test_shares_an_instance_across_names_of_the_same_model(self) -> None:
|
||||
model_cache = ModelCache()
|
||||
bare = model_cache.get(InferenceEntry(OpenClipTextualEncoder, "ViT-B-32__openai", TextualOptions()))
|
||||
prefixed = model_cache.get(
|
||||
InferenceEntry(OpenClipTextualEncoder, "immich-app/ViT-B-32__openai", TextualOptions(language="en-US"))
|
||||
)
|
||||
assert bare is prefixed
|
||||
|
||||
async def test_separate_instances_per_graph_option(self) -> None:
|
||||
# the real TextDetector, so that keying it on anything but its resolution fails this
|
||||
model_cache = ModelCache()
|
||||
|
||||
Generated
+173
-55
@@ -175,11 +175,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.11.12"
|
||||
version = "2026.10.19"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -364,18 +364,19 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.136.3"
|
||||
version = "0.148.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/da/e962004c32f34fb630e8393456cadcd08d4300b6fa0099ec0ae5052b8418/fastapi-0.148.0.tar.gz", hash = "sha256:06ee16c50e429daf993a58ecbb5306d284c4c1ee8135380af39a5e1cfa715187", size = 482594, upload-time = "2026-10-19T12:26:05.615Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/8e/7d284c1f06093a5e96e1704369a4053b93a9cf4d41b5b40f5dbf94bde819/fastapi-0.148.0-py3-none-any.whl", hash = "sha256:2848088aa482c875c180ef92b7139fec345393d09cdcf258ba4b8555ab068a3b", size = 145110, upload-time = "2026-10-19T12:26:03.9Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -538,14 +539,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "25.3.0"
|
||||
version = "26.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/f4/e78fa054248fab913e2eab0332c6c2cb07421fca1ce56d8fe43b6aef57a4/gunicorn-25.3.0.tar.gz", hash = "sha256:f74e1b2f9f76f6cd1ca01198968bd2dd65830edc24b6e8e4d78de8320e2fe889", size = 634883, upload-time = "2026-03-27T00:00:26.092Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/8a/e4ef6ee11701b6cd64702848415ffb69eeff85cb388a3c6c7fe86f22f3f8/gunicorn-26.2.0.tar.gz", hash = "sha256:62b864895d9ebff0b2f9867ba04fe811c93121596540830c9c916d0769668447", size = 787921, upload-time = "2026-08-24T15:05:59.3Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/c8/8aaf447698c4d59aa853fd318eed300b5c9e44459f242ab8ead6c9c09792/gunicorn-25.3.0-py3-none-any.whl", hash = "sha256:cacea387dab08cd6776501621c295a904fe8e3b7aae9a1a3cbb26f4e7ed54660", size = 208403, upload-time = "2026-03-27T00:00:27.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/85/7522a52e5e2f42faf1a129113ab63e548c42e103e9af395b7bfe65e403e2/gunicorn-26.2.0-py3-none-any.whl", hash = "sha256:bd249d0b3f7972f7432f0a6b6ff3b3ee2d129f70cd1ff6c09a9dd9e29a2b88e3", size = 228389, upload-time = "2026-08-24T15:05:57.67Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1467,6 +1465,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/8d/db8673846ee53cbb5de4c2b4decc11cf733e203eb7d5146297869f69bd48/opencv_python_headless-4.14.0.94-cp37-abi3-win_amd64.whl", hash = "sha256:cbed65415b8f6a9541c705afe3e64795840524d0ff3bc58f507826284a1dc64b", size = 41028030, upload-time = "2026-07-28T18:32:36.724Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-api"
|
||||
version = "1.45.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/02/6e0ae9cc61bd3169d401077b507b3ebc344745171e1051ab430be012dcd9/opentelemetry_api-1.45.1.tar.gz", hash = "sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75", size = 72804, upload-time = "2026-10-06T17:32:58.133Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/41/f7dcf80b81ee8e71c1a2b59f14208bc723edbd89ed027a73b175abf6348e/opentelemetry_api-1.45.1-py3-none-any.whl", hash = "sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb", size = 60256, upload-time = "2026-10-06T17:32:33.506Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openvino"
|
||||
version = "2026.5.0.dev20261005"
|
||||
@@ -2115,27 +2125,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.16"
|
||||
version = "0.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e7/e6d5749983b020e6cb1055abec0a6f361a8e94f0184cdcf56d4760995f02/ruff-0.17.1.tar.gz", hash = "sha256:5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f", size = 5323349, upload-time = "2026-10-15T15:23:18.64Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/78/90c46ca6250709f7c37e397b60d06061adee8d0f7d13959ed6a8e49b14fd/ruff-0.17.1-py3-none-linux_armv6l.whl", hash = "sha256:1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a", size = 10147528, upload-time = "2026-10-15T15:22:33.667Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/d2/a2e28bc35b083ae51fa07dfba5280a667c8b01ecaee7802fc8849dcd5c46/ruff-0.17.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb", size = 10225974, upload-time = "2026-10-15T15:22:36.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/71/7a9029a81c8206ee01a98f6cfeeb56e6c095af86857581a37045c46d7e94/ruff-0.17.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74", size = 9270361, upload-time = "2026-10-15T15:22:38.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/8c/e075e9c917cb0575297880c96768304e881240171c870cbd8d9b7b54bec2/ruff-0.17.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978", size = 9938300, upload-time = "2026-10-15T15:22:41.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/27/0b4bb926a49de2383f161272b4d863571a4c7ecdbf595a6bc58813a13367/ruff-0.17.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640", size = 9948464, upload-time = "2026-10-15T15:22:43.681Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/27/518d57e0e02aecb8ea4acac6b57b51a53173d25fa734952bfdd7d531e88e/ruff-0.17.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1", size = 10638639, upload-time = "2026-10-15T15:22:46.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/82/d89a4e498ade59e6cee52d67f8da569019372212b87468c1fdeae884601c/ruff-0.17.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05", size = 11337238, upload-time = "2026-10-15T15:22:48.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/7c/3b0fdef9352656317c977086fe1d802902406e4f2a0d8578da90c0e6d1c5/ruff-0.17.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785", size = 10896327, upload-time = "2026-10-15T15:22:51.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/86/5566290219708ce2928f3a29c4f843a1185b341ec6111c4631a33c5ceced/ruff-0.17.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189", size = 10350811, upload-time = "2026-10-15T15:22:54.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/d2/ca8462dabb3c413990d49703ba306c30c862195a75448d080fa8fcce9775/ruff-0.17.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467", size = 10569994, upload-time = "2026-10-15T15:22:57.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/ad/dd1864c1f1db17c8cfb297ce70b352388e6977620f66f7182c52855407df/ruff-0.17.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab", size = 10059672, upload-time = "2026-10-15T15:23:00.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/45/3f56cf1d2168c2165cdd5ac6f9668aee66a4cfdd4caf2b2cc5ccb2b1d568/ruff-0.17.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca", size = 9947803, upload-time = "2026-10-15T15:23:03.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/4b/5457cb2be4c1d1ea5e919558131a0f197a4a454a0530d9e30820e8206e4c/ruff-0.17.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4", size = 10351807, upload-time = "2026-10-15T15:23:05.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/fb/db15b4f8fa24a1e36b99e1401f42ef6df16ee299a48670d5d262b771e7d7/ruff-0.17.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a", size = 10765716, upload-time = "2026-10-15T15:23:08.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/e8/1e186578076aff7a80735f625005fde678412ab609cd323ed182eb7d6b87/ruff-0.17.1-py3-none-win32.whl", hash = "sha256:1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a", size = 9895093, upload-time = "2026-10-15T15:23:10.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/2e/da3d9589bae8e8b58f3a48bd5bc0b980232fb14a83bf02c3be7660f87c1b/ruff-0.17.1-py3-none-win_amd64.whl", hash = "sha256:5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7", size = 10097955, upload-time = "2026-10-15T15:23:13.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/c9/0f2a4dea06a5e12c1191ee3d96b1e1233797d0d5e18f97a2b909a95a9836/ruff-0.17.1-py3-none-win_arm64.whl", hash = "sha256:42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4", size = 9853542, upload-time = "2026-10-15T15:23:16.05Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2321,20 +2331,20 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "types-setuptools"
|
||||
version = "82.0.0.20260518"
|
||||
version = "84.0.0.20261006"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/bc/73c2c27e047e42f114ac50fb3bdef986c56cbdb68096f8690eeafb839a93/types_setuptools-82.0.0.20260518.tar.gz", hash = "sha256:3b743cfe63d0981ea4c15b90710fc1ed41e3464a537d51e705be514e891c1d07", size = 44999, upload-time = "2026-05-18T06:02:55.642Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/36/012e992d81f3c3575e6428d53c2bbe266858cf1b7df7335476ab99f17908/types_setuptools-84.0.0.20261006.tar.gz", hash = "sha256:0f123655f44390a15ec62c9fa30b57f6dafe53014524d28b62cab1edbc303059", size = 46604, upload-time = "2026-10-06T08:15:27.42Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/8f/d5e2d493f09a7a98c95619edda1cb37cee377626c0a869d53274c26f2858/types_setuptools-82.0.0.20260518-py3-none-any.whl", hash = "sha256:31c04a62b57a653a5021caf191be0f10f70df890f813b51f02bab3969d300f20", size = 68444, upload-time = "2026-05-18T06:02:54.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/24/add82014f533f2ebf62088b39db86d6a341bb0fd94af50b3a3c1e8886683/types_setuptools-84.0.0.20261006-py3-none-any.whl", hash = "sha256:f435ec88f8f2319316969e37b5a1ca49496b276889124de249b422b061b492fe", size = 70326, upload-time = "2026-10-06T08:15:26.289Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-simplejson"
|
||||
version = "3.20.0.20260518"
|
||||
version = "4.2.0.20261006"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/e4/f8ecdc3529688ad058b0496f67a1bdfbd2576c3669e1c478b6347b12cb7e/types_simplejson-3.20.0.20260518.tar.gz", hash = "sha256:298c5b2fc2df3eb128d2077abd5bd1f5b0419dcf72fdf0551b2af2f03b1ff2b3", size = 10777, upload-time = "2026-05-18T06:04:39.57Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/84/10c94f04f123c9e20e00c8733ed63f757ae7dfc41132c7eaca1008a0d7af/types_simplejson-4.2.0.20261006.tar.gz", hash = "sha256:af8957b430aded84f6f872be2ea3e21e1c3121da917d1585edd34ac1822c3fdd", size = 11253, upload-time = "2026-10-06T08:15:15.643Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/41/16937326596bec6283a51cc901c701a964019569b861ac5f88128bc98b43/types_simplejson-3.20.0.20260518-py3-none-any.whl", hash = "sha256:f4792dbc1ab049fcaee5d99ca950bd6468ea462fe79af25fe8ca08b6bee313ac", size = 10421, upload-time = "2026-05-18T06:04:38.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/a4/125d32bdbc912592fc1f00f6d413dbda7b89025e91502a25f7105500955d/types_simplejson-4.2.0.20261006-py3-none-any.whl", hash = "sha256:0a11e50d899f593738e013c728353eba3d83376da4093d3e7da1e0f22c533254", size = 10628, upload-time = "2026-10-06T08:15:14.818Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2378,20 +2388,19 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.49.0"
|
||||
version = "0.54.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
standard = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "httptools" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "pyyaml" },
|
||||
@@ -2467,22 +2476,131 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "12.0"
|
||||
version = "17.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/62/7a7874b7285413c954a4cca3c11fd851f11b2fe5b4ae2d9bee4f6d9bdb10/websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b", size = 104994, upload-time = "2023-10-21T14:21:11.88Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/89/3f825ab71c242fffb62ea8fe638741c290f62f8d7aadf8125ff897747af3/websockets-17.2.tar.gz", hash = "sha256:36c2fb94c990cc2545143b12690e2de6c16300f9dbe5b4f33fa300cf57dc8792", size = 188355, upload-time = "2026-10-03T14:56:53.5Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/6d/23cc898647c8a614a0d9ca703695dd04322fb5135096a20c2684b7c852b6/websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df", size = 124061, upload-time = "2023-10-21T14:20:02.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/34/364f30fdf1a375e4002a26ee3061138d1571dfda6421126127d379d13930/websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc", size = 121296, upload-time = "2023-10-21T14:20:03.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/00/96ae1c9dcb3bc316ef683f2febd8c97dde9f254dc36c3afc65c7645f734c/websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b", size = 121326, upload-time = "2023-10-21T14:20:04.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/f1/bba1e64430685dd456c1a1fd6b0c791ae33104967b928aefeff261761e8d/websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb", size = 131807, upload-time = "2023-10-21T14:20:06.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/3b/98ee269712f37d892b93852ce07b3e6d7653160ca4c0d4f8c8663f8021f8/websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92", size = 130751, upload-time = "2023-10-21T14:20:07.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/00/d6f01ca2b191f8b0808e4132ccd2e7691f0453cbd7d0f72330eb97453c3a/websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed", size = 131176, upload-time = "2023-10-21T14:20:09.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/9c/703ff3cd8109dcdee6152bae055d852ebaa7750117760ded697ab836cbcf/websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5", size = 136246, upload-time = "2023-10-21T14:20:10.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/a5/1a38fb85a456b9dc874ec984f3ff34f6550eafd17a3da28753cd3c1628e8/websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2", size = 135466, upload-time = "2023-10-21T14:20:11.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/98/1261f289dff7e65a38d59d2f591de6ed0a2580b729aebddec033c4d10881/websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113", size = 136083, upload-time = "2023-10-21T14:20:13.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/1c/f68769fba63ccb9c13fe0a25b616bd5aebeef1c7ddebc2ccc32462fb784d/websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d", size = 124460, upload-time = "2023-10-21T14:20:14.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/52/8915f51f9aaef4e4361c89dd6cf69f72a0159f14e0d25026c81b6ad22525/websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f", size = 124985, upload-time = "2023-10-21T14:20:15.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/4d/9cc401e7b07e80532ebc8c8e993f42541534da9e9249c59ee0139dcb0352/websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e", size = 118370, upload-time = "2023-10-21T14:21:10.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/de/87854af9b38fe4738fd85f7f21c5b49558ae20aec898880894e435f33375/websockets-17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:916ebdfd82e7fc68041d36b2b5f60361b9abce1e087454da15f8bd004839e090", size = 217757, upload-time = "2026-10-03T14:53:23.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/2e/1e80b5efa41544f626d56bd15ccb53dbfc56bf28bf80ab9cd6f82c4b1d20/websockets-17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3621f3686397708b8eeabfd0a9d75267c1f29a7537d2fe31e65d099e71587fa4", size = 215439, upload-time = "2026-10-03T14:53:24.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/6e/82c78b595aee05be76a7ee78539323da1593c1848e4fef51c704c696568f/websockets-17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a81e19710d48da88653473b6b9c366d47e99fe4f58e37ce415be47966748f31f", size = 215703, upload-time = "2026-10-03T14:53:26.226Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/c4/905ef6aa80423c03dba99e1e26fc0acf63a2a9a6a2d9e8c0e6a63caaf952/websockets-17.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f2731f9067976c8c4127212c0d2f2ada42d497d935e470419e029802365b12bb", size = 225023, upload-time = "2026-10-03T14:53:27.744Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/c0/a6d8be9c43e4456fb9597fdf8b5e0ce1f0a5df41503acce6d869536e4e23/websockets-17.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6627b913b8586b1c06db9516b31dd0dfbc621de3bb9312616d92a7e44f268a5b", size = 225299, upload-time = "2026-10-03T14:53:29.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/d4/976d34b5491258b0a86c2ce9b9aabb9fdd68919ffd7fe65999c14a502a98/websockets-17.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0198c4ec6a3406a2f7557c032967de426474c2c995c81076585e09d29a9f407b", size = 226540, upload-time = "2026-10-03T14:53:31.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/2f/c4cfd42f53c697a8ed123fd82b8f85fcd13b6360d47f9f1d1d45d6ec6627/websockets-17.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:88c6a42c2632ff469e84155e44f6ed92cb15ccb047bf5fcb59225ae5a12fd33d", size = 229371, upload-time = "2026-10-03T14:53:33.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/55/9a221b29c6232ff9282eecb2fc102402cb9e42a3479264db0e5fc4fe6835/websockets-17.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:eb0023e6cdb4b8ece0b33875188dd16104ad8c335361d396a98394f99e30ff7a", size = 227173, upload-time = "2026-10-03T14:53:34.502Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/07/125e6d010c56c253d3d2b93cabaea0f96d33898151a16b49066a594acecf/websockets-17.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c1c09d5d4646eb96bda2cfb97493bcea21a0956a981de116e6b1f4a9de07f3fd", size = 225929, upload-time = "2026-10-03T14:53:36.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/a8/aad3bd902aee84e1b261ad6ab83b405e4a564af43101b8ad1dc0293ff4f4/websockets-17.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0360c4dc13ac569cc245e0efa2f4d4b1e4733d24c47b8ab3f3747227b1356348", size = 223167, upload-time = "2026-10-03T14:53:37.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f4/ec8ab9be1a5310b4fea829f088c7aa2b7a58b61d34bce1b2a9338635ff12/websockets-17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76693a16dead737946b651375ee3109d7db7ad9569a1c55c60aaed3ef85cfcc6", size = 225974, upload-time = "2026-10-03T14:53:38.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/45/ba6503f8257d3f98b0f07ebaad0fd099c9023eae744fd5b775416743597e/websockets-17.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:77a42cc507993ec5471b5283f7eef869239173b6000031543e3938a86d1af0fd", size = 224581, upload-time = "2026-10-03T14:53:40.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/45/05cca59a876c6776727d96fc7ba59e0b6f9aa496afbf13e7e04ad0b63678/websockets-17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3bbc5543e39ee025d524077c5c15c2d67bc11c9f6676afe5b531839e24d701f6", size = 225347, upload-time = "2026-10-03T14:53:42.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/00/cf0e43292ae949b13f67535be84317102891d69fd1986ec2bf2ead42747b/websockets-17.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8da58558bfb0ca6ccac2419773521f1111e40654038b1afabdfc69c02cb82614", size = 226457, upload-time = "2026-10-03T14:53:43.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/0d/9a5c61a18f0cc9876d94c70ccb3daf7614a9fee56abbb37c0e64e757fb96/websockets-17.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:01420cb1cb47433e8e7075d32cb8017ad3ffed0654bd1e48c0251b865920dec3", size = 224011, upload-time = "2026-10-03T14:53:45.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/ed/991c1ab80ab2ce40e1c939fef6fa8f971c3ef3b21caf988a7a107e0ad27d/websockets-17.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c49c9edd47d0e44d360299e2d8865e2950d2fcf1b4098782c9d7dcd070919e5a", size = 224990, upload-time = "2026-10-03T14:53:46.8Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/7a/363c835d17923e967fb66376188e67b9a261c85d826a0cd5e4dd3471221d/websockets-17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96f6c8d0fe21930d1f982bfce2382789d2e8d005d2ab63d21280660f95ef8fe1", size = 225265, upload-time = "2026-10-03T14:53:48.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/90/6c51f6d78636bd1cd6781fae8ea5ea7bf1d5b4059354f3c1f5f8de793338/websockets-17.2-cp312-cp312-win32.whl", hash = "sha256:b25659ab2d655d742701487d5591e3f98e8f8b329fc999e05e3d59691ab344a1", size = 218228, upload-time = "2026-10-03T14:53:49.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/2a/90008411c652dcfae34345a2169f4becd066a4ba71eebfa8dd801e0445e1/websockets-17.2-cp312-cp312-win_amd64.whl", hash = "sha256:faa763b677e96f1beccc6b4d7e8c079dfeed2f249f57a19debc321b519ee64ec", size = 218528, upload-time = "2026-10-03T14:53:51.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/a1/b8ad6c17f8e75ba2215422fffe0d7f0c4b690dcff1c47c0473db0d253d51/websockets-17.2-cp312-cp312-win_arm64.whl", hash = "sha256:63499fc49efe48bccc2fca40723bc7adb198866cbe159093dd979905316994b6", size = 218457, upload-time = "2026-10-03T14:53:52.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/54/a935a32dbc2e7365b1b59eb74b5ab7515456f02370fdca4c4efc3574e96f/websockets-17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b24b83fbb34b2d8de06cf0f0d4bd7737344ef854482a614826d4356c0c3f0c12", size = 217752, upload-time = "2026-10-03T14:53:54.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/95/cb8881851abe2662730e6c61cc521b4c96513fdf9103a44f169afce2eba8/websockets-17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8a829db795e3f87053904493d184b185c8eb1f497c852f434168ec856aa6f997", size = 215436, upload-time = "2026-10-03T14:53:56.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/1e/621bb93f35ab7d337be98f1958294437527e2a1797089b5e734ddc5eec5f/websockets-17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf8811d285acc91216368df7fb55cc8c9bf6fcd90eea42429c7186c7385a12b9", size = 215690, upload-time = "2026-10-03T14:53:57.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/4a/49d0c983c082676d5d413b28e6ba5ae1d174c00268467bf78d9fe986a2d2/websockets-17.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89c4898da776193577279173dcf9860487590611d7320d379435a145881b048d", size = 225080, upload-time = "2026-10-03T14:53:59.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/13/95a45eb410019772002d8f53d81396dad4120f7df39ca9962f86f5d7cd01/websockets-17.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d87091c4347daadbcc0833b65812ff38d7350c67339625d4e4a512cf38e3e8ef", size = 225361, upload-time = "2026-10-03T14:54:00.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/fe/0f0eda80bb441f54becdaf793eb20ee080926f8d2356388377cf262187e5/websockets-17.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1110fbfd530c447380e6e6db88b7e43ffe33d54178f5b0ff0aaa5a280301e668", size = 226602, upload-time = "2026-10-03T14:54:02.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/36/067fc09d8e6f154abde7c2f747c52cc442a02c5eb14816f5c39cb9f8bcc6/websockets-17.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:83abd8beab056aa77a116364811f8fc262dffbcc7abea48de0c85ccbfc6f1428", size = 228035, upload-time = "2026-10-03T14:54:03.545Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/a2/939bade7a396b4c381aebbf3941969f124d0f98d56753f81cd256f3fc4d6/websockets-17.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:876da8ca5520d65b5d0f2ca6b4e7a00d35bb90ccda35cb2ce3cda4b6c711e84a", size = 227227, upload-time = "2026-10-03T14:54:05.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/8a/37b1033e21709dd7fa39239ea4d9cd7f348ad5bcba94eb47253878576f8a/websockets-17.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8462395df8f224d2daa3d80db3ae4450d9d4b7243c8483ac79a82862f1599dd6", size = 225985, upload-time = "2026-10-03T14:54:06.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/3a/0d89539900b06d86366facb7558198046de125ab8c371d9248d6262da70d/websockets-17.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6e9a04e69456015e6ae5e0d486d995137fd435794442122b00ce5f9526ea3ba8", size = 223226, upload-time = "2026-10-03T14:54:08.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/9a/bfc5633e3d538d0a71cfbe7a5fee56c712e16c2dbd0ce17c83196a2a96a9/websockets-17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a2321bcb73758c44c8076509024d02c15ee484fe77ce04edea4bf4d257492cc", size = 226042, upload-time = "2026-10-03T14:54:10.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/1f/cbaf1786d8e3aeafe9d76951fc01139ec353b92555580336f23669382a55/websockets-17.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8be4a87b3baca380ec3c7b1643b2dd268ac9d42c5097c0e8dc9a49342faf4774", size = 224639, upload-time = "2026-10-03T14:54:11.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/49/175faa5bd169486f835602ac0ae6303318aa65693b79cdc72c5ee53b148d/websockets-17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:eb7b737ce8d18c8a08beb68f751572b7bf6a18093ecd1406ca1256b50592552e", size = 225407, upload-time = "2026-10-03T14:54:13.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/d1/3662f612456cfb2dcc128c8e596f0a55fb7b695025e2ebe8ba2abb355c3b/websockets-17.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d6605630c2808b33f362d6d08582e79821f77ed2bd3f49f9d467ea70defea06d", size = 226513, upload-time = "2026-10-03T14:54:15.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/6b/07af5177a49e30156b0922556fa93624a920a2b17d3e63bf4ad94668112c/websockets-17.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dd9252828073fd0d69e7667af4275a1b17c18d0833b1ab7f59db272f194a6b9a", size = 224072, upload-time = "2026-10-03T14:54:16.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/34/d18054ff4d8314524164f8b8efec2cb17627287e099f122c28ed6fa598e0/websockets-17.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:06c7386128a9d85de4e1960114604f3031c084d2f4eee8db382637f1634cbab1", size = 225022, upload-time = "2026-10-03T14:54:18.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/12/75433caa3e9fa3e51d7751dc6bad24a86addf76cbfb51e52b11d037ba7fd/websockets-17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:98f2d03df74977fd252831c997c388cd6c3f691a8a9d022b266d3cbd9849838f", size = 225303, upload-time = "2026-10-03T14:54:19.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/de/23e21c002aa2786ac9807c0876faa3b2576493b29ca3386287b0db46f021/websockets-17.2-cp313-cp313-win32.whl", hash = "sha256:5b43a1f7e4853ce08c3f6d3bf69799ee5b46548bfb71792a8158f7e45d66b547", size = 218219, upload-time = "2026-10-03T14:54:21.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/eb/960411c0c574535d629c16e96a2b4e5353dbe4109df8ecea859e1b5245ee/websockets-17.2-cp313-cp313-win_amd64.whl", hash = "sha256:27c7a59b5352a8f741b422820adfe89dfe47c8f2d84fb32111e76111edaa0e83", size = 218531, upload-time = "2026-10-03T14:54:23.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1a/3ac07bb52378952eff1d52d04a7ee6e82ce84e3da319a52a4739cd9c78f5/websockets-17.2-cp313-cp313-win_arm64.whl", hash = "sha256:533b7c82bb1eafbeb921dfe131c9f88e55451ddc328d84bde1c9340ba72d2808", size = 218466, upload-time = "2026-10-03T14:54:24.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/74/6bc991a28ac983600e65de408ebd1b1413d554ed0468ae5c831bc52dded6/websockets-17.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ecb748910e9ba4624ebe2057791df51dcbffb48c37108ab94a3c593472023c9e", size = 217791, upload-time = "2026-10-03T14:54:26.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/2f/158e99426be6e71d09520bae53f29294fbb614b2fc5fbf8867b1d08395a7/websockets-17.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2ab9af5cb7265899e659f079eb71691375a1025b6d5fbd3caa495dd08f70833a", size = 215486, upload-time = "2026-10-03T14:54:27.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/09/1abf942723c0001d9c2fca1551907dade6304517b982b0bf10bba107fa81/websockets-17.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:06e46da092bca3a52e98f0458c66b247993ce501a07cd09c858be3296511ab7d", size = 215699, upload-time = "2026-10-03T14:54:29.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/1d/1ade03963ef497c47e6bad79e24370827b2fe6145fa8f58070ff2b7dcbac/websockets-17.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fcce735ffd72ac4056db05325d9f0232382b74826f0196eb6a15ca903abdaa0f", size = 225081, upload-time = "2026-10-03T14:54:31.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/fd/47b8a0361c49da939b976a07b27a72a9f893d01dfcf4d2a28b53419ce1ef/websockets-17.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:42cbca10f82a8b2fb1536e8a0830ca6ceeb6bb3d8d64b766e0795369135654a8", size = 225430, upload-time = "2026-10-03T14:54:32.917Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/26/f4d4c76264ee037c5556ab5f50fcba302746dabf7528955534e4dda9965e/websockets-17.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63ff5a21f26bd0e6a8464b53fadbe174825c8718ac14180df45665eaacdb6af", size = 226676, upload-time = "2026-10-03T14:54:34.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/b3/c8b1c981322a050c4babfd327ffc9880f9c3834f5b15d2574e37eeb8768c/websockets-17.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:63f543463601c1558b755f8dd7618b6ec3dd0934dda051d3b7030d8c76e54de2", size = 228048, upload-time = "2026-10-03T14:54:36.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/5a/1cb29ddb23e6bc27ffd1c5316cd3616360d1ba0c3854eaa134ee3207bd28/websockets-17.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4c32eb565ad9ce8a6444248e5b7a19dbb86a81c811fe5fcc2fba7a735aed5163", size = 227281, upload-time = "2026-10-03T14:54:38.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/64/135274572dc0c845fc1111e2b932c807c395daac75d6eae6cfa148d8a208/websockets-17.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d459bbb6c22f26dcebea56924a362aba50d453b9867912862c970434fcf0d94", size = 226025, upload-time = "2026-10-03T14:54:39.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/75/f1e386aec3124489411caf5138cdd5a2bc43d3fd4a681c69adcf5f6272a5/websockets-17.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f19ca1a21871f024e38faf4107b433047df27558dff1b72a1dac31481e2c1fe5", size = 223277, upload-time = "2026-10-03T14:54:41.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/eb/24733a0f568c2eb99e60f9faa620a98fb228c06a01e7e2f348b33290ed9c/websockets-17.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c76b4bcbf0f713194591673fc86a42820e14da6bbd1bb445d3d002cc4d1e4521", size = 226148, upload-time = "2026-10-03T14:54:42.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/6d/ea66a30af74f5983cae31ebb9ef78b178b366a12856a414e1472225c4a34/websockets-17.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:30201a7f69833b015556c72feb69ea501b645986fd0b90dab13f589e995ff428", size = 224615, upload-time = "2026-10-03T14:54:44.41Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/80/c6f2228ad89774429d270179375ebddb657119215f52d1df7c680d65cad7/websockets-17.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0c8600aec354cc259f1691b0b42816f04a9886a953f82cb227246df76057f97a", size = 225398, upload-time = "2026-10-03T14:54:46.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/4a/3d8da19732ad468d4be7f1e3ac298078b60bdda55edde6589bef84a5eb7e/websockets-17.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:307fc22ea496be8542d67b82ae8c867a978dfd19ac35573d4f15943fd9277dfe", size = 226571, upload-time = "2026-10-03T14:54:47.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/22/1231657122d9cc24791bb90af13cc2f4e84cf0d3a454cb37e3abfdcb2fd9/websockets-17.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9c88697fa943bd4ef67cc919a17d81de6581846f52bfa8c6f64a916098986556", size = 224125, upload-time = "2026-10-03T14:54:49.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/04/350ca2445da758bc42cdb4218b44d4ce0d5a9c1d5e4cc4a58d64348ad9da/websockets-17.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f7eac84d4969da82166d5e90d9c38d2f416fe24f9708a7013569b193745b9a31", size = 225081, upload-time = "2026-10-03T14:54:51.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/c4/dec952b0df3a5d918ed2a545abb0c25ae519c3bc2d9aba3b7c46abae8f05/websockets-17.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:313f6703023d53baabab6d6c5c37cf637b2c4fee255acf2ed5e92ad69e28f1b7", size = 225376, upload-time = "2026-10-03T14:54:52.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/b4/198a260afbcc086ff4979774e51834ed7fb5b95f9ef305e0c4924630b857/websockets-17.2-cp314-cp314-win32.whl", hash = "sha256:08d90cf344bdb971ba3a826b78d4da9bfd56cc6a97a604d9b88cbd40bfa6c735", size = 217760, upload-time = "2026-10-03T14:54:54.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/9e/0523f8bc2f7aaddf39562d4fa01b4d38fa61b23d980917a16d2dd19c8dac/websockets-17.2-cp314-cp314-win_amd64.whl", hash = "sha256:dac93bf7a9beb215be3282b8441173cd50806c41c007b8be9bb24e03c60ad563", size = 218104, upload-time = "2026-10-03T14:54:55.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/17/7b8bb4cb64a199e7082f1f9be784d657842fefc327ac777d6c1493504804/websockets-17.2-cp314-cp314-win_arm64.whl", hash = "sha256:2ab742249f953d148a9ba696c8b9944361e8cb92e8bc61ba2dd53a178403afd3", size = 217989, upload-time = "2026-10-03T14:54:57.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/76/f54ed054b6e860f1e0bbc7019542a048352d41231fdff6d904b379f881c7/websockets-17.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a69ce25be5f1330ee1c74eb6fabbbceaa96b384beedd2627cecded7546490c40", size = 218125, upload-time = "2026-10-03T14:54:58.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/4c/0f3375cea66a125ae01d21fb9c537aae955ef499bfe7e2b2376a34362f2a/websockets-17.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8e24b878cf54843a63985d90480f163ca7f692689fbcbe9cdbd8165521083a8b", size = 215658, upload-time = "2026-10-03T14:55:00.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/05/7c871a67bfb4b61adc1fe13583db97803f87dfeca644fe6ef51df7bb276d/websockets-17.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f33c7908a6885dcae9f462a4a8347b637053b4ff2b96beb4c23fba1cf7818e5f", size = 215858, upload-time = "2026-10-03T14:55:02.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/8e/59df4d9cd357e902d1c74b13c3c0c3841c8df6e4b1b3d131bf26a23fdcb1/websockets-17.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c796a1bb3e4015249639849f30e8e680df8a431b45d417ba8acf843d2451d95f", size = 225443, upload-time = "2026-10-03T14:55:03.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/64/5e486a3a44e041203c62eccf1fc89c7f8824e21104a7b82b182e5b21c228/websockets-17.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:983bcdc898662f6ba9d6a025c30d29946ff0986d9ad60d400af0da3671f7cbf3", size = 225726, upload-time = "2026-10-03T14:55:05.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/98/b6eb53121c91fbe8b6897aba06861ce60f9ab58faffc6bca5750cbc21681/websockets-17.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35e0f088ddfd9d9bc5019e27ff3767411779e92b59db5bb1507f2731a5b61158", size = 226895, upload-time = "2026-10-03T14:55:07.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/18/8c091321b99c91eb3eaec9acbd940e69308b4e465b5605c430af0cf7d3a5/websockets-17.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:19e2511412ad3393191de652513bc7a0ca3c93af143b32d96d46e59fbbddf1d4", size = 229040, upload-time = "2026-10-03T14:55:09.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/96/3a92f944305b7de42fcb7530b9fa69607b4b4ce993c36a9f2330dbc318ba/websockets-17.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb5e2bf969ac99a6ae3c71208a5eb05cfde973192540ffa6e1068b57fb78c4f8", size = 227469, upload-time = "2026-10-03T14:55:10.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/a9/624f6d75ba326c22d03698b34c0ada984f1d76196322a62f6c22903b831d/websockets-17.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:691780fca2be3dec512cb603cb91060271968cb4af86b51d07c57445c5754a37", size = 226202, upload-time = "2026-10-03T14:55:12.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/af/1e6e8c625aeb268830af2c4227fe05e8db59f4f4debe1dadfd0ada214895/websockets-17.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2d39c19b1ba6a6791050383fd69efdd3b63533e2254693d0263879cd5f5921ba", size = 223743, upload-time = "2026-10-03T14:55:14.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/81/33c5280f4f6f81637c93ae065c6a594dfe35935622af135a5f7c3768bf22/websockets-17.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e48ac2b302986c6f55cf61e8e36b4dd97d0132c5078a713a697a940934ba422e", size = 226492, upload-time = "2026-10-03T14:55:15.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/f3/7aa9fc36e67caccbcfee2c48f4ada41e9da512d41523c024d039f0f22ba3/websockets-17.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:e136197f1262620ef2e507afc3ea759c1ae7d221886da20eec5f4c9f2618c2aa", size = 224940, upload-time = "2026-10-03T14:55:17.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/8c/457aff7081a63d1261608bb4d7b0b0f9dfe780697a2a334671745742850b/websockets-17.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3eb44019a2b0b3b91bac95998f1e4e5589730421170e060fe654a2b7be727dc7", size = 225835, upload-time = "2026-10-03T14:55:19.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/c3/7a13a3b3050db2c36772ded49f8d48f99eb080948e9f6f762e7529925ab5/websockets-17.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e5855e574804398859c5fbaf4fc7882b96278b7f6572a3d889627e6eb6cfca59", size = 226848, upload-time = "2026-10-03T14:55:21.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/3e/d5b2c1e473b1031a4a0ec0e10de69df5b981ab4a10aa482bb45c18dd43f5/websockets-17.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5dc29815520c329f5662f6eb3ebadecf0d4f8c82dfa416d4d6efbf8f39245559", size = 224541, upload-time = "2026-10-03T14:55:22.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/5d/bb81976cc1aa546afb51395ce42913521e9dea062bb34a61308cfff30726/websockets-17.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d1a4f9462da6496b6cb79bbb09c60d17f7e63e8a1df136797b3afabec9560e4d", size = 225315, upload-time = "2026-10-03T14:55:24.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/6b/314962d5440c61b4c107914599c13ceeecc6bdb6e2e73a5f7e566a7d1f26/websockets-17.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9496bff5541086478264678bac73c0a75b2fde94fdf6568893bca1f7c6d50d18", size = 225747, upload-time = "2026-10-03T14:55:26.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/fc/9eb64b34a3a4458eb08f3f24bde01508f72a00790330723c158ebb965048/websockets-17.2-cp314-cp314t-win32.whl", hash = "sha256:e1e3bc8090a7eae79fdf634b63bdbfa3c93999991023c37c6fd3b469fc8ff5dc", size = 217891, upload-time = "2026-10-03T14:55:27.681Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/ed/3a4e2a09b0822d6e525cbc6e44a4885669bad5b22ab9c64fa2444bc15325/websockets-17.2-cp314-cp314t-win_amd64.whl", hash = "sha256:65a89a5bde227bfe908016f35b5bd347970cd1e5b0360f389502eba1c7fde6e0", size = 218229, upload-time = "2026-10-03T14:55:29.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/66/cffb75ee746dd060984c3c3e2eac7f875a866225a30dfa53e2cd18232565/websockets-17.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1c27339934109dfaca83f18ab2c23db06714e9d5deca2c8e37e8f492ab90d20b", size = 218146, upload-time = "2026-10-03T14:55:31.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/e9/10a9b1633b63594054c87b97af048628cea2b21b5089a52a9fc1e0af60a3/websockets-17.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:a7c4bb26de6ef496d24822aee4f6a305d97cd33d21a2b85f290292d69ba1c25e", size = 217719, upload-time = "2026-10-03T14:55:32.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/00/ff4020fe0886dac7199a16ce2805c7afd7b981bd2e81d3fa18dff5d9863a/websockets-17.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c08da1f15040bd1e1a6074bd4518a6ef20e67b1594ecfb0aa75e5b45f87e6d6d", size = 215448, upload-time = "2026-10-03T14:55:34.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/06/bc7b944f81514378b2c2ab96c17df19e871cd33b9be0f1f6dfc975457e5e/websockets-17.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3117abfd32b183bdb6194df9317766d32c6517f3d1c0aa8c62d5c6ccfda0b4a8", size = 215674, upload-time = "2026-10-03T14:55:35.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/da/2b2b76faa2f10c4813e3872c9577fd13a798f5918b1785b86ff7d635eb2a/websockets-17.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a046227daa7f191e843d26b911c1146233e9a33d249e0c954dcb3ac7c398710e", size = 225119, upload-time = "2026-10-03T14:55:37.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/d4/22cbe288c0d5cef7620503be92c0098d82220353fc7e188034a19c517240/websockets-17.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2901bdf24f20bc884124b3e88c61f7ece260c20c81e610f2196007395264a4aa", size = 225549, upload-time = "2026-10-03T14:55:39.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/0a/504b0d3063679f2c60430c3539482d42a4cb8bd1a76646baf742030a93cc/websockets-17.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f60e39adfecf998488166aca8ff24ab1ac406c9ecbecbcf9b3bcfc43cb1ec9a1", size = 226717, upload-time = "2026-10-03T14:55:40.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/ea/5da9309cc55c2665a6eebc22c369d9918c0d77258c61e92058e6b08d5ff1/websockets-17.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d4df62fd8448a85c752bbea1803cb3a2785e6fc8352009ab64ad7447af079b3c", size = 228413, upload-time = "2026-10-03T14:55:42.54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/74/5a24df72aa5500f311105687af864c27f1f9da910e968e97818c6149e6b0/websockets-17.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8eea55fdfa9ba65c6981eea38bd20c800bce2f092a2803d82de764ecf0f071a", size = 227196, upload-time = "2026-10-03T14:55:44.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/ee/ca32cc1ed892dc4ac30a922e8f648048233fbdb8b0bce7048860ec4c60ec/websockets-17.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3f0def1279644acaa9bc861d4234af3f82ea9cee7e460dffac5cb63e691501e9", size = 226092, upload-time = "2026-10-03T14:55:45.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/0c/12d4a73324aa9798d5165d20c088f9dba66c75c871960e5d921ec66694e4/websockets-17.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb78fb4158c12f77a934a003006784108a27a6553cfc0c6f10483c9c02e94f48", size = 223486, upload-time = "2026-10-03T14:55:47.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/a4/7fe15da5abb8f0f61e6a357593f7f2ed55724825b7db0ffe72b5c5fad68d/websockets-17.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:f8969ad228115ad8869b5fed801f899e52ab8ad376fdb165ba4760a277c8258a", size = 226200, upload-time = "2026-10-03T14:55:49.126Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/b9/4cd3a311f96a2eea0ed458bc01fe2cce42f9cd50aa9e64315dfc855d63a9/websockets-17.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:4a49ca342efc0800e6ae94ed5c9cbdcb319308f75e73c21181e4c24d6710e8dd", size = 224862, upload-time = "2026-10-03T14:55:50.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/b5/22caa3460f75e42bfcc74028870b556d22847ea9a9034aa03986f07f16a9/websockets-17.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:06fa3ce9c3154826c33d4395b225b2994aa64f1f3bcd8be8ed932019175d9268", size = 225391, upload-time = "2026-10-03T14:55:52.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/be/8d28f92092076abf1ddfb3206b0ce956120a22e7c3105f6a3029d727deae/websockets-17.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:50644d8715be7e0ec0682f9d7744b63008e199c5e1618a48fa153756a332235f", size = 226545, upload-time = "2026-10-03T14:55:54.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/7b/ff943fa383e540fe17f066cc10a3eeedef26e50fd45aae2bdc6746d6f95a/websockets-17.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:60deca33e584c09e91f70f8b55a0b1de7d671d6a63f051d154920f48bed717c7", size = 224352, upload-time = "2026-10-03T14:55:55.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/df/1e6c3e06c473c9fd833a5c1620b15e2c3b37647b91b7d41871d20bc098de/websockets-17.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b5f79366a8d8dbb981d53ba800bb54a95454595ab8a4548c2b95501b32a08326", size = 225255, upload-time = "2026-10-03T14:55:57.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/f8/d8a4f988f7cbb568d8bd69da4632c5b6010aa9cd9366f285e23b73b678d9/websockets-17.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f2bbf3f28d0b63157577c8b774b9136f076afa6797e1a52a2ecd477f23cad3a8", size = 225513, upload-time = "2026-10-03T14:55:59.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/e0/920357165b2797a2530fc9e271d79a9b5fee2b750b154c990c740f767af3/websockets-17.2-cp315-cp315-win32.whl", hash = "sha256:74836317b7010b579522bb52426f1e225608b042c9e78cbe2493522bebb8a318", size = 217722, upload-time = "2026-10-03T14:56:01.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/eb/25bdca25bbc329ffb330ef33993397d6556a871e40a0d196e757699ea3f7/websockets-17.2-cp315-cp315-win_amd64.whl", hash = "sha256:aaead3d926e9ab4124ada727d20cd62d396649917822df4f771d1f07f1079b40", size = 218017, upload-time = "2026-10-03T14:56:02.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/cb/ea30a552bbcd1c75f0d14bfce6c884ee36187030b85b74a242aacc02406e/websockets-17.2-cp315-cp315-win_arm64.whl", hash = "sha256:40960554e60eb60c3eec4ff9e42a80f84f8cd3ca9bc80a5481a61f1e64d807c9", size = 217929, upload-time = "2026-10-03T14:56:04.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/01/477664c619af8aa3c908d482e2a95e13ceed9d78f21d15902013c3bc6c28/websockets-17.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:9a2a60a7f0ea5f239efb6391d2b28630a640d82dad63e3bee47cf2c623c4495d", size = 218029, upload-time = "2026-10-03T14:56:06.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/a9/b0be62ff1c0e2bc966da56b36d3d820c7e2ad3c0c4a4ac414fc7335b214f/websockets-17.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:cca2fcb72c007103740fa4fc3df19fdb1a318c641c69f3b0cc47ed63a889336e", size = 215607, upload-time = "2026-10-03T14:56:08.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/2b/a6738530de0437a31c1b168e4096ecf790aafaf561f33a009886c7d8042e/websockets-17.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b789356bc4e2e6c20ba52817f92c3fed74e24657654237ecd536c54843b80c6c", size = 215817, upload-time = "2026-10-03T14:56:09.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/c2/2fc44ddc419cbb09ee1708af3e78d8a4b018db01fc7e4f91bd730e2f8d9e/websockets-17.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:222fb626fa15701a850eccc778be17312142b2f6a0e16aea80770b7459adb784", size = 225979, upload-time = "2026-10-03T14:56:11.85Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/91/a215b14caa7ea65bc36db81609108899c259503300d1560dae9c70a135e7/websockets-17.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4497e87c34a2d21cbec1227858fec3af8e514dd70c47625557a122fcebc081dc", size = 226250, upload-time = "2026-10-03T14:56:13.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/b9/9406a18e9edf558ed504d2a7679371d0f8107e4ef526c80b154ea4ec9752/websockets-17.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6281c171557ce0e408e19d9a223f22d915117ac38a5a7f32ed83809e7492316c", size = 227579, upload-time = "2026-10-03T14:56:15.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/45/a73af119244f46f5130005d7ab63f1c75890c890141a0ca2adc9d97d4671/websockets-17.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:08d97098644728bd1895caa7ecf3090b8e563d70809870d2adb33a107bd061d0", size = 229205, upload-time = "2026-10-03T14:56:17.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/92/ccd8e2e921d134a56f1ed4642d276500d9e33b3dc4d6deb63d614b3e53a6/websockets-17.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1fdb8d5a1660307dc6d36d0b7fc725213cbd7f80800904dc4896aa3208b89121", size = 228011, upload-time = "2026-10-03T14:56:18.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/ef/7d71105d19a7aaab5ff87b9c712f6c1dda44e72ea56aa0e7b777f2fc274b/websockets-17.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18b0a46e5e9b315e2b54ce8c3bafdeef0e1388ca363114fa868e6aab2dc58512", size = 226892, upload-time = "2026-10-03T14:56:20.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/f7/87012d628b21e66e699440f39bfa7cc55fae7f52b2c532ab62184a589624/websockets-17.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f115d5d804a2163dd89245710049078b0e726a58c1f44a1f86c2c6e79055d76", size = 224241, upload-time = "2026-10-03T14:56:22.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/f5/495371068b27ee5f7c435187f9dafd62402f195e2c76063bdd4653da1565/websockets-17.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:1d829946a2e7630f92f9d7b45b62f3abe9f393cc2dea6a35edb3988f865e75f2", size = 227076, upload-time = "2026-10-03T14:56:23.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/18/3dce3cc6099be5e044e0fd5d0e0c9931c8e3387511cdec8014a345f619e5/websockets-17.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:6c274fc1572edf7c197094a0eb1887d45fdc95254bc80597dc7599550486c06a", size = 225727, upload-time = "2026-10-03T14:56:25.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/30/57d0c7aaf8d4473926fa8829b8136483f561388d1e747ae71c9f2a83d5fd/websockets-17.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:4173a4b8a025ae44313d9d9b4ecf31e886c7b7faf45386d51a8ca4ff2dcf3f2a", size = 226225, upload-time = "2026-10-03T14:56:27.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/9f/9dce1203756756c00b407b9a6b13a7500fcd38f2634d4daa3f65575814ec/websockets-17.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:d8cfe9522ad69b6abb26b413ed1deca43cb915cefc588433d557cb3ae1c783e2", size = 227333, upload-time = "2026-10-03T14:56:28.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/2f/d3b6b876678ebb03017b7afd7111fe44d54b93f036a80ebb4b481dd1ab74/websockets-17.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:908d81d88bb16141613a6275059b5114656d5c2f0b5400b421d54fe6f1943507", size = 225082, upload-time = "2026-10-03T14:56:30.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/b0/a69b573a5e56d2e7a5dcbb447466f442380cf81515e1cb1220cd626c8042/websockets-17.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:c6590e1eb624ff6b15b872421bc9a10bc6d2057635d69c6cd244ac3f928f85c6", size = 225945, upload-time = "2026-10-03T14:56:32.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/be/a72911dc8e33f74c196012366ce4d99b1a803894a377a1ed0c8e66df9caa/websockets-17.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:61040f6f7da5a279d2f77496c69d51132aba75f701c52bded400d4c639277b18", size = 226241, upload-time = "2026-10-03T14:56:34.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/a9/02a68c1d8e5572918e0962d3aad881078f73ede43abd9b1336e4efaa8909/websockets-17.2-cp315-cp315t-win32.whl", hash = "sha256:f90bad2839c185a1edf8ee22a257cfc8a39e0e337a0490ab185dfa76ef04d1bd", size = 217847, upload-time = "2026-10-03T14:56:36.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/bf/3d7c33b8d5e7712a60e0149c017ed50394ec5e8cf72e5cb6a1ffaf11a42d/websockets-17.2-cp315-cp315t-win_amd64.whl", hash = "sha256:315551f4ccedbbf9fd4f7e8bf037a5948c976ade0e919ba5d8f581d465f6f725", size = 218169, upload-time = "2026-10-03T14:56:37.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/57/ab34cc6460c5322e6932750fa5c6c64be89e6ee4e2707d13c4e9d3312b25/websockets-17.2-cp315-cp315t-win_arm64.whl", hash = "sha256:0a6220bdf8d5f11af71251a599092d89ac1d6bfac691c7f5951c5b07953947a0", size = 218089, upload-time = "2026-10-03T14:56:39.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/58/835cd51934d6780fa586f275b5d9901eead6d81569b4343b3767cdbaae4c/websockets-17.2-py3-none-any.whl", hash = "sha256:6aa59f0ef92e796b2db6f5f26550c4713c0e4036899fadf02f55e2ed4db0b7ae", size = 211883, upload-time = "2026-10-03T14:56:51.898Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -212,36 +212,37 @@ checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
|
||||
|
||||
[[tools.node]]
|
||||
version = "24.21.0"
|
||||
version = "24.22.1"
|
||||
backend = "core:node"
|
||||
specifiers = ["24.22.1"]
|
||||
|
||||
[tools.node."platforms.linux-arm64"]
|
||||
checksum = "sha256:724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5"
|
||||
url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-arm64.tar.gz"
|
||||
checksum = "sha256:33c37be234ba8bee041961047b19160f4d7d0f164e8e246f78bdd10feff148b3"
|
||||
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-arm64.tar.gz"
|
||||
|
||||
[tools.node."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:3048b0811e158ca0d8672b59c839861763e144492980d8d29b369dfee45747e4"
|
||||
url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-arm64-musl.tar.gz"
|
||||
|
||||
[tools.node."platforms.linux-x64"]
|
||||
checksum = "sha256:6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff"
|
||||
url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-x64.tar.gz"
|
||||
checksum = "sha256:eb6728e77c576a35162c122c45ccadf224d89ff00abc8dae46cc2897c6002d7c"
|
||||
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-x64.tar.gz"
|
||||
|
||||
[tools.node."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:01a713e34da98e7b4d2a3191ee7ca3f93dd0384d1a2ef23c379b2161ee6e133a"
|
||||
url = "https://unofficial-builds.nodejs.org/download/release/v24.21.0/node-v24.21.0-linux-x64-musl.tar.gz"
|
||||
|
||||
[tools.node."platforms.macos-arm64"]
|
||||
checksum = "sha256:bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057"
|
||||
url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-arm64.tar.gz"
|
||||
checksum = "sha256:89c682593d76cd44233fa53b1563264e7de9cbb189cad169135c5b4fadfbc0a7"
|
||||
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-darwin-arm64.tar.gz"
|
||||
|
||||
[tools.node."platforms.macos-x64"]
|
||||
checksum = "sha256:1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097"
|
||||
url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-x64.tar.gz"
|
||||
checksum = "sha256:d132557e7ee2b85af08b0c8c34338e65c6430f2238d01f727728ef42dc83b035"
|
||||
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-darwin-x64.tar.gz"
|
||||
|
||||
[tools.node."platforms.windows-x64"]
|
||||
checksum = "sha256:158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541"
|
||||
url = "https://nodejs.org/dist/v24.21.0/node-v24.21.0-win-x64.zip"
|
||||
checksum = "sha256:d2046d28c2c3dcd211ac5145c0bf30e2047ef9dfb306693c8c6e2eccd8b95fa5"
|
||||
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-win-x64.zip"
|
||||
|
||||
[[tools."npm:@openapitools/openapi-generator-cli"]]
|
||||
version = "2.41.0"
|
||||
@@ -252,13 +253,14 @@ version = "7.5.0"
|
||||
backend = "npm:oazapfts"
|
||||
|
||||
[[tools.opentofu]]
|
||||
version = "1.12.6"
|
||||
version = "1.13.1"
|
||||
backend = "aqua:opentofu/opentofu"
|
||||
specifiers = ["1.13.1"]
|
||||
|
||||
[tools.opentofu."platforms.linux-arm64"]
|
||||
checksum = "sha256:9bd0228a81bcd0c88f7045c74378f45a815779f19897191dff7d9efba9976b9e"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762688"
|
||||
checksum = "sha256:9c1ef375aa1852db0b2888aa921b640c71f8140d4682aa4fec99378a64fa7dc3"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_linux_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708055"
|
||||
|
||||
[tools.opentofu."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:9bd0228a81bcd0c88f7045c74378f45a815779f19897191dff7d9efba9976b9e"
|
||||
@@ -266,9 +268,9 @@ url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762688"
|
||||
|
||||
[tools.opentofu."platforms.linux-x64"]
|
||||
checksum = "sha256:50a6106fa4de523d09c87af85f3db1dd47535fc005727fdca6852146476b88ec"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762723"
|
||||
checksum = "sha256:378ada19d4bc70c43732004e8159be771b23b9a5afdf059e5f8a2b3fa2c70a69"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_linux_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708017"
|
||||
|
||||
[tools.opentofu."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:50a6106fa4de523d09c87af85f3db1dd47535fc005727fdca6852146476b88ec"
|
||||
@@ -276,58 +278,53 @@ url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762723"
|
||||
|
||||
[tools.opentofu."platforms.macos-arm64"]
|
||||
checksum = "sha256:f958ec5e511063be9feb180ca015a4cb7977566a9cf6a8550bba8c2a9b5aba74"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_darwin_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762710"
|
||||
checksum = "sha256:be78f659f04ef06a9dbd9b3934d46af95d787a3aa38396d459dea395261816a9"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_darwin_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708059"
|
||||
|
||||
[tools.opentofu."platforms.macos-x64"]
|
||||
checksum = "sha256:44bb1855f372f17f365fb94517906e78da5001da10f4c98de57a39bf982f3a92"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_darwin_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762655"
|
||||
checksum = "sha256:a73720443ba38712d7d96dc1e857add02c15a790919c653ad07492e9952f8c27"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_darwin_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708142"
|
||||
|
||||
[tools.opentofu."platforms.windows-x64"]
|
||||
checksum = "sha256:bcbcd9ca744af6c4053e2d5e75dae79ff33ad7dc38d45e1233e693d096284538"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_windows_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762708"
|
||||
checksum = "sha256:6825ae311e9a15747ce2ede7b7e97c8bb744a4d271fa8113259eeeee3d09438c"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_windows_amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708102"
|
||||
|
||||
[[tools.pnpm]]
|
||||
version = "11.27.1"
|
||||
version = "12.14.0"
|
||||
backend = "aqua:pnpm/pnpm"
|
||||
specifiers = ["12.14.0"]
|
||||
|
||||
[tools.pnpm."platforms.linux-arm64"]
|
||||
checksum = "sha256:dbf8f84263d2bc696306d4219be68256d881d9af409345374d878676a7edcf0c"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.27.1/pnpm-linux-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/577602580"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:7a7854e90bdb7d339430d9943ea482b34d7bde817cfc1c749d3c04ddd464142d"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.27.1/pnpm-linux-arm64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/577602583"
|
||||
checksum = "sha256:cfc780b9e690549e6633e1045fb1fb6ef84e390711e2099e6ccbae91c32456ea"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v12.14.0/pnpm-linux-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/643957409"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-x64"]
|
||||
checksum = "sha256:6be1426540406747b7731564a46c5b30d3023ea6c19e4ce99f994c4ec122f3d2"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.27.1/pnpm-linux-x64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/577602576"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:cc66c1558a1a03b320ff5b4022f755386fa1de54db3f97ff624112015be1ccf1"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.27.1/pnpm-linux-x64-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/577602579"
|
||||
checksum = "sha256:3910c3852136bed5bc1511b9137a45fd4357ed6b3970d9d08893641e9005867e"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v12.14.0/pnpm-linux-x64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/643957410"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.macos-arm64"]
|
||||
checksum = "sha256:352f67ceaaa5aba294a53f483da93c376005b4c7e785fe5411e0ce720fe74741"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.27.1/pnpm-darwin-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/577602575"
|
||||
checksum = "sha256:50f48e0e17508952790a3fbf58fcf14f3b82efa5f594ca04ad904e29aaf854b0"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v12.14.0/pnpm-darwin-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/643957413"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.macos-x64"]
|
||||
checksum = "sha256:2c412e07382049b4edb04fa6da78f3d4ef7810b75853c80544dcfb7c0ed79880"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v12.14.0/pnpm-darwin-x64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/643957411"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.pnpm."platforms.windows-x64"]
|
||||
checksum = "sha256:4b56d1184c3390b08146060fa4b414b8ceec9164aa41bf996e87a0f0bc135851"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v11.27.1/pnpm-win32-x64.zip"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/577602578"
|
||||
checksum = "sha256:6f0f38e917c8ce76ef31db528c357e2bfa354a1bcb16464e3cdec39e6530d784"
|
||||
url = "https://github.com/pnpm/pnpm/releases/download/v12.14.0/pnpm-win32-x64.zip"
|
||||
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/643957404"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[[tools.terragrunt]]
|
||||
|
||||
@@ -15,10 +15,10 @@ config_roots = [
|
||||
]
|
||||
|
||||
[tools]
|
||||
node = "24.21.0"
|
||||
pnpm = "11.27.1"
|
||||
node = "24.22.1"
|
||||
pnpm = "12.14.0"
|
||||
terragrunt = "1.1.1"
|
||||
opentofu = "1.12.6"
|
||||
opentofu = "1.13.1"
|
||||
"npm:@openapitools/openapi-generator-cli" = "2.41.0"
|
||||
"npm:oazapfts" = "7.5.0"
|
||||
"github:extism/cli" = "1.6.3"
|
||||
|
||||
@@ -127,6 +127,12 @@ dart_code_metrics:
|
||||
severity: perf
|
||||
- name: tr
|
||||
description: 'Use context.t.<key> or StaticTranslations.instance.<key> instead of tr().'
|
||||
- type: Locale
|
||||
entries:
|
||||
- name: toLanguageTag
|
||||
description: 'Pass resolvedDateTimeLocale() to DateFormat, not the raw locale. DateFormat throws for locales intl has no date data for (e.g. mi).'
|
||||
exclude-paths:
|
||||
- 'lib/main.dart'
|
||||
- type: String
|
||||
entries:
|
||||
- name: tr
|
||||
|
||||
@@ -75,6 +75,7 @@ abstract class AppConfig with _$AppConfig {
|
||||
.timelineTilesPerRow => timeline.tilesPerRow,
|
||||
.timelineGroupAssetsBy => timeline.groupAssetsBy,
|
||||
.timelineStorageIndicator => timeline.storageIndicator,
|
||||
.timelineSmoothScrubbing => timeline.smoothScrubbing,
|
||||
.mapShowFavoriteOnly => map.favoritesOnly,
|
||||
.mapRelativeDate => map.relativeDays,
|
||||
.mapCustomFrom => map.customFrom,
|
||||
@@ -131,6 +132,7 @@ abstract class AppConfig with _$AppConfig {
|
||||
.timelineTilesPerRow => copyWith(timeline: timeline.copyWith(tilesPerRow: value as int)),
|
||||
.timelineGroupAssetsBy => copyWith(timeline: timeline.copyWith(groupAssetsBy: value as GroupAssetsBy)),
|
||||
.timelineStorageIndicator => copyWith(timeline: timeline.copyWith(storageIndicator: value as bool)),
|
||||
.timelineSmoothScrubbing => copyWith(timeline: timeline.copyWith(smoothScrubbing: value as bool)),
|
||||
.mapShowFavoriteOnly => copyWith(map: map.copyWith(favoritesOnly: value as bool)),
|
||||
.mapRelativeDate => copyWith(map: map.copyWith(relativeDays: value as int)),
|
||||
.mapCustomFrom => copyWith(map: map.copyWith(customFrom: .fromNullable(value as DateTime?))),
|
||||
|
||||
@@ -10,5 +10,6 @@ abstract class TimelineConfig with _$TimelineConfig {
|
||||
@Default(4) int tilesPerRow,
|
||||
@Default(GroupAssetsBy.day) GroupAssetsBy groupAssetsBy,
|
||||
@Default(true) bool storageIndicator,
|
||||
@Default(false) bool smoothScrubbing,
|
||||
}) = _TimelineConfig;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ enum SettingsKey<T> {
|
||||
timelineTilesPerRow<int>(),
|
||||
timelineGroupAssetsBy<GroupAssetsBy>(codec: EnumCodec(GroupAssetsBy.values)),
|
||||
timelineStorageIndicator<bool>(),
|
||||
timelineSmoothScrubbing<bool>(),
|
||||
|
||||
// Log
|
||||
logLevel<LogLevel>(codec: EnumCodec(LogLevel.values)),
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:http/http.dart' show ClientException;
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart' show ApiException;
|
||||
|
||||
/// Service responsible for handling application logging.
|
||||
///
|
||||
@@ -14,8 +18,15 @@ import 'package:logging/logging.dart';
|
||||
/// writes them to a persistent [LogRepository], and manages log levels via
|
||||
/// [SettingsRepository].
|
||||
class LogService {
|
||||
static final _schemeAndHost = RegExp(r'''https?://[^\s/?#'"<>(),]*''');
|
||||
static final _ipAddress = RegExp(
|
||||
r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b|'
|
||||
r'\[?\b(?:(?:[0-9a-f]{1,4}:){7}[0-9a-f]{1,4}|(?:[0-9a-f]{1,4}:)+(?::[0-9a-f]{1,4})+)(?:%\w+)?\]?',
|
||||
);
|
||||
|
||||
final LogRepository _logRepository;
|
||||
final SettingsRepository _settingsRepository;
|
||||
final StoreService _storeService;
|
||||
|
||||
final List<LogMessage> _msgBuffer = [];
|
||||
|
||||
@@ -39,11 +50,13 @@ class LogService {
|
||||
static Future<LogService> init({
|
||||
required LogRepository logRepository,
|
||||
required SettingsRepository settingsRepository,
|
||||
required StoreService storeService,
|
||||
bool shouldBuffer = true,
|
||||
}) async {
|
||||
_instance ??= await create(
|
||||
logRepository: logRepository,
|
||||
settingsRepository: settingsRepository,
|
||||
storeService: storeService,
|
||||
shouldBuffer: shouldBuffer,
|
||||
);
|
||||
return _instance!;
|
||||
@@ -52,16 +65,17 @@ class LogService {
|
||||
static Future<LogService> create({
|
||||
required LogRepository logRepository,
|
||||
required SettingsRepository settingsRepository,
|
||||
required StoreService storeService,
|
||||
bool shouldBuffer = true,
|
||||
}) async {
|
||||
final instance = LogService._(logRepository, settingsRepository, shouldBuffer);
|
||||
final instance = LogService._(logRepository, settingsRepository, storeService, shouldBuffer);
|
||||
await logRepository.truncate(limit: kLogTruncateLimit);
|
||||
final level = instance._settingsRepository.appConfig.logLevel;
|
||||
Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO;
|
||||
return instance;
|
||||
}
|
||||
|
||||
LogService._(this._logRepository, this._settingsRepository, this._shouldBuffer) {
|
||||
LogService._(this._logRepository, this._settingsRepository, this._storeService, this._shouldBuffer) {
|
||||
_logSubscription = Logger.root.onRecord.listen(_handleLogRecord);
|
||||
}
|
||||
|
||||
@@ -74,12 +88,12 @@ class LogService {
|
||||
);
|
||||
|
||||
final record = LogMessage(
|
||||
message: r.message,
|
||||
message: _redact(r.message),
|
||||
level: r.level.toLogLevel(),
|
||||
createdAt: r.time,
|
||||
logger: r.loggerName,
|
||||
error: r.error?.toString(),
|
||||
stack: r.stackTrace?.toString(),
|
||||
error: r.error == null ? null : _redactError(r.error!),
|
||||
stack: r.stackTrace == null ? null : _redact(r.stackTrace.toString()),
|
||||
);
|
||||
|
||||
if (_shouldBuffer) {
|
||||
@@ -100,6 +114,64 @@ class LogService {
|
||||
return [..._msgBuffer.reversed, ...logsFromDb];
|
||||
}
|
||||
|
||||
String _redact(String text) {
|
||||
var redacted = text.replaceAllMapped(_schemeAndHost, (match) => _urlLabel(Uri.tryParse(match[0]!)));
|
||||
final accessToken = _storeService.tryGet(StoreKey.accessToken);
|
||||
if (accessToken != null) {
|
||||
redacted = redacted.replaceAll(accessToken, '[ACCESS TOKEN]');
|
||||
}
|
||||
// as a header map prints it, so a short value like "1" can't match other text
|
||||
for (final header in _settingsRepository.appConfig.network.customHeaders.entries) {
|
||||
redacted = redacted.replaceAll(
|
||||
RegExp('${RegExp.escape(header.key)}:\\s+${RegExp.escape(header.value)}'),
|
||||
'${header.key}: [HEADER VALUE]',
|
||||
);
|
||||
}
|
||||
return redacted;
|
||||
}
|
||||
|
||||
String _redactError(Object error) {
|
||||
final redacted = _redact(error.toString());
|
||||
final httpError = error is ApiException ? error.innerException : error;
|
||||
if (httpError is! ClientException) {
|
||||
return redacted;
|
||||
}
|
||||
|
||||
// ok_http also prints the host and its ip on their own: "Failed to connect to host/203.0.113.9:443"
|
||||
final url = httpError.uri;
|
||||
final withoutHost = url == null || url.host.isEmpty
|
||||
? redacted
|
||||
: redacted.replaceAll(RegExp('\\b${RegExp.escape(url.host)}\\b'), _urlLabel(url));
|
||||
return withoutHost.replaceAll(_ipAddress, '[IP ADDRESS]');
|
||||
}
|
||||
|
||||
String _urlLabel(Uri? url) {
|
||||
if (url == null) {
|
||||
return '[OTHER URL]';
|
||||
}
|
||||
|
||||
// a server url that is also a switching entry gets the entry's label
|
||||
final network = _settingsRepository.appConfig.network;
|
||||
for (var i = 0; i < network.externalEndpointList.length; i++) {
|
||||
if (_isSameAddress(url, network.externalEndpointList[i])) {
|
||||
return '[EXTERNAL ENDPOINT ${i + 1}]';
|
||||
}
|
||||
}
|
||||
if (_isSameAddress(url, network.localEndpoint)) {
|
||||
return '[LOCAL ENDPOINT]';
|
||||
}
|
||||
if (_isSameAddress(url, _storeService.tryGet(StoreKey.serverUrl)) ||
|
||||
_isSameAddress(url, _storeService.tryGet(StoreKey.serverEndpoint))) {
|
||||
return '[SERVER URL]';
|
||||
}
|
||||
return '[OTHER URL]';
|
||||
}
|
||||
|
||||
bool _isSameAddress(Uri url, String? savedUrl) {
|
||||
final saved = savedUrl == null ? null : Uri.tryParse(savedUrl);
|
||||
return saved != null && saved.scheme == url.scheme && saved.host == url.host && saved.port == url.port;
|
||||
}
|
||||
|
||||
Future<void> clearLogs() async {
|
||||
_flushTimer?.cancel();
|
||||
_flushTimer = null;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:immich_mobile/data/db/main/database.dart';
|
||||
import 'package:immich_mobile/data/db/main/table/local/asset.dart';
|
||||
import 'package:immich_mobile/data/db/main/table/remote/asset.dart';
|
||||
@@ -13,6 +12,7 @@ import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/map.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.drift.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:stream_transform/stream_transform.dart';
|
||||
|
||||
@DriftAccessor()
|
||||
|
||||
@@ -40,6 +40,7 @@ import 'package:immich_mobile/theme/theme_data.dart';
|
||||
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||
import 'package:immich_mobile/utils/cache/widgets_binding.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:immich_mobile/utils/fallback_localizations.dart';
|
||||
import 'package:immich_mobile/utils/licenses.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
import 'package:immich_mobile/wm_executor.dart';
|
||||
@@ -73,6 +74,7 @@ void main() async {
|
||||
),
|
||||
);
|
||||
} catch (error, stack) {
|
||||
Logger('main').severe('Failed to start', error, stack);
|
||||
runApp(BootstrapErrorWidget(error: error.toString(), stack: stack.toString()));
|
||||
}
|
||||
}
|
||||
@@ -279,7 +281,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
||||
title: 'Immich',
|
||||
debugShowCheckedModeBanner: true,
|
||||
scaffoldMessengerKey: scaffoldMessengerKey,
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
localizationsDelegates: [...context.localizationDelegates, ...fallbackLocalizationDelegates],
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: context.locale,
|
||||
themeMode: ref.watch(appConfigProvider.select((config) => config.theme.mode)),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
@@ -105,7 +106,7 @@ class AppLogPage extends HookWidget {
|
||||
style: TextStyle(fontSize: 14.0, color: context.colorScheme.onSurface, fontFamily: "GoogleSansCode"),
|
||||
),
|
||||
subtitle: Text(
|
||||
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.logger}",
|
||||
"at ${DateFormat("HH:mm:ss.SSS", resolvedDateTimeLocale()).format(logMessage.createdAt)} in ${logMessage.logger}",
|
||||
style: TextStyle(fontSize: 12.0, color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
leading: buildLeadingIcon(logMessage.level),
|
||||
|
||||
@@ -22,8 +22,10 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||
import 'package:immich_mobile/theme/color_scheme.dart';
|
||||
import 'package:immich_mobile/theme/theme_data.dart';
|
||||
import 'package:immich_mobile/utils/fallback_localizations.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_logo.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_title_text.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -50,7 +52,7 @@ class BootstrapErrorWidget extends StatelessWidget {
|
||||
builder: (lCtx) => MaterialApp(
|
||||
title: 'Immich',
|
||||
debugShowCheckedModeBanner: true,
|
||||
localizationsDelegates: lCtx.localizationDelegates,
|
||||
localizationsDelegates: [...lCtx.localizationDelegates, ...fallbackLocalizationDelegates],
|
||||
supportedLocales: lCtx.supportedLocales,
|
||||
locale: lCtx.locale,
|
||||
themeMode: ThemeMode.system,
|
||||
@@ -77,9 +79,12 @@ class BootstrapErrorWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
const SafeArea(
|
||||
SafeArea(
|
||||
top: false,
|
||||
child: Padding(padding: EdgeInsets.fromLTRB(24, 16, 24, 16), child: _BottomPanel()),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
|
||||
child: _BottomPanel(startupError: '$error\n\n$stack'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -92,7 +97,9 @@ class BootstrapErrorWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _BottomPanel extends StatefulWidget {
|
||||
const _BottomPanel();
|
||||
final String startupError;
|
||||
|
||||
const _BottomPanel({required this.startupError});
|
||||
|
||||
@override
|
||||
State<_BottomPanel> createState() => _BottomPanelState();
|
||||
@@ -156,8 +163,8 @@ class _BottomPanelState extends State<_BottomPanel> {
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
_ActionLink(
|
||||
icon: Icons.chat_bubble_outline,
|
||||
@@ -172,6 +179,11 @@ class _BottomPanelState extends State<_BottomPanel> {
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
),
|
||||
_ActionLink(
|
||||
icon: Icons.share_outlined,
|
||||
label: context.t.share_logs,
|
||||
onTap: () => ImmichLogger.shareLogs(context, startupError: widget.startupError),
|
||||
),
|
||||
if (!_cleared)
|
||||
_ActionLink(
|
||||
icon: Icons.delete_outline,
|
||||
@@ -287,21 +299,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(
|
||||
ref
|
||||
.read(authProvider.notifier)
|
||||
.setOpenApiServiceEndpoint()
|
||||
.then(logConnectionInfo)
|
||||
.whenComplete(() => resumeSession()),
|
||||
);
|
||||
}
|
||||
|
||||
void logConnectionInfo(String? endpoint) {
|
||||
if (endpoint == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Resuming session at $endpoint");
|
||||
unawaited(ref.read(authProvider.notifier).setOpenApiServiceEndpoint().whenComplete(() => resumeSession()));
|
||||
}
|
||||
|
||||
Future<void> resumeSession() async {
|
||||
@@ -355,7 +353,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
}
|
||||
},
|
||||
onError: (exception) {
|
||||
log.severe('Failed to update auth info with access token: $accessToken');
|
||||
log.severe('Failed to update auth info', exception);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/models/folder/recursive_folder.model.dart';
|
||||
@@ -201,7 +202,7 @@ class FolderContent extends HookConsumerWidget {
|
||||
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: Text(
|
||||
"${asset.exifInfo.fileSize != null ? "${formatBytes(asset.exifInfo.fileSize ?? 0)} • " : ""}${DateFormat.yMMMd().format(asset.createdAt.toLocal())}",
|
||||
"${asset.exifInfo.fileSize != null ? "${formatBytes(asset.exifInfo.fileSize ?? 0)} • " : ""}${DateFormat.yMMMd(resolvedDateTimeLocale()).format(asset.createdAt.toLocal())}",
|
||||
style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
@@ -179,7 +180,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String formatDateTime(DateTime dateTime) => DateFormat.yMMMd(context.locale.toString()).add_Hm().format(dateTime);
|
||||
String formatDateTime(DateTime dateTime) => DateFormat.yMMMd(resolvedDateTimeLocale()).add_Hm().format(dateTime);
|
||||
|
||||
DateTime? getExpiresAtFromPreset(Duration preset) => preset == Duration.zero ? null : DateTime.now().add(preset);
|
||||
|
||||
|
||||
@@ -417,21 +417,57 @@ final sharedWithPartnerProvider = StreamProvider.autoDispose<Iterable<Partner>>(
|
||||
return ref.watch(partnerServiceProvider).search(currentUser.id, .sharedWith);
|
||||
});
|
||||
|
||||
class _QuickAccessItem {
|
||||
final Widget leading;
|
||||
final String title;
|
||||
final VoidCallback onTap;
|
||||
final EdgeInsetsGeometry? contentPadding;
|
||||
|
||||
const _QuickAccessItem({required this.leading, required this.title, required this.onTap, this.contentPadding});
|
||||
}
|
||||
|
||||
class _QuickAccessButtonList extends ConsumerWidget {
|
||||
const _QuickAccessButtonList();
|
||||
|
||||
static const _tileRadius = 20.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final partnerSharedWithAsync = ref.watch(sharedWithPartnerProvider);
|
||||
final partners = partnerSharedWithAsync.valueOrNull ?? [];
|
||||
|
||||
final items = [
|
||||
_QuickAccessItem(
|
||||
leading: const Icon(Icons.folder_outlined, size: 26),
|
||||
title: context.t.folders,
|
||||
onTap: () => context.pushRoute(FolderRoute()),
|
||||
),
|
||||
_QuickAccessItem(
|
||||
leading: const Icon(Icons.lock_outline_rounded, size: 26),
|
||||
title: context.t.locked_folder,
|
||||
onTap: () => context.pushRoute(const LockedFolderRoute()),
|
||||
),
|
||||
_QuickAccessItem(
|
||||
leading: const Icon(Icons.group_outlined, size: 26),
|
||||
title: context.t.partners,
|
||||
onTap: () => context.pushRoute(const PartnerRoute()),
|
||||
),
|
||||
for (final partner in partners)
|
||||
_QuickAccessItem(
|
||||
contentPadding: const EdgeInsets.only(left: 12.0, right: 18.0),
|
||||
leading: PartnerUserAvatar(userId: partner.id, name: partner.name),
|
||||
title: context.t.partner_list_user_photos(user: partner.name),
|
||||
onTap: () => context.pushRoute(PartnerDetailRoute(partner: partner)),
|
||||
),
|
||||
];
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.only(left: 16, top: 12, right: 16, bottom: 32),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(_tileRadius)),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.colorScheme.primary.withAlpha(10),
|
||||
@@ -442,83 +478,33 @@ class _QuickAccessButtonList extends ConsumerWidget {
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: ListView(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
ListTile(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
final isFirst = index == 0;
|
||||
final isLast = index == items.length - 1;
|
||||
return ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: const Radius.circular(20),
|
||||
topRight: const Radius.circular(20),
|
||||
bottomLeft: Radius.circular(partners.isEmpty ? 20 : 0),
|
||||
bottomRight: Radius.circular(partners.isEmpty ? 20 : 0),
|
||||
topLeft: Radius.circular(isFirst ? _tileRadius : 0),
|
||||
topRight: Radius.circular(isFirst ? _tileRadius : 0),
|
||||
bottomLeft: Radius.circular(isLast ? _tileRadius : 0),
|
||||
bottomRight: Radius.circular(isLast ? _tileRadius : 0),
|
||||
),
|
||||
),
|
||||
leading: const Icon(Icons.folder_outlined, size: 26),
|
||||
title: Text(
|
||||
context.t.folders,
|
||||
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
onTap: () => context.pushRoute(FolderRoute()),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.lock_outline_rounded, size: 26),
|
||||
title: Text(
|
||||
context.t.locked_folder,
|
||||
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
onTap: () => context.pushRoute(const LockedFolderRoute()),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.group_outlined, size: 26),
|
||||
title: Text(
|
||||
context.t.partners,
|
||||
style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500),
|
||||
),
|
||||
onTap: () => context.pushRoute(const PartnerRoute()),
|
||||
),
|
||||
_PartnerList(partners: partners.toList()),
|
||||
],
|
||||
contentPadding: item.contentPadding,
|
||||
leading: item.leading,
|
||||
title: Text(item.title, style: context.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w500)),
|
||||
onTap: item.onTap,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PartnerList extends StatelessWidget {
|
||||
const _PartnerList({required this.partners});
|
||||
|
||||
final List<Partner> partners;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: partners.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final partner = partners[index];
|
||||
final isLastItem = index == partners.length - 1;
|
||||
return ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(isLastItem ? 20 : 0),
|
||||
bottomRight: Radius.circular(isLastItem ? 20 : 0),
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.only(left: 12.0, right: 18.0),
|
||||
leading: PartnerUserAvatar(userId: partner.id, name: partner.name),
|
||||
title: Text(
|
||||
context.t.partner_list_user_photos(user: partner.name),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
onTap: () => context.pushRoute(PartnerDetailRoute(partner: partner)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ class TechnicalDetails extends ConsumerWidget {
|
||||
static String _getFileInfo(BaseAsset asset, ExifInfo? exifInfo) {
|
||||
final height = asset.height;
|
||||
final width = asset.width;
|
||||
final resolution = (width != null && height != null) ? "$width x $height" : null;
|
||||
final resolution = (width != null && height != null) ? "$width × $height" : null;
|
||||
final fileSize = exifInfo?.fileSize != null ? formatBytes(exifInfo!.fileSize!) : null;
|
||||
|
||||
return switch ((fileSize, resolution)) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/memory/memory_title.widget.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
@@ -16,7 +17,7 @@ class MemoryBottomInfo extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final df = DateFormat.yMMMMd();
|
||||
final df = DateFormat.yMMMMd(resolvedDateTimeLocale());
|
||||
final fileCreatedDate = asset.createdAt;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/data/store.dart';
|
||||
import 'package:immich_mobile/domain/models/person.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
@@ -61,7 +62,7 @@ class _PersonBirthdayEditFormState extends ConsumerState<PersonBirthdayEditForm>
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||
child: ScrollDatePicker(
|
||||
viewType: datePickerColumnOrder(DateFormat.yMd(context.locale.toLanguageTag()).pattern),
|
||||
viewType: datePickerColumnOrder(DateFormat.yMd(resolvedDateTimeLocale()).pattern),
|
||||
options: DatePickerOptions(
|
||||
backgroundColor: context.colorScheme.surfaceContainerHigh,
|
||||
itemExtent: 50,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
@@ -11,11 +12,11 @@ sealed class DateFilterInputModel {
|
||||
// If date range is less than 24 hours, set the end date to the end of the day
|
||||
final date = asDateTimeRange();
|
||||
if (date.end.difference(date.start).inHours < 24) {
|
||||
return DateFormat.yMMMd().format(date.start.toLocal());
|
||||
return DateFormat.yMMMd(resolvedDateTimeLocale()).format(date.start.toLocal());
|
||||
} else {
|
||||
return context.t.search_filter_date_interval(
|
||||
start: DateFormat.yMMMd().format(date.start.toLocal()),
|
||||
end: DateFormat.yMMMd().format(date.end.toLocal()),
|
||||
start: DateFormat.yMMMd(resolvedDateTimeLocale()).format(date.start.toLocal()),
|
||||
end: DateFormat.yMMMd(resolvedDateTimeLocale()).format(date.end.toLocal()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
@@ -27,15 +28,15 @@ class TimelineHeader extends HookWidget {
|
||||
required this.assetOffset,
|
||||
});
|
||||
|
||||
String _formatMonth(BuildContext context, DateTime date) {
|
||||
String _formatMonth(DateTime date) {
|
||||
final formatter = date.year == DateTime.now().year
|
||||
? DateFormat.MMMM(context.locale.toLanguageTag())
|
||||
: DateFormat.yMMMM(context.locale.toLanguageTag());
|
||||
? DateFormat.MMMM(resolvedDateTimeLocale())
|
||||
: DateFormat.yMMMM(resolvedDateTimeLocale());
|
||||
return formatter.format(date);
|
||||
}
|
||||
|
||||
String _formatDay(BuildContext context, DateTime date) {
|
||||
final formatter = DateFormat.yMMMEd(context.locale.toLanguageTag());
|
||||
String _formatDay(DateTime date) {
|
||||
final formatter = DateFormat.yMMMEd(resolvedDateTimeLocale());
|
||||
return formatter.format(date);
|
||||
}
|
||||
|
||||
@@ -61,7 +62,7 @@ class TimelineHeader extends HookWidget {
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
toBeginningOfSentenceCase(_formatMonth(context, date)),
|
||||
toBeginningOfSentenceCase(_formatMonth(date)),
|
||||
style: context.textTheme.labelLarge?.copyWith(fontSize: 24),
|
||||
),
|
||||
const Spacer(),
|
||||
@@ -72,7 +73,7 @@ class TimelineHeader extends HookWidget {
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
toBeginningOfSentenceCase(_formatDay(context, date)),
|
||||
toBeginningOfSentenceCase(_formatDay(date)),
|
||||
style: context.textTheme.labelLarge?.copyWith(fontSize: 15),
|
||||
),
|
||||
const Spacer(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||
@@ -62,7 +63,7 @@ List<_Segment> _buildSegments({required List<Segment> layoutSegments, required d
|
||||
return [];
|
||||
}
|
||||
|
||||
final formatter = DateFormat.yMMM();
|
||||
final formatter = DateFormat.yMMM(resolvedDateTimeLocale());
|
||||
DateTime? lastDate;
|
||||
double lastOffset = -offsetThreshold;
|
||||
for (final layoutSegment in layoutSegments) {
|
||||
|
||||
@@ -328,6 +328,8 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi
|
||||
final isMultiSelectStatusVisible = !isSelectionMode && isMultiSelectEnabled;
|
||||
final isBottomWidgetVisible =
|
||||
widget.bottomSheet != null && (isMultiSelectStatusVisible || widget.persistentBottomBar);
|
||||
final smoothScrubbing = ref.watch(appConfigProvider.select((config) => config.timeline.smoothScrubbing));
|
||||
final snapToMonth = widget.snapToMonth && !smoothScrubbing;
|
||||
|
||||
return PopScope(
|
||||
canPop: !isMultiSelectEnabled,
|
||||
@@ -409,7 +411,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi
|
||||
final Widget timeline;
|
||||
if (widget.withScrubber) {
|
||||
timeline = Scrubber(
|
||||
snapToMonth: widget.snapToMonth,
|
||||
snapToMonth: snapToMonth,
|
||||
layoutSegments: segments,
|
||||
timelineHeight: maxHeight,
|
||||
topPadding: topPadding,
|
||||
|
||||
@@ -84,8 +84,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
// Needs to be logged in
|
||||
if (isAuthenticated) {
|
||||
// switch endpoint if needed
|
||||
final endpoint = await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint();
|
||||
_log.info("Using server URL: $endpoint");
|
||||
await _ref.read(authProvider.notifier).setOpenApiServiceEndpoint();
|
||||
|
||||
await _ref.read(serverInfoProvider.notifier).getServerVersion();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||
import 'package:immich_mobile/services/share_intent_service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
final shareIntentUploadProvider = StateNotifierProvider<ShareIntentUploadStateNotifier, List<ShareIntentAttachment>>(
|
||||
@@ -21,7 +20,6 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
|
||||
final AppRouter router;
|
||||
final ForegroundUploadService _foregroundUploadService;
|
||||
final ShareIntentService _shareIntentService;
|
||||
final Logger _logger = Logger('ShareIntentUploadStateNotifier');
|
||||
|
||||
ShareIntentUploadStateNotifier(this.router, this._foregroundUploadService, this._shareIntentService) : super([]);
|
||||
|
||||
@@ -74,8 +72,7 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
|
||||
onSuccess: (fileId, _) {
|
||||
_updateStatus(fileId, UploadStatus.complete, progress: 1.0);
|
||||
},
|
||||
onError: (fileId, errorMessage) {
|
||||
_logger.warning("Upload failed for file: $fileId, error: $errorMessage");
|
||||
onError: (fileId, _) {
|
||||
_updateStatus(fileId, UploadStatus.failed);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -221,8 +221,6 @@ class BackupNotifier extends StateNotifier<BackupState> {
|
||||
}
|
||||
|
||||
void _handleForegroundBackupError(String localAssetId, String errorMessage) {
|
||||
_logger.severe("Upload failed for $localAssetId: $errorMessage");
|
||||
|
||||
final currentItem = state.uploadItems[localAssetId];
|
||||
if (currentItem != null) {
|
||||
state = state.copyWith(
|
||||
|
||||
@@ -88,7 +88,7 @@ class UploadRepository {
|
||||
} on RequestAbortedException {
|
||||
rethrow;
|
||||
} on ClientException catch (error) {
|
||||
logger.warning("Upload $logContext failed before a response, resending once: $error");
|
||||
logger.warning("Upload $logContext failed before a response, resending once", error);
|
||||
response = await client.send(buildRequest());
|
||||
}
|
||||
|
||||
@@ -124,8 +124,8 @@ class UploadRepository {
|
||||
logger.warning("Upload $logContext was cancelled");
|
||||
return UploadResult.cancelled();
|
||||
} catch (error, stackTrace) {
|
||||
logger.warning("Error uploading $logContext: $error: $stackTrace");
|
||||
return UploadResult.error(errorMessage: error.toString());
|
||||
logger.warning("Error uploading $logContext", error, stackTrace);
|
||||
return UploadResult(isSuccess: false, isCancelled: false, errorMessage: error.toString(), error: error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,6 +166,7 @@ class UploadResult {
|
||||
final String? remoteAssetId;
|
||||
final String? errorMessage;
|
||||
final int? statusCode;
|
||||
final Object? error;
|
||||
|
||||
const UploadResult({
|
||||
required this.isSuccess,
|
||||
@@ -173,6 +174,7 @@ class UploadResult {
|
||||
this.remoteAssetId,
|
||||
this.errorMessage,
|
||||
this.statusCode,
|
||||
this.error,
|
||||
});
|
||||
|
||||
factory UploadResult.success({required String remoteAssetId}) {
|
||||
|
||||
@@ -162,6 +162,7 @@ class AuthService {
|
||||
final localEndpoint = _authRepository.getLocalEndpoint();
|
||||
if (localEndpoint != null) {
|
||||
await _apiService.resolveAndSetEndpoint(localEndpoint);
|
||||
_log.info("Using local endpoint");
|
||||
return localEndpoint;
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
@@ -181,9 +182,11 @@ class AuthService {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (final endpoint in endpointList) {
|
||||
for (var i = 0; i < endpointList.length; i++) {
|
||||
try {
|
||||
return await _apiService.resolveAndSetEndpoint(endpoint.url);
|
||||
final url = await _apiService.resolveAndSetEndpoint(endpointList[i].url);
|
||||
_log.info("Using external endpoint ${i + 1}");
|
||||
return url;
|
||||
} on ApiException catch (error) {
|
||||
_log.severe("Cannot resolve endpoint", error);
|
||||
continue;
|
||||
|
||||
@@ -182,6 +182,7 @@ class ForegroundUploadService {
|
||||
if (result.isSuccess) {
|
||||
onSuccess?.call(fileId, result.remoteAssetId!);
|
||||
} else if (!result.isCancelled && result.errorMessage != null) {
|
||||
_logger.warning("Error(${result.statusCode}) uploading $fileId", result.error ?? result.errorMessage);
|
||||
onError?.call(fileId, result.errorMessage!);
|
||||
}
|
||||
},
|
||||
@@ -395,7 +396,8 @@ class ForegroundUploadService {
|
||||
} else if (result.errorMessage != null) {
|
||||
_logger.severe(
|
||||
() =>
|
||||
"Error(${result.statusCode}) uploading ${asset.localId} | $originalFileName | Created on ${asset.createdAt} | ${result.errorMessage}",
|
||||
"Error(${result.statusCode}) uploading ${asset.localId} | $originalFileName | Created on ${asset.createdAt}",
|
||||
result.error ?? result.errorMessage,
|
||||
);
|
||||
|
||||
callbacks.onError?.call(asset.localId!, result.errorMessage!);
|
||||
|
||||
@@ -2,7 +2,11 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:immich_mobile/data/db/logger/database.dart';
|
||||
import 'package:immich_mobile/data/db/main/database.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
@@ -15,15 +19,37 @@ import 'package:share_plus/share_plus.dart';
|
||||
/// Logs can be shared by calling the `shareLogs` method, which will open a share dialog
|
||||
/// and generate a csv file.
|
||||
abstract final class ImmichLogger {
|
||||
static Future<void> shareLogs(BuildContext context) async {
|
||||
// the startup error screen can show before the log service starts, and then the startup error was not logged
|
||||
static Future<List<LogMessage>> _read(String? startupError) async {
|
||||
try {
|
||||
return await LogService.I.getMessages();
|
||||
} on LoggerUnInitializedException {
|
||||
final connection = await openSqliteConnection(name: 'immich_logs');
|
||||
final logs = DriftLogger.sqlite(connection);
|
||||
try {
|
||||
return [
|
||||
// if the log service had started, main would have logged this already
|
||||
if (startupError != null)
|
||||
LogMessage(message: startupError, level: LogLevel.severe, createdAt: DateTime.now()),
|
||||
...await LogRepository(logs).getAll(),
|
||||
];
|
||||
} finally {
|
||||
await logs.close();
|
||||
await connection.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> shareLogs(BuildContext context, {String? startupError}) async {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final dateTime = DateTime.now().toIso8601String();
|
||||
final filePath = '${tempDir.path}/Immich_log_$dateTime.log';
|
||||
final safeTimestamp = dateTime.replaceAll(':', '-');
|
||||
final filePath = '${tempDir.path}/Immich_log_$safeTimestamp.log';
|
||||
final logFile = await File(filePath).create();
|
||||
final io = logFile.openWrite();
|
||||
try {
|
||||
// Write messages
|
||||
for (final m in await LogService.I.getMessages()) {
|
||||
for (final m in await _read(startupError)) {
|
||||
final created = m.createdAt;
|
||||
final level = m.level.name.padRight(8);
|
||||
final logger = (m.logger ?? "<UNKNOWN_LOGGER>").padRight(20);
|
||||
|
||||
@@ -26,7 +26,7 @@ class OAuthService {
|
||||
);
|
||||
|
||||
final authUrl = dto?.url;
|
||||
log.info('Received Authorization URL: $authUrl');
|
||||
log.info('Received Authorization URL');
|
||||
|
||||
return authUrl;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ class OAuthService {
|
||||
Future<LoginResponseDto?> oAuthLogin(String oauthUrl, String state, String codeVerifier) async {
|
||||
String result = await FlutterWebAuth2.authenticate(url: oauthUrl, callbackUrlScheme: callbackUrlScheme);
|
||||
|
||||
log.info('Received OAuth callback: $result');
|
||||
log.info('Received OAuth callback');
|
||||
|
||||
if (result.startsWith('app.immich:/oauth-callback')) {
|
||||
result = result.replaceAll('app.immich:/oauth-callback', 'app.immich:///oauth-callback');
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/data/data_controller.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||
@@ -72,6 +73,7 @@ abstract final class Bootstrap {
|
||||
await LogService.init(
|
||||
logRepository: LogRepository(dataController.logDb),
|
||||
settingsRepository: settingsRepo,
|
||||
storeService: StoreService.I,
|
||||
shouldBuffer: shouldBufferLogs,
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// Flutter ships no Material or Cupertino strings for some of our languages, use English for those
|
||||
const List<LocalizationsDelegate> fallbackLocalizationDelegates = [
|
||||
_FallbackLocalizationsDelegate<MaterialLocalizations>(DefaultMaterialLocalizations.delegate),
|
||||
_FallbackLocalizationsDelegate<CupertinoLocalizations>(DefaultCupertinoLocalizations.delegate),
|
||||
];
|
||||
|
||||
class _FallbackLocalizationsDelegate<T> extends LocalizationsDelegate<T> {
|
||||
final LocalizationsDelegate<T> delegate;
|
||||
|
||||
const _FallbackLocalizationsDelegate(this.delegate);
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) => true;
|
||||
|
||||
@override
|
||||
Future<T> load(Locale locale) => delegate.load(const Locale('en'));
|
||||
|
||||
@override
|
||||
bool shouldReload(_FallbackLocalizationsDelegate<T> old) => false;
|
||||
}
|
||||
@@ -37,6 +37,7 @@ final Map<String, Map<String, Object?>> openApiPatches = {
|
||||
'ffmpeg': {
|
||||
'realtime': {'enabled': false},
|
||||
},
|
||||
'passkey': {'enabled': false},
|
||||
},
|
||||
'ServerConfigDto': {
|
||||
'mapLightStyleUrl': 'https://tiles.immich.cloud/v1/style/light.json',
|
||||
@@ -55,12 +56,16 @@ final Map<String, Map<String, Object?>> openApiPatches = {
|
||||
'PeopleResponse': {'updateStrategy': 'everyone'},
|
||||
'PersonResponseDto': {'otherPeople': const [], 'sharedBy': const [], 'sharedWith': const []},
|
||||
'ServerVersionResponseDto': {'featureLevel': 0},
|
||||
'WorkflowResponseDto': {'logging': false},
|
||||
'AdminConfigDto': {
|
||||
'passkey': {'enabled': false, 'domain': null, 'additionalDomains': const []},
|
||||
},
|
||||
'UserConfigDto': {
|
||||
'notifications': {
|
||||
'smtp': {'enabled': false},
|
||||
},
|
||||
'passkey': {'enabled': false},
|
||||
},
|
||||
'WorkflowResponseDto': {'logging': false},
|
||||
};
|
||||
|
||||
// ignore: unused-code
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/widgets/common/dropdown_search_menu.dart';
|
||||
@@ -108,7 +109,7 @@ class _DateTimePicker extends HookWidget {
|
||||
}
|
||||
|
||||
void popWithDateTime() {
|
||||
final formattedDateTime = DateFormat("yyyy-MM-dd'T'HH:mm:ss").format(date.value);
|
||||
final formattedDateTime = DateFormat("yyyy-MM-dd'T'HH:mm:ss", 'en').format(date.value);
|
||||
final dtWithOffset =
|
||||
formattedDateTime + Duration(milliseconds: tzOffset.value.offsetInMilliseconds).formatAsOffset();
|
||||
context.pop(dtWithOffset);
|
||||
@@ -149,7 +150,10 @@ class _DateTimePicker extends HookWidget {
|
||||
1,
|
||||
),
|
||||
trailing: Icon(Icons.edit_outlined, size: 18, color: context.primaryColor),
|
||||
title: Text(DateFormat("dd-MM-yyyy hh:mm a").format(date.value), style: context.textTheme.bodyMedium),
|
||||
title: Text(
|
||||
DateFormat("dd-MM-yyyy hh:mm a", resolvedDateTimeLocale()).format(date.value),
|
||||
style: context.textTheme.bodyMedium,
|
||||
),
|
||||
onTap: pickDate,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
@@ -36,64 +36,63 @@ class _MesmerizingSliverAppBarState extends ConsumerState<MesmerizingSliverAppBa
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||
Widget hideWhenSelecting(Widget child) => IgnorePointer(
|
||||
ignoring: isMultiSelectEnabled,
|
||||
child: AnimatedOpacity(duration: Durations.medium1, opacity: isMultiSelectEnabled ? 0 : 1, child: child),
|
||||
);
|
||||
|
||||
return isMultiSelectEnabled
|
||||
? SliverToBoxAdapter(
|
||||
child: switch (_scrollProgress) {
|
||||
< 0.8 => const SizedBox(height: 120),
|
||||
_ => const SizedBox(height: 352),
|
||||
},
|
||||
)
|
||||
: SliverAppBar(
|
||||
expandedHeight: 300.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
snap: false,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(
|
||||
Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back,
|
||||
color: Color.lerp(Colors.white, context.primaryColor, _scrollProgress),
|
||||
shadows: [
|
||||
_scrollProgress < 0.95
|
||||
? Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5))
|
||||
: const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
flexibleSpace: Builder(
|
||||
builder: (context) {
|
||||
final settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
|
||||
final scrollProgress = _calculateScrollProgress(settings);
|
||||
return SliverAppBar(
|
||||
expandedHeight: 300.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
snap: false,
|
||||
elevation: 0,
|
||||
leading: hideWhenSelecting(
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back,
|
||||
color: Color.lerp(Colors.white, context.primaryColor, _scrollProgress),
|
||||
shadows: [
|
||||
_scrollProgress < 0.95
|
||||
? Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5))
|
||||
: const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
flexibleSpace: Builder(
|
||||
builder: (context) {
|
||||
final settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
|
||||
final scrollProgress = _calculateScrollProgress(settings);
|
||||
|
||||
// Update scroll progress for the leading button
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _scrollProgress != scrollProgress) {
|
||||
setState(() {
|
||||
_scrollProgress = scrollProgress;
|
||||
});
|
||||
}
|
||||
});
|
||||
// Update scroll progress for the leading button
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _scrollProgress != scrollProgress) {
|
||||
setState(() {
|
||||
_scrollProgress = scrollProgress;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: scrollProgress > 0.95
|
||||
? Text(
|
||||
widget.title,
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
background: _ExpandedBackground(scrollProgress: scrollProgress, title: widget.title),
|
||||
);
|
||||
},
|
||||
return FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: scrollProgress > 0.95
|
||||
? Text(
|
||||
widget.title,
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
background: _ExpandedBackground(scrollProgress: scrollProgress, title: widget.title),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/person.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/sliver_app_bar/item_count_text.widget.dart';
|
||||
@@ -53,6 +54,10 @@ class _MesmerizingSliverAppBarState extends ConsumerState<PersonSliverAppBar> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||
Widget hideWhenSelecting(Widget child) => IgnorePointer(
|
||||
ignoring: isMultiSelectEnabled,
|
||||
child: AnimatedOpacity(duration: Durations.medium1, opacity: isMultiSelectEnabled ? 0 : 1, child: child),
|
||||
);
|
||||
final Color? actionIconColor = Color.lerp(Colors.white, context.primaryColor, _scrollProgress);
|
||||
final List<Shadow> actionIconShadows = [
|
||||
if (_scrollProgress < 0.95)
|
||||
@@ -61,74 +66,74 @@ class _MesmerizingSliverAppBarState extends ConsumerState<PersonSliverAppBar> {
|
||||
const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
||||
];
|
||||
|
||||
return isMultiSelectEnabled
|
||||
? SliverToBoxAdapter(
|
||||
child: switch (_scrollProgress) {
|
||||
< 0.8 => const SizedBox(height: 120),
|
||||
_ => const SizedBox(height: 352),
|
||||
},
|
||||
)
|
||||
: SliverAppBar(
|
||||
expandedHeight: 300.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
snap: false,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: Icon(
|
||||
Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back,
|
||||
color: Color.lerp(Colors.white, context.primaryColor, _scrollProgress),
|
||||
shadows: [
|
||||
_scrollProgress < 0.95
|
||||
? Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5))
|
||||
: const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.more_vert, color: actionIconColor, shadows: actionIconShadows),
|
||||
onPressed: widget.onShowOptions,
|
||||
),
|
||||
return SliverAppBar(
|
||||
expandedHeight: 300.0,
|
||||
floating: false,
|
||||
pinned: true,
|
||||
snap: false,
|
||||
elevation: 0,
|
||||
leading: hideWhenSelecting(
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Platform.isIOS ? Icons.arrow_back_ios_new_rounded : Icons.arrow_back,
|
||||
color: Color.lerp(Colors.white, context.primaryColor, _scrollProgress),
|
||||
shadows: [
|
||||
_scrollProgress < 0.95
|
||||
? Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5))
|
||||
: const Shadow(offset: Offset(0, 2), blurRadius: 0, color: Colors.transparent),
|
||||
],
|
||||
flexibleSpace: Builder(
|
||||
builder: (context) {
|
||||
final settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
|
||||
final scrollProgress = _calculateScrollProgress(settings);
|
||||
),
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
hideWhenSelecting(
|
||||
IconButton(
|
||||
icon: Icon(Icons.more_vert, color: actionIconColor, shadows: actionIconShadows),
|
||||
onPressed: widget.onShowOptions,
|
||||
),
|
||||
),
|
||||
],
|
||||
flexibleSpace: Builder(
|
||||
builder: (context) {
|
||||
final settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
|
||||
final scrollProgress = _calculateScrollProgress(settings);
|
||||
|
||||
// Update scroll progress for the leading button
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _scrollProgress != scrollProgress) {
|
||||
setState(() {
|
||||
_scrollProgress = scrollProgress;
|
||||
});
|
||||
}
|
||||
});
|
||||
// Update scroll progress for the leading button
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _scrollProgress != scrollProgress) {
|
||||
setState(() {
|
||||
_scrollProgress = scrollProgress;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: scrollProgress > 0.95
|
||||
? Text(
|
||||
widget.person.name,
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
background: _ExpandedBackground(
|
||||
scrollProgress: scrollProgress,
|
||||
person: widget.person,
|
||||
onNameTap: widget.onNameTap,
|
||||
onBirthdayTap: widget.onBirthdayTap,
|
||||
),
|
||||
);
|
||||
},
|
||||
return FlexibleSpaceBar(
|
||||
centerTitle: true,
|
||||
title: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: scrollProgress > 0.95
|
||||
? Text(
|
||||
widget.person.name,
|
||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.w600, fontSize: 18),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
background: IgnorePointer(
|
||||
ignoring: isMultiSelectEnabled,
|
||||
child: _ExpandedBackground(
|
||||
scrollProgress: scrollProgress,
|
||||
person: widget.person,
|
||||
onNameTap: widget.onNameTap,
|
||||
onBirthdayTap: widget.onBirthdayTap,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +295,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S
|
||||
|
||||
if (widget.person.birthDate != null)
|
||||
Text(
|
||||
"${DateFormat.yMMMd(context.locale.toString()).format(widget.person.birthDate!)} (${formatAge(widget.person.birthDate!, DateTime.now())})",
|
||||
"${DateFormat.yMMMd(resolvedDateTimeLocale()).format(widget.person.birthDate!)} (${formatAge(widget.person.birthDate!, DateTime.now())})",
|
||||
style: context.textTheme.labelLarge?.copyWith(
|
||||
color: Colors.white,
|
||||
height: 1.2,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/time_range.model.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/utils/option.dart';
|
||||
|
||||
@@ -19,7 +20,7 @@ class MapTimeRange extends StatelessWidget {
|
||||
title: Text(context.t.date_after),
|
||||
subtitle: Text(
|
||||
timeRange.from != null
|
||||
? DateFormat.yMMMd(context.locale.toLanguageTag()).add_jm().format(timeRange.from!)
|
||||
? DateFormat.yMMMd(resolvedDateTimeLocale()).add_jm().format(timeRange.from!)
|
||||
: context.t.not_set,
|
||||
),
|
||||
trailing: timeRange.from != null
|
||||
@@ -45,7 +46,7 @@ class MapTimeRange extends StatelessWidget {
|
||||
title: Text(context.t.date_before),
|
||||
subtitle: Text(
|
||||
timeRange.to != null
|
||||
? DateFormat.yMMMd(context.locale.toLanguageTag()).add_jm().format(timeRange.to!)
|
||||
? DateFormat.yMMMd(resolvedDateTimeLocale()).add_jm().format(timeRange.to!)
|
||||
: context.t.not_set,
|
||||
),
|
||||
trailing: timeRange.to != null
|
||||
|
||||
@@ -16,6 +16,7 @@ class AssetListSettings extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final storageIndicator = useValueNotifier(ref.watch(appConfigProvider.select((s) => s.timeline.storageIndicator)));
|
||||
final smoothScrubbing = useValueNotifier(ref.watch(appConfigProvider.select((s) => s.timeline.smoothScrubbing)));
|
||||
|
||||
final assetListSetting = [
|
||||
SettingsSwitchListTile(
|
||||
@@ -27,6 +28,16 @@ class AssetListSettings extends HookConsumerWidget {
|
||||
ref.invalidate(settingsProvider);
|
||||
},
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: smoothScrubbing,
|
||||
title: context.t.theme_setting_asset_list_smooth_scrubbing_title,
|
||||
subtitle: context.t.theme_setting_asset_list_smooth_scrubbing_subtitle,
|
||||
onChanged: (value) {
|
||||
unawaited(ref.read(settingsProvider).write(.timelineSmoothScrubbing, value));
|
||||
ref.invalidate(appSettingsServiceProvider);
|
||||
ref.invalidate(settingsProvider);
|
||||
},
|
||||
),
|
||||
const LayoutSettings(),
|
||||
const GroupSettings(),
|
||||
];
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:immich_mobile/providers/cleanup.provider.dart';
|
||||
@@ -423,7 +424,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
||||
),
|
||||
subtitle: hasDate
|
||||
? Text(
|
||||
DateFormat.yMMMd().format(state.selectedDate!),
|
||||
DateFormat.yMMMd(resolvedDateTimeLocale()).format(state.selectedDate!),
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -628,7 +629,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
|
||||
Text(
|
||||
context.t.cleanup_step4_summary(
|
||||
count: state.assetsToDelete.length,
|
||||
date: DateFormat.yMMMd().format(state.selectedDate!),
|
||||
date: DateFormat.yMMMd(resolvedDateTimeLocale()).format(state.selectedDate!),
|
||||
),
|
||||
style: context.textTheme.labelLarge?.copyWith(fontSize: 15),
|
||||
),
|
||||
@@ -687,7 +688,10 @@ class _DeleteConfirmationDialog extends StatelessWidget {
|
||||
return AlertDialog(
|
||||
title: Text(context.t.cleanup_confirm_prompt_title),
|
||||
content: Text(
|
||||
context.t.cleanup_confirm_description(count: assetCount, date: DateFormat.yMMMd().format(cutoffDate)),
|
||||
context.t.cleanup_confirm_description(
|
||||
count: assetCount,
|
||||
date: DateFormat.yMMMd(resolvedDateTimeLocale()).format(cutoffDate),
|
||||
),
|
||||
style: context.textTheme.labelLarge?.copyWith(fontSize: 15),
|
||||
),
|
||||
actions: [
|
||||
|
||||
@@ -139,6 +139,7 @@ depends = ["//:open-api-dart"]
|
||||
run = [
|
||||
# This receives passed `--enforce-lockfile` args
|
||||
"flutter pub get",
|
||||
{ task = "install:build-runner-patch" },
|
||||
{ task = "install:ios" },
|
||||
]
|
||||
|
||||
@@ -198,6 +199,11 @@ fi
|
||||
cd ios && pod install
|
||||
'''
|
||||
|
||||
[tasks."install:build-runner-patch"]
|
||||
description = "Patch build_runner to prevent crashes when sources are deleted"
|
||||
hide = true
|
||||
run = "node ./scripts/build_runner_patch.mjs"
|
||||
|
||||
[tasks."analyze:dart"]
|
||||
description = "Run Dart analysis"
|
||||
hide = true
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 3.3.3-local.4+3030403
|
||||
version: 3.3.5-local.7+3030500
|
||||
|
||||
environment:
|
||||
sdk: '>=3.12.0 <4.0.0'
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Prevents build_runner crashing when a source file is deleted from a path dependency, which commonly occurs to `generated/openapi` on branch switch
|
||||
//
|
||||
// Fixed upstream in 2.15.3 (https://github.com/dart-lang/build/pull/5022), but we can't take that dependency due to Dart analyzer being locked
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
|
||||
const packageConfig = new URL('../.dart_tool/package_config.json', import.meta.url);
|
||||
|
||||
const config = JSON.parse(readFileSync(packageConfig, 'utf8'));
|
||||
const buildRunner = new URL(config.packages.find(({ name }) => name === 'build_runner').rootUri);
|
||||
|
||||
const file = new URL(`${buildRunner}/lib/src/build_plan/build_plan.dart`);
|
||||
|
||||
const buggy = 'for (final id in deletedOutputs) {';
|
||||
const fixed = 'for (final id in deletedOutputs.difference(buildInputs.deletedSources.build().asSet())) {';
|
||||
|
||||
const source = readFileSync(file, 'utf8');
|
||||
if (source.includes(buggy)) {
|
||||
writeFileSync(file, source.replace(buggy, fixed));
|
||||
console.log('build_runner patch: stopped deleting removed sources from path dependencies');
|
||||
}
|
||||
@@ -1,13 +1,23 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' show ClientException;
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/data/db/main/database.dart';
|
||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/network_config.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:openapi/api.dart' show ApiException;
|
||||
|
||||
import '../../infrastructure/repository.mock.dart';
|
||||
import '../../test_utils.dart';
|
||||
@@ -30,6 +40,16 @@ void main() {
|
||||
late LogService sut;
|
||||
late LogRepository mockLogRepo;
|
||||
late MockSettingsRepository mockSettingsRepository;
|
||||
late Drift db;
|
||||
|
||||
setUpAll(() async {
|
||||
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
await StoreService.init(storeRepository: StoreRepository(db), listenUpdates: false);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
mockLogRepo = MockLogRepository();
|
||||
@@ -45,11 +65,16 @@ void main() {
|
||||
when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true);
|
||||
when(() => mockLogRepo.insertAll(any())).thenAnswer((_) async => true);
|
||||
|
||||
sut = await LogService.create(logRepository: mockLogRepo, settingsRepository: mockSettingsRepository);
|
||||
sut = await LogService.create(
|
||||
logRepository: mockLogRepo,
|
||||
settingsRepository: mockSettingsRepository,
|
||||
storeService: StoreService.I,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await sut.dispose();
|
||||
await StoreService.I.clear();
|
||||
});
|
||||
|
||||
group("Log Service Init:", () {
|
||||
@@ -87,6 +112,7 @@ void main() {
|
||||
sut = await LogService.create(
|
||||
logRepository: mockLogRepo,
|
||||
settingsRepository: mockSettingsRepository,
|
||||
storeService: StoreService.I,
|
||||
shouldBuffer: true,
|
||||
);
|
||||
|
||||
@@ -105,6 +131,7 @@ void main() {
|
||||
sut = await LogService.create(
|
||||
logRepository: mockLogRepo,
|
||||
settingsRepository: mockSettingsRepository,
|
||||
storeService: StoreService.I,
|
||||
shouldBuffer: true,
|
||||
);
|
||||
|
||||
@@ -126,6 +153,7 @@ void main() {
|
||||
sut = await LogService.create(
|
||||
logRepository: mockLogRepo,
|
||||
settingsRepository: mockSettingsRepository,
|
||||
storeService: StoreService.I,
|
||||
shouldBuffer: false,
|
||||
);
|
||||
|
||||
@@ -160,6 +188,7 @@ void main() {
|
||||
sut = await LogService.create(
|
||||
logRepository: mockLogRepo,
|
||||
settingsRepository: mockSettingsRepository,
|
||||
storeService: StoreService.I,
|
||||
shouldBuffer: true,
|
||||
);
|
||||
|
||||
@@ -176,4 +205,144 @@ void main() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group("Log Service Redaction:", () {
|
||||
const accessToken = 'Kq3Zr8WbN2xYp5aLm7cT9vQ4sD6fH1jE0gU2iO5wA8y';
|
||||
|
||||
test('Replaces urls, the access token and header values when a line is logged', () async {
|
||||
await StoreService.I.put(StoreKey.serverUrl, 'https://immich.example.com');
|
||||
await StoreService.I.put(StoreKey.serverEndpoint, 'https://api.example.com/api');
|
||||
await StoreService.I.put(StoreKey.accessToken, accessToken);
|
||||
when(() => mockSettingsRepository.appConfig).thenReturn(
|
||||
const AppConfig(
|
||||
network: NetworkConfig(
|
||||
localEndpoint: 'http://192.168.1.5:2283',
|
||||
externalEndpointList: ['https://immich.example.com', 'http://nas.lan:2283'],
|
||||
customHeaders: {'X-Proxy-Key': 'cf-secret-abcdef0123', 'X-Debug': '1'},
|
||||
),
|
||||
),
|
||||
);
|
||||
const rows = {
|
||||
'Resuming session at https://immich.example.com/api': 'Resuming session at [EXTERNAL ENDPOINT 1]/api',
|
||||
'GET http://nas.lan:2283/api/server/ping': 'GET [EXTERNAL ENDPOINT 2]/api/server/ping',
|
||||
'GET http://192.168.1.5:2283/api/server/ping': 'GET [LOCAL ENDPOINT]/api/server/ping',
|
||||
'Using server URL: https://api.example.com/api': 'Using server URL: [SERVER URL]/api',
|
||||
'Received Authorization URL: https://auth.example.org/authorize':
|
||||
'Received Authorization URL: [OTHER URL]/authorize',
|
||||
'Causing statement: INSERT INTO "store_entity", parameters: 11, $accessToken':
|
||||
'Causing statement: INSERT INTO "store_entity", parameters: 11, [ACCESS TOKEN]',
|
||||
'Could not enqueue task DownloadTask{headers: {X-Proxy-Key: cf-secret-abcdef0123, X-Debug: 1}, retries: 1}':
|
||||
'Could not enqueue task DownloadTask{headers: {X-Proxy-Key: [HEADER VALUE], X-Debug: [HEADER VALUE]}, retries: 1}',
|
||||
'X-Proxy-Key:\tcf-secret-abcdef0123': 'X-Proxy-Key: [HEADER VALUE]',
|
||||
'Uploaded 1 assets in 1.2s': 'Uploaded 1 assets in 1.2s',
|
||||
};
|
||||
for (final row in rows.keys) {
|
||||
Logger('Test').info(row);
|
||||
}
|
||||
await sut.flush();
|
||||
|
||||
final stored = verify(() => mockLogRepo.insertAll(captureAny())).captured.single as List<LogMessage>;
|
||||
expect(stored.map((m) => m.message), rows.values);
|
||||
});
|
||||
|
||||
test('Hides the host and ip in http client errors', () async {
|
||||
when(
|
||||
() => mockSettingsRepository.appConfig,
|
||||
).thenReturn(const AppConfig(network: NetworkConfig(externalEndpointList: ['http://immich:2283'])));
|
||||
final connectError = ClientException(
|
||||
'java.net.ConnectException: Failed to connect to immich/192.168.1.5:2283',
|
||||
Uri.parse('http://immich:2283/api/server/ping'),
|
||||
);
|
||||
final unknownHostError = ClientException(
|
||||
'java.net.UnknownHostException: Unable to resolve host "typo.example.org": No address associated with hostname',
|
||||
Uri.parse('https://alice:secret@typo.example.org/api/users/me'),
|
||||
);
|
||||
|
||||
Logger('Test').severe(
|
||||
'Error while checking server availability',
|
||||
ApiException.withInner(
|
||||
400,
|
||||
'HTTP connection failed: GET /server/ping',
|
||||
connectError,
|
||||
StackTrace.fromString('#0 ApiService._isEndpointAvailable (package:immich_mobile/services/api.service.dart)'),
|
||||
),
|
||||
);
|
||||
Logger('Test').severe('Error validating auxiliary endpoint', unknownHostError);
|
||||
await sut.flush();
|
||||
|
||||
final stored = verify(() => mockLogRepo.insertAll(captureAny())).captured.single as List<LogMessage>;
|
||||
expect(
|
||||
stored.first.error,
|
||||
'ApiException 400: HTTP connection failed: GET /server/ping (Inner exception: ClientException: '
|
||||
'java.net.ConnectException: Failed to connect to [EXTERNAL ENDPOINT 1]/[IP ADDRESS]:2283, '
|
||||
'uri=[EXTERNAL ENDPOINT 1]/api/server/ping)\n\n'
|
||||
'#0 ApiService._isEndpointAvailable (package:immich_mobile/services/api.service.dart)',
|
||||
);
|
||||
expect(
|
||||
stored.last.error,
|
||||
'ClientException: java.net.UnknownHostException: Unable to resolve host "[OTHER URL]": '
|
||||
'No address associated with hostname, uri=[OTHER URL]/api/users/me',
|
||||
);
|
||||
});
|
||||
|
||||
test('Hides ipv6 addresses in http client errors', () async {
|
||||
const rows = {
|
||||
'java.net.ConnectException: Failed to connect to immich.example.com/[2001:db8::5]:443':
|
||||
'java.net.ConnectException: Failed to connect to [OTHER URL]/[IP ADDRESS]:443',
|
||||
'java.net.ConnectException: Failed to connect to immich.example.com/2001:db8::5:443':
|
||||
'java.net.ConnectException: Failed to connect to [OTHER URL]/[IP ADDRESS]',
|
||||
'java.net.SocketTimeoutException: failed to connect to immich.example.com/2001:db8::5 (port 443) '
|
||||
'from /2001:db8:1234:5678:9abc:def0:1234:5678 (port 41234) after 30000ms':
|
||||
'java.net.SocketTimeoutException: failed to connect to [OTHER URL]/[IP ADDRESS] (port 443) '
|
||||
'from /[IP ADDRESS] (port 41234) after 30000ms',
|
||||
'java.net.ConnectException: Failed to connect to /[fe80::5%wlan0]:2283':
|
||||
'java.net.ConnectException: Failed to connect to /[IP ADDRESS]:2283',
|
||||
'at 12:34:56': 'at 12:34:56',
|
||||
'0:00:01.500000': '0:00:01.500000',
|
||||
'deadbeef': 'deadbeef',
|
||||
'#0 ApiClient.invokeAPI (package:openapi/api_client.dart:131:7)':
|
||||
'#0 ApiClient.invokeAPI (package:openapi/api_client.dart:131:7)',
|
||||
};
|
||||
for (final row in rows.keys) {
|
||||
Logger('Test').severe('Upload error', ClientException(row, Uri.parse('https://immich.example.com/api/assets')));
|
||||
}
|
||||
await sut.flush();
|
||||
|
||||
final stored = verify(() => mockLogRepo.insertAll(captureAny())).captured.single as List<LogMessage>;
|
||||
expect(stored.map((m) => m.error), [
|
||||
for (final row in rows.values) 'ClientException: $row, uri=[OTHER URL]/api/assets',
|
||||
]);
|
||||
});
|
||||
|
||||
test('Keeps the labels of a line logged before a logout', () async {
|
||||
final settings = await SettingsRepository.ensureInitialized(db);
|
||||
addTearDown(SettingsRepository.reset);
|
||||
await settings.write(SettingsKey.networkExternalEndpointList, ['https://immich.example.com']);
|
||||
await StoreService.I.put(StoreKey.accessToken, accessToken);
|
||||
await sut.dispose();
|
||||
sut = await LogService.create(
|
||||
logRepository: mockLogRepo,
|
||||
settingsRepository: settings,
|
||||
storeService: StoreService.I,
|
||||
);
|
||||
|
||||
Logger('Test').info('Resuming session at https://immich.example.com/api with $accessToken');
|
||||
// the store and settings changes of AuthService.clearLocalData
|
||||
await StoreService.I.delete(StoreKey.accessToken);
|
||||
await settings.clear(const [
|
||||
.networkAutoEndpointSwitching,
|
||||
.networkPreferredWifiName,
|
||||
.networkLocalEndpoint,
|
||||
.networkExternalEndpointList,
|
||||
]);
|
||||
Logger('Test').info('Resuming session at https://immich.example.com/api');
|
||||
await sut.flush();
|
||||
|
||||
final stored = verify(() => mockLogRepo.insertAll(captureAny())).captured.single as List<LogMessage>;
|
||||
expect(stored.map((m) => m.message), [
|
||||
'Resuming session at [EXTERNAL ENDPOINT 1]/api with [ACCESS TOKEN]',
|
||||
'Resuming session at [OTHER URL]/api',
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ void main() {
|
||||
logService = await LogService.init(
|
||||
logRepository: logRepository,
|
||||
settingsRepository: settingsRepository,
|
||||
storeService: MockStoreService(),
|
||||
shouldBuffer: false,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/repositories/upload.repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class _MockHttpClient extends Mock implements http.Client {}
|
||||
@@ -124,4 +125,21 @@ void main() {
|
||||
expect(result.errorMessage, 'boom');
|
||||
verify(() => client.send(any())).called(1);
|
||||
});
|
||||
|
||||
test('a transport failure is logged as the error, not in the message', () async {
|
||||
final error = http.ClientException('Failed to connect to /203.0.113.9:2283');
|
||||
stubSend((_) => throw error);
|
||||
final level = Logger.root.level;
|
||||
addTearDown(() => Logger.root.level = level);
|
||||
Logger.root.level = Level.ALL;
|
||||
final records = <LogRecord>[];
|
||||
final subscription = sut.logger.onRecord.listen(records.add);
|
||||
|
||||
final result = await upload();
|
||||
await subscription.cancel();
|
||||
|
||||
expect(records.map((r) => r.error), [error, error]);
|
||||
expect(records.map((r) => r.message), everyElement(isNot(contains('203.0.113.9'))));
|
||||
expect(result.error, error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/data/db/logger/database.dart';
|
||||
import 'package:immich_mobile/data/db/main/database.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Shares the startup error and the stored logs before the log service starts', (tester) async {
|
||||
final dir = Directory.systemTemp.createTempSync('immich-logs');
|
||||
addTearDown(() => dir.delete(recursive: true));
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
const MethodChannel('plugins.flutter.io/path_provider'),
|
||||
(_) async => dir.path,
|
||||
);
|
||||
late String shared;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
const MethodChannel('dev.fluttercommunity.plus/share'),
|
||||
(call) async {
|
||||
shared = File(((call.arguments as Map)['paths'] as List).single as String).readAsStringSync();
|
||||
return 'success';
|
||||
},
|
||||
);
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
|
||||
await tester.runAsync(() async {
|
||||
final connection = await openSqliteConnection(name: 'immich_logs');
|
||||
final logs = DriftLogger.sqlite(connection);
|
||||
await LogRepository(logs).insertAll([
|
||||
LogMessage(
|
||||
message: 'Failed to connect to old.example.net/203.0.113.9:443, uri=https://old.example.net/api',
|
||||
level: LogLevel.warning,
|
||||
createdAt: DateTime(2026, 10, 15),
|
||||
),
|
||||
]);
|
||||
await logs.close();
|
||||
await connection.close();
|
||||
|
||||
await ImmichLogger.shareLogs(
|
||||
tester.element(find.byType(SizedBox)),
|
||||
startupError: 'SqliteException(2067): UNIQUE constraint failed, parameters: 14, someone@example.com',
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
shared.split('\n').first,
|
||||
endsWith('| SqliteException(2067): UNIQUE constraint failed, parameters: 14, someone@example.com |'),
|
||||
);
|
||||
expect(
|
||||
shared,
|
||||
contains('| Failed to connect to old.example.net/203.0.113.9:443, uri=https://old.example.net/api |'),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
void main() {
|
||||
final originalLocale = Intl.defaultLocale;
|
||||
setUpAll(initializeDateFormatting);
|
||||
tearDown(() => Intl.defaultLocale = originalLocale);
|
||||
|
||||
test('date and time helpers fall back for an unsupported locale', () {
|
||||
Intl.defaultLocale = 'mi';
|
||||
final date = DateTime(2023, 8, 28, 13, 45);
|
||||
expect(resolvedDateTimeLocale(), 'en_US');
|
||||
expect(date.formatDate(), 'Aug 28, 2023');
|
||||
expect(date.formatTime(alwaysUse24HourFormat: true), '13:45');
|
||||
expect(DateRangeFormatting.formatDateRange(date, DateTime(2023, 8, 29)), 'Aug 28 - Aug 29, 2023');
|
||||
});
|
||||
|
||||
test('supported Russian formatting is preserved', () {
|
||||
Intl.defaultLocale = 'ru';
|
||||
final date = DateTime(2023, 8, 28, 13, 45);
|
||||
expect(resolvedDateTimeLocale(), 'ru');
|
||||
expect(date.formatDate(), DateFormat.yMMMd('ru').format(date));
|
||||
expect(date.formatTime(alwaysUse24HourFormat: true), '13:45');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../widget_tester_extensions.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Material and Cupertino widgets work in languages Flutter has no translations for', (tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
const Column(children: [BackButton(), CupertinoSearchTextField()]),
|
||||
locale: const Locale('mi'),
|
||||
);
|
||||
|
||||
expect(find.byTooltip('Back'), findsOneWidget);
|
||||
expect(find.text('Search'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/utils/fallback_localizations.dart';
|
||||
|
||||
extension PumpConsumerWidget on WidgetTester {
|
||||
/// Wraps the provided [widget] with a localized Material app such that it
|
||||
@@ -20,12 +21,13 @@ extension PumpConsumerWidget on WidgetTester {
|
||||
Duration? duration,
|
||||
EnginePhase phase = EnginePhase.sendSemanticsUpdate,
|
||||
List<Override> overrides = const [],
|
||||
Locale? locale,
|
||||
}) async {
|
||||
await pumpWidget(
|
||||
EasyLocalization(
|
||||
supportedLocales: locales.values.toList(),
|
||||
path: translationsPath,
|
||||
startLocale: locales.values.first,
|
||||
startLocale: locale ?? locales.values.first,
|
||||
fallbackLocale: locales.values.first,
|
||||
saveLocale: false,
|
||||
useFallbackTranslations: true,
|
||||
@@ -35,7 +37,7 @@ extension PumpConsumerWidget on WidgetTester {
|
||||
child: Builder(
|
||||
builder: (context) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
localizationsDelegates: [...context.localizationDelegates, ...fallbackLocalizationDelegates],
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: context.locale,
|
||||
home: Material(child: widget),
|
||||
|
||||
@@ -9708,6 +9708,415 @@
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/passkeys": {
|
||||
"get": {
|
||||
"description": "Search the passkeys of the current user.",
|
||||
"operationId": "searchPasskeys",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter by passkey ID",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PasskeyResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Search passkeys",
|
||||
"tags": [
|
||||
"Passkeys"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "passkey.read",
|
||||
"x-immich-state": "Alpha"
|
||||
}
|
||||
},
|
||||
"/passkeys/authentication/finish": {
|
||||
"post": {
|
||||
"description": "Verify the response from the authenticator and receive a session token.",
|
||||
"operationId": "finishAuthentication",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PasskeyAuthenticationFinishDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LoginResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"summary": "Finish passkey authentication",
|
||||
"tags": [
|
||||
"Passkeys"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Alpha"
|
||||
}
|
||||
},
|
||||
"/passkeys/authentication/start": {
|
||||
"post": {
|
||||
"description": "Generate the options used by the browser to sign in with a passkey.",
|
||||
"operationId": "startAuthentication",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PasskeyAuthenticationOptionsDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"summary": "Start passkey authentication",
|
||||
"tags": [
|
||||
"Passkeys"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Alpha"
|
||||
}
|
||||
},
|
||||
"/passkeys/registration/finish": {
|
||||
"post": {
|
||||
"description": "Verify the response from the authenticator and save the new passkey for the current user.",
|
||||
"operationId": "finishRegistration",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PasskeyRegistrationFinishDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PasskeyResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Finish passkey registration",
|
||||
"tags": [
|
||||
"Passkeys"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Alpha"
|
||||
}
|
||||
},
|
||||
"/passkeys/registration/start": {
|
||||
"post": {
|
||||
"description": "Generate the options used by the browser to create a new passkey for the current user.",
|
||||
"operationId": "startRegistration",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PasskeyRegistrationOptionsDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Start passkey registration",
|
||||
"tags": [
|
||||
"Passkeys"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Alpha"
|
||||
}
|
||||
},
|
||||
"/passkeys/{id}": {
|
||||
"delete": {
|
||||
"description": "Delete a passkey by its ID. The current user must own this passkey.",
|
||||
"operationId": "deletePasskey",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Delete a passkey",
|
||||
"tags": [
|
||||
"Passkeys"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "passkey.delete",
|
||||
"x-immich-state": "Alpha"
|
||||
},
|
||||
"get": {
|
||||
"description": "Retrieve a passkey by its ID. The current user must own this passkey.",
|
||||
"operationId": "getPasskey",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PasskeyResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Retrieve a passkey",
|
||||
"tags": [
|
||||
"Passkeys"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "passkey.read",
|
||||
"x-immich-state": "Alpha"
|
||||
},
|
||||
"patch": {
|
||||
"description": "Update the name of a passkey by its ID. The current user must own this passkey.",
|
||||
"operationId": "updatePasskey",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PasskeyUpdateDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PasskeyResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Update a passkey",
|
||||
"tags": [
|
||||
"Passkeys"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "passkey.update",
|
||||
"x-immich-state": "Alpha"
|
||||
}
|
||||
},
|
||||
"/people": {
|
||||
"delete": {
|
||||
"description": "Bulk delete a list of people at once.",
|
||||
@@ -18211,7 +18620,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "3.3.3",
|
||||
"version": "3.3.5",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [
|
||||
@@ -18315,6 +18724,10 @@
|
||||
"name": "Partners",
|
||||
"description": "A partner is a link with another user that allows sharing of assets between two users."
|
||||
},
|
||||
{
|
||||
"name": "Passkeys",
|
||||
"description": "A passkey is a WebAuthn credential that can be used to login without a password."
|
||||
},
|
||||
{
|
||||
"name": "People",
|
||||
"description": "A person is a collection of faces, which can be favorited and named. A person can also be merged into another person. People are automatically created via the face recognition job."
|
||||
@@ -18619,6 +19032,9 @@
|
||||
"oauth": {
|
||||
"$ref": "#/components/schemas/AdminConfigOAuthDto"
|
||||
},
|
||||
"passkey": {
|
||||
"$ref": "#/components/schemas/AdminConfigPasskeyDto"
|
||||
},
|
||||
"passwordLogin": {
|
||||
"$ref": "#/components/schemas/AdminConfigPasswordLoginDto"
|
||||
},
|
||||
@@ -18659,6 +19075,7 @@
|
||||
"nightlyTasks",
|
||||
"notifications",
|
||||
"oauth",
|
||||
"passkey",
|
||||
"passwordLogin",
|
||||
"reverseGeocoding",
|
||||
"server",
|
||||
@@ -19521,6 +19938,34 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdminConfigPasskeyDto": {
|
||||
"properties": {
|
||||
"additionalDomains": {
|
||||
"description": "Additional domains",
|
||||
"items": {
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"domain": {
|
||||
"description": "Passkey domain",
|
||||
"format": "uri",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"description": "Enabled",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"additionalDomains",
|
||||
"domain",
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AdminConfigPasswordLoginDto": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
@@ -22790,6 +23235,7 @@
|
||||
"AssetGenerateThumbnailsQueueAll",
|
||||
"AssetGenerateThumbnails",
|
||||
"AuditTableCleanup",
|
||||
"AuthChallengeCleanup",
|
||||
"DatabaseBackup",
|
||||
"FacialRecognitionQueueAll",
|
||||
"FacialRecognition",
|
||||
@@ -24918,6 +25364,127 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PasskeyAuthenticationFinishDto": {
|
||||
"properties": {
|
||||
"response": {
|
||||
"additionalProperties": {},
|
||||
"description": "Response returned by @simplewebauthn/browser's `startAuthentication()`",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Credential ID",
|
||||
"format": "base64url",
|
||||
"pattern": "^[A-Za-z0-9_-]*$",
|
||||
"type": "string"
|
||||
},
|
||||
"response": {
|
||||
"additionalProperties": {},
|
||||
"properties": {
|
||||
"userHandle": {
|
||||
"description": "User handle",
|
||||
"format": "base64url",
|
||||
"pattern": "^[A-Za-z0-9_-]*$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"response"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"response"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PasskeyAuthenticationOptionsDto": {
|
||||
"additionalProperties": {},
|
||||
"description": "Response returned by @simplewebauthn/server's `generateAuthenticationOptions`",
|
||||
"type": "object"
|
||||
},
|
||||
"PasskeyRegistrationFinishDto": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Passkey name",
|
||||
"type": "string"
|
||||
},
|
||||
"response": {
|
||||
"description": "Response returned by @simplewebauthn/browser's `startAuthentication()`"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"response"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PasskeyRegistrationOptionsDto": {
|
||||
"additionalProperties": {},
|
||||
"description": "Response returned by @simplewebauthn/server's `generateRegistrationOptions`",
|
||||
"type": "object"
|
||||
},
|
||||
"PasskeyResponseDto": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"description": "Creation date",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "Passkey ID",
|
||||
"format": "uuid",
|
||||
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Passkey name",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"description": "Last update date",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
|
||||
"type": "string"
|
||||
},
|
||||
"usedAt": {
|
||||
"description": "Last used date",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"createdAt",
|
||||
"id",
|
||||
"name",
|
||||
"updatedAt",
|
||||
"usedAt"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PasskeyUpdateDto": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Passkey name",
|
||||
"minLength": 1,
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PeopleDeleteDto": {
|
||||
"properties": {
|
||||
"ids": {
|
||||
@@ -25280,6 +25847,9 @@
|
||||
"partner.read",
|
||||
"partner.update",
|
||||
"partner.delete",
|
||||
"passkey.read",
|
||||
"passkey.update",
|
||||
"passkey.delete",
|
||||
"person.create",
|
||||
"person.read",
|
||||
"person.update",
|
||||
@@ -25963,6 +26533,9 @@
|
||||
"oauth": {
|
||||
"$ref": "#/components/schemas/PublicConfigOAuthDto"
|
||||
},
|
||||
"passkey": {
|
||||
"$ref": "#/components/schemas/PublicConfigPasskeyDto"
|
||||
},
|
||||
"passwordLogin": {
|
||||
"$ref": "#/components/schemas/PublicConfigPasswordLoginDto"
|
||||
},
|
||||
@@ -25974,6 +26547,7 @@
|
||||
"ffmpeg",
|
||||
"map",
|
||||
"oauth",
|
||||
"passkey",
|
||||
"passwordLogin",
|
||||
"server"
|
||||
],
|
||||
@@ -26048,6 +26622,18 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PublicConfigPasskeyDto": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Enabled",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PublicConfigPasswordLoginDto": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
@@ -26183,8 +26769,7 @@
|
||||
"failed",
|
||||
"completed",
|
||||
"delayed",
|
||||
"waiting",
|
||||
"paused"
|
||||
"waiting"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -26278,7 +26863,23 @@
|
||||
"description": "Number of paused jobs",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": -9007199254740991,
|
||||
"type": "integer"
|
||||
"type": "integer",
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2.0.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Updated",
|
||||
"description": "A paused queue will now also shows its job as `waiting`"
|
||||
},
|
||||
{
|
||||
"version": "v4.0.0",
|
||||
"state": "Deprecated"
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Deprecated"
|
||||
},
|
||||
"waiting": {
|
||||
"description": "Number of waiting jobs",
|
||||
@@ -33158,6 +33759,9 @@
|
||||
"oauth": {
|
||||
"$ref": "#/components/schemas/UserConfigOAuthDto"
|
||||
},
|
||||
"passkey": {
|
||||
"$ref": "#/components/schemas/UserConfigPasskeyDto"
|
||||
},
|
||||
"passwordLogin": {
|
||||
"$ref": "#/components/schemas/UserConfigPasswordLoginDto"
|
||||
},
|
||||
@@ -33181,6 +33785,7 @@
|
||||
"map",
|
||||
"notifications",
|
||||
"oauth",
|
||||
"passkey",
|
||||
"passwordLogin",
|
||||
"reverseGeocoding",
|
||||
"server",
|
||||
@@ -33401,6 +34006,18 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UserConfigPasskeyDto": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"description": "Enabled",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UserConfigPasswordLoginDto": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
@@ -33695,11 +34312,6 @@
|
||||
"name": {
|
||||
"description": "User name",
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"deprecated": true,
|
||||
"description": "User password (deprecated, use change password endpoint)",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-monorepo",
|
||||
"version": "3.3.3",
|
||||
"version": "3.3.5",
|
||||
"description": "Monorepo for Immich",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -8,7 +8,7 @@
|
||||
"format": "prettier --cache --check i18n/",
|
||||
"format:fix": "prettier --cache --write --list-different i18n"
|
||||
},
|
||||
"packageManager": "pnpm@11.27.1",
|
||||
"packageManager": "pnpm@12.14.0",
|
||||
"engines": {
|
||||
"pnpm": ">=10.0.0"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.21.0-trixie-slim@sha256:8ec5d7557396cfe32d21c3f9c13072355ceab22b584578ca4bb28af31120cffe AS core
|
||||
FROM node:24.22.1-trixie-slim@sha256:fdc9220097e0ae3bd947c44b10cd3af5867a67a6d3a23b30b30b70929219034e AS core
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY package* pnpm* .pnpmfile.cjs ./
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "3.3.3",
|
||||
"version": "3.3.5",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,7 +26,7 @@
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^24.19.2",
|
||||
"@vitest/coverage-v8": "^4.0.0",
|
||||
"@vitest/coverage-v8": "^5.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
"commander": "^15.0.0",
|
||||
@@ -42,7 +42,7 @@
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
"vite": "^8.0.0",
|
||||
"vitest": "^4.0.0",
|
||||
"vitest": "^5.0.0",
|
||||
"vitest-fetch-mock": "^0.4.0",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
@@ -62,7 +62,7 @@
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.3",
|
||||
"chokidar": "^5.0.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fastq": "^1.17.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@@ -200,6 +200,39 @@ describe('checkForDuplicates', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores accepted files that were not part of the request', async () => {
|
||||
vi.mocked(checkBulkUpload).mockResolvedValue({
|
||||
results: [
|
||||
{ action: AssetUploadAction.Accept, id: testFilePath },
|
||||
{ action: AssetUploadAction.Accept, id: '/etc/passwd' },
|
||||
],
|
||||
});
|
||||
|
||||
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
|
||||
duplicates: [],
|
||||
newFiles: [testFilePath],
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores rejected files that were not part of the request', async () => {
|
||||
vi.mocked(checkBulkUpload).mockResolvedValue({
|
||||
results: [
|
||||
{ action: AssetUploadAction.Accept, id: testFilePath },
|
||||
{
|
||||
action: AssetUploadAction.Reject,
|
||||
id: '/etc/passwd',
|
||||
assetId: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
|
||||
reason: AssetRejectReason.Duplicate,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
|
||||
duplicates: [],
|
||||
newFiles: [testFilePath],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns results when check duplicates retry is successful', async () => {
|
||||
let mocked = vi.mocked(checkBulkUpload);
|
||||
for (let i = 1; i < retry; i++) {
|
||||
|
||||
@@ -232,8 +232,14 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
|
||||
const response = await checkBulkUpload({ assetBulkUploadCheckDto: { assets } });
|
||||
|
||||
const results = response.results as AssetBulkUploadCheckResults;
|
||||
const requested = new Set(assets.map(({ id }) => id));
|
||||
|
||||
for (const { id: filepath, assetId, action } of results) {
|
||||
if (!requested.has(filepath)) {
|
||||
console.warn(`Ignoring unexpected file in bulk upload check response: ${filepath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (action === AssetUploadAction.Accept) {
|
||||
newFiles.push(filepath);
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24.21.0-trixie-slim@sha256:8ec5d7557396cfe32d21c3f9c13072355ceab22b584578ca4bb28af31120cffe
|
||||
FROM node:24.22.1-trixie-slim@sha256:fdc9220097e0ae3bd947c44b10cd3af5867a67a6d3a23b30b30b70929219034e
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"oidc-provider": "^9.0.0",
|
||||
"tsx": "^4.20.6"
|
||||
},
|
||||
"packageManager": "pnpm@11.27.1"
|
||||
"packageManager": "pnpm@12.14.0"
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"packageManager": "pnpm@11.27.1",
|
||||
"packageManager": "pnpm@12.14.0",
|
||||
"devDependencies": {
|
||||
"@extism/js-pdk": "^1.1.1",
|
||||
"@immich/sdk": "workspace:*",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@types/node": "^24.19.2",
|
||||
"@types/semver": "^7.7.1",
|
||||
"vite": "^8.0.16",
|
||||
"vitest": "^4.1.8"
|
||||
"vitest": "^5.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@11.27.1"
|
||||
"packageManager": "pnpm@12.14.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "3.3.3",
|
||||
"version": "3.3.5",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -357,6 +357,14 @@ export type AdminConfigOAuthDto = {
|
||||
timeout: number;
|
||||
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod;
|
||||
};
|
||||
export type AdminConfigPasskeyDto = {
|
||||
/** Additional domains */
|
||||
additionalDomains: string[];
|
||||
/** Passkey domain */
|
||||
domain: string | null;
|
||||
/** Enabled */
|
||||
enabled: boolean;
|
||||
};
|
||||
export type AdminConfigPasswordLoginDto = {
|
||||
/** Enabled */
|
||||
enabled: boolean;
|
||||
@@ -421,6 +429,7 @@ export type AdminConfigDto = {
|
||||
nightlyTasks: AdminConfigNightlyTasksDto;
|
||||
notifications: AdminConfigNotificationsDto;
|
||||
oauth: AdminConfigOAuthDto;
|
||||
passkey: AdminConfigPasskeyDto;
|
||||
passwordLogin: AdminConfigPasswordLoginDto;
|
||||
reverseGeocoding: AdminConfigReverseGeocodingDto;
|
||||
server: AdminConfigServerDto;
|
||||
@@ -1614,6 +1623,10 @@ export type UserConfigOAuthDto = {
|
||||
/** Enabled */
|
||||
enabled: boolean;
|
||||
};
|
||||
export type UserConfigPasskeyDto = {
|
||||
/** Enabled */
|
||||
enabled: boolean;
|
||||
};
|
||||
export type UserConfigPasswordLoginDto = {
|
||||
/** Enabled */
|
||||
enabled: boolean;
|
||||
@@ -1647,6 +1660,7 @@ export type UserConfigDto = {
|
||||
map: UserConfigMapDto;
|
||||
notifications: UserConfigNotificationsDto;
|
||||
oauth: UserConfigOAuthDto;
|
||||
passkey: UserConfigPasskeyDto;
|
||||
passwordLogin: UserConfigPasswordLoginDto;
|
||||
reverseGeocoding: UserConfigReverseGeocodingDto;
|
||||
server: UserConfigServerDto;
|
||||
@@ -1997,6 +2011,47 @@ export type PartnerUpdateDto = {
|
||||
/** Show partner assets in timeline */
|
||||
inTimeline: boolean;
|
||||
};
|
||||
export type PasskeyResponseDto = {
|
||||
/** Creation date */
|
||||
createdAt: string;
|
||||
/** Passkey ID */
|
||||
id: string;
|
||||
/** Passkey name */
|
||||
name: string | null;
|
||||
/** Last update date */
|
||||
updatedAt: string;
|
||||
/** Last used date */
|
||||
usedAt: string | null;
|
||||
};
|
||||
export type PasskeyAuthenticationFinishDto = {
|
||||
/** Response returned by @simplewebauthn/browser's `startAuthentication()` */
|
||||
response: {
|
||||
/** Credential ID */
|
||||
id: string;
|
||||
response: {
|
||||
/** User handle */
|
||||
userHandle?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
export type PasskeyAuthenticationOptionsDto = {
|
||||
[key: string]: any;
|
||||
};
|
||||
export type PasskeyRegistrationFinishDto = {
|
||||
/** Passkey name */
|
||||
name?: string;
|
||||
/** Response returned by @simplewebauthn/browser's `startAuthentication()` */
|
||||
response: any;
|
||||
};
|
||||
export type PasskeyRegistrationOptionsDto = {
|
||||
[key: string]: any;
|
||||
};
|
||||
export type PasskeyUpdateDto = {
|
||||
/** Passkey name */
|
||||
name: string | null;
|
||||
};
|
||||
export type PeopleDeleteDto = {
|
||||
/** IDs to process */
|
||||
ids: string[];
|
||||
@@ -2198,6 +2253,10 @@ export type PublicConfigOAuthDto = {
|
||||
/** Enabled */
|
||||
enabled: boolean;
|
||||
};
|
||||
export type PublicConfigPasskeyDto = {
|
||||
/** Enabled */
|
||||
enabled: boolean;
|
||||
};
|
||||
export type PublicConfigPasswordLoginDto = {
|
||||
/** Enabled */
|
||||
enabled: boolean;
|
||||
@@ -2210,6 +2269,7 @@ export type PublicConfigDto = {
|
||||
ffmpeg: PublicConfigFFmpegDto;
|
||||
map: PublicConfigMapDto;
|
||||
oauth: PublicConfigOAuthDto;
|
||||
passkey: PublicConfigPasskeyDto;
|
||||
passwordLogin: PublicConfigPasswordLoginDto;
|
||||
server: PublicConfigServerDto;
|
||||
};
|
||||
@@ -3206,8 +3266,6 @@ export type UserUpdateMeDto = {
|
||||
email?: string;
|
||||
/** User name */
|
||||
name?: string;
|
||||
/** User password (deprecated, use change password endpoint) */
|
||||
password?: string;
|
||||
};
|
||||
export type OnboardingResponseDto = {
|
||||
/** Is user onboarded */
|
||||
@@ -6126,6 +6184,115 @@ export function updatePartner({ id, partnerUpdateDto }: {
|
||||
body: partnerUpdateDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Search passkeys
|
||||
*/
|
||||
export function searchPasskeys({ id }: {
|
||||
id?: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: PasskeyResponseDto[];
|
||||
}>(`/passkeys${QS.query(QS.explode({
|
||||
id
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Finish passkey authentication
|
||||
*/
|
||||
export function finishAuthentication({ passkeyAuthenticationFinishDto }: {
|
||||
passkeyAuthenticationFinishDto: PasskeyAuthenticationFinishDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 201;
|
||||
data: LoginResponseDto;
|
||||
}>("/passkeys/authentication/finish", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: passkeyAuthenticationFinishDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Start passkey authentication
|
||||
*/
|
||||
export function startAuthentication(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: PasskeyAuthenticationOptionsDto;
|
||||
}>("/passkeys/authentication/start", {
|
||||
...opts,
|
||||
method: "POST"
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Finish passkey registration
|
||||
*/
|
||||
export function finishRegistration({ passkeyRegistrationFinishDto }: {
|
||||
passkeyRegistrationFinishDto: PasskeyRegistrationFinishDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 201;
|
||||
data: PasskeyResponseDto;
|
||||
}>("/passkeys/registration/finish", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: passkeyRegistrationFinishDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Start passkey registration
|
||||
*/
|
||||
export function startRegistration(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: PasskeyRegistrationOptionsDto;
|
||||
}>("/passkeys/registration/start", {
|
||||
...opts,
|
||||
method: "POST"
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Delete a passkey
|
||||
*/
|
||||
export function deletePasskey({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText(`/passkeys/${encodeURIComponent(id)}`, {
|
||||
...opts,
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Retrieve a passkey
|
||||
*/
|
||||
export function getPasskey({ id }: {
|
||||
id: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: PasskeyResponseDto;
|
||||
}>(`/passkeys/${encodeURIComponent(id)}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Update a passkey
|
||||
*/
|
||||
export function updatePasskey({ id, passkeyUpdateDto }: {
|
||||
id: string;
|
||||
passkeyUpdateDto: PasskeyUpdateDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: PasskeyResponseDto;
|
||||
}>(`/passkeys/${encodeURIComponent(id)}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "PATCH",
|
||||
body: passkeyUpdateDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Delete people
|
||||
*/
|
||||
@@ -8221,6 +8388,9 @@ export enum Permission {
|
||||
PartnerRead = "partner.read",
|
||||
PartnerUpdate = "partner.update",
|
||||
PartnerDelete = "partner.delete",
|
||||
PasskeyRead = "passkey.read",
|
||||
PasskeyUpdate = "passkey.update",
|
||||
PasskeyDelete = "passkey.delete",
|
||||
PersonCreate = "person.create",
|
||||
PersonRead = "person.read",
|
||||
PersonUpdate = "person.update",
|
||||
@@ -8436,8 +8606,7 @@ export enum QueueJobStatus {
|
||||
Failed = "failed",
|
||||
Completed = "completed",
|
||||
Delayed = "delayed",
|
||||
Waiting = "waiting",
|
||||
Paused = "paused"
|
||||
Waiting = "waiting"
|
||||
}
|
||||
export enum JobName {
|
||||
AssetDelete = "AssetDelete",
|
||||
@@ -8456,6 +8625,7 @@ export enum JobName {
|
||||
AssetGenerateThumbnailsQueueAll = "AssetGenerateThumbnailsQueueAll",
|
||||
AssetGenerateThumbnails = "AssetGenerateThumbnails",
|
||||
AuditTableCleanup = "AuditTableCleanup",
|
||||
AuthChallengeCleanup = "AuthChallengeCleanup",
|
||||
DatabaseBackup = "DatabaseBackup",
|
||||
FacialRecognitionQueueAll = "FacialRecognitionQueueAll",
|
||||
FacialRecognition = "FacialRecognition",
|
||||
|
||||
Generated
+1024
-641
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -73,5 +73,5 @@ shamefullyHoist: false
|
||||
verifyDepsBeforeRun: install
|
||||
minimumReleaseAgeExclude:
|
||||
- '@immich/ui'
|
||||
- '@immich/sql-tools@0.6.3'
|
||||
- '@immich/sql-tools@0.6.3 || 0.6.6'
|
||||
- '@nestjs/cli@12.0.1'
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
FROM ghcr.io/immich-app/base-server-dev:202610131110@sha256:082dc5e2611953d63d7b30dc8427da37ff5cd1d932aa9e99ccc4a26298b5f7a5 AS builder
|
||||
FROM ghcr.io/immich-app/base-server-dev:202610211607@sha256:f99c605b44b291834cb680b80fd606f2b7c4bee5d8f9591bfdac51fdbd68c21d AS builder
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
CI=1 \
|
||||
COREPACK_HOME=/tmp \
|
||||
@@ -68,7 +68,7 @@ FROM plugin-sdk AS plugins
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2026.9.12@sha256:868b004119abb2e7c146618da63371ec65da40bfb0f38a0ec4128ca9e96c057a /usr/local/bin/mise /usr/local/bin/mise
|
||||
COPY --from=ghcr.io/jdx/mise:2026.10.15@sha256:0593528c637b0dae8a0acca66b5d49266cf262a811f06530d7273d3cc002f3a0 /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
COPY ./mise.toml ./mise.toml
|
||||
COPY ./mise.lock ./mise.lock
|
||||
@@ -86,7 +86,7 @@ RUN --mount=type=cache,id=pnpm,target=/buildcache/pnpm-store \
|
||||
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
mise //:plugins
|
||||
|
||||
FROM ghcr.io/immich-app/base-server-prod:202610131110@sha256:9d4ce4bfd72d77edddd3072fbca78b7f4307a32c8cbe75276349944dd6405be3
|
||||
FROM ghcr.io/immich-app/base-server-prod:202610211607@sha256:e50a1b36e2638dce42981ddfa84c134246c48fb2ad290fab6acfc1bb9b2bd891
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:202610131110@sha256:082dc5e2611953d63d7b30dc8427da37ff5cd1d932aa9e99ccc4a26298b5f7a5 AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:202610211607@sha256:f99c605b44b291834cb680b80fd606f2b7c4bee5d8f9591bfdac51fdbd68c21d AS dev
|
||||
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2026.9.12@sha256:868b004119abb2e7c146618da63371ec65da40bfb0f38a0ec4128ca9e96c057a /usr/local/bin/mise /usr/local/bin/mise
|
||||
COPY --from=ghcr.io/jdx/mise:2026.10.15@sha256:0593528c637b0dae8a0acca66b5d49266cf262a811f06530d7273d3cc002f3a0 /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
RUN echo "devdir=/buildcache/node-gyp" >> /usr/local/etc/npmrc && \
|
||||
echo "store-dir=/buildcache/pnpm-store" >> /usr/local/etc/npmrc && \
|
||||
|
||||
+22
-20
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "3.3.3",
|
||||
"version": "3.3.5",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -41,7 +41,7 @@
|
||||
"dependencies": {
|
||||
"@extism/extism": "2.0.0-rc13",
|
||||
"@immich/plugin-sdk": "workspace:*",
|
||||
"@immich/sql-tools": "^0.6.3",
|
||||
"@immich/sql-tools": "^0.6.6",
|
||||
"@nestjs/bullmq": "^12.0.0",
|
||||
"@nestjs/common": "^12.0.0",
|
||||
"@nestjs/core": "^12.0.0",
|
||||
@@ -52,27 +52,29 @@
|
||||
"@nestjs/websockets": "^12.0.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/context-async-hooks": "^2.0.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.222.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.222.0",
|
||||
"@opentelemetry/instrumentation-ioredis": "^0.70.0",
|
||||
"@opentelemetry/instrumentation-nestjs-core": "^0.68.0",
|
||||
"@opentelemetry/instrumentation-pg": "^0.74.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.223.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.223.0",
|
||||
"@opentelemetry/instrumentation-ioredis": "^0.71.0",
|
||||
"@opentelemetry/instrumentation-nestjs-core": "^0.70.0",
|
||||
"@opentelemetry/instrumentation-pg": "^0.76.0",
|
||||
"@opentelemetry/resources": "^2.0.1",
|
||||
"@opentelemetry/sdk-metrics": "^2.0.1",
|
||||
"@opentelemetry/sdk-node": "^0.222.0",
|
||||
"@opentelemetry/sdk-node": "^0.223.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.34.0",
|
||||
"@simplewebauthn/server": "14.0.3",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"archiver": "^7.0.0",
|
||||
"aaguids": "^0.0.71",
|
||||
"archiver": "^8.0.0",
|
||||
"async-lock": "^1.4.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"body-parser": "^2.2.0",
|
||||
"bullmq": "^5.51.0",
|
||||
"chokidar": "^4.0.3",
|
||||
"bullmq": "^6.0.0",
|
||||
"chokidar": "^5.0.0",
|
||||
"compression": "^1.8.0",
|
||||
"cookie": "^1.0.2",
|
||||
"cookie": "^2.0.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cron": "4.4.0",
|
||||
"exiftool-vendored": "^35.20.0",
|
||||
"exiftool-vendored": "^39.0.0",
|
||||
"express": "^5.1.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
@@ -80,12 +82,12 @@
|
||||
"geo-tz": "^8.0.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"helmet": "^8.1.0",
|
||||
"ioredis": "^5.8.2",
|
||||
"ioredis": "^6.0.0",
|
||||
"jose": "^6.0.0",
|
||||
"js-yaml": "^5.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"kysely": "^0.29.5",
|
||||
"kysely-postgres-js": "^3.0.0",
|
||||
"kysely-postgres-js": "^5.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"luxon": "^3.4.2",
|
||||
"mnemonist": "^0.40.3",
|
||||
@@ -95,7 +97,7 @@
|
||||
"nestjs-kysely": "3.1.4",
|
||||
"nestjs-otel": "^8.0.0",
|
||||
"nestjs-zod": "^5.5.0",
|
||||
"nodemailer": "^9.0.0",
|
||||
"nodemailer": "^10.0.0",
|
||||
"openid-client": "^6.3.3",
|
||||
"pg": "^8.11.3",
|
||||
"picomatch": "^4.0.2",
|
||||
@@ -124,7 +126,7 @@
|
||||
"@nestjs/schematics": "^12.0.0",
|
||||
"@nestjs/testing": "^12.0.0",
|
||||
"@swc/core": "^1.4.14",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/archiver": "^8.0.0",
|
||||
"@types/async-lock": "^1.4.2",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/body-parser": "^1.19.6",
|
||||
@@ -148,7 +150,7 @@
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/validator": "^13.15.2",
|
||||
"@typescript/native": "npm:typescript@^7.0.2",
|
||||
"@vitest/coverage-v8": "^4.0.0",
|
||||
"@vitest/coverage-v8": "^5.0.0",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
@@ -165,8 +167,8 @@
|
||||
"testcontainers": "^12.0.0",
|
||||
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
||||
"typescript-eslint": "^8.28.0",
|
||||
"unplugin-swc": "^1.4.5",
|
||||
"vitest": "^4.0.0"
|
||||
"unplugin-swc": "^2.0.0",
|
||||
"vitest": "^5.0.0"
|
||||
},
|
||||
"overrides": {
|
||||
"sharp": "^0.35.3"
|
||||
|
||||
@@ -60,7 +60,7 @@ export const reverseGeocodeMaxDistance = 25_000;
|
||||
export const MOBILE_REDIRECT = 'app.immich:///oauth-callback';
|
||||
export const LOGIN_URL = '/auth/login?autoLaunch=0';
|
||||
|
||||
export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
|
||||
export const excludePaths = ['/.well-known/immich', '/.well-known/webauthn', '/custom.css', '/favicon.ico'];
|
||||
|
||||
export const FACE_THUMBNAIL_SIZE = 250;
|
||||
|
||||
@@ -175,6 +175,7 @@ export const endpointTags: Record<ApiTag, string> = {
|
||||
'A notification is a specialized message sent to users to inform them of important events. Currently, these notifications are only shown in the Immich web application.',
|
||||
[ApiTag.NotificationsAdmin]: 'Notification administrative endpoints.',
|
||||
[ApiTag.Partners]: 'A partner is a link with another user that allows sharing of assets between two users.',
|
||||
[ApiTag.Passkeys]: 'A passkey is a WebAuthn credential that can be used to login without a password.',
|
||||
[ApiTag.People]:
|
||||
'A person is a collection of faces, which can be favorited and named. A person can also be merged into another person. People are automatically created via the face recognition job.',
|
||||
[ApiTag.Plugins]:
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import request from 'supertest';
|
||||
import { AppController } from 'src/controllers/app.controller.js';
|
||||
import { PasskeyService } from 'src/services/passkey.service.js';
|
||||
import { SystemConfigService } from 'src/services/system-config.service.js';
|
||||
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils.js';
|
||||
|
||||
describe(AppController.name, () => {
|
||||
let ctx: ControllerContext;
|
||||
const passkeyService = mockBaseService(PasskeyService);
|
||||
|
||||
beforeAll(async () => {
|
||||
ctx = await controllerSetup(AppController, [
|
||||
{ provide: SystemConfigService, useValue: mockBaseService(SystemConfigService) },
|
||||
{ provide: PasskeyService, useValue: passkeyService },
|
||||
]);
|
||||
return () => ctx.close();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.reset();
|
||||
passkeyService.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /.well-known/immich', () => {
|
||||
@@ -34,6 +38,13 @@ describe(AppController.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /.well-known/webauthn', () => {
|
||||
it('should not be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).post('/.well-known/webauthn');
|
||||
expect(ctx.authenticate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /custom.css', () => {
|
||||
it('should not be an authenticated route', async () => {
|
||||
await request(ctx.getHttpServer()).post('/custom.css');
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Controller, Get, Header } from '@nestjs/common';
|
||||
import { ApiExcludeEndpoint } from '@nestjs/swagger';
|
||||
import { Authenticated } from 'src/middleware/auth.guard.js';
|
||||
import { PasskeyService } from 'src/services/passkey.service.js';
|
||||
import { SystemConfigService } from 'src/services/system-config.service.js';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private service: SystemConfigService) {}
|
||||
constructor(
|
||||
private service: SystemConfigService,
|
||||
private passkeyService: PasskeyService,
|
||||
) {}
|
||||
|
||||
@ApiExcludeEndpoint()
|
||||
@Get('.well-known/immich')
|
||||
@@ -18,6 +22,13 @@ export class AppController {
|
||||
};
|
||||
}
|
||||
|
||||
@ApiExcludeEndpoint()
|
||||
@Get('.well-known/webauthn')
|
||||
@Authenticated({ public: true })
|
||||
getWebAuthnWellKnown() {
|
||||
return this.passkeyService.getWellKnown();
|
||||
}
|
||||
|
||||
@ApiExcludeEndpoint()
|
||||
@Get('custom.css')
|
||||
@Authenticated({ public: true })
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user