diff --git a/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte b/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte
index a9fb831e8..b374a7696 100644
--- a/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte
+++ b/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte
@@ -1,6 +1,6 @@
{
- if (userInteraction.aboutInfo && userInteraction.versions && $serverVersion) {
- info = userInteraction.aboutInfo;
- versions = userInteraction.versions;
- return;
- }
- await requestServerInfo();
- [info, versions] = await Promise.all([getAboutInfo(), getVersionHistory()]);
- userInteraction.aboutInfo = info;
- userInteraction.versions = versions;
- });
+ const aboutQuery = createQuery(() => getAboutInfoQuery());
+ const info = $derived(aboutQuery.data);
+ const historyQuery = createQuery(() => getVersionHistoryQuery());
+ const versions = $derived(historyQuery.data ?? []);
let isMain = $derived(info?.sourceRef === 'main' && info.repository === 'immich-app/immich');
let version = $derived($serverVersion ? semverToName($serverVersion) : null);
diff --git a/web/src/lib/components/shared-components/side-bar/StorageSpace.svelte b/web/src/lib/components/shared-components/side-bar/StorageSpace.svelte
index 45c40f6c2..19a48e76a 100644
--- a/web/src/lib/components/shared-components/side-bar/StorageSpace.svelte
+++ b/web/src/lib/components/shared-components/side-bar/StorageSpace.svelte
@@ -1,36 +1,27 @@
- {#if userInteraction.serverInfo}
+ {#if serverInfo}
{});
+ const serverInfo = await queryClient.query(getAboutInfoQuery()).catch(() => {});
if (serverInfo?.licensed) {
this.isPurchased = true;
}
diff --git a/web/src/lib/queries/library.query.ts b/web/src/lib/queries/library.query.ts
new file mode 100644
index 000000000..920de891e
--- /dev/null
+++ b/web/src/lib/queries/library.query.ts
@@ -0,0 +1,8 @@
+import { getLibraryStatistics } from '@immich/sdk';
+import { queryOptions } from '@tanstack/svelte-query';
+
+export const getLibraryStatisticsQuery = (id: string) =>
+ queryOptions({
+ queryKey: ['libraries', id, 'statistics'],
+ queryFn: ({ signal }) => getLibraryStatistics({ id }, { signal }),
+ });
diff --git a/web/src/lib/queries/server.query.ts b/web/src/lib/queries/server.query.ts
new file mode 100644
index 000000000..675658305
--- /dev/null
+++ b/web/src/lib/queries/server.query.ts
@@ -0,0 +1,29 @@
+import { getAboutInfo, getServerStatistics, getStorage, getVersionHistory } from '@immich/sdk';
+import { queryOptions } from '@tanstack/svelte-query';
+
+export const getAboutInfoQuery = () =>
+ queryOptions({
+ queryKey: ['server', 'about'],
+ queryFn: ({ signal }) => getAboutInfo({ signal }),
+ staleTime: Infinity,
+ });
+
+export const getVersionHistoryQuery = () =>
+ queryOptions({
+ queryKey: ['server', 'version-history'],
+ queryFn: ({ signal }) => getVersionHistory({ signal }),
+ staleTime: Infinity,
+ });
+
+export const getStorageQuery = () =>
+ queryOptions({
+ queryKey: ['server', 'storage'],
+ queryFn: ({ signal }) => getStorage({ signal }),
+ staleTime: Infinity,
+ });
+
+export const getServerStatisticsQuery = () =>
+ queryOptions({
+ queryKey: ['server', 'statistics'],
+ queryFn: ({ signal }) => getServerStatistics({ signal }),
+ });
diff --git a/web/src/lib/stores/user.svelte.ts b/web/src/lib/stores/user.svelte.ts
index e5bb4dfa2..f1ae7f5a4 100644
--- a/web/src/lib/stores/user.svelte.ts
+++ b/web/src/lib/stores/user.svelte.ts
@@ -1,23 +1,12 @@
-import type {
- AlbumResponseDto,
- ServerAboutResponseDto,
- ServerStorageResponseDto,
- ServerVersionHistoryResponseDto,
-} from '@immich/sdk';
+import type { AlbumResponseDto } from '@immich/sdk';
import { eventManager } from '$lib/managers/event-manager.svelte';
interface UserInteractions {
recentAlbums?: AlbumResponseDto[];
- versions?: ServerVersionHistoryResponseDto[];
- aboutInfo?: ServerAboutResponseDto;
- serverInfo?: ServerStorageResponseDto;
}
const defaultUserInteraction: UserInteractions = {
recentAlbums: undefined,
- versions: undefined,
- aboutInfo: undefined,
- serverInfo: undefined,
};
export const userInteraction = $state(defaultUserInteraction);
diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts
index 283038054..f9832f8c3 100644
--- a/web/src/lib/stores/websocket.ts
+++ b/web/src/lib/stores/websocket.ts
@@ -8,6 +8,7 @@ import {
type SyncAssetEditV1,
type SyncAssetV2,
} from '@immich/sdk';
+import { isEqual } from 'lodash-es';
import { io, type Socket } from 'socket.io-client';
import { get, writable } from 'svelte/store';
import { page } from '$app/state';
@@ -69,7 +70,13 @@ websocket
websocketStore.connected.set(true);
})
.on('disconnect', () => websocketStore.connected.set(false))
- .on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion))
+ .on('on_server_version', (serverVersion) => {
+ const previous = get(websocketStore.serverVersion);
+ websocketStore.serverVersion.set(serverVersion);
+ if (previous && !isEqual(previous, serverVersion)) {
+ void queryClient.invalidateQueries({ queryKey: ['server'] });
+ }
+ })
.on(
'on_config_update',
() => void queryClient.invalidateQueries({ queryKey: getConfigQuery().queryKey, exact: true }),
diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts
index aa6ffccdb..85aaadbac 100644
--- a/web/src/lib/utils/auth.ts
+++ b/web/src/lib/utils/auth.ts
@@ -1,9 +1,7 @@
-import { getStorage } from '@immich/sdk';
import { redirect } from '@sveltejs/kit';
import { DateTime } from 'luxon';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { Route } from '$lib/route';
-import { userInteraction } from '$lib/stores/user.svelte';
export interface AuthOptions {
admin?: true;
@@ -27,15 +25,6 @@ export const authenticate = async (url: URL, options?: AuthOptions) => {
}
};
-export const requestServerInfo = async () => {
- if (!authManager.authenticated) {
- return;
- }
-
- const data = await getStorage();
- userInteraction.serverInfo = data;
-};
-
export const getAccountAge = (): number => {
if (!authManager.authenticated) {
return 0;
diff --git a/web/src/lib/utils/license-utils.ts b/web/src/lib/utils/license-utils.ts
index c53780ef8..45f47c9fe 100644
--- a/web/src/lib/utils/license-utils.ts
+++ b/web/src/lib/utils/license-utils.ts
@@ -3,6 +3,8 @@ import { PUBLIC_IMMICH_BUY_HOST, PUBLIC_IMMICH_PAY_HOST } from '$env/static/publ
import type { ImmichProduct } from '$lib/constants';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
+import { queryClient } from '$lib/queries/client';
+import { getAboutInfoQuery } from '$lib/queries/server.query';
export const activateProduct = async (licenseKey: string, activationKey: string): Promise => {
// TODO is this needed?
@@ -11,7 +13,14 @@ export const activateProduct = async (licenseKey: string, activationKey: string)
const isServerActivation = authManager.user.isAdmin && licenseKey.search('IMSV') !== -1;
const licenseKeyDto = { licenseKey, activationKey };
// Send server key to user activation if user is not admin
- return isServerActivation ? setServerLicense({ licenseKeyDto }) : setUserLicense({ licenseKeyDto });
+ if (!isServerActivation) {
+ return setUserLicense({ licenseKeyDto });
+ }
+
+ const license = await setServerLicense({ licenseKeyDto });
+ // contains license status
+ await queryClient.invalidateQueries({ queryKey: getAboutInfoQuery().queryKey });
+ return license;
};
export const getActivationKey = async (licenseKey: string): Promise => {
diff --git a/web/src/routes/(user)/user-settings/UserPurchaseSettings.svelte b/web/src/routes/(user)/user-settings/UserPurchaseSettings.svelte
index 747a7bc3a..07df2f9fe 100644
--- a/web/src/routes/(user)/user-settings/UserPurchaseSettings.svelte
+++ b/web/src/routes/(user)/user-settings/UserPurchaseSettings.svelte
@@ -5,13 +5,14 @@
import SettingSwitch from '$lib/components/shared-components/settings/SettingSwitch.svelte';
import { dateFormats } from '$lib/constants';
import { authManager } from '$lib/managers/auth-manager.svelte';
+ import { getAboutInfoQuery } from '$lib/queries/server.query';
+ import { queryClient } from '$lib/queries/client';
import { locale } from '$lib/stores/preferences.store';
import { handleError } from '$lib/utils/handle-error';
import { setSupportBadgeVisibility } from '$lib/utils/purchase-utils';
import {
deleteUserLicense as deleteIndividualProductKey,
deleteServerLicense as deleteServerProductKey,
- getAboutInfo,
getMyUser,
getServerLicense,
isHttpError,
@@ -26,7 +27,7 @@
let serverPurchaseInfo: LicenseResponseDto | null = $state(null);
const checkPurchaseInfo = async () => {
- const serverInfo = await getAboutInfo();
+ const serverInfo = await queryClient.query(getAboutInfoQuery());
isServerProduct = serverInfo.licensed;
const response = await getMyUser();
@@ -90,6 +91,8 @@
}
await deleteServerProductKey();
+ // contains license status
+ await queryClient.invalidateQueries({ queryKey: getAboutInfoQuery().queryKey });
authManager.isPurchased = false;
} catch (error) {
handleError(error, $t('errors.failed_to_remove_product_key'));
diff --git a/web/src/routes/admin/library-management/(list)/+layout.ts b/web/src/routes/admin/library-management/(list)/+layout.ts
index 5c8442180..4721cf550 100644
--- a/web/src/routes/admin/library-management/(list)/+layout.ts
+++ b/web/src/routes/admin/library-management/(list)/+layout.ts
@@ -1,12 +1,11 @@
import { getAllLibraries, getLibraryStatistics, getUserAdmin, searchUsersAdmin } from '@immich/sdk';
-import { authenticate, requestServerInfo } from '$lib/utils/auth';
+import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { LayoutLoad } from './$types';
export const load = (async ({ url, depends }) => {
depends('app:libraries');
await authenticate(url, { admin: true });
- await requestServerInfo();
const allUsers = await searchUsersAdmin({ withDeleted: false });
const $t = await getFormatter();
diff --git a/web/src/routes/admin/library-management/[id]/+layout.svelte b/web/src/routes/admin/library-management/[id]/+layout.svelte
index d0a8700cc..32b412275 100644
--- a/web/src/routes/admin/library-management/[id]/+layout.svelte
+++ b/web/src/routes/admin/library-management/[id]/+layout.svelte
@@ -8,6 +8,8 @@
import EmptyPlaceholder from '$lib/components/shared-components/EmptyPlaceholder.svelte';
import TableButton from '$lib/components/TableButton.svelte';
import LibraryFolderAddModal from '$lib/modals/LibraryFolderAddModal.svelte';
+ import { queryClient } from '$lib/queries/client';
+ import { getLibraryStatisticsQuery } from '$lib/queries/library.query';
import { Route } from '$lib/route';
import {
getLibraryActions,
@@ -16,6 +18,7 @@
} from '$lib/services/library.service';
import { getBytesWithUnit } from '$lib/utils/byte-units';
import { Code, CommandPaletteDefaultProvider, Container, Heading, modalManager } from '@immich/ui';
+ import { createQuery } from '@tanstack/svelte-query';
import { mdiCameraIris, mdiChartPie, mdiFilterMinusOutline, mdiFolderOutline, mdiPlayCircle } from '@mdi/js';
import type { Snippet } from 'svelte';
import { t } from 'svelte-i18n';
@@ -28,20 +31,25 @@
const { children, data }: Props = $props();
- const photosPromise = $derived(data.statisticsPromise.then((stats) => ({ value: stats.photos })));
-
- const videosPromise = $derived(data.statisticsPromise.then((stats) => ({ value: stats.videos })));
-
- const usagePromise = $derived(
- data.statisticsPromise.then((stats) => {
- const [value, unit] = getBytesWithUnit(stats.usage);
- return { value, unit };
- }),
- );
-
const library = $derived(data.library);
- const onLibraryUpdate = () => invalidate('app:library');
+ const query = createQuery(() => getLibraryStatisticsQuery(library.id));
+ const stats = $derived(query.data);
+
+ const photos = $derived(stats && { value: stats.photos });
+ const videos = $derived(stats && { value: stats.videos });
+ const usage = $derived.by(() => {
+ if (!stats) {
+ return;
+ }
+ const [value, unit] = getBytesWithUnit(stats.usage);
+ return { value, unit };
+ });
+
+ const onLibraryUpdate = async () => {
+ await invalidate('app:library');
+ await queryClient.invalidateQueries({ queryKey: getLibraryStatisticsQuery(library.id).queryKey });
+ };
const onLibraryDelete = async ({ id }: { id: string }) => {
if (id === library.id) {
@@ -64,9 +72,9 @@
{library.name}
-
-
-
+
+
+
diff --git a/web/src/routes/admin/library-management/[id]/+layout.ts b/web/src/routes/admin/library-management/[id]/+layout.ts
index 50b7e0791..9e2b44974 100644
--- a/web/src/routes/admin/library-management/[id]/+layout.ts
+++ b/web/src/routes/admin/library-management/[id]/+layout.ts
@@ -1,4 +1,4 @@
-import { getLibrary, getLibraryStatistics, type LibraryResponseDto } from '@immich/sdk';
+import { getLibrary, type LibraryResponseDto } from '@immich/sdk';
import { redirect } from '@sveltejs/kit';
import { Route } from '$lib/route';
import { authenticate } from '$lib/utils/auth';
@@ -17,12 +17,10 @@ export const load = (async ({ params: { id }, url, depends }) => {
redirect(307, Route.libraries());
}
- const statisticsPromise = getLibraryStatistics({ id });
const $t = await getFormatter();
return {
library,
- statisticsPromise,
meta: {
title: $t('admin.library_details'),
},
diff --git a/web/src/routes/admin/maintenance/+page.svelte b/web/src/routes/admin/maintenance/+page.svelte
index f7af961cb..a8163c439 100644
--- a/web/src/routes/admin/maintenance/+page.svelte
+++ b/web/src/routes/admin/maintenance/+page.svelte
@@ -143,7 +143,7 @@
{#snippet footer()}
diff --git a/web/src/routes/admin/queues/[name]/+page.ts b/web/src/routes/admin/queues/[name]/+page.ts
index b04667efb..446485e4f 100644
--- a/web/src/routes/admin/queues/[name]/+page.ts
+++ b/web/src/routes/admin/queues/[name]/+page.ts
@@ -1,13 +1,12 @@
import { getQueue, getQueueJobs, QueueJobStatus } from '@immich/sdk';
import { redirect } from '@sveltejs/kit';
import { fromQueueSlug, Route } from '$lib/route';
-import { authenticate, requestServerInfo } from '$lib/utils/auth';
+import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url, { admin: true });
- await requestServerInfo();
const name = fromQueueSlug(params.name);
if (!name) {
diff --git a/web/src/routes/admin/server-status/+page.svelte b/web/src/routes/admin/server-status/+page.svelte
index d8eb636cf..aacfeae60 100644
--- a/web/src/routes/admin/server-status/+page.svelte
+++ b/web/src/routes/admin/server-status/+page.svelte
@@ -1,9 +1,7 @@
-
+
diff --git a/web/src/routes/admin/server-status/+page.ts b/web/src/routes/admin/server-status/+page.ts
index 91d478ae4..44a7861db 100644
--- a/web/src/routes/admin/server-status/+page.ts
+++ b/web/src/routes/admin/server-status/+page.ts
@@ -1,16 +1,18 @@
-import { getServerStatistics, searchUsersAdmin } from '@immich/sdk';
+import { searchUsersAdmin } from '@immich/sdk';
+import { noop } from 'lodash-es';
+import { queryClient } from '$lib/queries/client';
+import { getServerStatisticsQuery } from '$lib/queries/server.query';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { PageLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url, { admin: true });
- const statsPromise = getServerStatistics();
+ void queryClient.query(getServerStatisticsQuery()).catch(noop);
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/server-status/ServerStatisticsPanel.svelte b/web/src/routes/admin/server-status/ServerStatisticsPanel.svelte
index c24692ad7..3c93977d2 100644
--- a/web/src/routes/admin/server-status/ServerStatisticsPanel.svelte
+++ b/web/src/routes/admin/server-status/ServerStatisticsPanel.svelte
@@ -1,8 +1,9 @@
{#snippet placeholder()}
@@ -66,9 +65,9 @@
{$t('total_usage')}
-
-
-
+
+
+
@@ -80,13 +79,11 @@
- {#await statsPromise}
- {zeros(0)}
- {:then stats}
+ {#if stats}
{zeros(stats.photos)}{stats.photos}
- {:catch}
- {zeros(0)}
- {/await}
+ {:else}
+ {zeros(0)}
+ {/if}
@@ -96,13 +93,11 @@
- {#await statsPromise}
- {zeros(0)}
- {:then stats}
+ {#if stats}
{zeros(stats.videos)}{stats.videos}
- {:catch}
- {zeros(0)}
- {/await}
+ {:else}
+ {zeros(0)}
+ {/if}
@@ -112,9 +107,7 @@
- {#await statsPromise}
- {zeros(0)}
- {:then stats}
+ {#if stats}
{@const storageUsageWithUnit = getStorageUsageWithUnit(stats.usage)}
{zeros(storageUsageWithUnit[0])}{storageUsageWithUnit[0]}
{storageUsageWithUnit[1]}
- {:catch}
-
{zeros(0)}
- {/await}
+ {:else}
+
{zeros(0)}
+ {/if}
@@ -145,42 +138,39 @@
{#each users as user (user.id)}