diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index b9dbb40d41..ef7300f7d4 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -105,7 +105,7 @@ jobs: - name: Generate docker image tags id: metadata - uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: flavor: | latest=false diff --git a/.github/workflows/close-duplicates.yml b/.github/workflows/close-duplicates.yml index 83df461aa0..24630bbb87 100644 --- a/.github/workflows/close-duplicates.yml +++ b/.github/workflows/close-duplicates.yml @@ -35,7 +35,7 @@ jobs: needs: [get_body, should_run] if: ${{ needs.should_run.outputs.should_run == 'true' }} container: - image: ghcr.io/immich-app/mdq:main@sha256:73a05fc805dfd3bd29bebc08442aedfec5c419c5ad3421ec73edc5647233891a + image: ghcr.io/immich-app/mdq:main@sha256:237cdae7783609c96f18037a513d38088713cf4a2e493a3aa136d0c45490749a outputs: checked: ${{ steps.get_checkbox.outputs.checked }} steps: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 76c60c11ff..dafc6eee3b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -57,7 +57,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 + uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -70,7 +70,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 + uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -83,6 +83,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 + uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 75ff935ed3..f02114a39a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -572,7 +572,7 @@ jobs: token: ${{ steps.token.outputs.token }} - name: Install uv uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4 - - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 # TODO: add caching when supported (https://github.com/actions/setup-python/pull/818) # with: # python-version: 3.11 diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index e2fb8fbc30..6fa1c51bdd 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -135,7 +135,7 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:8@sha256:81db6d39e1bba3b3ff32bd3a1b19a6d69690f94a3954ec131277b9a26b95b3aa + image: docker.io/valkey/valkey:9@sha256:4503e204c900a00ad393bec83c8c7c4c76b0529cd629e23b34b52011aefd1d27 healthcheck: test: redis-cli ping || exit 1 diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 90dc00d942..1aae886bc6 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -56,7 +56,7 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:8@sha256:81db6d39e1bba3b3ff32bd3a1b19a6d69690f94a3954ec131277b9a26b95b3aa + image: docker.io/valkey/valkey:9@sha256:4503e204c900a00ad393bec83c8c7c4c76b0529cd629e23b34b52011aefd1d27 healthcheck: test: redis-cli ping || exit 1 restart: always diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e4e0f964d3..51b6d3e860 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -49,7 +49,7 @@ services: redis: container_name: immich_redis - image: docker.io/valkey/valkey:8@sha256:81db6d39e1bba3b3ff32bd3a1b19a6d69690f94a3954ec131277b9a26b95b3aa + image: docker.io/valkey/valkey:9@sha256:4503e204c900a00ad393bec83c8c7c4c76b0529cd629e23b34b52011aefd1d27 healthcheck: test: redis-cli ping || exit 1 restart: always diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index e3df672d35..9dcfcac48b 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -133,9 +133,9 @@ There are a few different scenarios that can lead to this situation. The solutio The job is only automatically run once per asset after upload. If metadata extraction originally failed, the jobs were cleared/canceled, etc., the job may not have run automatically the first time. -### How can I hide photos from the timeline? +### How can I hide a photo or video from the timeline? -You can _archive_ them. +You can _archive_ them. This will hide the asset from the main timeline and folder view, but it will still show up in searches. All archived assets can be found in the _Archive_ view ### How can I backup data from Immich? diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index c3e9b3acd3..76784b285a 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -62,10 +62,10 @@ Information on the current workers can be found [here](/administration/jobs-work ## Ports -| Variable | Description | Default | -| :------------ | :------------- | :----------------------------------------: | -| `IMMICH_HOST` | Listening host | `0.0.0.0` | -| `IMMICH_PORT` | Listening port | `2283` (server), `3003` (machine learning) | +| Variable | Description | Default | Containers | +| :------------ | :------------- | :----------------------------------------: | :----------------------- | +| `IMMICH_HOST` | Listening host | `0.0.0.0` | server, machine learning | +| `IMMICH_PORT` | Listening port | `2283` (server), `3003` (machine learning) | server, machine learning | ## Database diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index 59ff74cc43..4d67a84647 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -1006,7 +1006,7 @@ describe('/libraries', () => { rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true }); }); - it('should switch from using file metadata to file.xmp metadata when asset refreshes', async () => { + it('should switch from using file metadata to file.ext.xmp metadata when asset refreshes', async () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, importPaths: [`${testAssetDirInternal}/temp/xmp`], diff --git a/i18n/en.json b/i18n/en.json index 2e6cada158..2ea5896eab 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2223,6 +2223,7 @@ "view_album": "View Album", "view_all": "View All", "view_all_users": "View all users", + "view_asset_owners": "View asset owners", "view_details": "View Details", "view_in_timeline": "View in timeline", "view_link": "View link", diff --git a/machine-learning/immich_ml/models/ocr/detection.py b/machine-learning/immich_ml/models/ocr/detection.py index 4101a5c6f7..d34a51684e 100644 --- a/machine-learning/immich_ml/models/ocr/detection.py +++ b/machine-learning/immich_ml/models/ocr/detection.py @@ -82,6 +82,7 @@ class TextDetector(InferenceModel): ratio = float(self.max_resolution) / img.height else: ratio = float(self.max_resolution) / img.width + ratio = min(ratio, 1.0) resize_h = int(img.height * ratio) resize_w = int(img.width * ratio) diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index ca21ffe389..356e954ef4 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -2206,7 +2206,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.4" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2214,9 +2214,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] diff --git a/mise.toml b/mise.toml index 5f3aef61de..0b61f3c26a 100644 --- a/mise.toml +++ b/mise.toml @@ -3,7 +3,7 @@ experimental_monorepo_root = true [tools] node = "24.11.1" flutter = "3.35.7" -pnpm = "10.22.0" +pnpm = "10.24.0" terragrunt = "0.93.10" opentofu = "1.10.7" java = "25.0.1" diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index d8404db409..a18644cd2a 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -71,6 +71,7 @@ enum StoreKey { readonlyModeEnabled._(138), autoPlayVideo._(139), + albumGridView._(140), // Experimental stuff photoManagerCustomFilter._(1000), diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index 67e91188e2..68c72255b0 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; class RemoteAlbumService { final DriftRemoteAlbumRepository _repository; @@ -32,16 +33,16 @@ class RemoteAlbumService { Future> sortAlbums( List albums, - RemoteAlbumSortMode sortMode, { + AlbumSortMode sortMode, { bool isReverse = false, }) async { final List sorted = switch (sortMode) { - RemoteAlbumSortMode.created => albums.sortedBy((album) => album.createdAt), - RemoteAlbumSortMode.title => albums.sortedBy((album) => album.name), - RemoteAlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt), - RemoteAlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount), - RemoteAlbumSortMode.mostRecent => await _sortByNewestAsset(albums), - RemoteAlbumSortMode.mostOldest => await _sortByOldestAsset(albums), + AlbumSortMode.created => albums.sortedBy((album) => album.createdAt), + AlbumSortMode.title => albums.sortedBy((album) => album.name), + AlbumSortMode.lastModified => albums.sortedBy((album) => album.updatedAt), + AlbumSortMode.assetCount => albums.sortedBy((album) => album.assetCount), + AlbumSortMode.mostRecent => await _sortByNewestAsset(albums), + AlbumSortMode.mostOldest => await _sortByOldestAsset(albums), }; return (isReverse ? sorted.reversed : sorted).toList(); @@ -211,16 +212,3 @@ class RemoteAlbumService { return sorted.reversed.toList(); } } - -enum RemoteAlbumSortMode { - title("library_page_sort_title"), - assetCount("library_page_sort_asset_count"), - lastModified("library_page_sort_last_modified"), - created("library_page_sort_created"), - mostRecent("sort_newest"), - mostOldest("sort_oldest"); - - final String key; - - const RemoteAlbumSortMode(this.key); -} diff --git a/mobile/lib/extensions/asset_extensions.dart b/mobile/lib/extensions/asset_extensions.dart index 22d5d5030a..a8ca7ef2aa 100644 --- a/mobile/lib/extensions/asset_extensions.dart +++ b/mobile/lib/extensions/asset_extensions.dart @@ -1,5 +1,5 @@ import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:timezone/timezone.dart'; +import 'package:immich_mobile/utils/timezone.dart'; extension TZExtension on Asset { /// Returns the created time of the asset from the exif info (if available) or from @@ -7,24 +7,11 @@ extension TZExtension on Asset { /// the timezone offset in [Duration] (DateTime, Duration) getTZAdjustedTimeAndOffset() { DateTime dt = fileCreatedAt.toLocal(); + if (exifInfo?.dateTimeOriginal != null) { - dt = exifInfo!.dateTimeOriginal!; - if (exifInfo?.timeZone != null) { - dt = dt.toUtc(); - try { - final location = getLocation(exifInfo!.timeZone!); - dt = TZDateTime.from(dt, location); - } on LocationNotFoundException { - RegExp re = RegExp(r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$', caseSensitive: false); - final m = re.firstMatch(exifInfo!.timeZone!); - if (m != null) { - final duration = Duration(hours: int.parse(m.group(1) ?? '0'), minutes: int.parse(m.group(2) ?? '0')); - dt = dt.add(duration); - return (dt, duration); - } - } - } + return applyTimezoneOffset(dateTime: exifInfo!.dateTimeOriginal!, timeZone: exifInfo?.timeZone); } + return (dt, dt.timeZoneOffset); } } diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 0d5b9a7636..4110966e57 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -7,7 +7,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; @@ -17,6 +16,9 @@ import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart' import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/album_filter.utils.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; @@ -45,14 +47,28 @@ class _AlbumSelectorState extends ConsumerState { List shownAlbums = []; AlbumFilter filter = AlbumFilter(query: "", mode: QuickFilterMode.all); - AlbumSort sort = AlbumSort(mode: RemoteAlbumSortMode.lastModified, isReverse: true); + AlbumSort sort = AlbumSort(mode: AlbumSortMode.lastModified, isReverse: true); @override void initState() { super.initState(); - // Load albums when component mounts WidgetsBinding.instance.addPostFrameCallback((_) { + final appSettings = ref.read(appSettingsServiceProvider); + final savedSortMode = appSettings.getSetting(AppSettingsEnum.selectedAlbumSortOrder); + final savedIsReverse = appSettings.getSetting(AppSettingsEnum.selectedAlbumSortReverse); + final savedIsGrid = appSettings.getSetting(AppSettingsEnum.albumGridView); + + final albumSortMode = AlbumSortMode.values.firstWhere( + (e) => e.storeIndex == savedSortMode, + orElse: () => AlbumSortMode.lastModified, + ); + + setState(() { + sort = AlbumSort(mode: albumSortMode, isReverse: savedIsReverse); + isGrid = savedIsGrid; + }); + ref.read(remoteAlbumProvider.notifier).refresh(); }); @@ -82,6 +98,7 @@ class _AlbumSelectorState extends ConsumerState { setState(() { isGrid = !isGrid; }); + ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.albumGridView, isGrid); } void changeFilter(QuickFilterMode mode) { @@ -97,6 +114,10 @@ class _AlbumSelectorState extends ConsumerState { this.sort = sort; }); + final appSettings = ref.read(appSettingsServiceProvider); + await appSettings.setSetting(AppSettingsEnum.selectedAlbumSortOrder, sort.mode.storeIndex); + await appSettings.setSetting(AppSettingsEnum.selectedAlbumSortReverse, sort.isReverse); + await sortAlbums(); } @@ -181,6 +202,8 @@ class _AlbumSelectorState extends ConsumerState { onToggleViewMode: toggleViewMode, onSortChanged: changeSort, controller: menuController, + currentSortMode: sort.mode, + currentIsReverse: sort.isReverse, ), isGrid ? _AlbumGrid(albums: shownAlbums, userId: userId, onAlbumSelected: widget.onAlbumSelected) @@ -192,21 +215,46 @@ class _AlbumSelectorState extends ConsumerState { } class _SortButton extends ConsumerStatefulWidget { - const _SortButton(this.onSortChanged, {this.controller}); + const _SortButton( + this.onSortChanged, { + required this.initialSortMode, + required this.initialIsReverse, + this.controller, + }); final Future Function(AlbumSort) onSortChanged; final MenuController? controller; + final AlbumSortMode initialSortMode; + final bool initialIsReverse; @override ConsumerState<_SortButton> createState() => _SortButtonState(); } class _SortButtonState extends ConsumerState<_SortButton> { - RemoteAlbumSortMode albumSortOption = RemoteAlbumSortMode.lastModified; - bool albumSortIsReverse = true; + late AlbumSortMode albumSortOption; + late bool albumSortIsReverse; bool isSorting = false; - Future onMenuTapped(RemoteAlbumSortMode sortMode) async { + @override + void initState() { + super.initState(); + albumSortOption = widget.initialSortMode; + albumSortIsReverse = widget.initialIsReverse; + } + + @override + void didUpdateWidget(_SortButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.initialSortMode != widget.initialSortMode || oldWidget.initialIsReverse != widget.initialIsReverse) { + setState(() { + albumSortOption = widget.initialSortMode; + albumSortIsReverse = widget.initialIsReverse; + }); + } + } + + Future onMenuTapped(AlbumSortMode sortMode) async { final selected = albumSortOption == sortMode; // Switch direction if (selected) { @@ -240,7 +288,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { padding: const WidgetStatePropertyAll(EdgeInsets.all(4)), ), consumeOutsideTap: true, - menuChildren: RemoteAlbumSortMode.values + menuChildren: AlbumSortMode.values .map( (sortMode) => MenuItemButton( leadingIcon: albumSortOption == sortMode @@ -269,7 +317,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { ), ), child: Text( - sortMode.key.t(context: context), + sortMode.label.t(context: context), style: context.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, color: albumSortOption == sortMode @@ -298,7 +346,7 @@ class _SortButtonState extends ConsumerState<_SortButton> { : const Icon(Icons.keyboard_arrow_up_rounded), ), Text( - albumSortOption.key.t(context: context), + albumSortOption.label.t(context: context), style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, color: context.colorScheme.onSurface.withAlpha(225), @@ -465,6 +513,8 @@ class _QuickSortAndViewMode extends StatelessWidget { required this.isGrid, required this.onToggleViewMode, required this.onSortChanged, + required this.currentSortMode, + required this.currentIsReverse, this.controller, }); @@ -472,6 +522,8 @@ class _QuickSortAndViewMode extends StatelessWidget { final VoidCallback onToggleViewMode; final MenuController? controller; final Future Function(AlbumSort) onSortChanged; + final AlbumSortMode currentSortMode; + final bool currentIsReverse; @override Widget build(BuildContext context) { @@ -481,7 +533,12 @@ class _QuickSortAndViewMode extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _SortButton(onSortChanged, controller: controller), + _SortButton( + onSortChanged, + controller: controller, + initialSortMode: currentSortMode, + initialIsReverse: currentIsReverse, + ), IconButton( icon: Icon(isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined, size: 24), onPressed: onToggleViewMode, diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index 582a33136a..276034d3d6 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -10,6 +10,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/duration_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; @@ -29,6 +30,7 @@ import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/action_button.utils.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; +import 'package:immich_mobile/utils/timezone.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; const _kSeparator = ' • '; @@ -85,13 +87,21 @@ class AssetDetailBottomSheet extends ConsumerWidget { class _AssetDetailBottomSheet extends ConsumerWidget { const _AssetDetailBottomSheet(); - String _getDateTime(BuildContext ctx, BaseAsset asset) { - final dateTime = asset.createdAt.toLocal(); + String _getDateTime(BuildContext ctx, BaseAsset asset, ExifInfo? exifInfo) { + DateTime dateTime = asset.createdAt.toLocal(); + Duration timeZoneOffset = dateTime.timeZoneOffset; + + // Use EXIF timezone information if available (matching web app behavior) + if (exifInfo?.dateTimeOriginal != null) { + (dateTime, timeZoneOffset) = applyTimezoneOffset( + dateTime: exifInfo!.dateTimeOriginal!, + timeZone: exifInfo.timeZone, + ); + } + final date = DateFormat.yMMMEd(ctx.locale.toLanguageTag()).format(dateTime); final time = DateFormat.jm(ctx.locale.toLanguageTag()).format(dateTime); - final timezone = dateTime.timeZoneOffset.isNegative - ? 'UTC-${dateTime.timeZoneOffset.inHours.abs().toString().padLeft(2, '0')}:${(dateTime.timeZoneOffset.inMinutes.abs() % 60).toString().padLeft(2, '0')}' - : 'UTC+${dateTime.timeZoneOffset.inHours.toString().padLeft(2, '0')}:${(dateTime.timeZoneOffset.inMinutes.abs() % 60).toString().padLeft(2, '0')}'; + final timezone = 'GMT${timeZoneOffset.formatAsOffset()}'; return '$date$_kSeparator$time $timezone'; } @@ -269,7 +279,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { children: [ // Asset Date and Time SheetTile( - title: _getDateTime(context, asset), + title: _getDateTime(context, asset, exifInfo), titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null, onTap: asset.hasRemote && isOwner ? () async => await _editDateTime(context, ref) : null, diff --git a/mobile/lib/providers/infrastructure/remote_album.provider.dart b/mobile/lib/providers/infrastructure/remote_album.provider.dart index 38ba52dc56..e3cffeb093 100644 --- a/mobile/lib/providers/infrastructure/remote_album.provider.dart +++ b/mobile/lib/providers/infrastructure/remote_album.provider.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -70,7 +71,7 @@ class RemoteAlbumNotifier extends Notifier { Future> sortAlbums( List albums, - RemoteAlbumSortMode sortMode, { + AlbumSortMode sortMode, { bool isReverse = false, }) async { return await _remoteAlbumService.sortAlbums(albums, sortMode, isReverse: isReverse); diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 59b627ecc3..4261613a19 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -15,6 +15,7 @@ import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/download.repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/timezone.dart'; import 'package:immich_mobile/widgets/common/date_time_picker.dart'; import 'package:immich_mobile/widgets/common/location_picker.dart'; import 'package:maplibre_gl/maplibre_gl.dart' as maplibre; @@ -175,9 +176,17 @@ class ActionService { } final exifData = await _remoteAssetRepository.getExif(assetId); - initialDate = asset.createdAt.toLocal(); - offset = initialDate.timeZoneOffset; - timeZone = exifData?.timeZone; + + // Use EXIF timezone information if available (matching web app and display behavior) + DateTime dt = asset.createdAt.toLocal(); + offset = dt.timeZoneOffset; + + if (exifData?.dateTimeOriginal != null) { + timeZone = exifData!.timeZone; + (dt, offset) = applyTimezoneOffset(dateTime: exifData.dateTimeOriginal!, timeZone: exifData.timeZone); + } + + initialDate = dt; } final dateTime = await showDateTimePicker( diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 7149408e8a..fc08193d11 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -51,9 +51,10 @@ enum AppSettingsEnum { enableBackup(StoreKey.enableBackup, null, false), useCellularForUploadVideos(StoreKey.useWifiForUploadVideos, null, false), useCellularForUploadPhotos(StoreKey.useWifiForUploadPhotos, null, false), + readonlyModeEnabled(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false), + albumGridView(StoreKey.albumGridView, "albumGridView", false), backupRequireCharging(StoreKey.backupRequireCharging, null, false), - backupTriggerDelay(StoreKey.backupTriggerDelay, null, 30), - readonlyModeEnabled(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false); + backupTriggerDelay(StoreKey.backupTriggerDelay, null, 30); const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/utils/album_filter.utils.dart b/mobile/lib/utils/album_filter.utils.dart index 02142b1571..8f9363d4d9 100644 --- a/mobile/lib/utils/album_filter.utils.dart +++ b/mobile/lib/utils/album_filter.utils.dart @@ -1,5 +1,5 @@ -import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; class AlbumFilter { String? userId; @@ -14,12 +14,12 @@ class AlbumFilter { } class AlbumSort { - RemoteAlbumSortMode mode; + AlbumSortMode mode; bool isReverse; AlbumSort({required this.mode, this.isReverse = false}); - AlbumSort copyWith({RemoteAlbumSortMode? mode, bool? isReverse}) { + AlbumSort copyWith({AlbumSortMode? mode, bool? isReverse}) { return AlbumSort(mode: mode ?? this.mode, isReverse: isReverse ?? this.isReverse); } } diff --git a/mobile/lib/utils/timezone.dart b/mobile/lib/utils/timezone.dart new file mode 100644 index 0000000000..d75122062f --- /dev/null +++ b/mobile/lib/utils/timezone.dart @@ -0,0 +1,35 @@ +import 'package:timezone/timezone.dart'; + +/// Applies timezone conversion to a DateTime using EXIF timezone information. +/// +/// This function handles two timezone formats: +/// 1. Named timezone locations (e.g., "Asia/Hong_Kong") +/// 2. UTC offset format (e.g., "UTC+08:00", "UTC-05:00") +/// +/// Returns a tuple of (adjusted DateTime, timezone offset Duration) +(DateTime, Duration) applyTimezoneOffset({required DateTime dateTime, required String? timeZone}) { + DateTime dt = dateTime.toUtc(); + + if (timeZone == null) { + return (dt, dt.timeZoneOffset); + } + + try { + // Try to get timezone location from database + final location = getLocation(timeZone); + dt = TZDateTime.from(dt, location); + return (dt, dt.timeZoneOffset); + } on LocationNotFoundException { + // Handle UTC offset format (e.g., "UTC+08:00") + RegExp re = RegExp(r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$', caseSensitive: false); + final m = re.firstMatch(timeZone); + if (m != null) { + final duration = Duration(hours: int.parse(m.group(1) ?? '0'), minutes: int.parse(m.group(2) ?? '0')); + dt = dt.add(duration); + return (dt, duration); + } + } + + // If timezone is invalid, return UTC + return (dt, dt.timeZoneOffset); +} diff --git a/mobile/openapi/lib/model/workflow_action_item_dto.dart b/mobile/openapi/lib/model/workflow_action_item_dto.dart index ee0b30216d..cb0c39eae9 100644 --- a/mobile/openapi/lib/model/workflow_action_item_dto.dart +++ b/mobile/openapi/lib/model/workflow_action_item_dto.dart @@ -14,7 +14,7 @@ class WorkflowActionItemDto { /// Returns a new [WorkflowActionItemDto] instance. WorkflowActionItemDto({ this.actionConfig, - required this.actionId, + required this.pluginActionId, }); /// @@ -25,21 +25,21 @@ class WorkflowActionItemDto { /// Object? actionConfig; - String actionId; + String pluginActionId; @override bool operator ==(Object other) => identical(this, other) || other is WorkflowActionItemDto && other.actionConfig == actionConfig && - other.actionId == actionId; + other.pluginActionId == pluginActionId; @override int get hashCode => // ignore: unnecessary_parenthesis (actionConfig == null ? 0 : actionConfig!.hashCode) + - (actionId.hashCode); + (pluginActionId.hashCode); @override - String toString() => 'WorkflowActionItemDto[actionConfig=$actionConfig, actionId=$actionId]'; + String toString() => 'WorkflowActionItemDto[actionConfig=$actionConfig, pluginActionId=$pluginActionId]'; Map toJson() { final json = {}; @@ -48,7 +48,7 @@ class WorkflowActionItemDto { } else { // json[r'actionConfig'] = null; } - json[r'actionId'] = this.actionId; + json[r'pluginActionId'] = this.pluginActionId; return json; } @@ -62,7 +62,7 @@ class WorkflowActionItemDto { return WorkflowActionItemDto( actionConfig: mapValueOfType(json, r'actionConfig'), - actionId: mapValueOfType(json, r'actionId')!, + pluginActionId: mapValueOfType(json, r'pluginActionId')!, ); } return null; @@ -110,7 +110,7 @@ class WorkflowActionItemDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'actionId', + 'pluginActionId', }; } diff --git a/mobile/openapi/lib/model/workflow_action_response_dto.dart b/mobile/openapi/lib/model/workflow_action_response_dto.dart index 6528f018c9..5132623e89 100644 --- a/mobile/openapi/lib/model/workflow_action_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_action_response_dto.dart @@ -14,41 +14,41 @@ class WorkflowActionResponseDto { /// Returns a new [WorkflowActionResponseDto] instance. WorkflowActionResponseDto({ required this.actionConfig, - required this.actionId, required this.id, required this.order, + required this.pluginActionId, required this.workflowId, }); Object? actionConfig; - String actionId; - String id; num order; + String pluginActionId; + String workflowId; @override bool operator ==(Object other) => identical(this, other) || other is WorkflowActionResponseDto && other.actionConfig == actionConfig && - other.actionId == actionId && other.id == id && other.order == order && + other.pluginActionId == pluginActionId && other.workflowId == workflowId; @override int get hashCode => // ignore: unnecessary_parenthesis (actionConfig == null ? 0 : actionConfig!.hashCode) + - (actionId.hashCode) + (id.hashCode) + (order.hashCode) + + (pluginActionId.hashCode) + (workflowId.hashCode); @override - String toString() => 'WorkflowActionResponseDto[actionConfig=$actionConfig, actionId=$actionId, id=$id, order=$order, workflowId=$workflowId]'; + String toString() => 'WorkflowActionResponseDto[actionConfig=$actionConfig, id=$id, order=$order, pluginActionId=$pluginActionId, workflowId=$workflowId]'; Map toJson() { final json = {}; @@ -57,9 +57,9 @@ class WorkflowActionResponseDto { } else { // json[r'actionConfig'] = null; } - json[r'actionId'] = this.actionId; json[r'id'] = this.id; json[r'order'] = this.order; + json[r'pluginActionId'] = this.pluginActionId; json[r'workflowId'] = this.workflowId; return json; } @@ -74,9 +74,9 @@ class WorkflowActionResponseDto { return WorkflowActionResponseDto( actionConfig: mapValueOfType(json, r'actionConfig'), - actionId: mapValueOfType(json, r'actionId')!, id: mapValueOfType(json, r'id')!, order: num.parse('${json[r'order']}'), + pluginActionId: mapValueOfType(json, r'pluginActionId')!, workflowId: mapValueOfType(json, r'workflowId')!, ); } @@ -126,9 +126,9 @@ class WorkflowActionResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'actionConfig', - 'actionId', 'id', 'order', + 'pluginActionId', 'workflowId', }; } diff --git a/mobile/openapi/lib/model/workflow_filter_item_dto.dart b/mobile/openapi/lib/model/workflow_filter_item_dto.dart index 5b78585c3d..bd8090b05e 100644 --- a/mobile/openapi/lib/model/workflow_filter_item_dto.dart +++ b/mobile/openapi/lib/model/workflow_filter_item_dto.dart @@ -14,7 +14,7 @@ class WorkflowFilterItemDto { /// Returns a new [WorkflowFilterItemDto] instance. WorkflowFilterItemDto({ this.filterConfig, - required this.filterId, + required this.pluginFilterId, }); /// @@ -25,21 +25,21 @@ class WorkflowFilterItemDto { /// Object? filterConfig; - String filterId; + String pluginFilterId; @override bool operator ==(Object other) => identical(this, other) || other is WorkflowFilterItemDto && other.filterConfig == filterConfig && - other.filterId == filterId; + other.pluginFilterId == pluginFilterId; @override int get hashCode => // ignore: unnecessary_parenthesis (filterConfig == null ? 0 : filterConfig!.hashCode) + - (filterId.hashCode); + (pluginFilterId.hashCode); @override - String toString() => 'WorkflowFilterItemDto[filterConfig=$filterConfig, filterId=$filterId]'; + String toString() => 'WorkflowFilterItemDto[filterConfig=$filterConfig, pluginFilterId=$pluginFilterId]'; Map toJson() { final json = {}; @@ -48,7 +48,7 @@ class WorkflowFilterItemDto { } else { // json[r'filterConfig'] = null; } - json[r'filterId'] = this.filterId; + json[r'pluginFilterId'] = this.pluginFilterId; return json; } @@ -62,7 +62,7 @@ class WorkflowFilterItemDto { return WorkflowFilterItemDto( filterConfig: mapValueOfType(json, r'filterConfig'), - filterId: mapValueOfType(json, r'filterId')!, + pluginFilterId: mapValueOfType(json, r'pluginFilterId')!, ); } return null; @@ -110,7 +110,7 @@ class WorkflowFilterItemDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'filterId', + 'pluginFilterId', }; } diff --git a/mobile/openapi/lib/model/workflow_filter_response_dto.dart b/mobile/openapi/lib/model/workflow_filter_response_dto.dart index 5257c92b80..94dce27a3f 100644 --- a/mobile/openapi/lib/model/workflow_filter_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_filter_response_dto.dart @@ -14,41 +14,41 @@ class WorkflowFilterResponseDto { /// Returns a new [WorkflowFilterResponseDto] instance. WorkflowFilterResponseDto({ required this.filterConfig, - required this.filterId, required this.id, required this.order, + required this.pluginFilterId, required this.workflowId, }); Object? filterConfig; - String filterId; - String id; num order; + String pluginFilterId; + String workflowId; @override bool operator ==(Object other) => identical(this, other) || other is WorkflowFilterResponseDto && other.filterConfig == filterConfig && - other.filterId == filterId && other.id == id && other.order == order && + other.pluginFilterId == pluginFilterId && other.workflowId == workflowId; @override int get hashCode => // ignore: unnecessary_parenthesis (filterConfig == null ? 0 : filterConfig!.hashCode) + - (filterId.hashCode) + (id.hashCode) + (order.hashCode) + + (pluginFilterId.hashCode) + (workflowId.hashCode); @override - String toString() => 'WorkflowFilterResponseDto[filterConfig=$filterConfig, filterId=$filterId, id=$id, order=$order, workflowId=$workflowId]'; + String toString() => 'WorkflowFilterResponseDto[filterConfig=$filterConfig, id=$id, order=$order, pluginFilterId=$pluginFilterId, workflowId=$workflowId]'; Map toJson() { final json = {}; @@ -57,9 +57,9 @@ class WorkflowFilterResponseDto { } else { // json[r'filterConfig'] = null; } - json[r'filterId'] = this.filterId; json[r'id'] = this.id; json[r'order'] = this.order; + json[r'pluginFilterId'] = this.pluginFilterId; json[r'workflowId'] = this.workflowId; return json; } @@ -74,9 +74,9 @@ class WorkflowFilterResponseDto { return WorkflowFilterResponseDto( filterConfig: mapValueOfType(json, r'filterConfig'), - filterId: mapValueOfType(json, r'filterId')!, id: mapValueOfType(json, r'id')!, order: num.parse('${json[r'order']}'), + pluginFilterId: mapValueOfType(json, r'pluginFilterId')!, workflowId: mapValueOfType(json, r'workflowId')!, ); } @@ -126,9 +126,9 @@ class WorkflowFilterResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'filterConfig', - 'filterId', 'id', 'order', + 'pluginFilterId', 'workflowId', }; } diff --git a/mobile/test/domain/services/album.service_test.dart b/mobile/test/domain/services/album.service_test.dart index ebd94a9450..b86819536d 100644 --- a/mobile/test/domain/services/album.service_test.dart +++ b/mobile/test/domain/services/album.service_test.dart @@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -76,42 +77,42 @@ void main() { test('should sort correctly based on name', () async { final albums = [albumB, albumA]; - final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.title); + final result = await sut.sortAlbums(albums, AlbumSortMode.title); expect(result, [albumA, albumB]); }); test('should sort correctly based on createdAt', () async { final albums = [albumB, albumA]; - final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.created); + final result = await sut.sortAlbums(albums, AlbumSortMode.created); expect(result, [albumA, albumB]); }); test('should sort correctly based on updatedAt', () async { final albums = [albumB, albumA]; - final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.lastModified); + final result = await sut.sortAlbums(albums, AlbumSortMode.lastModified); expect(result, [albumA, albumB]); }); test('should sort correctly based on assetCount', () async { final albums = [albumB, albumA]; - final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.assetCount); + final result = await sut.sortAlbums(albums, AlbumSortMode.assetCount); expect(result, [albumA, albumB]); }); test('should sort correctly based on newestAssetTimestamp', () async { final albums = [albumB, albumA]; - final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.mostRecent); + final result = await sut.sortAlbums(albums, AlbumSortMode.mostRecent); expect(result, [albumA, albumB]); }); test('should sort correctly based on oldestAssetTimestamp', () async { final albums = [albumB, albumA]; - final result = await sut.sortAlbums(albums, RemoteAlbumSortMode.mostOldest); + final result = await sut.sortAlbums(albums, AlbumSortMode.mostOldest); expect(result, [albumB, albumA]); }); }); diff --git a/mobile/test/utils/timezone_test.dart b/mobile/test/utils/timezone_test.dart new file mode 100644 index 0000000000..d1e89dc473 --- /dev/null +++ b/mobile/test/utils/timezone_test.dart @@ -0,0 +1,278 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/utils/timezone.dart'; +import 'package:timezone/data/latest.dart' as tz; + +void main() { + setUpAll(() { + tz.initializeTimeZones(); + }); + + group('applyTimezoneOffset', () { + group('with named timezone locations', () { + test('should convert UTC to Asia/Hong_Kong (+08:00)', () { + final utcTime = DateTime.utc(2024, 6, 15, 12, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: utcTime, + timeZone: 'Asia/Hong_Kong', + ); + + expect(adjustedTime.hour, 20); // 12:00 UTC + 8 hours = 20:00 + expect(offset, const Duration(hours: 8)); + }); + + test('should convert UTC to America/New_York (handles DST)', () { + // Summer time (EDT = UTC-4) + final summerUtc = DateTime.utc(2024, 6, 15, 12, 0, 0); + final (summerTime, summerOffset) = applyTimezoneOffset( + dateTime: summerUtc, + timeZone: 'America/New_York', + ); + + expect(summerTime.hour, 8); // 12:00 UTC - 4 hours = 08:00 + expect(summerOffset, const Duration(hours: -4)); + + // Winter time (EST = UTC-5) + final winterUtc = DateTime.utc(2024, 1, 15, 12, 0, 0); + final (winterTime, winterOffset) = applyTimezoneOffset( + dateTime: winterUtc, + timeZone: 'America/New_York', + ); + + expect(winterTime.hour, 7); // 12:00 UTC - 5 hours = 07:00 + expect(winterOffset, const Duration(hours: -5)); + }); + + test('should convert UTC to Europe/London', () { + // Winter (GMT = UTC+0) + final winterUtc = DateTime.utc(2024, 1, 15, 12, 0, 0); + final (winterTime, winterOffset) = applyTimezoneOffset( + dateTime: winterUtc, + timeZone: 'Europe/London', + ); + + expect(winterTime.hour, 12); + expect(winterOffset, Duration.zero); + + // Summer (BST = UTC+1) + final summerUtc = DateTime.utc(2024, 6, 15, 12, 0, 0); + final (summerTime, summerOffset) = applyTimezoneOffset( + dateTime: summerUtc, + timeZone: 'Europe/London', + ); + + expect(summerTime.hour, 13); + expect(summerOffset, const Duration(hours: 1)); + }); + + test('should handle timezone with 30-minute offset (Asia/Kolkata)', () { + final utcTime = DateTime.utc(2024, 6, 15, 12, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: utcTime, + timeZone: 'Asia/Kolkata', + ); + + expect(adjustedTime.hour, 17); + expect(adjustedTime.minute, 30); // 12:00 UTC + 5:30 = 17:30 + expect(offset, const Duration(hours: 5, minutes: 30)); + }); + + test('should handle timezone with 45-minute offset (Asia/Kathmandu)', () { + final utcTime = DateTime.utc(2024, 6, 15, 12, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: utcTime, + timeZone: 'Asia/Kathmandu', + ); + + expect(adjustedTime.hour, 17); + expect(adjustedTime.minute, 45); // 12:00 UTC + 5:45 = 17:45 + expect(offset, const Duration(hours: 5, minutes: 45)); + }); + }); + + group('with UTC offset format', () { + test('should handle UTC+08:00 format', () { + final utcTime = DateTime.utc(2024, 6, 15, 12, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: utcTime, + timeZone: 'UTC+08:00', + ); + + expect(adjustedTime.hour, 20); + expect(offset, const Duration(hours: 8)); + }); + + test('should handle UTC-05:00 format', () { + final utcTime = DateTime.utc(2024, 6, 15, 12, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: utcTime, + timeZone: 'UTC-05:00', + ); + + expect(adjustedTime.hour, 7); + expect(offset, const Duration(hours: -5)); + }); + + test('should handle UTC+8 format (without minutes)', () { + final utcTime = DateTime.utc(2024, 6, 15, 12, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: utcTime, + timeZone: 'UTC+8', + ); + + expect(adjustedTime.hour, 20); + expect(offset, const Duration(hours: 8)); + }); + + test('should handle UTC-5 format (without minutes)', () { + final utcTime = DateTime.utc(2024, 6, 15, 12, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: utcTime, + timeZone: 'UTC-5', + ); + + expect(adjustedTime.hour, 7); + expect(offset, const Duration(hours: -5)); + }); + + test('should handle plain UTC format', () { + final utcTime = DateTime.utc(2024, 6, 15, 12, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: utcTime, + timeZone: 'UTC', + ); + + expect(adjustedTime.hour, 12); + expect(offset, Duration.zero); + }); + + test('should handle lowercase utc format', () { + final utcTime = DateTime.utc(2024, 6, 15, 12, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: utcTime, + timeZone: 'utc+08:00', + ); + + expect(adjustedTime.hour, 20); + expect(offset, const Duration(hours: 8)); + }); + + test('should handle UTC+05:30 format (with minutes)', () { + final utcTime = DateTime.utc(2024, 6, 15, 12, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: utcTime, + timeZone: 'UTC+05:30', + ); + + expect(adjustedTime.hour, 17); + expect(adjustedTime.minute, 30); + expect(offset, const Duration(hours: 5, minutes: 30)); + }); + }); + + group('with null or invalid timezone', () { + test('should return UTC time when timezone is null', () { + final localTime = DateTime(2024, 6, 15, 12, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: localTime, + timeZone: null, + ); + + expect(adjustedTime.isUtc, true); + expect(offset, adjustedTime.timeZoneOffset); + }); + + test('should return UTC time when timezone is invalid', () { + final utcTime = DateTime.utc(2024, 6, 15, 12, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: utcTime, + timeZone: 'Invalid/Timezone', + ); + + expect(adjustedTime.isUtc, true); + expect(adjustedTime.hour, 12); + expect(offset, adjustedTime.timeZoneOffset); + }); + + test('should return UTC time when UTC offset format is malformed', () { + final utcTime = DateTime.utc(2024, 6, 15, 12, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: utcTime, + timeZone: 'UTC++08', + ); + + expect(adjustedTime.isUtc, true); + expect(adjustedTime.hour, 12); + }); + }); + + group('edge cases', () { + test('should handle date crossing midnight forward', () { + final utcTime = DateTime.utc(2024, 6, 15, 20, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: utcTime, + timeZone: 'Asia/Tokyo', // UTC+9 + ); + + expect(adjustedTime.day, 16); // Crosses to next day + expect(adjustedTime.hour, 5); // 20:00 UTC + 9 = 05:00 next day + expect(offset, const Duration(hours: 9)); + }); + + test('should handle date crossing midnight backward', () { + final utcTime = DateTime.utc(2024, 6, 15, 3, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: utcTime, + timeZone: 'America/Los_Angeles', // UTC-7 in summer + ); + + expect(adjustedTime.day, 14); // Crosses to previous day + expect(adjustedTime.hour, 20); // 03:00 UTC - 7 = 20:00 previous day + expect(offset, const Duration(hours: -7)); + }); + + test('should handle year boundary crossing', () { + final utcTime = DateTime.utc(2024, 1, 1, 2, 0, 0); + + final (adjustedTime, offset) = applyTimezoneOffset( + dateTime: utcTime, + timeZone: 'America/New_York', // UTC-5 in winter + ); + + expect(adjustedTime.year, 2023); + expect(adjustedTime.month, 12); + expect(adjustedTime.day, 31); + expect(adjustedTime.hour, 21); // 02:00 UTC - 5 = 21:00 Dec 31 + }); + + test('should convert local time to UTC before applying timezone', () { + // Create a local time (not UTC) + final localTime = DateTime(2024, 6, 15, 12, 0, 0); + + final (adjustedTime, _) = applyTimezoneOffset( + dateTime: localTime, + timeZone: 'Asia/Hong_Kong', + ); + + // The function converts to UTC first, then applies timezone + // So local 12:00 -> UTC (depends on local timezone) -> HK time + // We can verify it's working by checking it's a TZDateTime + expect(adjustedTime, isNotNull); + }); + }); + }); +} diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 3adccdee24..aaec601d63 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -23603,13 +23603,13 @@ "actionConfig": { "type": "object" }, - "actionId": { + "pluginActionId": { "format": "uuid", "type": "string" } }, "required": [ - "actionId" + "pluginActionId" ], "type": "object" }, @@ -23619,24 +23619,24 @@ "nullable": true, "type": "object" }, - "actionId": { - "type": "string" - }, "id": { "type": "string" }, "order": { "type": "number" }, + "pluginActionId": { + "type": "string" + }, "workflowId": { "type": "string" } }, "required": [ "actionConfig", - "actionId", "id", "order", + "pluginActionId", "workflowId" ], "type": "object" @@ -23685,13 +23685,13 @@ "filterConfig": { "type": "object" }, - "filterId": { + "pluginFilterId": { "format": "uuid", "type": "string" } }, "required": [ - "filterId" + "pluginFilterId" ], "type": "object" }, @@ -23701,24 +23701,24 @@ "nullable": true, "type": "object" }, - "filterId": { - "type": "string" - }, "id": { "type": "string" }, "order": { "type": "number" }, + "pluginFilterId": { + "type": "string" + }, "workflowId": { "type": "string" } }, "required": [ "filterConfig", - "filterId", "id", "order", + "pluginFilterId", "workflowId" ], "type": "object" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 5f259472a0..f8663ecb1c 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1755,16 +1755,16 @@ export type CreateProfileImageResponseDto = { }; export type WorkflowActionResponseDto = { actionConfig: object | null; - actionId: string; id: string; order: number; + pluginActionId: string; workflowId: string; }; export type WorkflowFilterResponseDto = { filterConfig: object | null; - filterId: string; id: string; order: number; + pluginFilterId: string; workflowId: string; }; export type WorkflowResponseDto = { @@ -1780,11 +1780,11 @@ export type WorkflowResponseDto = { }; export type WorkflowActionItemDto = { actionConfig?: object; - actionId: string; + pluginActionId: string; }; export type WorkflowFilterItemDto = { filterConfig?: object; - filterId: string; + pluginFilterId: string; }; export type WorkflowCreateDto = { actions: WorkflowActionItemDto[]; diff --git a/package.json b/package.json index 5df5070e1e..a5421c0e36 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Monorepo for Immich", "private": true, - "packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c", + "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a", "engines": { "pnpm": ">=10.0.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1d38ac3a1..95e2d4945e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,7 +21,7 @@ importers: devDependencies: prettier: specifier: ^3.5.3 - version: 3.6.2 + version: 3.7.1 cli: dependencies: @@ -67,7 +67,7 @@ importers: 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@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.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)(terser@5.44.0)(yaml@2.8.2)) byte-size: specifier: ^9.0.0 version: 9.0.1 @@ -85,7 +85,7 @@ importers: version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2) + version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.1) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.1(jiti@2.6.1)) @@ -97,43 +97,43 @@ importers: version: 5.5.0 prettier: specifier: ^3.2.5 - version: 3.6.2 + version: 3.7.1 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.6.2)(typescript@5.9.3) + version: 4.3.0(prettier@3.7.1)(typescript@5.9.3) typescript: specifier: ^5.3.3 version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + version: 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.0.0 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) vite-tsconfig-paths: specifier: ^5.0.0 - version: 5.1.4(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)) vitest: specifier: ^3.0.0 - 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)(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)(terser@5.44.0)(yaml@2.8.2) vitest-fetch-mock: specifier: ^0.4.0 - 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)(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)(terser@5.44.0)(yaml@2.8.2)) yaml: specifier: ^2.3.1 - version: 2.8.1 + version: 2.8.2 docs: dependencies: '@docusaurus/core': specifier: ~3.9.0 - version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/preset-classic': specifier: ~3.9.0 - version: 3.9.2(@algolia/client-search@5.41.0)(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(@types/react@19.2.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) + version: 3.9.2(@algolia/client-search@5.41.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) '@docusaurus/theme-common': specifier: ~3.9.0 - version: 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mdi/js': specifier: ^7.3.67 version: 7.4.47 @@ -142,13 +142,13 @@ importers: version: 1.6.1 '@mdx-js/react': specifier: ^3.0.0 - version: 3.1.1(@types/react@19.2.6)(react@18.3.1) + version: 3.1.1(@types/react@19.2.7)(react@18.3.1) autoprefixer: specifier: ^10.4.17 version: 10.4.22(postcss@8.5.6) docusaurus-lunr-search: specifier: ^3.3.2 - version: 3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) lunr: specifier: ^2.3.9 version: 2.3.9 @@ -160,7 +160,7 @@ importers: version: 2.4.1(react@18.3.1) raw-loader: specifier: ^4.0.2 - version: 4.0.2(webpack@5.102.1) + version: 4.0.2(webpack@5.103.0) react: specifier: ^18.0.0 version: 18.3.1 @@ -169,7 +169,7 @@ importers: version: 18.3.1(react@18.3.1) tailwindcss: specifier: ^3.2.4 - version: 3.4.18(yaml@2.8.1) + version: 3.4.18(yaml@2.8.2) url: specifier: ^0.11.0 version: 0.11.4 @@ -185,7 +185,7 @@ importers: version: 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) prettier: specifier: ^3.2.4 - version: 3.6.2 + version: 3.7.1 typescript: specifier: ^5.1.6 version: 5.9.3 @@ -206,7 +206,7 @@ importers: version: link:../open-api/typescript-sdk '@playwright/test': specifier: ^1.44.1 - version: 1.56.1 + version: 1.57.0 '@socket.io/component-emitter': specifier: ^3.1.2 version: 3.1.2 @@ -239,7 +239,7 @@ importers: version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2) + version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.1) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.1(jiti@2.6.1)) @@ -266,10 +266,10 @@ importers: version: 7.0.0 prettier: specifier: ^3.2.5 - version: 3.6.2 + version: 3.7.1 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.6.2)(typescript@5.9.3) + version: 4.3.0(prettier@3.7.1)(typescript@5.9.3) sharp: specifier: ^0.34.5 version: 0.34.5 @@ -284,13 +284,13 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + version: 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) utimes: specifier: ^5.2.1 version: 5.2.1(encoding@0.1.13) vitest: specifier: ^3.0.0 - 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)(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)(terser@5.44.0)(yaml@2.8.2) open-api/typescript-sdk: dependencies: @@ -324,28 +324,28 @@ importers: version: 2.0.0-rc13 '@nestjs/bullmq': specifier: ^11.0.1 - version: 11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(bullmq@5.64.0) + version: 11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(bullmq@5.65.0) '@nestjs/common': specifier: ^11.0.4 - version: 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.4 - version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^11.0.4 - version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) '@nestjs/platform-socket.io': specifier: ^11.0.4 - version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.9)(rxjs@7.8.2) + version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.9)(rxjs@7.8.2) '@nestjs/schedule': specifier: ^6.0.0 - version: 6.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + version: 6.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) '@nestjs/swagger': specifier: ^11.0.2 - version: 11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) + version: 11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) '@nestjs/websockets': specifier: ^11.0.4 - version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.0 @@ -353,20 +353,20 @@ importers: specifier: ^2.0.0 version: 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-prometheus': - specifier: ^0.207.0 - version: 0.207.0(@opentelemetry/api@1.9.0) + specifier: ^0.208.0 + version: 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-http': - specifier: ^0.207.0 - version: 0.207.0(@opentelemetry/api@1.9.0) + specifier: ^0.208.0 + version: 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-ioredis': + specifier: ^0.56.0 + version: 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-nestjs-core': specifier: ^0.55.0 version: 0.55.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-nestjs-core': - specifier: ^0.54.0 - version: 0.54.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation-pg': - specifier: ^0.60.0 - version: 0.60.0(@opentelemetry/api@1.9.0) + specifier: ^0.61.0 + version: 0.61.1(@opentelemetry/api@1.9.0) '@opentelemetry/resources': specifier: ^2.0.1 version: 2.2.0(@opentelemetry/api@1.9.0) @@ -374,8 +374,8 @@ importers: specifier: ^2.0.1 version: 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-node': - specifier: ^0.207.0 - version: 0.207.0(@opentelemetry/api@1.9.0) + specifier: ^0.208.0 + version: 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': specifier: ^1.34.0 version: 1.38.0 @@ -405,7 +405,7 @@ importers: version: 2.2.1 bullmq: specifier: ^5.51.0 - version: 5.64.0 + version: 5.65.0 chokidar: specifier: ^4.0.3 version: 4.0.3 @@ -414,13 +414,13 @@ importers: version: 0.5.1 class-validator: specifier: ^0.14.0 - version: 0.14.2 + version: 0.14.3 compression: specifier: ^1.8.0 version: 1.8.1 cookie: specifier: ^1.0.2 - version: 1.0.2 + version: 1.1.1 cookie-parser: specifier: ^1.4.7 version: 1.4.7 @@ -432,7 +432,7 @@ importers: version: 33.5.0 express: specifier: ^5.1.0 - version: 5.1.0 + version: 5.2.0 fast-glob: specifier: ^3.3.2 version: 3.3.3 @@ -480,19 +480,19 @@ importers: version: 2.0.2 nest-commander: specifier: ^3.16.0 - version: 3.20.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@types/inquirer@8.2.11)(@types/node@24.10.1)(typescript@5.9.3) + version: 3.20.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@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.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 5.4.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) nestjs-kysely: specifier: 3.1.2 - version: 3.1.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(kysely@0.28.2)(reflect-metadata@0.2.2) + version: 3.1.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(kysely@0.28.2)(reflect-metadata@0.2.2) nestjs-otel: specifier: ^7.0.0 - version: 7.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + version: 7.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) nodemailer: specifier: ^7.0.0 - version: 7.0.10 + version: 7.0.11 openid-client: specifier: ^6.3.3 version: 6.8.1 @@ -543,7 +543,7 @@ importers: version: 4.8.1 tailwindcss-preset-email: specifier: ^1.4.0 - version: 1.4.1(tailwindcss@3.4.18(yaml@2.8.1)) + version: 1.4.1(tailwindcss@3.4.18(yaml@2.8.2)) thumbhash: specifier: ^0.1.1 version: 0.1.1 @@ -562,16 +562,16 @@ importers: version: 9.39.1 '@nestjs/cli': specifier: ^11.0.2 - version: 11.0.12(@swc/core@1.15.2(@swc/helpers@0.5.17))(@types/node@24.10.1) + version: 11.0.13(@swc/core@1.15.3(@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) '@nestjs/testing': specifier: ^11.0.4 - version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9) + version: 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9) '@swc/core': specifier: ^1.4.14 - version: 1.15.2(@swc/helpers@0.5.17) + version: 1.15.3(@swc/helpers@0.5.17) '@types/archiver': specifier: ^7.0.0 version: 7.0.0 @@ -604,7 +604,7 @@ importers: version: 9.0.10 '@types/lodash': specifier: ^4.14.197 - version: 4.17.20 + version: 4.17.21 '@types/luxon': specifier: ^3.6.2 version: 3.7.1 @@ -628,7 +628,7 @@ importers: version: 6.0.5 '@types/react': specifier: ^19.0.0 - version: 19.2.6 + version: 19.2.7 '@types/sanitize-html': specifier: ^2.13.0 version: 2.16.0 @@ -646,7 +646,7 @@ importers: version: 13.15.10 '@vitest/coverage-v8': specifier: ^3.0.0 - 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)(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)(terser@5.44.0)(yaml@2.8.2)) eslint: specifier: ^9.14.0 version: 9.39.1(jiti@2.6.1) @@ -655,7 +655,7 @@ importers: version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2) + version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.1) eslint-plugin-unicorn: specifier: ^62.0.0 version: 62.0.0(eslint@9.39.1(jiti@2.6.1)) @@ -673,10 +673,10 @@ importers: version: 7.0.0 prettier: specifier: ^3.0.2 - version: 3.6.2 + version: 3.7.1 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.6.2)(typescript@5.9.3) + version: 4.3.0(prettier@3.7.1)(typescript@5.9.3) sql-formatter: specifier: ^15.0.0 version: 15.6.10 @@ -685,25 +685,25 @@ importers: version: 7.1.4 tailwindcss: specifier: ^3.4.0 - version: 3.4.18(yaml@2.8.1) + version: 3.4.18(yaml@2.8.2) testcontainers: specifier: ^11.0.0 - version: 11.8.1 + version: 11.9.0 typescript: specifier: ^5.9.2 version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + version: 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) unplugin-swc: specifier: ^1.4.5 - version: 1.5.8(@swc/core@1.15.2(@swc/helpers@0.5.17))(rollup@4.53.3) + version: 1.5.9(@swc/core@1.15.3(@swc/helpers@0.5.17))(rollup@4.53.3) vite-tsconfig-paths: specifier: ^5.0.0 - version: 5.1.4(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)) vitest: specifier: ^3.0.0 - 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)(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)(terser@5.44.0)(yaml@2.8.2) web: dependencies: @@ -718,7 +718,7 @@ importers: version: link:../open-api/typescript-sdk '@immich/ui': specifier: ^0.49.2 - version: 0.49.2(svelte@5.45.2) + version: 0.49.3(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2) '@mapbox/mapbox-gl-rtl-text': specifier: 0.2.3 version: 0.2.3(mapbox-gl@1.13.3) @@ -748,10 +748,10 @@ importers: version: 7946.0.16 '@zoom-image/core': specifier: ^0.41.0 - version: 0.41.3 + version: 0.41.4 '@zoom-image/svelte': specifier: ^0.3.0 - version: 0.3.7(svelte@5.45.2) + version: 0.3.8(svelte@5.45.2) async-mutex: specifier: ^0.5.0 version: 0.5.0 @@ -796,7 +796,7 @@ importers: version: 1.5.4 simple-icons: specifier: ^15.15.0 - version: 15.21.0 + version: 15.22.0 socket.io-client: specifier: ~4.8.0 version: 4.8.1 @@ -833,25 +833,25 @@ importers: version: 3.1.2 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.10(@sveltejs/kit@2.48.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))) + version: 3.0.10(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2))) '@sveltejs/enhanced-img': - specifier: ^0.8.0 - version: 0.8.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)))(rollup@4.53.3)(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + specifier: ^0.9.0 + version: 0.9.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(rollup@4.53.3)(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)) '@sveltejs/kit': specifier: ^2.27.1 - version: 2.48.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + version: 2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)) '@sveltejs/vite-plugin-svelte': specifier: 6.2.1 - version: 6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + version: 6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)) '@tailwindcss/vite': specifier: ^4.1.7 - version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)) '@testing-library/jest-dom': specifier: ^6.4.2 version: 6.9.1 '@testing-library/svelte': specifier: ^5.2.8 - version: 5.2.9(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.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)(terser@5.44.0)(yaml@2.8.1)) + version: 5.2.9(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2))(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)(terser@5.44.0)(yaml@2.8.2)) '@testing-library/user-event': specifier: ^14.5.2 version: 14.6.1(@testing-library/dom@10.4.1) @@ -875,7 +875,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.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.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)(terser@5.44.0)(yaml@2.8.2)) dotenv: specifier: ^17.0.0 version: 17.2.3 @@ -902,16 +902,16 @@ importers: version: 16.5.0 prettier: specifier: ^3.4.2 - version: 3.6.2 + version: 3.7.1 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(prettier@3.6.2)(typescript@5.9.3) + version: 4.3.0(prettier@3.7.1)(typescript@5.9.3) prettier-plugin-sort-json: specifier: ^4.1.1 - version: 4.1.1(prettier@3.6.2) + version: 4.1.1(prettier@3.7.1) prettier-plugin-svelte: specifier: ^3.3.3 - version: 3.4.0(prettier@3.6.2)(svelte@5.45.2) + version: 3.4.0(prettier@3.7.1)(svelte@5.45.2) rollup-plugin-visualizer: specifier: ^6.0.0 version: 6.0.5(rollup@4.53.3) @@ -932,13 +932,13 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.45.0 - version: 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + version: 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.1.2 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) vitest: specifier: ^3.0.0 - 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)(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)(terser@5.44.0)(yaml@2.8.2) packages: @@ -2805,8 +2805,8 @@ packages: '@nestjs/common': ^11.0.20 '@nestjs/core': ^11.0.20 - '@grpc/grpc-js@1.14.1': - resolution: {integrity: sha512-sPxgEWtPUR3EnRJCEtbGZG2iX8LQDUls2wUS3o27jg07KqJFMq6YDeWvMo1wfpmy3rqRdS0rivpLwhqQtEyCuQ==} + '@grpc/grpc-js@1.14.2': + resolution: {integrity: sha512-QzVUtEFyu05UNx2xr0fCQmStUO17uVQhGNowtxs00IgTZT6/W2PBLfUkj30s0FKJ29VtTa3ArVNIhNP6akQhqA==} engines: {node: '>=12.10.0'} '@grpc/proto-loader@0.7.15': @@ -2986,8 +2986,8 @@ packages: peerDependencies: svelte: ^5.0.0 - '@immich/ui@0.49.2': - resolution: {integrity: sha512-7Tn/pG5LobXt0FoNICTxQyxjpADRGTy/Yr69Zb/hrAkFxvYUSykK13SPc3rTXiw0rd3ykkNKru8N7kfeCxqHqQ==} + '@immich/ui@0.49.3': + resolution: {integrity: sha512-joqT72Y6gmGK6z25Suzr2VhYANrLo43g20T4UHmbQenz/z/Ax6sl1Ao9SjIOwEkKMm9N3Txoh7WOOzmHVl04OA==} peerDependencies: svelte: ^5.0.0 @@ -3377,8 +3377,8 @@ packages: '@nestjs/core': ^10.0.0 || ^11.0.0 bullmq: ^3.0.0 || ^4.0.0 || ^5.0.0 - '@nestjs/cli@11.0.12': - resolution: {integrity: sha512-V3fD1xESlFcJ1xpwOtUhn0edLvIa76Sx8mkvdR1s8cM4c/rZO+yGmXP30ZQwPfIJPTgBvsw93F/i+87eV96wcQ==} + '@nestjs/cli@11.0.13': + resolution: {integrity: sha512-cPDDahP/IPDcqraIHjUZQXPupYQDoZlBRFOjgM76vVfcVnWQ45TUaat0tgR2ptfeQG0gY2kwtm4/JYwe5xu/8A==} engines: {node: '>= 20.11'} hasBin: true peerDependencies: @@ -3532,8 +3532,8 @@ packages: '@oazapfts/runtime@1.0.4': resolution: {integrity: sha512-7t6C2shug/6tZhQgkCa532oTYBLEnbASV/i1SG1rH2GB4h3aQQujYciYSPT92hvN4IwTe8S2hPkN/6iiOyTlCg==} - '@opentelemetry/api-logs@0.207.0': - resolution: {integrity: sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==} + '@opentelemetry/api-logs@0.208.0': + resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} engines: {node: '>=8.0.0'} '@opentelemetry/api@1.9.0': @@ -3552,62 +3552,62 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-logs-otlp-grpc@0.207.0': - resolution: {integrity: sha512-K92RN+kQGTMzFDsCzsYNGqOsXRUnko/Ckk+t/yPJao72MewOLgBUTWVHhebgkNfRCYqDz1v3K0aPT9OJkemvgg==} + '@opentelemetry/exporter-logs-otlp-grpc@0.208.0': + resolution: {integrity: sha512-AmZDKFzbq/idME/yq68M155CJW1y056MNBekH9OZewiZKaqgwYN4VYfn3mXVPftYsfrCM2r4V6tS8H2LmfiDCg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-http@0.207.0': - resolution: {integrity: sha512-JpOh7MguEUls8eRfkVVW3yRhClo5b9LqwWTOg8+i4gjr/+8eiCtquJnC7whvpTIGyff06cLZ2NsEj+CVP3Mjeg==} + '@opentelemetry/exporter-logs-otlp-http@0.208.0': + resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-proto@0.207.0': - resolution: {integrity: sha512-RQJEV/K6KPbQrIUbsrRkEe0ufks1o5OGLHy6jbDD8tRjeCsbFHWfg99lYBRqBV33PYZJXsigqMaAbjWGTFYzLw==} + '@opentelemetry/exporter-logs-otlp-proto@0.208.0': + resolution: {integrity: sha512-Wy8dZm16AOfM7yddEzSFzutHZDZ6HspKUODSUJVjyhnZFMBojWDjSNgduyCMlw6qaxJYz0dlb0OEcb4Eme+BfQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-grpc@0.207.0': - resolution: {integrity: sha512-6flX89W54gkwmqYShdcTBR1AEF5C1Ob0O8pDgmLPikTKyEv27lByr9yBmO5WrP0+5qJuNPHrLfgFQFYi6npDGA==} + '@opentelemetry/exporter-metrics-otlp-grpc@0.208.0': + resolution: {integrity: sha512-YbEnk7jjYmvhIwp2xJGkEvdgnayrA2QSr28R1LR1klDPvCxsoQPxE6TokDbQpoCEhD3+KmJVEXfb4EeEQxjymg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-http@0.207.0': - resolution: {integrity: sha512-fG8FAJmvXOrKXGIRN8+y41U41IfVXxPRVwyB05LoMqYSjugx/FSBkMZUZXUT/wclTdmBKtS5MKoi0bEKkmRhSw==} + '@opentelemetry/exporter-metrics-otlp-http@0.208.0': + resolution: {integrity: sha512-QZ3TrI90Y0i1ezWQdvreryjY0a5TK4J9gyDLIyhLBwV+EQUvyp5wR7TFPKCAexD4TDSWM0t3ulQDbYYjVtzTyA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-proto@0.207.0': - resolution: {integrity: sha512-kDBxiTeQjaRlUQzS1COT9ic+et174toZH6jxaVuVAvGqmxOkgjpLOjrI5ff8SMMQE69r03L3Ll3nPKekLopLwg==} + '@opentelemetry/exporter-metrics-otlp-proto@0.208.0': + resolution: {integrity: sha512-CvvVD5kRDmRB/uSMalvEF6kiamY02pB46YAqclHtfjJccNZFxbkkXkMMmcJ7NgBFa5THmQBNVQ2AHyX29nRxOw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-prometheus@0.207.0': - resolution: {integrity: sha512-Y5p1s39FvIRmU+F1++j7ly8/KSqhMmn6cMfpQqiDCqDjdDHwUtSq0XI0WwL3HYGnZeaR/VV4BNmsYQJ7GAPrhw==} + '@opentelemetry/exporter-prometheus@0.208.0': + resolution: {integrity: sha512-Rgws8GfIfq2iNWCD3G1dTD9xwYsCof1+tc5S5X0Ahdb5CrAPE+k5P70XCWHqrFFurVCcKaHLJ/6DjIBHWVfLiw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.207.0': - resolution: {integrity: sha512-7u2ZmcIx6D4KG/+5np4X2qA0o+O0K8cnUDhR4WI/vr5ZZ0la9J9RG+tkSjC7Yz+2XgL6760gSIM7/nyd3yaBLA==} + '@opentelemetry/exporter-trace-otlp-grpc@0.208.0': + resolution: {integrity: sha512-E/eNdcqVUTAT7BC+e8VOw/krqb+5rjzYkztMZ/o+eyJl+iEY6PfczPXpwWuICwvsm0SIhBoh9hmYED5Vh5RwIw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-http@0.207.0': - resolution: {integrity: sha512-HSRBzXHIC7C8UfPQdu15zEEoBGv0yWkhEwxqgPCHVUKUQ9NLHVGXkVrf65Uaj7UwmAkC1gQfkuVYvLlD//AnUQ==} + '@opentelemetry/exporter-trace-otlp-http@0.208.0': + resolution: {integrity: sha512-jbzDw1q+BkwKFq9yxhjAJ9rjKldbt5AgIy1gmEIJjEV/WRxQ3B6HcLVkwbjJ3RcMif86BDNKR846KJ0tY0aOJA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-proto@0.207.0': - resolution: {integrity: sha512-ruUQB4FkWtxHjNmSXjrhmJZFvyMm+tBzHyMm7YPQshApy4wvZUTcrpPyP/A/rCl/8M4BwoVIZdiwijMdbZaq4w==} + '@opentelemetry/exporter-trace-otlp-proto@0.208.0': + resolution: {integrity: sha512-q844Jc3ApkZVdWYd5OAl+an3n1XXf3RWHa3Zgmnhw3HpsM3VluEKHckUUEqHPzbwDUx2lhPRVkqK7LsJ/CbDzA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -3624,50 +3624,50 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-http@0.207.0': - resolution: {integrity: sha512-FC4i5hVixTzuhg4SV2ycTEAYx+0E2hm+GwbdoVPSA6kna0pPVI4etzaA9UkpJ9ussumQheFXP6rkGIaFJjMxsw==} + '@opentelemetry/instrumentation-http@0.208.0': + resolution: {integrity: sha512-rhmK46DRWEbQQB77RxmVXGyjs6783crXCnFjYQj+4tDH/Kpv9Rbg3h2kaNyp5Vz2emF1f9HOQQvZoHzwMWOFZQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-ioredis@0.55.0': - resolution: {integrity: sha512-ASuBMzh0ImmfOnWj9vCPtBMqSjr54/r/HluUIylwZB7xzTU6gL2SfybxySJMzEL9+386gJJVApwQktVznAtrWA==} + '@opentelemetry/instrumentation-ioredis@0.56.0': + resolution: {integrity: sha512-XSWeqsd3rKSsT3WBz/JKJDcZD4QYElZEa0xVdX8f9dh4h4QgXhKRLorVsVkK3uXFbC2sZKAS2Ds+YolGwD83Dg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-nestjs-core@0.54.0': - resolution: {integrity: sha512-kqJcOVcniazueWTXt9czK6gd9xlHw5IM5JQM4wfH0ZkjZjNkKtQNzlhjdJpvqVhU9bGHet1yfrHOKXxlP4YeOA==} + '@opentelemetry/instrumentation-nestjs-core@0.55.0': + resolution: {integrity: sha512-JFLNhbbEGnnQrMKOYoXx0nNk5N9cPeghu4xP/oup40a7VaSeYruyOiFbg9nkbS4ZQiI8aMuRqUT3Mo4lQjKEKg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-pg@0.60.0': - resolution: {integrity: sha512-qZKeQojYJMoo7kWbHw/+SHopSdhfTxNISsBS+ZBbkr44sepmk/PmyU2AbOsSK7VOKvFt3oZde2n2nly7rk0SsA==} + '@opentelemetry/instrumentation-pg@0.61.1': + resolution: {integrity: sha512-VKKts/XcOCa7IPBxVjL2B4UyG+YTNa4Dh1Xx2vqL0jOEQBJlNsv++I12BUw/8NRLEr2K/gOM5tpVU7QqhWA65A==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.207.0': - resolution: {integrity: sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA==} + '@opentelemetry/instrumentation@0.208.0': + resolution: {integrity: sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-exporter-base@0.207.0': - resolution: {integrity: sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw==} + '@opentelemetry/otlp-exporter-base@0.208.0': + resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-grpc-exporter-base@0.207.0': - resolution: {integrity: sha512-eKFjKNdsPed4q9yYqeI5gBTLjXxDM/8jwhiC0icw3zKxHVGBySoDsed5J5q/PGY/3quzenTr3FiTxA3NiNT+nw==} + '@opentelemetry/otlp-grpc-exporter-base@0.208.0': + resolution: {integrity: sha512-fGvAg3zb8fC0oJAzfz7PQppADI2HYB7TSt/XoCaBJFi1mSquNUjtHXEoviMgObLAa1NRIgOC1lsV1OUKi+9+lQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-transformer@0.207.0': - resolution: {integrity: sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA==} + '@opentelemetry/otlp-transformer@0.208.0': + resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -3694,8 +3694,8 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-logs@0.207.0': - resolution: {integrity: sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg==} + '@opentelemetry/sdk-logs@0.208.0': + resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' @@ -3706,8 +3706,8 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-node@0.207.0': - resolution: {integrity: sha512-hnRsX/M8uj0WaXOBvFenQ8XsE8FLVh2uSnn1rkWu4mx+qu7EKGUZvZng6y/95cyzsqOfiaDDr08Ek4jppkIDNg==} + '@opentelemetry/sdk-node@0.208.0': + resolution: {integrity: sha512-pbAqpZ7zTMFuTf3YecYsecsto/mheuvnK2a/jgstsE5ynWotBjgF5bnz5500W9Xl2LeUfg04WMt63TWtAgzRMw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -3778,8 +3778,8 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.56.1': - resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==} + '@playwright/test@1.57.0': + resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} engines: {node: '>=18'} hasBin: true @@ -4291,8 +4291,8 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - '@sveltejs/acorn-typescript@1.0.7': - resolution: {integrity: sha512-znp1A/Y1Jj4l/Zy7PX5DZKBE0ZNY+5QBngiE21NJkfSTyzzC5iKNWOtwFXKtIrn7MXEFBck4jD95iBNkGjK92Q==} + '@sveltejs/acorn-typescript@1.0.8': + resolution: {integrity: sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==} peerDependencies: acorn: ^8.9.0 @@ -4301,15 +4301,15 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/enhanced-img@0.8.5': - resolution: {integrity: sha512-DVJYSAucbzMPD+B7+9yDZNzAysf+OkSifPZwh8tFpzQDqW/imxtkLjyeVBSn/kwLa709wAbiY08vghDZDpqIbQ==} + '@sveltejs/enhanced-img@0.9.2': + resolution: {integrity: sha512-hAYZ8YFgYtqrQ0dXyq6rdmHBFyG+eIQnNjdIoVhqeZQEBIREXoBThkx+7FtDa6ZV35lTRaT9dgFKF4W+4LbuaQ==} peerDependencies: '@sveltejs/vite-plugin-svelte': ^6.0.0 svelte: ^5.0.0 vite: ^6.3.0 || >=7.0.0 - '@sveltejs/kit@2.48.5': - resolution: {integrity: sha512-/rnwfSWS3qwUSzvHynUTORF9xSJi7PCR9yXkxUOnRrNqyKmCmh3FPHH+E9BbgqxXfTevGXBqgnlh9kMb+9T5XA==} + '@sveltejs/kit@2.49.0': + resolution: {integrity: sha512-oH8tXw7EZnie8FdOWYrF7Yn4IKrqTFHhXvl8YxXxbKwTMcD/5NNCryUSEXRk2ZR4ojnub0P8rNrsVGHXWqIDtA==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -4414,68 +4414,68 @@ packages: resolution: {integrity: sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==} engines: {node: '>=14'} - '@swc/core-darwin-arm64@1.15.2': - resolution: {integrity: sha512-Ghyz4RJv4zyXzrUC1B2MLQBbppIB5c4jMZJybX2ebdEQAvryEKp3gq1kBksCNsatKGmEgXul88SETU19sMWcrw==} + '@swc/core-darwin-arm64@1.15.3': + resolution: {integrity: sha512-AXfeQn0CvcQ4cndlIshETx6jrAM45oeUrK8YeEY6oUZU/qzz0Id0CyvlEywxkWVC81Ajpd8TQQ1fW5yx6zQWkQ==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.15.2': - resolution: {integrity: sha512-7n/PGJOcL2QoptzL42L5xFFfXY5rFxLHnuz1foU+4ruUTG8x2IebGhtwVTpaDN8ShEv2UZObBlT1rrXTba15Zw==} + '@swc/core-darwin-x64@1.15.3': + resolution: {integrity: sha512-p68OeCz1ui+MZYG4wmfJGvcsAcFYb6Sl25H9TxWl+GkBgmNimIiRdnypK9nBGlqMZAcxngNPtnG3kEMNnvoJ2A==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.15.2': - resolution: {integrity: sha512-ZUQVCfRJ9wimuxkStRSlLwqX4TEDmv6/J+E6FicGkQ6ssLMWoKDy0cAo93HiWt/TWEee5vFhFaSQYzCuBEGO6A==} + '@swc/core-linux-arm-gnueabihf@1.15.3': + resolution: {integrity: sha512-Nuj5iF4JteFgwrai97mUX+xUOl+rQRHqTvnvHMATL/l9xE6/TJfPBpd3hk/PVpClMXG3Uvk1MxUFOEzM1JrMYg==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.15.2': - resolution: {integrity: sha512-GZh3pYBmfnpQ+JIg+TqLuz+pM+Mjsk5VOzi8nwKn/m+GvQBsxD5ectRtxuWUxMGNG8h0lMy4SnHRqdK3/iJl7A==} + '@swc/core-linux-arm64-gnu@1.15.3': + resolution: {integrity: sha512-2Nc/s8jE6mW2EjXWxO/lyQuLKShcmTrym2LRf5Ayp3ICEMX6HwFqB1EzDhwoMa2DcUgmnZIalesq2lG3krrUNw==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.15.2': - resolution: {integrity: sha512-5av6VYZZeneiYIodwzGMlnyVakpuYZryGzFIbgu1XP8wVylZxduEzup4eP8atiMDFmIm+s4wn8GySJmYqeJC0A==} + '@swc/core-linux-arm64-musl@1.15.3': + resolution: {integrity: sha512-j4SJniZ/qaZ5g8op+p1G9K1z22s/EYGg1UXIb3+Cg4nsxEpF5uSIGEE4mHUfA70L0BR9wKT2QF/zv3vkhfpX4g==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.15.2': - resolution: {integrity: sha512-1nO/UfdCLuT/uE/7oB3EZgTeZDCIa6nL72cFEpdegnqpJVNDI6Qb8U4g/4lfVPkmHq2lvxQ0L+n+JdgaZLhrRA==} + '@swc/core-linux-x64-gnu@1.15.3': + resolution: {integrity: sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.15.2': - resolution: {integrity: sha512-Ksfrb0Tx310kr+TLiUOvB/I80lyZ3lSOp6cM18zmNRT/92NB4mW8oX2Jo7K4eVEI2JWyaQUAFubDSha2Q+439A==} + '@swc/core-linux-x64-musl@1.15.3': + resolution: {integrity: sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.15.2': - resolution: {integrity: sha512-IzUb5RlMUY0r1A9IuJrQ7Tbts1wWb73/zXVXT8VhewbHGoNlBKE0qUhKMED6Tv4wDF+pmbtUJmKXDthytAvLmg==} + '@swc/core-win32-arm64-msvc@1.15.3': + resolution: {integrity: sha512-L9AjzP2ZQ/Xh58e0lTRMLvEDrcJpR7GwZqAtIeNLcTK7JVE+QineSyHp0kLkO1rttCHyCy0U74kDTj0dRz6raA==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.15.2': - resolution: {integrity: sha512-kCATEzuY2LP9AlbU2uScjcVhgnCAkRdu62vbce17Ro5kxEHxYWcugkveyBRS3AqZGtwAKYbMAuNloer9LS/hpw==} + '@swc/core-win32-ia32-msvc@1.15.3': + resolution: {integrity: sha512-B8UtogMzErUPDWUoKONSVBdsgKYd58rRyv2sHJWKOIMCHfZ22FVXICR4O/VwIYtlnZ7ahERcjayBHDlBZpR0aw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.15.2': - resolution: {integrity: sha512-iJaHeYCF4jTn7OEKSa3KRiuVFIVYts8jYjNmCdyz1u5g8HRyTDISD76r8+ljEOgm36oviRQvcXaw6LFp1m0yyA==} + '@swc/core-win32-x64-msvc@1.15.3': + resolution: {integrity: sha512-SpZKMR9QBTecHeqpzJdYEfgw30Oo8b/Xl6rjSzBt1g0ZsXyy60KLXrp6IagQyfTYqNYE/caDvwtF2FPn7pomog==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.15.2': - resolution: {integrity: sha512-OQm+yJdXxvSjqGeaWhP6Ia264ogifwAO7Q12uTDVYj/Ks4jBTI4JknlcjDRAXtRhqbWsfbZyK/5RtuIPyptk3w==} + '@swc/core@1.15.3': + resolution: {integrity: sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -4828,8 +4828,8 @@ packages: '@types/lodash-es@4.17.12': resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} - '@types/lodash@4.17.20': - resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + '@types/lodash@4.17.21': + resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} '@types/luxon@3.7.1': resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==} @@ -4885,9 +4885,6 @@ packages: '@types/pg-pool@2.0.6': resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} - '@types/pg@8.15.5': - resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==} - '@types/pg@8.15.6': resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} @@ -4918,8 +4915,8 @@ packages: '@types/react-router@5.1.20': resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} - '@types/react@19.2.6': - resolution: {integrity: sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==} + '@types/react@19.2.7': + resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} '@types/readdir-glob@1.1.5': resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} @@ -4996,63 +4993,63 @@ packages: '@types/yargs@17.0.34': resolution: {integrity: sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==} - '@typescript-eslint/eslint-plugin@8.47.0': - resolution: {integrity: sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==} + '@typescript-eslint/eslint-plugin@8.48.0': + resolution: {integrity: sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.47.0 + '@typescript-eslint/parser': ^8.48.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.47.0': - resolution: {integrity: sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==} + '@typescript-eslint/parser@8.48.0': + resolution: {integrity: sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.47.0': - resolution: {integrity: sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==} + '@typescript-eslint/project-service@8.48.0': + resolution: {integrity: sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.47.0': - resolution: {integrity: sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==} + '@typescript-eslint/scope-manager@8.48.0': + resolution: {integrity: sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.47.0': - resolution: {integrity: sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==} + '@typescript-eslint/tsconfig-utils@8.48.0': + resolution: {integrity: sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.47.0': - resolution: {integrity: sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==} + '@typescript-eslint/type-utils@8.48.0': + resolution: {integrity: sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.47.0': - resolution: {integrity: sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==} + '@typescript-eslint/types@8.48.0': + resolution: {integrity: sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.47.0': - resolution: {integrity: sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==} + '@typescript-eslint/typescript-estree@8.48.0': + resolution: {integrity: sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.47.0': - resolution: {integrity: sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==} + '@typescript-eslint/utils@8.48.0': + resolution: {integrity: sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.47.0': - resolution: {integrity: sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==} + '@typescript-eslint/visitor-keys@8.48.0': + resolution: {integrity: sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ungap/structured-clone@1.3.0': @@ -5151,11 +5148,11 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - '@zoom-image/core@0.41.3': - resolution: {integrity: sha512-yW2sZRP6FnZZ7vREpYucDpSCsVFgMSWq22Sp1kabsMPz5+6LSs+sBPPDCVs/ahVYSHa9YOX8rlGHM5iiTFPZLA==} + '@zoom-image/core@0.41.4': + resolution: {integrity: sha512-zUJNHWQzx8rmfNOlp2Rr0+n8I7QK9hLNThnusdtvz20/HN+J//RcDJmCuRDj6jUW/qJGh9FWR5sROMFBuPLPfQ==} - '@zoom-image/svelte@0.3.7': - resolution: {integrity: sha512-vCgA2ClH8J2JBTzpJEgzy03yTi5uJE0963cUjBwc5fi7q8g1wxbbj4/W7YHwQfDmdh9uqHxlX0VcmUQqV44d+w==} + '@zoom-image/svelte@0.3.8': + resolution: {integrity: sha512-rkXS+JS4qkBccmRK9+I5j+Pe4rp78GWK/7y0EduBJNtt38q+AwmKhhQs8oTMKTU6lOzLgxjXy1TI802mtvcAmw==} peerDependencies: svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 @@ -5462,8 +5459,8 @@ packages: bare-abort-controller: optional: true - bare-fs@4.5.1: - resolution: {integrity: sha512-zGUCsm3yv/ePt2PHNbVxjjn0nNB1MkIaR4wOCxJ2ig5pCf5cCVAYJXVhQg/3OhhJV6DB1ts7Hv0oUaElc2TPQg==} + bare-fs@4.5.2: + resolution: {integrity: sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw==} engines: {bare: '>=1.16.0'} peerDependencies: bare-buffer: '*' @@ -5527,8 +5524,8 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - bits-ui@2.9.8: - resolution: {integrity: sha512-oVAqdhLSuGIgEiT0yu3ShSI7AxncCxX26Gv6Lul94BuKHV2uzHoKfIodtnMQSq+udJ54svuCIRqA58whsv7vaA==} + bits-ui@2.14.4: + resolution: {integrity: sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg==} engines: {node: '>=20'} peerDependencies: '@internationalized/date': ^3.8.1 @@ -5537,8 +5534,8 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - body-parser@1.20.3: - resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} body-parser@2.2.1: @@ -5593,16 +5590,16 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - buildcheck@0.0.6: - resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==} + buildcheck@0.0.7: + resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} engines: {node: '>=10.0.0'} builtin-modules@5.0.0: resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} engines: {node: '>=18.20'} - bullmq@5.64.0: - resolution: {integrity: sha512-4o+RWWayu/yiRzETY7rP4Ol/iRh4YpMn37kRrIR0UkbxHVwQyuj3ReAgAEpPeHmGRXsMnnI1+lA/zxMSJZQ3tA==} + bullmq@5.65.0: + resolution: {integrity: sha512-fyOcyf2ad4zrNmE18vdF/ie7DrW0TwhLt5e0DkqDxbRpDNiUdYqgp2QZJW2ntnUN08T2mDMC4deUUhF2UOAmeQ==} bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} @@ -5782,8 +5779,8 @@ packages: class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} - class-validator@0.14.2: - resolution: {integrity: sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==} + class-validator@0.14.3: + resolution: {integrity: sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==} clean-css@5.3.3: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} @@ -5983,9 +5980,9 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} - content-disposition@1.0.0: - resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} - engines: {node: '>= 0.6'} + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} @@ -6001,6 +5998,9 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -6009,16 +6009,12 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} - cookie@0.7.1: - resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} - engines: {node: '>= 0.6'} - cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - cookie@1.0.2: - resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} cookiejar@2.1.4: @@ -6869,14 +6865,18 @@ packages: exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} - express@4.21.2: - resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} engines: {node: '>= 0.10.0'} express@5.1.0: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} + express@5.2.0: + resolution: {integrity: sha512-XdpJDLxfztVY59X0zPI6sibRiGcxhTPXRD3IhJmjKf2jwMvkRGV1j7loB8U+heeamoU3XvihAaGRTR4aXXUN3A==} + engines: {node: '>= 18'} + exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -6981,13 +6981,13 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@1.3.1: - resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} - finalhandler@2.1.0: - resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} - engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} find-cache-dir@4.0.0: resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==} @@ -7206,11 +7206,6 @@ packages: engines: {node: 20 || >=22} hasBin: true - glob@12.0.0: - resolution: {integrity: sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==} - engines: {node: 20 || >=22} - hasBin: true - glob@13.0.0: resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} engines: {node: 20 || >=22} @@ -7545,9 +7540,9 @@ packages: engines: {node: '>=16.x'} hasBin: true - imagetools-core@8.0.0: - resolution: {integrity: sha512-5i4Cx5vrBpVdvT3gvkSGAzzkUCrg/5Jm54UwWbDUSTMp4AjDI4IxiC6dI4+X1PRJYi6eKqWuE+684NJY2iOn3w==} - engines: {node: '>=18.0.0'} + imagetools-core@9.1.0: + resolution: {integrity: sha512-xQjs+2vrxLnAjCq+omuNkd5UQTld9/bP8+YT0LyYTlKfuSQtgUBvqhUwGugzSAh6sCdN+LnROMuLswn5hZ9Fhg==} + engines: {node: '>=20.0.0'} immediate@3.3.0: resolution: {integrity: sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==} @@ -7599,6 +7594,9 @@ packages: inline-style-parser@0.2.4: resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + inquirer@8.2.7: resolution: {integrity: sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==} engines: {node: '>=12.0.0'} @@ -8035,8 +8033,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libphonenumber-js@1.12.9: - resolution: {integrity: sha512-VWwAdNeJgN7jFOD+wN4qx83DTPMVPPAUyx9/TUkBXKLiNkuWWk6anV0439tgdtwaJDrEdqkvdN22iA6J4bUCZg==} + libphonenumber-js@1.12.31: + resolution: {integrity: sha512-Z3IhgVgrqO1S5xPYM3K5XwbkDasU67/Vys4heW+lfSBALcUZjeIIzI8zCLifY+OCzSq+fpDdywMDa7z+4srJPQ==} lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} @@ -8233,8 +8231,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.2: - resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -8851,8 +8849,8 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} - nodemailer@7.0.10: - resolution: {integrity: sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==} + nodemailer@7.0.11: + resolution: {integrity: sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==} engines: {node: '>=6.0.0'} nopt@1.0.10: @@ -9226,13 +9224,13 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - playwright-core@1.56.1: - resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==} + playwright-core@1.57.0: + resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} engines: {node: '>=18'} hasBin: true - playwright@1.56.1: - resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==} + playwright@1.57.0: + resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} engines: {node: '>=18'} hasBin: true @@ -9764,8 +9762,8 @@ packages: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + prettier@3.7.1: + resolution: {integrity: sha512-RWKXE4qB3u5Z6yz7omJkjWwmTfLdcbv44jUVHC5NpfXwFGzvpQM798FGv/6WNK879tc+Cn0AAyherCl1KjbyZQ==} engines: {node: '>=14'} hasBin: true @@ -9863,10 +9861,6 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - qs@6.13.0: - resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} - engines: {node: '>=0.6'} - qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} @@ -9909,8 +9903,8 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} raw-body@3.0.2: @@ -10240,10 +10234,14 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - runed@0.29.2: - resolution: {integrity: sha512-0cq6cA6sYGZwl/FvVqjx9YN+1xEBu9sDDyuWdDW1yWX7JF2wmvmVKfH+hVCZs+csW+P3ARH92MjI3H9QTagOQA==} + runed@0.35.1: + resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==} peerDependencies: + '@sveltejs/kit': ^2.21.0 svelte: ^5.7.0 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} @@ -10331,6 +10329,10 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@0.19.1: + resolution: {integrity: sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==} + engines: {node: '>= 0.8.0'} + send@1.2.0: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} @@ -10428,8 +10430,8 @@ packages: simple-get@3.1.1: resolution: {integrity: sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==} - simple-icons@15.21.0: - resolution: {integrity: sha512-xEKtyjl3bmmnyxZIsxNlN4RnN/BaP2tecQCSxOj1C6llJKt0TG4u+ZNJKSA82yBGrkO0CWW10UBTFMkH6tVKzQ==} + simple-icons@15.22.0: + resolution: {integrity: sha512-i/w5Ie4tENfGYbdCo2iJ+oies0vOFd8QXWHopKOUzudfLCvnmeheF2PpHp89Z2azpc+c2su3lMiWO/SpP+429A==} engines: {node: '>=0.12.18'} sirv@2.0.4: @@ -10673,6 +10675,9 @@ packages: style-to-object@1.0.11: resolution: {integrity: sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==} + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + stylehacks@6.1.1: resolution: {integrity: sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==} engines: {node: ^14 || ^16 || >=18.0} @@ -10730,8 +10735,8 @@ packages: svelte-gestures@5.2.2: resolution: {integrity: sha512-Y+chXPaSx8OsPoFppUwPk8PJzgrZ7xoDJKXeiEc7JBqyKKzXer9hlf8F9O34eFuAWB4/WQEvccACvyBplESL7A==} - svelte-highlight@7.8.4: - resolution: {integrity: sha512-aVp+Q0hH9kI7PlSDrklmFTF4Uj7wYj7UGuqkREnkXlqpEffxr2g6esZcMMaTgECdg5rb2mJqM88+nwS10ecoTg==} + svelte-highlight@7.9.0: + resolution: {integrity: sha512-226LBTtvTnM2L2JkQq8mZeKEeMfPLYyta7VxZatFT4UPX5zdHEerKeMTvrfbxm7MVTWc7TPThsNoVdhWC177KQ==} svelte-i18n@4.0.1: resolution: {integrity: sha512-jaykGlGT5PUaaq04JWbJREvivlCnALtT+m87Kbm0fxyYHynkQaxQMnIKHLm2WeIuBRoljzwgyvz0Z6/CMwfdmQ==} @@ -10766,8 +10771,8 @@ packages: peerDependencies: svelte: ^3.48.0 || ^4 || ^5 - svelte-toolbelt@0.9.3: - resolution: {integrity: sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw==} + svelte-toolbelt@0.10.6: + resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==} engines: {node: '>=18', pnpm: '>=8.7.0'} peerDependencies: svelte: ^5.30.2 @@ -10812,11 +10817,11 @@ packages: tabbable@6.3.0: resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==} - tailwind-merge@3.3.1: - resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwind-merge@3.4.0: + resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} - tailwind-variants@3.1.1: - resolution: {integrity: sha512-ftLXe3krnqkMHsuBTEmaVUXYovXtPyTK7ckEfDRXS8PBZx0bAUas+A0jYxuKA5b8qg++wvQ3d2MQ7l/xeZxbZQ==} + tailwind-variants@3.2.2: + resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==} engines: {node: '>=16.x', pnpm: '>=7.x'} peerDependencies: tailwind-merge: '>=3.0.0' @@ -10900,8 +10905,8 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} - testcontainers@11.8.1: - resolution: {integrity: sha512-XeqoHbgVA8lx9ufrgYIKlYV4eubVCn3CL6Dh7sdHT793/hbDu/S7N786MqBxQZjzDG+YRKFSCNPTEwp8lREY9Q==} + testcontainers@11.9.0: + resolution: {integrity: sha512-SQ6OqQUig7HcGVF72i+ZVIMvxPSpEz8cgC/B63ekqMzgf98DnveoBbOmqux/Wa5wQAQCt4mEPNMa/Jz7vMg9fQ==} text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} @@ -11120,8 +11125,8 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@8.47.0: - resolution: {integrity: sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==} + typescript-eslint@8.48.0: + resolution: {integrity: sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -11252,13 +11257,13 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - unplugin-swc@1.5.8: - resolution: {integrity: sha512-6qVAQsCn/AMFiw7XGXkNC/q/Or4OpL0zRPertzLT1BoigjOQp0ktPxDWY4lc51FGwVbzW4V1BURaal1rpbJwSg==} + unplugin-swc@1.5.9: + resolution: {integrity: sha512-RKwK3yf0M+MN17xZfF14bdKqfx0zMXYdtOdxLiE6jHAoidupKq3jGdJYANyIM1X/VmABhh1WpdO+/f4+Ol89+g==} peerDependencies: '@swc/core': ^1.2.108 - unplugin@2.3.10: - resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==} + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} update-browserslist-db@1.1.4: @@ -11361,9 +11366,9 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-imagetools@8.0.0: - resolution: {integrity: sha512-3bkkA0vQ57tMynsetY2j4QhCnZKrxFv0RScaZipzYgkjkkUBEmZL5UIVHOUHhVMfwCetAeM9e3DNwyPK1ff4xg==} - engines: {node: '>=18.0.0'} + vite-imagetools@9.0.2: + resolution: {integrity: sha512-FV5DXw4swU81t+g8JOLT+T7gKuBOXuVsZ0WGhi7y0R182+GfBYkcf6V9/T0Nweu/vn1X0DA2p5ePMnaGZlRl1A==} + engines: {node: '>=20.0.0'} vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} @@ -11540,18 +11545,8 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - webpack@5.100.2: - resolution: {integrity: sha512-QaNKAvGCDRh3wW1dsDjeMdDXwZm2vqq3zn6Pvq4rHOEOGSaUMgOOjG2Y9ZbIGzpfkJk9ZYTHpDqgDfeBDcnLaw==} - engines: {node: '>=10.13.0'} - hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - - webpack@5.102.1: - resolution: {integrity: sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==} + webpack@5.103.0: + resolution: {integrity: sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -11746,8 +11741,8 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.8.1: - resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} engines: {node: '>= 14.6'} hasBin: true @@ -13461,7 +13456,7 @@ snapshots: '@docsearch/css@4.2.0': {} - '@docsearch/react@4.2.0(@algolia/client-search@5.41.0)(@types/react@19.2.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)': + '@docsearch/react@4.2.0(@algolia/client-search@5.41.0)(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)': dependencies: '@ai-sdk/react': 2.0.82(react@18.3.1)(zod@4.1.12) '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.41.0)(algoliasearch@5.41.0)(search-insights@2.17.3) @@ -13471,7 +13466,7 @@ snapshots: marked: 16.4.1 zod: 4.1.12 optionalDependencies: - '@types/react': 19.2.6 + '@types/react': 19.2.7 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) search-insights: 2.17.3 @@ -13512,24 +13507,24 @@ snapshots: '@docusaurus/logger': 3.9.2 '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - babel-loader: 9.2.1(@babel/core@7.28.5)(webpack@5.102.1) + babel-loader: 9.2.1(@babel/core@7.28.5)(webpack@5.103.0) clean-css: 5.3.3 - copy-webpack-plugin: 11.0.0(webpack@5.102.1) - css-loader: 6.11.0(webpack@5.102.1) - css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.102.1) + copy-webpack-plugin: 11.0.0(webpack@5.103.0) + css-loader: 6.11.0(webpack@5.103.0) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.103.0) cssnano: 6.1.2(postcss@8.5.6) - file-loader: 6.2.0(webpack@5.102.1) + file-loader: 6.2.0(webpack@5.103.0) html-minifier-terser: 7.2.0 - mini-css-extract-plugin: 2.9.4(webpack@5.102.1) - null-loader: 4.0.1(webpack@5.102.1) + mini-css-extract-plugin: 2.9.4(webpack@5.103.0) + null-loader: 4.0.1(webpack@5.103.0) postcss: 8.5.6 - postcss-loader: 7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.102.1) + postcss-loader: 7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.103.0) postcss-preset-env: 10.4.0(postcss@8.5.6) - terser-webpack-plugin: 5.3.14(webpack@5.102.1) + terser-webpack-plugin: 5.3.14(webpack@5.103.0) tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.102.1))(webpack@5.102.1) - webpack: 5.102.1 - webpackbar: 6.0.1(webpack@5.102.1) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.103.0))(webpack@5.103.0) + webpack: 5.103.0 + webpackbar: 6.0.1(webpack@5.103.0) transitivePeerDependencies: - '@parcel/css' - '@rspack/core' @@ -13545,7 +13540,7 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: '@docusaurus/babel': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/bundler': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) @@ -13554,7 +13549,7 @@ snapshots: '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mdx-js/react': 3.1.1(@types/react@19.2.6)(react@18.3.1) + '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@18.3.1) boxen: 6.2.1 chalk: 4.1.2 chokidar: 3.6.0 @@ -13569,7 +13564,7 @@ snapshots: execa: 5.1.1 fs-extra: 11.3.2 html-tags: 3.3.1 - html-webpack-plugin: 5.6.4(webpack@5.102.1) + html-webpack-plugin: 5.6.4(webpack@5.103.0) leven: 3.1.0 lodash: 4.17.21 open: 8.4.2 @@ -13579,7 +13574,7 @@ snapshots: react-dom: 18.3.1(react@18.3.1) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' - react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.102.1) + react-loadable-ssr-addon-v5-slorber: 1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.103.0) react-router: 5.3.4(react@18.3.1) react-router-config: 5.1.1(react-router@5.3.4(react@18.3.1))(react@18.3.1) react-router-dom: 5.3.4(react@18.3.1) @@ -13588,9 +13583,9 @@ snapshots: tinypool: 1.1.1 tslib: 2.8.1 update-notifier: 6.0.2 - webpack: 5.102.1 + webpack: 5.103.0 webpack-bundle-analyzer: 4.10.2 - webpack-dev-server: 5.2.2(webpack@5.102.1) + webpack-dev-server: 5.2.2(webpack@5.103.0) webpack-merge: 6.0.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -13630,7 +13625,7 @@ snapshots: '@slorber/remark-comment': 1.0.0 escape-html: 1.0.3 estree-util-value-to-estree: 3.5.0 - file-loader: 6.2.0(webpack@5.102.1) + file-loader: 6.2.0(webpack@5.103.0) fs-extra: 11.3.2 image-size: 2.0.2 mdast-util-mdx: 3.0.0 @@ -13646,9 +13641,9 @@ snapshots: tslib: 2.8.1 unified: 11.0.5 unist-util-visit: 5.0.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.102.1))(webpack@5.102.1) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.103.0))(webpack@5.103.0) vfile: 6.0.3 - webpack: 5.102.1 + webpack: 5.103.0 transitivePeerDependencies: - '@swc/core' - esbuild @@ -13660,7 +13655,7 @@ snapshots: dependencies: '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/history': 4.7.11 - '@types/react': 19.2.6 + '@types/react': 19.2.7 '@types/react-router-config': 5.0.11 '@types/react-router-dom': 5.3.3 react: 18.3.1 @@ -13674,13 +13669,13 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/plugin-content-blog@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-content-blog@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13696,7 +13691,7 @@ snapshots: tslib: 2.8.1 unist-util-visit: 5.0.0 utility-types: 3.11.0 - webpack: 5.102.1 + webpack: 5.103.0 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13715,13 +13710,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13736,7 +13731,7 @@ snapshots: schema-dts: 1.1.5 tslib: 2.8.1 utility-types: 3.11.0 - webpack: 5.102.1 + webpack: 5.103.0 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13755,9 +13750,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-pages@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-content-pages@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13766,7 +13761,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 - webpack: 5.102.1 + webpack: 5.103.0 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13785,9 +13780,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-css-cascade-layers@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-css-cascade-layers@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13812,9 +13807,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-debug@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-debug@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) fs-extra: 11.3.2 @@ -13840,9 +13835,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-analytics@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-google-analytics@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -13866,9 +13861,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-gtag@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-google-gtag@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/gtag.js': 0.0.12 @@ -13893,9 +13888,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-tag-manager@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-google-tag-manager@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 @@ -13919,9 +13914,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-sitemap@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-sitemap@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13950,9 +13945,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-svgr@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/plugin-svgr@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -13961,7 +13956,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) tslib: 2.8.1 - webpack: 5.102.1 + webpack: 5.103.0 transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -13980,22 +13975,22 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/preset-classic@3.9.2(@algolia/client-search@5.41.0)(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(@types/react@19.2.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': + '@docusaurus/preset-classic@3.9.2(@algolia/client-search@5.41.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-google-analytics': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-google-gtag': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-google-tag-manager': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-sitemap': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-svgr': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-classic': 3.9.2(@types/react@19.2.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/theme-search-algolia': 3.9.2(@algolia/client-search@5.41.0)(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(@types/react@19.2.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-css-cascade-layers': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-debug': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-google-analytics': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-google-gtag': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-google-tag-manager': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-sitemap': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-svgr': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-classic': 3.9.2(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/theme-search-algolia': 3.9.2(@algolia/client-search@5.41.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3) '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -14022,25 +14017,25 @@ snapshots: '@docusaurus/react-loadable@6.0.0(react@18.3.1)': dependencies: - '@types/react': 19.2.6 + '@types/react': 19.2.7 react: 18.3.1 - '@docusaurus/theme-classic@3.9.2(@types/react@19.2.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': + '@docusaurus/theme-classic@3.9.2(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3)': dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-blog': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-pages': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.9.2 '@docusaurus/types': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@mdx-js/react': 3.1.1(@types/react@19.2.6)(react@18.3.1) + '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@18.3.1) clsx: 2.1.1 infima: 0.2.0-alpha.45 lodash: 4.17.21 @@ -14072,15 +14067,15 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@docusaurus/theme-common@3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@docusaurus/mdx-loader': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/module-type-aliases': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/history': 4.7.11 - '@types/react': 19.2.6 + '@types/react': 19.2.7 '@types/react-router-config': 5.0.11 clsx: 2.1.1 parse-numeric-range: 1.3.0 @@ -14096,13 +14091,13 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.41.0)(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(@types/react@19.2.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': + '@docusaurus/theme-search-algolia@3.9.2(@algolia/client-search@5.41.0)(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(typescript@5.9.3)': dependencies: - '@docsearch/react': 4.2.0(@algolia/client-search@5.41.0)(@types/react@19.2.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docsearch/react': 4.2.0(@algolia/client-search@5.41.0)(@types/react@19.2.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) '@docusaurus/logger': 3.9.2 - '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) - '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@docusaurus/plugin-content-docs': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/theme-common': 3.9.2(@docusaurus/plugin-content-docs@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/theme-translations': 3.9.2 '@docusaurus/utils': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@docusaurus/utils-validation': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -14149,14 +14144,14 @@ snapshots: '@mdx-js/mdx': 3.1.1 '@types/history': 4.7.11 '@types/mdast': 4.0.4 - '@types/react': 19.2.6 + '@types/react': 19.2.7 commander: 5.1.0 joi: 17.13.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)' utility-types: 3.11.0 - webpack: 5.102.1 + webpack: 5.103.0 webpack-merge: 5.10.0 transitivePeerDependencies: - '@swc/core' @@ -14204,7 +14199,7 @@ snapshots: '@docusaurus/utils-common': 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) escape-string-regexp: 4.0.0 execa: 5.1.1 - file-loader: 6.2.0(webpack@5.102.1) + file-loader: 6.2.0(webpack@5.103.0) fs-extra: 11.3.2 github-slugger: 1.5.0 globby: 11.1.0 @@ -14217,9 +14212,9 @@ snapshots: prompts: 2.4.2 resolve-pathname: 3.0.0 tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.102.1))(webpack@5.102.1) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.103.0))(webpack@5.103.0) utility-types: 3.11.0 - webpack: 5.102.1 + webpack: 5.103.0 transitivePeerDependencies: - '@swc/core' - esbuild @@ -14516,7 +14511,7 @@ snapshots: '@fig/complete-commander@3.2.0(commander@11.1.0)': dependencies: commander: 11.1.0 - prettier: 3.6.2 + prettier: 3.7.1 '@floating-ui/core@1.7.3': dependencies: @@ -14555,13 +14550,13 @@ snapshots: dependencies: tslib: 2.8.1 - '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': + '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) lodash: 4.17.21 - '@grpc/grpc-js@1.14.1': + '@grpc/grpc-js@1.14.2': dependencies: '@grpc/proto-loader': 0.8.0 '@js-sdsl/ordered-map': 4.4.2 @@ -14699,19 +14694,21 @@ snapshots: dependencies: svelte: 5.45.2 - '@immich/ui@0.49.2(svelte@5.45.2)': + '@immich/ui@0.49.3(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)': dependencies: '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.45.2) '@internationalized/date': 3.10.0 '@mdi/js': 7.4.47 - bits-ui: 2.9.8(@internationalized/date@3.10.0)(svelte@5.45.2) + bits-ui: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2) luxon: 3.7.2 - simple-icons: 15.21.0 + simple-icons: 15.22.0 svelte: 5.45.2 - svelte-highlight: 7.8.4 - tailwind-merge: 3.3.1 - tailwind-variants: 3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.17) + svelte-highlight: 7.9.0 + tailwind-merge: 3.4.0 + tailwind-variants: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.17) tailwindcss: 4.1.17 + transitivePeerDependencies: + - '@sveltejs/kit' '@inquirer/ansi@1.0.2': {} @@ -14971,8 +14968,8 @@ snapshots: '@koddsson/eslint-plugin-tscompat@0.2.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@mdn/browser-compat-data': 6.0.27 - '@typescript-eslint/type-utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) browserslist: 4.28.0 transitivePeerDependencies: - eslint @@ -15119,10 +15116,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1)': + '@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1)': dependencies: '@types/mdx': 2.0.13 - '@types/react': 19.2.6 + '@types/react': 19.2.7 react: 18.3.1 '@microsoft/tsdoc@0.16.0': {} @@ -15147,21 +15144,21 @@ snapshots: '@namnode/store@0.1.0': {} - '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': + '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(bullmq@5.64.0)': + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(bullmq@5.65.0)': dependencies: - '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) - bullmq: 5.64.0 + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + bullmq: 5.65.0 tslib: 2.8.1 - '@nestjs/cli@11.0.12(@swc/core@1.15.2(@swc/helpers@0.5.17))(@types/node@24.10.1)': + '@nestjs/cli@11.0.13(@swc/core@1.15.3(@swc/helpers@0.5.17))(@types/node@24.10.1)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) @@ -15172,24 +15169,24 @@ snapshots: chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.100.2(@swc/core@1.15.2(@swc/helpers@0.5.17))) - glob: 12.0.0 + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.103.0(@swc/core@1.15.3(@swc/helpers@0.5.17))) + glob: 13.0.0 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.100.2(@swc/core@1.15.2(@swc/helpers@0.5.17)) + webpack: 5.103.0(@swc/core@1.15.3(@swc/helpers@0.5.17)) webpack-node-externals: 3.0.0 optionalDependencies: - '@swc/core': 1.15.2(@swc/helpers@0.5.17) + '@swc/core': 1.15.3(@swc/helpers@0.5.17) transitivePeerDependencies: - '@types/node' - esbuild - uglify-js - webpack-cli - '@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: file-type: 21.1.0 iterare: 1.2.1 @@ -15200,13 +15197,13 @@ snapshots: uid: 2.0.2 optionalDependencies: class-transformer: 0.5.1 - class-validator: 0.14.2 + class-validator: 0.14.3 transitivePeerDependencies: - supports-color - '@nestjs/core@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -15216,21 +15213,21 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) - '@nestjs/websockets': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + '@nestjs/websockets': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 optionalDependencies: class-transformer: 0.5.1 - class-validator: 0.14.2 + class-validator: 0.14.3 - '@nestjs/platform-express@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': + '@nestjs/platform-express@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.5 express: 5.1.0 multer: 2.0.2 @@ -15239,10 +15236,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/platform-socket.io@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.9)(rxjs@7.8.2)': + '@nestjs/platform-socket.io@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.9)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/websockets': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/websockets': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) rxjs: 7.8.2 socket.io: 4.8.1 tslib: 2.8.1 @@ -15251,10 +15248,10 @@ snapshots: - supports-color - utf-8-validate - '@nestjs/schedule@6.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': + '@nestjs/schedule@6.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) cron: 4.3.3 '@nestjs/schematics@11.0.9(chokidar@4.0.3)(typescript@5.9.3)': @@ -15268,12 +15265,12 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.2.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.16.0 - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) js-yaml: 4.1.1 lodash: 4.17.21 path-to-regexp: 8.3.0 @@ -15281,27 +15278,27 @@ snapshots: swagger-ui-dist: 5.30.2 optionalDependencies: class-transformer: 0.5.1 - class-validator: 0.14.2 + class-validator: 0.14.3 - '@nestjs/testing@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)': + '@nestjs/testing@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + '@nestjs/platform-express': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) - '@nestjs/websockets@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/websockets@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-socket.io@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) iterare: 1.2.1 object-hash: 3.0.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-socket.io': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.9)(rxjs@7.8.2) + '@nestjs/platform-socket.io': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.9)(rxjs@7.8.2) '@noble/hashes@1.8.0': {} @@ -15322,7 +15319,7 @@ snapshots: agent-base: 7.1.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 - lru-cache: 11.2.2 + lru-cache: 11.2.4 socks-proxy-agent: 8.0.5 transitivePeerDependencies: - supports-color @@ -15337,7 +15334,7 @@ snapshots: '@oazapfts/runtime@1.0.4': {} - '@opentelemetry/api-logs@0.207.0': + '@opentelemetry/api-logs@0.208.0': dependencies: '@opentelemetry/api': 1.9.0 @@ -15352,100 +15349,100 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.14.1 + '@grpc/grpc-js': 1.14.2 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.207.0 + '@opentelemetry/api-logs': 0.208.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-logs-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.207.0 + '@opentelemetry/api-logs': 0.208.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.14.1 + '@grpc/grpc-js': 1.14.2 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-metrics-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-prometheus@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.14.1 + '@grpc/grpc-js': 1.14.2 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) @@ -15462,74 +15459,74 @@ snapshots: '@opentelemetry/api': 1.9.0 systeminformation: 5.23.8 - '@opentelemetry/instrumentation-http@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-http@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 forwarded-parse: 2.1.2 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-ioredis@0.55.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-ioredis@0.56.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.38.2 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-nestjs-core@0.54.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-nestjs-core@0.55.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-pg@0.60.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation-pg@0.61.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) - '@types/pg': 8.15.5 + '@types/pg': 8.15.6 '@types/pg-pool': 2.0.6 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.207.0 + '@opentelemetry/api-logs': 0.208.0 import-in-the-middle: 2.0.0 require-in-the-middle: 8.0.0 transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-grpc-exporter-base@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-grpc-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': dependencies: - '@grpc/grpc-js': 1.14.1 + '@grpc/grpc-js': 1.14.2 '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-exporter-base': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/otlp-transformer@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.207.0 + '@opentelemetry/api-logs': 0.208.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) protobufjs: 7.5.4 @@ -15552,10 +15549,10 @@ snapshots: '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 - '@opentelemetry/sdk-logs@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.207.0 + '@opentelemetry/api-logs': 0.208.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) @@ -15565,27 +15562,27 @@ snapshots: '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node@0.207.0(@opentelemetry/api@1.9.0)': + '@opentelemetry/sdk-node@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.207.0 + '@opentelemetry/api-logs': 0.208.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-grpc': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-http': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-logs-otlp-proto': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-http': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-metrics-otlp-proto': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-prometheus': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-grpc': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-http': 0.207.0(@opentelemetry/api@1.9.0) - '@opentelemetry/exporter-trace-otlp-proto': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/exporter-zipkin': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/propagator-b3': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/propagator-jaeger': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-logs': 0.207.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-node': 2.2.0(@opentelemetry/api@1.9.0) @@ -15653,9 +15650,9 @@ snapshots: '@pkgr/core@0.2.9': {} - '@playwright/test@1.56.1': + '@playwright/test@1.57.0': dependencies: - playwright: 1.56.1 + playwright: 1.57.0 '@pnpm/config.env-replace@1.1.0': {} @@ -15785,7 +15782,7 @@ snapshots: '@react-email/render@1.4.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: html-to-text: 9.0.5 - prettier: 3.6.2 + prettier: 3.7.1 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) react-promise-suspense: 0.3.4 @@ -16204,33 +16201,33 @@ snapshots: '@standard-schema/spec@1.0.0': {} - '@sveltejs/acorn-typescript@1.0.7(acorn@8.15.0)': + '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.48.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))': dependencies: - '@sveltejs/kit': 2.48.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + '@sveltejs/kit': 2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)) - '@sveltejs/enhanced-img@0.8.5(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)))(rollup@4.53.3)(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))': + '@sveltejs/enhanced-img@0.9.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(rollup@4.53.3)(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)) magic-string: 0.30.21 sharp: 0.34.5 svelte: 5.45.2 svelte-parse-markup: 0.1.5(svelte@5.45.2) - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) - vite-imagetools: 8.0.0(rollup@4.53.3) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) + vite-imagetools: 9.0.2(rollup@4.53.3) zimmerframe: 1.1.4 transitivePeerDependencies: - rollup - supports-color - '@sveltejs/kit@2.48.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))': + '@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2))': dependencies: '@standard-schema/spec': 1.0.0 - '@sveltejs/acorn-typescript': 1.0.7(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -16243,28 +16240,28 @@ snapshots: set-cookie-parser: 2.7.2 sirv: 3.0.2 svelte: 5.45.2 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) optionalDependencies: '@opentelemetry/api': 1.9.0 - '@sveltejs/vite-plugin-svelte-inspector@5.0.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.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.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)) debug: 4.4.3 svelte: 5.45.2 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.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.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)) debug: 4.4.3 deepmerge: 4.3.1 magic-string: 0.30.21 svelte: 5.45.2 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) - vitefu: 1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) + vitefu: 1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color @@ -16361,51 +16358,51 @@ snapshots: - supports-color - typescript - '@swc/core-darwin-arm64@1.15.2': + '@swc/core-darwin-arm64@1.15.3': optional: true - '@swc/core-darwin-x64@1.15.2': + '@swc/core-darwin-x64@1.15.3': optional: true - '@swc/core-linux-arm-gnueabihf@1.15.2': + '@swc/core-linux-arm-gnueabihf@1.15.3': optional: true - '@swc/core-linux-arm64-gnu@1.15.2': + '@swc/core-linux-arm64-gnu@1.15.3': optional: true - '@swc/core-linux-arm64-musl@1.15.2': + '@swc/core-linux-arm64-musl@1.15.3': optional: true - '@swc/core-linux-x64-gnu@1.15.2': + '@swc/core-linux-x64-gnu@1.15.3': optional: true - '@swc/core-linux-x64-musl@1.15.2': + '@swc/core-linux-x64-musl@1.15.3': optional: true - '@swc/core-win32-arm64-msvc@1.15.2': + '@swc/core-win32-arm64-msvc@1.15.3': optional: true - '@swc/core-win32-ia32-msvc@1.15.2': + '@swc/core-win32-ia32-msvc@1.15.3': optional: true - '@swc/core-win32-x64-msvc@1.15.2': + '@swc/core-win32-x64-msvc@1.15.3': optional: true - '@swc/core@1.15.2(@swc/helpers@0.5.17)': + '@swc/core@1.15.3(@swc/helpers@0.5.17)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.25 optionalDependencies: - '@swc/core-darwin-arm64': 1.15.2 - '@swc/core-darwin-x64': 1.15.2 - '@swc/core-linux-arm-gnueabihf': 1.15.2 - '@swc/core-linux-arm64-gnu': 1.15.2 - '@swc/core-linux-arm64-musl': 1.15.2 - '@swc/core-linux-x64-gnu': 1.15.2 - '@swc/core-linux-x64-musl': 1.15.2 - '@swc/core-win32-arm64-msvc': 1.15.2 - '@swc/core-win32-ia32-msvc': 1.15.2 - '@swc/core-win32-x64-msvc': 1.15.2 + '@swc/core-darwin-arm64': 1.15.3 + '@swc/core-darwin-x64': 1.15.3 + '@swc/core-linux-arm-gnueabihf': 1.15.3 + '@swc/core-linux-arm64-gnu': 1.15.3 + '@swc/core-linux-arm64-musl': 1.15.3 + '@swc/core-linux-x64-gnu': 1.15.3 + '@swc/core-linux-x64-musl': 1.15.3 + '@swc/core-win32-arm64-msvc': 1.15.3 + '@swc/core-win32-ia32-msvc': 1.15.3 + '@swc/core-win32-x64-msvc': 1.15.3 '@swc/helpers': 0.5.17 '@swc/counter@0.1.3': {} @@ -16483,12 +16480,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 - '@tailwindcss/vite@4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))': + '@tailwindcss/vite@4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2))': dependencies: '@tailwindcss/node': 4.1.17 '@tailwindcss/oxide': 4.1.17 tailwindcss: 4.1.17 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) '@testing-library/dom@10.4.1': dependencies: @@ -16510,13 +16507,13 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/svelte@5.2.9(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.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)(terser@5.44.0)(yaml@2.8.1))': + '@testing-library/svelte@5.2.9(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2))(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)(terser@5.44.0)(yaml@2.8.2))': dependencies: '@testing-library/dom': 10.4.1 svelte: 5.45.2 optionalDependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.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)(terser@5.44.0)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) + 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)(terser@5.44.0)(yaml@2.8.2) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: @@ -16790,9 +16787,9 @@ snapshots: '@types/lodash-es@4.17.12': dependencies: - '@types/lodash': 4.17.20 + '@types/lodash': 4.17.21 - '@types/lodash@4.17.20': {} + '@types/lodash@4.17.21': {} '@types/luxon@3.7.1': {} @@ -16857,12 +16854,6 @@ snapshots: dependencies: '@types/pg': 8.15.6 - '@types/pg@8.15.5': - dependencies: - '@types/node': 24.10.1 - pg-protocol: 1.10.3 - pg-types: 2.2.0 - '@types/pg@8.15.6': dependencies: '@types/node': 24.10.1 @@ -16888,21 +16879,21 @@ snapshots: '@types/react-router-config@5.0.11': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.6 + '@types/react': 19.2.7 '@types/react-router': 5.1.20 '@types/react-router-dom@5.3.3': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.6 + '@types/react': 19.2.7 '@types/react-router': 5.1.20 '@types/react-router@5.1.20': dependencies: '@types/history': 4.7.11 - '@types/react': 19.2.6 + '@types/react': 19.2.7 - '@types/react@19.2.6': + '@types/react@19.2.7': dependencies: csstype: 3.2.3 @@ -16998,14 +16989,14 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.47.0 - '@typescript-eslint/type-utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.47.0 + '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/type-utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.48.0 eslint: 9.39.1(jiti@2.6.1) graphemer: 1.4.0 ignore: 7.0.5 @@ -17015,41 +17006,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.47.0 - '@typescript-eslint/types': 8.47.0 - '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.47.0 + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.48.0 debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.47.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.48.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.9.3) - '@typescript-eslint/types': 8.47.0 + '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3) + '@typescript-eslint/types': 8.48.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.47.0': + '@typescript-eslint/scope-manager@8.48.0': dependencies: - '@typescript-eslint/types': 8.47.0 - '@typescript-eslint/visitor-keys': 8.47.0 + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/visitor-keys': 8.48.0 - '@typescript-eslint/tsconfig-utils@8.47.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.48.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.47.0 - '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) @@ -17057,45 +17048,44 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.47.0': {} + '@typescript-eslint/types@8.48.0': {} - '@typescript-eslint/typescript-estree@8.47.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.48.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.47.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.9.3) - '@typescript-eslint/types': 8.47.0 - '@typescript-eslint/visitor-keys': 8.47.0 + '@typescript-eslint/project-service': 8.48.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3) + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/visitor-keys': 8.48.0 debug: 4.4.3 - fast-glob: 3.3.3 - is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.3 + tinyglobby: 0.2.15 ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.47.0 - '@typescript-eslint/types': 8.47.0 - '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.47.0': + '@typescript-eslint/visitor-keys@8.48.0': dependencies: - '@typescript-eslint/types': 8.47.0 + '@typescript-eslint/types': 8.48.0 eslint-visitor-keys: 4.2.1 '@ungap/structured-clone@1.3.0': {} '@vercel/oidc@3.0.3': {} - '@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)(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)(terser@5.44.0)(yaml@2.8.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -17110,7 +17100,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@24.10.1)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.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)(terser@5.44.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -17122,13 +17112,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) '@vitest/pretty-format@3.2.4': dependencies: @@ -17236,13 +17226,13 @@ snapshots: '@xtuc/long@4.2.2': {} - '@zoom-image/core@0.41.3': + '@zoom-image/core@0.41.4': dependencies: '@namnode/store': 0.1.0 - '@zoom-image/svelte@0.3.7(svelte@5.45.2)': + '@zoom-image/svelte@0.3.8(svelte@5.45.2)': dependencies: - '@zoom-image/core': 0.41.3 + '@zoom-image/core': 0.41.4 svelte: 5.45.2 abab@2.0.6: @@ -17502,12 +17492,12 @@ snapshots: b4a@1.6.7: {} - babel-loader@9.2.1(@babel/core@7.28.5)(webpack@5.102.1): + babel-loader@9.2.1(@babel/core@7.28.5)(webpack@5.103.0): dependencies: '@babel/core': 7.28.5 find-cache-dir: 4.0.0 schema-utils: 4.3.3 - webpack: 5.102.1 + webpack: 5.103.0 babel-plugin-dynamic-import-node@2.3.3: dependencies: @@ -17545,7 +17535,7 @@ snapshots: bare-events@2.8.2: {} - bare-fs@4.5.1: + bare-fs@4.5.2: dependencies: bare-events: 2.8.2 bare-path: 3.0.0 @@ -17606,16 +17596,18 @@ snapshots: binary-extensions@2.3.0: {} - bits-ui@2.9.8(@internationalized/date@3.10.0)(svelte@5.45.2): + bits-ui@2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2): dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/dom': 1.7.4 '@internationalized/date': 3.10.0 esm-env: 1.2.2 - runed: 0.29.2(svelte@5.45.2) + runed: 0.35.1(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2) svelte: 5.45.2 - svelte-toolbelt: 0.9.3(svelte@5.45.2) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2) tabbable: 6.3.0 + transitivePeerDependencies: + - '@sveltejs/kit' bl@4.1.0: dependencies: @@ -17623,18 +17615,18 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - body-parser@1.20.3: + body-parser@1.20.4: dependencies: bytes: 3.1.2 content-type: 1.0.5 debug: 2.6.9 depd: 2.0.0 destroy: 1.2.0 - http-errors: 2.0.0 + http-errors: 2.0.1 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.13.0 - raw-body: 2.5.2 + qs: 6.14.0 + raw-body: 2.5.3 type-is: 1.6.18 unpipe: 1.0.0 transitivePeerDependencies: @@ -17722,12 +17714,12 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - buildcheck@0.0.6: + buildcheck@0.0.7: optional: true builtin-modules@5.0.0: {} - bullmq@5.64.0: + bullmq@5.65.0: dependencies: cron-parser: 4.9.0 ioredis: 5.8.2 @@ -17762,7 +17754,7 @@ snapshots: '@npmcli/fs': 5.0.0 fs-minipass: 3.0.3 glob: 13.0.0 - lru-cache: 11.2.2 + lru-cache: 11.2.4 minipass: 7.1.2 minipass-collect: 2.0.1 minipass-flush: 1.0.5 @@ -17932,10 +17924,10 @@ snapshots: class-transformer@0.5.1: {} - class-validator@0.14.2: + class-validator@0.14.3: dependencies: '@types/validator': 13.15.10 - libphonenumber-js: 1.12.9 + libphonenumber-js: 1.12.31 validator: 13.15.23 clean-css@5.3.3: @@ -18118,9 +18110,7 @@ snapshots: dependencies: safe-buffer: 5.2.1 - content-disposition@1.0.0: - dependencies: - safe-buffer: 5.2.1 + content-disposition@1.0.1: {} content-type@1.0.5: {} @@ -18133,15 +18123,15 @@ snapshots: cookie-signature@1.0.6: {} + cookie-signature@1.0.7: {} + cookie-signature@1.2.2: {} cookie@0.6.0: {} - cookie@0.7.1: {} - cookie@0.7.2: {} - cookie@1.0.2: {} + cookie@1.1.1: {} cookiejar@2.1.4: {} @@ -18150,7 +18140,7 @@ snapshots: depd: 2.0.0 keygrip: 1.1.0 - copy-webpack-plugin@11.0.0(webpack@5.102.1): + copy-webpack-plugin@11.0.0(webpack@5.103.0): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -18158,7 +18148,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.102.1 + webpack: 5.103.0 core-js-compat@3.46.0: dependencies: @@ -18186,7 +18176,7 @@ snapshots: cpu-features@0.0.10: dependencies: - buildcheck: 0.0.6 + buildcheck: 0.0.7 nan: 2.23.1 optional: true @@ -18232,7 +18222,7 @@ snapshots: postcss-selector-parser: 7.1.0 postcss-value-parser: 4.2.0 - css-loader@6.11.0(webpack@5.102.1): + css-loader@6.11.0(webpack@5.103.0): dependencies: icss-utils: 5.1.0(postcss@8.5.6) postcss: 8.5.6 @@ -18243,9 +18233,9 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.3 optionalDependencies: - webpack: 5.102.1 + webpack: 5.103.0 - css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.102.1): + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.103.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 cssnano: 6.1.2(postcss@8.5.6) @@ -18253,7 +18243,7 @@ snapshots: postcss: 8.5.6 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.102.1 + webpack: 5.103.0 optionalDependencies: clean-css: 5.3.3 @@ -18535,7 +18525,7 @@ snapshots: docker-compose@1.3.0: dependencies: - yaml: 2.8.1 + yaml: 2.8.2 docker-modem@5.0.6: dependencies: @@ -18549,7 +18539,7 @@ snapshots: dockerode@4.0.9: dependencies: '@balena/dockerignore': 1.0.2 - '@grpc/grpc-js': 1.14.1 + '@grpc/grpc-js': 1.14.2 '@grpc/proto-loader': 0.7.15 docker-modem: 5.0.6 protobufjs: 7.5.4 @@ -18558,9 +18548,9 @@ snapshots: transitivePeerDependencies: - supports-color - docusaurus-lunr-search@3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + docusaurus-lunr-search@3.6.0(@docusaurus/core@3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.6)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) + '@docusaurus/core': 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.7)(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) autocomplete.js: 0.37.1 clsx: 2.1.1 gauge: 3.0.2 @@ -18911,10 +18901,10 @@ snapshots: lodash.memoize: 4.1.2 semver: 7.7.3 - eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2): + eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.1): dependencies: eslint: 9.39.1(jiti@2.6.1) - prettier: 3.6.2 + prettier: 3.7.1 prettier-linter-helpers: 1.0.0 synckit: 0.11.11 optionalDependencies: @@ -19152,36 +19142,36 @@ snapshots: exponential-backoff@3.1.3: {} - express@4.21.2: + express@4.22.1: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.3 + body-parser: 1.20.4 content-disposition: 0.5.4 content-type: 1.0.5 - cookie: 0.7.1 - cookie-signature: 1.0.6 + cookie: 0.7.2 + cookie-signature: 1.0.7 debug: 2.6.9 depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.3.1 + finalhandler: 1.3.2 fresh: 0.5.2 - http-errors: 2.0.0 + http-errors: 2.0.1 merge-descriptors: 1.0.3 methods: 1.1.2 on-finished: 2.4.1 parseurl: 1.3.3 path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.13.0 + qs: 6.14.0 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.19.0 + send: 0.19.1 serve-static: 1.16.2 setprototypeof: 1.2.0 - statuses: 2.0.1 + statuses: 2.0.2 type-is: 1.6.18 utils-merge: 1.0.1 vary: 1.1.2 @@ -19192,7 +19182,7 @@ snapshots: dependencies: accepts: 2.0.0 body-parser: 2.2.1 - content-disposition: 1.0.0 + content-disposition: 1.0.1 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 @@ -19200,7 +19190,40 @@ snapshots: encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.0 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + express@5.2.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.1 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 fresh: 2.0.0 http-errors: 2.0.1 merge-descriptors: 2.0.0 @@ -19305,11 +19328,11 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-loader@6.2.0(webpack@5.102.1): + file-loader@6.2.0(webpack@5.103.0): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.102.1 + webpack: 5.103.0 file-source@0.6.1: dependencies: @@ -19328,19 +19351,19 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@1.3.1: + finalhandler@1.3.2: dependencies: debug: 2.6.9 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.1 + statuses: 2.0.2 unpipe: 1.0.0 transitivePeerDependencies: - supports-color - finalhandler@2.1.0: + finalhandler@2.1.1: dependencies: debug: 4.4.3 encodeurl: 2.0.0 @@ -19394,7 +19417,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(@swc/core@1.15.2(@swc/helpers@0.5.17))): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.103.0(@swc/core@1.15.3(@swc/helpers@0.5.17))): dependencies: '@babel/code-frame': 7.27.1 chalk: 4.1.2 @@ -19409,7 +19432,7 @@ snapshots: semver: 7.7.3 tapable: 2.3.0 typescript: 5.9.3 - webpack: 5.100.2(@swc/core@1.15.2(@swc/helpers@0.5.17)) + webpack: 5.103.0(@swc/core@1.15.3(@swc/helpers@0.5.17)) form-data-encoder@2.1.4: {} @@ -19572,15 +19595,6 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 2.0.1 - glob@12.0.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 4.1.1 - minimatch: 10.1.1 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 2.0.1 - glob@13.0.0: dependencies: minimatch: 10.1.1 @@ -19916,7 +19930,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.4(webpack@5.102.1): + html-webpack-plugin@5.6.4(webpack@5.103.0): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -19924,7 +19938,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.3.0 optionalDependencies: - webpack: 5.102.1 + webpack: 5.103.0 htmlparser2@6.1.0: dependencies: @@ -20070,7 +20084,7 @@ snapshots: image-size@2.0.2: {} - imagetools-core@8.0.0: {} + imagetools-core@9.1.0: {} immediate@3.3.0: {} @@ -20111,6 +20125,8 @@ snapshots: inline-style-parser@0.2.4: {} + inline-style-parser@0.2.7: {} + inquirer@8.2.7(@types/node@24.10.1): dependencies: '@inquirer/external-editor': 1.0.3(@types/node@24.10.1) @@ -20600,7 +20616,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libphonenumber-js@1.12.9: {} + libphonenumber-js@1.12.31: {} lightningcss-android-arm64@1.30.2: optional: true @@ -20748,7 +20764,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.2.2: {} + lru-cache@11.2.4: {} lru-cache@5.1.1: dependencies: @@ -21436,11 +21452,11 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.9.4(webpack@5.102.1): + mini-css-extract-plugin@2.9.4(webpack@5.103.0): dependencies: schema-utils: 4.3.3 tapable: 2.3.0 - webpack: 5.102.1 + webpack: 5.103.0 minimalistic-assert@1.0.1: {} @@ -21598,12 +21614,12 @@ snapshots: neo-async@2.6.2: {} - nest-commander@3.20.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@types/inquirer@8.2.11)(@types/node@24.10.1)(typescript@5.9.3): + nest-commander@3.20.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@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.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@types/inquirer': 8.2.11 commander: 11.1.0 cosmiconfig: 8.3.6(typescript@5.9.3) @@ -21612,25 +21628,25 @@ snapshots: - '@types/node' - typescript - nestjs-cls@5.4.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2): + nestjs-cls@5.4.3(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - nestjs-kysely@3.1.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(kysely@0.28.2)(reflect-metadata@0.2.2): + nestjs-kysely@3.1.2(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(kysely@0.28.2)(reflect-metadata@0.2.2): dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) kysely: 0.28.2 reflect-metadata: 0.2.2 tslib: 2.8.1 - nestjs-otel@7.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9): + nestjs-otel@7.0.1(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9): dependencies: - '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(@nestjs/websockets@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@opentelemetry/api': 1.9.0 '@opentelemetry/host-metrics': 0.36.0(@opentelemetry/api@1.9.0) response-time: 2.3.4 @@ -21697,7 +21713,7 @@ snapshots: node-releases@2.0.27: {} - nodemailer@7.0.10: {} + nodemailer@7.0.11: {} nopt@1.0.10: dependencies: @@ -21738,11 +21754,11 @@ snapshots: dependencies: boolbase: 1.0.0 - null-loader@4.0.1(webpack@5.102.1): + null-loader@4.0.1(webpack@5.103.0): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.102.1 + webpack: 5.103.0 nwsapi@2.2.22: optional: true @@ -21996,7 +22012,7 @@ snapshots: path-scurry@2.0.1: dependencies: - lru-cache: 11.2.2 + lru-cache: 11.2.4 minipass: 7.1.2 path-source@0.1.3: @@ -22088,11 +22104,11 @@ snapshots: exsolve: 1.0.7 pathe: 2.0.3 - playwright-core@1.56.1: {} + playwright-core@1.57.0: {} - playwright@1.56.1: + playwright@1.57.0: dependencies: - playwright-core: 1.56.1 + playwright-core: 1.57.0 optionalDependencies: fsevents: 2.3.2 @@ -22276,21 +22292,21 @@ snapshots: optionalDependencies: postcss: 8.5.6 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.1): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 postcss: 8.5.6 - yaml: 2.8.1 + yaml: 2.8.2 - postcss-loader@7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.102.1): + postcss-loader@7.3.4(postcss@8.5.6)(typescript@5.9.3)(webpack@5.103.0): dependencies: cosmiconfig: 8.3.6(typescript@5.9.3) jiti: 1.21.7 postcss: 8.5.6 semver: 7.7.3 - webpack: 5.102.1 + webpack: 5.103.0 transitivePeerDependencies: - typescript @@ -22614,21 +22630,21 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.9.3): + prettier-plugin-organize-imports@4.3.0(prettier@3.7.1)(typescript@5.9.3): dependencies: - prettier: 3.6.2 + prettier: 3.7.1 typescript: 5.9.3 - prettier-plugin-sort-json@4.1.1(prettier@3.6.2): + prettier-plugin-sort-json@4.1.1(prettier@3.7.1): dependencies: - prettier: 3.6.2 + prettier: 3.7.1 - prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.45.2): + prettier-plugin-svelte@3.4.0(prettier@3.7.1)(svelte@5.45.2): dependencies: - prettier: 3.6.2 + prettier: 3.7.1 svelte: 5.45.2 - prettier@3.6.2: {} + prettier@3.7.1: {} pretty-error@4.0.0: dependencies: @@ -22739,10 +22755,6 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 - qs@6.13.0: - dependencies: - side-channel: 1.1.0 - qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -22775,10 +22787,10 @@ snapshots: range-parser@1.2.1: {} - raw-body@2.5.2: + raw-body@2.5.3: dependencies: bytes: 3.1.2 - http-errors: 2.0.0 + http-errors: 2.0.1 iconv-lite: 0.4.24 unpipe: 1.0.0 @@ -22789,11 +22801,11 @@ snapshots: iconv-lite: 0.7.0 unpipe: 1.0.0 - raw-loader@4.0.2(webpack@5.102.1): + raw-loader@4.0.2(webpack@5.103.0): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.102.1 + webpack: 5.103.0 rc@1.2.8: dependencies: @@ -22846,11 +22858,11 @@ snapshots: dependencies: react: 18.3.1 - react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.102.1): + react-loadable-ssr-addon-v5-slorber@1.0.1(@docusaurus/react-loadable@6.0.0(react@18.3.1))(webpack@5.103.0): dependencies: '@babel/runtime': 7.28.4 react-loadable: '@docusaurus/react-loadable@6.0.0(react@18.3.1)' - webpack: 5.102.1 + webpack: 5.103.0 react-promise-suspense@0.3.4: dependencies: @@ -23232,10 +23244,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 - runed@0.29.2(svelte@5.45.2): + runed@0.35.1(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2): dependencies: + dequal: 2.0.3 esm-env: 1.2.2 + lz-string: 1.5.0 svelte: 5.45.2 + optionalDependencies: + '@sveltejs/kit': 2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)) rw@1.3.3: {} @@ -23342,6 +23358,24 @@ snapshots: transitivePeerDependencies: - supports-color + send@0.19.1: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + send@1.2.0: dependencies: debug: 4.4.3 @@ -23521,7 +23555,7 @@ snapshots: simple-concat: 1.0.1 optional: true - simple-icons@15.21.0: {} + simple-icons@15.22.0: {} sirv@2.0.4: dependencies: @@ -23796,6 +23830,10 @@ snapshots: dependencies: inline-style-parser: 0.2.4 + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + stylehacks@6.1.1(postcss@8.5.6): dependencies: browserslist: 4.28.0 @@ -23876,7 +23914,7 @@ snapshots: svelte-gestures@5.2.2: {} - svelte-highlight@7.8.4: + svelte-highlight@7.9.0: dependencies: highlight.js: 11.11.1 @@ -23908,18 +23946,20 @@ snapshots: dependencies: svelte: 5.45.2 - svelte-toolbelt@0.9.3(svelte@5.45.2): + svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2): dependencies: clsx: 2.1.1 - runed: 0.29.2(svelte@5.45.2) - style-to-object: 1.0.11 + runed: 0.35.1(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2) + style-to-object: 1.0.14 svelte: 5.45.2 + transitivePeerDependencies: + - '@sveltejs/kit' svelte@5.45.2: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.7(acorn@8.15.0) + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) '@types/estree': 1.0.8 acorn: 8.15.0 aria-query: 5.3.2 @@ -23968,29 +24008,29 @@ snapshots: tabbable@6.3.0: {} - tailwind-merge@3.3.1: {} + tailwind-merge@3.4.0: {} - tailwind-variants@3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.17): + tailwind-variants@3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.17): dependencies: tailwindcss: 4.1.17 optionalDependencies: - tailwind-merge: 3.3.1 + tailwind-merge: 3.4.0 - tailwindcss-email-variants@3.0.5(tailwindcss@3.4.18(yaml@2.8.1)): + tailwindcss-email-variants@3.0.5(tailwindcss@3.4.18(yaml@2.8.2)): dependencies: - tailwindcss: 3.4.18(yaml@2.8.1) + tailwindcss: 3.4.18(yaml@2.8.2) - tailwindcss-mso@2.0.3(tailwindcss@3.4.18(yaml@2.8.1)): + tailwindcss-mso@2.0.3(tailwindcss@3.4.18(yaml@2.8.2)): dependencies: - tailwindcss: 3.4.18(yaml@2.8.1) + tailwindcss: 3.4.18(yaml@2.8.2) - tailwindcss-preset-email@1.4.1(tailwindcss@3.4.18(yaml@2.8.1)): + tailwindcss-preset-email@1.4.1(tailwindcss@3.4.18(yaml@2.8.2)): dependencies: - tailwindcss: 3.4.18(yaml@2.8.1) - tailwindcss-email-variants: 3.0.5(tailwindcss@3.4.18(yaml@2.8.1)) - tailwindcss-mso: 2.0.3(tailwindcss@3.4.18(yaml@2.8.1)) + tailwindcss: 3.4.18(yaml@2.8.2) + tailwindcss-email-variants: 3.0.5(tailwindcss@3.4.18(yaml@2.8.2)) + tailwindcss-mso: 2.0.3(tailwindcss@3.4.18(yaml@2.8.2)) - tailwindcss@3.4.18(yaml@2.8.1): + tailwindcss@3.4.18(yaml@2.8.2): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -24009,7 +24049,7 @@ snapshots: postcss: 8.5.6 postcss-import: 15.1.0(postcss@8.5.6) postcss-js: 4.1.0(postcss@8.5.6) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.1) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(yaml@2.8.2) postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 resolve: 1.22.11 @@ -24034,7 +24074,7 @@ snapshots: pump: 3.0.3 tar-stream: 3.1.7 optionalDependencies: - bare-fs: 4.5.1 + bare-fs: 4.5.2 bare-path: 3.0.0 transitivePeerDependencies: - bare-abort-controller @@ -24073,25 +24113,25 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - terser-webpack-plugin@5.3.14(@swc/core@1.15.2(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.15.2(@swc/helpers@0.5.17))): + terser-webpack-plugin@5.3.14(@swc/core@1.15.3(@swc/helpers@0.5.17))(webpack@5.103.0(@swc/core@1.15.3(@swc/helpers@0.5.17))): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.44.0 - webpack: 5.100.2(@swc/core@1.15.2(@swc/helpers@0.5.17)) + webpack: 5.103.0(@swc/core@1.15.3(@swc/helpers@0.5.17)) optionalDependencies: - '@swc/core': 1.15.2(@swc/helpers@0.5.17) + '@swc/core': 1.15.3(@swc/helpers@0.5.17) - terser-webpack-plugin@5.3.14(webpack@5.102.1): + terser-webpack-plugin@5.3.14(webpack@5.103.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.44.0 - webpack: 5.102.1 + webpack: 5.103.0 terser@5.44.0: dependencies: @@ -24106,7 +24146,7 @@ snapshots: glob: 10.5.0 minimatch: 9.0.5 - testcontainers@11.8.1: + testcontainers@11.9.0: dependencies: '@balena/dockerignore': 1.0.2 '@types/dockerode': 3.3.47 @@ -24317,12 +24357,12 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -24457,16 +24497,16 @@ snapshots: unpipe@1.0.0: {} - unplugin-swc@1.5.8(@swc/core@1.15.2(@swc/helpers@0.5.17))(rollup@4.53.3): + unplugin-swc@1.5.9(@swc/core@1.15.3(@swc/helpers@0.5.17))(rollup@4.53.3): dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.53.3) - '@swc/core': 1.15.2(@swc/helpers@0.5.17) + '@swc/core': 1.15.3(@swc/helpers@0.5.17) load-tsconfig: 0.2.5 - unplugin: 2.3.10 + unplugin: 2.3.11 transitivePeerDependencies: - rollup - unplugin@2.3.10: + unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.15.0 @@ -24500,14 +24540,14 @@ snapshots: dependencies: punycode: 2.3.1 - url-loader@4.1.1(file-loader@6.2.0(webpack@5.102.1))(webpack@5.102.1): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.103.0))(webpack@5.103.0): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.102.1 + webpack: 5.103.0 optionalDependencies: - file-loader: 6.2.0(webpack@5.102.1) + file-loader: 6.2.0(webpack@5.103.0) url-parse@1.5.10: dependencies: @@ -24585,22 +24625,22 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-imagetools@8.0.0(rollup@4.53.3): + vite-imagetools@9.0.2(rollup@4.53.3): dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.53.3) - imagetools-core: 8.0.0 + imagetools-core: 9.1.0 sharp: 0.34.5 transitivePeerDependencies: - rollup - supports-color - vite-node@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.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)(terser@5.44.0)(yaml@2.8.2): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - jiti @@ -24615,18 +24655,18 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - typescript - vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1): + vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -24640,21 +24680,21 @@ snapshots: jiti: 2.6.1 lightningcss: 1.30.2 terser: 5.44.0 - yaml: 2.8.1 + yaml: 2.8.2 - vitefu@1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)): + vitefu@1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)): optionalDependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) - 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)(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)(terser@5.44.0)(yaml@2.8.2)): dependencies: - 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)(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)(terser@5.44.0)(yaml@2.8.2) - 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)(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)(terser@5.44.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -24672,8 +24712,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.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)(terser@5.44.0)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -24694,11 +24734,11 @@ snapshots: - tsx - yaml - 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)(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)(terser@5.44.0)(yaml@2.8.2): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -24716,8 +24756,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.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)(terser@5.44.0)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) + vite-node: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -24794,7 +24834,7 @@ snapshots: - bufferutil - utf-8-validate - webpack-dev-middleware@7.4.5(webpack@5.102.1): + webpack-dev-middleware@7.4.5(webpack@5.103.0): dependencies: colorette: 2.0.20 memfs: 4.50.0 @@ -24803,9 +24843,9 @@ snapshots: range-parser: 1.2.1 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.102.1 + webpack: 5.103.0 - webpack-dev-server@5.2.2(webpack@5.102.1): + webpack-dev-server@5.2.2(webpack@5.103.0): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -24821,7 +24861,7 @@ snapshots: colorette: 2.0.20 compression: 1.8.1 connect-history-api-fallback: 2.0.0 - express: 4.21.2 + express: 4.22.1 graceful-fs: 4.2.11 http-proxy-middleware: 2.0.9(@types/express@4.17.25) ipaddr.js: 2.2.0 @@ -24833,10 +24873,10 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(webpack@5.102.1) + webpack-dev-middleware: 7.4.5(webpack@5.103.0) ws: 8.18.3 optionalDependencies: - webpack: 5.102.1 + webpack: 5.103.0 transitivePeerDependencies: - bufferutil - debug @@ -24861,7 +24901,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.100.2(@swc/core@1.15.2(@swc/helpers@0.5.17)): + webpack@5.103.0: dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -24885,7 +24925,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(@swc/core@1.15.2(@swc/helpers@0.5.17))(webpack@5.100.2(@swc/core@1.15.2(@swc/helpers@0.5.17))) + terser-webpack-plugin: 5.3.14(webpack@5.103.0) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: @@ -24893,7 +24933,7 @@ snapshots: - esbuild - uglify-js - webpack@5.102.1: + webpack@5.103.0(@swc/core@1.15.3(@swc/helpers@0.5.17)): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -24917,7 +24957,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(webpack@5.102.1) + terser-webpack-plugin: 5.3.14(@swc/core@1.15.3(@swc/helpers@0.5.17))(webpack@5.103.0(@swc/core@1.15.3(@swc/helpers@0.5.17))) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: @@ -24925,7 +24965,7 @@ snapshots: - esbuild - uglify-js - webpackbar@6.0.1(webpack@5.102.1): + webpackbar@6.0.1(webpack@5.103.0): dependencies: ansi-escapes: 4.3.2 chalk: 4.1.2 @@ -24934,7 +24974,7 @@ snapshots: markdown-table: 2.0.0 pretty-time: 1.1.0 std-env: 3.10.0 - webpack: 5.102.1 + webpack: 5.103.0 wrap-ansi: 7.0.0 websocket-driver@0.7.4: @@ -25078,7 +25118,7 @@ snapshots: yaml@1.10.2: {} - yaml@2.8.1: {} + yaml@2.8.2: {} yargs-parser@18.1.3: dependencies: diff --git a/server/package.json b/server/package.json index 771fe36dbc..915e45c116 100644 --- a/server/package.json +++ b/server/package.json @@ -45,14 +45,14 @@ "@nestjs/websockets": "^11.0.4", "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.0.0", - "@opentelemetry/exporter-prometheus": "^0.207.0", - "@opentelemetry/instrumentation-http": "^0.207.0", - "@opentelemetry/instrumentation-ioredis": "^0.55.0", - "@opentelemetry/instrumentation-nestjs-core": "^0.54.0", - "@opentelemetry/instrumentation-pg": "^0.60.0", + "@opentelemetry/exporter-prometheus": "^0.208.0", + "@opentelemetry/instrumentation-http": "^0.208.0", + "@opentelemetry/instrumentation-ioredis": "^0.56.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.55.0", + "@opentelemetry/instrumentation-pg": "^0.61.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", - "@opentelemetry/sdk-node": "^0.207.0", + "@opentelemetry/sdk-node": "^0.208.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@react-email/components": "^0.5.0", "@react-email/render": "^1.1.2", diff --git a/server/src/controllers/asset-media.controller.spec.ts b/server/src/controllers/asset-media.controller.spec.ts index eb594fbe47..c2f6aeacef 100644 --- a/server/src/controllers/asset-media.controller.spec.ts +++ b/server/src/controllers/asset-media.controller.spec.ts @@ -85,19 +85,6 @@ describe(AssetMediaController.name, () => { expect(body).toEqual(factory.responses.badRequest(['metadata must be valid JSON'])); }); - it('should validate iCloudId is a string', async () => { - const { status, body } = await request(ctx.getHttpServer()) - .post('/assets') - .attach('assetData', assetData, filename) - .field({ - ...makeUploadDto(), - metadata: JSON.stringify([{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 123 } }]), - }); - - expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['metadata.0.value.iCloudId must be a string'])); - }); - it('should require `deviceAssetId`', async () => { const { status, body } = await request(ctx.getHttpServer()) .post('/assets') diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index 25573cb08e..96623092f1 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -305,7 +305,7 @@ export class StorageCore { return this.assetRepository.update({ id, encodedVideoPath: newPath }); } case AssetPathType.Sidecar: { - return this.assetRepository.update({ id, sidecarPath: newPath }); + return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Sidecar, path: newPath }); } case PersonPathType.Face: { return this.personRepository.update({ id, thumbnailPath: newPath }); diff --git a/server/src/database.ts b/server/src/database.ts index 4aa69127ff..a3c38ae61e 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -122,7 +122,6 @@ export type Asset = { originalFileName: string; originalPath: string; ownerId: string; - sidecarPath: string | null; type: AssetType; }; @@ -156,13 +155,6 @@ export type StorageAsset = { encodedVideoPath: string | null; }; -export type SidecarWriteAsset = { - id: string; - sidecarPath: string | null; - originalPath: string; - tags: Array<{ value: string }>; -}; - export type Stack = { id: string; primaryAssetId: string; @@ -309,14 +301,14 @@ export type Workflow = Selectable & { export type WorkflowFilter = Selectable & { workflowId: string; - filterId: string; + pluginFilterId: string; filterConfig: FilterConfig | null; order: number; }; export type WorkflowAction = Selectable & { workflowId: string; - actionId: string; + pluginActionId: string; actionConfig: ActionConfig | null; order: number; }; @@ -347,7 +339,6 @@ export const columns = { 'asset.originalFileName', 'asset.originalPath', 'asset.ownerId', - 'asset.sidecarPath', 'asset.type', ], assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'], diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 1716c327f3..e228cd8f9f 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -124,7 +124,6 @@ export type MapAsset = { originalPath: string; owner?: User | null; ownerId: string; - sidecarPath: string | null; stack?: Stack | null; stackId: string | null; tags?: Tag[]; diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index dc43a0200c..03d1e31fb9 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -19,7 +19,6 @@ import { import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AssetMetadataKey, AssetType, AssetVisibility } from 'src/enum'; import { AssetStats } from 'src/repositories/asset.repository'; -import { AssetMetadata, AssetMetadataItem } from 'src/types'; import { IsNotSiblingOf, Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; export class DeviceIdDto { @@ -154,23 +153,12 @@ export class AssetMetadataUpsertDto { items!: AssetMetadataUpsertItemDto[]; } -export class AssetMetadataUpsertItemDto implements AssetMetadataItem { +export class AssetMetadataUpsertItemDto { @ValidateEnum({ enum: AssetMetadataKey, name: 'AssetMetadataKey' }) key!: AssetMetadataKey; @IsObject() - @ValidateNested() - @Type((options) => { - switch (options?.object.key) { - case AssetMetadataKey.MobileApp: { - return AssetMetadataMobileAppDto; - } - default: { - return Object; - } - } - }) - value!: AssetMetadata[AssetMetadataKey]; + value!: object; } export class AssetMetadataMobileAppDto { diff --git a/server/src/dtos/workflow.dto.ts b/server/src/dtos/workflow.dto.ts index 307440945d..7bfb90e11f 100644 --- a/server/src/dtos/workflow.dto.ts +++ b/server/src/dtos/workflow.dto.ts @@ -7,7 +7,7 @@ import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; export class WorkflowFilterItemDto { @IsUUID() - filterId!: string; + pluginFilterId!: string; @IsObject() @Optional() @@ -16,7 +16,7 @@ export class WorkflowFilterItemDto { export class WorkflowActionItemDto { @IsUUID() - actionId!: string; + pluginActionId!: string; @IsObject() @Optional() @@ -86,7 +86,7 @@ export class WorkflowResponseDto { export class WorkflowFilterResponseDto { id!: string; workflowId!: string; - filterId!: string; + pluginFilterId!: string; filterConfig!: FilterConfig | null; order!: number; } @@ -94,7 +94,7 @@ export class WorkflowFilterResponseDto { export class WorkflowActionResponseDto { id!: string; workflowId!: string; - actionId!: string; + pluginActionId!: string; actionConfig!: ActionConfig | null; order!: number; } @@ -103,7 +103,7 @@ export function mapWorkflowFilter(filter: WorkflowFilter): WorkflowFilterRespons return { id: filter.id, workflowId: filter.workflowId, - filterId: filter.filterId, + pluginFilterId: filter.pluginFilterId, filterConfig: filter.filterConfig, order: filter.order, }; @@ -113,7 +113,7 @@ export function mapWorkflowAction(action: WorkflowAction): WorkflowActionRespons return { id: action.id, workflowId: action.workflowId, - actionId: action.actionId, + pluginActionId: action.pluginActionId, actionConfig: action.actionConfig, order: action.order, }; diff --git a/server/src/enum.ts b/server/src/enum.ts index 18206ea1ae..41804bf18e 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -44,6 +44,7 @@ export enum AssetFileType { FullSize = 'fullsize', Preview = 'preview', Thumbnail = 'thumbnail', + Sidecar = 'sidecar', } export enum AlbumUserRole { diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index ebfd1a08c9..d6dc564458 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -20,8 +20,23 @@ limit -- AssetJobRepository.getForSidecarWriteJob select "id", - "sidecarPath", "originalPath", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "asset_file"."id", + "asset_file"."path", + "asset_file"."type" + from + "asset_file" + where + "asset_file"."assetId" = "asset"."id" + and "asset_file"."type" = $1 + ) as agg + ) as "files", ( select coalesce(json_agg(agg), '[]') @@ -39,21 +54,36 @@ select from "asset" where - "asset"."id" = $1::uuid + "asset"."id" = $2::uuid limit - $2 + $3 -- AssetJobRepository.getForSidecarCheckJob select "id", - "sidecarPath", - "originalPath" + "originalPath", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "asset_file"."id", + "asset_file"."path", + "asset_file"."type" + from + "asset_file" + where + "asset_file"."assetId" = "asset"."id" + and "asset_file"."type" = $1 + ) as agg + ) as "files" from "asset" where - "asset"."id" = $1::uuid + "asset"."id" = $2::uuid limit - $2 + $3 -- AssetJobRepository.streamForThumbnailJob select @@ -158,7 +188,6 @@ select "asset"."originalFileName", "asset"."originalPath", "asset"."ownerId", - "asset"."sidecarPath", "asset"."type", ( select @@ -173,11 +202,27 @@ select "asset_face"."assetId" = "asset"."id" and "asset_face"."deletedAt" is null ) as agg - ) as "faces" + ) as "faces", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "asset_file"."id", + "asset_file"."path", + "asset_file"."type" + from + "asset_file" + where + "asset_file"."assetId" = "asset"."id" + and "asset_file"."type" = $1 + ) as agg + ) as "files" from "asset" where - "asset"."id" = $1 + "asset"."id" = $2 -- AssetJobRepository.getAlbumThumbnailFiles select @@ -322,7 +367,6 @@ select "asset"."libraryId", "asset"."ownerId", "asset"."livePhotoVideoId", - "asset"."sidecarPath", "asset"."encodedVideoPath", "asset"."originalPath", to_json("asset_exif") as "exifInfo", @@ -433,18 +477,33 @@ select "asset"."checksum", "asset"."originalPath", "asset"."isExternal", - "asset"."sidecarPath", "asset"."originalFileName", "asset"."livePhotoVideoId", "asset"."fileCreatedAt", "asset_exif"."timeZone", - "asset_exif"."fileSizeInByte" + "asset_exif"."fileSizeInByte", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "asset_file"."id", + "asset_file"."path", + "asset_file"."type" + from + "asset_file" + where + "asset_file"."assetId" = "asset"."id" + and "asset_file"."type" = $1 + ) as agg + ) as "files" from "asset" inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" where "asset"."deletedAt" is null - and "asset"."id" = $1 + and "asset"."id" = $2 -- AssetJobRepository.streamForStorageTemplateJob select @@ -454,12 +513,27 @@ select "asset"."checksum", "asset"."originalPath", "asset"."isExternal", - "asset"."sidecarPath", "asset"."originalFileName", "asset"."livePhotoVideoId", "asset"."fileCreatedAt", "asset_exif"."timeZone", - "asset_exif"."fileSizeInByte" + "asset_exif"."fileSizeInByte", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "asset_file"."id", + "asset_file"."path", + "asset_file"."type" + from + "asset_file" + where + "asset_file"."assetId" = "asset"."id" + and "asset_file"."type" = $1 + ) as agg + ) as "files" from "asset" inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId" @@ -481,11 +555,15 @@ select from "asset" where - ( - "asset"."sidecarPath" = $1 - or "asset"."sidecarPath" is null + not exists ( + select + "asset_file"."id" + from + "asset_file" + where + "asset_file"."assetId" = "asset"."id" + and "asset_file"."type" = $1 ) - and "asset"."visibility" != $2 -- AssetJobRepository.streamForDetectFacesJob select diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 6cf3ec2f54..01cc6a7a89 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -216,6 +216,34 @@ from limit 3 +-- AssetRepository.getForCopy +select + "id", + "stackId", + "originalPath", + "isFavorite", + ( + select + coalesce(json_agg(agg), '[]') + from + ( + select + "asset_file"."id", + "asset_file"."path", + "asset_file"."type" + from + "asset_file" + where + "asset_file"."assetId" = "asset"."id" + ) as agg + ) as "files" +from + "asset" +where + "id" = $1::uuid +limit + $2 + -- AssetRepository.getById select "asset".* diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index 8d54e93c87..dc6e9e2573 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -6,7 +6,6 @@ import { Asset, columns } from 'src/database'; import { DummyValue, GenerateSql } from 'src/decorators'; import { AssetFileType, AssetType, AssetVisibility } from 'src/enum'; import { DB } from 'src/schema'; -import { StorageAsset } from 'src/types'; import { anyUuid, asUuid, @@ -40,7 +39,8 @@ export class AssetJobRepository { return this.db .selectFrom('asset') .where('asset.id', '=', asUuid(id)) - .select(['id', 'sidecarPath', 'originalPath']) + .select(['id', 'originalPath']) + .select((eb) => withFiles(eb, AssetFileType.Sidecar)) .select((eb) => jsonArrayFrom( eb @@ -59,7 +59,8 @@ export class AssetJobRepository { return this.db .selectFrom('asset') .where('asset.id', '=', asUuid(id)) - .select(['id', 'sidecarPath', 'originalPath']) + .select(['id', 'originalPath']) + .select((eb) => withFiles(eb, AssetFileType.Sidecar)) .limit(1) .executeTakeFirst(); } @@ -122,6 +123,7 @@ export class AssetJobRepository { .selectFrom('asset') .select(columns.asset) .select(withFaces) + .select((eb) => withFiles(eb, AssetFileType.Sidecar)) .where('asset.id', '=', id) .executeTakeFirst(); } @@ -228,7 +230,6 @@ export class AssetJobRepository { 'asset.libraryId', 'asset.ownerId', 'asset.livePhotoVideoId', - 'asset.sidecarPath', 'asset.encodedVideoPath', 'asset.originalPath', ]) @@ -306,26 +307,24 @@ export class AssetJobRepository { 'asset.checksum', 'asset.originalPath', 'asset.isExternal', - 'asset.sidecarPath', 'asset.originalFileName', 'asset.livePhotoVideoId', 'asset.fileCreatedAt', 'asset_exif.timeZone', 'asset_exif.fileSizeInByte', ]) + .select((eb) => withFiles(eb, AssetFileType.Sidecar)) .where('asset.deletedAt', 'is', null); } @GenerateSql({ params: [DummyValue.UUID] }) - getForStorageTemplateJob(id: string): Promise { - return this.storageTemplateAssetQuery().where('asset.id', '=', id).executeTakeFirst() as Promise< - StorageAsset | undefined - >; + getForStorageTemplateJob(id: string) { + return this.storageTemplateAssetQuery().where('asset.id', '=', id).executeTakeFirst(); } @GenerateSql({ params: [], stream: true }) streamForStorageTemplateJob() { - return this.storageTemplateAssetQuery().stream() as AsyncIterableIterator; + return this.storageTemplateAssetQuery().stream(); } @GenerateSql({ params: [DummyValue.DATE], stream: true }) @@ -343,9 +342,18 @@ export class AssetJobRepository { .selectFrom('asset') .select(['asset.id']) .$if(!force, (qb) => - qb.where((eb) => eb.or([eb('asset.sidecarPath', '=', ''), eb('asset.sidecarPath', 'is', null)])), + qb.where((eb) => + eb.not( + eb.exists( + eb + .selectFrom('asset_file') + .select('asset_file.id') + .whereRef('asset_file.assetId', '=', 'asset.id') + .where('asset_file.type', '=', AssetFileType.Sidecar), + ), + ), + ), ) - .where('asset.visibility', '!=', AssetVisibility.Hidden) .stream(); } diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index d3d9ada80f..4b8cbd7a7a 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -11,7 +11,6 @@ import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { AssetFileTable } from 'src/schema/tables/asset-file.table'; import { AssetJobStatusTable } from 'src/schema/tables/asset-job-status.table'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { AssetMetadataItem } from 'src/types'; import { anyUuid, asUuid, @@ -224,7 +223,7 @@ export class AssetRepository { .execute(); } - upsertMetadata(id: string, items: AssetMetadataItem[]) { + upsertMetadata(id: string, items: Array<{ key: AssetMetadataKey; value: object }>) { return this.db .insertInto('asset_metadata') .values(items.map((item) => ({ assetId: id, ...item }))) @@ -397,6 +396,17 @@ export class AssetRepository { return this.db.selectFrom('asset_file').select(['assetId', 'path']).limit(sql.lit(3)).execute(); } + @GenerateSql({ params: [DummyValue.UUID] }) + getForCopy(id: string) { + return this.db + .selectFrom('asset') + .select(['id', 'stackId', 'originalPath', 'isFavorite']) + .select(withFiles) + .where('id', '=', asUuid(id)) + .limit(1) + .executeTakeFirst(); + } + @GenerateSql({ params: [DummyValue.UUID] }) getById(id: string, { exifInfo, faces, files, library, owner, smartSearch, stack, tags }: GetByIdsRelations = {}) { return this.db @@ -843,6 +853,10 @@ export class AssetRepository { .execute(); } + async deleteFile({ assetId, type }: { assetId: string; type: AssetFileType }): Promise { + await this.db.deleteFrom('asset_file').where('assetId', '=', asUuid(assetId)).where('type', '=', type).execute(); + } + async deleteFiles(files: Pick, 'id'>[]): Promise { if (files.length === 0) { return; diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index 842576fafb..0fbaabf930 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -403,7 +403,6 @@ export class DatabaseRepository { .set((eb) => ({ originalPath: eb.fn('REGEXP_REPLACE', ['originalPath', source, target]), encodedVideoPath: eb.fn('REGEXP_REPLACE', ['encodedVideoPath', source, target]), - sidecarPath: eb.fn('REGEXP_REPLACE', ['sidecarPath', source, target]), })) .execute(); diff --git a/server/src/repositories/oauth.repository.ts b/server/src/repositories/oauth.repository.ts index 58b1144647..a42955ba10 100644 --- a/server/src/repositories/oauth.repository.ts +++ b/server/src/repositories/oauth.repository.ts @@ -24,9 +24,8 @@ export class OAuthRepository { } async authorize(config: OAuthConfig, redirectUrl: string, state?: string, codeChallenge?: string) { - const { buildAuthorizationUrl, randomState, randomPKCECodeVerifier, calculatePKCECodeChallenge } = await import( - 'openid-client' - ); + const { buildAuthorizationUrl, randomState, randomPKCECodeVerifier, calculatePKCECodeChallenge } = + await import('openid-client'); const client = await this.getClient(config); state ??= randomState(); diff --git a/server/src/repositories/ocr.repository.ts b/server/src/repositories/ocr.repository.ts index 1da9a96ec5..a39f0d368c 100644 --- a/server/src/repositories/ocr.repository.ts +++ b/server/src/repositories/ocr.repository.ts @@ -45,12 +45,12 @@ export class OcrRepository { textScore: DummyValue.NUMBER, }, ], + DummyValue.STRING, ], }) - upsert(assetId: string, ocrDataList: Insertable[]) { + upsert(assetId: string, ocrDataList: Insertable[], searchText: string) { let query = this.db.with('deleted_ocr', (db) => db.deleteFrom('asset_ocr').where('assetId', '=', assetId)); if (ocrDataList.length > 0) { - const searchText = ocrDataList.map((item) => item.text.trim()).join(' '); (query as any) = query .with('inserted_ocr', (db) => db.insertInto('asset_ocr').values(ocrDataList)) .with('inserted_search', (db) => diff --git a/server/src/schema/migrations/1764483051488-OCRBigramsForCJK.ts b/server/src/schema/migrations/1764483051488-OCRBigramsForCJK.ts new file mode 100644 index 0000000000..3f5cb5fa8f --- /dev/null +++ b/server/src/schema/migrations/1764483051488-OCRBigramsForCJK.ts @@ -0,0 +1,31 @@ +import { Kysely, sql } from 'kysely'; +import { tokenizeForSearch } from 'src/utils/database'; + +export async function up(db: Kysely): Promise { + await sql`truncate ${sql.table('ocr_search')}`.execute(db); + + let lastAssetId: string | undefined; + while (true) { + const rows = await db + .selectFrom('asset_ocr') + .select(['assetId', sql`string_agg(text, ' ')`.as('text')]) + .$if(lastAssetId !== undefined, (qb) => qb.where('assetId', '>', lastAssetId)) + .groupBy('assetId') + .orderBy('assetId') + .limit(5000) + .execute(); + + if (rows.length === 0) { + break; + } + + await db + .insertInto('ocr_search') + .values(rows.map(({ assetId, text }) => ({ assetId, text: tokenizeForSearch(text).join(' ') }))) + .execute(); + + lastAssetId = rows.at(-1)!.assetId; + } +} + +export async function down(): Promise {} diff --git a/server/src/schema/migrations/1764698859174-SidecarInAssetFile.ts b/server/src/schema/migrations/1764698859174-SidecarInAssetFile.ts new file mode 100644 index 0000000000..183a77832e --- /dev/null +++ b/server/src/schema/migrations/1764698859174-SidecarInAssetFile.ts @@ -0,0 +1,24 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`INSERT INTO "asset_file" ("assetId", "path", "type") + SELECT + id, "sidecarPath", 'sidecar' + FROM "asset" + WHERE "sidecarPath" IS NOT NULL AND "sidecarPath" != '';`.execute(db); + + await sql`ALTER TABLE "asset" DROP COLUMN "sidecarPath";`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "asset" ADD "sidecarPath" character varying;`.execute(db); + + await sql` + UPDATE "asset" + SET "sidecarPath" = "asset_file"."path" + FROM "asset_file" + WHERE "asset"."id" = "asset_file"."assetId" AND "asset_file"."type" = 'sidecar'; + `.execute(db); + + await sql`DELETE FROM "asset_file" WHERE "type" = 'sidecar';`.execute(db); +} diff --git a/server/src/schema/migrations/1764705680041-ChangeWorkflowTableColumnsName.ts b/server/src/schema/migrations/1764705680041-ChangeWorkflowTableColumnsName.ts new file mode 100644 index 0000000000..c495d2a8e2 --- /dev/null +++ b/server/src/schema/migrations/1764705680041-ChangeWorkflowTableColumnsName.ts @@ -0,0 +1,27 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`DROP INDEX "workflow_filter_filterId_idx";`.execute(db); + await sql`DROP INDEX "workflow_action_actionId_idx";`.execute(db); + await sql`ALTER TABLE "workflow_filter" DROP CONSTRAINT "workflow_filter_filterId_fkey";`.execute(db); + await sql`ALTER TABLE "workflow_action" DROP CONSTRAINT "workflow_action_actionId_fkey";`.execute(db); + await sql`ALTER TABLE "workflow_filter" RENAME COLUMN "filterId" TO "pluginFilterId";`.execute(db); + await sql`ALTER TABLE "workflow_action" RENAME COLUMN "actionId" TO "pluginActionId";`.execute(db); + await sql`ALTER TABLE "workflow_filter" ADD CONSTRAINT "workflow_filter_pluginFilterId_fkey" FOREIGN KEY ("pluginFilterId") REFERENCES "plugin_filter" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "workflow_action" ADD CONSTRAINT "workflow_action_pluginActionId_fkey" FOREIGN KEY ("pluginActionId") REFERENCES "plugin_action" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`CREATE INDEX "workflow_filter_pluginFilterId_idx" ON "workflow_filter" ("pluginFilterId");`.execute(db); + await sql`CREATE INDEX "workflow_action_pluginActionId_idx" ON "workflow_action" ("pluginActionId");`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP INDEX "workflow_filter_pluginFilterId_idx";`.execute(db); + await sql`DROP INDEX "workflow_action_pluginActionId_idx";`.execute(db); + await sql`ALTER TABLE "workflow_filter" DROP CONSTRAINT "workflow_filter_pluginFilterId_fkey";`.execute(db); + await sql`ALTER TABLE "workflow_action" DROP CONSTRAINT "workflow_action_pluginActionId_fkey";`.execute(db); + await sql`ALTER TABLE "workflow_filter" RENAME COLUMN "pluginFilterId" TO "filterId";`.execute(db); + await sql`ALTER TABLE "workflow_action" RENAME COLUMN "pluginActionId" TO "actionId";`.execute(db); + await sql`ALTER TABLE "workflow_filter" ADD CONSTRAINT "workflow_filter_filterId_fkey" FOREIGN KEY ("filterId") REFERENCES "plugin_filter" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`ALTER TABLE "workflow_action" ADD CONSTRAINT "workflow_action_actionId_fkey" FOREIGN KEY ("actionId") REFERENCES "plugin_action" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); + await sql`CREATE INDEX "workflow_filter_filterId_idx" ON "workflow_filter" ("filterId");`.execute(db); + await sql`CREATE INDEX "workflow_action_actionId_idx" ON "workflow_action" ("actionId");`.execute(db); +} diff --git a/server/src/schema/tables/asset-metadata.table.ts b/server/src/schema/tables/asset-metadata.table.ts index 486101408d..d529d6ad7b 100644 --- a/server/src/schema/tables/asset-metadata.table.ts +++ b/server/src/schema/tables/asset-metadata.table.ts @@ -12,7 +12,6 @@ import { Timestamp, UpdateDateColumn, } from 'src/sql-tools'; -import { AssetMetadata, AssetMetadataItem } from 'src/types'; @UpdatedAtTrigger('asset_metadata_updated_at') @Table('asset_metadata') @@ -22,7 +21,7 @@ import { AssetMetadata, AssetMetadataItem } from 'src/types'; referencingOldTableAs: 'old', when: 'pg_trigger_depth() = 0', }) -export class AssetMetadataTable implements AssetMetadataItem { +export class AssetMetadataTable { @ForeignKeyColumn(() => AssetTable, { onUpdate: 'CASCADE', onDelete: 'CASCADE', @@ -33,10 +32,10 @@ export class AssetMetadataTable; diff --git a/server/src/schema/tables/asset.table.ts b/server/src/schema/tables/asset.table.ts index e92e01a1bd..b28fc99e4a 100644 --- a/server/src/schema/tables/asset.table.ts +++ b/server/src/schema/tables/asset.table.ts @@ -105,9 +105,6 @@ export class AssetTable { @Column({ index: true }) originalFileName!: string; - @Column({ nullable: true }) - sidecarPath!: string | null; - @Column({ type: 'bytea', nullable: true }) thumbhash!: Buffer | null; diff --git a/server/src/schema/tables/workflow.table.ts b/server/src/schema/tables/workflow.table.ts index 8f7c9adb0d..62a5531d8e 100644 --- a/server/src/schema/tables/workflow.table.ts +++ b/server/src/schema/tables/workflow.table.ts @@ -38,7 +38,7 @@ export class WorkflowTable { } @Index({ columns: ['workflowId', 'order'] }) -@Index({ columns: ['filterId'] }) +@Index({ columns: ['pluginFilterId'] }) @Table('workflow_filter') export class WorkflowFilterTable { @PrimaryGeneratedColumn('uuid') @@ -48,7 +48,7 @@ export class WorkflowFilterTable { workflowId!: Generated; @ForeignKeyColumn(() => PluginFilterTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) - filterId!: string; + pluginFilterId!: string; @Column({ type: 'jsonb', nullable: true }) filterConfig!: FilterConfig | null; @@ -58,7 +58,7 @@ export class WorkflowFilterTable { } @Index({ columns: ['workflowId', 'order'] }) -@Index({ columns: ['actionId'] }) +@Index({ columns: ['pluginActionId'] }) @Table('workflow_action') export class WorkflowActionTable { @PrimaryGeneratedColumn('uuid') @@ -68,7 +68,7 @@ export class WorkflowActionTable { workflowId!: Generated; @ForeignKeyColumn(() => PluginActionTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) - actionId!: string; + pluginActionId!: string; @Column({ type: 'jsonb', nullable: true }) actionConfig!: ActionConfig | null; diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index f32385b937..95eb8b3c97 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -174,7 +174,6 @@ const assetEntity = Object.freeze({ longitude: 10.703_075, }, livePhotoVideoId: null, - sidecarPath: null, } as MapAsset); const existingAsset = Object.freeze({ @@ -188,7 +187,6 @@ const existingAsset = Object.freeze({ const sidecarAsset = Object.freeze({ ...existingAsset, - sidecarPath: 'sidecar-path', checksum: Buffer.from('_getExistingAssetWithSideCar', 'utf8'), }) as MapAsset; @@ -721,18 +719,22 @@ describe(AssetMediaService.name, () => { expect(mocks.asset.update).toHaveBeenCalledWith( expect.objectContaining({ id: existingAsset.id, - sidecarPath: null, originalFileName: 'photo1.jpeg', originalPath: 'fake_path/photo1.jpeg', }), ); expect(mocks.asset.create).toHaveBeenCalledWith( expect.objectContaining({ - sidecarPath: null, originalFileName: 'existing-filename.jpeg', originalPath: 'fake_path/asset_1.jpeg', }), ); + expect(mocks.asset.deleteFile).toHaveBeenCalledWith( + expect.objectContaining({ + assetId: existingAsset.id, + type: AssetFileType.Sidecar, + }), + ); expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], { deletedAt: expect.any(Date), @@ -769,6 +771,13 @@ describe(AssetMediaService.name, () => { deletedAt: expect.any(Date), status: AssetStatus.Trashed, }); + expect(mocks.asset.upsertFile).toHaveBeenCalledWith( + expect.objectContaining({ + assetId: existingAsset.id, + path: sidecarFile.originalPath, + type: AssetFileType.Sidecar, + }), + ); expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size); expect(mocks.storage.utimes).toHaveBeenCalledWith( updatedFile.originalPath, @@ -798,6 +807,12 @@ describe(AssetMediaService.name, () => { deletedAt: expect.any(Date), status: AssetStatus.Trashed, }); + expect(mocks.asset.deleteFile).toHaveBeenCalledWith( + expect.objectContaining({ + assetId: existingAsset.id, + type: AssetFileType.Sidecar, + }), + ); expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size); expect(mocks.storage.utimes).toHaveBeenCalledWith( updatedFile.originalPath, @@ -827,6 +842,9 @@ describe(AssetMediaService.name, () => { expect(mocks.asset.create).not.toHaveBeenCalled(); expect(mocks.asset.updateAll).not.toHaveBeenCalled(); + expect(mocks.asset.upsertFile).not.toHaveBeenCalled(); + expect(mocks.asset.deleteFile).not.toHaveBeenCalled(); + expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.FileDelete, data: { files: [updatedFile.originalPath, undefined] }, diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 4db60c349f..d2e1c14210 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -21,7 +21,16 @@ import { UploadFieldName, } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetStatus, AssetType, AssetVisibility, CacheControl, JobName, Permission, StorageFolder } from 'src/enum'; +import { + AssetFileType, + AssetStatus, + AssetType, + AssetVisibility, + CacheControl, + JobName, + Permission, + StorageFolder, +} from 'src/enum'; import { AuthRequest } from 'src/middleware/auth.guard'; import { BaseService } from 'src/services/base.service'; import { UploadFile, UploadRequest } from 'src/types'; @@ -354,9 +363,12 @@ export class AssetMediaService extends BaseService { duration: dto.duration || null, livePhotoVideoId: null, - sidecarPath: sidecarPath || null, }); + await (sidecarPath + ? this.assetRepository.upsertFile({ assetId, type: AssetFileType.Sidecar, path: sidecarPath }) + : this.assetRepository.deleteFile({ assetId, type: AssetFileType.Sidecar })); + await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt)); await this.assetRepository.upsertExif({ assetId, fileSizeInByte: file.size }); await this.jobRepository.queue({ @@ -384,7 +396,6 @@ export class AssetMediaService extends BaseService { localDateTime: asset.localDateTime, fileModifiedAt: asset.fileModifiedAt, livePhotoVideoId: asset.livePhotoVideoId, - sidecarPath: asset.sidecarPath, }); const { size } = await this.storageRepository.stat(created.originalPath); @@ -414,7 +425,6 @@ export class AssetMediaService extends BaseService { visibility: dto.visibility ?? AssetVisibility.Timeline, livePhotoVideoId: dto.livePhotoVideoId, originalFileName: dto.filename || file.originalName, - sidecarPath: sidecarFile?.originalPath, }); if (dto.metadata) { @@ -422,6 +432,11 @@ export class AssetMediaService extends BaseService { } if (sidecarFile) { + await this.assetRepository.upsertFile({ + assetId: asset.id, + path: sidecarFile.originalPath, + type: AssetFileType.Sidecar, + }); await this.storageRepository.utimes(sidecarFile.originalPath, new Date(), new Date(dto.fileModifiedAt)); } await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt)); diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 4b0086c957..8c646e45b9 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -585,8 +585,8 @@ describe(AssetService.name, () => { '/uploads/user-id/webp/path.ext', '/uploads/user-id/thumbs/path.jpg', '/uploads/user-id/fullsize/path.webp', - assetWithFace.encodedVideoPath, - assetWithFace.sidecarPath, + assetWithFace.encodedVideoPath, // this value is null + undefined, // no sidecar path assetWithFace.originalPath, ], }, diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 23cc6791dd..0a9aa7f355 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -2,6 +2,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import _ from 'lodash'; import { DateTime, Duration } from 'luxon'; import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; +import { AssetFile } from 'src/database'; import { OnJob } from 'src/decorators'; import { AssetResponseDto, MapAsset, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { @@ -18,7 +19,16 @@ import { } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; -import { AssetMetadataKey, AssetStatus, AssetVisibility, JobName, JobStatus, Permission, QueueName } from 'src/enum'; +import { + AssetFileType, + AssetMetadataKey, + AssetStatus, + AssetVisibility, + JobName, + JobStatus, + Permission, + QueueName, +} from 'src/enum'; import { BaseService } from 'src/services/base.service'; import { ISidecarWriteJob, JobItem, JobOf } from 'src/types'; import { requireElevatedPermission } from 'src/utils/access'; @@ -197,8 +207,8 @@ export class AssetService extends BaseService { }: AssetCopyDto, ) { await this.requireAccess({ auth, permission: Permission.AssetCopy, ids: [sourceId, targetId] }); - const sourceAsset = await this.assetRepository.getById(sourceId); - const targetAsset = await this.assetRepository.getById(targetId); + const sourceAsset = await this.assetRepository.getForCopy(sourceId); + const targetAsset = await this.assetRepository.getForCopy(targetId); if (!sourceAsset || !targetAsset) { throw new BadRequestException('Both assets must exist'); @@ -252,19 +262,25 @@ export class AssetService extends BaseService { sourceAsset, targetAsset, }: { - sourceAsset: { sidecarPath: string | null }; - targetAsset: { id: string; sidecarPath: string | null; originalPath: string }; + sourceAsset: { files: AssetFile[] }; + targetAsset: { id: string; files: AssetFile[]; originalPath: string }; }) { - if (!sourceAsset.sidecarPath) { + const { sidecarFile: sourceFile } = getAssetFiles(sourceAsset.files); + if (!sourceFile?.path) { return; } - if (targetAsset.sidecarPath) { - await this.storageRepository.unlink(targetAsset.sidecarPath); + const { sidecarFile: targetFile } = getAssetFiles(targetAsset.files ?? []); + if (targetFile?.path) { + await this.storageRepository.unlink(targetFile.path); } - await this.storageRepository.copyFile(sourceAsset.sidecarPath, `${targetAsset.originalPath}.xmp`); - await this.assetRepository.update({ id: targetAsset.id, sidecarPath: `${targetAsset.originalPath}.xmp` }); + await this.storageRepository.copyFile(sourceFile.path, `${targetAsset.originalPath}.xmp`); + await this.assetRepository.upsertFile({ + assetId: targetAsset.id, + path: `${targetAsset.originalPath}.xmp`, + type: AssetFileType.Sidecar, + }); await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: targetAsset.id } }); } @@ -344,11 +360,11 @@ export class AssetService extends BaseService { } } - const { fullsizeFile, previewFile, thumbnailFile } = getAssetFiles(asset.files ?? []); + const { fullsizeFile, previewFile, thumbnailFile, sidecarFile } = getAssetFiles(asset.files ?? []); const files = [thumbnailFile?.path, previewFile?.path, fullsizeFile?.path, asset.encodedVideoPath]; if (deleteOnDisk) { - files.push(asset.sidecarPath, asset.originalPath); + files.push(sidecarFile?.path, asset.originalPath); } await this.jobRepository.queue({ name: JobName.FileDelete, data: { files } }); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 54ddc0de48..9027e89d66 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -174,8 +174,10 @@ export class MediaService extends BaseService { thumbhash: Buffer; }; if (asset.type === AssetType.Video || asset.originalFileName.toLowerCase().endsWith('.gif')) { + this.logger.verbose(`Thumbnail generation for video ${id} ${asset.originalPath}`); generated = await this.generateVideoThumbnails(asset); } else if (asset.type === AssetType.Image) { + this.logger.verbose(`Thumbnail generation for image ${id} ${asset.originalPath}`); generated = await this.generateImageThumbnails(asset); } else { this.logger.warn(`Skipping thumbnail generation for asset ${id}: ${asset.type} is not an image or video`); diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 1760c8a3d7..46d6fe7abc 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -4,7 +4,16 @@ import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; import { defaults } from 'src/config'; import { MapAsset } from 'src/dtos/asset-response.dto'; -import { AssetType, AssetVisibility, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum'; +import { + AssetFileType, + AssetType, + AssetVisibility, + ExifOrientation, + ImmichWorker, + JobName, + JobStatus, + SourceType, +} from 'src/enum'; import { ImmichTags } from 'src/repositories/metadata.repository'; import { firstDateTime, MetadataService } from 'src/services/metadata.service'; import { assetStub } from 'test/fixtures/asset.stub'; @@ -15,17 +24,24 @@ import { tagStub } from 'test/fixtures/tag.stub'; import { factory } from 'test/small.factory'; import { makeStream, newTestService, ServiceMocks } from 'test/utils'; +const removeNonSidecarFiles = (asset: any) => { + return { + ...asset, + files: asset.files.filter((file: any) => file.type === AssetFileType.Sidecar), + }; +}; + const forSidecarJob = ( asset: { id?: string; originalPath?: string; - sidecarPath?: string | null; + files?: { id: string; type: AssetFileType; path: string }[]; } = {}, ) => { return { id: factory.uuid(), originalPath: '/path/to/IMG_123.jpg', - sidecarPath: null, + files: [], ...asset, }; }; @@ -166,7 +182,7 @@ describe(MetadataService.name, () => { it('should handle a date in a sidecar file', async () => { const originalDate = new Date('2023-11-21T16:13:17.517Z'); const sidecarDate = new Date('2022-01-01T00:00:00.000Z'); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.sidecar); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.sidecar)); mockReadTags({ CreationDate: originalDate.toISOString() }, { CreationDate: sidecarDate.toISOString() }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -185,7 +201,7 @@ describe(MetadataService.name, () => { it('should take the file modification date when missing exif and earlier than creation date', async () => { const fileCreatedAt = new Date('2022-01-01T00:00:00.000Z'); const fileModifiedAt = new Date('2021-01-01T00:00:00.000Z'); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); mocks.storage.stat.mockResolvedValue({ size: 123_456, mtime: fileModifiedAt, @@ -211,7 +227,7 @@ describe(MetadataService.name, () => { it('should take the file creation date when missing exif and earlier than modification date', async () => { const fileCreatedAt = new Date('2021-01-01T00:00:00.000Z'); const fileModifiedAt = new Date('2022-01-01T00:00:00.000Z'); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); mocks.storage.stat.mockResolvedValue({ size: 123_456, mtime: fileModifiedAt, @@ -234,7 +250,7 @@ describe(MetadataService.name, () => { it('should determine dateTimeOriginal regardless of the server time zone', async () => { process.env.TZ = 'America/Los_Angeles'; - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.sidecar); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.sidecar)); mockReadTags({ DateTimeOriginal: '2022:01:01 00:00:00' }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -252,7 +268,7 @@ describe(MetadataService.name, () => { }); it('should handle lists of numbers', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); mocks.storage.stat.mockResolvedValue({ size: 123_456, mtime: assetStub.image.fileModifiedAt, @@ -305,7 +321,7 @@ describe(MetadataService.name, () => { }); it('should apply reverse geocoding', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.withLocation); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.withLocation)); mocks.systemMetadata.get.mockResolvedValue({ reverseGeocoding: { enabled: true } }); mocks.map.reverseGeocode.mockResolvedValue({ city: 'City', state: 'State', country: 'Country' }); mocks.storage.stat.mockResolvedValue({ @@ -334,7 +350,7 @@ describe(MetadataService.name, () => { }); it('should discard latitude and longitude on null island', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.withLocation); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.withLocation)); mockReadTags({ GPSLatitude: 0, GPSLongitude: 0, @@ -346,7 +362,7 @@ describe(MetadataService.name, () => { }); it('should extract tags from TagsList', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); mockReadTags({ TagsList: ['Parent'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -356,7 +372,7 @@ describe(MetadataService.name, () => { }); it('should extract hierarchy from TagsList', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); mockReadTags({ TagsList: ['Parent/Child'] }); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert); @@ -376,7 +392,7 @@ describe(MetadataService.name, () => { }); it('should extract tags from Keywords as a string', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); mockReadTags({ Keywords: 'Parent' }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -386,7 +402,7 @@ describe(MetadataService.name, () => { }); it('should extract tags from Keywords as a list', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); mockReadTags({ Keywords: ['Parent'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -396,7 +412,7 @@ describe(MetadataService.name, () => { }); it('should extract tags from Keywords as a list with a number', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); mockReadTags({ Keywords: ['Parent', 2024] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -407,7 +423,7 @@ describe(MetadataService.name, () => { }); it('should extract hierarchal tags from Keywords', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); mockReadTags({ Keywords: 'Parent/Child' }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -426,7 +442,7 @@ describe(MetadataService.name, () => { }); it('should ignore Keywords when TagsList is present', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -445,7 +461,7 @@ describe(MetadataService.name, () => { }); it('should extract hierarchy from HierarchicalSubject', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] }); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert); @@ -466,7 +482,7 @@ describe(MetadataService.name, () => { }); it('should extract tags from HierarchicalSubject as a list with a number', async () => { - mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue(removeNonSidecarFiles(assetStub.image)); mockReadTags({ HierarchicalSubject: ['Parent', 2024] }); mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); @@ -1030,8 +1046,15 @@ describe(MetadataService.name, () => { it('should prefer Duration from exif over sidecar', async () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ ...assetStub.image, - sidecarPath: '/path/to/something', + files: [ + { + id: 'some-id', + type: AssetFileType.Sidecar, + path: '/path/to/something', + }, + ], }); + mockReadTags({ Duration: 123 }, { Duration: 456 }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -1536,18 +1559,25 @@ describe(MetadataService.name, () => { }); it('should detect a new sidecar at .jpg.xmp', async () => { - const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg' }); + const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg', files: [] }); mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset); mocks.storage.checkFileExists.mockResolvedValueOnce(true); await expect(sut.handleSidecarCheck({ id: asset.id })).resolves.toBe(JobStatus.Success); - expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, sidecarPath: `/path/to/IMG_123.jpg.xmp` }); + expect(mocks.asset.upsertFile).toHaveBeenCalledWith({ + assetId: asset.id, + type: AssetFileType.Sidecar, + path: '/path/to/IMG_123.jpg.xmp', + }); }); it('should detect a new sidecar at .xmp', async () => { - const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg' }); + const asset = forSidecarJob({ + originalPath: '/path/to/IMG_123.jpg', + files: [], + }); mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset); mocks.storage.checkFileExists.mockResolvedValueOnce(false); @@ -1555,33 +1585,44 @@ describe(MetadataService.name, () => { await expect(sut.handleSidecarCheck({ id: asset.id })).resolves.toBe(JobStatus.Success); - expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, sidecarPath: '/path/to/IMG_123.xmp' }); + expect(mocks.asset.upsertFile).toHaveBeenCalledWith({ + assetId: asset.id, + type: AssetFileType.Sidecar, + path: '/path/to/IMG_123.xmp', + }); }); - it('should unset sidecar path if file does not exist anymore', async () => { - const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg', sidecarPath: '/path/to/IMG_123.jpg.xmp' }); + it('should unset sidecar path if file no longer exist', async () => { + const asset = forSidecarJob({ + originalPath: '/path/to/IMG_123.jpg', + files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar }], + }); mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset); mocks.storage.checkFileExists.mockResolvedValue(false); await expect(sut.handleSidecarCheck({ id: asset.id })).resolves.toBe(JobStatus.Success); - expect(mocks.asset.update).toHaveBeenCalledWith({ id: asset.id, sidecarPath: null }); + expect(mocks.asset.deleteFile).toHaveBeenCalledWith({ assetId: asset.id, type: AssetFileType.Sidecar }); }); it('should do nothing if the sidecar file still exists', async () => { - const asset = forSidecarJob({ originalPath: '/path/to/IMG_123.jpg', sidecarPath: '/path/to/IMG_123.jpg' }); + const asset = forSidecarJob({ + originalPath: '/path/to/IMG_123.jpg', + files: [{ id: 'sidecar', path: '/path/to/IMG_123.jpg.xmp', type: AssetFileType.Sidecar }], + }); mocks.assetJob.getForSidecarCheckJob.mockResolvedValue(asset); mocks.storage.checkFileExists.mockResolvedValueOnce(true); await expect(sut.handleSidecarCheck({ id: asset.id })).resolves.toBe(JobStatus.Skipped); - expect(mocks.asset.update).not.toHaveBeenCalled(); + expect(mocks.asset.upsertFile).not.toHaveBeenCalled(); + expect(mocks.asset.deleteFile).not.toHaveBeenCalled(); }); }); describe('handleSidecarWrite', () => { - it('should skip assets that do not exist anymore', async () => { + it('should skip assets that no longer exist', async () => { mocks.assetJob.getForSidecarWriteJob.mockResolvedValue(void 0); await expect(sut.handleSidecarWrite({ id: 'asset-123' })).resolves.toBe(JobStatus.Failed); expect(mocks.metadata.writeTags).not.toHaveBeenCalled(); @@ -1610,7 +1651,7 @@ describe(MetadataService.name, () => { dateTimeOriginal: date, }), ).resolves.toBe(JobStatus.Success); - expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.sidecarPath, { + expect(mocks.metadata.writeTags).toHaveBeenCalledWith(asset.files[0].path, { Description: description, ImageDescription: description, DateTimeOriginal: date, diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 4efefcb97a..4d6d4c190f 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -8,9 +8,10 @@ import { constants } from 'node:fs/promises'; import { join, parse } from 'node:path'; import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; -import { Asset, AssetFace } from 'src/database'; +import { Asset, AssetFace, AssetFile } from 'src/database'; import { OnEvent, OnJob } from 'src/decorators'; import { + AssetFileType, AssetType, AssetVisibility, DatabaseLock, @@ -29,6 +30,7 @@ import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { PersonTable } from 'src/schema/tables/person.table'; import { BaseService } from 'src/services/base.service'; import { JobItem, JobOf } from 'src/types'; +import { getAssetFiles } from 'src/utils/asset.util'; import { isAssetChecksumConstraint } from 'src/utils/database'; import { isFaceImportEnabled } from 'src/utils/misc'; import { upsertTags } from 'src/utils/tag'; @@ -359,17 +361,21 @@ export class MetadataService extends BaseService { break; } - const isChanged = sidecarPath !== asset.sidecarPath; + const { sidecarFile } = getAssetFiles(asset.files); + + const isChanged = sidecarPath !== sidecarFile?.path; this.logger.debug( - `Sidecar check found old=${asset.sidecarPath}, new=${sidecarPath} will ${isChanged ? 'update' : 'do nothing for'} asset ${asset.id}: ${asset.originalPath}`, + `Sidecar check found old=${sidecarFile?.path}, new=${sidecarPath} will ${isChanged ? 'update' : 'do nothing for'} asset ${asset.id}: ${asset.originalPath}`, ); if (!isChanged) { return JobStatus.Skipped; } - await this.assetRepository.update({ id: asset.id, sidecarPath }); + await (sidecarPath === null + ? this.assetRepository.deleteFile({ assetId: asset.id, type: AssetFileType.Sidecar }) + : this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.Sidecar, path: sidecarPath })); return JobStatus.Success; } @@ -394,7 +400,9 @@ export class MetadataService extends BaseService { const tagsList = (asset.tags || []).map((tag) => tag.value); - const sidecarPath = asset.sidecarPath || `${asset.originalPath}.xmp`; + const { sidecarFile } = getAssetFiles(asset.files); + const sidecarPath = sidecarFile?.path || `${asset.originalPath}.xmp`; + const exif = _.omitBy( { Description: description, @@ -414,18 +422,19 @@ export class MetadataService extends BaseService { await this.metadataRepository.writeTags(sidecarPath, exif); - if (!asset.sidecarPath) { - await this.assetRepository.update({ id, sidecarPath }); + if (asset.files.length === 0) { + await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Sidecar, path: sidecarPath }); } return JobStatus.Success; } - private getSidecarCandidates({ sidecarPath, originalPath }: { sidecarPath: string | null; originalPath: string }) { + private getSidecarCandidates({ files, originalPath }: { files: AssetFile[]; originalPath: string }) { const candidates: string[] = []; - if (sidecarPath) { - candidates.push(sidecarPath); + const { sidecarFile } = getAssetFiles(files); + if (sidecarFile?.path) { + candidates.push(sidecarFile.path); } const assetPath = parse(originalPath); @@ -456,14 +465,12 @@ export class MetadataService extends BaseService { return { width, height }; } - private async getExifTags(asset: { - originalPath: string; - sidecarPath: string | null; - type: AssetType; - }): Promise { + private async getExifTags(asset: { originalPath: string; files: AssetFile[]; type: AssetType }): Promise { + const { sidecarFile } = getAssetFiles(asset.files); + const [mediaTags, sidecarTags, videoTags] = await Promise.all([ this.metadataRepository.readTags(asset.originalPath), - asset.sidecarPath ? this.metadataRepository.readTags(asset.sidecarPath) : null, + sidecarFile ? this.metadataRepository.readTags(sidecarFile.path) : null, asset.type === AssetType.Video ? this.getVideoTags(asset.originalPath) : null, ]); diff --git a/server/src/services/ocr.service.spec.ts b/server/src/services/ocr.service.spec.ts index 6eedba1a5f..404f423cac 100644 --- a/server/src/services/ocr.service.spec.ts +++ b/server/src/services/ocr.service.spec.ts @@ -12,8 +12,21 @@ describe(OcrService.name, () => { ({ sut, mocks } = newTestService(OcrService)); mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices); + mocks.assetJob.getForOcr.mockResolvedValue({ + visibility: AssetVisibility.Timeline, + previewFile: assetStub.image.files[1].path, + }); }); + const mockOcrResult = (...texts: string[]) => { + mocks.machineLearning.ocr.mockResolvedValue({ + box: texts.flatMap((_, i) => Array.from({ length: 8 }, (_, j) => i * 10 + j)), + boxScore: texts.map(() => 0.9), + text: texts, + textScore: texts.map(() => 0.95), + }); + }; + it('should work', () => { expect(sut).toBeDefined(); }); @@ -72,10 +85,6 @@ describe(OcrService.name, () => { text: ['One Two Three', 'Four Five'], textScore: [0.95, 0.85], }); - mocks.assetJob.getForOcr.mockResolvedValue({ - visibility: AssetVisibility.Timeline, - previewFile: assetStub.image.files[1].path, - }); expect(await sut.handleOcr({ id: assetStub.image.id })).toEqual(JobStatus.Success); @@ -88,36 +97,40 @@ describe(OcrService.name, () => { maxResolution: 736, }), ); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, [ - { - assetId: assetStub.image.id, - boxScore: 0.9, - text: 'One Two Three', - textScore: 0.95, - x1: 10, - y1: 20, - x2: 30, - y2: 40, - x3: 50, - y3: 60, - x4: 70, - y4: 80, - }, - { - assetId: assetStub.image.id, - boxScore: 0.8, - text: 'Four Five', - textScore: 0.85, - x1: 90, - y1: 100, - x2: 110, - y2: 120, - x3: 130, - y3: 140, - x4: 150, - y4: 160, - }, - ]); + expect(mocks.ocr.upsert).toHaveBeenCalledWith( + assetStub.image.id, + [ + { + assetId: assetStub.image.id, + boxScore: 0.9, + text: 'One Two Three', + textScore: 0.95, + x1: 10, + y1: 20, + x2: 30, + y2: 40, + x3: 50, + y3: 60, + x4: 70, + y4: 80, + }, + { + assetId: assetStub.image.id, + boxScore: 0.8, + text: 'Four Five', + textScore: 0.85, + x1: 90, + y1: 100, + x2: 110, + y2: 120, + x3: 130, + y3: 140, + x4: 150, + y4: 160, + }, + ], + 'One Two Three Four Five', + ); }); it('should apply config settings', async () => { @@ -133,11 +146,7 @@ describe(OcrService.name, () => { }, }, }); - mocks.machineLearning.ocr.mockResolvedValue({ box: [], boxScore: [], text: [], textScore: [] }); - mocks.assetJob.getForOcr.mockResolvedValue({ - visibility: AssetVisibility.Timeline, - previewFile: assetStub.image.files[1].path, - }); + mockOcrResult(); expect(await sut.handleOcr({ id: assetStub.image.id })).toEqual(JobStatus.Success); @@ -150,7 +159,7 @@ describe(OcrService.name, () => { maxResolution: 1500, }), ); - expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, []); + expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, [], ''); }); it('should skip invisible assets', async () => { @@ -173,5 +182,83 @@ describe(OcrService.name, () => { expect(mocks.machineLearning.ocr).not.toHaveBeenCalled(); expect(mocks.ocr.upsert).not.toHaveBeenCalled(); }); + + describe('search tokenization', () => { + it('should generate bigrams for Chinese text', async () => { + mockOcrResult('機器學習'); + + await sut.handleOcr({ id: assetStub.image.id }); + + expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '機器 器學 學習'); + }); + + it('should generate bigrams for Japanese text', async () => { + mockOcrResult('テスト'); + + await sut.handleOcr({ id: assetStub.image.id }); + + expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'テス スト'); + }); + + it('should generate bigrams for Korean text', async () => { + mockOcrResult('한국어'); + + await sut.handleOcr({ id: assetStub.image.id }); + + expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '한국 국어'); + }); + + it('should pass through Latin text unchanged', async () => { + mockOcrResult('Hello World'); + + await sut.handleOcr({ id: assetStub.image.id }); + + expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'Hello World'); + }); + + it('should handle mixed CJK and Latin text', async () => { + mockOcrResult('機器學習Model'); + + await sut.handleOcr({ id: assetStub.image.id }); + + expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '機器 器學 學習 Model'); + }); + + it('should handle year followed by CJK', async () => { + mockOcrResult('2024年レポート'); + + await sut.handleOcr({ id: assetStub.image.id }); + + expect(mocks.ocr.upsert).toHaveBeenCalledWith( + assetStub.image.id, + expect.any(Array), + '2024 年レ レポ ポー ート', + ); + }); + + it('should join multiple OCR boxes', async () => { + mockOcrResult('機器', 'Learning'); + + await sut.handleOcr({ id: assetStub.image.id }); + + expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), '機器 Learning'); + }); + + it('should normalize whitespace', async () => { + mockOcrResult(' Hello World '); + + await sut.handleOcr({ id: assetStub.image.id }); + + expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'Hello World'); + }); + + it('should keep single CJK characters', async () => { + mockOcrResult('A', '中', 'B'); + + await sut.handleOcr({ id: assetStub.image.id }); + + expect(mocks.ocr.upsert).toHaveBeenCalledWith(assetStub.image.id, expect.any(Array), 'A 中 B'); + }); + }); }); }); diff --git a/server/src/services/ocr.service.ts b/server/src/services/ocr.service.ts index cba57e5bc7..d92d399dba 100644 --- a/server/src/services/ocr.service.ts +++ b/server/src/services/ocr.service.ts @@ -5,6 +5,7 @@ import { AssetVisibility, JobName, JobStatus, QueueName } from 'src/enum'; import { OCR } from 'src/repositories/machine-learning.repository'; import { BaseService } from 'src/services/base.service'; import { JobItem, JobOf } from 'src/types'; +import { tokenizeForSearch } from 'src/utils/database'; import { isOcrEnabled } from 'src/utils/misc'; @Injectable() @@ -53,8 +54,8 @@ export class OcrService extends BaseService { } const ocrResults = await this.machineLearningRepository.ocr(asset.previewFile, machineLearning.ocr); - - await this.ocrRepository.upsert(id, this.parseOcrResults(id, ocrResults)); + const { ocrDataList, searchText } = this.parseOcrResults(id, ocrResults); + await this.ocrRepository.upsert(id, ocrDataList, searchText); await this.assetRepository.upsertJobStatus({ assetId: id, ocrAt: new Date() }); @@ -64,7 +65,9 @@ export class OcrService extends BaseService { private parseOcrResults(id: string, { box, boxScore, text, textScore }: OCR) { const ocrDataList = []; + const searchTokens = []; for (let i = 0; i < text.length; i++) { + const rawText = text[i]; const boxOffset = i * 8; ocrDataList.push({ assetId: id, @@ -78,9 +81,11 @@ export class OcrService extends BaseService { y4: box[boxOffset + 7], boxScore: boxScore[i], textScore: textScore[i], - text: text[i], + text: rawText, }); + searchTokens.push(...tokenizeForSearch(rawText)); } - return ocrDataList; + + return { ocrDataList, searchText: searchTokens.join(' ') }; } } diff --git a/server/src/services/plugin.service.ts b/server/src/services/plugin.service.ts index 28d1ac56ca..9336f0003a 100644 --- a/server/src/services/plugin.service.ts +++ b/server/src/services/plugin.service.ts @@ -247,9 +247,9 @@ export class PluginService extends BaseService { private async executeFilters(workflowFilters: WorkflowFilter[], context: WorkflowContext): Promise { for (const workflowFilter of workflowFilters) { - const filter = await this.pluginRepository.getFilter(workflowFilter.filterId); + const filter = await this.pluginRepository.getFilter(workflowFilter.pluginFilterId); if (!filter) { - this.logger.error(`Filter ${workflowFilter.filterId} not found`); + this.logger.error(`Filter ${workflowFilter.pluginFilterId} not found`); return false; } @@ -291,9 +291,9 @@ export class PluginService extends BaseService { private async executeActions(workflowActions: WorkflowAction[], context: WorkflowContext): Promise { for (const workflowAction of workflowActions) { - const action = await this.pluginRepository.getAction(workflowAction.actionId); + const action = await this.pluginRepository.getAction(workflowAction.pluginActionId); if (!action) { - throw new Error(`Action ${workflowAction.actionId} not found`); + throw new Error(`Action ${workflowAction.pluginActionId} not found`); } const pluginInstance = this.loadedPlugins.get(action.pluginId); diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 1d38bf7011..864207bf05 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -6,10 +6,20 @@ import sanitize from 'sanitize-filename'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent, OnJob } from 'src/decorators'; import { SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto'; -import { AssetPathType, AssetType, DatabaseLock, JobName, JobStatus, QueueName, StorageFolder } from 'src/enum'; +import { + AssetFileType, + AssetPathType, + AssetType, + DatabaseLock, + JobName, + JobStatus, + QueueName, + StorageFolder, +} from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; import { JobOf, StorageAsset } from 'src/types'; +import { getAssetFile } from 'src/utils/asset.util'; import { getLivePhotoMotionFilename } from 'src/utils/file'; const storageTokens = { @@ -196,7 +206,7 @@ export class StorageTemplateService extends BaseService { } return this.databaseRepository.withLock(DatabaseLock.StorageTemplateMigration, async () => { - const { id, sidecarPath, originalPath, checksum, fileSizeInByte } = asset; + const { id, originalPath, checksum, fileSizeInByte } = asset; const oldPath = originalPath; const newPath = await this.getTemplatePath(asset, metadata); @@ -213,6 +223,8 @@ export class StorageTemplateService extends BaseService { newPath, assetInfo: { sizeInBytes: fileSizeInByte, checksum }, }); + + const sidecarPath = getAssetFile(asset.files, AssetFileType.Sidecar)?.path; if (sidecarPath) { await this.storageCore.moveFile({ entityId: id, diff --git a/server/src/services/workflow.service.ts b/server/src/services/workflow.service.ts index ae72187d7d..301931421f 100644 --- a/server/src/services/workflow.service.ts +++ b/server/src/services/workflow.service.ts @@ -78,13 +78,13 @@ export class WorkflowService extends BaseService { } private async validateAndMapFilters( - filters: Array<{ filterId: string; filterConfig?: any }>, + filters: Array<{ pluginFilterId: string; filterConfig?: any }>, requiredContext: PluginContext, ) { for (const dto of filters) { - const filter = await this.pluginRepository.getFilter(dto.filterId); + const filter = await this.pluginRepository.getFilter(dto.pluginFilterId); if (!filter) { - throw new BadRequestException(`Invalid filter ID: ${dto.filterId}`); + throw new BadRequestException(`Invalid filter ID: ${dto.pluginFilterId}`); } if (!filter.supportedContexts.includes(requiredContext)) { @@ -95,20 +95,20 @@ export class WorkflowService extends BaseService { } return filters.map((dto, index) => ({ - filterId: dto.filterId, + pluginFilterId: dto.pluginFilterId, filterConfig: dto.filterConfig || null, order: index, })); } private async validateAndMapActions( - actions: Array<{ actionId: string; actionConfig?: any }>, + actions: Array<{ pluginActionId: string; actionConfig?: any }>, requiredContext: PluginContext, ) { for (const dto of actions) { - const action = await this.pluginRepository.getAction(dto.actionId); + const action = await this.pluginRepository.getAction(dto.pluginActionId); if (!action) { - throw new BadRequestException(`Invalid action ID: ${dto.actionId}`); + throw new BadRequestException(`Invalid action ID: ${dto.pluginActionId}`); } if (!action.supportedContexts.includes(requiredContext)) { throw new BadRequestException( @@ -118,7 +118,7 @@ export class WorkflowService extends BaseService { } return actions.map((dto, index) => ({ - actionId: dto.actionId, + pluginActionId: dto.pluginActionId, actionConfig: dto.actionConfig || null, order: index, })); diff --git a/server/src/types.ts b/server/src/types.ts index fd9ed84e65..cd91f5df5f 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -1,11 +1,10 @@ import { SystemConfig } from 'src/config'; import { VECTOR_EXTENSIONS } from 'src/constants'; -import { Asset } from 'src/database'; +import { Asset, AssetFile } from 'src/database'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { SetMaintenanceModeDto } from 'src/dtos/maintenance.dto'; import { - AssetMetadataKey, AssetOrder, AssetType, DatabaseSslMode, @@ -477,8 +476,8 @@ export type StorageAsset = { fileCreatedAt: Date; originalPath: string; originalFileName: string; - sidecarPath: string | null; fileSizeInByte: number | null; + files: AssetFile[]; }; export type OnThisDayData = { year: number }; @@ -566,12 +565,3 @@ export interface UserMetadata extends Record = { - key: T; - value: AssetMetadata[T]; -}; - -export interface AssetMetadata extends Record> { - [AssetMetadataKey.MobileApp]: { iCloudId: string }; -} diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index 629b3bf819..f3f807c829 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -21,6 +21,7 @@ export const getAssetFiles = (files: AssetFile[]) => ({ fullsizeFile: getAssetFile(files, AssetFileType.FullSize), previewFile: getAssetFile(files, AssetFileType.Preview), thumbnailFile: getAssetFile(files, AssetFileType.Thumbnail), + sidecarFile: getAssetFile(files, AssetFileType.Sidecar), }); export const addAssets = async ( diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 0cc3788f1a..f8dbd5e78c 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -306,6 +306,46 @@ export function withTagId(qb: SelectQueryBuilder, tagId: stri ); } +const isCJK = (c: number): boolean => + (c >= 0x4e_00 && c <= 0x9f_ff) || + (c >= 0xac_00 && c <= 0xd7_af) || + (c >= 0x30_40 && c <= 0x30_9f) || + (c >= 0x30_a0 && c <= 0x30_ff) || + (c >= 0x34_00 && c <= 0x4d_bf); + +export const tokenizeForSearch = (text: string): string[] => { + /* eslint-disable unicorn/prefer-code-point */ + const tokens: string[] = []; + let i = 0; + while (i < text.length) { + const c = text.charCodeAt(i); + if (c <= 32) { + i++; + continue; + } + + const start = i; + if (isCJK(c)) { + while (i < text.length && isCJK(text.charCodeAt(i))) { + i++; + } + if (i - start === 1) { + tokens.push(text[start]); + } else { + for (let k = start; k < i - 1; k++) { + tokens.push(text[k] + text[k + 1]); + } + } + } else { + while (i < text.length && text.charCodeAt(i) > 32 && !isCJK(text.charCodeAt(i))) { + i++; + } + tokens.push(text.slice(start, i)); + } + } + return tokens; +}; + const joinDeduplicationPlugin = new DeduplicateJoinsPlugin(); /** TODO: This should only be used for search-related queries, not as a general purpose query builder */ @@ -391,7 +431,7 @@ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuild .$if(!!options.ocr, (qb) => qb .innerJoin('ocr_search', 'asset.id', 'ocr_search.assetId') - .where(() => sql`f_unaccent(ocr_search.text) %>> f_unaccent(${options.ocr!})`), + .where(() => sql`f_unaccent(ocr_search.text) %>> f_unaccent(${tokenizeForSearch(options.ocr!).join(' ')})`), ) .$if(!!options.type, (qb) => qb.where('asset.type', '=', options.type!)) .$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!)) diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 0fd2189268..f5935d5d0e 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -24,6 +24,18 @@ const fullsizeFile: AssetFile = { path: '/uploads/user-id/fullsize/path.webp', }; +const sidecarFileWithExt: AssetFile = { + id: 'sidecar-with-ext', + type: AssetFileType.Sidecar, + path: '/original/path.ext.xmp', +}; + +const sidecarFileWithoutExt: AssetFile = { + id: 'sidecar-without-ext', + type: AssetFileType.Sidecar, + path: '/original/path.xmp', +}; + const files: AssetFile[] = [fullsizeFile, previewFile, thumbnailFile]; export const stackStub = (stackId: string, assets: (MapAsset & { exifInfo: Exif })[]) => { @@ -51,8 +63,8 @@ export const assetStub = { fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), originalPath: '/original/path.jpg', originalFileName: 'IMG_123.jpg', - sidecarPath: null, fileSizeInByte: 12_345, + files: [], ...asset, }), noResizePath: Object.freeze({ @@ -81,7 +93,6 @@ export const assetStub = { sharedLinks: [], faces: [], exifInfo: {} as Exif, - sidecarPath: null, deletedAt: null, isExternal: false, duplicateId: null, @@ -117,7 +128,6 @@ export const assetStub = { sharedLinks: [], originalFileName: 'IMG_456.jpg', faces: [], - sidecarPath: null, isExternal: false, exifInfo: { fileSizeInByte: 123_000, @@ -157,7 +167,6 @@ export const assetStub = { sharedLinks: [], originalFileName: 'asset-id.ext', faces: [], - sidecarPath: null, deletedAt: null, duplicateId: null, isOffline: false, @@ -194,7 +203,6 @@ export const assetStub = { originalFileName: 'asset-id.jpg', faces: [], deletedAt: null, - sidecarPath: null, exifInfo: { fileSizeInByte: 5000, exifImageHeight: 1000, @@ -243,7 +251,6 @@ export const assetStub = { originalFileName: 'asset-id.jpg', faces: [], deletedAt: null, - sidecarPath: null, exifInfo: { fileSizeInByte: 5000, exifImageHeight: 3840, @@ -285,7 +292,6 @@ export const assetStub = { sharedLinks: [], originalFileName: 'asset-id.jpg', faces: [], - sidecarPath: null, exifInfo: { fileSizeInByte: 5000, exifImageHeight: 3840, @@ -328,7 +334,6 @@ export const assetStub = { sharedLinks: [], originalFileName: 'asset-id.jpg', faces: [], - sidecarPath: null, exifInfo: { fileSizeInByte: 5000, exifImageHeight: 3840, @@ -367,7 +372,6 @@ export const assetStub = { originalFileName: 'asset-id.jpg', faces: [], deletedAt: null, - sidecarPath: null, exifInfo: { fileSizeInByte: 5000, exifImageHeight: 3840, @@ -409,7 +413,6 @@ export const assetStub = { originalFileName: 'asset-id.jpg', faces: [], deletedAt: null, - sidecarPath: null, exifInfo: { fileSizeInByte: 5000, } as Exif, @@ -448,7 +451,6 @@ export const assetStub = { sharedLinks: [], originalFileName: 'asset-id.ext', faces: [], - sidecarPath: null, exifInfo: { fileSizeInByte: 5000, } as Exif, @@ -490,7 +492,6 @@ export const assetStub = { sharedLinks: [], originalFileName: 'asset-id.ext', faces: [], - sidecarPath: null, exifInfo: { fileSizeInByte: 5000, } as Exif, @@ -526,7 +527,6 @@ export const assetStub = { livePhotoVideoId: null, sharedLinks: [], faces: [], - sidecarPath: null, exifInfo: { fileSizeInByte: 100_000, exifImageHeight: 2160, @@ -553,6 +553,7 @@ export const assetStub = { fileSizeInByte: 100_000, timeZone: `America/New_York`, }, + files: [] as AssetFile[], libraryId: null, visibility: AssetVisibility.Hidden, } as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif }), @@ -573,7 +574,7 @@ export const assetStub = { files, faces: [] as AssetFace[], visibility: AssetVisibility.Timeline, - } as MapAsset & { faces: AssetFace[] }), + } as MapAsset & { faces: AssetFace[]; files: AssetFile[] }), livePhotoWithOriginalFileName: Object.freeze({ id: 'live-photo-still-asset', @@ -589,10 +590,11 @@ export const assetStub = { fileSizeInByte: 25_000, timeZone: `America/New_York`, }, + files: [] as AssetFile[], libraryId: null, faces: [] as AssetFace[], visibility: AssetVisibility.Timeline, - } as MapAsset & { faces: AssetFace[] }), + } as MapAsset & { faces: AssetFace[]; files: AssetFile[] }), withLocation: Object.freeze({ id: 'asset-with-favorite-id', @@ -605,7 +607,6 @@ export const assetStub = { deviceId: 'device-id', checksum: Buffer.from('file hash', 'utf8'), originalPath: '/original/path.ext', - sidecarPath: null, type: AssetType.Image, files: [previewFile], thumbhash: null, @@ -652,7 +653,7 @@ export const assetStub = { thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.Image, - files: [previewFile], + files: [previewFile, sidecarFileWithExt], encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -665,7 +666,6 @@ export const assetStub = { sharedLinks: [], originalFileName: 'asset-id.ext', faces: [], - sidecarPath: '/original/path.ext.xmp', deletedAt: null, duplicateId: null, isOffline: false, @@ -688,7 +688,7 @@ export const assetStub = { thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.Image, - files: [previewFile], + files: [previewFile, sidecarFileWithoutExt], encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -701,7 +701,6 @@ export const assetStub = { sharedLinks: [], originalFileName: 'asset-id.ext', faces: [], - sidecarPath: '/original/path.xmp', deletedAt: null, duplicateId: null, isOffline: false, @@ -734,7 +733,6 @@ export const assetStub = { livePhotoVideoId: null, sharedLinks: [], faces: [], - sidecarPath: null, exifInfo: { fileSizeInByte: 100_000, } as Exif, @@ -776,7 +774,6 @@ export const assetStub = { originalFileName: 'photo.jpg', faces: [], deletedAt: null, - sidecarPath: null, exifInfo: { fileSizeInByte: 5000, } as Exif, @@ -812,7 +809,6 @@ export const assetStub = { originalFileName: 'asset-id.dng', faces: [], deletedAt: null, - sidecarPath: null, exifInfo: { fileSizeInByte: 5000, profileDescription: 'Adobe RGB', @@ -853,7 +849,6 @@ export const assetStub = { originalFileName: 'asset-id.hif', faces: [], deletedAt: null, - sidecarPath: null, exifInfo: { fileSizeInByte: 5000, profileDescription: 'Adobe RGB', diff --git a/server/test/medium/specs/services/asset.service.spec.ts b/server/test/medium/specs/services/asset.service.spec.ts index b3fc481708..e9246c62b1 100644 --- a/server/test/medium/specs/services/asset.service.spec.ts +++ b/server/test/medium/specs/services/asset.service.spec.ts @@ -1,5 +1,5 @@ import { Kysely } from 'kysely'; -import { JobName, SharedLinkType } from 'src/enum'; +import { AssetFileType, JobName, SharedLinkType } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; import { AlbumRepository } from 'src/repositories/album.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; @@ -184,7 +184,15 @@ describe(AssetService.name, () => { jobRepo.queue.mockResolvedValue(); const { user } = await ctx.newUser(); - const { asset: oldAsset } = await ctx.newAsset({ ownerId: user.id, sidecarPath: '/path/to/my/sidecar.xmp' }); + + const { asset: oldAsset } = await ctx.newAsset({ ownerId: user.id }); + + await ctx.newAssetFile({ + assetId: oldAsset.id, + path: '/path/to/my/sidecar.xmp', + type: AssetFileType.Sidecar, + }); + const { asset: newAsset } = await ctx.newAsset({ ownerId: user.id }); await ctx.newExif({ assetId: oldAsset.id, description: 'foo' }); diff --git a/server/test/medium/specs/services/metadata.service.spec.ts b/server/test/medium/specs/services/metadata.service.spec.ts index 5d44079be5..1d144e9c9c 100644 --- a/server/test/medium/specs/services/metadata.service.spec.ts +++ b/server/test/medium/specs/services/metadata.service.spec.ts @@ -82,7 +82,11 @@ describe(MetadataService.name, () => { process.env.TZ = serverTimeZone ?? undefined; const { filePath } = await createTestFile(exifData); - mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ id: 'asset-1', originalPath: filePath } as any); + mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ + id: 'asset-1', + originalPath: filePath, + files: [], + } as any); await sut.handleMetadataExtraction({ id: 'asset-1' }); diff --git a/server/test/medium/specs/services/workflow.service.spec.ts b/server/test/medium/specs/services/workflow.service.spec.ts index aaf1c8b9ec..1fddc2a7cf 100644 --- a/server/test/medium/specs/services/workflow.service.spec.ts +++ b/server/test/medium/specs/services/workflow.service.spec.ts @@ -113,13 +113,13 @@ describe(WorkflowService.name, () => { enabled: true, filters: [ { - filterId: testFilterId, + pluginFilterId: testFilterId, filterConfig: { key: 'value' }, }, ], actions: [ { - actionId: testActionId, + pluginActionId: testActionId, actionConfig: { action: 'test' }, }, ], @@ -137,7 +137,7 @@ describe(WorkflowService.name, () => { expect(workflow.filters[0]).toMatchObject({ id: expect.any(String), workflowId: workflow.id, - filterId: testFilterId, + pluginFilterId: testFilterId, filterConfig: { key: 'value' }, order: 0, }); @@ -146,7 +146,7 @@ describe(WorkflowService.name, () => { expect(workflow.actions[0]).toMatchObject({ id: expect.any(String), workflowId: workflow.id, - actionId: testActionId, + pluginActionId: testActionId, actionConfig: { action: 'test' }, order: 0, }); @@ -163,7 +163,7 @@ describe(WorkflowService.name, () => { name: 'invalid-workflow', description: 'A workflow with invalid filter', enabled: true, - filters: [{ filterId: factory.uuid(), filterConfig: { key: 'value' } }], + filters: [{ pluginFilterId: factory.uuid(), filterConfig: { key: 'value' } }], actions: [], }), ).rejects.toThrow('Invalid filter ID'); @@ -181,7 +181,7 @@ describe(WorkflowService.name, () => { description: 'A workflow with invalid action', enabled: true, filters: [], - actions: [{ actionId: factory.uuid(), actionConfig: { action: 'test' } }], + actions: [{ pluginActionId: factory.uuid(), actionConfig: { action: 'test' } }], }), ).rejects.toThrow('Invalid action ID'); }); @@ -220,7 +220,7 @@ describe(WorkflowService.name, () => { name: 'invalid-context-workflow', description: 'A workflow with context mismatch', enabled: true, - filters: [{ filterId: result.filters[0].id }], + filters: [{ pluginFilterId: result.filters[0].id }], actions: [], }), ).rejects.toThrow('does not support asset context'); @@ -261,7 +261,7 @@ describe(WorkflowService.name, () => { description: 'A workflow with context mismatch', enabled: true, filters: [], - actions: [{ actionId: result.actions[0].id }], + actions: [{ pluginActionId: result.actions[0].id }], }), ).rejects.toThrow('does not support asset context'); }); @@ -277,13 +277,13 @@ describe(WorkflowService.name, () => { description: 'A workflow with multiple filters and actions', enabled: true, filters: [ - { filterId: testFilterId, filterConfig: { step: 1 } }, - { filterId: testFilterId, filterConfig: { step: 2 } }, + { pluginFilterId: testFilterId, filterConfig: { step: 1 } }, + { pluginFilterId: testFilterId, filterConfig: { step: 2 } }, ], actions: [ - { actionId: testActionId, actionConfig: { step: 1 } }, - { actionId: testActionId, actionConfig: { step: 2 } }, - { actionId: testActionId, actionConfig: { step: 3 } }, + { pluginActionId: testActionId, actionConfig: { step: 1 } }, + { pluginActionId: testActionId, actionConfig: { step: 2 } }, + { pluginActionId: testActionId, actionConfig: { step: 3 } }, ], }); @@ -378,8 +378,8 @@ describe(WorkflowService.name, () => { name: 'test-workflow', description: 'A test workflow', enabled: true, - filters: [{ filterId: testFilterId, filterConfig: { key: 'value' } }], - actions: [{ actionId: testActionId, actionConfig: { action: 'test' } }], + filters: [{ pluginFilterId: testFilterId, filterConfig: { key: 'value' } }], + actions: [{ pluginActionId: testActionId, actionConfig: { action: 'test' } }], }); const workflow = await sut.get(auth, created.id); @@ -461,14 +461,14 @@ describe(WorkflowService.name, () => { name: 'test-workflow', description: 'Test', enabled: true, - filters: [{ filterId: testFilterId, filterConfig: { old: 'config' } }], + filters: [{ pluginFilterId: testFilterId, filterConfig: { old: 'config' } }], actions: [], }); const updated = await sut.update(auth, created.id, { filters: [ - { filterId: testFilterId, filterConfig: { new: 'config' } }, - { filterId: testFilterId, filterConfig: { second: 'filter' } }, + { pluginFilterId: testFilterId, filterConfig: { new: 'config' } }, + { pluginFilterId: testFilterId, filterConfig: { second: 'filter' } }, ], }); @@ -488,13 +488,13 @@ describe(WorkflowService.name, () => { description: 'Test', enabled: true, filters: [], - actions: [{ actionId: testActionId, actionConfig: { old: 'config' } }], + actions: [{ pluginActionId: testActionId, actionConfig: { old: 'config' } }], }); const updated = await sut.update(auth, created.id, { actions: [ - { actionId: testActionId, actionConfig: { new: 'config' } }, - { actionId: testActionId, actionConfig: { second: 'action' } }, + { pluginActionId: testActionId, actionConfig: { new: 'config' } }, + { pluginActionId: testActionId, actionConfig: { second: 'action' } }, ], }); @@ -513,7 +513,7 @@ describe(WorkflowService.name, () => { name: 'test-workflow', description: 'Test', enabled: true, - filters: [{ filterId: testFilterId, filterConfig: { key: 'value' } }], + filters: [{ pluginFilterId: testFilterId, filterConfig: { key: 'value' } }], actions: [], }); @@ -588,7 +588,7 @@ describe(WorkflowService.name, () => { await expect( sut.update(auth, created.id, { - filters: [{ filterId: factory.uuid(), filterConfig: {} }], + filters: [{ pluginFilterId: factory.uuid(), filterConfig: {} }], }), ).rejects.toThrow(); }); @@ -608,7 +608,7 @@ describe(WorkflowService.name, () => { }); await expect( - sut.update(auth, created.id, { actions: [{ actionId: factory.uuid(), actionConfig: {} }] }), + sut.update(auth, created.id, { actions: [{ pluginActionId: factory.uuid(), actionConfig: {} }] }), ).rejects.toThrow(); }); }); @@ -643,8 +643,8 @@ describe(WorkflowService.name, () => { name: 'test-workflow', description: 'Test', enabled: true, - filters: [{ filterId: testFilterId, filterConfig: {} }], - actions: [{ actionId: testActionId, actionConfig: {} }], + filters: [{ pluginFilterId: testFilterId, filterConfig: {} }], + actions: [{ pluginActionId: testActionId, actionConfig: {} }], }); await sut.delete(auth, workflow.id); diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index e735b37564..5ba77ddc2f 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -10,6 +10,7 @@ export const newAssetRepositoryMock = (): Mocked = {}) => ({ originalFileName: 'IMG_123.jpg', originalPath: `/data/12/34/IMG_123.jpg`, ownerId: newUuid(), - sidecarPath: null, stackId: null, thumbhash: null, type: AssetType.Image, @@ -312,12 +319,17 @@ const versionHistoryFactory = () => ({ version: '1.123.45', }); -const assetSidecarWriteFactory = (asset: Partial = {}) => ({ +const assetSidecarWriteFactory = () => ({ id: newUuid(), - sidecarPath: '/path/to/original-path.jpg.xmp', originalPath: '/path/to/original-path.jpg.xmp', tags: [], - ...asset, + files: [ + { + id: newUuid(), + path: '/path/to/original-path.jpg.xmp', + type: AssetFileType.Sidecar, + }, + ], }); const assetOcrFactory = ( diff --git a/web/package.json b/web/package.json index c44dd6d4b7..72be145acb 100644 --- a/web/package.json +++ b/web/package.json @@ -69,7 +69,7 @@ "@koddsson/eslint-plugin-tscompat": "^0.2.0", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.8", - "@sveltejs/enhanced-img": "^0.8.0", + "@sveltejs/enhanced-img": "^0.9.0", "@sveltejs/kit": "^2.27.1", "@sveltejs/vite-plugin-svelte": "6.2.1", "@tailwindcss/vite": "^4.1.7", diff --git a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte index 108714348f..32622a1547 100644 --- a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte @@ -2,13 +2,14 @@ import StorageTemplateSettings from '$lib/components/admin-settings/StorageTemplateSettings.svelte'; import FormatMessage from '$lib/elements/FormatMessage.svelte'; import { user } from '$lib/stores/user.store'; + import { Link } from '@immich/ui';

{#snippet children({ message })} - {message} + {message} {/snippet}

diff --git a/web/src/lib/components/shared-components/map/map.svelte b/web/src/lib/components/shared-components/map/map.svelte index 063d747750..e3a926f9b6 100644 --- a/web/src/lib/components/shared-components/map/map.svelte +++ b/web/src/lib/components/shared-components/map/map.svelte @@ -376,7 +376,7 @@ } }} > - {#snippet children({ feature }: { feature: Feature })} + {#snippet children({ feature }: { feature: Feature })} {#if useLocationPin} {:else} diff --git a/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte b/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte index d5b1d2ecf6..b731635355 100644 --- a/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte +++ b/web/src/lib/components/timeline/actions/TimelineKeyboardActions.svelte @@ -80,10 +80,7 @@ const toggleArchive = async () => { const visibility = assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive; const ids = await archiveAssets(assetInteraction.selectedAssets, visibility); - timelineManager.updateAssetOperation(ids, (asset) => { - asset.visibility = visibility; - return { remove: false }; - }); + timelineManager.update(ids, (asset) => (asset.visibility = visibility)); deselectAllAssets(); }; diff --git a/web/src/lib/managers/timeline-manager/day-group.svelte.ts b/web/src/lib/managers/timeline-manager/day-group.svelte.ts index 934ca1d4ff..e21e54a6e5 100644 --- a/web/src/lib/managers/timeline-manager/day-group.svelte.ts +++ b/web/src/lib/managers/timeline-manager/day-group.svelte.ts @@ -6,7 +6,7 @@ import { plainDateTimeCompare } from '$lib/utils/timeline-util'; import { SvelteSet } from 'svelte/reactivity'; import type { MonthGroup } from './month-group.svelte'; -import type { AssetOperation, Direction, MoveAsset, TimelineAsset } from './types'; +import type { Direction, MoveAsset, TimelineAsset } from './types'; import { ViewerAsset } from './viewer-asset.svelte'; export class DayGroup { @@ -101,7 +101,7 @@ export class DayGroup { return this.viewerAssets.map((viewerAsset) => viewerAsset.asset); } - runAssetOperation(ids: Set, operation: AssetOperation) { + runAssetCallback(ids: Set, callback: (asset: TimelineAsset) => void | { remove?: boolean }) { if (ids.size === 0) { return { moveAssets: [] as MoveAsset[], @@ -122,7 +122,8 @@ export class DayGroup { const asset = this.viewerAssets[index].asset!; const oldTime = { ...asset.localDateTime }; - let { remove } = operation(asset); + const callbackResult = callback(asset); + let remove = (callbackResult as { remove?: boolean } | undefined)?.remove ?? false; const newTime = asset.localDateTime; if (oldTime.year !== newTime.year || oldTime.month !== newTime.month || oldTime.day !== newTime.day) { const { year, month, day } = newTime; diff --git a/web/src/lib/managers/timeline-manager/internal/operations-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/operations-support.svelte.ts deleted file mode 100644 index 4bc99c0315..0000000000 --- a/web/src/lib/managers/timeline-manager/internal/operations-support.svelte.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { setDifference, type TimelineDate } from '$lib/utils/timeline-util'; -import { AssetOrder } from '@immich/sdk'; - -import { SvelteSet } from 'svelte/reactivity'; -import { GroupInsertionCache } from '../group-insertion-cache.svelte'; -import { MonthGroup } from '../month-group.svelte'; -import type { TimelineManager } from '../timeline-manager.svelte'; -import type { AssetOperation, TimelineAsset } from '../types'; -import { updateGeometry } from './layout-support.svelte'; -import { getMonthGroupByDate } from './search-support.svelte'; - -export function addAssetsToMonthGroups( - timelineManager: TimelineManager, - assets: TimelineAsset[], - options: { order: AssetOrder }, -) { - if (assets.length === 0) { - return; - } - - const addContext = new GroupInsertionCache(); - const updatedMonthGroups = new SvelteSet(); - const monthCount = timelineManager.months.length; - for (const asset of assets) { - let month = getMonthGroupByDate(timelineManager, asset.localDateTime); - - if (!month) { - month = new MonthGroup(timelineManager, asset.localDateTime, 1, options.order); - month.isLoaded = true; - timelineManager.months.push(month); - } - - month.addTimelineAsset(asset, addContext); - updatedMonthGroups.add(month); - } - - if (timelineManager.months.length !== monthCount) { - timelineManager.months.sort((a, b) => { - return a.yearMonth.year === b.yearMonth.year - ? b.yearMonth.month - a.yearMonth.month - : b.yearMonth.year - a.yearMonth.year; - }); - } - - for (const group of addContext.existingDayGroups) { - group.sortAssets(options.order); - } - - for (const monthGroup of addContext.bucketsWithNewDayGroups) { - monthGroup.sortDayGroups(); - } - - for (const month of addContext.updatedBuckets) { - month.sortDayGroups(); - updateGeometry(timelineManager, month, { invalidateHeight: true }); - } - timelineManager.updateIntersections(); -} - -export function runAssetOperation( - timelineManager: TimelineManager, - ids: Set, - operation: AssetOperation, - options: { order: AssetOrder }, -) { - if (ids.size === 0) { - return { processedIds: new SvelteSet(), unprocessedIds: ids, changedGeometry: false }; - } - - const changedMonthGroups = new SvelteSet(); - let idsToProcess = new SvelteSet(ids); - const idsProcessed = new SvelteSet(); - const combinedMoveAssets: { asset: TimelineAsset; date: TimelineDate }[][] = []; - for (const month of timelineManager.months) { - if (idsToProcess.size > 0) { - const { moveAssets, processedIds, changedGeometry } = month.runAssetOperation(idsToProcess, operation); - if (moveAssets.length > 0) { - combinedMoveAssets.push(moveAssets); - } - idsToProcess = setDifference(idsToProcess, processedIds); - for (const id of processedIds) { - idsProcessed.add(id); - } - if (changedGeometry) { - changedMonthGroups.add(month); - } - } - } - if (combinedMoveAssets.length > 0) { - addAssetsToMonthGroups( - timelineManager, - combinedMoveAssets.flat().map((a) => a.asset), - options, - ); - } - const changedGeometry = changedMonthGroups.size > 0; - for (const month of changedMonthGroups) { - updateGeometry(timelineManager, month, { invalidateHeight: true }); - } - if (changedGeometry) { - timelineManager.updateIntersections(); - } - return { unprocessedIds: idsToProcess, processedIds: idsProcessed, changedGeometry }; -} diff --git a/web/src/lib/managers/timeline-manager/month-group.svelte.ts b/web/src/lib/managers/timeline-manager/month-group.svelte.ts index 1d9e1bbaa7..3926055cca 100644 --- a/web/src/lib/managers/timeline-manager/month-group.svelte.ts +++ b/web/src/lib/managers/timeline-manager/month-group.svelte.ts @@ -21,7 +21,7 @@ import { SvelteSet } from 'svelte/reactivity'; import { DayGroup } from './day-group.svelte'; import { GroupInsertionCache } from './group-insertion-cache.svelte'; import type { TimelineManager } from './timeline-manager.svelte'; -import type { AssetDescriptor, AssetOperation, Direction, MoveAsset, TimelineAsset } from './types'; +import type { AssetDescriptor, Direction, MoveAsset, TimelineAsset } from './types'; import { ViewerAsset } from './viewer-asset.svelte'; export class MonthGroup { @@ -50,12 +50,13 @@ export class MonthGroup { readonly yearMonth: TimelineYearMonth; constructor( - store: TimelineManager, + timelineManager: TimelineManager, yearMonth: TimelineYearMonth, initialCount: number, + loaded: boolean, order: AssetOrder = AssetOrder.Desc, ) { - this.timelineManager = store; + this.timelineManager = timelineManager; this.#initialCount = initialCount; this.#sortOrder = order; @@ -72,6 +73,9 @@ export class MonthGroup { }, this.#handleLoadError, ); + if (loaded) { + this.isLoaded = true; + } } set intersecting(newValue: boolean) { @@ -112,7 +116,7 @@ export class MonthGroup { return this.dayGroups.sort((a, b) => b.day - a.day); } - runAssetOperation(ids: Set, operation: AssetOperation) { + runAssetCallback(ids: Set, callback: (asset: TimelineAsset) => void | { remove?: boolean }) { if (ids.size === 0) { return { moveAssets: [] as MoveAsset[], @@ -130,7 +134,7 @@ export class MonthGroup { while (index--) { if (idsToProcess.size > 0) { const group = dayGroups[index]; - const { moveAssets, processedIds, changedGeometry } = group.runAssetOperation(ids, operation); + const { moveAssets, processedIds, changedGeometry } = group.runAssetCallback(ids, callback); if (moveAssets.length > 0) { combinedMoveAssets.push(moveAssets); } diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts index 62053f7a0d..bb58704214 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts @@ -278,10 +278,11 @@ describe('TimelineManager', () => { }); it('updates existing asset', () => { + const updateAssetsSpy = vi.spyOn(timelineManager, 'upsertAssets'); const asset = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build()); timelineManager.upsertAssets([asset]); - timelineManager.upsertAssets([asset]); + expect(updateAssetsSpy).toBeCalledWith([asset]); expect(timelineManager.assetCount).toEqual(1); }); @@ -691,4 +692,42 @@ describe('TimelineManager', () => { expect(discoveredAssets.size).toBe(assetCount); }); }); + + describe('showAssetOwners', () => { + const LS_KEY = 'album-show-asset-owners'; + + beforeEach(() => { + // ensure clean state + globalThis.localStorage?.removeItem(LS_KEY); + }); + + it('defaults to false', () => { + const timelineManager = new TimelineManager(); + expect(timelineManager.showAssetOwners).toBe(false); + }); + + it('setShowAssetOwners updates value', () => { + const timelineManager = new TimelineManager(); + timelineManager.setShowAssetOwners(true); + expect(timelineManager.showAssetOwners).toBe(true); + timelineManager.setShowAssetOwners(false); + expect(timelineManager.showAssetOwners).toBe(false); + }); + + it('toggleShowAssetOwners flips value', () => { + const timelineManager = new TimelineManager(); + expect(timelineManager.showAssetOwners).toBe(false); + timelineManager.toggleShowAssetOwners(); + expect(timelineManager.showAssetOwners).toBe(true); + timelineManager.toggleShowAssetOwners(); + expect(timelineManager.showAssetOwners).toBe(false); + }); + + it('persists across instances via localStorage', () => { + const a = new TimelineManager(); + a.setShowAssetOwners(true); + const b = new TimelineManager(); + expect(b.showAssetOwners).toBe(true); + }); + }); }); diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts index e3327663b4..feba73a0f8 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts @@ -1,12 +1,9 @@ import { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte'; +import { GroupInsertionCache } from '$lib/managers/timeline-manager/group-insertion-cache.svelte'; import { updateIntersectionMonthGroup } from '$lib/managers/timeline-manager/internal/intersection-support.svelte'; import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte'; import { loadFromTimeBuckets } from '$lib/managers/timeline-manager/internal/load-support.svelte'; -import { - addAssetsToMonthGroups, - runAssetOperation, -} from '$lib/managers/timeline-manager/internal/operations-support.svelte'; import { findClosestGroupForDate, findMonthGroupForAsset as findMonthGroupForAssetUtil, @@ -17,17 +14,23 @@ import { } from '$lib/managers/timeline-manager/internal/search-support.svelte'; import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websocket-support.svelte'; import { CancellableTask } from '$lib/utils/cancellable-task'; -import { toTimelineAsset, type TimelineDateTime, type TimelineYearMonth } from '$lib/utils/timeline-util'; +import { PersistedLocalStorage } from '$lib/utils/persisted'; +import { + setDifference, + toTimelineAsset, + type TimelineDateTime, + type TimelineYearMonth, +} from '$lib/utils/timeline-util'; import { AssetOrder, getAssetInfo, getTimeBuckets } from '@immich/sdk'; import { clamp, isEqual } from 'lodash-es'; -import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity'; +import { SvelteDate, SvelteSet } from 'svelte/reactivity'; import { DayGroup } from './day-group.svelte'; import { isMismatched, updateObject } from './internal/utils.svelte'; import { MonthGroup } from './month-group.svelte'; import type { AssetDescriptor, - AssetOperation, Direction, + MoveAsset, ScrubberMonth, TimelineAsset, TimelineManagerOptions, @@ -88,6 +91,19 @@ export class TimelineManager extends VirtualScrollManager { #options: TimelineManagerOptions = TimelineManager.#INIT_OPTIONS; #updatingIntersections = false; #scrollableElement: HTMLElement | undefined = $state(); + #showAssetOwners = new PersistedLocalStorage('album-show-asset-owners', false); + + get showAssetOwners() { + return this.#showAssetOwners.current; + } + + setShowAssetOwners(value: boolean) { + this.#showAssetOwners.current = value; + } + + toggleShowAssetOwners() { + this.#showAssetOwners.current = !this.#showAssetOwners.current; + } constructor() { super(); @@ -218,6 +234,7 @@ export class TimelineManager extends VirtualScrollManager { this, { year: date.getUTCFullYear(), month: date.getUTCMonth() + 1 }, timeBucket.count, + false, this.#options.order, ); }); @@ -323,7 +340,7 @@ export class TimelineManager extends VirtualScrollManager { upsertAssets(assets: TimelineAsset[]) { const notUpdated = this.#updateAssets(assets); const notExcluded = notUpdated.filter((asset) => !this.isExcluded(asset)); - addAssetsToMonthGroups(this, [...notExcluded], { order: this.#options.order ?? AssetOrder.Desc }); + this.addAssetsUpsertSegments([...notExcluded]); } async findMonthGroupForAsset(id: string) { @@ -400,38 +417,107 @@ export class TimelineManager extends VirtualScrollManager { return randomDay.viewerAssets[randomAssetIndex - accumulatedCount].asset; } - updateAssetOperation(ids: string[], operation: AssetOperation) { - runAssetOperation(this, new SvelteSet(ids), operation, { order: this.#options.order ?? AssetOrder.Desc }); - } - - #updateAssets(assets: TimelineAsset[]) { - const lookup = new SvelteMap(assets.map((asset) => [asset.id, asset])); - const { unprocessedIds } = runAssetOperation( - this, - new SvelteSet(lookup.keys()), - (asset) => { - updateObject(asset, lookup.get(asset.id)); - return { remove: false }; - }, - { order: this.#options.order ?? AssetOrder.Desc }, - ); - const result: TimelineAsset[] = []; - for (const id of unprocessedIds.values()) { - result.push(lookup.get(id)!); - } - return result; + /** + * Executes callback on assets, handling moves between groups and removals due to filter criteria. + */ + update(ids: string[], callback: (asset: TimelineAsset) => void) { + // eslint-disable-next-line svelte/prefer-svelte-reactivity + return this.#runAssetCallback(new Set(ids), callback); } removeAssets(ids: string[]) { - const { unprocessedIds } = runAssetOperation( - this, - new SvelteSet(ids), - () => { - return { remove: true }; - }, - { order: this.#options.order ?? AssetOrder.Desc }, - ); - return [...unprocessedIds]; + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const result = this.#runAssetCallback(new Set(ids), () => ({ remove: true })); + return [...result.notUpdated]; + } + + protected upsertSegmentForAsset(asset: TimelineAsset) { + let month = getMonthGroupByDate(this, asset.localDateTime); + + if (!month) { + month = new MonthGroup(this, asset.localDateTime, 1, true, this.#options.order); + this.months.push(month); + } + return month; + } + + /** + * Adds assets to existing segments, creating new segments as needed. + * + * This is an internal method that assumes the provided assets are not already + * present in the timeline. For updating existing assets, use updateAssetOperation(). + */ + protected addAssetsUpsertSegments(assets: TimelineAsset[]) { + if (assets.length === 0) { + return; + } + const context = new GroupInsertionCache(); + const monthCount = this.months.length; + for (const asset of assets) { + this.upsertSegmentForAsset(asset).addTimelineAsset(asset, context); + } + if (this.months.length !== monthCount) { + this.postCreateSegments(); + } + this.postUpsert(context); + } + + #updateAssets(assets: TimelineAsset[]) { + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const cache = new Map(assets.map((asset) => [asset.id, asset])); + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const idsToUpdate = new Set(cache.keys()); + const result = this.#runAssetCallback(idsToUpdate, (asset) => void updateObject(asset, cache.get(asset.id))); + const notUpdated: TimelineAsset[] = []; + for (const assetId of result.notUpdated) { + notUpdated.push(cache.get(assetId)!); + } + return notUpdated; + } + + #runAssetCallback(ids: Set, callback: (asset: TimelineAsset) => void | { remove?: boolean }) { + if (ids.size === 0) { + // eslint-disable-next-line svelte/prefer-svelte-reactivity + return { updated: new Set(), notUpdated: ids, changedGeometry: false }; + } + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const changedMonthGroups = new Set(); + // eslint-disable-next-line svelte/prefer-svelte-reactivity + let notUpdated = new Set(ids); + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const updated = new Set(); + const assetsToMoveSegments: MoveAsset[][] = []; + for (const month of this.months) { + if (notUpdated.size === 0) { + break; + } + const result = month.runAssetCallback(notUpdated, callback); + if (result.moveAssets.length > 0) { + assetsToMoveSegments.push(result.moveAssets); + } + if (result.changedGeometry) { + changedMonthGroups.add(month); + } + notUpdated = setDifference(notUpdated, result.processedIds); + for (const id of result.processedIds) { + updated.add(id); + } + } + const assetsToAdd = []; + for (const segment of assetsToMoveSegments) { + for (const moveAsset of segment) { + assetsToAdd.push(moveAsset.asset); + } + } + this.addAssetsUpsertSegments(assetsToAdd); + const changedGeometry = changedMonthGroups.size > 0; + for (const month of changedMonthGroups) { + updateGeometry(this, month, { invalidateHeight: true }); + } + if (changedGeometry) { + this.updateIntersections(); + } + return { updated, notUpdated, changedGeometry }; } override refreshLayout() { @@ -493,4 +579,28 @@ export class TimelineManager extends VirtualScrollManager { getAssetOrder() { return this.#options.order ?? AssetOrder.Desc; } + + protected postCreateSegments(): void { + this.months.sort((a, b) => { + return a.yearMonth.year === b.yearMonth.year + ? b.yearMonth.month - a.yearMonth.month + : b.yearMonth.year - a.yearMonth.year; + }); + } + + protected postUpsert(context: GroupInsertionCache): void { + for (const group of context.existingDayGroups) { + group.sortAssets(this.#options.order); + } + + for (const monthGroup of context.bucketsWithNewDayGroups) { + monthGroup.sortDayGroups(); + } + + for (const month of context.updatedBuckets) { + month.sortDayGroups(); + updateGeometry(this, month, { invalidateHeight: true }); + } + this.updateIntersections(); + } } diff --git a/web/src/lib/managers/timeline-manager/types.ts b/web/src/lib/managers/timeline-manager/types.ts index 27c27dcb63..35d7178f97 100644 --- a/web/src/lib/managers/timeline-manager/types.ts +++ b/web/src/lib/managers/timeline-manager/types.ts @@ -37,8 +37,6 @@ export type TimelineAsset = { longitude?: number | null; }; -export type AssetOperation = (asset: TimelineAsset) => { remove: boolean }; - export type MoveAsset = { asset: TimelineAsset; date: TimelineDate }; export interface Viewport { diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts index 2eb081a490..05de75d3bc 100644 --- a/web/src/lib/utils/actions.ts +++ b/web/src/lib/utils/actions.ts @@ -79,14 +79,15 @@ const undoDeleteAssets = async (onUndoDelete: OnUndoDelete, assets: TimelineAsse */ export function updateStackedAssetInTimeline(timelineManager: TimelineManager, { stack, toDeleteIds }: StackResponse) { if (stack != undefined) { - timelineManager.updateAssetOperation([stack.primaryAssetId], (asset) => { - asset.stack = { - id: stack.id, - primaryAssetId: stack.primaryAssetId, - assetCount: stack.assets.length, - }; - return { remove: false }; - }); + timelineManager.update( + [stack.primaryAssetId], + (asset) => + (asset.stack = { + id: stack.id, + primaryAssetId: stack.primaryAssetId, + assetCount: stack.assets.length, + }), + ); timelineManager.removeAssets(toDeleteIds); } @@ -101,7 +102,7 @@ export function updateStackedAssetInTimeline(timelineManager: TimelineManager, { * @param assets - The array of asset response DTOs to update in the timeline manager. */ export function updateUnstackedAssetInTimeline(timelineManager: TimelineManager, assets: TimelineAsset[]) { - timelineManager.updateAssetOperation( + timelineManager.update( assets.map((asset) => asset.id), (asset) => { asset.stack = null; diff --git a/web/src/lib/utils/tree-utils.ts b/web/src/lib/utils/tree-utils.ts index 267bb2eec7..64b51158c4 100644 --- a/web/src/lib/utils/tree-utils.ts +++ b/web/src/lib/utils/tree-utils.ts @@ -62,8 +62,16 @@ export class TreeNode extends Map { const child = this.values().next().value!; child.value = joinPaths(this.value, child.value); child.parent = this.parent; - this.parent.delete(this.value); - this.parent.set(child.value, child); + + const entries = Array.from(this.parent.entries()); + this.parent.clear(); + for (const [key, value] of entries) { + if (key === this.value) { + this.parent.set(child.value, child); + } else { + this.parent.set(key, value); + } + } } for (const child of this.values()) { diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 74b0f1d6b3..3f4d3dd39f 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -66,6 +66,7 @@ } from '@immich/sdk'; import { Button, Icon, IconButton, modalManager, toastManager } from '@immich/ui'; import { + mdiAccountEye, mdiAccountEyeOutline, mdiArrowLeft, mdiCogOutline, @@ -101,7 +102,9 @@ let isCreatingSharedAlbum = $state(false); let isShowActivity = $state(false); let albumOrder: AssetOrder | undefined = $state(data.album.order); - let showAlbumUsers = $state(false); + + let timelineManager = $state() as TimelineManager; + let showAlbumUsers = $derived(timelineManager?.showAssetOwners ?? false); const assetInteraction = new AssetInteraction(); const timelineInteraction = new AssetInteraction(); @@ -303,7 +306,6 @@ } }); - let timelineManager = $state() as TimelineManager; const options = $derived.by(() => { if (viewMode === AlbumPageViewMode.SELECT_ASSETS) { return { @@ -555,11 +557,7 @@ {#if assetInteraction.isAllUserOwned} - timelineManager.updateAssetOperation(ids, (asset) => { - asset.isFavorite = isFavorite; - return { remove: false }; - })} + onFavorite={(ids, isFavorite) => timelineManager.update(ids, (asset) => (asset.isFavorite = isFavorite))} > {/if} @@ -578,11 +576,7 @@ - timelineManager.updateAssetOperation(ids, (asset) => { - asset.visibility = visibility; - return { remove: false }; - })} + onArchive={(ids, visibility) => timelineManager.update(ids, (asset) => (asset.visibility = visibility))} /> {/if} @@ -605,17 +599,6 @@ {#snippet trailing()} - {#if containsEditors} - (showAlbumUsers = !showAlbumUsers)} - /> - {/if} - {#if isEditor} + {#if containsEditors} + timelineManager.toggleShowAssetOwners()} + /> + {/if} {#if album.assetCount > 0} - timelineManager.updateAssetOperation(ids, (asset) => { - asset.visibility = visibility; - return { remove: false }; - })} + onArchive={(ids, visibility) => timelineManager.update(ids, (asset) => (asset.visibility = visibility))} /> @@ -80,11 +76,7 @@ - timelineManager.updateAssetOperation(ids, (asset) => { - asset.isFavorite = isFavorite; - return { remove: false }; - })} + onFavorite={(ids, isFavorite) => timelineManager.update(ids, (asset) => (asset.isFavorite = isFavorite))} /> diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte index 7cb3bf8e17..781dc80ec8 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -85,11 +85,7 @@ - timelineManager.updateAssetOperation(ids, (asset) => { - asset.visibility = visibility; - return { remove: false }; - })} + onArchive={(ids, visibility) => timelineManager.update(ids, (asset) => (asset.visibility = visibility))} /> {#if $preferences.tags.enabled} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 5dabd58e76..c822855310 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -492,11 +492,7 @@ - timelineManager.updateAssetOperation(ids, (asset) => { - asset.isFavorite = isFavorite; - return { remove: false }; - })} + onFavorite={(ids, isFavorite) => timelineManager.update(ids, (asset) => (asset.isFavorite = isFavorite))} /> @@ -511,11 +507,7 @@ - timelineManager.updateAssetOperation(ids, (asset) => { - asset.visibility = visibility; - return { remove: false }; - })} + onArchive={(ids, visibility) => timelineManager.update(ids, (asset) => (asset.visibility = visibility))} /> {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index 669ea23921..8bf8dce94e 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -120,11 +120,7 @@ - timelineManager.updateAssetOperation(ids, (asset) => { - asset.isFavorite = isFavorite; - return { remove: false }; - })} + onFavorite={(ids, isFavorite) => timelineManager.update(ids, (asset) => (asset.isFavorite = isFavorite))} > @@ -148,11 +144,7 @@ - timelineManager.updateAssetOperation(ids, (asset) => { - asset.visibility = visibility; - return { remove: false }; - })} + onArchive={(ids, visibility) => timelineManager.update(ids, (asset) => (asset.visibility = visibility))} /> {#if $preferences.tags.enabled}