refactor: config queries (#32498)

(cherry picked from commit b45ec0f55abb9dc39fbbb39dcf5296d45a726ba0)
This commit is contained in:
Jason Rasmussen
2026-10-22 20:43:14 +03:00
committed by Codex
parent 1e20bdd74e
commit 493870ad7b
76 changed files with 698 additions and 340 deletions
+22
View File
@@ -1,5 +1,6 @@
import { BrowserContext } from '@playwright/test';
import { playwrightHost } from 'src/../playwright.config.js';
import { adminConfig, publicConfig, userConfig } from 'src/ui/mock-network/config-fixtures.js';
export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserId: string) => {
await context.addCookies([
@@ -113,6 +114,27 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
},
});
});
await context.route('**/api/public/config', async (route) => {
return route.fulfill({
status: 200,
contentType: 'application/json',
json: publicConfig,
});
});
await context.route('**/api/config', async (route) => {
return route.fulfill({
status: 200,
contentType: 'application/json',
json: userConfig,
});
});
await context.route('**/api/admin/config', async (route) => {
return route.fulfill({
status: 200,
contentType: 'application/json',
json: adminConfig,
});
});
await context.route('**/api/server/features', async (route) => {
return route.fulfill({
status: 200,
+171
View File
@@ -0,0 +1,171 @@
export const publicConfig = {
ffmpeg: { realtime: { enabled: false } },
map: {
enabled: true,
lightStyle: 'https://tiles.immich.cloud/v1/style/light.json',
darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json',
},
oauth: { autoLaunch: false, buttonText: 'Login with OAuth', enabled: false },
passkey: { enabled: true },
passwordLogin: { enabled: true },
server: { loginPageMessage: '' },
};
export const userConfig = {
ffmpeg: { realtime: { enabled: false, videoCodecs: ['h264', 'hevc'], resolutions: [480, 720, 1080] } },
machineLearning: {
enabled: false,
clip: { enabled: true },
duplicateDetection: { enabled: true },
facialRecognition: { enabled: true, minFaces: 3 },
ocr: { enabled: true },
},
map: {
enabled: true,
lightStyle: 'https://tiles.immich.cloud/v1/style/light.json',
darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json',
},
reverseGeocoding: { enabled: true },
oauth: { autoLaunch: false, buttonText: 'Login with OAuth', enabled: false, accountManagementUrl: '' },
passkey: { enabled: true },
passwordLogin: { enabled: true },
image: { thumbnail: { size: 250 }, preview: { size: 1440 }, fullsize: { enabled: false } },
trash: { enabled: true, days: 30 },
notifications: { smtp: { enabled: false } },
server: { externalDomain: '', loginPageMessage: '', publicUsers: true },
user: { deleteDelay: 7 },
};
export const adminConfig = {
backup: { database: { enabled: true, cronExpression: '0 02 * * *', keepLastAmount: 14 } },
ffmpeg: {
crf: 23,
threads: 0,
preset: 'ultrafast',
targetVideoCodec: 'h264',
acceptedVideoCodecs: ['h264'],
targetAudioCodec: 'aac',
acceptedAudioCodecs: ['aac', 'mp3', 'opus'],
acceptedContainers: ['mov', 'ogg', 'webm'],
targetResolution: '720',
maxBitrate: '0',
bframes: -1,
refs: 0,
gopSize: 0,
temporalAQ: false,
cqMode: 'auto',
twoPass: false,
preferredHwDevice: 'auto',
transcode: 'required',
tonemap: 'hable',
accel: 'disabled',
accelDecode: true,
realtime: { enabled: false, videoCodecs: ['h264', 'hevc'], resolutions: [480, 720, 1080] },
},
integrityChecks: {
missingFiles: { enabled: true, cronExpression: '0 03 * * *' },
untrackedFiles: { enabled: true, cronExpression: '0 03 * * *' },
checksumFiles: { enabled: true, cronExpression: '0 03 * * *', timeLimit: 3_600_000, percentageLimit: 1 },
},
job: {
thumbnailGeneration: { concurrency: 3 },
metadataExtraction: { concurrency: 5 },
videoConversion: { concurrency: 1 },
faceDetection: { concurrency: 2 },
smartSearch: { concurrency: 2 },
backgroundTask: { concurrency: 5 },
migration: { concurrency: 5 },
search: { concurrency: 5 },
sidecar: { concurrency: 5 },
library: { concurrency: 5 },
notifications: { concurrency: 5 },
ocr: { concurrency: 1 },
workflow: { concurrency: 5 },
editor: { concurrency: 2 },
integrityCheck: { concurrency: 1 },
},
logging: { enabled: true, level: 'log' },
machineLearning: {
enabled: false,
urls: ['http://immich-machine-learning:3003'],
availabilityChecks: { enabled: true, timeout: 2000, interval: 30_000 },
clip: { enabled: true, modelName: 'ViT-B-32__openai' },
duplicateDetection: { enabled: true, maxDistance: 0.01 },
facialRecognition: { enabled: true, modelName: 'buffalo_l', minScore: 0.7, maxDistance: 0.5, minFaces: 3 },
ocr: {
enabled: true,
modelName: 'PP-OCRv5_mobile',
minDetectionScore: 0.5,
minRecognitionScore: 0.8,
maxResolution: 736,
},
},
map: {
enabled: true,
lightStyle: 'https://tiles.immich.cloud/v1/style/light.json',
darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json',
},
reverseGeocoding: { enabled: true },
metadata: { faces: { import: false } },
oauth: {
autoLaunch: false,
autoRegister: true,
buttonText: 'Login with OAuth',
clientId: '',
clientSecret: '',
defaultStorageQuota: null,
enabled: false,
issuerUrl: '',
accountManagementUrl: '',
endSessionEndpoint: '',
mobileOverrideEnabled: false,
mobileRedirectUri: '',
prompt: '',
scope: 'openid email profile',
signingAlgorithm: 'RS256',
profileSigningAlgorithm: 'none',
storageLabelClaim: 'preferred_username',
storageQuotaClaim: 'immich_quota',
roleClaim: 'immich_role',
tokenEndpointAuthMethod: 'client_secret_post',
timeout: 30_000,
allowInsecureRequests: false,
},
passkey: { enabled: true, domain: null, additionalDomains: [] },
passwordLogin: { enabled: true },
storageTemplate: {
enabled: false,
hashVerificationEnabled: true,
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
},
image: {
thumbnail: { format: 'webp', size: 250, quality: 80, progressive: false },
preview: { format: 'jpeg', size: 1440, quality: 80, progressive: false },
colorspace: 'p3',
extractEmbedded: false,
fullsize: { enabled: false, format: 'jpeg', quality: 80, progressive: false },
},
newVersionCheck: { enabled: true, channel: 'stable' },
nightlyTasks: {
startTime: '00:00',
databaseCleanup: true,
generateMemories: true,
syncQuotaUsage: true,
missingThumbnails: true,
clusterNewFaces: true,
},
trash: { enabled: true, days: 30 },
theme: { customCss: '' },
library: { scan: { enabled: true, cronExpression: '0 0 * * *' }, watch: { enabled: false } },
server: { externalDomain: '', loginPageMessage: '', publicUsers: true },
notifications: {
smtp: {
enabled: false,
from: '',
replyTo: '',
transport: { ignoreCert: false, host: '', port: 587, secure: false, username: '', password: '' },
},
},
templates: { email: { welcomeTemplate: '', albumInviteTemplate: '', albumUpdateTemplate: '' } },
user: { deleteDelay: 7 },
};
+4 -3
View File
@@ -31,7 +31,8 @@ import type { MessageFormatter } from 'svelte-i18n';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { readPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
import { Route } from '$lib/route';
import { copyToClipboard } from '$lib/utils';
@@ -92,7 +93,7 @@ export const getPagesProvider = ($t: MessageFormatter) => {
title: $t('map'),
icon: mdiMapOutline,
onAction: () => goto(Route.map()),
$if: () => authManager.authenticated && featureFlagsManager.value.map,
$if: () => authManager.authenticated && readPreloadedQuery(getUserConfigQuery()).map.enabled,
},
{
title: $t('people'),
@@ -173,7 +174,7 @@ export const getPagesProvider = ($t: MessageFormatter) => {
title: $t('trash'),
icon: mdiTrashCanOutline,
onAction: () => goto(Route.trash()),
$if: () => authManager.authenticated && featureFlagsManager.value.trash,
$if: () => authManager.authenticated && readPreloadedQuery(getUserConfigQuery()).trash.enabled,
},
{
title: $t('admin.user_settings'),
@@ -7,7 +7,7 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { createPreloadedQuery, executeMutation } from '$lib/queries/client';
import { getConfigQuery, updateConfigMutation } from '$lib/queries/system-config.query';
import { getAdminConfigQuery, updateAdminConfigMutation } from '$lib/queries/config-admin.query';
import StorageTemplateVariablesModal from '$lib/modals/StorageTemplateVariablesModal.svelte';
import { Route } from '$lib/route';
import { getStorageTemplateOptions, type SystemConfigTemplateStorageOptionDto } from '@immich/sdk';
@@ -40,7 +40,7 @@
type StorageTemplateOption = (typeof StorageTemplateOption)[keyof typeof StorageTemplateOption];
const disabled = $derived(featureFlagsManager.value.configFile);
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
let configToEdit = $state(cloneDeep(config));
@@ -155,7 +155,7 @@
onDestroy(() => {
if (saveOnClose) {
void executeMutation(updateConfigMutation, { storageTemplate: configToEdit.storageTemplate });
void executeMutation(updateAdminConfigMutation, { storageTemplate: configToEdit.storageTemplate });
}
});
</script>
@@ -7,7 +7,6 @@
import Timeline from '$lib/components/timeline/Timeline.svelte';
import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { handleDownloadAlbum } from '$lib/services/album.service';
import { getGlobalActions } from '$lib/services/app.service';
@@ -23,6 +22,11 @@
import ControlAppBar from '../shared-components/ControlAppBar.svelte';
import ThemeButton from '../shared-components/ThemeButton.svelte';
import AlbumSummary from './AlbumSummary.svelte';
import { createPreloadedQuery } from '$lib/queries/client';
import { getPublicConfigQuery } from '$lib/queries/config-public.query';
const query = createPreloadedQuery(getPublicConfigQuery);
const publicConfig = $derived(query.data);
interface Props {
sharedLink: SharedLinkResponseDto;
@@ -144,7 +148,7 @@
icon={mdiDownload}
/>
{/if}
{#if sharedLink.showMetadata && featureFlagsManager.value.map}
{#if sharedLink.showMetadata && publicConfig.map.enabled}
<AlbumMap {album} />
{/if}
<ThemeButton />
@@ -1,6 +1,9 @@
import type { UserConfigDto } from '@immich/sdk';
import '@testing-library/jest-dom';
import { getResizeObserverMock } from '$lib/__mocks__/resize-observer.mock';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { queryClient } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
import { renderWithTooltips } from '$tests/helpers';
import { assetFactory } from '@test-data/factories/asset-factory';
import { preferencesFactory } from '@test-data/factories/preferences-factory';
@@ -60,6 +63,16 @@ describe('AssetViewerNavBar component', () => {
const user = userAdminFactory.build({ id: ownerId });
const asset = assetFactory.build({ ownerId, isTrashed: false });
authManager.setUser(user);
queryClient.setQueryData(getUserConfigQuery().queryKey, {
machineLearning: {
enabled: true,
clip: { enabled: true },
duplicateDetection: { enabled: true },
facialRecognition: { enabled: true },
ocr: { enabled: true },
},
trash: { enabled: true, days: 30 },
} as UserConfigDto);
const preferences = preferencesFactory.build({ cast: { gCastEnabled: false } });
authManager.setPreferences(preferences);
@@ -8,7 +8,6 @@
import { timeToLoadTheMap } from '$lib/constants';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { Route } from '$lib/route';
import { locale } from '$lib/stores/preferences.store';
import { getAssetMediaUrl } from '$lib/utils';
@@ -34,6 +33,11 @@
import AlbumListItemDetails from './AlbumListItemDetails.svelte';
import DetailPanelPeople from '$lib/components/asset-viewer/DetailPanelPeople.svelte';
import { faceManager } from '$lib/stores/face.svelte';
import { createPreloadedQuery } from '$lib/queries/client';
import { getPublicConfigQuery } from '$lib/queries/config-public.query';
const query = createPreloadedQuery(getPublicConfigQuery);
const publicConfig = $derived(query.data);
interface Props {
asset: AssetResponseDto;
@@ -278,7 +282,7 @@
</div>
</section>
{#if latlng && featureFlagsManager.value.map}
{#if latlng && publicConfig.map.enabled}
<div class="h-90">
{#await import('$lib/components/shared-components/map/Map.svelte')}
{#await delay(timeToLoadTheMap) then}
@@ -4,7 +4,6 @@
import { assetViewerFadeDuration } from '$lib/constants';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { castManager } from '$lib/managers/cast-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { mediaCapabilitiesManager } from '$lib/managers/media-capabilities-manager.svelte';
import { autoPlayVideo, lang, loopVideo as loopVideoPreference } from '$lib/stores/preferences.store';
@@ -45,6 +44,11 @@
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
import './immich-time-range';
import { createPreloadedQuery } from '$lib/queries/client';
import { getPublicConfigQuery } from '$lib/queries/config-public.query';
const query = createPreloadedQuery(getPublicConfigQuery);
const publicConfig = $derived(query.data);
interface Props {
asset: AssetResponseDto;
@@ -78,7 +82,7 @@
let isLoading = $state(true);
let hasLoadedMetadata = $state(false);
let assetFileUrl = $derived.by(() => {
if (featureFlagsManager.value.realtimeTranscoding) {
if (publicConfig.ffmpeg.realtime.enabled) {
return getAssetHlsUrl(assetId);
}
@@ -376,7 +380,7 @@
style:aspect-ratio={aspectRatio}
defaultduration={asset.duration! / 1000}
>
{#if featureFlagsManager.value.realtimeTranscoding}
{#if publicConfig.ffmpeg.realtime.enabled}
<hls-video
bind:this={videoPlayer}
slot="media"
@@ -440,7 +444,7 @@
<span slot="title">{$t('media_chrome.playback_rate')}</span>
</media-playback-rate-menu>
</media-settings-menu-item>
{#if featureFlagsManager.value.realtimeTranscoding}
{#if publicConfig.ffmpeg.realtime.enabled}
<media-settings-menu-item class="mx-1 rounded-lg p-1 ps-2">
{$t('video_quality')}
<Icon slot="suffix" icon={mdiChevronRight} class="m-2" />
@@ -12,7 +12,6 @@
import { afterNavigate } from '$app/navigation';
import OnEvents from '$lib/components/OnEvents.svelte';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import MapSettingsModal from '$lib/modals/MapSettingsModal.svelte';
import { mapSettings } from '$lib/stores/preferences.store';
import { getAssetMediaUrl, handlePromiseError } from '$lib/utils';
@@ -48,6 +47,11 @@
ScaleControl,
} from 'svelte-maplibre';
import type { SelectionBBox } from './types';
import { createPreloadedQuery } from '$lib/queries/client';
import { getPublicConfigQuery } from '$lib/queries/config-public.query';
const query = createPreloadedQuery(getPublicConfigQuery);
const publicConfig = $derived(query.data);
interface Props {
mapMarkers?: MapMarkerResponseDto[];
@@ -111,9 +115,7 @@
let abortController: AbortController;
const mapTheme = $derived($mapSettings.allowDarkMode ? themeManager.value : Theme.Light);
const styleUrl = $derived(
mapTheme === Theme.Dark ? serverConfigManager.value.mapDarkStyleUrl : serverConfigManager.value.mapLightStyleUrl,
);
const styleUrl = $derived(mapTheme === Theme.Dark ? publicConfig.map.darkStyle : publicConfig.map.lightStyle);
export function addClipMapMarker(lng: number, lat: number) {
if (!map) {
@@ -1,10 +1,14 @@
<script lang="ts">
import { getSearchTypeTitle } from './search-bar-utils';
import SearchButton from './SearchButton.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { searchManager } from '$lib/managers/search-manager.svelte';
import { Text } from '@immich/ui';
import { t } from 'svelte-i18n';
import { createPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
const query = createPreloadedQuery(getUserConfigQuery);
const config = $derived(query.data);
let queryType = $derived(searchManager.filter.queryType);
@@ -17,7 +21,7 @@
<fieldset>
<Text class="mb-5">{$t('search_type_description')}</Text>
<div class="flex flex-wrap gap-2">
{#if featureFlagsManager.value.smartSearch}
{#if config.machineLearning.clip.enabled}
<SearchButton checked active={queryType === 'smart'} onclick={() => setType('smart')}>
{$t('context')}
</SearchButton>
@@ -31,7 +35,7 @@
<SearchButton checked active={queryType === 'fullPath'} onclick={() => setType('fullPath')}>
{getSearchTypeTitle('fullPath')}
</SearchButton>
{#if featureFlagsManager.value.ocr}
{#if config.machineLearning.ocr.enabled}
<SearchButton checked active={queryType === 'ocr'} onclick={() => setType('ocr')}>
{getSearchTypeTitle('ocr')}
</SearchButton>
@@ -1,6 +1,10 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigDefaultsQuery, getConfigQuery, updateConfigMutation } from '$lib/queries/system-config.query';
import {
getAdminConfigDefaultsQuery,
getAdminConfigQuery,
updateAdminConfigMutation,
} from '$lib/queries/config-admin.query';
import type { AdminConfigDto } from '@immich/sdk';
import { Button, toastManager } from '@immich/ui';
import { createMutation } from '@tanstack/svelte-query';
@@ -16,19 +20,24 @@
let { disabled, keys, configToEdit = $bindable(), onBeforeSave }: Props = $props();
const config = createPreloadedQuery(getConfigQuery);
const defaults = createPreloadedQuery(getConfigDefaultsQuery);
const save = createMutation(updateConfigMutation);
const queries = {
config: createPreloadedQuery(getAdminConfigQuery),
defaults: createPreloadedQuery(getAdminConfigDefaultsQuery),
};
const showResetToDefault = $derived(!isEqual(pick(config.data, keys), pick(defaults.data, keys)));
const config = $derived(queries.config.data);
const defaults = $derived(queries.defaults.data);
const save = createMutation(updateAdminConfigMutation);
const showResetToDefault = $derived(!isEqual(pick(config, keys), pick(defaults, keys)));
const handleReset = () => {
configToEdit = cloneDeep(config.data);
configToEdit = cloneDeep(config);
toastManager.info($t('admin.reset_settings_to_recent_saved'));
};
const handleResetToDefault = () => {
configToEdit = { ...configToEdit, ...cloneDeep(pick(defaults.data, keys)) };
configToEdit = { ...configToEdit, ...cloneDeep(pick(defaults, keys)) };
toastManager.info($t('admin.reset_settings_to_default'));
};
@@ -13,10 +13,13 @@
const { serverVersion, connected } = websocketStore;
const aboutQuery = createQuery(getAboutInfoQuery);
const info = $derived(aboutQuery.data);
const historyQuery = createQuery(getVersionHistoryQuery);
const versions = $derived(historyQuery.data ?? []);
const queries = {
about: createQuery(getAboutInfoQuery),
history: createQuery(getVersionHistoryQuery),
};
const info = $derived(queries.about.data);
const versions = $derived(queries.history.data ?? []);
let isMain = $derived(info?.sourceRef === 'main' && info.repository === 'immich-app/immich');
let version = $derived($serverVersion ? semverToName($serverVersion) : null);
@@ -3,7 +3,6 @@
import RecentAlbums from '$lib/components/shared-components/side-bar/RecentAlbums.svelte';
import Sidebar from '$lib/components/sidebar/Sidebar.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { Route } from '$lib/route';
import { recentAlbumsDropdown } from '$lib/stores/preferences.store';
import { NavbarGroup, NavbarItem } from '@immich/ui';
@@ -37,6 +36,11 @@
} from '@mdi/js';
import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition';
import { createPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
const query = createPreloadedQuery(getUserConfigQuery);
const config = $derived(query.data);
</script>
<Sidebar ariaLabel={$t('primary')}>
@@ -44,7 +48,7 @@
<NavbarItem title={$t('explore')} href={Route.explore()} icon={mdiMagnify} />
{#if featureFlagsManager.value.map}
{#if config.map.enabled}
<NavbarItem title={$t('map')} href={Route.map()} icon={mdiMapOutline} activeIcon={mdiMap} />
{/if}
@@ -111,7 +115,7 @@
<NavbarItem title={$t('locked_folder')} href={Route.locked()} icon={mdiLockOutline} activeIcon={mdiLock} />
{#if featureFlagsManager.value.trash}
{#if config.trash.enabled}
<NavbarItem title={$t('trash')} href={Route.trash()} icon={mdiTrashCanOutline} activeIcon={mdiTrashCan} />
{/if}
+10 -1
View File
@@ -10,6 +10,8 @@ import { goto } from '$app/navigation';
import { page } from '$app/state';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { queryClient } from '$lib/queries/client';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
import { getAboutInfoQuery } from '$lib/queries/server.query';
import { Route } from '$lib/route';
import { isSharedLinkRoute } from '$lib/utils/navigation';
@@ -62,7 +64,14 @@ class AuthManager {
async refresh() {
try {
const [user, preferences] = await Promise.all([getMyUser(), getMyPreferences()]);
const [user, preferences] = await Promise.all([
getMyUser(),
getMyPreferences(),
queryClient.query(getUserConfigQuery()),
]);
if (user.isAdmin) {
await queryClient.query(getAdminConfigQuery());
}
this.#preferences = preferences;
this.#user = user;
@@ -1,30 +0,0 @@
import { getServerConfig, type ServerConfigDto } from '@immich/sdk';
import { eventManager } from '$lib/managers/event-manager.svelte';
class ServerConfigManager {
#value?: ServerConfigDto = $state();
constructor() {
eventManager.on({
SystemConfigUpdate: () => this.loadServerConfig(),
});
}
async init() {
await this.loadServerConfig();
}
get value() {
if (!this.#value) {
throw new Error('Server config manager must be initialized first');
}
return this.#value;
}
async loadServerConfig() {
this.#value = await getServerConfig();
}
}
export const serverConfigManager = new ServerConfigManager();
+6 -2
View File
@@ -1,9 +1,13 @@
<script lang="ts">
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { handleResetPinCode } from '$lib/services/user.service';
import { BasicModal, Field, FormModal, HelperText, PasswordInput, Stack, type ModalSize } from '@immich/ui';
import { mdiLockReset } from '@mdi/js';
import { t } from 'svelte-i18n';
import { createPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
const query = createPreloadedQuery(getUserConfigQuery);
const config = $derived(query.data);
type Props = {
onClose: (success?: true) => void;
@@ -23,7 +27,7 @@
const common = $derived({ title: $t('reset'), size: 'small' as ModalSize, icon: mdiLockReset, onClose });
</script>
{#if featureFlagsManager.value.passwordLogin}
{#if config.passwordLogin.enabled}
<FormModal {...common} submitColor="danger" submitText={$t('reset')} disabled={!password} {onSubmit}>
<Stack gap={4}>
<div>{$t('reset_pin_code_description')}</div>
@@ -1,12 +1,15 @@
<script lang="ts">
import FormatMessage from '$lib/elements/FormatMessage.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { executeMutation } from '$lib/queries/client';
import { createPreloadedQuery, executeMutation } from '$lib/queries/client';
import { deleteUserAdminMutation } from '$lib/queries/user-admin.query';
import { type UserAdminResponseDto } from '@immich/sdk';
import { Alert, Checkbox, ConfirmModal, Field, Input, Label, Text } from '@immich/ui';
import { mdiTrashCanOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
const query = createPreloadedQuery(getUserConfigQuery);
const config = $derived(query.data);
type Props = {
user: UserAdminResponseDto;
@@ -48,10 +51,7 @@
{/snippet}
</FormatMessage>
{:else}
<FormatMessage
key="admin.user_delete_delay"
values={{ user: user.name, delay: serverConfigManager.value.userDeleteDelay }}
>
<FormatMessage key="admin.user_delete_delay" values={{ user: user.name, delay: config.user.deleteDelay }}>
{#snippet children({ message })}
<b>{message}</b>
{/snippet}
+45 -28
View File
@@ -1,12 +1,12 @@
import { toastManager } from '@immich/ui';
import {
createQuery,
hashKey,
MutationCache,
mutationOptions,
MutationObserver,
QueryClient,
type CreateMutationOptions,
type DataTag,
type DefaultError,
type DefinedCreateQueryResult,
type MutateOptions,
@@ -17,6 +17,8 @@ import {
import { noop } from 'lodash-es';
import type { Translations } from 'svelte-i18n';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { getPublicConfigQuery } from '$lib/queries/config-public.query';
import { getServerConfigQuery } from '$lib/queries/server.query';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
@@ -67,7 +69,15 @@ const createQueryClient = () => {
}),
});
eventManager.on({ AuthLogout: () => client.clear() });
const publicQueryHashes = new Set(
[getServerConfigQuery(), getPublicConfigQuery()].map(({ queryKey }) => hashKey(queryKey)),
);
eventManager.on({
AuthLogout: () => {
client.removeQueries({ predicate: ({ queryHash }) => !publicQueryHashes.has(queryHash) });
client.getMutationCache().clear();
},
});
return client;
};
@@ -79,35 +89,42 @@ export const asMutation = <TArgs extends unknown[], TData, TError = DefaultError
options?: Omit<CreateMutationOptions<TData, TError, TArgs, TOnMutateResult>, 'mutationFn' | 'mutationKey'>,
) => mutationOptions({ ...options, mutationFn: (args: TArgs) => fn(...args) });
export const createPreloadedQuery = <
TQueryFnData,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
>(
optionsFn: () => UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> & {
queryKey: DataTag<TQueryKey, TQueryFnData, TError>;
},
const getPreloadedData = <TQueryFnData>(queryKey: QueryKey) => {
const data = queryClient.getQueryData<TQueryFnData, QueryKey, NonUndefinedGuard<TQueryFnData>>(queryKey);
if (data === undefined) {
const error = new Error(`${queryKey.join('/')} must be loaded first`);
console.error(error);
throw error;
}
return data;
};
export const readPreloadedQuery = <TQueryFnData, TError, TData, TQueryKey extends QueryKey>(
options: UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
): TData => {
const data = getPreloadedData<TQueryFnData>(options.queryKey);
return options.select ? options.select(data) : (data as unknown as TData);
};
export const createPreloadedQuery = <TQueryFnData, TError, TData, TQueryKey extends QueryKey>(
optionsFn: () => UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
): DefinedCreateQueryResult<TData, TError> =>
createQuery(() => {
const options = optionsFn();
const initialData = queryClient.getQueryData<TQueryFnData, TQueryKey, NonUndefinedGuard<TQueryFnData>>(
options.queryKey,
);
if (initialData === undefined) {
throw new Error(`${options.queryKey.join('/')} must be loaded first`);
}
createQuery(
() => {
const options = optionsFn();
return { ...options, initialData: getPreloadedData<TQueryFnData>(options.queryKey) };
},
() => queryClient,
);
return { ...options, initialData };
});
type MutationArgs<TData, TError, TVariables, TOnMutateResult> = TVariables extends void
? [variables?: TVariables, mutateOptions?: MutateOptions<TData, TError, TVariables, TOnMutateResult>]
: [variables: TVariables, mutateOptions?: MutateOptions<TData, TError, TVariables, TOnMutateResult>];
type MutationArgs<TData, TError, TVariables, TContext> = TVariables extends void
? [variables?: TVariables, mutateOptions?: MutateOptions<TData, TError, TVariables, TContext>]
: [variables: TVariables, mutateOptions?: MutateOptions<TData, TError, TVariables, TContext>];
export const executeMutation = <TData, TError, TVariables, TContext>(
options: () => CreateMutationOptions<TData, TError, TVariables, TContext>,
...[variables, mutateOptions]: MutationArgs<TData, TError, TVariables, TContext>
export const executeMutation = <TData, TError, TVariables, TOnMutateResult>(
options: () => CreateMutationOptions<TData, TError, TVariables, TOnMutateResult>,
...[variables, mutateOptions]: MutationArgs<TData, TError, TVariables, TOnMutateResult>
) => {
const observer = new MutationObserver(queryClient, options());
const unsubscribe = observer.subscribe(noop);
+51
View File
@@ -0,0 +1,51 @@
import { getAdminConfig, getAdminConfigDefaults, updateAdminConfig, type AdminConfigDto } from '@immich/sdk';
import { toastManager } from '@immich/ui';
import { mutationOptions, queryOptions } from '@tanstack/svelte-query';
import { isEqual } from 'lodash-es';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { queryClient } from '$lib/queries/client';
import { getPublicConfigQuery } from '$lib/queries/config-public.query';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
import { getFormatter } from '$lib/utils/i18n';
export const getAdminConfigQuery = () =>
queryOptions({
queryKey: ['config', 'admin'],
queryFn: ({ signal }) => getAdminConfig({ signal }),
});
export const getAdminConfigDefaultsQuery = () =>
queryOptions({
queryKey: ['config', 'admin', 'defaults'],
queryFn: ({ signal }) => getAdminConfigDefaults({ signal }),
});
export const updateAdminConfigMutation = () =>
mutationOptions({
mutationFn: async (update: Partial<AdminConfigDto>) => {
const config = await getAdminConfig();
const adminConfigDto = { ...config, ...update };
if (isEqual(config, adminConfigDto)) {
return;
}
return updateAdminConfig({ adminConfigDto });
},
onSuccess: async (config) => {
if (!config) {
return;
}
queryClient.setQueryData(getAdminConfigQuery().queryKey, config);
await Promise.all([
queryClient.invalidateQueries({ queryKey: getUserConfigQuery().queryKey }),
queryClient.invalidateQueries({ queryKey: getPublicConfigQuery().queryKey }),
]);
eventManager.emit('SystemConfigUpdate', config);
const $t = await getFormatter();
toastManager.primary($t('settings_saved'));
},
meta: { error: { toast: 'errors.unable_to_save_settings' } },
});
@@ -0,0 +1,8 @@
import { getPublicConfig } from '@immich/sdk';
import { queryOptions } from '@tanstack/svelte-query';
export const getPublicConfigQuery = () =>
queryOptions({
queryKey: ['config', 'public'],
queryFn: ({ signal }) => getPublicConfig({ signal }),
});
+27
View File
@@ -0,0 +1,27 @@
import { getUserConfig, type UserConfigDto } from '@immich/sdk';
import { queryOptions } from '@tanstack/svelte-query';
import { cloneDeep } from 'lodash-es';
const withEffectiveFlags = (dto: UserConfigDto) => {
const config = cloneDeep(dto);
const { machineLearning } = config;
if (!machineLearning.enabled) {
machineLearning.clip.enabled = false;
machineLearning.facialRecognition.enabled = false;
machineLearning.ocr.enabled = false;
}
if (!machineLearning.clip.enabled) {
machineLearning.duplicateDetection.enabled = false;
}
return config;
};
export const getUserConfigQuery = () =>
queryOptions({
queryKey: ['config', 'user'],
queryFn: ({ signal }) => getUserConfig({ signal }),
select: withEffectiveFlags,
});
+7 -1
View File
@@ -1,4 +1,4 @@
import { getAboutInfo, getServerStatistics, getStorage, getVersionHistory } from '@immich/sdk';
import { getAboutInfo, getServerConfig, getServerStatistics, getStorage, getVersionHistory } from '@immich/sdk';
import { queryOptions } from '@tanstack/svelte-query';
export const getAboutInfoQuery = () =>
@@ -19,6 +19,12 @@ export const getStorageQuery = () =>
queryFn: ({ signal }) => getStorage({ signal }),
});
export const getServerConfigQuery = () =>
queryOptions({
queryKey: ['server', 'config'],
queryFn: ({ signal }) => getServerConfig({ signal }),
});
export const getServerStatisticsQuery = () =>
queryOptions({
queryKey: ['server', 'statistics'],
@@ -1,45 +0,0 @@
import { getConfig, getConfigDefaults, updateConfig, type AdminConfigDto } from '@immich/sdk';
import { toastManager } from '@immich/ui';
import { mutationOptions, queryOptions } from '@tanstack/svelte-query';
import { isEqual } from 'lodash-es';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { queryClient } from '$lib/queries/client';
import { getFormatter } from '$lib/utils/i18n';
export const getConfigQuery = () =>
queryOptions({
queryKey: ['system-config'],
queryFn: ({ signal }) => getConfig({ signal }),
});
export const getConfigDefaultsQuery = () =>
queryOptions({
queryKey: ['system-config', 'defaults'],
queryFn: ({ signal }) => getConfigDefaults({ signal }),
});
export const updateConfigMutation = () =>
mutationOptions({
mutationFn: async (update: Partial<AdminConfigDto>) => {
const config = await getConfig();
const adminConfigDto = { ...config, ...update };
if (isEqual(config, adminConfigDto)) {
return;
}
return updateConfig({ adminConfigDto });
},
onSuccess: async (config) => {
if (!config) {
return;
}
queryClient.setQueryData(getConfigQuery().queryKey, config);
eventManager.emit('SystemConfigUpdate', config);
const $t = await getFormatter();
toastManager.primary($t('settings_saved'));
},
meta: { error: { toast: 'errors.unable_to_save_settings' } },
});
+12 -7
View File
@@ -1,7 +1,9 @@
import { getAssetInfo } from '@immich/sdk';
import { getAssetInfo, type UserConfigDto } from '@immich/sdk';
import { toastManager } from '@immich/ui';
import { vitest } from 'vitest';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { queryClient } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
import { getAssetActions, handleDownloadAsset } from '$lib/services/asset.service';
import { setSharedLink } from '$lib/utils';
import { getFormatter } from '$lib/utils/i18n';
@@ -31,16 +33,19 @@ vitest.mock('$lib/utils', async () => {
};
});
vi.mock(import('$lib/managers/feature-flags-manager.svelte'), function () {
return {
featureFlagsManager: { init: vi.fn(), loadFeatureFlags: vi.fn(), value: {} } as never,
};
});
describe('AssetService', () => {
describe('getAssetActions', () => {
beforeEach(() => {
authManager.setPreferences(preferencesFactory.build());
queryClient.setQueryData(getUserConfigQuery().queryKey, {
machineLearning: {
enabled: false,
clip: { enabled: false },
duplicateDetection: { enabled: false },
facialRecognition: { enabled: false },
ocr: { enabled: false },
},
} as UserConfigDto);
});
it('should allow shared link downloads if the user owns the asset and shared link downloads are disabled', () => {
+3 -2
View File
@@ -48,11 +48,12 @@ import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manage
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import AssetAddToAlbumModal from '$lib/modals/AssetAddToAlbumModal.svelte';
import AssetTagModal from '$lib/modals/AssetTagModal.svelte';
import ProfileImageCropperModal from '$lib/modals/ProfileImageCropperModal.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
import { readPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
import { Route } from '$lib/route';
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { getAssetMediaUrl, getSharedLink, sleep } from '$lib/utils';
@@ -149,7 +150,7 @@ export const getAssetActions = (
const authUser = authManager.authenticated ? authManager.user : undefined;
const isOwner = !!(authUser && authUser.id === asset.ownerId);
const isAlbumOwner = !!(authUser && authUser.id === album?.albumUsers[0].user.id);
const smartSearchEnabled = featureFlagsManager.value.smartSearch;
const smartSearchEnabled = !!authUser && readPreloadedQuery(getUserConfigQuery()).machineLearning.clip.enabled;
const Share: ActionItem = {
title: $t('share'),
@@ -1,13 +1,9 @@
import type { ServerConfigDto } from '@immich/sdk';
import { asUrl } from '$lib/services/shared-link.service';
import { sharedLinkFactory } from '@test-data/factories/shared-link-factory';
vi.mock(import('$lib/managers/server-config-manager.svelte'), () => ({
serverConfigManager: {
value: { externalDomain: 'http://localhost:2283' } as ServerConfigDto,
init: vi.fn(),
loadServerConfig: vi.fn(),
},
vi.mock(import('$lib/queries/client'), async (importOriginal) => ({
...(await importOriginal()),
readPreloadedQuery: vi.fn().mockReturnValue({ server: { externalDomain: 'http://localhost:2283' } }),
}));
describe('SharedLinkService', () => {
+3 -2
View File
@@ -13,8 +13,9 @@ import { mdiContentCopy, mdiLink, mdiPencilOutline, mdiQrcode, mdiTrashCanOutlin
import type { MessageFormatter } from 'svelte-i18n';
import { goto } from '$app/navigation';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
import { readPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
import { Route } from '$lib/route';
import { copyToClipboard } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
@@ -61,7 +62,7 @@ export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLin
export const asUrl = (sharedLink: SharedLinkResponseDto) => {
const path = Route.viewSharedLink(sharedLink);
return new URL(path, serverConfigManager.value.externalDomain || location.origin).href;
return new URL(path, readPreloadedQuery(getUserConfigQuery()).server.externalDomain || location.origin).href;
};
export const handleCreateSharedLink = async (dto: SharedLinkCreateDto) => {
@@ -3,7 +3,7 @@ import type { ActionItem } from '@immich/ui';
import { mdiContentCopy, mdiDownload, mdiUpload } from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n';
import { executeMutation } from '$lib/queries/client';
import { updateConfigMutation } from '$lib/queries/system-config.query';
import { updateAdminConfigMutation } from '$lib/queries/config-admin.query';
import { copyToClipboard, downloadJson } from '$lib/utils';
export const getSystemConfigActions = (
@@ -55,7 +55,7 @@ const handleUploadConfig = () => {
}
const reader = async () => {
const text = await file.text();
await executeMutation(updateConfigMutation, JSON.parse(text));
await executeMutation(updateAdminConfigMutation, JSON.parse(text));
};
reader()
.catch((error) => console.error('Error handling JSON config upload', error))
+9 -5
View File
@@ -13,11 +13,11 @@ import { DateTime } from 'luxon';
import type { MessageFormatter } from 'svelte-i18n';
import { goto } from '$app/navigation';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import PasswordResetSuccessModal from '$lib/modals/PasswordResetSuccessModal.svelte';
import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte';
import { executeMutation } from '$lib/queries/client';
import { executeMutation, readPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
import { updateUserAdminMutation } from '$lib/queries/user-admin.query';
import { Route } from '$lib/route';
import type { HeaderButtonActionItem } from '$lib/types';
@@ -58,8 +58,10 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
shortcutOptions: { ignoreInputFields: true },
};
const getDeleteDate = (deletedAt: string): Date =>
DateTime.fromISO(deletedAt).plus({ days: serverConfigManager.value.userDeleteDelay }).toJSDate();
const getDeleteDate = (deletedAt: string): Date => {
const userConfig = readPreloadedQuery(getUserConfigQuery());
return DateTime.fromISO(deletedAt).plus({ days: userConfig.user.deleteDelay }).toJSDate();
};
const Restore: HeaderButtonActionItem = {
icon: mdiDeleteRestore,
@@ -96,7 +98,9 @@ const generatePassword = (length: number = 16) => {
const characterSet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,.-{}+!#$%/()=?';
for (let i = 0; i < length; i++) {
let randomNumber = crypto.getRandomValues(new Uint32Array(1))[0];
const buffer = new Uint32Array(1);
crypto.getRandomValues(buffer);
let randomNumber = buffer[0];
randomNumber /= 2 ** 32;
randomNumber = Math.floor(randomNumber * characterSet.length);
+5 -5
View File
@@ -15,7 +15,7 @@ import { page } from '$app/state';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { queryClient } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getServerConfigQuery } from '$lib/queries/server.query';
import { searchUsersAdminQuery } from '$lib/queries/user-admin.query';
import { Route } from '$lib/route';
import { maintenanceStore } from '$lib/stores/maintenance.store';
@@ -78,10 +78,10 @@ websocket
void queryClient.invalidateQueries({ queryKey: ['server'] });
}
})
.on(
'on_config_update',
() => void queryClient.invalidateQueries({ queryKey: getConfigQuery().queryKey, exact: true }),
)
.on('on_config_update', () => {
void queryClient.invalidateQueries({ queryKey: ['config'] });
void queryClient.invalidateQueries({ queryKey: getServerConfigQuery().queryKey });
})
.on('AppRestartV1', (mode) => websocketStore.serverRestarting.set(mode))
.on('MaintenanceStatusV1', (status) => {
maintenanceStore.status.set(status);
+6 -3
View File
@@ -2,8 +2,8 @@ import { setServerLicense, setUserLicense, type LicenseResponseDto } from '@immi
import { PUBLIC_IMMICH_BUY_HOST, PUBLIC_IMMICH_PAY_HOST } from '$env/static/public';
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 { queryClient, readPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
import { getAboutInfoQuery } from '$lib/queries/server.query';
export const activateProduct = async (licenseKey: string, activationKey: string): Promise<LicenseResponseDto> => {
@@ -34,6 +34,9 @@ export const getActivationKey = async (licenseKey: string): Promise<string> => {
export const getLicenseLink = (license: ImmichProduct) => {
const url = new URL('/', PUBLIC_IMMICH_BUY_HOST);
url.searchParams.append('productId', license);
url.searchParams.append('instanceUrl', serverConfigManager.value.externalDomain || globalThis.origin);
url.searchParams.append(
'instanceUrl',
readPreloadedQuery(getUserConfigQuery()).server.externalDomain || globalThis.origin,
);
return url.href;
};
+6 -4
View File
@@ -2,7 +2,9 @@ import { setFetch } from '@immich/sdk';
import { memoize } from 'lodash-es';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { queryClient } from '$lib/queries/client';
import { getPublicConfigQuery } from '$lib/queries/config-public.query';
import { getServerConfigQuery } from '$lib/queries/server.query';
import { initLanguage } from '$lib/utils';
type Fetch = typeof fetch;
@@ -13,11 +15,11 @@ async function _init(fetch: Fetch) {
// https://github.com/oazapfts/oazapfts/blob/main/README.md#fetch-options
setFetch(fetch);
await initLanguage();
await serverConfigManager.init();
const serverConfig = await queryClient.query(getServerConfigQuery());
await authManager.load();
if (!serverConfigManager.value.maintenanceMode) {
await featureFlagsManager.init();
if (!serverConfig.maintenanceMode) {
await Promise.all([queryClient.query(getPublicConfigQuery()), featureFlagsManager.init()]);
}
}
@@ -29,7 +29,6 @@
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { Route } from '$lib/route';
@@ -68,6 +67,11 @@
import AlbumTitle from './AlbumTitle.svelte';
import ActionMenuItem from '$lib/components/ActionMenuItem.svelte';
import { delay } from '$lib/utils/asset-utils';
import { createPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
const query = createPreloadedQuery(getUserConfigQuery);
const config = $derived(query.data);
interface Props {
data: PageData;
@@ -518,7 +522,7 @@
<ActionButton action={Actions.Share} />
{#if featureFlagsManager.value.map}
{#if config.map.enabled}
<AlbumMap {album} />
{/if}
@@ -6,7 +6,6 @@
import { timeToLoadTheMap } from '$lib/constants';
import Portal from '$lib/elements/Portal.svelte';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { Route } from '$lib/route';
import { handlePromiseError } from '$lib/utils';
import { delay } from '$lib/utils/asset-utils';
@@ -15,6 +14,11 @@
import { LoadingSpinner } from '@immich/ui';
import { onDestroy } from 'svelte';
import type { PageData } from './$types';
import { createPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
const query = createPreloadedQuery(getUserConfigQuery);
const config = $derived(query.data);
interface Props {
data: PageData;
@@ -35,9 +39,11 @@
assetViewerManager.showAssetViewer(false);
});
if (!featureFlagsManager.value.map) {
handlePromiseError(goto(Route.photos()));
}
$effect(() => {
if (!config.map.enabled) {
handlePromiseError(goto(Route.photos()));
}
});
async function onViewAssets(assetIds: string[]) {
await assetViewerManager.setAssetId(assetIds[0]);
@@ -53,7 +59,7 @@
}
</script>
{#if featureFlagsManager.value.map}
{#if config.map.enabled}
<UserPageLayout title={data.meta.title}>
<div class="isolate flex size-full flex-col sm:flex-row">
<div
@@ -18,7 +18,6 @@
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
import { QueryParameter } from '$lib/constants';
import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { searchManager } from '$lib/managers/search-manager.svelte';
import type { Viewport } from '$lib/managers/timeline-manager/types';
import { Route } from '$lib/route';
@@ -44,6 +43,11 @@
import { mdiArrowLeft, mdiClose, mdiDotsVertical, mdiImageOffOutline, mdiSelectAll } from '@mdi/js';
import { onMount, tick, untrack } from 'svelte';
import { t } from 'svelte-i18n';
import { createPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
const query = createPreloadedQuery(getUserConfigQuery);
const config = $derived(query.data);
const viewport: Viewport = $state({ width: 0, height: 0 });
let searchResultsElement: HTMLElement | undefined = $state();
@@ -62,7 +66,6 @@
type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query' | 'queryAssetId'>;
let searchQuery = $derived(page.url.searchParams.get(QueryParameter.QUERY));
let smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch);
let terms = $derived<SearchTerms>(searchQuery ? JSON.parse(searchQuery) : {});
let searchTermKeys = $derived(getObjectKeys(terms));
@@ -139,7 +142,7 @@
try {
const { albums, assets } =
('query' in searchDto || 'queryAssetId' in searchDto) && smartSearchEnabled
('query' in searchDto || 'queryAssetId' in searchDto) && config.machineLearning.clip.enabled
? await searchSmart({
smartSearchDto: { visibility: AssetVisibility.Timeline, ...searchDto, language: $lang },
})
@@ -9,14 +9,17 @@
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
import Timeline from '$lib/components/timeline/Timeline.svelte';
import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { Route } from '$lib/route';
import { getTrashActions } from '$lib/services/trash.service';
import { handlePromiseError } from '$lib/utils';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
import { createPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
const query = createPreloadedQuery(getUserConfigQuery);
const config = $derived(query.data);
type Props = {
data: PageData;
@@ -27,7 +30,7 @@
let timelineManager = $state<TimelineManager>() as TimelineManager;
const options = { isTrashed: true };
if (!featureFlagsManager.value.trash) {
if (!config.trash.enabled) {
handlePromiseError(goto(Route.photos()));
}
@@ -43,7 +46,7 @@
const { Empty, RestoreAll } = $derived(getTrashActions($t, timelineManager?.assetCount ?? 0));
</script>
{#if featureFlagsManager.value.trash}
{#if config.trash.enabled}
<UserPageLayout
hideNavbar={assetMultiSelectManager.selectionActive}
actions={assetMultiSelectManager.selectionActive ? [] : [Empty, RestoreAll]}
@@ -59,7 +62,7 @@
>
<p class="p-4 font-medium text-gray-500/60 dark:text-gray-300/60">
{$t('trashed_items_will_be_permanently_deleted_after', {
values: { days: serverConfigManager.value.trashDays },
values: { days: config.trash.days },
})}
</p>
{#snippet empty()}
@@ -1,5 +1,4 @@
<script lang="ts">
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import SettingAccordion from '$lib/components/shared-components/settings/SettingAccordion.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { handleError } from '$lib/utils/handle-error';
@@ -7,6 +6,11 @@
import { Button, Field, NumberInput, Select, Switch, toastManager } from '@immich/ui';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
import { createPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
const query = createPreloadedQuery(getUserConfigQuery);
const config = $derived(query.data);
// Albums
let defaultAssetOrder = $state(authManager.preferences.albums?.defaultAssetOrder ?? AssetOrder.Desc);
@@ -23,7 +27,9 @@
// People
let peopleEnabled = $state(authManager.preferences.people?.enabled ?? false);
let peopleSidebar = $state(authManager.preferences.people?.sidebarWeb ?? false);
let peopleMinFaces = $state(authManager.preferences.people?.minimumFaces ?? serverConfigManager.value.minFaces);
let peopleMinFaces = $state(
authManager.preferences.people?.minimumFaces ?? config.machineLearning.facialRecognition.minFaces,
);
let peopleUpdateStrategy = $state(authManager.preferences.people?.updateStrategy ?? PersonUpdateStrategy.Everyone);
// Ratings
@@ -1,14 +1,23 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { oauth } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { Button, LoadingSpinner, toastManager } from '@immich/ui';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
import { createPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
import { getServerConfigQuery } from '$lib/queries/server.query';
const queries = {
config: createPreloadedQuery(getUserConfigQuery),
serverConfig: createPreloadedQuery(getServerConfigQuery),
};
const config = $derived(queries.config.data);
const serverConfig = $derived(queries.serverConfig.data);
let loading = $state(true);
@@ -47,10 +56,10 @@
<div class="flex place-content-center place-items-center">
<LoadingSpinner />
</div>
{:else if featureFlagsManager.value.oauth}
{:else if config.oauth.enabled}
{#if authManager.user.oauthId}
{#if serverConfigManager.value.oauthAccountManagementUrl}
<Button shape="round" size="small" href={serverConfigManager.value.oauthAccountManagementUrl}
{#if serverConfig.oauthAccountManagementUrl}
<Button shape="round" size="small" href={serverConfig.oauthAccountManagementUrl}
>{$t('manage_oauth_account')}</Button
>
{/if}
@@ -7,7 +7,6 @@
import UserPurchaseSettings from './UserPurchaseSettings.svelte';
import UserUsageStatistic from './UserUsageStatistic.svelte';
import { OpenQueryParam, QueryParameter } from '$lib/constants';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { oauth } from '$lib/utils';
import { type ApiKeyResponseDto, type PasskeyResponseDto, type SessionResponseDto } from '@immich/sdk';
import {
@@ -32,11 +31,16 @@
import AppUpdateSettings from './AppUpdateSettings.svelte';
import ChangePasswordSettings from './ChangePasswordSettings.svelte';
import DeviceList from './DeviceList.svelte';
import OauthSettings from './OauthSettings.svelte';
import OAuthSettings from './OAuthSettings.svelte';
import SharingSettings from './SharingSettings.svelte';
import UserApiKeyList from './UserApiKeyList.svelte';
import UserPasskeyList from './UserPasskeyList.svelte';
import UserProfileSettings from './UserProfileSettings.svelte';
import { createPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
const query = createPreloadedQuery(getUserConfigQuery);
const config = $derived(query.data);
interface Props {
keys?: ApiKeyResponseDto[];
@@ -127,7 +131,7 @@
<NotificationsSettings />
</SettingAccordion>
{#if featureFlagsManager.value.oauth}
{#if config.oauth.enabled}
<SettingAccordion
icon={mdiTwoFactorAuthentication}
key={OpenQueryParam.OAUTH}
@@ -135,7 +139,7 @@
subtitle={$t('manage_your_oauth_connection')}
isOpen={oauthOpen || undefined}
>
<OauthSettings />
<OAuthSettings />
</SettingAccordion>
{/if}
@@ -6,7 +6,6 @@
import LinkToDocs from './LinkToDocs.svelte';
import DuplicatesCompareControl from './DuplicatesCompareControl.svelte';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { languageManager } from '$lib/managers/language-manager.svelte';
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
import { Route } from '$lib/route';
@@ -26,6 +25,11 @@
} from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
import { createPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
const query = createPreloadedQuery(getUserConfigQuery);
const config = $derived(query.data);
interface Props {
data: PageData;
@@ -90,14 +94,14 @@
return;
}
const message = featureFlagsManager.value.trash
const message = config.trash.enabled
? $t('assets_moved_to_trash_count', { values: { count: trashedCount } })
: $t('permanently_deleted_assets_count', { values: { count: trashedCount } });
toastManager.primary(message);
};
const handleResolve = async (duplicateId: string, duplicateAssetIds: string[], trashIds: string[]) => {
const forceDelete = !featureFlagsManager.value.trash;
const forceDelete = !config.trash.enabled;
const shouldConfirmDelete = trashIds.length > 0 && forceDelete;
return withConfirmation(
@@ -141,7 +145,7 @@
});
let prompt, confirmText;
if (featureFlagsManager.value.trash) {
if (config.trash.enabled) {
prompt = $t('bulk_trash_duplicates_confirmation', { values: { count: idsToDelete.length } });
confirmText = $t('confirm');
} else {
+5 -5
View File
@@ -1,7 +1,8 @@
import { commandPaletteManager } from '@immich/ui';
import { goto } from '$app/navigation';
import { languageManager } from '$lib/managers/language-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { readPreloadedQuery } from '$lib/queries/client';
import { getServerConfigQuery } from '$lib/queries/server.query';
import { maintenanceCreateUrl, maintenanceReturnUrl, maintenanceShouldRedirect } from '$lib/utils/maintenance';
import { init } from '$lib/utils/server';
import type { LayoutLoad } from './$types';
@@ -14,10 +15,9 @@ export const load = (async ({ fetch, url }) => {
try {
await init(fetch);
if (maintenanceShouldRedirect(serverConfigManager.value.maintenanceMode, url)) {
await goto(
serverConfigManager.value.maintenanceMode ? maintenanceCreateUrl(url) : maintenanceReturnUrl(url.searchParams),
);
const { maintenanceMode } = readPreloadedQuery(getServerConfigQuery());
if (maintenanceShouldRedirect(maintenanceMode, url)) {
await goto(maintenanceMode ? maintenanceCreateUrl(url) : maintenanceReturnUrl(url.searchParams));
}
} catch (initError) {
error = initError;
+5 -3
View File
@@ -1,6 +1,7 @@
import { redirect } from '@sveltejs/kit';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { readPreloadedQuery } from '$lib/queries/client';
import { getServerConfigQuery } from '$lib/queries/server.query';
import { Route } from '$lib/route';
import { getFormatter } from '$lib/utils/i18n';
import { init } from '$lib/utils/server';
@@ -13,7 +14,8 @@ export const load = (async ({ fetch }) => {
try {
await init(fetch);
if (serverConfigManager.value.maintenanceMode) {
const serverConfig = readPreloadedQuery(getServerConfigQuery());
if (serverConfig.maintenanceMode) {
redirect(307, Route.maintenanceMode());
}
@@ -22,7 +24,7 @@ export const load = (async ({ fetch }) => {
redirect(307, Route.photos());
}
if (serverConfigManager.value.isInitialized) {
if (serverConfig.isInitialized) {
// Redirect to login page if there exists an admin account (i.e. server is initialized)
redirect(307, Route.login());
}
@@ -18,11 +18,13 @@
const { library }: Props = $props();
const ownerQuery = createPreloadedQuery(() => getUserAdminQuery(library.ownerId));
const owner = $derived(ownerQuery.data);
const queries = {
owner: createPreloadedQuery(() => getUserAdminQuery(library.ownerId)),
statistics: createQuery(() => getLibraryStatisticsQuery(library.id)),
};
const query = createQuery(() => getLibraryStatisticsQuery(library.id));
const stats = $derived(query.data);
const owner = $derived(queries.owner.data);
const stats = $derived(queries.statistics.data);
const actions = $derived.by(() => {
const { Detail, Scan, Edit, Delete } = getLibraryActions($t, library);
@@ -23,11 +23,13 @@
const { children, params }: LayoutProps = $props();
const libraryQuery = createPreloadedQuery(() => getLibraryQuery(params.id));
const library = $derived(libraryQuery.data);
const queries = {
library: createPreloadedQuery(() => getLibraryQuery(params.id)),
statistics: createQuery(() => getLibraryStatisticsQuery(params.id)),
};
const statisticsQuery = createQuery(() => getLibraryStatisticsQuery(library.id));
const stats = $derived(statisticsQuery.data);
const library = $derived(queries.library.data);
const stats = $derived(queries.statistics.data);
const [usage, usageUnit] = $derived(stats ? getBytesWithUnit(stats.usage) : [undefined, undefined]);
+12 -11
View File
@@ -1,8 +1,9 @@
<script lang="ts">
import QueueCard from './QueueCard.svelte';
import QueueStorageMigrationDescription from './QueueStorageMigrationDescription.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { queueManager } from '$lib/managers/queue-manager.svelte';
import { createPreloadedQuery } from '$lib/queries/client';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
import { asQueueItem } from '$lib/services/queue.service';
import { handleError } from '$lib/utils/handle-error';
import {
@@ -21,7 +22,8 @@
};
let { queues }: Props = $props();
const featureFlags = featureFlagsManager.value;
const query = createPreloadedQuery(getUserConfigQuery);
const { machineLearning } = $derived(query.data);
type QueueDetails = {
description?: Component;
@@ -32,7 +34,7 @@
handleCommand?: (jobId: QueueName, jobCommand: QueueCommandDto) => Promise<void>;
};
const queueDetails: Partial<Record<QueueName, QueueDetails>> = {
const queueDetails: Partial<Record<QueueName, QueueDetails>> = $derived({
[QueueName.ThumbnailGeneration]: {
allText: $t('all'),
missingText: $t('missing'),
@@ -47,33 +49,32 @@
[QueueName.Sidecar]: {
allText: $t('sync'),
missingText: $t('discover'),
disabled: !featureFlags.sidecar,
},
[QueueName.SmartSearch]: {
allText: $t('all'),
missingText: $t('missing'),
disabled: !featureFlags.smartSearch,
disabled: !machineLearning.clip.enabled,
},
[QueueName.DuplicateDetection]: {
allText: $t('all'),
missingText: $t('missing'),
disabled: !featureFlags.duplicateDetection,
disabled: !machineLearning.duplicateDetection.enabled,
},
[QueueName.FaceDetection]: {
allText: $t('reset'),
refreshText: $t('refresh'),
missingText: $t('missing'),
disabled: !featureFlags.facialRecognition,
disabled: !machineLearning.facialRecognition.enabled,
},
[QueueName.FacialRecognition]: {
allText: $t('reset'),
missingText: $t('missing'),
disabled: !featureFlags.facialRecognition,
disabled: !machineLearning.facialRecognition.enabled,
},
[QueueName.Ocr]: {
allText: $t('all'),
missingText: $t('missing'),
disabled: !featureFlags.ocr,
disabled: !machineLearning.ocr.enabled,
},
[QueueName.VideoConversion]: {
allText: $t('all'),
@@ -86,9 +87,9 @@
[QueueName.Migration]: {
missingText: $t('start'),
},
};
});
let queueList = Object.entries(queueDetails) as [QueueName, QueueDetails][];
const queueList = $derived(Object.entries(queueDetails) as [QueueName, QueueDetails][]);
const handleCommand = async (name: QueueName, dto: QueueCommandDto) => {
const item = asQueueItem($t, { name });
@@ -21,11 +21,13 @@
import { mdiCameraIris, mdiChartPie, mdiPlayCircle } from '@mdi/js';
import { t } from 'svelte-i18n';
const usersQuery = createPreloadedQuery(searchUsersAdminQuery);
const users = $derived(usersQuery.data);
const queries = {
users: createPreloadedQuery(searchUsersAdminQuery),
statistics: createQuery(() => ({ ...getServerStatisticsQuery(), refetchInterval: 5000 })),
};
const query = createQuery(() => ({ ...getServerStatisticsQuery(), refetchInterval: 5000 }));
const stats = $derived(query.data);
const users = $derived(queries.users.data);
const stats = $derived(queries.statistics.data);
const getStorageUsageWithUnit = (usage: number) => {
const TiB = 1024 ** 4;
@@ -22,7 +22,7 @@
import SearchBar from '$lib/elements/SearchBar.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { getSystemConfigActions } from '$lib/services/system-config.service';
import { Alert, CommandPaletteDefaultProvider, Container } from '@immich/ui';
import {
@@ -57,7 +57,8 @@
const { data }: Props = $props();
const config = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const settings: Array<{
component: Component;
@@ -205,14 +206,12 @@
let filteredSettings = $derived(
settings.filter(({ title, subtitle }) => {
const query = searchQuery.toLowerCase();
return title.toLowerCase().includes(query) || subtitle.toLowerCase().includes(query);
const term = searchQuery.toLowerCase();
return title.toLowerCase().includes(term) || subtitle.toLowerCase().includes(term);
}),
);
const { CopyToClipboard, Upload, Download } = $derived(
getSystemConfigActions($t, featureFlagsManager.value, config.data),
);
const { CopyToClipboard, Upload, Download } = $derived(getSystemConfigActions($t, featureFlagsManager.value, config));
</script>
<CommandPaletteDefaultProvider name={$t('admin.system_settings')} actions={[CopyToClipboard, Upload, Download]} />
@@ -1,12 +1,12 @@
import { queryClient } from '$lib/queries/client';
import { getConfigDefaultsQuery, getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigDefaultsQuery } from '$lib/queries/config-admin.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 });
await Promise.all([queryClient.query(getConfigQuery()), queryClient.query(getConfigDefaultsQuery())]);
await queryClient.query(getAdminConfigDefaultsQuery());
const $t = await getFormatter();
return {
@@ -8,7 +8,7 @@
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import AuthDisableLoginConfirmModal from '$lib/modals/AuthDisableLoginConfirmModal.svelte';
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { handleError } from '$lib/utils/handle-error';
import { OAuthTokenEndpointAuthMethod, unlinkAllOAuthAccountsAdmin } from '@immich/sdk';
import { Button, IconButton, Link, modalManager, Text, toastManager } from '@immich/ui';
@@ -18,7 +18,7 @@
import { fade } from 'svelte/transition';
import SettingSelect from './SettingSelect.svelte';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { cloneDeep } from 'lodash-es';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingInputField from '$lib/components/shared-components/settings/SettingInputField.svelte';
@@ -13,7 +13,7 @@
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import SettingAccordion from '$lib/components/shared-components/settings/SettingAccordion.svelte';
import SettingCheckboxes from './SettingCheckboxes.svelte';
import SettingInputField from '$lib/components/shared-components/settings/SettingInputField.svelte';
@@ -26,7 +26,7 @@
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { cloneDeep } from 'lodash-es';
import SettingSelect from './SettingSelect.svelte';
import { Colorspace, ImageFormat } from '@immich/sdk';
@@ -14,7 +14,7 @@
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { t } from 'svelte-i18n';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { cloneDeep } from 'lodash-es';
import SettingAccordion from '$lib/components/shared-components/settings/SettingAccordion.svelte';
import SettingInputField from '$lib/components/shared-components/settings/SettingInputField.svelte';
@@ -13,7 +13,7 @@
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { cloneDeep } from 'lodash-es';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingInputField from '$lib/components/shared-components/settings/SettingInputField.svelte';
@@ -10,7 +10,7 @@
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { cloneDeep } from 'lodash-es';
import SettingAccordion from '$lib/components/shared-components/settings/SettingAccordion.svelte';
import SettingInputField from '$lib/components/shared-components/settings/SettingInputField.svelte';
@@ -14,7 +14,7 @@
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { cloneDeep } from 'lodash-es';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingSelect from './SettingSelect.svelte';
@@ -10,7 +10,7 @@
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import SettingAccordion from '$lib/components/shared-components/settings/SettingAccordion.svelte';
import SettingInputField from '$lib/components/shared-components/settings/SettingInputField.svelte';
import SettingSelect from './SettingSelect.svelte';
@@ -15,7 +15,7 @@
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -171,7 +171,7 @@
min={0.001}
max={0.1}
description={$t('admin.machine_learning_max_detection_distance_description')}
disabled={disabled || !featureFlagsManager.value.duplicateDetection}
disabled={disabled || !config.machineLearning.duplicateDetection.enabled}
isEdited={configToEdit.machineLearning.duplicateDetection.maxDistance !==
config.machineLearning.duplicateDetection.maxDistance}
/>
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { cloneDeep } from 'lodash-es';
import SettingAccordion from '$lib/components/shared-components/settings/SettingAccordion.svelte';
import SettingInputField from '$lib/components/shared-components/settings/SettingInputField.svelte';
@@ -13,7 +13,7 @@
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { cloneDeep } from 'lodash-es';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/SettingSwitch.svelte';
@@ -8,7 +8,7 @@
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const disabled = $derived(featureFlagsManager.value.configFile);
let configToEdit = $state(cloneDeep(query.data));
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { cloneDeep } from 'lodash-es';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/SettingSwitch.svelte';
@@ -10,7 +10,7 @@
import SettingSelect from './SettingSelect.svelte';
import { ReleaseChannel } from '@immich/sdk';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { cloneDeep } from 'lodash-es';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingInputField from '$lib/components/shared-components/settings/SettingInputField.svelte';
@@ -10,7 +10,7 @@
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -9,14 +9,14 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { createPreloadedQuery, executeMutation } from '$lib/queries/client';
import { getConfigQuery, updateConfigMutation } from '$lib/queries/system-config.query';
import { getAdminConfigQuery, updateAdminConfigMutation } from '$lib/queries/config-admin.query';
import { handleError } from '$lib/utils/handle-error';
import { sendTestEmailAdmin } from '@immich/sdk';
import { Button, toastManager } from '@immich/ui';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -53,7 +53,7 @@
);
if (!disabled) {
await executeMutation(updateConfigMutation, { notifications: configToEdit.notifications });
await executeMutation(updateAdminConfigMutation, { notifications: configToEdit.notifications });
}
} catch (error) {
handleError(error, $t('admin.notification_email_test_email_failed'));
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { cloneDeep } from 'lodash-es';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingInputField from '$lib/components/shared-components/settings/SettingInputField.svelte';
@@ -10,7 +10,7 @@
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -3,7 +3,7 @@
import SettingTextarea from './SettingTextarea.svelte';
import FormatMessage from '$lib/elements/FormatMessage.svelte';
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import EmailTemplatePreviewModal from '$lib/modals/EmailTemplatePreviewModal.svelte';
import { handleError } from '$lib/utils/handle-error';
import { type AdminConfigDto, type AdminConfigTemplateEmailsDto, getNotificationTemplateAdmin } from '@immich/sdk';
@@ -18,7 +18,7 @@
let { config = $bindable() }: Props = $props();
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
let loadingPreview = $state(false);
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { cloneDeep } from 'lodash-es';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingTextarea from './SettingTextarea.svelte';
@@ -8,7 +8,7 @@
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { cloneDeep } from 'lodash-es';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingInputField from '$lib/components/shared-components/settings/SettingInputField.svelte';
@@ -10,7 +10,7 @@
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -1,6 +1,6 @@
<script lang="ts">
import { createPreloadedQuery } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { getAdminConfigQuery } from '$lib/queries/config-admin.query';
import { cloneDeep } from 'lodash-es';
import { fade } from 'svelte/transition';
@@ -10,7 +10,7 @@
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { t } from 'svelte-i18n';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const config = $derived(query.data);
const disabled = $derived(featureFlagsManager.value.configFile);
@@ -1,17 +1,22 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { Route } from '$lib/route';
import { executeMutation } from '$lib/queries/client';
import { createPreloadedQuery, executeMutation } from '$lib/queries/client';
import { getStorageQuery } from '$lib/queries/server.query';
import { createUserAdminMutation } from '$lib/queries/user-admin.query';
import { ByteUnit, convertToBytes } from '$lib/utils/byte-units';
import { Field, FormModal, HelperText, Input, PasswordInput, Stack, Switch } from '@immich/ui';
import { createQuery } from '@tanstack/svelte-query';
import { t } from 'svelte-i18n';
import { getUserConfigQuery } from '$lib/queries/config-user.query';
const query = createQuery(getStorageQuery);
const serverInfo = $derived(query.data);
const queries = {
config: createPreloadedQuery(getUserConfigQuery),
storage: createQuery(getStorageQuery),
};
const config = $derived(queries.config.data);
const serverInfo = $derived(queries.storage.data);
let success = $state(false);
@@ -66,17 +71,17 @@
<Input bind:value={email} type="email" />
</Field>
{#if featureFlagsManager.value.email}
{#if config.notifications.smtp.enabled}
<Field label={$t('admin.send_welcome_email')}>
<Switch id="send-welcome-email" bind:checked={notify} class="text-sm" />
</Field>
{/if}
<Field label={$t('password')} required={!featureFlagsManager.value.oauth}>
<Field label={$t('password')} required={!config.oauth.enabled}>
<PasswordInput id="password" bind:value={password} autocomplete="new-password" />
</Field>
<Field label={$t('confirm_password')} required={!featureFlagsManager.value.oauth}>
<Field label={$t('confirm_password')} required={!config.oauth.enabled}>
<PasswordInput id="confirmPassword" bind:value={passwordConfirm} autocomplete="new-password" />
<HelperText color="danger">{passwordMismatchMessage}</HelperText>
</Field>
@@ -14,11 +14,13 @@
const { params }: PageProps = $props();
const query = createQuery(getStorageQuery);
const serverInfo = $derived(query.data);
const queries = {
user: createPreloadedQuery(() => getUserAdminQuery(params.id)),
storage: createQuery(getStorageQuery),
};
const userQuery = createPreloadedQuery(() => getUserAdminQuery(params.id));
const user = $derived(userQuery.data);
const user = $derived(queries.user.data);
const serverInfo = $derived(queries.storage.data);
let { isAdmin, name, email } = $derived(user);
let storageLabel = $derived(user.storageLabel || '');
const previousQuota = $derived(user.quotaSizeInBytes);
+12 -4
View File
@@ -2,7 +2,9 @@
import { goto } from '$app/navigation';
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { createPreloadedQuery } from '$lib/queries/client';
import { getPublicConfigQuery } from '$lib/queries/config-public.query';
import { getServerConfigQuery } from '$lib/queries/server.query';
import { Route } from '$lib/route';
import { handleLoginWithPasskey, isPasskeyCancelled, isPasskeySupported } from '$lib/services/passkey.service';
import { oauth } from '$lib/utils';
@@ -28,10 +30,16 @@
let loading = $state(false);
let oauthLoading = $state(true);
let passkeyLoading = $state(false);
const passkeySupported = isPasskeySupported();
const serverConfig = $derived(serverConfigManager.value);
const publicConfig = $derived(data.publicConfig);
const queries = {
publicConfig: createPreloadedQuery(getPublicConfigQuery),
serverConfig: createPreloadedQuery(getServerConfigQuery),
};
const publicConfig = $derived(queries.publicConfig.data);
const serverConfig = $derived(queries.serverConfig.data);
const passkeySupported = isPasskeySupported();
const passkeyEnabled = $derived(publicConfig.passkey.enabled && passkeySupported);
const onSuccess = async (user: LoginResponseDto) => {
+3 -6
View File
@@ -1,7 +1,7 @@
import { getPublicConfig } from '@immich/sdk';
import { redirect } from '@sveltejs/kit';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { readPreloadedQuery } from '$lib/queries/client';
import { getServerConfigQuery } from '$lib/queries/server.query';
import { Route } from '$lib/route';
import { getFormatter } from '$lib/utils/i18n';
import type { PageLoad } from './$types';
@@ -15,19 +15,16 @@ export const load = (async ({ parent, url }) => {
redirect(307, continueUrl);
}
if (!serverConfigManager.value.isInitialized) {
if (!readPreloadedQuery(getServerConfigQuery()).isInitialized) {
// Admin not registered
redirect(307, Route.register());
}
const publicConfig = await getPublicConfig();
const $t = await getFormatter();
return {
meta: {
title: $t('login'),
},
continueUrl,
publicConfig,
};
}) satisfies PageLoad;
+8 -15
View File
@@ -11,9 +11,6 @@
import OnboardingTheme from './OnboardingTheme.svelte';
import OnboardingUserPrivacy from './OnboardingUserPrivacy.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { queryClient } from '$lib/queries/client';
import { getConfigQuery } from '$lib/queries/system-config.query';
import { Route } from '$lib/route';
import { OnboardingRole } from '$lib/types';
import { setUserOnboarding, updateAdminOnboarding } from '@immich/sdk';
@@ -25,8 +22,12 @@
mdiThemeLightDark,
mdiTranslate,
} from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { createPreloadedQuery, queryClient } from '$lib/queries/client';
import { getServerConfigQuery } from '$lib/queries/server.query';
const query = createPreloadedQuery(getServerConfigQuery);
const serverConfig = $derived(query.data);
interface OnboardingStep {
name: string;
@@ -102,7 +103,7 @@
return temporaryIndex === -1 ? 0 : temporaryIndex;
});
let userRole = $derived(
authManager.user.isAdmin && !serverConfigManager.value.isOnboarded ? OnboardingRole.SERVER : OnboardingRole.USER,
authManager.user.isAdmin && !serverConfig.isOnboarded ? OnboardingRole.SERVER : OnboardingRole.USER,
);
let onboardingStepCount = $derived(onboardingSteps.filter((step) => shouldRunStep(step.role, userRole)).length);
@@ -113,9 +114,7 @@
const shouldRunStep = (stepRole: OnboardingRole, userRole: OnboardingRole) => {
return (
stepRole === OnboardingRole.USER ||
(stepRole === OnboardingRole.SERVER &&
userRole === OnboardingRole.SERVER &&
!serverConfigManager.value.isOnboarded)
(stepRole === OnboardingRole.SERVER && userRole === OnboardingRole.SERVER && !serverConfig.isOnboarded)
);
};
@@ -131,7 +130,7 @@
if (nextStepIndex === -1) {
if (authManager.user.isAdmin) {
await updateAdminOnboarding({ adminOnboardingUpdateDto: { isOnboarded: true } });
await serverConfigManager.loadServerConfig();
await queryClient.invalidateQueries({ queryKey: getServerConfigQuery().queryKey });
}
await setUserOnboarding({
@@ -153,12 +152,6 @@
};
const OnboardingStep = $derived(onboardingSteps[index].component);
onMount(async () => {
if (userRole === OnboardingRole.SERVER) {
await queryClient.query(getConfigQuery());
}
});
</script>
<section id="onboarding-page" class="flex min-h-dvh min-w-dvw p-4">
@@ -1,12 +1,16 @@
<script lang="ts">
import { authManager } from '$lib/managers/auth-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { OnboardingRole } from '$lib/types';
import { Logo } from '@immich/ui';
import { t } from 'svelte-i18n';
import { createPreloadedQuery } from '$lib/queries/client';
import { getServerConfigQuery } from '$lib/queries/server.query';
const query = createPreloadedQuery(getServerConfigQuery);
const serverConfig = $derived(query.data);
let userRole = $derived(
authManager.user.isAdmin && !serverConfigManager.value.isOnboarded ? OnboardingRole.SERVER : OnboardingRole.USER,
authManager.user.isAdmin && !serverConfig.isOnboarded ? OnboardingRole.SERVER : OnboardingRole.USER,
);
</script>
@@ -1,16 +1,16 @@
<script lang="ts">
import SettingSwitch from '$lib/components/shared-components/settings/SettingSwitch.svelte';
import { createPreloadedQuery, executeMutation } from '$lib/queries/client';
import { getConfigQuery, updateConfigMutation } from '$lib/queries/system-config.query';
import { getAdminConfigQuery, updateAdminConfigMutation } from '$lib/queries/config-admin.query';
import { cloneDeep } from 'lodash-es';
import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n';
const query = createPreloadedQuery(getConfigQuery);
const query = createPreloadedQuery(getAdminConfigQuery);
const configToEdit = $state(cloneDeep(query.data));
onDestroy(() => {
void executeMutation(updateConfigMutation, {
void executeMutation(updateAdminConfigMutation, {
map: configToEdit.map,
newVersionCheck: configToEdit.newVersionCheck,
});
+3 -2
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import { goto } from '$app/navigation';
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { queryClient } from '$lib/queries/client';
import { getServerConfigQuery } from '$lib/queries/server.query';
import { Route } from '$lib/route';
import { handleError } from '$lib/utils/handle-error';
import { signUpAdmin } from '@immich/sdk';
@@ -37,7 +38,7 @@
try {
await signUpAdmin({ signUpDto: { email, password, name } });
await serverConfigManager.loadServerConfig();
await queryClient.refetchQueries({ queryKey: getServerConfigQuery().queryKey });
await goto(Route.login());
} catch (error) {
handleError(error, $t('errors.unable_to_create_admin_account'));
+3 -2
View File
@@ -1,12 +1,13 @@
import { redirect } from '@sveltejs/kit';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { readPreloadedQuery } from '$lib/queries/client';
import { getServerConfigQuery } from '$lib/queries/server.query';
import { Route } from '$lib/route';
import { getFormatter } from '$lib/utils/i18n';
import type { PageLoad } from './$types';
export const load = (async ({ parent }) => {
await parent();
if (serverConfigManager.value.isInitialized) {
if (readPreloadedQuery(getServerConfigQuery()).isInitialized) {
// Admin has been registered, redirect to login
redirect(307, Route.login());
}