refactor: job vs queue naming (#23902)

This commit is contained in:
Jason Rasmussen
2025-11-14 14:42:00 -05:00
committed by GitHub
parent 1200bfad13
commit d784d431d0
36 changed files with 1356 additions and 1325 deletions

View File

@@ -4,8 +4,8 @@
import { SettingInputFieldType } from '$lib/constants';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
import { getJobName } from '$lib/utils';
import { JobName, type SystemConfigJobDto } from '@immich/sdk';
import { getQueueName } from '$lib/utils';
import { QueueName, type SystemConfigJobDto } from '@immich/sdk';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
@@ -13,18 +13,18 @@
const config = $derived(systemConfigManager.value);
let configToEdit = $state(systemConfigManager.cloneValue());
const jobNames = [
JobName.ThumbnailGeneration,
JobName.MetadataExtraction,
JobName.Library,
JobName.Sidecar,
JobName.SmartSearch,
JobName.FaceDetection,
JobName.FacialRecognition,
JobName.VideoConversion,
JobName.StorageTemplateMigration,
JobName.Migration,
JobName.Ocr,
const queueNames = [
QueueName.ThumbnailGeneration,
QueueName.MetadataExtraction,
QueueName.Library,
QueueName.Sidecar,
QueueName.SmartSearch,
QueueName.FaceDetection,
QueueName.FacialRecognition,
QueueName.VideoConversion,
QueueName.StorageTemplateMigration,
QueueName.Migration,
QueueName.Ocr,
];
function isSystemConfigJobDto(jobName: string): jobName is keyof SystemConfigJobDto {
@@ -35,22 +35,22 @@
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
{#each jobNames as jobName (jobName)}
{#each queueNames as queueName (queueName)}
<div class="ms-4 mt-4 flex flex-col gap-4">
{#if isSystemConfigJobDto(jobName)}
{#if isSystemConfigJobDto(queueName)}
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
{disabled}
label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })}
label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })}
description=""
bind:value={configToEdit.job[jobName].concurrency}
bind:value={configToEdit.job[queueName].concurrency}
required={true}
isEdited={!(configToEdit.job[jobName].concurrency == config.job[jobName].concurrency)}
isEdited={!(configToEdit.job[queueName].concurrency == config.job[queueName].concurrency)}
/>
{:else}
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })}
label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })}
description=""
value={1}
disabled={true}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import Badge from '$lib/elements/Badge.svelte';
import { locale } from '$lib/stores/preferences.store';
import { JobCommand, type JobCommandDto, type JobCountsDto, type QueueStatusDto } from '@immich/sdk';
import { QueueCommand, type QueueCommandDto, type QueueStatisticsDto, type QueueStatusDto } from '@immich/sdk';
import { Icon, IconButton } from '@immich/ui';
import {
mdiAlertCircle,
@@ -22,21 +22,21 @@
title: string;
subtitle: string | undefined;
description: Component | undefined;
jobCounts: JobCountsDto;
statistics: QueueStatisticsDto;
queueStatus: QueueStatusDto;
icon: string;
disabled?: boolean;
allText: string | undefined;
refreshText: string | undefined;
missingText: string;
onCommand: (command: JobCommandDto) => void;
onCommand: (command: QueueCommandDto) => void;
}
let {
title,
subtitle,
description,
jobCounts,
statistics,
queueStatus,
icon,
disabled = false,
@@ -46,7 +46,7 @@
onCommand,
}: Props = $props();
let waitingCount = $derived(jobCounts.waiting + jobCounts.paused + jobCounts.delayed);
let waitingCount = $derived(statistics.waiting + statistics.paused + statistics.delayed);
let isIdle = $derived(!queueStatus.isActive && !queueStatus.isPaused);
let multipleButtons = $derived(allText || refreshText);
@@ -67,11 +67,11 @@
<span class="uppercase">{title}</span>
</span>
<div class="flex gap-2">
{#if jobCounts.failed > 0}
{#if statistics.failed > 0}
<Badge>
<div class="flex flex-row gap-1">
<span class="text-sm">
{$t('admin.jobs_failed', { values: { jobCount: jobCounts.failed.toLocaleString($locale) } })}
{$t('admin.jobs_failed', { values: { jobCount: statistics.failed.toLocaleString($locale) } })}
</span>
<IconButton
color="primary"
@@ -79,15 +79,15 @@
aria-label={$t('clear_message')}
size="tiny"
shape="round"
onclick={() => onCommand({ command: JobCommand.ClearFailed, force: false })}
onclick={() => onCommand({ command: QueueCommand.ClearFailed, force: false })}
/>
</div>
</Badge>
{/if}
{#if jobCounts.delayed > 0}
{#if statistics.delayed > 0}
<Badge>
<span class="text-sm">
{$t('admin.jobs_delayed', { values: { jobCount: jobCounts.delayed.toLocaleString($locale) } })}
{$t('admin.jobs_delayed', { values: { jobCount: statistics.delayed.toLocaleString($locale) } })}
</span>
</Badge>
{/if}
@@ -111,7 +111,7 @@
>
<p>{$t('active')}</p>
<p class="text-2xl">
{jobCounts.active.toLocaleString($locale)}
{statistics.active.toLocaleString($locale)}
</p>
</div>
@@ -131,7 +131,7 @@
<JobTileButton
disabled={true}
color="light-gray"
onClick={() => onCommand({ command: JobCommand.Start, force: false })}
onClick={() => onCommand({ command: QueueCommand.Start, force: false })}
>
<Icon icon={mdiAlertCircle} size="36" />
<span class="uppercase">{$t('disabled')}</span>
@@ -140,20 +140,20 @@
{#if !disabled && !isIdle}
{#if waitingCount > 0}
<JobTileButton color="gray" onClick={() => onCommand({ command: JobCommand.Empty, force: false })}>
<JobTileButton color="gray" onClick={() => onCommand({ command: QueueCommand.Empty, force: false })}>
<Icon icon={mdiClose} size="24" />
<span class="uppercase">{$t('clear')}</span>
</JobTileButton>
{/if}
{#if queueStatus.isPaused}
{@const size = waitingCount > 0 ? '24' : '48'}
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Resume, force: false })}>
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Resume, force: false })}>
<!-- size property is not reactive, so have to use width and height -->
<Icon icon={mdiFastForward} {size} />
<span class="uppercase">{$t('resume')}</span>
</JobTileButton>
{:else}
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Pause, force: false })}>
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Pause, force: false })}>
<Icon icon={mdiPause} size="24" />
<span class="uppercase">{$t('pause')}</span>
</JobTileButton>
@@ -162,25 +162,25 @@
{#if !disabled && multipleButtons && isIdle}
{#if allText}
<JobTileButton color="dark-gray" onClick={() => onCommand({ command: JobCommand.Start, force: true })}>
<JobTileButton color="dark-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: true })}>
<Icon icon={mdiAllInclusive} size="24" />
<span class="uppercase">{allText}</span>
</JobTileButton>
{/if}
{#if refreshText}
<JobTileButton color="gray" onClick={() => onCommand({ command: JobCommand.Start, force: undefined })}>
<JobTileButton color="gray" onClick={() => onCommand({ command: QueueCommand.Start, force: undefined })}>
<Icon icon={mdiImageRefreshOutline} size="24" />
<span class="uppercase">{refreshText}</span>
</JobTileButton>
{/if}
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Start, force: false })}>
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: false })}>
<Icon icon={mdiSelectionSearch} size="24" />
<span class="uppercase">{missingText}</span>
</JobTileButton>
{/if}
{#if !disabled && !multipleButtons && isIdle}
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Start, force: false })}>
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: false })}>
<Icon icon={mdiPlay} size="48" />
<span class="uppercase">{missingText}</span>
</JobTileButton>

View File

@@ -1,8 +1,14 @@
<script lang="ts">
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { getJobName } from '$lib/utils';
import { getQueueName } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { JobCommand, JobName, sendJobCommand, type AllJobStatusResponseDto, type JobCommandDto } from '@immich/sdk';
import {
QueueCommand,
type QueueCommandDto,
QueueName,
type QueuesResponseDto,
runQueueCommandLegacy,
} from '@immich/sdk';
import { modalManager, toastManager } from '@immich/ui';
import {
mdiContentDuplicate,
@@ -23,7 +29,7 @@
import StorageMigrationDescription from './StorageMigrationDescription.svelte';
interface Props {
jobs: AllJobStatusResponseDto;
jobs: QueuesResponseDto;
}
let { jobs = $bindable() }: Props = $props();
@@ -38,17 +44,17 @@
missingText: string;
disabled?: boolean;
icon: string;
handleCommand?: (jobId: JobName, jobCommand: JobCommandDto) => Promise<void>;
handleCommand?: (jobId: QueueName, jobCommand: QueueCommandDto) => Promise<void>;
};
const handleConfirmCommand = async (jobId: JobName, dto: JobCommandDto) => {
const handleConfirmCommand = async (jobId: QueueName, dto: QueueCommandDto) => {
if (dto.force) {
const isConfirmed = await modalManager.showDialog({
prompt: $t('admin.confirm_reprocess_all_faces'),
});
if (isConfirmed) {
await handleCommand(jobId, { command: JobCommand.Start, force: true });
await handleCommand(jobId, { command: QueueCommand.Start, force: true });
return;
}
@@ -58,54 +64,54 @@
await handleCommand(jobId, dto);
};
let jobDetails: Partial<Record<JobName, JobDetails>> = {
[JobName.ThumbnailGeneration]: {
let jobDetails: Partial<Record<QueueName, JobDetails>> = {
[QueueName.ThumbnailGeneration]: {
icon: mdiFileJpgBox,
title: $getJobName(JobName.ThumbnailGeneration),
title: $getQueueName(QueueName.ThumbnailGeneration),
subtitle: $t('admin.thumbnail_generation_job_description'),
allText: $t('all'),
missingText: $t('missing'),
},
[JobName.MetadataExtraction]: {
[QueueName.MetadataExtraction]: {
icon: mdiTable,
title: $getJobName(JobName.MetadataExtraction),
title: $getQueueName(QueueName.MetadataExtraction),
subtitle: $t('admin.metadata_extraction_job_description'),
allText: $t('all'),
missingText: $t('missing'),
},
[JobName.Library]: {
[QueueName.Library]: {
icon: mdiLibraryShelves,
title: $getJobName(JobName.Library),
title: $getQueueName(QueueName.Library),
subtitle: $t('admin.library_tasks_description'),
missingText: $t('rescan'),
},
[JobName.Sidecar]: {
title: $getJobName(JobName.Sidecar),
[QueueName.Sidecar]: {
title: $getQueueName(QueueName.Sidecar),
icon: mdiFileXmlBox,
subtitle: $t('admin.sidecar_job_description'),
allText: $t('sync'),
missingText: $t('discover'),
disabled: !featureFlags.sidecar,
},
[JobName.SmartSearch]: {
[QueueName.SmartSearch]: {
icon: mdiImageSearch,
title: $getJobName(JobName.SmartSearch),
title: $getQueueName(QueueName.SmartSearch),
subtitle: $t('admin.smart_search_job_description'),
allText: $t('all'),
missingText: $t('missing'),
disabled: !featureFlags.smartSearch,
},
[JobName.DuplicateDetection]: {
[QueueName.DuplicateDetection]: {
icon: mdiContentDuplicate,
title: $getJobName(JobName.DuplicateDetection),
title: $getQueueName(QueueName.DuplicateDetection),
subtitle: $t('admin.duplicate_detection_job_description'),
allText: $t('all'),
missingText: $t('missing'),
disabled: !featureFlags.duplicateDetection,
},
[JobName.FaceDetection]: {
[QueueName.FaceDetection]: {
icon: mdiFaceRecognition,
title: $getJobName(JobName.FaceDetection),
title: $getQueueName(QueueName.FaceDetection),
subtitle: $t('admin.face_detection_description'),
allText: $t('reset'),
refreshText: $t('refresh'),
@@ -113,67 +119,67 @@
handleCommand: handleConfirmCommand,
disabled: !featureFlags.facialRecognition,
},
[JobName.FacialRecognition]: {
[QueueName.FacialRecognition]: {
icon: mdiTagFaces,
title: $getJobName(JobName.FacialRecognition),
title: $getQueueName(QueueName.FacialRecognition),
subtitle: $t('admin.facial_recognition_job_description'),
allText: $t('reset'),
missingText: $t('missing'),
handleCommand: handleConfirmCommand,
disabled: !featureFlags.facialRecognition,
},
[JobName.Ocr]: {
[QueueName.Ocr]: {
icon: mdiOcr,
title: $getJobName(JobName.Ocr),
title: $getQueueName(QueueName.Ocr),
subtitle: $t('admin.ocr_job_description'),
allText: $t('all'),
missingText: $t('missing'),
disabled: !featureFlags.ocr,
},
[JobName.VideoConversion]: {
[QueueName.VideoConversion]: {
icon: mdiVideo,
title: $getJobName(JobName.VideoConversion),
title: $getQueueName(QueueName.VideoConversion),
subtitle: $t('admin.video_conversion_job_description'),
allText: $t('all'),
missingText: $t('missing'),
},
[JobName.StorageTemplateMigration]: {
[QueueName.StorageTemplateMigration]: {
icon: mdiFolderMove,
title: $getJobName(JobName.StorageTemplateMigration),
title: $getQueueName(QueueName.StorageTemplateMigration),
missingText: $t('start'),
description: StorageMigrationDescription,
},
[JobName.Migration]: {
[QueueName.Migration]: {
icon: mdiFolderMove,
title: $getJobName(JobName.Migration),
title: $getQueueName(QueueName.Migration),
subtitle: $t('admin.migration_job_description'),
missingText: $t('start'),
},
};
let jobList = Object.entries(jobDetails) as [JobName, JobDetails][];
let jobList = Object.entries(jobDetails) as [QueueName, JobDetails][];
async function handleCommand(jobId: JobName, jobCommand: JobCommandDto) {
const title = jobDetails[jobId]?.title;
async function handleCommand(name: QueueName, dto: QueueCommandDto) {
const title = jobDetails[name]?.title;
try {
jobs[jobId] = await sendJobCommand({ id: jobId, jobCommandDto: jobCommand });
jobs[name] = await runQueueCommandLegacy({ name, queueCommandDto: dto });
switch (jobCommand.command) {
case JobCommand.Empty: {
switch (dto.command) {
case QueueCommand.Empty: {
toastManager.success($t('admin.cleared_jobs', { values: { job: title } }));
break;
}
}
} catch (error) {
handleError(error, $t('admin.failed_job_command', { values: { command: jobCommand.command, job: title } }));
handleError(error, $t('admin.failed_job_command', { values: { command: dto.command, job: title } }));
}
}
</script>
<div class="flex flex-col gap-7">
{#each jobList as [jobName, { title, subtitle, description, disabled, allText, refreshText, missingText, icon, handleCommand: handleCommandOverride }] (jobName)}
{@const { jobCounts, queueStatus } = jobs[jobName]}
{@const { jobCounts: statistics, queueStatus } = jobs[jobName]}
<JobTile
{icon}
{title}
@@ -183,7 +189,7 @@
{allText}
{refreshText}
{missingText}
{jobCounts}
{statistics}
{queueStatus}
onCommand={(command) => (handleCommandOverride || handleCommand)(jobName, command)}
/>

View File

@@ -5,8 +5,8 @@ import { handleError } from '$lib/utils/handle-error';
import {
AssetJobName,
AssetMediaSize,
JobName,
MemoryType,
QueueName,
finishOAuth,
getAssetOriginalPath,
getAssetPlaybackPath,
@@ -143,28 +143,28 @@ export const downloadRequest = <TBody = unknown>(options: DownloadRequestOptions
});
};
export const getJobName = derived(t, ($t) => {
return (jobName: JobName) => {
const names: Record<JobName, string> = {
[JobName.ThumbnailGeneration]: $t('admin.thumbnail_generation_job'),
[JobName.MetadataExtraction]: $t('admin.metadata_extraction_job'),
[JobName.Sidecar]: $t('admin.sidecar_job'),
[JobName.SmartSearch]: $t('admin.machine_learning_smart_search'),
[JobName.DuplicateDetection]: $t('admin.machine_learning_duplicate_detection'),
[JobName.FaceDetection]: $t('admin.face_detection'),
[JobName.FacialRecognition]: $t('admin.machine_learning_facial_recognition'),
[JobName.VideoConversion]: $t('admin.video_conversion_job'),
[JobName.StorageTemplateMigration]: $t('admin.storage_template_migration'),
[JobName.Migration]: $t('admin.migration_job'),
[JobName.BackgroundTask]: $t('admin.background_task_job'),
[JobName.Search]: $t('search'),
[JobName.Library]: $t('external_libraries'),
[JobName.Notifications]: $t('notifications'),
[JobName.BackupDatabase]: $t('admin.backup_database'),
[JobName.Ocr]: $t('admin.machine_learning_ocr'),
export const getQueueName = derived(t, ($t) => {
return (name: QueueName) => {
const names: Record<QueueName, string> = {
[QueueName.ThumbnailGeneration]: $t('admin.thumbnail_generation_job'),
[QueueName.MetadataExtraction]: $t('admin.metadata_extraction_job'),
[QueueName.Sidecar]: $t('admin.sidecar_job'),
[QueueName.SmartSearch]: $t('admin.machine_learning_smart_search'),
[QueueName.DuplicateDetection]: $t('admin.machine_learning_duplicate_detection'),
[QueueName.FaceDetection]: $t('admin.face_detection'),
[QueueName.FacialRecognition]: $t('admin.machine_learning_facial_recognition'),
[QueueName.VideoConversion]: $t('admin.video_conversion_job'),
[QueueName.StorageTemplateMigration]: $t('admin.storage_template_migration'),
[QueueName.Migration]: $t('admin.migration_job'),
[QueueName.BackgroundTask]: $t('admin.background_task_job'),
[QueueName.Search]: $t('search'),
[QueueName.Library]: $t('external_libraries'),
[QueueName.Notifications]: $t('notifications'),
[QueueName.BackupDatabase]: $t('admin.backup_database'),
[QueueName.Ocr]: $t('admin.machine_learning_ocr'),
};
return names[jobName];
return names[name];
};
});

View File

@@ -5,13 +5,7 @@
import JobCreateModal from '$lib/modals/JobCreateModal.svelte';
import { asyncTimeout } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import {
getAllJobsStatus,
JobCommand,
sendJobCommand,
type AllJobStatusResponseDto,
type JobName,
} from '@immich/sdk';
import { getQueuesLegacy, QueueCommand, QueueName, runQueueCommandLegacy, type QueuesResponseDto } from '@immich/sdk';
import { Button, HStack, modalManager, Text } from '@immich/ui';
import { mdiCog, mdiPlay, mdiPlus } from '@mdi/js';
import { onDestroy, onMount } from 'svelte';
@@ -24,23 +18,23 @@
let { data }: Props = $props();
let jobs: AllJobStatusResponseDto | undefined = $state();
let jobs: QueuesResponseDto | undefined = $state();
let running = true;
const pausedJobs = $derived(
Object.entries(jobs ?? {})
.filter(([_, jobStatus]) => jobStatus.queueStatus?.isPaused)
.map(([jobName]) => jobName as JobName),
.filter(([_, queue]) => queue.queueStatus?.isPaused)
.map(([name]) => name as QueueName),
);
const handleResumePausedJobs = async () => {
try {
for (const jobName of pausedJobs) {
await sendJobCommand({ id: jobName, jobCommandDto: { command: JobCommand.Resume, force: false } });
for (const name of pausedJobs) {
await runQueueCommandLegacy({ name, queueCommandDto: { command: QueueCommand.Resume, force: false } });
}
// Refresh jobs status immediately after resuming
jobs = await getAllJobsStatus();
jobs = await getQueuesLegacy();
} catch (error) {
handleError(error, $t('admin.failed_job_command', { values: { command: 'resume', job: 'paused jobs' } }));
}
@@ -48,7 +42,7 @@
onMount(async () => {
while (running) {
jobs = await getAllJobsStatus();
jobs = await getQueuesLegacy();
await asyncTimeout(5000);
}
});

View File

@@ -1,12 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAllJobsStatus } from '@immich/sdk';
import { getQueuesLegacy } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async ({ url }) => {
await authenticate(url, { admin: true });
const jobs = await getAllJobsStatus();
const jobs = await getQueuesLegacy();
const $t = await getFormatter();
return {

View File

@@ -17,10 +17,10 @@
getAllLibraries,
getLibraryStatistics,
getUserAdmin,
JobCommand,
JobName,
QueueCommand,
QueueName,
runQueueCommandLegacy,
scanLibrary,
sendJobCommand,
updateLibrary,
type LibraryResponseDto,
type LibraryStatsResponseDto,
@@ -151,7 +151,7 @@
const handleScanAll = async () => {
try {
await sendJobCommand({ id: JobName.Library, jobCommandDto: { command: JobCommand.Start } });
await runQueueCommandLegacy({ name: QueueName.Library, queueCommandDto: { command: QueueCommand.Start } });
toastManager.info($t('admin.refreshing_all_libraries'));
} catch (error) {