diff --git a/.github/.nvmrc b/.github/.nvmrc index 0a492611a0..9e2934aa34 100644 --- a/.github/.nvmrc +++ b/.github/.nvmrc @@ -1 +1 @@ -24.11.0 +24.11.1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69a6a9e33b..5c28084d32 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,7 +74,7 @@ jobs: echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Download APK - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: name: release-apk-signed github-token: ${{ steps.generate-token.outputs.token }} diff --git a/cli/.nvmrc b/cli/.nvmrc index 0a492611a0..9e2934aa34 100644 --- a/cli/.nvmrc +++ b/cli/.nvmrc @@ -1 +1 @@ -24.11.0 +24.11.1 diff --git a/cli/Dockerfile b/cli/Dockerfile index 8c74fe12b1..d56190ee16 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS core +FROM node:24.1.0-alpine3.20@sha256:8fe019e0d57dbdce5f5c27c0b63d2775cf34b00e3755a7dea969802d7e0c2b25 AS core WORKDIR /usr/src/app COPY package* pnpm* .pnpmfile.cjs ./ diff --git a/cli/package.json b/cli/package.json index bfd318d471..eaa547764b 100644 --- a/cli/package.json +++ b/cli/package.json @@ -20,7 +20,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.19.1", + "@types/node": "^24.10.1", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -69,6 +69,6 @@ "micromatch": "^4.0.8" }, "volta": { - "node": "24.11.0" + "node": "24.11.1" } } diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index e01f4ead22..90dc00d942 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -95,7 +95,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:12.2.1-ubuntu@sha256:797530c642f7b41ba7848c44cfda5e361ef1f3391a98bed1e5d448c472b6826a + image: grafana/grafana:12.3.0-ubuntu@sha256:cee936306135e1925ab21dffa16f8a411535d16ab086bef2309339a8e74d62df volumes: - grafana-data:/var/lib/grafana diff --git a/docs/.nvmrc b/docs/.nvmrc index 0a492611a0..9e2934aa34 100644 --- a/docs/.nvmrc +++ b/docs/.nvmrc @@ -1 +1 @@ -24.11.0 +24.11.1 diff --git a/docs/package.json b/docs/package.json index a7c958351c..b96059c523 100644 --- a/docs/package.json +++ b/docs/package.json @@ -57,6 +57,6 @@ "node": ">=20" }, "volta": { - "node": "24.11.0" + "node": "24.11.1" } } diff --git a/e2e/.nvmrc b/e2e/.nvmrc index 0a492611a0..9e2934aa34 100644 --- a/e2e/.nvmrc +++ b/e2e/.nvmrc @@ -1 +1 @@ -24.11.0 +24.11.1 diff --git a/e2e/package.json b/e2e/package.json index 47a4df6dc4..b45af7c736 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -26,7 +26,7 @@ "@playwright/test": "^1.44.1", "@socket.io/component-emitter": "^3.1.2", "@types/luxon": "^3.4.2", - "@types/node": "^22.19.1", + "@types/node": "^24.10.1", "@types/oidc-provider": "^9.0.0", "@types/pg": "^8.15.1", "@types/pngjs": "^6.0.4", @@ -54,6 +54,6 @@ "vitest": "^3.0.0" }, "volta": { - "node": "24.11.0" + "node": "24.11.1" } } diff --git a/e2e/src/mock-network/timeline-network.ts b/e2e/src/mock-network/timeline-network.ts index 012defe4ab..59bce71dd8 100644 --- a/e2e/src/mock-network/timeline-network.ts +++ b/e2e/src/mock-network/timeline-network.ts @@ -62,50 +62,60 @@ export const setupTimelineMockApiRoutes = async ( return route.continue(); }); - await context.route('**/api/assets/**', async (route, request) => { + await context.route('**/api/assets/*', async (route, request) => { + const url = new URL(request.url()); + const pathname = url.pathname; + const assetId = basename(pathname); + const asset = getAsset(timelineRestData, assetId); + return route.fulfill({ + status: 200, + contentType: 'application/json', + json: asset, + }); + }); + + await context.route('**/api/assets/*/ocr', async (route) => { + return route.fulfill({ status: 200, contentType: 'application/json', json: [] }); + }); + + await context.route('**/api/assets/*/thumbnail?size=*', async (route, request) => { const pattern = /\/api\/assets\/(?[^/]+)\/thumbnail\?size=(?preview|thumbnail)/; const match = request.url().match(pattern); - if (!match) { - const url = new URL(request.url()); - const pathname = url.pathname; - const assetId = basename(pathname); - const asset = getAsset(timelineRestData, assetId); - return route.fulfill({ - status: 200, - contentType: 'application/json', - json: asset, - }); + if (!match?.groups) { + throw new Error(`Invalid URL for thumbnail endpoint: ${request.url()}`); } - if (match.groups?.size === 'preview') { + + if (match.groups.size === 'preview') { if (!route.request().serviceWorker()) { return route.continue(); } - const asset = getAsset(timelineRestData, match.groups?.assetId); + const asset = getAsset(timelineRestData, match.groups.assetId); return route.fulfill({ status: 200, headers: { 'content-type': 'image/jpeg', ETag: 'abc123', 'Cache-Control': 'public, max-age=3600' }, body: await randomPreview( - match.groups?.assetId, + match.groups.assetId, (asset?.exifInfo?.exifImageWidth ?? 0) / (asset?.exifInfo?.exifImageHeight ?? 1), ), }); } - if (match.groups?.size === 'thumbnail') { + if (match.groups.size === 'thumbnail') { if (!route.request().serviceWorker()) { return route.continue(); } - const asset = getAsset(timelineRestData, match.groups?.assetId); + const asset = getAsset(timelineRestData, match.groups.assetId); return route.fulfill({ status: 200, headers: { 'content-type': 'image/jpeg' }, body: await randomThumbnail( - match.groups?.assetId, + match.groups.assetId, (asset?.exifInfo?.exifImageWidth ?? 0) / (asset?.exifInfo?.exifImageHeight ?? 1), ), }); } return route.continue(); }); + await context.route('**/api/albums/**', async (route, request) => { const pattern = /\/api\/albums\/(?[^/?]+)/; const match = request.url().match(pattern); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index f045ea2efd..15bb112cd8 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -12,7 +12,7 @@ import { PersonCreateDto, QueueCommandDto, QueueName, - QueuesResponseDto, + QueuesResponseLegacyDto, SharedLinkCreateDto, UpdateLibraryDto, UserAdminCreateDto, @@ -564,13 +564,13 @@ export const utils = { await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) }); }, - isQueueEmpty: async (accessToken: string, queue: keyof QueuesResponseDto) => { + isQueueEmpty: async (accessToken: string, queue: keyof QueuesResponseLegacyDto) => { const queues = await getQueuesLegacy({ headers: asBearerAuth(accessToken) }); const jobCounts = queues[queue].jobCounts; return !jobCounts.active && !jobCounts.waiting; }, - waitForQueueFinish: (accessToken: string, queue: keyof QueuesResponseDto, ms?: number) => { + waitForQueueFinish: (accessToken: string, queue: keyof QueuesResponseLegacyDto, ms?: number) => { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 10_000); diff --git a/i18n/en.json b/i18n/en.json index e3f144356e..f5979db745 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -861,6 +861,7 @@ "deselect_all": "Deselect All", "details": "Details", "direction": "Direction", + "disable": "Disable", "disabled": "Disabled", "disallow_edits": "Disallow edits", "discord": "Discord", @@ -935,7 +936,6 @@ "empty_folder": "This folder is empty", "empty_trash": "Empty trash", "empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!", - "disable": "Disable", "enable": "Enable", "enable_backup": "Enable Backup", "enable_biometric_auth_description": "Enter your PIN code to enable biometric authentication", @@ -1016,13 +1016,13 @@ "unable_to_create_api_key": "Unable to create a new API Key", "unable_to_create_library": "Unable to create library", "unable_to_create_user": "Unable to create user", - "unable_to_delete": "Unable to delete workflow", "unable_to_delete_album": "Unable to delete album", "unable_to_delete_asset": "Unable to delete asset", "unable_to_delete_assets": "Error deleting assets", "unable_to_delete_exclusion_pattern": "Unable to delete exclusion pattern", "unable_to_delete_shared_link": "Unable to delete shared link", "unable_to_delete_user": "Unable to delete user", + "unable_to_delete_workflow": "Unable to delete workflow", "unable_to_download_files": "Unable to download files", "unable_to_edit_exclusion_pattern": "Unable to edit exclusion pattern", "unable_to_empty_trash": "Unable to empty trash", @@ -1073,6 +1073,7 @@ "unable_to_update_settings": "Unable to update settings", "unable_to_update_timeline_display_status": "Unable to update timeline display status", "unable_to_update_user": "Unable to update user", + "unable_to_update_workflow": "Unable to update workflow", "unable_to_upload_file": "Unable to upload file" }, "exclusion_pattern": "Exclusion pattern", @@ -1126,6 +1127,7 @@ "filter": "Filter", "filter_people": "Filter people", "filter_places": "Filter places", + "filters": "Filters", "find_them_fast": "Find them fast by name with search", "first": "First", "fix_incorrect_match": "Fix incorrect match", @@ -1169,10 +1171,10 @@ "hi_user": "Hi {name} ({email})", "hide_all_people": "Hide all people", "hide_gallery": "Hide gallery", - "hide_json": "Hide JSON", "hide_named_person": "Hide person {name}", "hide_password": "Hide password", "hide_person": "Hide person", + "hide_schema": "Hide schema", "hide_text_recognition": "Hide text recognition", "hide_unnamed_people": "Hide unnamed people", "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", @@ -1989,13 +1991,13 @@ "show_hidden_people": "Show hidden people", "show_in_timeline": "Show in timeline", "show_in_timeline_setting_description": "Show photos and videos from this user in your timeline", - "show_json": "Show JSON", "show_keyboard_shortcuts": "Show keyboard shortcuts", "show_metadata": "Show metadata", "show_or_hide_info": "Show or hide info", "show_password": "Show password", "show_person_options": "Show person options", "show_progress_bar": "Show Progress Bar", + "show_schema": "Show schema", "show_search_options": "Show search options", "show_shared_links": "Show shared links", "show_slideshow_transition": "Show slideshow transition", @@ -2241,6 +2243,7 @@ "welcome_to_immich": "Welcome to Immich", "wifi_name": "Wi-Fi Name", "workflow": "Workflow", + "workflow_delete_prompt": "Are you sure you want to delete this workflow?", "workflow_deleted": "Workflow deleted", "workflow_json": "Workflow JSON", "workflow_json_help": "Edit the workflow configuration in JSON format. Changes will sync to the visual builder.", diff --git a/mise.toml b/mise.toml index cf3b86c6cc..ce7cc15a75 100644 --- a/mise.toml +++ b/mise.toml @@ -1,11 +1,12 @@ experimental_monorepo_root = true [tools] -node = "24.11.0" +node = "24.11.1" flutter = "3.35.7" pnpm = "10.20.0" terragrunt = "0.91.2" opentofu = "1.10.6" +java = "25.0.1" [tools."github:CQLabs/homebrew-dcm"] version = "1.30.0" diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index e6c5afde3b..a91696524d 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -137,8 +137,10 @@ Class | Method | HTTP request | Description *DeprecatedApi* | [**getAllUserAssetsByDeviceId**](doc//DeprecatedApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID *DeprecatedApi* | [**getDeltaSync**](doc//DeprecatedApi.md#getdeltasync) | **POST** /sync/delta-sync | Get delta sync for user *DeprecatedApi* | [**getFullSyncForUser**](doc//DeprecatedApi.md#getfullsyncforuser) | **POST** /sync/full-sync | Get full sync for user +*DeprecatedApi* | [**getQueuesLegacy**](doc//DeprecatedApi.md#getqueueslegacy) | **GET** /jobs | Retrieve queue counts and status *DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | Get random assets *DeprecatedApi* | [**replaceAsset**](doc//DeprecatedApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset +*DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information *DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | Delete a duplicate @@ -199,6 +201,11 @@ Class | Method | HTTP request | Description *PluginsApi* | [**getPlugin**](doc//PluginsApi.md#getplugin) | **GET** /plugins/{id} | Retrieve a plugin *PluginsApi* | [**getPlugins**](doc//PluginsApi.md#getplugins) | **GET** /plugins | List all plugins *PluginsApi* | [**getTriggers**](doc//PluginsApi.md#gettriggers) | **GET** /plugins/triggers | List all plugin triggers +*QueuesApi* | [**emptyQueue**](doc//QueuesApi.md#emptyqueue) | **DELETE** /queues/{name}/jobs | Empty a queue +*QueuesApi* | [**getQueue**](doc//QueuesApi.md#getqueue) | **GET** /queues/{name} | Retrieve a queue +*QueuesApi* | [**getQueueJobs**](doc//QueuesApi.md#getqueuejobs) | **GET** /queues/{name}/jobs | Retrieve queue jobs +*QueuesApi* | [**getQueues**](doc//QueuesApi.md#getqueues) | **GET** /queues | List all queues +*QueuesApi* | [**updateQueue**](doc//QueuesApi.md#updatequeue) | **PUT** /queues/{name} | Update a queue *SearchApi* | [**getAssetsByCity**](doc//SearchApi.md#getassetsbycity) | **GET** /search/cities | Retrieve assets by city *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | Retrieve explore data *SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions | Retrieve search suggestions @@ -397,6 +404,7 @@ Class | Method | HTTP request | Description - [FoldersUpdate](doc//FoldersUpdate.md) - [ImageFormat](doc//ImageFormat.md) - [JobCreateDto](doc//JobCreateDto.md) + - [JobName](doc//JobName.md) - [JobSettingsDto](doc//JobSettingsDto.md) - [LibraryResponseDto](doc//LibraryResponseDto.md) - [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md) @@ -467,11 +475,16 @@ Class | Method | HTTP request | Description - [PurchaseUpdate](doc//PurchaseUpdate.md) - [QueueCommand](doc//QueueCommand.md) - [QueueCommandDto](doc//QueueCommandDto.md) + - [QueueDeleteDto](doc//QueueDeleteDto.md) + - [QueueJobResponseDto](doc//QueueJobResponseDto.md) + - [QueueJobStatus](doc//QueueJobStatus.md) - [QueueName](doc//QueueName.md) - [QueueResponseDto](doc//QueueResponseDto.md) + - [QueueResponseLegacyDto](doc//QueueResponseLegacyDto.md) - [QueueStatisticsDto](doc//QueueStatisticsDto.md) - - [QueueStatusDto](doc//QueueStatusDto.md) - - [QueuesResponseDto](doc//QueuesResponseDto.md) + - [QueueStatusLegacyDto](doc//QueueStatusLegacyDto.md) + - [QueueUpdateDto](doc//QueueUpdateDto.md) + - [QueuesResponseLegacyDto](doc//QueuesResponseLegacyDto.md) - [RandomSearchDto](doc//RandomSearchDto.md) - [RatingsResponse](doc//RatingsResponse.md) - [RatingsUpdate](doc//RatingsUpdate.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 0e6e1c63b9..3ccce45c95 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -50,6 +50,7 @@ part 'api/notifications_admin_api.dart'; part 'api/partners_api.dart'; part 'api/people_api.dart'; part 'api/plugins_api.dart'; +part 'api/queues_api.dart'; part 'api/search_api.dart'; part 'api/server_api.dart'; part 'api/sessions_api.dart'; @@ -154,6 +155,7 @@ part 'model/folders_response.dart'; part 'model/folders_update.dart'; part 'model/image_format.dart'; part 'model/job_create_dto.dart'; +part 'model/job_name.dart'; part 'model/job_settings_dto.dart'; part 'model/library_response_dto.dart'; part 'model/library_stats_response_dto.dart'; @@ -224,11 +226,16 @@ part 'model/purchase_response.dart'; part 'model/purchase_update.dart'; part 'model/queue_command.dart'; part 'model/queue_command_dto.dart'; +part 'model/queue_delete_dto.dart'; +part 'model/queue_job_response_dto.dart'; +part 'model/queue_job_status.dart'; part 'model/queue_name.dart'; part 'model/queue_response_dto.dart'; +part 'model/queue_response_legacy_dto.dart'; part 'model/queue_statistics_dto.dart'; -part 'model/queue_status_dto.dart'; -part 'model/queues_response_dto.dart'; +part 'model/queue_status_legacy_dto.dart'; +part 'model/queue_update_dto.dart'; +part 'model/queues_response_legacy_dto.dart'; part 'model/random_search_dto.dart'; part 'model/ratings_response.dart'; part 'model/ratings_update.dart'; diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index aaf7c074b9..d0d92d804d 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -248,6 +248,54 @@ class DeprecatedApi { return null; } + /// Retrieve queue counts and status + /// + /// Retrieve the counts of the current queue, as well as the current status. + /// + /// Note: This method returns the HTTP [Response]. + Future getQueuesLegacyWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/jobs'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Retrieve queue counts and status + /// + /// Retrieve the counts of the current queue, as well as the current status. + Future getQueuesLegacy() async { + final response = await getQueuesLegacyWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueuesResponseLegacyDto',) as QueuesResponseLegacyDto; + + } + return null; + } + /// Get random assets /// /// Retrieve a specified number of random assets for the authenticated user. @@ -444,4 +492,65 @@ class DeprecatedApi { } return null; } + + /// Run jobs + /// + /// Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [QueueCommandDto] queueCommandDto (required): + Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/jobs/{name}' + .replaceAll('{name}', name.toString()); + + // ignore: prefer_final_locals + Object? postBody = queueCommandDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Run jobs + /// + /// Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [QueueCommandDto] queueCommandDto (required): + Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { + final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueueResponseLegacyDto',) as QueueResponseLegacyDto; + + } + return null; + } } diff --git a/mobile/openapi/lib/api/jobs_api.dart b/mobile/openapi/lib/api/jobs_api.dart index 906dce6d37..9dda59a883 100644 --- a/mobile/openapi/lib/api/jobs_api.dart +++ b/mobile/openapi/lib/api/jobs_api.dart @@ -97,7 +97,7 @@ class JobsApi { /// Retrieve queue counts and status /// /// Retrieve the counts of the current queue, as well as the current status. - Future getQueuesLegacy() async { + Future getQueuesLegacy() async { final response = await getQueuesLegacyWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -106,7 +106,7 @@ class JobsApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueuesResponseDto',) as QueuesResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueuesResponseLegacyDto',) as QueuesResponseLegacyDto; } return null; @@ -158,7 +158,7 @@ class JobsApi { /// * [QueueName] name (required): /// /// * [QueueCommandDto] queueCommandDto (required): - Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { + Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { final response = await runQueueCommandLegacyWithHttpInfo(name, queueCommandDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -167,7 +167,7 @@ class JobsApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueueResponseDto',) as QueueResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueueResponseLegacyDto',) as QueueResponseLegacyDto; } return null; diff --git a/mobile/openapi/lib/api/queues_api.dart b/mobile/openapi/lib/api/queues_api.dart new file mode 100644 index 0000000000..50575ed706 --- /dev/null +++ b/mobile/openapi/lib/api/queues_api.dart @@ -0,0 +1,308 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class QueuesApi { + QueuesApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Empty a queue + /// + /// Removes all jobs from the specified queue. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [QueueDeleteDto] queueDeleteDto (required): + Future emptyQueueWithHttpInfo(QueueName name, QueueDeleteDto queueDeleteDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/queues/{name}/jobs' + .replaceAll('{name}', name.toString()); + + // ignore: prefer_final_locals + Object? postBody = queueDeleteDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Empty a queue + /// + /// Removes all jobs from the specified queue. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [QueueDeleteDto] queueDeleteDto (required): + Future emptyQueue(QueueName name, QueueDeleteDto queueDeleteDto,) async { + final response = await emptyQueueWithHttpInfo(name, queueDeleteDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Retrieve a queue + /// + /// Retrieves a specific queue by its name. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + Future getQueueWithHttpInfo(QueueName name,) async { + // ignore: prefer_const_declarations + final apiPath = r'/queues/{name}' + .replaceAll('{name}', name.toString()); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Retrieve a queue + /// + /// Retrieves a specific queue by its name. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + Future getQueue(QueueName name,) async { + final response = await getQueueWithHttpInfo(name,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueueResponseDto',) as QueueResponseDto; + + } + return null; + } + + /// Retrieve queue jobs + /// + /// Retrieves a list of queue jobs from the specified queue. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [List] status: + Future getQueueJobsWithHttpInfo(QueueName name, { List? status, }) async { + // ignore: prefer_const_declarations + final apiPath = r'/queues/{name}/jobs' + .replaceAll('{name}', name.toString()); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (status != null) { + queryParams.addAll(_queryParams('multi', 'status', status)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Retrieve queue jobs + /// + /// Retrieves a list of queue jobs from the specified queue. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [List] status: + Future?> getQueueJobs(QueueName name, { List? status, }) async { + final response = await getQueueJobsWithHttpInfo(name, status: status, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// List all queues + /// + /// Retrieves a list of queues. + /// + /// Note: This method returns the HTTP [Response]. + Future getQueuesWithHttpInfo() async { + // ignore: prefer_const_declarations + final apiPath = r'/queues'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + apiPath, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// List all queues + /// + /// Retrieves a list of queues. + Future?> getQueues() async { + final response = await getQueuesWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// Update a queue + /// + /// Change the paused status of a specific queue. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [QueueUpdateDto] queueUpdateDto (required): + Future updateQueueWithHttpInfo(QueueName name, QueueUpdateDto queueUpdateDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/queues/{name}' + .replaceAll('{name}', name.toString()); + + // ignore: prefer_final_locals + Object? postBody = queueUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Update a queue + /// + /// Change the paused status of a specific queue. + /// + /// Parameters: + /// + /// * [QueueName] name (required): + /// + /// * [QueueUpdateDto] queueUpdateDto (required): + Future updateQueue(QueueName name, QueueUpdateDto queueUpdateDto,) async { + final response = await updateQueueWithHttpInfo(name, queueUpdateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'QueueResponseDto',) as QueueResponseDto; + + } + return null; + } +} diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 80439fbd64..e82880760f 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -358,6 +358,8 @@ class ApiClient { return ImageFormatTypeTransformer().decode(value); case 'JobCreateDto': return JobCreateDto.fromJson(value); + case 'JobName': + return JobNameTypeTransformer().decode(value); case 'JobSettingsDto': return JobSettingsDto.fromJson(value); case 'LibraryResponseDto': @@ -498,16 +500,26 @@ class ApiClient { return QueueCommandTypeTransformer().decode(value); case 'QueueCommandDto': return QueueCommandDto.fromJson(value); + case 'QueueDeleteDto': + return QueueDeleteDto.fromJson(value); + case 'QueueJobResponseDto': + return QueueJobResponseDto.fromJson(value); + case 'QueueJobStatus': + return QueueJobStatusTypeTransformer().decode(value); case 'QueueName': return QueueNameTypeTransformer().decode(value); case 'QueueResponseDto': return QueueResponseDto.fromJson(value); + case 'QueueResponseLegacyDto': + return QueueResponseLegacyDto.fromJson(value); case 'QueueStatisticsDto': return QueueStatisticsDto.fromJson(value); - case 'QueueStatusDto': - return QueueStatusDto.fromJson(value); - case 'QueuesResponseDto': - return QueuesResponseDto.fromJson(value); + case 'QueueStatusLegacyDto': + return QueueStatusLegacyDto.fromJson(value); + case 'QueueUpdateDto': + return QueueUpdateDto.fromJson(value); + case 'QueuesResponseLegacyDto': + return QueuesResponseLegacyDto.fromJson(value); case 'RandomSearchDto': return RandomSearchDto.fromJson(value); case 'RatingsResponse': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index e6d39d5eb7..2c97eeb314 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -94,6 +94,9 @@ String parameterToString(dynamic value) { if (value is ImageFormat) { return ImageFormatTypeTransformer().encode(value).toString(); } + if (value is JobName) { + return JobNameTypeTransformer().encode(value).toString(); + } if (value is LogLevel) { return LogLevelTypeTransformer().encode(value).toString(); } @@ -133,6 +136,9 @@ String parameterToString(dynamic value) { if (value is QueueCommand) { return QueueCommandTypeTransformer().encode(value).toString(); } + if (value is QueueJobStatus) { + return QueueJobStatusTypeTransformer().encode(value).toString(); + } if (value is QueueName) { return QueueNameTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/job_name.dart b/mobile/openapi/lib/model/job_name.dart new file mode 100644 index 0000000000..038a17a8e6 --- /dev/null +++ b/mobile/openapi/lib/model/job_name.dart @@ -0,0 +1,244 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class JobName { + /// Instantiate a new enum with the provided [value]. + const JobName._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const assetDelete = JobName._(r'AssetDelete'); + static const assetDeleteCheck = JobName._(r'AssetDeleteCheck'); + static const assetDetectFacesQueueAll = JobName._(r'AssetDetectFacesQueueAll'); + static const assetDetectFaces = JobName._(r'AssetDetectFaces'); + static const assetDetectDuplicatesQueueAll = JobName._(r'AssetDetectDuplicatesQueueAll'); + static const assetDetectDuplicates = JobName._(r'AssetDetectDuplicates'); + static const assetEncodeVideoQueueAll = JobName._(r'AssetEncodeVideoQueueAll'); + static const assetEncodeVideo = JobName._(r'AssetEncodeVideo'); + static const assetEmptyTrash = JobName._(r'AssetEmptyTrash'); + static const assetExtractMetadataQueueAll = JobName._(r'AssetExtractMetadataQueueAll'); + static const assetExtractMetadata = JobName._(r'AssetExtractMetadata'); + static const assetFileMigration = JobName._(r'AssetFileMigration'); + static const assetGenerateThumbnailsQueueAll = JobName._(r'AssetGenerateThumbnailsQueueAll'); + static const assetGenerateThumbnails = JobName._(r'AssetGenerateThumbnails'); + static const auditLogCleanup = JobName._(r'AuditLogCleanup'); + static const auditTableCleanup = JobName._(r'AuditTableCleanup'); + static const databaseBackup = JobName._(r'DatabaseBackup'); + static const facialRecognitionQueueAll = JobName._(r'FacialRecognitionQueueAll'); + static const facialRecognition = JobName._(r'FacialRecognition'); + static const fileDelete = JobName._(r'FileDelete'); + static const fileMigrationQueueAll = JobName._(r'FileMigrationQueueAll'); + static const libraryDeleteCheck = JobName._(r'LibraryDeleteCheck'); + static const libraryDelete = JobName._(r'LibraryDelete'); + static const libraryRemoveAsset = JobName._(r'LibraryRemoveAsset'); + static const libraryScanAssetsQueueAll = JobName._(r'LibraryScanAssetsQueueAll'); + static const librarySyncAssets = JobName._(r'LibrarySyncAssets'); + static const librarySyncFilesQueueAll = JobName._(r'LibrarySyncFilesQueueAll'); + static const librarySyncFiles = JobName._(r'LibrarySyncFiles'); + static const libraryScanQueueAll = JobName._(r'LibraryScanQueueAll'); + static const memoryCleanup = JobName._(r'MemoryCleanup'); + static const memoryGenerate = JobName._(r'MemoryGenerate'); + static const notificationsCleanup = JobName._(r'NotificationsCleanup'); + static const notifyUserSignup = JobName._(r'NotifyUserSignup'); + static const notifyAlbumInvite = JobName._(r'NotifyAlbumInvite'); + static const notifyAlbumUpdate = JobName._(r'NotifyAlbumUpdate'); + static const userDelete = JobName._(r'UserDelete'); + static const userDeleteCheck = JobName._(r'UserDeleteCheck'); + static const userSyncUsage = JobName._(r'UserSyncUsage'); + static const personCleanup = JobName._(r'PersonCleanup'); + static const personFileMigration = JobName._(r'PersonFileMigration'); + static const personGenerateThumbnail = JobName._(r'PersonGenerateThumbnail'); + static const sessionCleanup = JobName._(r'SessionCleanup'); + static const sendMail = JobName._(r'SendMail'); + static const sidecarQueueAll = JobName._(r'SidecarQueueAll'); + static const sidecarCheck = JobName._(r'SidecarCheck'); + static const sidecarWrite = JobName._(r'SidecarWrite'); + static const smartSearchQueueAll = JobName._(r'SmartSearchQueueAll'); + static const smartSearch = JobName._(r'SmartSearch'); + static const storageTemplateMigration = JobName._(r'StorageTemplateMigration'); + static const storageTemplateMigrationSingle = JobName._(r'StorageTemplateMigrationSingle'); + static const tagCleanup = JobName._(r'TagCleanup'); + static const versionCheck = JobName._(r'VersionCheck'); + static const ocrQueueAll = JobName._(r'OcrQueueAll'); + static const ocr = JobName._(r'Ocr'); + static const workflowRun = JobName._(r'WorkflowRun'); + + /// List of all possible values in this [enum][JobName]. + static const values = [ + assetDelete, + assetDeleteCheck, + assetDetectFacesQueueAll, + assetDetectFaces, + assetDetectDuplicatesQueueAll, + assetDetectDuplicates, + assetEncodeVideoQueueAll, + assetEncodeVideo, + assetEmptyTrash, + assetExtractMetadataQueueAll, + assetExtractMetadata, + assetFileMigration, + assetGenerateThumbnailsQueueAll, + assetGenerateThumbnails, + auditLogCleanup, + auditTableCleanup, + databaseBackup, + facialRecognitionQueueAll, + facialRecognition, + fileDelete, + fileMigrationQueueAll, + libraryDeleteCheck, + libraryDelete, + libraryRemoveAsset, + libraryScanAssetsQueueAll, + librarySyncAssets, + librarySyncFilesQueueAll, + librarySyncFiles, + libraryScanQueueAll, + memoryCleanup, + memoryGenerate, + notificationsCleanup, + notifyUserSignup, + notifyAlbumInvite, + notifyAlbumUpdate, + userDelete, + userDeleteCheck, + userSyncUsage, + personCleanup, + personFileMigration, + personGenerateThumbnail, + sessionCleanup, + sendMail, + sidecarQueueAll, + sidecarCheck, + sidecarWrite, + smartSearchQueueAll, + smartSearch, + storageTemplateMigration, + storageTemplateMigrationSingle, + tagCleanup, + versionCheck, + ocrQueueAll, + ocr, + workflowRun, + ]; + + static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = JobName.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [JobName] to String, +/// and [decode] dynamic data back to [JobName]. +class JobNameTypeTransformer { + factory JobNameTypeTransformer() => _instance ??= const JobNameTypeTransformer._(); + + const JobNameTypeTransformer._(); + + String encode(JobName data) => data.value; + + /// Decodes a [dynamic value][data] to a JobName. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + JobName? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'AssetDelete': return JobName.assetDelete; + case r'AssetDeleteCheck': return JobName.assetDeleteCheck; + case r'AssetDetectFacesQueueAll': return JobName.assetDetectFacesQueueAll; + case r'AssetDetectFaces': return JobName.assetDetectFaces; + case r'AssetDetectDuplicatesQueueAll': return JobName.assetDetectDuplicatesQueueAll; + case r'AssetDetectDuplicates': return JobName.assetDetectDuplicates; + case r'AssetEncodeVideoQueueAll': return JobName.assetEncodeVideoQueueAll; + case r'AssetEncodeVideo': return JobName.assetEncodeVideo; + case r'AssetEmptyTrash': return JobName.assetEmptyTrash; + case r'AssetExtractMetadataQueueAll': return JobName.assetExtractMetadataQueueAll; + case r'AssetExtractMetadata': return JobName.assetExtractMetadata; + case r'AssetFileMigration': return JobName.assetFileMigration; + case r'AssetGenerateThumbnailsQueueAll': return JobName.assetGenerateThumbnailsQueueAll; + case r'AssetGenerateThumbnails': return JobName.assetGenerateThumbnails; + case r'AuditLogCleanup': return JobName.auditLogCleanup; + case r'AuditTableCleanup': return JobName.auditTableCleanup; + case r'DatabaseBackup': return JobName.databaseBackup; + case r'FacialRecognitionQueueAll': return JobName.facialRecognitionQueueAll; + case r'FacialRecognition': return JobName.facialRecognition; + case r'FileDelete': return JobName.fileDelete; + case r'FileMigrationQueueAll': return JobName.fileMigrationQueueAll; + case r'LibraryDeleteCheck': return JobName.libraryDeleteCheck; + case r'LibraryDelete': return JobName.libraryDelete; + case r'LibraryRemoveAsset': return JobName.libraryRemoveAsset; + case r'LibraryScanAssetsQueueAll': return JobName.libraryScanAssetsQueueAll; + case r'LibrarySyncAssets': return JobName.librarySyncAssets; + case r'LibrarySyncFilesQueueAll': return JobName.librarySyncFilesQueueAll; + case r'LibrarySyncFiles': return JobName.librarySyncFiles; + case r'LibraryScanQueueAll': return JobName.libraryScanQueueAll; + case r'MemoryCleanup': return JobName.memoryCleanup; + case r'MemoryGenerate': return JobName.memoryGenerate; + case r'NotificationsCleanup': return JobName.notificationsCleanup; + case r'NotifyUserSignup': return JobName.notifyUserSignup; + case r'NotifyAlbumInvite': return JobName.notifyAlbumInvite; + case r'NotifyAlbumUpdate': return JobName.notifyAlbumUpdate; + case r'UserDelete': return JobName.userDelete; + case r'UserDeleteCheck': return JobName.userDeleteCheck; + case r'UserSyncUsage': return JobName.userSyncUsage; + case r'PersonCleanup': return JobName.personCleanup; + case r'PersonFileMigration': return JobName.personFileMigration; + case r'PersonGenerateThumbnail': return JobName.personGenerateThumbnail; + case r'SessionCleanup': return JobName.sessionCleanup; + case r'SendMail': return JobName.sendMail; + case r'SidecarQueueAll': return JobName.sidecarQueueAll; + case r'SidecarCheck': return JobName.sidecarCheck; + case r'SidecarWrite': return JobName.sidecarWrite; + case r'SmartSearchQueueAll': return JobName.smartSearchQueueAll; + case r'SmartSearch': return JobName.smartSearch; + case r'StorageTemplateMigration': return JobName.storageTemplateMigration; + case r'StorageTemplateMigrationSingle': return JobName.storageTemplateMigrationSingle; + case r'TagCleanup': return JobName.tagCleanup; + case r'VersionCheck': return JobName.versionCheck; + case r'OcrQueueAll': return JobName.ocrQueueAll; + case r'Ocr': return JobName.ocr; + case r'WorkflowRun': return JobName.workflowRun; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [JobNameTypeTransformer] instance. + static JobNameTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index 0a2f0d1791..3b9a3964b6 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -152,6 +152,12 @@ class Permission { static const userProfileImagePeriodRead = Permission._(r'userProfileImage.read'); static const userProfileImagePeriodUpdate = Permission._(r'userProfileImage.update'); static const userProfileImagePeriodDelete = Permission._(r'userProfileImage.delete'); + static const queuePeriodRead = Permission._(r'queue.read'); + static const queuePeriodUpdate = Permission._(r'queue.update'); + static const queueJobPeriodCreate = Permission._(r'queueJob.create'); + static const queueJobPeriodRead = Permission._(r'queueJob.read'); + static const queueJobPeriodUpdate = Permission._(r'queueJob.update'); + static const queueJobPeriodDelete = Permission._(r'queueJob.delete'); static const workflowPeriodCreate = Permission._(r'workflow.create'); static const workflowPeriodRead = Permission._(r'workflow.read'); static const workflowPeriodUpdate = Permission._(r'workflow.update'); @@ -294,6 +300,12 @@ class Permission { userProfileImagePeriodRead, userProfileImagePeriodUpdate, userProfileImagePeriodDelete, + queuePeriodRead, + queuePeriodUpdate, + queueJobPeriodCreate, + queueJobPeriodRead, + queueJobPeriodUpdate, + queueJobPeriodDelete, workflowPeriodCreate, workflowPeriodRead, workflowPeriodUpdate, @@ -471,6 +483,12 @@ class PermissionTypeTransformer { case r'userProfileImage.read': return Permission.userProfileImagePeriodRead; case r'userProfileImage.update': return Permission.userProfileImagePeriodUpdate; case r'userProfileImage.delete': return Permission.userProfileImagePeriodDelete; + case r'queue.read': return Permission.queuePeriodRead; + case r'queue.update': return Permission.queuePeriodUpdate; + case r'queueJob.create': return Permission.queueJobPeriodCreate; + case r'queueJob.read': return Permission.queueJobPeriodRead; + case r'queueJob.update': return Permission.queueJobPeriodUpdate; + case r'queueJob.delete': return Permission.queueJobPeriodDelete; case r'workflow.create': return Permission.workflowPeriodCreate; case r'workflow.read': return Permission.workflowPeriodRead; case r'workflow.update': return Permission.workflowPeriodUpdate; diff --git a/mobile/openapi/lib/model/queue_delete_dto.dart b/mobile/openapi/lib/model/queue_delete_dto.dart new file mode 100644 index 0000000000..d319238f92 --- /dev/null +++ b/mobile/openapi/lib/model/queue_delete_dto.dart @@ -0,0 +1,109 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class QueueDeleteDto { + /// Returns a new [QueueDeleteDto] instance. + QueueDeleteDto({ + this.failed, + }); + + /// If true, will also remove failed jobs from the queue. + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? failed; + + @override + bool operator ==(Object other) => identical(this, other) || other is QueueDeleteDto && + other.failed == failed; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (failed == null ? 0 : failed!.hashCode); + + @override + String toString() => 'QueueDeleteDto[failed=$failed]'; + + Map toJson() { + final json = {}; + if (this.failed != null) { + json[r'failed'] = this.failed; + } else { + // json[r'failed'] = null; + } + return json; + } + + /// Returns a new [QueueDeleteDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static QueueDeleteDto? fromJson(dynamic value) { + upgradeDto(value, "QueueDeleteDto"); + if (value is Map) { + final json = value.cast(); + + return QueueDeleteDto( + failed: mapValueOfType(json, r'failed'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = QueueDeleteDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = QueueDeleteDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of QueueDeleteDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = QueueDeleteDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/queue_job_response_dto.dart b/mobile/openapi/lib/model/queue_job_response_dto.dart new file mode 100644 index 0000000000..1bfaa56195 --- /dev/null +++ b/mobile/openapi/lib/model/queue_job_response_dto.dart @@ -0,0 +1,132 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class QueueJobResponseDto { + /// Returns a new [QueueJobResponseDto] instance. + QueueJobResponseDto({ + required this.data, + this.id, + required this.name, + required this.timestamp, + }); + + Object data; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? id; + + JobName name; + + int timestamp; + + @override + bool operator ==(Object other) => identical(this, other) || other is QueueJobResponseDto && + other.data == data && + other.id == id && + other.name == name && + other.timestamp == timestamp; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (data.hashCode) + + (id == null ? 0 : id!.hashCode) + + (name.hashCode) + + (timestamp.hashCode); + + @override + String toString() => 'QueueJobResponseDto[data=$data, id=$id, name=$name, timestamp=$timestamp]'; + + Map toJson() { + final json = {}; + json[r'data'] = this.data; + if (this.id != null) { + json[r'id'] = this.id; + } else { + // json[r'id'] = null; + } + json[r'name'] = this.name; + json[r'timestamp'] = this.timestamp; + return json; + } + + /// Returns a new [QueueJobResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static QueueJobResponseDto? fromJson(dynamic value) { + upgradeDto(value, "QueueJobResponseDto"); + if (value is Map) { + final json = value.cast(); + + return QueueJobResponseDto( + data: mapValueOfType(json, r'data')!, + id: mapValueOfType(json, r'id'), + name: JobName.fromJson(json[r'name'])!, + timestamp: mapValueOfType(json, r'timestamp')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = QueueJobResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = QueueJobResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of QueueJobResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = QueueJobResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'data', + 'name', + 'timestamp', + }; +} + diff --git a/mobile/openapi/lib/model/queue_job_status.dart b/mobile/openapi/lib/model/queue_job_status.dart new file mode 100644 index 0000000000..03a1371cc5 --- /dev/null +++ b/mobile/openapi/lib/model/queue_job_status.dart @@ -0,0 +1,97 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class QueueJobStatus { + /// Instantiate a new enum with the provided [value]. + const QueueJobStatus._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const active = QueueJobStatus._(r'active'); + static const failed = QueueJobStatus._(r'failed'); + static const completed = QueueJobStatus._(r'completed'); + static const delayed = QueueJobStatus._(r'delayed'); + static const waiting = QueueJobStatus._(r'waiting'); + static const paused = QueueJobStatus._(r'paused'); + + /// List of all possible values in this [enum][QueueJobStatus]. + static const values = [ + active, + failed, + completed, + delayed, + waiting, + paused, + ]; + + static QueueJobStatus? fromJson(dynamic value) => QueueJobStatusTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = QueueJobStatus.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [QueueJobStatus] to String, +/// and [decode] dynamic data back to [QueueJobStatus]. +class QueueJobStatusTypeTransformer { + factory QueueJobStatusTypeTransformer() => _instance ??= const QueueJobStatusTypeTransformer._(); + + const QueueJobStatusTypeTransformer._(); + + String encode(QueueJobStatus data) => data.value; + + /// Decodes a [dynamic value][data] to a QueueJobStatus. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + QueueJobStatus? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'active': return QueueJobStatus.active; + case r'failed': return QueueJobStatus.failed; + case r'completed': return QueueJobStatus.completed; + case r'delayed': return QueueJobStatus.delayed; + case r'waiting': return QueueJobStatus.waiting; + case r'paused': return QueueJobStatus.paused; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [QueueJobStatusTypeTransformer] instance. + static QueueJobStatusTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/queue_response_dto.dart b/mobile/openapi/lib/model/queue_response_dto.dart index b20449f721..c5d4ed8e3d 100644 --- a/mobile/openapi/lib/model/queue_response_dto.dart +++ b/mobile/openapi/lib/model/queue_response_dto.dart @@ -13,32 +13,38 @@ part of openapi.api; class QueueResponseDto { /// Returns a new [QueueResponseDto] instance. QueueResponseDto({ - required this.jobCounts, - required this.queueStatus, + required this.isPaused, + required this.name, + required this.statistics, }); - QueueStatisticsDto jobCounts; + bool isPaused; - QueueStatusDto queueStatus; + QueueName name; + + QueueStatisticsDto statistics; @override bool operator ==(Object other) => identical(this, other) || other is QueueResponseDto && - other.jobCounts == jobCounts && - other.queueStatus == queueStatus; + other.isPaused == isPaused && + other.name == name && + other.statistics == statistics; @override int get hashCode => // ignore: unnecessary_parenthesis - (jobCounts.hashCode) + - (queueStatus.hashCode); + (isPaused.hashCode) + + (name.hashCode) + + (statistics.hashCode); @override - String toString() => 'QueueResponseDto[jobCounts=$jobCounts, queueStatus=$queueStatus]'; + String toString() => 'QueueResponseDto[isPaused=$isPaused, name=$name, statistics=$statistics]'; Map toJson() { final json = {}; - json[r'jobCounts'] = this.jobCounts; - json[r'queueStatus'] = this.queueStatus; + json[r'isPaused'] = this.isPaused; + json[r'name'] = this.name; + json[r'statistics'] = this.statistics; return json; } @@ -51,8 +57,9 @@ class QueueResponseDto { final json = value.cast(); return QueueResponseDto( - jobCounts: QueueStatisticsDto.fromJson(json[r'jobCounts'])!, - queueStatus: QueueStatusDto.fromJson(json[r'queueStatus'])!, + isPaused: mapValueOfType(json, r'isPaused')!, + name: QueueName.fromJson(json[r'name'])!, + statistics: QueueStatisticsDto.fromJson(json[r'statistics'])!, ); } return null; @@ -100,8 +107,9 @@ class QueueResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'jobCounts', - 'queueStatus', + 'isPaused', + 'name', + 'statistics', }; } diff --git a/mobile/openapi/lib/model/queue_response_legacy_dto.dart b/mobile/openapi/lib/model/queue_response_legacy_dto.dart new file mode 100644 index 0000000000..214b0b31f6 --- /dev/null +++ b/mobile/openapi/lib/model/queue_response_legacy_dto.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class QueueResponseLegacyDto { + /// Returns a new [QueueResponseLegacyDto] instance. + QueueResponseLegacyDto({ + required this.jobCounts, + required this.queueStatus, + }); + + QueueStatisticsDto jobCounts; + + QueueStatusLegacyDto queueStatus; + + @override + bool operator ==(Object other) => identical(this, other) || other is QueueResponseLegacyDto && + other.jobCounts == jobCounts && + other.queueStatus == queueStatus; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (jobCounts.hashCode) + + (queueStatus.hashCode); + + @override + String toString() => 'QueueResponseLegacyDto[jobCounts=$jobCounts, queueStatus=$queueStatus]'; + + Map toJson() { + final json = {}; + json[r'jobCounts'] = this.jobCounts; + json[r'queueStatus'] = this.queueStatus; + return json; + } + + /// Returns a new [QueueResponseLegacyDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static QueueResponseLegacyDto? fromJson(dynamic value) { + upgradeDto(value, "QueueResponseLegacyDto"); + if (value is Map) { + final json = value.cast(); + + return QueueResponseLegacyDto( + jobCounts: QueueStatisticsDto.fromJson(json[r'jobCounts'])!, + queueStatus: QueueStatusLegacyDto.fromJson(json[r'queueStatus'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = QueueResponseLegacyDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = QueueResponseLegacyDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of QueueResponseLegacyDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = QueueResponseLegacyDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'jobCounts', + 'queueStatus', + }; +} + diff --git a/mobile/openapi/lib/model/queue_status_dto.dart b/mobile/openapi/lib/model/queue_status_legacy_dto.dart similarity index 63% rename from mobile/openapi/lib/model/queue_status_dto.dart rename to mobile/openapi/lib/model/queue_status_legacy_dto.dart index 77591affe2..88c4eac340 100644 --- a/mobile/openapi/lib/model/queue_status_dto.dart +++ b/mobile/openapi/lib/model/queue_status_legacy_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class QueueStatusDto { - /// Returns a new [QueueStatusDto] instance. - QueueStatusDto({ +class QueueStatusLegacyDto { + /// Returns a new [QueueStatusLegacyDto] instance. + QueueStatusLegacyDto({ required this.isActive, required this.isPaused, }); @@ -22,7 +22,7 @@ class QueueStatusDto { bool isPaused; @override - bool operator ==(Object other) => identical(this, other) || other is QueueStatusDto && + bool operator ==(Object other) => identical(this, other) || other is QueueStatusLegacyDto && other.isActive == isActive && other.isPaused == isPaused; @@ -33,7 +33,7 @@ class QueueStatusDto { (isPaused.hashCode); @override - String toString() => 'QueueStatusDto[isActive=$isActive, isPaused=$isPaused]'; + String toString() => 'QueueStatusLegacyDto[isActive=$isActive, isPaused=$isPaused]'; Map toJson() { final json = {}; @@ -42,15 +42,15 @@ class QueueStatusDto { return json; } - /// Returns a new [QueueStatusDto] instance and imports its values from + /// Returns a new [QueueStatusLegacyDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static QueueStatusDto? fromJson(dynamic value) { - upgradeDto(value, "QueueStatusDto"); + static QueueStatusLegacyDto? fromJson(dynamic value) { + upgradeDto(value, "QueueStatusLegacyDto"); if (value is Map) { final json = value.cast(); - return QueueStatusDto( + return QueueStatusLegacyDto( isActive: mapValueOfType(json, r'isActive')!, isPaused: mapValueOfType(json, r'isPaused')!, ); @@ -58,11 +58,11 @@ class QueueStatusDto { return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = QueueStatusDto.fromJson(row); + final value = QueueStatusLegacyDto.fromJson(row); if (value != null) { result.add(value); } @@ -71,12 +71,12 @@ class QueueStatusDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = QueueStatusDto.fromJson(entry.value); + final value = QueueStatusLegacyDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -85,14 +85,14 @@ class QueueStatusDto { return map; } - // maps a json object with a list of QueueStatusDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of QueueStatusLegacyDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = QueueStatusDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = QueueStatusLegacyDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/queue_update_dto.dart b/mobile/openapi/lib/model/queue_update_dto.dart new file mode 100644 index 0000000000..ce89e51878 --- /dev/null +++ b/mobile/openapi/lib/model/queue_update_dto.dart @@ -0,0 +1,108 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class QueueUpdateDto { + /// Returns a new [QueueUpdateDto] instance. + QueueUpdateDto({ + this.isPaused, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isPaused; + + @override + bool operator ==(Object other) => identical(this, other) || other is QueueUpdateDto && + other.isPaused == isPaused; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isPaused == null ? 0 : isPaused!.hashCode); + + @override + String toString() => 'QueueUpdateDto[isPaused=$isPaused]'; + + Map toJson() { + final json = {}; + if (this.isPaused != null) { + json[r'isPaused'] = this.isPaused; + } else { + // json[r'isPaused'] = null; + } + return json; + } + + /// Returns a new [QueueUpdateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static QueueUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "QueueUpdateDto"); + if (value is Map) { + final json = value.cast(); + + return QueueUpdateDto( + isPaused: mapValueOfType(json, r'isPaused'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = QueueUpdateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = QueueUpdateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of QueueUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = QueueUpdateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/queues_response_dto.dart b/mobile/openapi/lib/model/queues_response_legacy_dto.dart similarity index 55% rename from mobile/openapi/lib/model/queues_response_dto.dart rename to mobile/openapi/lib/model/queues_response_legacy_dto.dart index be40a56fb1..4aab6d863b 100644 --- a/mobile/openapi/lib/model/queues_response_dto.dart +++ b/mobile/openapi/lib/model/queues_response_legacy_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class QueuesResponseDto { - /// Returns a new [QueuesResponseDto] instance. - QueuesResponseDto({ +class QueuesResponseLegacyDto { + /// Returns a new [QueuesResponseLegacyDto] instance. + QueuesResponseLegacyDto({ required this.backgroundTask, required this.backupDatabase, required this.duplicateDetection, @@ -32,42 +32,42 @@ class QueuesResponseDto { required this.workflow, }); - QueueResponseDto backgroundTask; + QueueResponseLegacyDto backgroundTask; - QueueResponseDto backupDatabase; + QueueResponseLegacyDto backupDatabase; - QueueResponseDto duplicateDetection; + QueueResponseLegacyDto duplicateDetection; - QueueResponseDto faceDetection; + QueueResponseLegacyDto faceDetection; - QueueResponseDto facialRecognition; + QueueResponseLegacyDto facialRecognition; - QueueResponseDto library_; + QueueResponseLegacyDto library_; - QueueResponseDto metadataExtraction; + QueueResponseLegacyDto metadataExtraction; - QueueResponseDto migration; + QueueResponseLegacyDto migration; - QueueResponseDto notifications; + QueueResponseLegacyDto notifications; - QueueResponseDto ocr; + QueueResponseLegacyDto ocr; - QueueResponseDto search; + QueueResponseLegacyDto search; - QueueResponseDto sidecar; + QueueResponseLegacyDto sidecar; - QueueResponseDto smartSearch; + QueueResponseLegacyDto smartSearch; - QueueResponseDto storageTemplateMigration; + QueueResponseLegacyDto storageTemplateMigration; - QueueResponseDto thumbnailGeneration; + QueueResponseLegacyDto thumbnailGeneration; - QueueResponseDto videoConversion; + QueueResponseLegacyDto videoConversion; - QueueResponseDto workflow; + QueueResponseLegacyDto workflow; @override - bool operator ==(Object other) => identical(this, other) || other is QueuesResponseDto && + bool operator ==(Object other) => identical(this, other) || other is QueuesResponseLegacyDto && other.backgroundTask == backgroundTask && other.backupDatabase == backupDatabase && other.duplicateDetection == duplicateDetection && @@ -108,7 +108,7 @@ class QueuesResponseDto { (workflow.hashCode); @override - String toString() => 'QueuesResponseDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]'; + String toString() => 'QueuesResponseLegacyDto[backgroundTask=$backgroundTask, backupDatabase=$backupDatabase, duplicateDetection=$duplicateDetection, faceDetection=$faceDetection, facialRecognition=$facialRecognition, library_=$library_, metadataExtraction=$metadataExtraction, migration=$migration, notifications=$notifications, ocr=$ocr, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, storageTemplateMigration=$storageTemplateMigration, thumbnailGeneration=$thumbnailGeneration, videoConversion=$videoConversion, workflow=$workflow]'; Map toJson() { final json = {}; @@ -132,42 +132,42 @@ class QueuesResponseDto { return json; } - /// Returns a new [QueuesResponseDto] instance and imports its values from + /// Returns a new [QueuesResponseLegacyDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static QueuesResponseDto? fromJson(dynamic value) { - upgradeDto(value, "QueuesResponseDto"); + static QueuesResponseLegacyDto? fromJson(dynamic value) { + upgradeDto(value, "QueuesResponseLegacyDto"); if (value is Map) { final json = value.cast(); - return QueuesResponseDto( - backgroundTask: QueueResponseDto.fromJson(json[r'backgroundTask'])!, - backupDatabase: QueueResponseDto.fromJson(json[r'backupDatabase'])!, - duplicateDetection: QueueResponseDto.fromJson(json[r'duplicateDetection'])!, - faceDetection: QueueResponseDto.fromJson(json[r'faceDetection'])!, - facialRecognition: QueueResponseDto.fromJson(json[r'facialRecognition'])!, - library_: QueueResponseDto.fromJson(json[r'library'])!, - metadataExtraction: QueueResponseDto.fromJson(json[r'metadataExtraction'])!, - migration: QueueResponseDto.fromJson(json[r'migration'])!, - notifications: QueueResponseDto.fromJson(json[r'notifications'])!, - ocr: QueueResponseDto.fromJson(json[r'ocr'])!, - search: QueueResponseDto.fromJson(json[r'search'])!, - sidecar: QueueResponseDto.fromJson(json[r'sidecar'])!, - smartSearch: QueueResponseDto.fromJson(json[r'smartSearch'])!, - storageTemplateMigration: QueueResponseDto.fromJson(json[r'storageTemplateMigration'])!, - thumbnailGeneration: QueueResponseDto.fromJson(json[r'thumbnailGeneration'])!, - videoConversion: QueueResponseDto.fromJson(json[r'videoConversion'])!, - workflow: QueueResponseDto.fromJson(json[r'workflow'])!, + return QueuesResponseLegacyDto( + backgroundTask: QueueResponseLegacyDto.fromJson(json[r'backgroundTask'])!, + backupDatabase: QueueResponseLegacyDto.fromJson(json[r'backupDatabase'])!, + duplicateDetection: QueueResponseLegacyDto.fromJson(json[r'duplicateDetection'])!, + faceDetection: QueueResponseLegacyDto.fromJson(json[r'faceDetection'])!, + facialRecognition: QueueResponseLegacyDto.fromJson(json[r'facialRecognition'])!, + library_: QueueResponseLegacyDto.fromJson(json[r'library'])!, + metadataExtraction: QueueResponseLegacyDto.fromJson(json[r'metadataExtraction'])!, + migration: QueueResponseLegacyDto.fromJson(json[r'migration'])!, + notifications: QueueResponseLegacyDto.fromJson(json[r'notifications'])!, + ocr: QueueResponseLegacyDto.fromJson(json[r'ocr'])!, + search: QueueResponseLegacyDto.fromJson(json[r'search'])!, + sidecar: QueueResponseLegacyDto.fromJson(json[r'sidecar'])!, + smartSearch: QueueResponseLegacyDto.fromJson(json[r'smartSearch'])!, + storageTemplateMigration: QueueResponseLegacyDto.fromJson(json[r'storageTemplateMigration'])!, + thumbnailGeneration: QueueResponseLegacyDto.fromJson(json[r'thumbnailGeneration'])!, + videoConversion: QueueResponseLegacyDto.fromJson(json[r'videoConversion'])!, + workflow: QueueResponseLegacyDto.fromJson(json[r'workflow'])!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = QueuesResponseDto.fromJson(row); + final value = QueuesResponseLegacyDto.fromJson(row); if (value != null) { result.add(value); } @@ -176,12 +176,12 @@ class QueuesResponseDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = QueuesResponseDto.fromJson(entry.value); + final value = QueuesResponseLegacyDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -190,14 +190,14 @@ class QueuesResponseDto { return map; } - // maps a json object with a list of QueuesResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of QueuesResponseLegacyDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = QueuesResponseDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = QueuesResponseLegacyDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 75f5ffb200..de70524993 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4929,6 +4929,7 @@ }, "/jobs": { "get": { + "deprecated": true, "description": "Retrieve the counts of the current queue, as well as the current status.", "operationId": "getQueuesLegacy", "parameters": [], @@ -4937,7 +4938,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QueuesResponseDto" + "$ref": "#/components/schemas/QueuesResponseLegacyDto" } } }, @@ -4957,7 +4958,8 @@ ], "summary": "Retrieve queue counts and status", "tags": [ - "Jobs" + "Jobs", + "Deprecated" ], "x-immich-admin-only": true, "x-immich-history": [ @@ -4972,10 +4974,14 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v2.4.0", + "state": "Deprecated" } ], "x-immich-permission": "job.read", - "x-immich-state": "Stable" + "x-immich-state": "Deprecated" }, "post": { "description": "Run a specific job. Most jobs are queued automatically, but this endpoint allows for manual creation of a handful of jobs, including various cleanup tasks, as well as creating a new database backup.", @@ -5032,6 +5038,7 @@ }, "/jobs/{name}": { "put": { + "deprecated": true, "description": "Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.", "operationId": "runQueueCommandLegacy", "parameters": [ @@ -5059,7 +5066,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" } } }, @@ -5079,7 +5086,8 @@ ], "summary": "Run jobs", "tags": [ - "Jobs" + "Jobs", + "Deprecated" ], "x-immich-admin-only": true, "x-immich-history": [ @@ -5094,10 +5102,14 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v2.4.0", + "state": "Deprecated" } ], "x-immich-permission": "job.create", - "x-immich-state": "Stable" + "x-immich-state": "Deprecated" } }, "/libraries": { @@ -8113,6 +8125,303 @@ "x-immich-state": "Alpha" } }, + "/queues": { + "get": { + "description": "Retrieves a list of queues.", + "operationId": "getQueues", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/QueueResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "List all queues", + "tags": [ + "Queues" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.4.0", + "state": "Added" + }, + { + "version": "v2.4.0", + "state": "Alpha" + } + ], + "x-immich-permission": "queue.read", + "x-immich-state": "Alpha" + } + }, + "/queues/{name}": { + "get": { + "description": "Retrieves a specific queue by its name.", + "operationId": "getQueue", + "parameters": [ + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "$ref": "#/components/schemas/QueueName" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve a queue", + "tags": [ + "Queues" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.4.0", + "state": "Added" + }, + { + "version": "v2.4.0", + "state": "Alpha" + } + ], + "x-immich-permission": "queue.read", + "x-immich-state": "Alpha" + }, + "put": { + "description": "Change the paused status of a specific queue.", + "operationId": "updateQueue", + "parameters": [ + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "$ref": "#/components/schemas/QueueName" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Update a queue", + "tags": [ + "Queues" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.4.0", + "state": "Added" + }, + { + "version": "v2.4.0", + "state": "Alpha" + } + ], + "x-immich-permission": "queue.update", + "x-immich-state": "Alpha" + } + }, + "/queues/{name}/jobs": { + "delete": { + "description": "Removes all jobs from the specified queue.", + "operationId": "emptyQueue", + "parameters": [ + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "$ref": "#/components/schemas/QueueName" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueDeleteDto" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Empty a queue", + "tags": [ + "Queues" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.4.0", + "state": "Added" + }, + { + "version": "v2.4.0", + "state": "Alpha" + } + ], + "x-immich-permission": "queueJob.delete", + "x-immich-state": "Alpha" + }, + "get": { + "description": "Retrieves a list of queue jobs from the specified queue.", + "operationId": "getQueueJobs", + "parameters": [ + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "$ref": "#/components/schemas/QueueName" + } + }, + { + "name": "status", + "required": false, + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueueJobStatus" + } + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/QueueJobResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve queue jobs", + "tags": [ + "Queues" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v2.4.0", + "state": "Added" + }, + { + "version": "v2.4.0", + "state": "Alpha" + } + ], + "x-immich-permission": "queueJob.read", + "x-immich-state": "Alpha" + } + }, "/search/cities": { "get": { "description": "Retrieve a list of assets with each asset belonging to a different city. This endpoint is used on the places pages to show a single thumbnail for each city the user has assets in.", @@ -14092,6 +14401,10 @@ "name": "Plugins", "description": "A plugin is an installed module that makes filters and actions available for the workflow feature." }, + { + "name": "Queues", + "description": "Queues and background jobs are used for processing tasks asynchronously. Queues can be paused and resumed as needed." + }, { "name": "Search", "description": "Endpoints related to searching assets via text, smart search, optical character recognition (OCR), and other filters like person, album, and other metadata. Search endpoints usually support pagination and sorting." @@ -16340,6 +16653,66 @@ ], "type": "object" }, + "JobName": { + "enum": [ + "AssetDelete", + "AssetDeleteCheck", + "AssetDetectFacesQueueAll", + "AssetDetectFaces", + "AssetDetectDuplicatesQueueAll", + "AssetDetectDuplicates", + "AssetEncodeVideoQueueAll", + "AssetEncodeVideo", + "AssetEmptyTrash", + "AssetExtractMetadataQueueAll", + "AssetExtractMetadata", + "AssetFileMigration", + "AssetGenerateThumbnailsQueueAll", + "AssetGenerateThumbnails", + "AuditLogCleanup", + "AuditTableCleanup", + "DatabaseBackup", + "FacialRecognitionQueueAll", + "FacialRecognition", + "FileDelete", + "FileMigrationQueueAll", + "LibraryDeleteCheck", + "LibraryDelete", + "LibraryRemoveAsset", + "LibraryScanAssetsQueueAll", + "LibrarySyncAssets", + "LibrarySyncFilesQueueAll", + "LibrarySyncFiles", + "LibraryScanQueueAll", + "MemoryCleanup", + "MemoryGenerate", + "NotificationsCleanup", + "NotifyUserSignup", + "NotifyAlbumInvite", + "NotifyAlbumUpdate", + "UserDelete", + "UserDeleteCheck", + "UserSyncUsage", + "PersonCleanup", + "PersonFileMigration", + "PersonGenerateThumbnail", + "SessionCleanup", + "SendMail", + "SidecarQueueAll", + "SidecarCheck", + "SidecarWrite", + "SmartSearchQueueAll", + "SmartSearch", + "StorageTemplateMigration", + "StorageTemplateMigrationSingle", + "TagCleanup", + "VersionCheck", + "OcrQueueAll", + "Ocr", + "WorkflowRun" + ], + "type": "string" + }, "JobSettingsDto": { "properties": { "concurrency": { @@ -17632,6 +18005,12 @@ "userProfileImage.read", "userProfileImage.update", "userProfileImage.delete", + "queue.read", + "queue.update", + "queueJob.create", + "queueJob.read", + "queueJob.update", + "queueJob.delete", "workflow.create", "workflow.read", "workflow.update", @@ -18168,6 +18547,63 @@ ], "type": "object" }, + "QueueDeleteDto": { + "properties": { + "failed": { + "description": "If true, will also remove failed jobs from the queue.", + "type": "boolean", + "x-immich-history": [ + { + "version": "v2.4.0", + "state": "Added" + }, + { + "version": "v2.4.0", + "state": "Alpha" + } + ], + "x-immich-state": "Alpha" + } + }, + "type": "object" + }, + "QueueJobResponseDto": { + "properties": { + "data": { + "type": "object" + }, + "id": { + "type": "string" + }, + "name": { + "allOf": [ + { + "$ref": "#/components/schemas/JobName" + } + ] + }, + "timestamp": { + "type": "integer" + } + }, + "required": [ + "data", + "name", + "timestamp" + ], + "type": "object" + }, + "QueueJobStatus": { + "enum": [ + "active", + "failed", + "completed", + "delayed", + "waiting", + "paused" + ], + "type": "string" + }, "QueueName": { "enum": [ "thumbnailGeneration", @@ -18191,12 +18627,35 @@ "type": "string" }, "QueueResponseDto": { + "properties": { + "isPaused": { + "type": "boolean" + }, + "name": { + "allOf": [ + { + "$ref": "#/components/schemas/QueueName" + } + ] + }, + "statistics": { + "$ref": "#/components/schemas/QueueStatisticsDto" + } + }, + "required": [ + "isPaused", + "name", + "statistics" + ], + "type": "object" + }, + "QueueResponseLegacyDto": { "properties": { "jobCounts": { "$ref": "#/components/schemas/QueueStatisticsDto" }, "queueStatus": { - "$ref": "#/components/schemas/QueueStatusDto" + "$ref": "#/components/schemas/QueueStatusLegacyDto" } }, "required": [ @@ -18236,7 +18695,7 @@ ], "type": "object" }, - "QueueStatusDto": { + "QueueStatusLegacyDto": { "properties": { "isActive": { "type": "boolean" @@ -18251,58 +18710,66 @@ ], "type": "object" }, - "QueuesResponseDto": { + "QueueUpdateDto": { + "properties": { + "isPaused": { + "type": "boolean" + } + }, + "type": "object" + }, + "QueuesResponseLegacyDto": { "properties": { "backgroundTask": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "backupDatabase": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "duplicateDetection": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "faceDetection": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "facialRecognition": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "library": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "metadataExtraction": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "migration": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "notifications": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "ocr": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "search": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "sidecar": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "smartSearch": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "storageTemplateMigration": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "thumbnailGeneration": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "videoConversion": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, "workflow": { - "$ref": "#/components/schemas/QueueResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" } }, "required": [ diff --git a/open-api/typescript-sdk/.nvmrc b/open-api/typescript-sdk/.nvmrc index 0a492611a0..9e2934aa34 100644 --- a/open-api/typescript-sdk/.nvmrc +++ b/open-api/typescript-sdk/.nvmrc @@ -1 +1 @@ -24.11.0 +24.11.1 diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 6804f0a4fd..56520b0efe 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.19.1", + "@types/node": "^24.10.1", "typescript": "^5.3.3" }, "repository": { @@ -28,6 +28,6 @@ "directory": "open-api/typescript-sdk" }, "volta": { - "node": "24.11.0" + "node": "24.11.1" } } diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 73d41ee760..3b7d4fbccf 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -716,32 +716,32 @@ export type QueueStatisticsDto = { paused: number; waiting: number; }; -export type QueueStatusDto = { +export type QueueStatusLegacyDto = { isActive: boolean; isPaused: boolean; }; -export type QueueResponseDto = { +export type QueueResponseLegacyDto = { jobCounts: QueueStatisticsDto; - queueStatus: QueueStatusDto; + queueStatus: QueueStatusLegacyDto; }; -export type QueuesResponseDto = { - backgroundTask: QueueResponseDto; - backupDatabase: QueueResponseDto; - duplicateDetection: QueueResponseDto; - faceDetection: QueueResponseDto; - facialRecognition: QueueResponseDto; - library: QueueResponseDto; - metadataExtraction: QueueResponseDto; - migration: QueueResponseDto; - notifications: QueueResponseDto; - ocr: QueueResponseDto; - search: QueueResponseDto; - sidecar: QueueResponseDto; - smartSearch: QueueResponseDto; - storageTemplateMigration: QueueResponseDto; - thumbnailGeneration: QueueResponseDto; - videoConversion: QueueResponseDto; - workflow: QueueResponseDto; +export type QueuesResponseLegacyDto = { + backgroundTask: QueueResponseLegacyDto; + backupDatabase: QueueResponseLegacyDto; + duplicateDetection: QueueResponseLegacyDto; + faceDetection: QueueResponseLegacyDto; + facialRecognition: QueueResponseLegacyDto; + library: QueueResponseLegacyDto; + metadataExtraction: QueueResponseLegacyDto; + migration: QueueResponseLegacyDto; + notifications: QueueResponseLegacyDto; + ocr: QueueResponseLegacyDto; + search: QueueResponseLegacyDto; + sidecar: QueueResponseLegacyDto; + smartSearch: QueueResponseLegacyDto; + storageTemplateMigration: QueueResponseLegacyDto; + thumbnailGeneration: QueueResponseLegacyDto; + videoConversion: QueueResponseLegacyDto; + workflow: QueueResponseLegacyDto; }; export type JobCreateDto = { name: ManualJobName; @@ -973,6 +973,24 @@ export type PluginTriggerResponseDto = { schema: object | null; triggerType: PluginTriggerType; }; +export type QueueResponseDto = { + isPaused: boolean; + name: QueueName; + statistics: QueueStatisticsDto; +}; +export type QueueUpdateDto = { + isPaused?: boolean; +}; +export type QueueDeleteDto = { + /** If true, will also remove failed jobs from the queue. */ + failed?: boolean; +}; +export type QueueJobResponseDto = { + data: object; + id?: string; + name: JobName; + timestamp: number; +}; export type SearchExploreItem = { data: AssetResponseDto; value: string; @@ -2933,7 +2951,7 @@ export function reassignFacesById({ id, faceDto }: { export function getQueuesLegacy(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: QueuesResponseDto; + data: QueuesResponseLegacyDto; }>("/jobs", { ...opts })); @@ -2959,7 +2977,7 @@ export function runQueueCommandLegacy({ name, queueCommandDto }: { }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: QueueResponseDto; + data: QueueResponseLegacyDto; }>(`/jobs/${encodeURIComponent(name)}`, oazapfts.json({ ...opts, method: "PUT", @@ -3670,6 +3688,75 @@ export function getPlugin({ id }: { ...opts })); } +/** + * List all queues + */ +export function getQueues(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: QueueResponseDto[]; + }>("/queues", { + ...opts + })); +} +/** + * Retrieve a queue + */ +export function getQueue({ name }: { + name: QueueName; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: QueueResponseDto; + }>(`/queues/${encodeURIComponent(name)}`, { + ...opts + })); +} +/** + * Update a queue + */ +export function updateQueue({ name, queueUpdateDto }: { + name: QueueName; + queueUpdateDto: QueueUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: QueueResponseDto; + }>(`/queues/${encodeURIComponent(name)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: queueUpdateDto + }))); +} +/** + * Empty a queue + */ +export function emptyQueue({ name, queueDeleteDto }: { + name: QueueName; + queueDeleteDto: QueueDeleteDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/queues/${encodeURIComponent(name)}/jobs`, oazapfts.json({ + ...opts, + method: "DELETE", + body: queueDeleteDto + }))); +} +/** + * Retrieve queue jobs + */ +export function getQueueJobs({ name, status }: { + name: QueueName; + status?: QueueJobStatus[]; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: QueueJobResponseDto[]; + }>(`/queues/${encodeURIComponent(name)}/jobs${QS.query(QS.explode({ + status + }))}`, { + ...opts + })); +} /** * Retrieve assets by city */ @@ -5260,6 +5347,12 @@ export enum Permission { UserProfileImageRead = "userProfileImage.read", UserProfileImageUpdate = "userProfileImage.update", UserProfileImageDelete = "userProfileImage.delete", + QueueRead = "queue.read", + QueueUpdate = "queue.update", + QueueJobCreate = "queueJob.create", + QueueJobRead = "queueJob.read", + QueueJobUpdate = "queueJob.update", + QueueJobDelete = "queueJob.delete", WorkflowCreate = "workflow.create", WorkflowRead = "workflow.read", WorkflowUpdate = "workflow.update", @@ -5353,6 +5446,71 @@ export enum PluginTriggerType { AssetCreate = "AssetCreate", PersonRecognized = "PersonRecognized" } +export enum QueueJobStatus { + Active = "active", + Failed = "failed", + Completed = "completed", + Delayed = "delayed", + Waiting = "waiting", + Paused = "paused" +} +export enum JobName { + AssetDelete = "AssetDelete", + AssetDeleteCheck = "AssetDeleteCheck", + AssetDetectFacesQueueAll = "AssetDetectFacesQueueAll", + AssetDetectFaces = "AssetDetectFaces", + AssetDetectDuplicatesQueueAll = "AssetDetectDuplicatesQueueAll", + AssetDetectDuplicates = "AssetDetectDuplicates", + AssetEncodeVideoQueueAll = "AssetEncodeVideoQueueAll", + AssetEncodeVideo = "AssetEncodeVideo", + AssetEmptyTrash = "AssetEmptyTrash", + AssetExtractMetadataQueueAll = "AssetExtractMetadataQueueAll", + AssetExtractMetadata = "AssetExtractMetadata", + AssetFileMigration = "AssetFileMigration", + AssetGenerateThumbnailsQueueAll = "AssetGenerateThumbnailsQueueAll", + AssetGenerateThumbnails = "AssetGenerateThumbnails", + AuditLogCleanup = "AuditLogCleanup", + AuditTableCleanup = "AuditTableCleanup", + DatabaseBackup = "DatabaseBackup", + FacialRecognitionQueueAll = "FacialRecognitionQueueAll", + FacialRecognition = "FacialRecognition", + FileDelete = "FileDelete", + FileMigrationQueueAll = "FileMigrationQueueAll", + LibraryDeleteCheck = "LibraryDeleteCheck", + LibraryDelete = "LibraryDelete", + LibraryRemoveAsset = "LibraryRemoveAsset", + LibraryScanAssetsQueueAll = "LibraryScanAssetsQueueAll", + LibrarySyncAssets = "LibrarySyncAssets", + LibrarySyncFilesQueueAll = "LibrarySyncFilesQueueAll", + LibrarySyncFiles = "LibrarySyncFiles", + LibraryScanQueueAll = "LibraryScanQueueAll", + MemoryCleanup = "MemoryCleanup", + MemoryGenerate = "MemoryGenerate", + NotificationsCleanup = "NotificationsCleanup", + NotifyUserSignup = "NotifyUserSignup", + NotifyAlbumInvite = "NotifyAlbumInvite", + NotifyAlbumUpdate = "NotifyAlbumUpdate", + UserDelete = "UserDelete", + UserDeleteCheck = "UserDeleteCheck", + UserSyncUsage = "UserSyncUsage", + PersonCleanup = "PersonCleanup", + PersonFileMigration = "PersonFileMigration", + PersonGenerateThumbnail = "PersonGenerateThumbnail", + SessionCleanup = "SessionCleanup", + SendMail = "SendMail", + SidecarQueueAll = "SidecarQueueAll", + SidecarCheck = "SidecarCheck", + SidecarWrite = "SidecarWrite", + SmartSearchQueueAll = "SmartSearchQueueAll", + SmartSearch = "SmartSearch", + StorageTemplateMigration = "StorageTemplateMigration", + StorageTemplateMigrationSingle = "StorageTemplateMigrationSingle", + TagCleanup = "TagCleanup", + VersionCheck = "VersionCheck", + OcrQueueAll = "OcrQueueAll", + Ocr = "Ocr", + WorkflowRun = "WorkflowRun" +} export enum SearchSuggestionType { Country = "country", State = "state", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8cb0d95b38..471f427bf0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,11 +63,11 @@ importers: specifier: ^4.13.1 version: 4.13.4 '@types/node': - specifier: ^22.19.1 - version: 22.19.1 + specifier: ^24.10.1 + version: 24.10.1 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) byte-size: specifier: ^9.0.0 version: 9.0.1 @@ -109,16 +109,16 @@ importers: version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.0.0 - version: 7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + version: 7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.0.0 - version: 5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) vitest-fetch-mock: specifier: ^0.4.0 - version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) yaml: specifier: ^2.3.1 version: 2.8.1 @@ -214,8 +214,8 @@ importers: specifier: ^3.4.2 version: 3.7.1 '@types/node': - specifier: ^22.19.1 - version: 22.19.1 + specifier: ^24.10.1 + version: 24.10.1 '@types/oidc-provider': specifier: ^9.0.0 version: 9.5.0 @@ -290,7 +290,7 @@ importers: version: 5.2.1(encoding@0.1.13) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) open-api/typescript-sdk: dependencies: @@ -299,8 +299,8 @@ importers: version: 1.0.4 devDependencies: '@types/node': - specifier: ^22.19.1 - version: 22.19.1 + specifier: ^24.10.1 + version: 24.10.1 typescript: specifier: ^5.3.3 version: 5.9.3 @@ -480,7 +480,7 @@ importers: version: 2.0.2 nest-commander: specifier: ^3.16.0 - version: 3.20.1(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(@types/inquirer@8.2.11)(@types/node@22.19.1)(typescript@5.9.3) + version: 3.20.1(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(@types/inquirer@8.2.11)(@types/node@24.10.1)(typescript@5.9.3) nestjs-cls: specifier: ^5.0.0 version: 5.4.3(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -562,7 +562,7 @@ importers: version: 9.38.0 '@nestjs/cli': specifier: ^11.0.2 - version: 11.0.10(@swc/core@1.14.0(@swc/helpers@0.5.17))(@types/node@22.19.1) + version: 11.0.10(@swc/core@1.14.0(@swc/helpers@0.5.17))(@types/node@24.10.1) '@nestjs/schematics': specifier: ^11.0.0 version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) @@ -573,8 +573,8 @@ importers: specifier: ^1.4.14 version: 1.14.0(@swc/helpers@0.5.17) '@types/archiver': - specifier: ^6.0.0 - version: 6.0.4 + specifier: ^7.0.0 + version: 7.0.0 '@types/async-lock': specifier: ^1.4.2 version: 1.4.2 @@ -615,8 +615,8 @@ importers: specifier: ^2.0.0 version: 2.0.0 '@types/node': - specifier: ^22.19.1 - version: 22.19.1 + specifier: ^24.10.1 + version: 24.10.1 '@types/nodemailer': specifier: ^7.0.0 version: 7.0.3 @@ -646,7 +646,7 @@ importers: version: 13.15.4 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) eslint: specifier: ^9.14.0 version: 9.38.0(jiti@2.6.1) @@ -700,10 +700,10 @@ importers: version: 1.5.8(@swc/core@1.14.0(@swc/helpers@0.5.17))(rollup@4.52.5) vite-tsconfig-paths: specifier: ^5.0.0 - version: 5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) web: dependencies: @@ -717,8 +717,8 @@ importers: specifier: file:../open-api/typescript-sdk version: link:../open-api/typescript-sdk '@immich/ui': - specifier: ^0.46.0 - version: 0.46.0(svelte@5.43.0) + specifier: ^0.47.0 + version: 0.47.0(svelte@5.43.0) '@mapbox/mapbox-gl-rtl-text': specifier: 0.2.3 version: 0.2.3(mapbox-gl@1.13.3) @@ -836,25 +836,25 @@ importers: version: 3.1.2 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.10(@sveltejs/kit@2.48.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))) + version: 3.0.10(@sveltejs/kit@2.48.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))) '@sveltejs/enhanced-img': specifier: ^0.8.0 - version: 0.8.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(rollup@4.52.5)(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + version: 0.8.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(rollup@4.52.5)(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) '@sveltejs/kit': specifier: ^2.27.1 - version: 2.48.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + version: 2.48.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) '@sveltejs/vite-plugin-svelte': specifier: 6.2.1 - version: 6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + version: 6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) '@tailwindcss/vite': specifier: ^4.1.7 - version: 4.1.16(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + version: 4.1.16(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) '@testing-library/jest-dom': specifier: ^6.4.2 version: 6.9.1 '@testing-library/svelte': specifier: ^5.2.8 - version: 5.2.8(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.0)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + version: 5.2.8(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) '@testing-library/user-event': specifier: ^14.5.2 version: 14.6.1(@testing-library/dom@10.4.0) @@ -878,7 +878,7 @@ importers: version: 1.5.6 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.0)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) dotenv: specifier: ^17.0.0 version: 17.2.3 @@ -938,10 +938,10 @@ importers: version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.1.2 - version: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + version: 7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.0)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) packages: @@ -2862,8 +2862,8 @@ packages: peerDependencies: svelte: ^5.0.0 - '@immich/ui@0.46.0': - resolution: {integrity: sha512-d/X41oYmglAiMg129qZDaVYUZxC6HB5v31viM7ZDmxTdFDXaM61AKv4RbbbLs69Nd4f94LM68xGKsCOPcol18g==} + '@immich/ui@0.47.0': + resolution: {integrity: sha512-cKydhXtgBLaJNlCX9JsDN5xYi9X9dosg2g6tcrID1L33fmBDBVQMf73TpAYUG9NJx0XtAvjlTkB81L/61O1huw==} peerDependencies: svelte: ^5.0.0 @@ -4632,8 +4632,8 @@ packages: '@types/accepts@1.3.7': resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} - '@types/archiver@6.0.4': - resolution: {integrity: sha512-ULdQpARQ3sz9WH4nb98mJDYA0ft2A8C4f4fovvUcFwINa1cgGjY36JCAYuP5YypRq4mco1lJp1/7jEMS2oR0Hg==} + '@types/archiver@7.0.0': + resolution: {integrity: sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==} '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -4862,11 +4862,8 @@ packages: '@types/node@20.19.24': resolution: {integrity: sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==} - '@types/node@22.19.1': - resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} - - '@types/node@24.10.0': - resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==} + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} '@types/nodemailer@7.0.3': resolution: {integrity: sha512-fC8w49YQ868IuPWRXqPfLf+MuTRex5Z1qxMoG8rr70riqqbOp2F5xgOKE9fODEBPzpnvjkJXFgK6IL2xgMSTnA==} @@ -12057,11 +12054,11 @@ snapshots: optionalDependencies: chokidar: 4.0.3 - '@angular-devkit/schematics-cli@19.2.15(@types/node@22.19.1)(chokidar@4.0.3)': + '@angular-devkit/schematics-cli@19.2.15(@types/node@24.10.1)(chokidar@4.0.3)': dependencies: '@angular-devkit/core': 19.2.15(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.15(chokidar@4.0.3) - '@inquirer/prompts': 7.3.2(@types/node@22.19.1) + '@inquirer/prompts': 7.3.2(@types/node@24.10.1) ansi-colors: 4.1.3 symbol-observable: 4.0.0 yargs-parser: 21.1.1 @@ -14757,7 +14754,7 @@ snapshots: dependencies: svelte: 5.43.0 - '@immich/ui@0.46.0(svelte@5.43.0)': + '@immich/ui@0.47.0(svelte@5.43.0)': dependencies: '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.43.0) '@internationalized/date': 3.10.0 @@ -14771,27 +14768,27 @@ snapshots: tailwind-variants: 3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.16) tailwindcss: 4.1.16 - '@inquirer/checkbox@4.2.1(@types/node@22.19.1)': + '@inquirer/checkbox@4.2.1(@types/node@24.10.1)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.19.1) + '@inquirer/core': 10.1.15(@types/node@24.10.1) '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@22.19.1) + '@inquirer/type': 3.0.8(@types/node@24.10.1) ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@inquirer/confirm@5.1.15(@types/node@22.19.1)': + '@inquirer/confirm@5.1.15(@types/node@24.10.1)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.19.1) - '@inquirer/type': 3.0.8(@types/node@22.19.1) + '@inquirer/core': 10.1.15(@types/node@24.10.1) + '@inquirer/type': 3.0.8(@types/node@24.10.1) optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@inquirer/core@10.1.15(@types/node@22.19.1)': + '@inquirer/core@10.1.15(@types/node@24.10.1)': dependencies: '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@22.19.1) + '@inquirer/type': 3.0.8(@types/node@24.10.1) ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 @@ -14799,115 +14796,115 @@ snapshots: wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@inquirer/editor@4.2.17(@types/node@22.19.1)': + '@inquirer/editor@4.2.17(@types/node@24.10.1)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.19.1) - '@inquirer/external-editor': 1.0.2(@types/node@22.19.1) - '@inquirer/type': 3.0.8(@types/node@22.19.1) + '@inquirer/core': 10.1.15(@types/node@24.10.1) + '@inquirer/external-editor': 1.0.2(@types/node@24.10.1) + '@inquirer/type': 3.0.8(@types/node@24.10.1) optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@inquirer/expand@4.0.17(@types/node@22.19.1)': + '@inquirer/expand@4.0.17(@types/node@24.10.1)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.19.1) - '@inquirer/type': 3.0.8(@types/node@22.19.1) + '@inquirer/core': 10.1.15(@types/node@24.10.1) + '@inquirer/type': 3.0.8(@types/node@24.10.1) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@inquirer/external-editor@1.0.2(@types/node@22.19.1)': + '@inquirer/external-editor@1.0.2(@types/node@24.10.1)': dependencies: chardet: 2.1.0 iconv-lite: 0.7.0 optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@inquirer/figures@1.0.13': {} - '@inquirer/input@4.2.1(@types/node@22.19.1)': + '@inquirer/input@4.2.1(@types/node@24.10.1)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.19.1) - '@inquirer/type': 3.0.8(@types/node@22.19.1) + '@inquirer/core': 10.1.15(@types/node@24.10.1) + '@inquirer/type': 3.0.8(@types/node@24.10.1) optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@inquirer/number@3.0.17(@types/node@22.19.1)': + '@inquirer/number@3.0.17(@types/node@24.10.1)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.19.1) - '@inquirer/type': 3.0.8(@types/node@22.19.1) + '@inquirer/core': 10.1.15(@types/node@24.10.1) + '@inquirer/type': 3.0.8(@types/node@24.10.1) optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@inquirer/password@4.0.17(@types/node@22.19.1)': + '@inquirer/password@4.0.17(@types/node@24.10.1)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.19.1) - '@inquirer/type': 3.0.8(@types/node@22.19.1) + '@inquirer/core': 10.1.15(@types/node@24.10.1) + '@inquirer/type': 3.0.8(@types/node@24.10.1) ansi-escapes: 4.3.2 optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@inquirer/prompts@7.3.2(@types/node@22.19.1)': + '@inquirer/prompts@7.3.2(@types/node@24.10.1)': dependencies: - '@inquirer/checkbox': 4.2.1(@types/node@22.19.1) - '@inquirer/confirm': 5.1.15(@types/node@22.19.1) - '@inquirer/editor': 4.2.17(@types/node@22.19.1) - '@inquirer/expand': 4.0.17(@types/node@22.19.1) - '@inquirer/input': 4.2.1(@types/node@22.19.1) - '@inquirer/number': 3.0.17(@types/node@22.19.1) - '@inquirer/password': 4.0.17(@types/node@22.19.1) - '@inquirer/rawlist': 4.1.5(@types/node@22.19.1) - '@inquirer/search': 3.1.0(@types/node@22.19.1) - '@inquirer/select': 4.3.1(@types/node@22.19.1) + '@inquirer/checkbox': 4.2.1(@types/node@24.10.1) + '@inquirer/confirm': 5.1.15(@types/node@24.10.1) + '@inquirer/editor': 4.2.17(@types/node@24.10.1) + '@inquirer/expand': 4.0.17(@types/node@24.10.1) + '@inquirer/input': 4.2.1(@types/node@24.10.1) + '@inquirer/number': 3.0.17(@types/node@24.10.1) + '@inquirer/password': 4.0.17(@types/node@24.10.1) + '@inquirer/rawlist': 4.1.5(@types/node@24.10.1) + '@inquirer/search': 3.1.0(@types/node@24.10.1) + '@inquirer/select': 4.3.1(@types/node@24.10.1) optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@inquirer/prompts@7.8.0(@types/node@22.19.1)': + '@inquirer/prompts@7.8.0(@types/node@24.10.1)': dependencies: - '@inquirer/checkbox': 4.2.1(@types/node@22.19.1) - '@inquirer/confirm': 5.1.15(@types/node@22.19.1) - '@inquirer/editor': 4.2.17(@types/node@22.19.1) - '@inquirer/expand': 4.0.17(@types/node@22.19.1) - '@inquirer/input': 4.2.1(@types/node@22.19.1) - '@inquirer/number': 3.0.17(@types/node@22.19.1) - '@inquirer/password': 4.0.17(@types/node@22.19.1) - '@inquirer/rawlist': 4.1.5(@types/node@22.19.1) - '@inquirer/search': 3.1.0(@types/node@22.19.1) - '@inquirer/select': 4.3.1(@types/node@22.19.1) + '@inquirer/checkbox': 4.2.1(@types/node@24.10.1) + '@inquirer/confirm': 5.1.15(@types/node@24.10.1) + '@inquirer/editor': 4.2.17(@types/node@24.10.1) + '@inquirer/expand': 4.0.17(@types/node@24.10.1) + '@inquirer/input': 4.2.1(@types/node@24.10.1) + '@inquirer/number': 3.0.17(@types/node@24.10.1) + '@inquirer/password': 4.0.17(@types/node@24.10.1) + '@inquirer/rawlist': 4.1.5(@types/node@24.10.1) + '@inquirer/search': 3.1.0(@types/node@24.10.1) + '@inquirer/select': 4.3.1(@types/node@24.10.1) optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@inquirer/rawlist@4.1.5(@types/node@22.19.1)': + '@inquirer/rawlist@4.1.5(@types/node@24.10.1)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.19.1) - '@inquirer/type': 3.0.8(@types/node@22.19.1) + '@inquirer/core': 10.1.15(@types/node@24.10.1) + '@inquirer/type': 3.0.8(@types/node@24.10.1) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@inquirer/search@3.1.0(@types/node@22.19.1)': + '@inquirer/search@3.1.0(@types/node@24.10.1)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.19.1) + '@inquirer/core': 10.1.15(@types/node@24.10.1) '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@22.19.1) + '@inquirer/type': 3.0.8(@types/node@24.10.1) yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@inquirer/select@4.3.1(@types/node@22.19.1)': + '@inquirer/select@4.3.1(@types/node@24.10.1)': dependencies: - '@inquirer/core': 10.1.15(@types/node@22.19.1) + '@inquirer/core': 10.1.15(@types/node@24.10.1) '@inquirer/figures': 1.0.13 - '@inquirer/type': 3.0.8(@types/node@22.19.1) + '@inquirer/type': 3.0.8(@types/node@24.10.1) ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@inquirer/type@3.0.8(@types/node@22.19.1)': + '@inquirer/type@3.0.8(@types/node@24.10.1)': optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@internationalized/date@3.10.0': dependencies: @@ -14945,7 +14942,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/yargs': 17.0.34 chalk: 4.1.2 @@ -15241,12 +15238,12 @@ snapshots: bullmq: 5.62.1 tslib: 2.8.1 - '@nestjs/cli@11.0.10(@swc/core@1.14.0(@swc/helpers@0.5.17))(@types/node@22.19.1)': + '@nestjs/cli@11.0.10(@swc/core@1.14.0(@swc/helpers@0.5.17))(@types/node@24.10.1)': dependencies: '@angular-devkit/core': 19.2.15(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.15(chokidar@4.0.3) - '@angular-devkit/schematics-cli': 19.2.15(@types/node@22.19.1)(chokidar@4.0.3) - '@inquirer/prompts': 7.8.0(@types/node@22.19.1) + '@angular-devkit/schematics-cli': 19.2.15(@types/node@24.10.1)(chokidar@4.0.3) + '@inquirer/prompts': 7.8.0(@types/node@24.10.1) '@nestjs/schematics': 11.0.9(chokidar@4.0.3)(typescript@5.8.3) ansis: 4.1.0 chokidar: 4.0.3 @@ -16369,29 +16366,29 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.48.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.48.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))': dependencies: - '@sveltejs/kit': 2.48.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + '@sveltejs/kit': 2.48.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) - '@sveltejs/enhanced-img@0.8.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(rollup@4.52.5)(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': + '@sveltejs/enhanced-img@0.8.4(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(rollup@4.52.5)(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) magic-string: 0.30.21 sharp: 0.34.4 svelte: 5.43.0 svelte-parse-markup: 0.1.5(svelte@5.43.0) - vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) vite-imagetools: 8.0.0(rollup@4.52.5) zimmerframe: 1.1.4 transitivePeerDependencies: - rollup - supports-color - '@sveltejs/kit@2.48.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': + '@sveltejs/kit@2.48.3(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -16404,28 +16401,28 @@ snapshots: set-cookie-parser: 2.7.2 sirv: 3.0.2 svelte: 5.43.0 - vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) optionalDependencies: '@opentelemetry/api': 1.9.0 - '@sveltejs/vite-plugin-svelte-inspector@5.0.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) debug: 4.4.3 svelte: 5.43.0 - vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) debug: 4.4.3 deepmerge: 4.3.1 magic-string: 0.30.21 svelte: 5.43.0 - vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) - vitefu: 1.1.1(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + vite: 7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vitefu: 1.1.1(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) transitivePeerDependencies: - supports-color @@ -16644,12 +16641,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.16 '@tailwindcss/oxide-win32-x64-msvc': 4.1.16 - '@tailwindcss/vite@4.1.16(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': + '@tailwindcss/vite@4.1.16(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': dependencies: '@tailwindcss/node': 4.1.16 '@tailwindcss/oxide': 4.1.16 tailwindcss: 4.1.16 - vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) '@testing-library/dom@10.4.0': dependencies: @@ -16671,13 +16668,13 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/svelte@5.2.8(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.0)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': + '@testing-library/svelte@5.2.8(svelte@5.43.0)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': dependencies: '@testing-library/dom': 10.4.0 svelte: 5.43.0 optionalDependencies: - vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.0)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.0)': dependencies: @@ -16719,9 +16716,9 @@ snapshots: '@types/accepts@1.3.7': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@types/archiver@6.0.4': + '@types/archiver@7.0.0': dependencies: '@types/readdir-glob': 1.1.5 @@ -16731,16 +16728,16 @@ snapshots: '@types/bcrypt@6.0.0': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/bonjour@3.5.13': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/braces@3.0.5': {} @@ -16761,21 +16758,21 @@ snapshots: '@types/cli-progress@3.11.6': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/compression@1.8.1': dependencies: '@types/express': 5.0.5 - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 5.1.0 - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/connect@3.4.38': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/content-disposition@0.5.9': {} @@ -16792,11 +16789,11 @@ snapshots: '@types/connect': 3.4.38 '@types/express': 5.0.5 '@types/keygrip': 1.0.6 - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/cors@2.8.19': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/debug@4.1.12': dependencies: @@ -16806,13 +16803,13 @@ snapshots: '@types/docker-modem@3.0.6': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/ssh2': 1.15.5 '@types/dockerode@3.3.45': dependencies: '@types/docker-modem': 3.0.6 - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/ssh2': 1.15.5 '@types/dom-to-image@2.6.7': {} @@ -16835,14 +16832,14 @@ snapshots: '@types/express-serve-static-core@4.19.7': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 '@types/express-serve-static-core@5.1.0': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -16868,7 +16865,7 @@ snapshots: '@types/fluent-ffmpeg@2.1.28': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/geojson-vt@3.2.5': dependencies: @@ -16900,7 +16897,7 @@ snapshots: '@types/http-proxy@1.17.17': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/inquirer@8.2.11': dependencies: @@ -16924,7 +16921,7 @@ snapshots: '@types/jsonwebtoken@9.0.10': dependencies: '@types/ms': 2.1.0 - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/justified-layout@4.1.4': {} @@ -16943,7 +16940,7 @@ snapshots: '@types/http-errors': 2.0.5 '@types/keygrip': 1.0.6 '@types/koa-compose': 3.2.8 - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/leaflet@1.9.21': dependencies: @@ -16973,7 +16970,7 @@ snapshots: '@types/mock-fs@4.13.4': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/ms@2.1.0': {} @@ -16983,7 +16980,7 @@ snapshots: '@types/node-forge@1.3.14': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/node@17.0.45': {} @@ -16995,19 +16992,14 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@22.19.1': - dependencies: - undici-types: 6.21.0 - - '@types/node@24.10.0': + '@types/node@24.10.1': dependencies: undici-types: 7.16.0 - optional: true '@types/nodemailer@7.0.3': dependencies: '@aws-sdk/client-sesv2': 3.919.0 - '@types/node': 22.19.1 + '@types/node': 24.10.1 transitivePeerDependencies: - aws-crt @@ -17015,7 +17007,7 @@ snapshots: dependencies: '@types/keygrip': 1.0.6 '@types/koa': 3.0.0 - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/parse5@5.0.3': {} @@ -17025,13 +17017,13 @@ snapshots: '@types/pg@8.15.5': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 pg-protocol: 1.10.3 pg-types: 2.2.0 '@types/pg@8.15.6': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 pg-protocol: 1.10.3 pg-types: 2.2.0 @@ -17039,13 +17031,13 @@ snapshots: '@types/pngjs@6.0.5': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/prismjs@1.26.5': {} '@types/qrcode@1.5.6': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/qs@6.14.0': {} @@ -17074,7 +17066,7 @@ snapshots: '@types/readdir-glob@1.1.5': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/retry@0.12.2': {} @@ -17084,18 +17076,18 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/semver@7.7.1': {} '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/send@1.2.1': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/serve-index@1.9.4': dependencies: @@ -17104,20 +17096,20 @@ snapshots: '@types/serve-static@1.15.10': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/send': 0.17.6 '@types/sockjs@0.3.36': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/ssh2-streams@0.1.13': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/ssh2@0.5.52': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/ssh2-streams': 0.1.13 '@types/ssh2@1.15.5': @@ -17128,7 +17120,7 @@ snapshots: dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 22.19.1 + '@types/node': 24.10.1 form-data: 4.0.4 '@types/supercluster@7.1.3': @@ -17142,7 +17134,7 @@ snapshots: '@types/through@0.0.33': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/ua-parser-js@0.7.39': {} @@ -17156,7 +17148,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@types/yargs-parser@21.0.3': {} @@ -17261,7 +17253,7 @@ snapshots: '@vercel/oidc@3.0.3': {} - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -17276,26 +17268,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) - transitivePeerDependencies: - - supports-color - - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.0)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': - dependencies: - '@ampproject/remapping': 2.3.0 - '@bcoe/v8-coverage': 1.0.2 - ast-v8-to-istanbul: 0.3.3 - debug: 4.4.3 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.1.7 - magic-string: 0.30.21 - magicast: 0.3.5 - std-env: 3.10.0 - test-exclude: 7.0.1 - tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.0)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -17307,21 +17280,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) - - '@vitest/mocker@3.2.4(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -18918,7 +18883,7 @@ snapshots: engine.io@6.6.4: dependencies: '@types/cors': 2.8.19 - '@types/node': 22.19.1 + '@types/node': 24.10.1 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 @@ -19307,7 +19272,7 @@ snapshots: eval@0.1.8: dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 require-like: 0.1.2 event-emitter@0.3.5: @@ -20300,9 +20265,9 @@ snapshots: inline-style-parser@0.2.4: {} - inquirer@8.2.7(@types/node@22.19.1): + inquirer@8.2.7(@types/node@24.10.1): dependencies: - '@inquirer/external-editor': 1.0.2(@types/node@22.19.1) + '@inquirer/external-editor': 1.0.2(@types/node@24.10.1) ansi-escapes: 4.3.2 chalk: 4.1.2 cli-cursor: 3.1.0 @@ -20516,7 +20481,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.19.1 + '@types/node': 24.10.1 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -20524,13 +20489,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -21810,7 +21775,7 @@ snapshots: neo-async@2.6.2: {} - nest-commander@3.20.1(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(@types/inquirer@8.2.11)(@types/node@22.19.1)(typescript@5.9.3): + nest-commander@3.20.1(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8)(@types/inquirer@8.2.11)(@types/node@24.10.1)(typescript@5.9.3): dependencies: '@fig/complete-commander': 3.2.0(commander@11.1.0) '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@11.1.8(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.8) @@ -21819,7 +21784,7 @@ snapshots: '@types/inquirer': 8.2.11 commander: 11.1.0 cosmiconfig: 8.3.6(typescript@5.9.3) - inquirer: 8.2.7(@types/node@22.19.1) + inquirer: 8.2.7(@types/node@24.10.1) transitivePeerDependencies: - '@types/node' - typescript @@ -22920,7 +22885,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 22.19.1 + '@types/node': 24.10.1 long: 5.3.2 protocol-buffers-schema@3.6.0: {} @@ -24630,8 +24595,7 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.16.0: - optional: true + undici-types@7.16.0: {} undici@7.16.0: {} @@ -24876,13 +24840,13 @@ snapshots: - rollup - supports-color - vite-node@3.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1): + vite-node@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -24897,39 +24861,18 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) transitivePeerDependencies: - supports-color - typescript - vite@7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1): + vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -24938,7 +24881,7 @@ snapshots: rollup: 4.52.5 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 @@ -24946,36 +24889,19 @@ snapshots: terser: 5.44.0 yaml: 2.8.1 - vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1): - dependencies: - esbuild: 0.25.12 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.52.5 - tinyglobby: 0.2.15 + vitefu@1.1.1(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)): optionalDependencies: - '@types/node': 24.10.0 - fsevents: 2.3.3 - jiti: 2.6.1 - lightningcss: 1.30.2 - sass: 1.94.2 - terser: 5.44.0 - yaml: 2.8.1 + vite: 7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) - vitefu@1.1.1(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)): - optionalDependencies: - vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) - - vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)): + vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)): dependencies: - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -24993,12 +24919,12 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 22.19.1 + '@types/node': 24.10.1 happy-dom: 20.0.10 jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13)) transitivePeerDependencies: @@ -25015,11 +24941,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -25037,56 +24963,12 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.12(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.1.12(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 22.19.1 - happy-dom: 20.0.10 - jsdom: 26.1.0(canvas@2.11.2) - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.0)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1): - dependencies: - '@types/chai': 5.2.2 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.2.0 - debug: 4.4.3 - expect-type: 1.2.1 - magic-string: 0.30.21 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@24.10.0)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.94.2)(terser@5.44.0)(yaml@2.8.1) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/debug': 4.1.12 - '@types/node': 24.10.0 + '@types/node': 24.10.1 happy-dom: 20.0.10 jsdom: 26.1.0(canvas@2.11.2) transitivePeerDependencies: diff --git a/server/.nvmrc b/server/.nvmrc index 0a492611a0..9e2934aa34 100644 --- a/server/.nvmrc +++ b/server/.nvmrc @@ -1 +1 @@ -24.11.0 +24.11.1 diff --git a/server/package.json b/server/package.json index b54ff53f1f..05362b3cea 100644 --- a/server/package.json +++ b/server/package.json @@ -120,7 +120,7 @@ "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.4", "@swc/core": "^1.4.14", - "@types/archiver": "^6.0.0", + "@types/archiver": "^7.0.0", "@types/async-lock": "^1.4.2", "@types/bcrypt": "^6.0.0", "@types/body-parser": "^1.19.6", @@ -134,7 +134,7 @@ "@types/luxon": "^3.6.2", "@types/mock-fs": "^4.13.1", "@types/multer": "^2.0.0", - "@types/node": "^22.19.1", + "@types/node": "^24.10.1", "@types/nodemailer": "^7.0.0", "@types/picomatch": "^4.0.0", "@types/pngjs": "^6.0.5", @@ -166,7 +166,7 @@ "vitest": "^3.0.0" }, "volta": { - "node": "24.11.0" + "node": "24.11.1" }, "overrides": { "sharp": "^0.34.4" diff --git a/server/src/constants.ts b/server/src/constants.ts index 68534c00e9..33f8e3b4c5 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -163,6 +163,8 @@ export const endpointTags: Record = { '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]: 'A plugin is an installed module that makes filters and actions available for the workflow feature.', + [ApiTag.Queues]: + 'Queues and background jobs are used for processing tasks asynchronously. Queues can be paused and resumed as needed.', [ApiTag.Search]: 'Endpoints related to searching assets via text, smart search, optical character recognition (OCR), and other filters like person, album, and other metadata. Search endpoints usually support pagination and sorting.', [ApiTag.Server]: diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index d5811de48c..6ba3d38a73 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -20,6 +20,7 @@ import { OAuthController } from 'src/controllers/oauth.controller'; import { PartnerController } from 'src/controllers/partner.controller'; import { PersonController } from 'src/controllers/person.controller'; import { PluginController } from 'src/controllers/plugin.controller'; +import { QueueController } from 'src/controllers/queue.controller'; import { SearchController } from 'src/controllers/search.controller'; import { ServerController } from 'src/controllers/server.controller'; import { SessionController } from 'src/controllers/session.controller'; @@ -59,6 +60,7 @@ export const controllers = [ PartnerController, PersonController, PluginController, + QueueController, SearchController, ServerController, SessionController, diff --git a/server/src/controllers/job.controller.ts b/server/src/controllers/job.controller.ts index 977f1e0f1e..783d5a3133 100644 --- a/server/src/controllers/job.controller.ts +++ b/server/src/controllers/job.controller.ts @@ -1,10 +1,12 @@ import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Endpoint, HistoryBuilder } from 'src/decorators'; +import { AuthDto } from 'src/dtos/auth.dto'; import { JobCreateDto } from 'src/dtos/job.dto'; -import { QueueCommandDto, QueueNameParamDto, QueueResponseDto, QueuesResponseDto } from 'src/dtos/queue.dto'; +import { QueueResponseLegacyDto, QueuesResponseLegacyDto } from 'src/dtos/queue-legacy.dto'; +import { QueueCommandDto, QueueNameParamDto } from 'src/dtos/queue.dto'; import { ApiTag, Permission } from 'src/enum'; -import { Authenticated } from 'src/middleware/auth.guard'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { JobService } from 'src/services/job.service'; import { QueueService } from 'src/services/queue.service'; @@ -21,10 +23,10 @@ export class JobController { @Endpoint({ summary: 'Retrieve queue counts and status', description: 'Retrieve the counts of the current queue, as well as the current status.', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), + history: new HistoryBuilder().added('v1').beta('v1').stable('v2').deprecated('v2.4.0'), }) - getQueuesLegacy(): Promise { - return this.queueService.getAll(); + getQueuesLegacy(@Auth() auth: AuthDto): Promise { + return this.queueService.getAllLegacy(auth); } @Post() @@ -46,9 +48,12 @@ export class JobController { summary: 'Run jobs', description: 'Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), + history: new HistoryBuilder().added('v1').beta('v1').stable('v2').deprecated('v2.4.0'), }) - runQueueCommandLegacy(@Param() { name }: QueueNameParamDto, @Body() dto: QueueCommandDto): Promise { - return this.queueService.runCommand(name, dto); + runQueueCommandLegacy( + @Param() { name }: QueueNameParamDto, + @Body() dto: QueueCommandDto, + ): Promise { + return this.queueService.runCommandLegacy(name, dto); } } diff --git a/server/src/controllers/queue.controller.ts b/server/src/controllers/queue.controller.ts new file mode 100644 index 0000000000..1d8d918c5f --- /dev/null +++ b/server/src/controllers/queue.controller.ts @@ -0,0 +1,85 @@ +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Put, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Endpoint, HistoryBuilder } from 'src/decorators'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { + QueueDeleteDto, + QueueJobResponseDto, + QueueJobSearchDto, + QueueNameParamDto, + QueueResponseDto, + QueueUpdateDto, +} from 'src/dtos/queue.dto'; +import { ApiTag, Permission } from 'src/enum'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { QueueService } from 'src/services/queue.service'; + +@ApiTags(ApiTag.Queues) +@Controller('queues') +export class QueueController { + constructor(private service: QueueService) {} + + @Get() + @Authenticated({ permission: Permission.QueueRead, admin: true }) + @Endpoint({ + summary: 'List all queues', + description: 'Retrieves a list of queues.', + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), + }) + getQueues(@Auth() auth: AuthDto): Promise { + return this.service.getAll(auth); + } + + @Get(':name') + @Authenticated({ permission: Permission.QueueRead, admin: true }) + @Endpoint({ + summary: 'Retrieve a queue', + description: 'Retrieves a specific queue by its name.', + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), + }) + getQueue(@Auth() auth: AuthDto, @Param() { name }: QueueNameParamDto): Promise { + return this.service.get(auth, name); + } + + @Put(':name') + @Authenticated({ permission: Permission.QueueUpdate, admin: true }) + @Endpoint({ + summary: 'Update a queue', + description: 'Change the paused status of a specific queue.', + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), + }) + updateQueue( + @Auth() auth: AuthDto, + @Param() { name }: QueueNameParamDto, + @Body() dto: QueueUpdateDto, + ): Promise { + return this.service.update(auth, name, dto); + } + + @Get(':name/jobs') + @Authenticated({ permission: Permission.QueueJobRead, admin: true }) + @Endpoint({ + summary: 'Retrieve queue jobs', + description: 'Retrieves a list of queue jobs from the specified queue.', + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), + }) + getQueueJobs( + @Auth() auth: AuthDto, + @Param() { name }: QueueNameParamDto, + @Query() dto: QueueJobSearchDto, + ): Promise { + return this.service.searchJobs(auth, name, dto); + } + + @Delete(':name/jobs') + @Authenticated({ permission: Permission.QueueJobDelete, admin: true }) + @HttpCode(HttpStatus.NO_CONTENT) + @Endpoint({ + summary: 'Empty a queue', + description: 'Removes all jobs from the specified queue.', + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), + }) + emptyQueue(@Auth() auth: AuthDto, @Param() { name }: QueueNameParamDto, @Body() dto: QueueDeleteDto): Promise { + return this.service.emptyQueue(auth, name, dto); + } +} diff --git a/server/src/dtos/queue-legacy.dto.ts b/server/src/dtos/queue-legacy.dto.ts new file mode 100644 index 0000000000..79155e3f74 --- /dev/null +++ b/server/src/dtos/queue-legacy.dto.ts @@ -0,0 +1,89 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { QueueResponseDto, QueueStatisticsDto } from 'src/dtos/queue.dto'; +import { QueueName } from 'src/enum'; + +export class QueueStatusLegacyDto { + isActive!: boolean; + isPaused!: boolean; +} + +export class QueueResponseLegacyDto { + @ApiProperty({ type: QueueStatusLegacyDto }) + queueStatus!: QueueStatusLegacyDto; + + @ApiProperty({ type: QueueStatisticsDto }) + jobCounts!: QueueStatisticsDto; +} + +export class QueuesResponseLegacyDto implements Record { + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.ThumbnailGeneration]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.MetadataExtraction]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.VideoConversion]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.SmartSearch]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.StorageTemplateMigration]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.Migration]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.BackgroundTask]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.Search]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.DuplicateDetection]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.FaceDetection]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.FacialRecognition]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.Sidecar]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.Library]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.Notification]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.BackupDatabase]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.Ocr]!: QueueResponseLegacyDto; + + @ApiProperty({ type: QueueResponseLegacyDto }) + [QueueName.Workflow]!: QueueResponseLegacyDto; +} + +export const mapQueueLegacy = (response: QueueResponseDto): QueueResponseLegacyDto => { + return { + queueStatus: { + isPaused: response.isPaused, + isActive: response.statistics.active > 0, + }, + jobCounts: response.statistics, + }; +}; + +export const mapQueuesLegacy = (responses: QueueResponseDto[]): QueuesResponseLegacyDto => { + const legacy = new QueuesResponseLegacyDto(); + + for (const response of responses) { + legacy[response.name] = mapQueueLegacy(response); + } + + return legacy; +}; diff --git a/server/src/dtos/queue.dto.ts b/server/src/dtos/queue.dto.ts index df00c5cfc2..38a4a4ac6b 100644 --- a/server/src/dtos/queue.dto.ts +++ b/server/src/dtos/queue.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { QueueCommand, QueueName } from 'src/enum'; +import { HistoryBuilder, Property } from 'src/decorators'; +import { JobName, QueueCommand, QueueJobStatus, QueueName } from 'src/enum'; import { ValidateBoolean, ValidateEnum } from 'src/validation'; export class QueueNameParamDto { @@ -15,6 +16,46 @@ export class QueueCommandDto { force?: boolean; // TODO: this uses undefined as a third state, which should be refactored to be more explicit } +export class QueueUpdateDto { + @ValidateBoolean({ optional: true }) + isPaused?: boolean; +} + +export class QueueDeleteDto { + @ValidateBoolean({ optional: true }) + @Property({ + description: 'If true, will also remove failed jobs from the queue.', + history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'), + }) + failed?: boolean; +} + +export class QueueJobSearchDto { + @ValidateEnum({ enum: QueueJobStatus, name: 'QueueJobStatus', optional: true, each: true }) + status?: QueueJobStatus[]; +} +export class QueueJobResponseDto { + id?: string; + + @ValidateEnum({ enum: JobName, name: 'JobName' }) + name!: JobName; + + data!: object; + + @ApiProperty({ type: 'integer' }) + timestamp!: number; +} + +export class QueueResponseDto { + @ValidateEnum({ enum: QueueName, name: 'QueueName' }) + name!: QueueName; + + @ValidateBoolean() + isPaused!: boolean; + + statistics!: QueueStatisticsDto; +} + export class QueueStatisticsDto { @ApiProperty({ type: 'integer' }) active!: number; @@ -29,69 +70,3 @@ export class QueueStatisticsDto { @ApiProperty({ type: 'integer' }) paused!: number; } - -export class QueueStatusDto { - isActive!: boolean; - isPaused!: boolean; -} - -export class QueueResponseDto { - @ApiProperty({ type: QueueStatisticsDto }) - jobCounts!: QueueStatisticsDto; - - @ApiProperty({ type: QueueStatusDto }) - queueStatus!: QueueStatusDto; -} - -export class QueuesResponseDto implements Record { - @ApiProperty({ type: QueueResponseDto }) - [QueueName.ThumbnailGeneration]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.MetadataExtraction]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.VideoConversion]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.SmartSearch]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.StorageTemplateMigration]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.Migration]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.BackgroundTask]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.Search]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.DuplicateDetection]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.FaceDetection]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.FacialRecognition]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.Sidecar]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.Library]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.Notification]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.BackupDatabase]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.Ocr]!: QueueResponseDto; - - @ApiProperty({ type: QueueResponseDto }) - [QueueName.Workflow]!: QueueResponseDto; -} diff --git a/server/src/enum.ts b/server/src/enum.ts index d397f9d2ae..87ff282f31 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -248,6 +248,14 @@ export enum Permission { UserProfileImageUpdate = 'userProfileImage.update', UserProfileImageDelete = 'userProfileImage.delete', + QueueRead = 'queue.read', + QueueUpdate = 'queue.update', + + QueueJobCreate = 'queueJob.create', + QueueJobRead = 'queueJob.read', + QueueJobUpdate = 'queueJob.update', + QueueJobDelete = 'queueJob.delete', + WorkflowCreate = 'workflow.create', WorkflowRead = 'workflow.read', WorkflowUpdate = 'workflow.update', @@ -543,6 +551,15 @@ export enum QueueName { Workflow = 'workflow', } +export enum QueueJobStatus { + Active = 'active', + Failed = 'failed', + Complete = 'completed', + Delayed = 'delayed', + Waiting = 'waiting', + Paused = 'paused', +} + export enum JobName { AssetDelete = 'AssetDelete', AssetDeleteCheck = 'AssetDeleteCheck', @@ -624,9 +641,13 @@ export enum JobName { export enum QueueCommand { Start = 'start', + /** @deprecated Use `updateQueue` instead */ Pause = 'pause', + /** @deprecated Use `updateQueue` instead */ Resume = 'resume', + /** @deprecated Use `emptyQueue` instead */ Empty = 'empty', + /** @deprecated Use `emptyQueue` instead */ ClearFailed = 'clear-failed', } @@ -823,6 +844,7 @@ export enum ApiTag { Partners = 'Partners', People = 'People', Plugins = 'Plugins', + Queues = 'Queues', Search = 'Search', Server = 'Server', Sessions = 'Sessions', diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index 05d4bd2ac3..60ec021b3b 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -249,7 +249,7 @@ const getEnv = (): EnvData => { prefix: 'immich_bull', connection: { ...redisConfig }, defaultJobOptions: { - attempts: 3, + attempts: 1, removeOnComplete: true, removeOnFail: false, }, diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts index cf2799a4cf..b12accb68e 100644 --- a/server/src/repositories/job.repository.ts +++ b/server/src/repositories/job.repository.ts @@ -5,11 +5,12 @@ import { JobsOptions, Queue, Worker } from 'bullmq'; import { ClassConstructor } from 'class-transformer'; import { setTimeout } from 'node:timers/promises'; import { JobConfig } from 'src/decorators'; -import { JobName, JobStatus, MetadataKey, QueueCleanType, QueueName } from 'src/enum'; +import { QueueJobResponseDto, QueueJobSearchDto } from 'src/dtos/queue.dto'; +import { JobName, JobStatus, MetadataKey, QueueCleanType, QueueJobStatus, QueueName } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { EventRepository } from 'src/repositories/event.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; -import { JobCounts, JobItem, JobOf, QueueStatus } from 'src/types'; +import { JobCounts, JobItem, JobOf } from 'src/types'; import { getKeyByValue, getMethodNames, ImmichStartupError } from 'src/utils/misc'; type JobMapItem = { @@ -115,13 +116,14 @@ export class JobRepository { worker.concurrency = concurrency; } - async getQueueStatus(name: QueueName): Promise { + async isActive(name: QueueName): Promise { const queue = this.getQueue(name); + const count = await queue.getActiveCount(); + return count > 0; + } - return { - isActive: !!(await queue.getActiveCount()), - isPaused: await queue.isPaused(), - }; + async isPaused(name: QueueName): Promise { + return this.getQueue(name).isPaused(); } pause(name: QueueName) { @@ -192,17 +194,28 @@ export class JobRepository { } async waitForQueueCompletion(...queues: QueueName[]): Promise { - let activeQueue: QueueStatus | undefined; - do { - const statuses = await Promise.all(queues.map((name) => this.getQueueStatus(name))); - activeQueue = statuses.find((status) => status.isActive); - } while (activeQueue); - { - this.logger.verbose(`Waiting for ${activeQueue} queue to stop...`); + const getPending = async () => { + const results = await Promise.all(queues.map(async (name) => ({ pending: await this.isActive(name), name }))); + return results.filter(({ pending }) => pending).map(({ name }) => name); + }; + + let pending = await getPending(); + + while (pending.length > 0) { + this.logger.verbose(`Waiting for ${pending[0]} queue to stop...`); await setTimeout(1000); + pending = await getPending(); } } + async searchJobs(name: QueueName, dto: QueueJobSearchDto): Promise { + const jobs = await this.getQueue(name).getJobs(dto.status ?? Object.values(QueueJobStatus), 0, 1000); + return jobs.map((job) => { + const { id, name, timestamp, data } = job; + return { id, name: name as JobName, timestamp, data }; + }); + } + private getJobOptions(item: JobItem): JobsOptions | null { switch (item.name) { case JobName.NotifyAlbumUpdate: { diff --git a/server/src/services/queue.service.spec.ts b/server/src/services/queue.service.spec.ts index 5dce9476e2..f5cf20413e 100644 --- a/server/src/services/queue.service.spec.ts +++ b/server/src/services/queue.service.spec.ts @@ -2,6 +2,7 @@ import { BadRequestException } from '@nestjs/common'; import { defaults, SystemConfig } from 'src/config'; import { ImmichWorker, JobName, QueueCommand, QueueName } from 'src/enum'; import { QueueService } from 'src/services/queue.service'; +import { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; describe(QueueService.name, () => { @@ -52,80 +53,64 @@ describe(QueueService.name, () => { describe('getAllJobStatus', () => { it('should get all job statuses', async () => { - mocks.job.getJobCounts.mockResolvedValue({ - active: 1, - completed: 1, - failed: 1, - delayed: 1, - waiting: 1, - paused: 1, - }); - mocks.job.getQueueStatus.mockResolvedValue({ - isActive: true, - isPaused: true, - }); + const stats = factory.queueStatistics({ active: 1 }); + const expected = { jobCounts: stats, queueStatus: { isActive: true, isPaused: true } }; - const expectedJobStatus = { - jobCounts: { - active: 1, - completed: 1, - delayed: 1, - failed: 1, - waiting: 1, - paused: 1, - }, - queueStatus: { - isActive: true, - isPaused: true, - }, - }; + mocks.job.getJobCounts.mockResolvedValue(stats); + mocks.job.isPaused.mockResolvedValue(true); - await expect(sut.getAll()).resolves.toEqual({ - [QueueName.BackgroundTask]: expectedJobStatus, - [QueueName.DuplicateDetection]: expectedJobStatus, - [QueueName.SmartSearch]: expectedJobStatus, - [QueueName.MetadataExtraction]: expectedJobStatus, - [QueueName.Search]: expectedJobStatus, - [QueueName.StorageTemplateMigration]: expectedJobStatus, - [QueueName.Migration]: expectedJobStatus, - [QueueName.ThumbnailGeneration]: expectedJobStatus, - [QueueName.VideoConversion]: expectedJobStatus, - [QueueName.FaceDetection]: expectedJobStatus, - [QueueName.FacialRecognition]: expectedJobStatus, - [QueueName.Sidecar]: expectedJobStatus, - [QueueName.Library]: expectedJobStatus, - [QueueName.Notification]: expectedJobStatus, - [QueueName.BackupDatabase]: expectedJobStatus, - [QueueName.Ocr]: expectedJobStatus, - [QueueName.Workflow]: expectedJobStatus, + await expect(sut.getAllLegacy(factory.auth())).resolves.toEqual({ + [QueueName.BackgroundTask]: expected, + [QueueName.DuplicateDetection]: expected, + [QueueName.SmartSearch]: expected, + [QueueName.MetadataExtraction]: expected, + [QueueName.Search]: expected, + [QueueName.StorageTemplateMigration]: expected, + [QueueName.Migration]: expected, + [QueueName.ThumbnailGeneration]: expected, + [QueueName.VideoConversion]: expected, + [QueueName.FaceDetection]: expected, + [QueueName.FacialRecognition]: expected, + [QueueName.Sidecar]: expected, + [QueueName.Library]: expected, + [QueueName.Notification]: expected, + [QueueName.BackupDatabase]: expected, + [QueueName.Ocr]: expected, + [QueueName.Workflow]: expected, }); }); }); describe('handleCommand', () => { it('should handle a pause command', async () => { - await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Pause, force: false }); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); + + await sut.runCommandLegacy(QueueName.MetadataExtraction, { command: QueueCommand.Pause, force: false }); expect(mocks.job.pause).toHaveBeenCalledWith(QueueName.MetadataExtraction); }); it('should handle a resume command', async () => { - await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Resume, force: false }); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); + + await sut.runCommandLegacy(QueueName.MetadataExtraction, { command: QueueCommand.Resume, force: false }); expect(mocks.job.resume).toHaveBeenCalledWith(QueueName.MetadataExtraction); }); it('should handle an empty command', async () => { - await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Empty, force: false }); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); + + await sut.runCommandLegacy(QueueName.MetadataExtraction, { command: QueueCommand.Empty, force: false }); expect(mocks.job.empty).toHaveBeenCalledWith(QueueName.MetadataExtraction); }); it('should not start a job that is already running', async () => { - mocks.job.getQueueStatus.mockResolvedValue({ isActive: true, isPaused: false }); + mocks.job.isActive.mockResolvedValue(true); await expect( - sut.runCommand(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }), + sut.runCommandLegacy(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.job.queue).not.toHaveBeenCalled(); @@ -133,33 +118,37 @@ describe(QueueService.name, () => { }); it('should handle a start video conversion command', async () => { - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(QueueName.VideoConversion, { command: QueueCommand.Start, force: false }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetEncodeVideoQueueAll, data: { force: false } }); }); it('should handle a start storage template migration command', async () => { - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.StorageTemplateMigration, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(QueueName.StorageTemplateMigration, { command: QueueCommand.Start, force: false }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.StorageTemplateMigration }); }); it('should handle a start smart search command', async () => { - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.SmartSearch, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(QueueName.SmartSearch, { command: QueueCommand.Start, force: false }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SmartSearchQueueAll, data: { force: false } }); }); it('should handle a start metadata extraction command', async () => { - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.MetadataExtraction, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(QueueName.MetadataExtraction, { command: QueueCommand.Start, force: false }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetExtractMetadataQueueAll, @@ -168,17 +157,19 @@ describe(QueueService.name, () => { }); it('should handle a start sidecar command', async () => { - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.Sidecar, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(QueueName.Sidecar, { command: QueueCommand.Start, force: false }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.SidecarQueueAll, data: { force: false } }); }); it('should handle a start thumbnail generation command', async () => { - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.ThumbnailGeneration, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(QueueName.ThumbnailGeneration, { command: QueueCommand.Start, force: false }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetGenerateThumbnailsQueueAll, @@ -187,34 +178,37 @@ describe(QueueService.name, () => { }); it('should handle a start face detection command', async () => { - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.FaceDetection, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(QueueName.FaceDetection, { command: QueueCommand.Start, force: false }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.AssetDetectFacesQueueAll, data: { force: false } }); }); it('should handle a start facial recognition command', async () => { - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.FacialRecognition, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(QueueName.FacialRecognition, { command: QueueCommand.Start, force: false }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FacialRecognitionQueueAll, data: { force: false } }); }); it('should handle a start backup database command', async () => { - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + mocks.job.isActive.mockResolvedValue(false); + mocks.job.getJobCounts.mockResolvedValue(factory.queueStatistics()); - await sut.runCommand(QueueName.BackupDatabase, { command: QueueCommand.Start, force: false }); + await sut.runCommandLegacy(QueueName.BackupDatabase, { command: QueueCommand.Start, force: false }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.DatabaseBackup, data: { force: false } }); }); it('should throw a bad request when an invalid queue is used', async () => { - mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); + mocks.job.isActive.mockResolvedValue(false); await expect( - sut.runCommand(QueueName.BackgroundTask, { command: QueueCommand.Start, force: false }), + sut.runCommandLegacy(QueueName.BackgroundTask, { command: QueueCommand.Start, force: false }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.job.queue).not.toHaveBeenCalled(); diff --git a/server/src/services/queue.service.ts b/server/src/services/queue.service.ts index bea665e8fd..cdfa2ad2ed 100644 --- a/server/src/services/queue.service.ts +++ b/server/src/services/queue.service.ts @@ -2,7 +2,21 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { ClassConstructor } from 'class-transformer'; import { SystemConfig } from 'src/config'; import { OnEvent } from 'src/decorators'; -import { QueueCommandDto, QueueResponseDto, QueuesResponseDto } from 'src/dtos/queue.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { + mapQueueLegacy, + mapQueuesLegacy, + QueueResponseLegacyDto, + QueuesResponseLegacyDto, +} from 'src/dtos/queue-legacy.dto'; +import { + QueueCommandDto, + QueueDeleteDto, + QueueJobResponseDto, + QueueJobSearchDto, + QueueResponseDto, + QueueUpdateDto, +} from 'src/dtos/queue.dto'; import { BootstrapEventPriority, CronJob, @@ -86,7 +100,7 @@ export class QueueService extends BaseService { this.services = services; } - async runCommand(name: QueueName, dto: QueueCommandDto): Promise { + async runCommandLegacy(name: QueueName, dto: QueueCommandDto): Promise { this.logger.debug(`Handling command: queue=${name},command=${dto.command},force=${dto.force}`); switch (dto.command) { @@ -117,28 +131,60 @@ export class QueueService extends BaseService { } } + const response = await this.getByName(name); + + return mapQueueLegacy(response); + } + + async getAll(_auth: AuthDto): Promise { + return Promise.all(Object.values(QueueName).map((name) => this.getByName(name))); + } + + async getAllLegacy(auth: AuthDto): Promise { + const responses = await this.getAll(auth); + return mapQueuesLegacy(responses); + } + + get(auth: AuthDto, name: QueueName): Promise { return this.getByName(name); } - async getAll(): Promise { - const response = new QueuesResponseDto(); - for (const name of Object.values(QueueName)) { - response[name] = await this.getByName(name); + async update(auth: AuthDto, name: QueueName, dto: QueueUpdateDto): Promise { + if (dto.isPaused === true) { + if (name === QueueName.BackgroundTask) { + throw new BadRequestException(`The BackgroundTask queue cannot be paused`); + } + await this.jobRepository.pause(name); } - return response; + + if (dto.isPaused === false) { + await this.jobRepository.resume(name); + } + + return this.getByName(name); } - async getByName(name: QueueName): Promise { - const [jobCounts, queueStatus] = await Promise.all([ - this.jobRepository.getJobCounts(name), - this.jobRepository.getQueueStatus(name), - ]); + searchJobs(auth: AuthDto, name: QueueName, dto: QueueJobSearchDto): Promise { + return this.jobRepository.searchJobs(name, dto); + } - return { jobCounts, queueStatus }; + async emptyQueue(auth: AuthDto, name: QueueName, dto: QueueDeleteDto) { + await this.jobRepository.empty(name); + if (dto.failed) { + await this.jobRepository.clear(name, QueueCleanType.Failed); + } + } + + private async getByName(name: QueueName): Promise { + const [statistics, isPaused] = await Promise.all([ + this.jobRepository.getJobCounts(name), + this.jobRepository.isPaused(name), + ]); + return { name, isPaused, statistics }; } private async start(name: QueueName, { force }: QueueCommandDto): Promise { - const { isActive } = await this.jobRepository.getQueueStatus(name); + const isActive = await this.jobRepository.isActive(name); if (isActive) { throw new BadRequestException(`Job is already running`); } diff --git a/server/src/types.ts b/server/src/types.ts index dd3d25a7cb..848d19177d 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -291,11 +291,6 @@ export interface JobCounts { paused: number; } -export interface QueueStatus { - isActive: boolean; - isPaused: boolean; -} - export type JobItem = // Audit | { name: JobName.AuditTableCleanup; data?: IBaseJob } diff --git a/server/test/repositories/job.repository.mock.ts b/server/test/repositories/job.repository.mock.ts index f0f4fdda00..4fc5460c8a 100644 --- a/server/test/repositories/job.repository.mock.ts +++ b/server/test/repositories/job.repository.mock.ts @@ -11,9 +11,11 @@ export const newJobRepositoryMock = (): Mocked Promise.resolve()), queueAll: vitest.fn().mockImplementation(() => Promise.resolve()), - getQueueStatus: vitest.fn(), + isActive: vitest.fn(), + isPaused: vitest.fn(), getJobCounts: vitest.fn(), clear: vitest.fn(), waitForQueueCompletion: vitest.fn(), diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index ea0df585ea..a0de947b2b 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -14,6 +14,7 @@ import { } from 'src/database'; import { MapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { QueueStatisticsDto } from 'src/dtos/queue.dto'; import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserMetadataKey, UserStatus } from 'src/enum'; import { OnThisDayData, UserMetadataItem } from 'src/types'; import { v4, v7 } from 'uuid'; @@ -139,6 +140,16 @@ const sessionFactory = (session: Partial = {}) => ({ ...session, }); +const queueStatisticsFactory = (dto?: Partial) => ({ + active: 0, + completed: 0, + failed: 0, + delayed: 0, + waiting: 0, + paused: 0, + ...dto, +}); + const stackFactory = () => ({ id: newUuid(), ownerId: newUuid(), @@ -353,6 +364,7 @@ export const factory = { library: libraryFactory, memory: memoryFactory, partner: partnerFactory, + queueStatistics: queueStatisticsFactory, session: sessionFactory, stack: stackFactory, user: userFactory, diff --git a/web/.nvmrc b/web/.nvmrc index 0a492611a0..9e2934aa34 100644 --- a/web/.nvmrc +++ b/web/.nvmrc @@ -1 +1 @@ -24.11.0 +24.11.1 diff --git a/web/package.json b/web/package.json index 8d28eeb9c2..ded82ce1aa 100644 --- a/web/package.json +++ b/web/package.json @@ -28,7 +28,7 @@ "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/justified-layout-wasm": "^0.4.3", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.46.0", + "@immich/ui": "^0.47.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.14.0", @@ -108,6 +108,6 @@ "vitest": "^3.0.0" }, "volta": { - "node": "24.11.0" + "node": "24.11.1" } } diff --git a/web/src/app.css b/web/src/app.css index f66743f736..bf7601f63b 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -76,14 +76,6 @@ --immich-dark-gray: 33 33 33; } - *, - ::after, - ::before, - ::backdrop, - ::file-selector-button { - border-color: rgb(var(--immich-ui-default-border)); - } - button:not(:disabled), [role='button']:not(:disabled) { cursor: pointer; diff --git a/web/src/lib/components/asset-viewer/activity-viewer.svelte b/web/src/lib/components/asset-viewer/activity-viewer.svelte index 73b311769a..d688b2e9dd 100644 --- a/web/src/lib/components/asset-viewer/activity-viewer.svelte +++ b/web/src/lib/components/asset-viewer/activity-viewer.svelte @@ -52,7 +52,7 @@ let innerHeight: number = $state(0); let activityHeight: number = $state(0); let chatHeight: number = $state(0); - let divHeight: number = $state(0); + let divHeight = $derived(innerHeight - activityHeight); let previousAssetId: string | undefined = $state(assetId); let message = $state(''); let isSendingMessage = $state(false); @@ -96,11 +96,7 @@ } isSendingMessage = false; }; - $effect(() => { - if (innerHeight && activityHeight) { - divHeight = innerHeight - activityHeight; - } - }); + $effect(() => { if (assetId && previousAssetId != assetId) { previousAssetId = assetId; diff --git a/web/src/lib/components/asset-viewer/album-list-item.svelte b/web/src/lib/components/asset-viewer/album-list-item.svelte index 21b9b385d8..da0df21839 100644 --- a/web/src/lib/components/asset-viewer/album-list-item.svelte +++ b/web/src/lib/components/asset-viewer/album-list-item.svelte @@ -35,15 +35,13 @@ }); }; - let albumNameArray: string[] = $state(['', '', '']); - // This part of the code is responsible for splitting album name into 3 parts where part 2 is the search query // It is used to highlight the search query in the album name - $effect(() => { + const albumNameArray: string[] = $derived.by(() => { let { albumName } = album; let findIndex = normalizeSearchString(albumName).indexOf(normalizeSearchString(searchQuery)); let findLength = searchQuery.length; - albumNameArray = [ + return [ albumName.slice(0, findIndex), albumName.slice(findIndex, findIndex + findLength), albumName.slice(findIndex + findLength), diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 0af27e8373..7570278e51 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -395,12 +395,12 @@ } }); - let currentAssetId = $derived(asset.id); + // primarily, this is reactive on `asset` $effect(() => { - if (currentAssetId) { - untrack(() => handlePromiseError(handleGetAllAlbums())); - ocrManager.clear(); - handlePromiseError(ocrManager.getAssetOcr(currentAssetId)); + handlePromiseError(handleGetAllAlbums()); + ocrManager.clear(); + if (!sharedLink) { + handlePromiseError(ocrManager.getAssetOcr(asset.id)); } }); diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 2ee4496830..60913ff47b 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -23,6 +23,7 @@ import { Icon, IconButton, LoadingSpinner, modalManager } from '@immich/ui'; import { mdiCalendar, + mdiCamera, mdiCameraIris, mdiClose, mdiEye, @@ -372,9 +373,9 @@ - {#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.fNumber} + {#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.exposureTime || asset.exifInfo?.iso}
-
+
{#if asset.exifInfo?.make || asset.exifInfo?.model} @@ -395,20 +396,34 @@

{/if} +
+ {#if asset.exifInfo.exposureTime} +

{`${asset.exifInfo.exposureTime} s`}

+ {/if} + + {#if asset.exifInfo.iso} +

{`ISO ${asset.exifInfo.iso}`}

+ {/if} +
+
+
+ {/if} + + {#if asset.exifInfo?.lensModel || asset.exifInfo?.fNumber || asset.exifInfo?.focalLength} +
+
+ +
{#if asset.exifInfo?.lensModel} - +

+ + {asset.exifInfo.lensModel} + +

{/if}
@@ -416,19 +431,9 @@

ƒ/{asset.exifInfo.fNumber.toLocaleString($locale)}

{/if} - {#if asset.exifInfo.exposureTime} -

{`${asset.exifInfo.exposureTime} s`}

- {/if} - {#if asset.exifInfo.focalLength}

{`${asset.exifInfo.focalLength.toLocaleString($locale)} mm`}

{/if} - - {#if asset.exifInfo.iso} -

- {`ISO ${asset.exifInfo.iso}`} -

- {/if}
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 261f194d34..2607f6de79 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -171,7 +171,6 @@ $effect(() => { if (assetFileUrl) { - // this can't be in an async context with $effect void cast(assetFileUrl); } }); diff --git a/web/src/lib/components/asset-viewer/video-native-viewer.svelte b/web/src/lib/components/asset-viewer/video-native-viewer.svelte index 92c467bc1e..a25789a76c 100644 --- a/web/src/lib/components/asset-viewer/video-native-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-native-viewer.svelte @@ -43,7 +43,9 @@ let videoPlayer: HTMLVideoElement | undefined = $state(); let isLoading = $state(true); - let assetFileUrl = $state(''); + let assetFileUrl = $derived( + playOriginalVideo ? getAssetOriginalUrl({ id: assetId, cacheKey }) : getAssetPlaybackUrl({ id: assetId, cacheKey }), + ); let isScrubbing = $state(false); let showVideo = $state(false); @@ -53,11 +55,9 @@ }); $effect(() => { - assetFileUrl = playOriginalVideo - ? getAssetOriginalUrl({ id: assetId, cacheKey }) - : getAssetPlaybackUrl({ id: assetId, cacheKey }); - if (videoPlayer) { - videoPlayer.load(); + // reactive on `assetFileUrl` changes + if (assetFileUrl) { + videoPlayer?.load(); } }); diff --git a/web/src/lib/components/asset-viewer/video-remote-viewer.svelte b/web/src/lib/components/asset-viewer/video-remote-viewer.svelte index 392028c49f..94a7e748c5 100644 --- a/web/src/lib/components/asset-viewer/video-remote-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-remote-viewer.svelte @@ -35,7 +35,6 @@ $effect(() => { if (assetFileUrl) { - // this can't be in an async context with $effect void cast(assetFileUrl); } }); diff --git a/web/src/lib/components/jobs/JobTile.svelte b/web/src/lib/components/jobs/JobTile.svelte index 64a6db5b7f..8bdd7c169a 100644 --- a/web/src/lib/components/jobs/JobTile.svelte +++ b/web/src/lib/components/jobs/JobTile.svelte @@ -1,7 +1,7 @@ -{#if hasPlaces} +{#if places.length > 0} {#if placesGroupOption === PlacesGroupBy.None} diff --git a/web/src/lib/components/server-statistics/ServerStatisticsPanel.svelte b/web/src/lib/components/server-statistics/ServerStatisticsPanel.svelte index d8de773943..1fc01faf9c 100644 --- a/web/src/lib/components/server-statistics/ServerStatisticsPanel.svelte +++ b/web/src/lib/components/server-statistics/ServerStatisticsPanel.svelte @@ -7,20 +7,11 @@ import { mdiCameraIris, mdiChartPie, mdiPlayCircle } from '@mdi/js'; import { t } from 'svelte-i18n'; - interface Props { - stats?: ServerStatsResponseDto; - } + type Props = { + stats: ServerStatsResponseDto; + }; - let { - stats = { - photos: 0, - videos: 0, - usage: 0, - usagePhotos: 0, - usageVideos: 0, - usageByUser: [], - }, - }: Props = $props(); + const { stats }: Props = $props(); const zeros = (value: number) => { const maxLength = 13; diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte index 23fd00190e..0102e34977 100644 --- a/web/src/lib/components/shared-components/change-location.svelte +++ b/web/src/lib/components/shared-components/change-location.svelte @@ -27,7 +27,7 @@ let { asset = undefined, point: initialPoint, onClose }: Props = $props(); let places: PlacesResponseDto[] = $state([]); - let suggestedPlaces: PlacesResponseDto[] = $state([]); + let suggestedPlaces: PlacesResponseDto[] = $derived(places.slice(0, 5)); let searchWord: string = $state(''); let latestSearchTimeout: number; let showLoadingSpinner = $state(false); @@ -52,9 +52,6 @@ }); $effect(() => { - if (places) { - suggestedPlaces = places.slice(0, 5); - } if (searchWord === '') { suggestedPlaces = []; } diff --git a/web/src/lib/components/shared-components/context-menu/context-menu.svelte b/web/src/lib/components/shared-components/context-menu/context-menu.svelte index f63cbd0621..7bf9ba58b3 100644 --- a/web/src/lib/components/shared-components/context-menu/context-menu.svelte +++ b/web/src/lib/components/shared-components/context-menu/context-menu.svelte @@ -33,37 +33,36 @@ children, }: Props = $props(); - let left: number = $state(0); - let top: number = $state(0); + const swap = (direction: string) => (direction === 'left' ? 'right' : 'left'); + + const layoutDirection = $derived(languageManager.rtl ? swap(direction) : direction); + const position = $derived.by(() => { + if (!menuElement) { + return { left: 0, top: 0 }; + } + + const rect = menuElement.getBoundingClientRect(); + const directionWidth = layoutDirection === 'left' ? rect.width : 0; + const menuHeight = Math.min(menuElement.clientHeight, height) || 0; + + const left = Math.max(8, Math.min(window.innerWidth - rect.width, x - directionWidth)); + const top = Math.max(8, Math.min(window.innerHeight - menuHeight, y)); + + return { left, top }; + }); // We need to bind clientHeight since the bounding box may return a height // of zero when starting the 'slide' animation. let height: number = $state(0); let isTransitioned = $state(false); - - $effect(() => { - if (menuElement) { - let layoutDirection = direction; - if (languageManager.rtl) { - layoutDirection = direction === 'left' ? 'right' : 'left'; - } - - const rect = menuElement.getBoundingClientRect(); - const directionWidth = layoutDirection === 'left' ? rect.width : 0; - const menuHeight = Math.min(menuElement.clientHeight, height) || 0; - - left = Math.max(8, Math.min(window.innerWidth - rect.width, x - directionWidth)); - top = Math.max(8, Math.min(window.innerHeight - menuHeight, y)); - } - });
{ diff --git a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte index 1d451eacc4..ac158aa8a3 100644 --- a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte @@ -64,10 +64,11 @@ } } - let makeFilter = $derived(filters.make); - let modelFilter = $derived(filters.model); - let lensModelFilter = $derived(filters.lensModel); + const makeFilter = $derived(filters.make); + const modelFilter = $derived(filters.model); + const lensModelFilter = $derived(filters.lensModel); + // TODO replace by async $derived, at the latest when it's in stable https://svelte.dev/docs/svelte/await-expressions $effect(() => { handlePromiseError(updateMakes()); }); diff --git a/web/src/lib/components/shared-components/search-bar/search-location-section.svelte b/web/src/lib/components/shared-components/search-bar/search-location-section.svelte index cba5e105af..37a4a3ca9b 100644 --- a/web/src/lib/components/shared-components/search-bar/search-location-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-location-section.svelte @@ -7,11 +7,10 @@
diff --git a/web/src/lib/components/timeline/AssetLayout.svelte b/web/src/lib/components/timeline/AssetLayout.svelte new file mode 100644 index 0000000000..1d3300ca71 --- /dev/null +++ b/web/src/lib/components/timeline/AssetLayout.svelte @@ -0,0 +1,66 @@ + + + +
+ {#each filterIntersecting(viewerAssets) as viewerAsset (viewerAsset.id)} + {@const position = viewerAsset.position!} + {@const asset = viewerAsset.asset!} + + +
+ {@render thumbnail({ asset, position })} + {@render customThumbnailLayout?.(asset)} +
+ {/each} +
+ + diff --git a/web/src/lib/components/timeline/Month.svelte b/web/src/lib/components/timeline/Month.svelte new file mode 100644 index 0000000000..f7ffb58c43 --- /dev/null +++ b/web/src/lib/components/timeline/Month.svelte @@ -0,0 +1,115 @@ + + +{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)} + {@const absoluteWidth = dayGroup.left} + {@const isDayGroupSelected = assetInteraction.selectedGroup.has(dayGroup.groupTitle)} + +
(hoveredDayGroup = dayGroup.groupTitle)} + onmouseleave={() => (hoveredDayGroup = null)} + > + +
+ {#if !singleSelect} +
onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))} + onkeydown={() => onDayGroupSelect(dayGroup, assetsSnapshot(dayGroup.getAssets()))} + > + {#if isDayGroupSelected} + + {:else} + + {/if} +
+ {/if} + + + {dayGroup.groupTitle} + +
+ + + {#snippet thumbnail({ asset, position })} + {@render thumbnailWithGroup({ asset, position, dayGroup, groupIndex })} + {/snippet} + +
+{/each} + + diff --git a/web/src/lib/components/timeline/Timeline.svelte b/web/src/lib/components/timeline/Timeline.svelte index 0a209fcde3..d2873eca70 100644 --- a/web/src/lib/components/timeline/Timeline.svelte +++ b/web/src/lib/components/timeline/Timeline.svelte @@ -2,6 +2,8 @@ import { afterNavigate, beforeNavigate } from '$app/navigation'; import { page } from '$app/state'; import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer'; + import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; + import Month from '$lib/components/timeline/Month.svelte'; import Scrubber from '$lib/components/timeline/Scrubber.svelte'; import TimelineAssetViewer from '$lib/components/timeline/TimelineAssetViewer.svelte'; import TimelineKeyboardActions from '$lib/components/timeline/actions/TimelineKeyboardActions.svelte'; @@ -19,13 +21,12 @@ import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte'; import { mobileDevice } from '$lib/stores/mobile-device.svelte'; - import { isAssetViewerRoute } from '$lib/utils/navigation'; + import { isAssetViewerRoute, navigate } from '$lib/utils/navigation'; import { getTimes, type ScrubberListener } from '$lib/utils/timeline-util'; import { type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk'; import { DateTime } from 'luxon'; import { onDestroy, onMount, type Snippet } from 'svelte'; import type { UpdatePayload } from 'vite'; - import TimelineDateGroup from './TimelineDateGroup.svelte'; interface Props { isSelectionMode?: boolean; @@ -54,7 +55,7 @@ onEscape?: () => void; children?: Snippet; empty?: Snippet; - customLayout?: Snippet<[TimelineAsset]>; + customThumbnailLayout?: Snippet<[TimelineAsset]>; onThumbnailClick?: ( asset: TimelineAsset, timelineManager: TimelineManager, @@ -86,7 +87,7 @@ onEscape = () => {}, children, empty, - customLayout, + customThumbnailLayout, onThumbnailClick, }: Props = $props(); @@ -398,7 +399,8 @@ lastAssetMouseEvent = asset; }; - const handleGroupSelect = (timelineManager: TimelineManager, group: string, assets: TimelineAsset[]) => { + const handleGroupSelect = (dayGroup: DayGroup, assets: TimelineAsset[]) => { + const group = dayGroup.groupTitle; if (assetInteraction.selectedGroup.has(group)) { assetInteraction.removeGroupFromMultiselectGroup(group); for (const asset of assets) { @@ -418,7 +420,7 @@ } }; - const handleSelectAssets = async (asset: TimelineAsset) => { + const onSelectAssets = async (asset: TimelineAsset) => { if (!asset) { return; } @@ -540,6 +542,40 @@ void timelineManager.loadMonthGroup({ year: localDateTime.year, month: localDateTime.month }); } }); + + const assetSelectHandler = ( + timelineManager: TimelineManager, + asset: TimelineAsset, + assetsInDayGroup: TimelineAsset[], + groupTitle: string, + ) => { + void onSelectAssets(asset); + + // Check if all assets are selected in a group to toggle the group selection's icon + let selectedAssetsInGroupCount = assetsInDayGroup.filter(({ id }) => assetInteraction.hasSelectedAsset(id)).length; + + // if all assets are selected in a group, add the group to selected group + if (selectedAssetsInGroupCount === assetsInDayGroup.length) { + assetInteraction.addGroupToMultiselectGroup(groupTitle); + } else { + assetInteraction.removeGroupFromMultiselectGroup(groupTitle); + } + + isSelectingAllAssets.set(timelineManager.assetCount === assetInteraction.selectedAssets.length); + }; + + const _onClick = ( + timelineManager: TimelineManager, + assets: TimelineAsset[], + groupTitle: string, + asset: TimelineAsset, + ) => { + if (isSelectionMode || assetInteraction.selectionActive) { + assetSelectHandler(timelineManager, asset, assets, groupTitle); + return; + } + void navigate({ targetRoute: 'current', assetId: asset.id }); + }; @@ -649,20 +685,47 @@ style:transform={`translate3d(0,${absoluteHeight}px,0)`} style:width="100%" > - handleGroupSelect(timelineManager, title, assets)} - onSelectAssetCandidates={handleSelectAssetCandidates} - onSelectAssets={handleSelectAssets} - {customLayout} - {onThumbnailClick} - /> + manager={timelineManager} + onDayGroupSelect={handleGroupSelect} + > + {#snippet thumbnail({ asset, position, dayGroup, groupIndex })} + {@const isAssetSelectionCandidate = assetInteraction.hasSelectionCandidate(asset.id)} + {@const isAssetSelected = + assetInteraction.hasSelectedAsset(asset.id) || timelineManager.albumAssets.has(asset.id)} + {@const isAssetDisabled = timelineManager.albumAssets.has(asset.id)} + { + if (typeof onThumbnailClick === 'function') { + onThumbnailClick(asset, timelineManager, dayGroup, _onClick); + } else { + _onClick(timelineManager, dayGroup.getAssets(), dayGroup.groupTitle, asset); + } + }} + onSelect={() => { + if (isSelectionMode || assetInteraction.selectionActive) { + assetSelectHandler(timelineManager, asset, dayGroup.getAssets(), dayGroup.groupTitle); + return; + } + void onSelectAssets(asset); + }} + onMouseEvent={() => handleSelectAssetCandidates(asset)} + selected={isAssetSelected} + selectionCandidate={isAssetSelectionCandidate} + disabled={isAssetDisabled} + thumbnailWidth={position.width} + thumbnailHeight={position.height} + /> + {/snippet} +
{/if} {/each} diff --git a/web/src/lib/components/timeline/TimelineDateGroup.svelte b/web/src/lib/components/timeline/TimelineDateGroup.svelte deleted file mode 100644 index c662c16e72..0000000000 --- a/web/src/lib/components/timeline/TimelineDateGroup.svelte +++ /dev/null @@ -1,246 +0,0 @@ - - -{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)} - {@const absoluteWidth = dayGroup.left} - - -
{ - isMouseOverGroup = true; - assetMouseEventHandler(dayGroup.groupTitle, null); - }} - onmouseleave={() => { - isMouseOverGroup = false; - assetMouseEventHandler(dayGroup.groupTitle, null); - }} - > - -
- {#if !singleSelect} -
handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))} - onkeydown={() => handleSelectGroup(dayGroup.groupTitle, assetsSnapshot(dayGroup.getAssets()))} - > - {#if assetInteraction.selectedGroup.has(dayGroup.groupTitle)} - - {:else} - - {/if} -
- {/if} - - - {dayGroup.groupTitle} - -
- - -
- {#each filterIntersecting(dayGroup.viewerAssets) as viewerAsset (viewerAsset.id)} - {@const position = viewerAsset.position!} - {@const asset = viewerAsset.asset!} - - - -
- { - if (typeof onThumbnailClick === 'function') { - onThumbnailClick(asset, timelineManager, dayGroup, _onClick); - } else { - _onClick(timelineManager, dayGroup.getAssets(), dayGroup.groupTitle, asset); - } - }} - onSelect={(asset) => assetSelectHandler(timelineManager, asset, dayGroup.getAssets(), dayGroup.groupTitle)} - onMouseEvent={() => assetMouseEventHandler(dayGroup.groupTitle, assetSnapshot(asset))} - selected={assetInteraction.hasSelectedAsset(asset.id) || - dayGroup.monthGroup.timelineManager.albumAssets.has(asset.id)} - selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)} - disabled={dayGroup.monthGroup.timelineManager.albumAssets.has(asset.id)} - thumbnailWidth={position.width} - thumbnailHeight={position.height} - /> - {#if customLayout} - {@render customLayout(asset)} - {/if} -
- - {/each} -
-
-{/each} - - diff --git a/web/src/lib/components/workflow/ActionBuilder.svelte b/web/src/lib/components/workflow/ActionBuilder.svelte deleted file mode 100644 index 36e45f1bc1..0000000000 --- a/web/src/lib/components/workflow/ActionBuilder.svelte +++ /dev/null @@ -1,142 +0,0 @@ - - -{#if actions.length === 0} -
- {$t('no_actions_added')} -
-{:else} -
- {#each actions as action, index (index)} - {@const actionDef = getActionById(action.actionId)} -
-
-
- - - -
-
- moveUp(index)} - disabled={index === 0} - size="small" - /> - moveDown(index)} - disabled={index === actions.length - 1} - size="small" - /> - removeAction(index)} - size="small" - /> -
-
- - {#if actionDef} -
- {actionDef.description} -
- {#if actionDef.schema} - - {/if} - {/if} -
- {/each} -
-{/if} - - diff --git a/web/src/lib/components/workflow/FilterBuilder.svelte b/web/src/lib/components/workflow/FilterBuilder.svelte deleted file mode 100644 index 8748674557..0000000000 --- a/web/src/lib/components/workflow/FilterBuilder.svelte +++ /dev/null @@ -1,142 +0,0 @@ - - -{#if filters.length === 0} -
- {$t('no_filters_added')} -
-{:else} -
- {#each filters as filter, index (index)} - {@const filterDef = getFilterById(filter.filterId)} -
-
-
- - - -
-
- moveUp(index)} - disabled={index === 0} - size="small" - /> - moveDown(index)} - disabled={index === filters.length - 1} - size="small" - /> - removeFilter(index)} - size="small" - /> -
-
- - {#if filterDef} -
- {filterDef.description} -
- {#if filterDef.schema} - - {/if} - {/if} -
- {/each} -
-{/if} - - diff --git a/web/src/lib/components/workflow/schema-form/SchemaFormFields.svelte b/web/src/lib/components/workflows/SchemaFormFields.svelte similarity index 100% rename from web/src/lib/components/workflow/schema-form/SchemaFormFields.svelte rename to web/src/lib/components/workflows/SchemaFormFields.svelte diff --git a/web/src/lib/components/workflows/workflow-card-connector.svelte b/web/src/lib/components/workflows/WorkflowCardConnector.svelte similarity index 100% rename from web/src/lib/components/workflows/workflow-card-connector.svelte rename to web/src/lib/components/workflows/WorkflowCardConnector.svelte diff --git a/web/src/lib/components/workflows/workflow-json-editor.svelte b/web/src/lib/components/workflows/WorkflowJsonEditor.svelte similarity index 100% rename from web/src/lib/components/workflows/workflow-json-editor.svelte rename to web/src/lib/components/workflows/WorkflowJsonEditor.svelte diff --git a/web/src/lib/components/workflows/WorkflowSummary.svelte b/web/src/lib/components/workflows/WorkflowSummary.svelte new file mode 100644 index 0000000000..762bdf48a4 --- /dev/null +++ b/web/src/lib/components/workflows/WorkflowSummary.svelte @@ -0,0 +1,178 @@ + + +{#if isOpen} + + +{:else} + +{/if} diff --git a/web/src/lib/components/workflows/workflow-trigger-card.svelte b/web/src/lib/components/workflows/WorkflowTriggerCard.svelte similarity index 100% rename from web/src/lib/components/workflows/workflow-trigger-card.svelte rename to web/src/lib/components/workflows/WorkflowTriggerCard.svelte diff --git a/web/src/lib/components/workflows/workflow-summary-sidebar.svelte b/web/src/lib/components/workflows/workflow-summary-sidebar.svelte deleted file mode 100644 index b635b7c2c7..0000000000 --- a/web/src/lib/components/workflows/workflow-summary-sidebar.svelte +++ /dev/null @@ -1,83 +0,0 @@ - - -
-
-

Workflow Summary

- -
- -
-
-
- -
-
-
-

{trigger.name}

-
-
- - - {#if filters.length > 0} -
- -
- - -
-
-
- -
-
-
-
- {#each filters as filter, index (filter.id)} -

- {index + 1}. {filter.title} -

- {/each} -
-
-
- {/if} - - - {#if actions.length > 0} -
- -
- - -
-
-
- -
-
-
-
- {#each actions as action, index (action.id)} -

- {index + 1}. {action.title} -

- {/each} -
-
-
- {/if} -
-
-
diff --git a/web/src/lib/modals/WorkflowDeleteConfirmModal.svelte b/web/src/lib/modals/WorkflowDeleteConfirmModal.svelte new file mode 100644 index 0000000000..21c957cf2b --- /dev/null +++ b/web/src/lib/modals/WorkflowDeleteConfirmModal.svelte @@ -0,0 +1,16 @@ + + + (confirmed ? onClose(true) : onClose(false))} +/> diff --git a/web/src/lib/stores/ocr.svelte.ts b/web/src/lib/stores/ocr.svelte.ts index 4922f630ec..f9862b1edc 100644 --- a/web/src/lib/stores/ocr.svelte.ts +++ b/web/src/lib/stores/ocr.svelte.ts @@ -19,21 +19,23 @@ export type OcrBoundingBox = { class OcrManager { #data = $state([]); showOverlay = $state(false); - hasOcrData = $state(false); + #hasOcrData = $derived(this.#data.length > 0); get data() { return this.#data; } + get hasOcrData() { + return this.#hasOcrData; + } + async getAssetOcr(id: string) { this.#data = await getAssetOcr({ id }); - this.hasOcrData = this.#data.length > 0; } clear() { this.#data = []; this.showOverlay = false; - this.hasOcrData = false; } toggleOcrBoundingBox() { diff --git a/web/src/routes/(user)/+layout.svelte b/web/src/routes/(user)/+layout.svelte index ea10c45444..e6e349fe91 100644 --- a/web/src/routes/(user)/+layout.svelte +++ b/web/src/routes/(user)/+layout.svelte @@ -1,8 +1,6 @@ diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index 97964344ef..b58210187b 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -44,7 +44,7 @@ } from '@immich/sdk'; import { Icon, IconButton, LoadingSpinner } from '@immich/ui'; import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; - import { tick } from 'svelte'; + import { tick, untrack } from 'svelte'; import { t } from 'svelte-i18n'; let { isViewing: showAssetViewer } = assetViewingStore; @@ -71,11 +71,10 @@ let terms = $derived(searchQuery ? JSON.parse(searchQuery) : {}); $effect(() => { + // we want this to *only* be reactive on `terms` // eslint-disable-next-line @typescript-eslint/no-unused-expressions terms; - setTimeout(() => { - handlePromiseError(onSearchQueryUpdate()); - }); + untrack(() => handlePromiseError(onSearchQueryUpdate())); }); const onEscape = () => { diff --git a/web/src/routes/(user)/utilities/geolocation/+page.svelte b/web/src/routes/(user)/utilities/geolocation/+page.svelte index 4bd1a29fe5..89615062d4 100644 --- a/web/src/routes/(user)/utilities/geolocation/+page.svelte +++ b/web/src/routes/(user)/utilities/geolocation/+page.svelte @@ -196,7 +196,7 @@ withStacked onThumbnailClick={handleThumbnailClick} > - {#snippet customLayout(asset: TimelineAsset)} + {#snippet customThumbnailLayout(asset: TimelineAsset)} {#if hasGps(asset)}
{asset.city || $t('gps')} diff --git a/web/src/routes/(user)/utilities/workflows/+page.svelte b/web/src/routes/(user)/utilities/workflows/+page.svelte index 2e4c89373c..9ba285ec29 100644 --- a/web/src/routes/(user)/utilities/workflows/+page.svelte +++ b/web/src/routes/(user)/utilities/workflows/+page.svelte @@ -2,6 +2,7 @@ import { goto } from '$app/navigation'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import { AppRoute } from '$lib/constants'; + import WorkflowDeleteConfirmModal from '$lib/modals/WorkflowDeleteConfirmModal.svelte'; import type { WorkflowPayload } from '$lib/services/workflow.service'; import { handleError } from '$lib/utils/handle-error'; import { @@ -9,9 +10,7 @@ deleteWorkflow, PluginTriggerType, updateWorkflow, - type PluginActionResponseDto, type PluginFilterResponseDto, - type PluginResponseDto, type WorkflowResponseDto, } from '@immich/sdk'; import { @@ -27,10 +26,12 @@ IconButton, MenuItemType, menuManager, + modalManager, Text, toastManager, + VStack, } from '@immich/ui'; - import { mdiCodeJson, mdiDelete, mdiDotsVertical, mdiPause, mdiPencil, mdiPlay, mdiPlus } from '@mdi/js'; + import { mdiClose, mdiCodeJson, mdiDelete, mdiDotsVertical, mdiPause, mdiPencil, mdiPlay, mdiPlus } from '@mdi/js'; import { t } from 'svelte-i18n'; import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import type { PageData } from './$types'; @@ -44,20 +45,20 @@ let workflows = $state(data.workflows); const expandedWorkflows = new SvelteSet(); - const pluginFilterLookup = new SvelteMap(); - const pluginActionLookup = new SvelteMap(); + const pluginFilterLookup = new SvelteMap(); + const pluginActionLookup = new SvelteMap(); - for (const plugin of data.plugins as PluginResponseDto[]) { + for (const plugin of data.plugins) { for (const filter of plugin.filters ?? []) { - pluginFilterLookup.set(filter.id, { ...filter, pluginTitle: plugin.title }); + pluginFilterLookup.set(filter.id, { ...filter }); } for (const action of plugin.actions ?? []) { - pluginActionLookup.set(action.id, { ...action, pluginTitle: plugin.title }); + pluginActionLookup.set(action.id, { ...action }); } } - const toggleExpanded = (id: string) => { + const toggleShowingSchema = (id: string) => { if (expandedWorkflows.has(id)) { expandedWorkflows.delete(id); } else { @@ -65,7 +66,7 @@ } }; - const buildShareableWorkflow = (workflow: WorkflowResponseDto): WorkflowPayload => { + const constructPayload = (workflow: WorkflowResponseDto): WorkflowPayload => { const orderedFilters = [...(workflow.filters ?? [])].sort((a, b) => a.order - b.order); const orderedActions = [...(workflow.actions ?? [])].sort((a, b) => a.order - b.order); @@ -74,24 +75,24 @@ description: workflow.description ?? '', enabled: workflow.enabled, triggerType: workflow.triggerType, - filters: orderedFilters.map((wfFilter) => { - const meta = pluginFilterLookup.get(wfFilter.filterId); - const key = meta?.methodName ?? wfFilter.filterId; + filters: orderedFilters.map((filter) => { + const meta = pluginFilterLookup.get(filter.filterId); + const key = meta?.methodName ?? filter.filterId; return { - [key]: wfFilter.filterConfig ?? {}, + [key]: filter.filterConfig ?? {}, }; }), - actions: orderedActions.map((wfAction) => { - const meta = pluginActionLookup.get(wfAction.actionId); - const key = meta?.methodName ?? wfAction.actionId; + actions: orderedActions.map((action) => { + const meta = pluginActionLookup.get(action.actionId); + const key = meta?.methodName ?? action.actionId; return { - [key]: wfAction.actionConfig ?? {}, + [key]: action.actionConfig ?? {}, }; }), }; }; - const getWorkflowJson = (workflow: WorkflowResponseDto) => JSON.stringify(buildShareableWorkflow(workflow), null, 2); + const getJson = (workflow: WorkflowResponseDto) => JSON.stringify(constructPayload(workflow), null, 2); const handleToggleEnabled = async (workflow: WorkflowResponseDto) => { try { @@ -102,18 +103,22 @@ workflows = workflows.map((w) => (w.id === updated.id ? updated : w)); toastManager.success($t('workflow_updated')); } catch (error) { - // @ts-expect-error - translation type issue - handleError(error, $t('errors.unable_to_update') as string); + handleError(error, $t('errors.unable_to_update_workflow')); } }; const handleDeleteWorkflow = async (workflow: WorkflowResponseDto) => { try { + const confirmed = await modalManager.show(WorkflowDeleteConfirmModal); + if (!confirmed) { + return; + } + await deleteWorkflow({ id: workflow.id }); workflows = workflows.filter((w) => w.id !== workflow.id); toastManager.success($t('workflow_deleted')); } catch (error) { - handleError(error, $t('errors.unable_to_delete') as string); + handleError(error, $t('errors.unable_to_delete_workflow')); } }; @@ -135,36 +140,14 @@ await goto(`${AppRoute.WORKFLOWS_EDIT}/${workflow.id}?editMode=visual`); }; - type WorkflowChip = { - id: string; - title: string; - subtitle: string; + const getFilterLabel = (filterId: string) => { + const meta = pluginFilterLookup.get(filterId); + return meta?.title ?? $t('filter'); }; - const getFilterChips = (workflow: WorkflowResponseDto): WorkflowChip[] => { - return [...(workflow.filters ?? [])] - .sort((a, b) => a.order - b.order) - .map((filter) => { - const meta = pluginFilterLookup.get(filter.filterId); - return { - id: filter.id, - title: meta?.title ?? $t('filter'), - subtitle: meta?.pluginTitle ?? $t('workflow'), - }; - }); - }; - - const getActionChips = (workflow: WorkflowResponseDto): WorkflowChip[] => { - return [...(workflow.actions ?? [])] - .sort((a, b) => a.order - b.order) - .map((action) => { - const meta = pluginActionLookup.get(action.actionId); - return { - id: action.id, - title: meta?.title ?? $t('action'), - subtitle: meta?.pluginTitle ?? $t('workflow'), - }; - }); + const getActionLabel = (actionId: string) => { + const meta = pluginActionLookup.get(actionId); + return meta?.title ?? $t('action'); }; const getTriggerLabel = (triggerType: string) => { @@ -175,34 +158,19 @@ return labels[triggerType] || triggerType; }; - const dateFormatter = new Intl.DateTimeFormat(undefined, { - dateStyle: 'medium', - timeStyle: 'short', - }); - - const formatTimestamp = (iso?: string) => { - if (!iso) { - return '—'; - } - return dateFormatter.format(new Date(iso)); - }; - - type WorkflowWithMeta = { - workflow: WorkflowResponseDto; - filterChips: WorkflowChip[]; - actionChips: WorkflowChip[]; - workflowJson: string; - }; - - const getWorkflowsWithMeta = (): WorkflowWithMeta[] => - workflows.map((workflow) => ({ - workflow, - filterChips: getFilterChips(workflow), - actionChips: getActionChips(workflow), - workflowJson: getWorkflowJson(workflow), - })); + const formatTimestamp = (createdAt: string) => + new Intl.DateTimeFormat(undefined, { + dateStyle: 'medium', + timeStyle: 'short', + }).format(new Date(createdAt)); +{#snippet chipItem(title: string)} + + {title} + +{/snippet} + {#snippet buttons()} @@ -229,23 +197,20 @@
{:else}
- {#each getWorkflowsWithMeta() as { workflow, filterChips, actionChips, workflowJson } (workflow.id)} + {#each workflows as workflow (workflow.id)}
- - {#if workflow.enabled} - - {/if} - - + {workflow.name}
@@ -273,7 +238,7 @@ items: [ { title: workflow.enabled ? $t('disable') : $t('enable'), - color: workflow.enabled ? 'warning' : 'success', + color: workflow.enabled ? 'danger' : 'primary', icon: workflow.enabled ? mdiPause : mdiPlay, onSelect: () => void handleToggleEnabled(workflow), }, @@ -284,9 +249,9 @@ }, { - title: expandedWorkflows.has(workflow.id) ? $t('hide_json') : $t('show_json'), + title: expandedWorkflows.has(workflow.id) ? $t('hide_schema') : $t('show_schema'), icon: mdiCodeJson, - onSelect: () => toggleExpanded(workflow.id), + onSelect: () => toggleShowingSchema(workflow.id), }, MenuItemType.Divider, { @@ -305,87 +270,64 @@
-
-
- {$t('trigger')} -
- - {getTriggerLabel(workflow.triggerType)} - +
+
+ {$t('trigger')} +
+ {@render chipItem(getTriggerLabel(workflow.triggerType))}
-
-
-
- {$t('filter')} -
-
- {workflow.filters.length} -
+
+
+ {$t('filters')}
- {#if filterChips.length === 0} + {#if workflow.filters.length === 0} {$t('no_filters_added')} {:else} - {#each filterChips as chip (chip.id)} - - {chip.title} - + {#each workflow.filters as workflowFilter (workflowFilter.id)} + {@render chipItem(getFilterLabel(workflowFilter.filterId))} {/each} {/if}
-
-
-
- {$t('actions')} -
-
- {workflow.actions.length} -
+
+
+ {$t('actions')}
-
- {#if actionChips.length === 0} + +
+ {#if workflow.actions.length === 0} {$t('no_actions_added')} {:else} - {#each actionChips as chip (chip.id)} - - {chip.title} - - {/each} +
+ {#each workflow.actions as workflowAction (workflowAction.id)} + {@render chipItem(getActionLabel(workflowAction.actionId))} + {/each} +
{/if}
{#if expandedWorkflows.has(workflow.id)} -
-

Workflow JSON

- -
+ + + + {/if} diff --git a/web/src/routes/(user)/utilities/workflows/edit/[workflowId]/+page.svelte b/web/src/routes/(user)/utilities/workflows/edit/[workflowId]/+page.svelte index 7d623d6ec4..f0de1c7f66 100644 --- a/web/src/routes/(user)/utilities/workflows/edit/[workflowId]/+page.svelte +++ b/web/src/routes/(user)/utilities/workflows/edit/[workflowId]/+page.svelte @@ -1,11 +1,12 @@
- +
diff --git a/web/src/routes/admin/users/+page.svelte b/web/src/routes/admin/users/+page.svelte index 307fbe1ba4..85cff25e97 100644 --- a/web/src/routes/admin/users/+page.svelte +++ b/web/src/routes/admin/users/+page.svelte @@ -6,7 +6,7 @@ import { getUserAdminActions, getUserAdminsActions } from '$lib/services/user-admin.service'; import { locale } from '$lib/stores/preferences.store'; import { getByteUnitString } from '$lib/utils/byte-units'; - import { searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk'; + import { type UserAdminResponseDto } from '@immich/sdk'; import { HStack, Icon } from '@immich/ui'; import { mdiInfinity } from '@mdi/js'; import { t } from 'svelte-i18n'; @@ -18,24 +18,20 @@ let { data }: Props = $props(); - let allUsers: UserAdminResponseDto[] = $derived(data.allUsers); + let allUsers: UserAdminResponseDto[] = $state(data.allUsers); - const refresh = async () => { - allUsers = await searchUsersAdmin({ withDeleted: true }); - }; - - const onUserAdminDeleted = ({ id: userId }: { id: string }) => { - const user = allUsers.find(({ id }) => id === userId); - if (user) { - allUsers = allUsers.filter((user) => user.id !== userId); + const onUpdate = (user: UserAdminResponseDto) => { + const index = allUsers.findIndex(({ id }) => id === user.id); + if (index !== -1) { + allUsers[index] = user; } }; - const UserAdminsActions = $derived(getUserAdminsActions($t)); - - const onUpdate = async () => { - await refresh(); + const onUserAdminDeleted = ({ id: userId }: { id: string }) => { + allUsers = allUsers.filter(({ id }) => id !== userId); }; + + const { Create } = $derived(getUserAdminsActions($t)); {#snippet buttons()} - + {/snippet}
@@ -69,7 +65,7 @@ {#each allUsers as user (user.id)} - {@const UserAdminActions = getUserAdminActions($t, user)} + {@const { View, ContextMenu } = getUserAdminActions($t, user)} - - + + {/each} diff --git a/web/src/routes/auth/onboarding/+page.svelte b/web/src/routes/auth/onboarding/+page.svelte index 44cd97637a..ed4235104c 100644 --- a/web/src/routes/auth/onboarding/+page.svelte +++ b/web/src/routes/auth/onboarding/+page.svelte @@ -1,6 +1,6 @@