From c65bc8762ff5e625f6e1afaaa6ecb13e3b3effb8 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Wed, 25 Feb 2026 00:39:58 +0100 Subject: [PATCH] fix comments --- .../lib/model/library_stats_response_dto.dart | 11 +- open-api/immich-openapi-specs.json | 6 - open-api/typescript-sdk/src/fetch-client.ts | 2 - server/src/dtos/library.dto.ts | 3 - server/src/queries/library.repository.sql | 18 +- server/src/repositories/library.repository.ts | 22 +-- server/src/services/library.service.spec.ts | 9 +- .../ServerStatisticsCard.svelte | 69 ++++--- .../ServerStatisticsPanel.svelte | 187 ++++++++++++++---- .../library-management/(list)/+layout.svelte | 128 ++++++++---- .../library-management/[id]/+layout.svelte | 66 ++----- .../routes/admin/server-status/+page.svelte | 20 +- web/src/routes/admin/server-status/+page.ts | 4 +- .../routes/admin/users/[id]/+layout.svelte | 18 +- 14 files changed, 336 insertions(+), 227 deletions(-) diff --git a/mobile/openapi/lib/model/library_stats_response_dto.dart b/mobile/openapi/lib/model/library_stats_response_dto.dart index 220605b388..6eec3ae8d7 100644 --- a/mobile/openapi/lib/model/library_stats_response_dto.dart +++ b/mobile/openapi/lib/model/library_stats_response_dto.dart @@ -13,16 +13,12 @@ part of openapi.api; class LibraryStatsResponseDto { /// Returns a new [LibraryStatsResponseDto] instance. LibraryStatsResponseDto({ - this.offline = 0, this.photos = 0, this.total = 0, this.usage = 0, this.videos = 0, }); - /// Number of offline assets - int offline; - /// Number of photos int photos; @@ -37,7 +33,6 @@ class LibraryStatsResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is LibraryStatsResponseDto && - other.offline == offline && other.photos == photos && other.total == total && other.usage == usage && @@ -46,18 +41,16 @@ class LibraryStatsResponseDto { @override int get hashCode => // ignore: unnecessary_parenthesis - (offline.hashCode) + (photos.hashCode) + (total.hashCode) + (usage.hashCode) + (videos.hashCode); @override - String toString() => 'LibraryStatsResponseDto[offline=$offline, photos=$photos, total=$total, usage=$usage, videos=$videos]'; + String toString() => 'LibraryStatsResponseDto[photos=$photos, total=$total, usage=$usage, videos=$videos]'; Map toJson() { final json = {}; - json[r'offline'] = this.offline; json[r'photos'] = this.photos; json[r'total'] = this.total; json[r'usage'] = this.usage; @@ -74,7 +67,6 @@ class LibraryStatsResponseDto { final json = value.cast(); return LibraryStatsResponseDto( - offline: mapValueOfType(json, r'offline')!, photos: mapValueOfType(json, r'photos')!, total: mapValueOfType(json, r'total')!, usage: mapValueOfType(json, r'usage')!, @@ -126,7 +118,6 @@ class LibraryStatsResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'offline', 'photos', 'total', 'usage', diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index f977b1d98f..38e1fe8e01 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -18288,11 +18288,6 @@ }, "LibraryStatsResponseDto": { "properties": { - "offline": { - "default": 0, - "description": "Number of offline assets", - "type": "integer" - }, "photos": { "default": 0, "description": "Number of photos", @@ -18316,7 +18311,6 @@ } }, "required": [ - "offline", "photos", "total", "usage", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 4fb1d5b0b6..1ae12cd091 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1321,8 +1321,6 @@ export type UpdateLibraryDto = { name?: string; }; export type LibraryStatsResponseDto = { - /** Number of offline assets */ - offline: number; /** Number of photos */ photos: number; /** Total number of assets */ diff --git a/server/src/dtos/library.dto.ts b/server/src/dtos/library.dto.ts index b3265a0b3a..3f71b8a0ed 100644 --- a/server/src/dtos/library.dto.ts +++ b/server/src/dtos/library.dto.ts @@ -136,9 +136,6 @@ export class LibraryStatsResponseDto { @ApiProperty({ type: 'integer', description: 'Total number of assets' }) total = 0; - @ApiProperty({ type: 'integer', description: 'Number of offline assets' }) - offline = 0; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage in bytes' }) usage = 0; } diff --git a/server/src/queries/library.repository.sql b/server/src/queries/library.repository.sql index 1766b9017b..f0bd05973f 100644 --- a/server/src/queries/library.repository.sql +++ b/server/src/queries/library.repository.sql @@ -36,37 +36,27 @@ select ( "asset"."type" = $1 and "asset"."visibility" != $2 - and "asset"."isOffline" = $3 ) ) as "photos", count(*) filter ( where ( - "asset"."type" = $4 - and "asset"."visibility" != $5 - and "asset"."isOffline" = $6 + "asset"."type" = $3 + and "asset"."visibility" != $4 ) ) as "videos", - count(*) filter ( - where - ( - "asset"."isOffline" = $7 - and "asset"."visibility" != $8 - ) - ) as "offline", - coalesce(sum("asset_exif"."fileSizeInByte"), $9) as "usage" + coalesce(sum("asset_exif"."fileSizeInByte"), $5) as "usage" from "library" inner join "asset" on "asset"."libraryId" = "library"."id" left join "asset_exif" on "asset_exif"."assetId" = "asset"."id" where - "library"."id" = $10 + "library"."id" = $6 group by "library"."id" select 0::int as "photos", 0::int as "videos", - 0::int as "offline", 0::int as "usage", 0::int as "total" from diff --git a/server/src/repositories/library.repository.ts b/server/src/repositories/library.repository.ts index 4341f940cc..68102ab765 100644 --- a/server/src/repositories/library.repository.ts +++ b/server/src/repositories/library.repository.ts @@ -79,11 +79,7 @@ export class LibraryRepository { eb.fn .countAll() .filterWhere((eb) => - eb.and([ - eb('asset.type', '=', AssetType.Image), - eb('asset.visibility', '!=', AssetVisibility.Hidden), - eb('asset.isOffline', '=', false), - ]), + eb.and([eb('asset.type', '=', AssetType.Image), eb('asset.visibility', '!=', AssetVisibility.Hidden)]), ) .as('photos'), ) @@ -91,22 +87,10 @@ export class LibraryRepository { eb.fn .countAll() .filterWhere((eb) => - eb.and([ - eb('asset.type', '=', AssetType.Video), - eb('asset.visibility', '!=', AssetVisibility.Hidden), - eb('asset.isOffline', '=', false), - ]), + eb.and([eb('asset.type', '=', AssetType.Video), eb('asset.visibility', '!=', AssetVisibility.Hidden)]), ) .as('videos'), ) - .select((eb) => - eb.fn - .countAll() - .filterWhere((eb) => - eb.and([eb('asset.isOffline', '=', true), eb('asset.visibility', '!=', AssetVisibility.Hidden)]), - ) - .as('offline'), - ) .select((eb) => eb.fn.coalesce((eb) => eb.fn.sum('asset_exif.fileSizeInByte'), eb.val(0)).as('usage')) .groupBy('library.id') .where('library.id', '=', id) @@ -119,7 +103,6 @@ export class LibraryRepository { .selectFrom('library') .select(zero.as('photos')) .select(zero.as('videos')) - .select(zero.as('offline')) .select(zero.as('usage')) .select(zero.as('total')) .where('library.id', '=', id) @@ -129,7 +112,6 @@ export class LibraryRepository { return { photos: stats.photos, videos: stats.videos, - offline: stats.offline, usage: stats.usage, total: stats.photos + stats.videos, }; diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 2c447bfad8..d0c2d0a785 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -681,19 +681,12 @@ describe(LibraryService.name, () => { it('should return library statistics', async () => { const library = factory.library(); - mocks.library.getStatistics.mockResolvedValue({ - photos: 10, - videos: 0, - total: 10, - usage: 1337, - offline: 67, - }); + mocks.library.getStatistics.mockResolvedValue({ photos: 10, videos: 0, total: 10, usage: 1337 }); await expect(sut.getStatistics(library.id)).resolves.toEqual({ photos: 10, videos: 0, total: 10, usage: 1337, - offline: 67, }); expect(mocks.library.getStatistics).toHaveBeenCalledWith(library.id); diff --git a/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte b/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte index b978ae9b33..13daf14a3a 100644 --- a/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte +++ b/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte @@ -1,26 +1,28 @@
@@ -29,14 +31,35 @@ {title}
-
- {#if value === undefined} - - {:else} - {zeros()}{value} - {#if unit} - {unit} - {/if} - {/if} -
+ {#await valuePromise} +
+ {zeros()} +
+ {:then data} +
+ {zeros(data)}{data.value} + {#if data.unit}{data.unit}{/if} +
+ {:catch _} +
+ {zeros()} +
+ {/await} + + diff --git a/web/src/lib/components/server-statistics/ServerStatisticsPanel.svelte b/web/src/lib/components/server-statistics/ServerStatisticsPanel.svelte index 3ae5e4283a..5b6e68db64 100644 --- a/web/src/lib/components/server-statistics/ServerStatisticsPanel.svelte +++ b/web/src/lib/components/server-statistics/ServerStatisticsPanel.svelte @@ -1,8 +1,8 @@ +{#snippet placeholder()} + + + +{/snippet} +
{$t('total_usage')}
@@ -54,7 +79,13 @@
- {zeros(stats.photos)}{stats.photos} + {#await statsPromise} + {zeros(0)} + {:then stats} + {zeros(stats.photos)}{stats.photos} + {:catch} + {zeros(0)} + {/await}
@@ -64,7 +95,13 @@
- {zeros(stats.videos)}{stats.videos} + {#await statsPromise} + {zeros(0)} + {:then stats} + {zeros(stats.videos)}{stats.videos} + {:catch} + {zeros(0)} + {/await}
@@ -74,11 +111,20 @@
- {zeros(statsUsage)}{statsUsage} + {#await statsPromise} + {zeros(0)} + {:then stats} + {@const storageUsageWithUnit = getStorageUsageWithUnit(stats.usage)} + {zeros(storageUsageWithUnit[0])}{storageUsageWithUnit[0]} -
- {statsUsageUnit} -
+
+ {storageUsageWithUnit[1]} +
+ {:catch} + {zeros(0)} + {/await}
@@ -95,34 +141,99 @@ {$t('usage')} - {#each stats.usageByUser as user (user.userId)} + {#each users as user (user.id)} - {user.userName} - - {user.photos.toLocaleString($locale)} () - - {user.videos.toLocaleString($locale)} () - - - {#if user.quotaSizeInBytes !== null} - / + {user.name} + {#await getUserStatsPromise(user.id)} + {@render placeholder()} + {:then userStats} + {#if userStats} + + {userStats.photos.toLocaleString($locale)} () + + {userStats.videos.toLocaleString($locale)} () + + + {#if userStats.quotaSizeInBytes !== null} + / + {/if} + + {#if userStats.quotaSizeInBytes !== null && userStats.quotaSizeInBytes >= 0} + ({(userStats.quotaSizeInBytes === 0 + ? 1 + : userStats.usage / userStats.quotaSizeInBytes + ).toLocaleString($locale, { + style: 'percent', + maximumFractionDigits: 0, + })}) + {:else} + ({$t('unlimited')}) + {/if} + + + {:else} + {@render placeholder()} {/if} - - {#if user.quotaSizeInBytes !== null && user.quotaSizeInBytes >= 0} - ({(user.quotaSizeInBytes === 0 ? 1 : user.usage / user.quotaSizeInBytes).toLocaleString($locale, { - style: 'percent', - maximumFractionDigits: 0, - })}) - {:else} - ({$t('unlimited')}) - {/if} - - + {:catch} + {@render placeholder()} + {/await} {/each} + + diff --git a/web/src/routes/admin/library-management/(list)/+layout.svelte b/web/src/routes/admin/library-management/(list)/+layout.svelte index 21f7de910e..4a89bf0b07 100644 --- a/web/src/routes/admin/library-management/(list)/+layout.svelte +++ b/web/src/routes/admin/library-management/(list)/+layout.svelte @@ -7,13 +7,18 @@ import { getLibrariesActions, getLibraryActions } from '$lib/services/library.service'; import { locale } from '$lib/stores/preferences.store'; import { getBytesWithUnit } from '$lib/utils/byte-units'; - import { getLibrary, getLibraryStatistics, type LibraryResponseDto, type LibraryStatsResponseDto } from '@immich/sdk'; + import { + getLibrary, + getLibraryStatistics, + type LibraryResponseDto, + type LibraryStatsResponseDto, + type UserAdminResponseDto, + } from '@immich/sdk'; import { CommandPaletteDefaultProvider, Container, ContextMenuButton, Link, - LoadingSpinner, MenuItemType, Table, TableBody, @@ -32,37 +37,36 @@ data: LayoutData; }; - let { children, data }: Props = $props(); + const props: Props = $props(); - let libraries = $state(data.libraries); + let libraries = $state([]); let statistics = $state>({}); - let owners = $state(data.owners); - - const loadStatistics = async () => { - try { - statistics = await data.statisticsPromise; - } catch (error) { - console.error('Failed to load library statistics:', error); - } - }; + let owners = $state>({}); $effect(() => { - void loadStatistics(); + libraries = [...props.data.libraries]; + owners = { ...props.data.owners }; }); - const onLibraryCreate = async (library: LibraryResponseDto) => { - await goto(Route.viewLibrary(library)); + const onLibraryCreate = (library: LibraryResponseDto) => { + void goto(Route.viewLibrary(library)); }; - const onLibraryUpdate = async (library: LibraryResponseDto) => { + const onLibraryUpdate = (library: LibraryResponseDto) => { const index = libraries.findIndex(({ id }) => id === library.id); if (index === -1) { return; } - libraries[index] = await getLibrary({ id: library.id }); - statistics[library.id] = await getLibraryStatistics({ id: library.id }); + void Promise.all([getLibrary({ id: library.id }), getLibraryStatistics({ id: library.id })]) + .then(([updatedLibrary, updatedStats]) => { + libraries[index] = updatedLibrary; + statistics[library.id] = updatedStats; + }) + .catch((error) => { + console.error(`Failed to refresh library after update: ${error}`); + }); }; const onLibraryDelete = ({ id }: { id: string }) => { @@ -92,7 +96,7 @@ - +
{#if libraries.length > 0} @@ -107,7 +111,6 @@ {#each libraries as library (library.id + library.name)} - {@const stats = statistics[library.id]} {@const owner = owners[library.id]} @@ -116,29 +119,40 @@ {owner.name} - - {#if stats} + {#await props.data.statisticsPromise} + + + + + + + + + + {:then loadedStats} + {@const stats = statistics[library.id] || loadedStats[library.id]} + {stats.photos.toLocaleString($locale)} - {:else} - - {/if} - - - {#if stats} + + {stats.videos.toLocaleString($locale)} - {:else} - - {/if} - - - {#if stats} + + {@const [diskUsage, diskUsageUnit] = getBytesWithUnit(stats.usage, 0)} {diskUsage} {diskUsageUnit} - {:else} - - {/if} - + + {:catch} + + + + + + + + + + {/await} @@ -155,7 +169,41 @@ /> {/if} - {@render children?.()} + {@render props.children?.()}
+ + diff --git a/web/src/routes/admin/library-management/[id]/+layout.svelte b/web/src/routes/admin/library-management/[id]/+layout.svelte index 495cc27455..b75f753640 100644 --- a/web/src/routes/admin/library-management/[id]/+layout.svelte +++ b/web/src/routes/admin/library-management/[id]/+layout.svelte @@ -14,19 +14,11 @@ getLibraryExclusionPatternActions, getLibraryFolderActions, } from '$lib/services/library.service'; - import type { ByteUnit } from '$lib/utils/byte-units'; import { getBytesWithUnit } from '$lib/utils/byte-units'; - import type { LibraryResponseDto, LibraryStatsResponseDto } from '@immich/sdk'; + import type { LibraryResponseDto } from '@immich/sdk'; import { Code, CommandPaletteDefaultProvider, Container, Heading, modalManager } from '@immich/ui'; - import { - mdiCameraIris, - mdiChartPie, - mdiFileDocumentRemoveOutline, - mdiFilterMinusOutline, - mdiFolderOutline, - mdiPlayCircle, - } from '@mdi/js'; + import { mdiCameraIris, mdiChartPie, mdiFilterMinusOutline, mdiFolderOutline, mdiPlayCircle } from '@mdi/js'; import type { Snippet } from 'svelte'; import { t } from 'svelte-i18n'; import type { LayoutData } from './$types'; @@ -36,46 +28,32 @@ data: LayoutData; }; - const { children, data }: Props = $props(); + let { children, data }: Props = $props(); + const statisticsPromise = $derived.by(() => data.statisticsPromise); - let statistics = $state(undefined); - let storageUsage = $state(undefined); - let unit = $state(undefined); + const photosPromise = $derived.by(() => statisticsPromise.then((stats) => ({ value: stats.photos }))); - $effect(() => { - if (statistics) { - const [usage, u] = getBytesWithUnit(statistics.usage); - storageUsage = usage; - unit = u; - } else { - storageUsage = undefined; - unit = undefined; - } - }); + const videosPromise = $derived.by(() => statisticsPromise.then((stats) => ({ value: stats.videos }))); - const loadStatistics = async () => { - try { - statistics = await data.statisticsPromise; - } catch (error) { - console.error('Failed to load statistics:', error); - } - }; + const usagePromise = $derived.by(() => + statisticsPromise.then((stats) => { + const [value, unit] = getBytesWithUnit(stats.usage); + return { value, unit }; + }), + ); - $effect(() => { - void loadStatistics(); - }); - - let library = $state(data.library); + let updatedLibrary = $state(undefined); + const library = $derived.by(() => (updatedLibrary?.id === data.library.id ? updatedLibrary : data.library)); const onLibraryUpdate = (newLibrary: LibraryResponseDto) => { if (newLibrary.id === library.id) { - library = newLibrary; + updatedLibrary = newLibrary; } }; - const onLibraryDelete = async ({ id }: { id: string }) => { + const onLibraryDelete = ({ id }: { id: string }) => { if (id === library.id) { - await goto(Route.libraries()); + void goto(Route.libraries()); } }; @@ -94,9 +72,9 @@
{library.name}
- - - + + +
@@ -145,10 +123,6 @@ - -
- -
{@render children?.()} diff --git a/web/src/routes/admin/server-status/+page.svelte b/web/src/routes/admin/server-status/+page.svelte index 6e0868b831..01f5009daa 100644 --- a/web/src/routes/admin/server-status/+page.svelte +++ b/web/src/routes/admin/server-status/+page.svelte @@ -2,7 +2,7 @@ import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte'; import ServerStatisticsPanel from '$lib/components/server-statistics/ServerStatisticsPanel.svelte'; import { getServerStatistics, type ServerStatsResponseDto } from '@immich/sdk'; - import { Container, LoadingSpinner } from '@immich/ui'; + import { Container } from '@immich/ui'; import { onMount } from 'svelte'; import type { PageData } from './$types'; @@ -14,20 +14,18 @@ let stats = $state(undefined); - const loadStatistics = async () => { - try { - stats = await data.statsPromise; - } catch (error) { - console.error('Failed to load server statistics:', error); + const statsPromise = $derived.by(() => { + if (stats) { + return Promise.resolve(stats); } - }; + return data.statsPromise; + }); const updateStatistics = async () => { stats = await getServerStatistics(); }; onMount(() => { - void loadStatistics(); const interval = setInterval(() => void updateStatistics(), 5000); return () => clearInterval(interval); @@ -36,10 +34,6 @@ - {#if stats} - - {:else} - - {/if} + diff --git a/web/src/routes/admin/server-status/+page.ts b/web/src/routes/admin/server-status/+page.ts index 7359f8c130..d717a876a4 100644 --- a/web/src/routes/admin/server-status/+page.ts +++ b/web/src/routes/admin/server-status/+page.ts @@ -1,15 +1,17 @@ import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { getServerStatistics } from '@immich/sdk'; +import { getServerStatistics, searchUsersAdmin } from '@immich/sdk'; import type { PageLoad } from './$types'; export const load = (async ({ url }) => { await authenticate(url, { admin: true }); const statsPromise = getServerStatistics(); + const users = await searchUsersAdmin({ withDeleted: false }); const $t = await getFormatter(); return { statsPromise, + users, meta: { title: $t('server_stats'), }, diff --git a/web/src/routes/admin/users/[id]/+layout.svelte b/web/src/routes/admin/users/[id]/+layout.svelte index db86d05e72..d4556dbea5 100644 --- a/web/src/routes/admin/users/[id]/+layout.svelte +++ b/web/src/routes/admin/users/[id]/+layout.svelte @@ -123,9 +123,21 @@
- - - + + +