diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 36b2c7bbf4..16f293f81a 100644
Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 091e900145..915c70f08e 100644
Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ
diff --git a/mobile/openapi/lib/api/jobs_api.dart b/mobile/openapi/lib/api/jobs_api.dart
index 5f9501d126..78afc15c93 100644
Binary files a/mobile/openapi/lib/api/jobs_api.dart and b/mobile/openapi/lib/api/jobs_api.dart differ
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index 9ec00aecc8..6a40de730c 100644
Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ
diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart
index 8dcef880f5..0f3cc41097 100644
Binary files a/mobile/openapi/lib/api_helper.dart and b/mobile/openapi/lib/api_helper.dart differ
diff --git a/mobile/openapi/lib/model/job_create_dto.dart b/mobile/openapi/lib/model/job_create_dto.dart
new file mode 100644
index 0000000000..a4734791bb
Binary files /dev/null and b/mobile/openapi/lib/model/job_create_dto.dart differ
diff --git a/mobile/openapi/lib/model/manual_job_name.dart b/mobile/openapi/lib/model/manual_job_name.dart
new file mode 100644
index 0000000000..7e8d9d51b2
Binary files /dev/null and b/mobile/openapi/lib/model/manual_job_name.dart differ
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index b4ec4505b9..af79815563 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -2561,6 +2561,39 @@
         "tags": [
           "Jobs"
         ]
+      },
+      "post": {
+        "operationId": "createJob",
+        "parameters": [],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/JobCreateDto"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "201": {
+            "description": ""
+          }
+        },
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ],
+        "tags": [
+          "Jobs"
+        ]
       }
     },
     "/jobs/{id}": {
@@ -9269,6 +9302,17 @@
         ],
         "type": "object"
       },
+      "JobCreateDto": {
+        "properties": {
+          "name": {
+            "$ref": "#/components/schemas/ManualJobName"
+          }
+        },
+        "required": [
+          "name"
+        ],
+        "type": "object"
+      },
       "JobName": {
         "enum": [
           "thumbnailGeneration",
@@ -9511,6 +9555,14 @@
         ],
         "type": "object"
       },
+      "ManualJobName": {
+        "enum": [
+          "person-cleanup",
+          "tag-cleanup",
+          "user-cleanup"
+        ],
+        "type": "string"
+      },
       "MapMarkerResponseDto": {
         "properties": {
           "city": {
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index 9350bd5604..da57313692 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -548,6 +548,9 @@ export type AllJobStatusResponseDto = {
     thumbnailGeneration: JobStatusDto;
     videoConversion: JobStatusDto;
 };
+export type JobCreateDto = {
+    name: ManualJobName;
+};
 export type JobCommandDto = {
     command: JobCommand;
     force: boolean;
@@ -1941,6 +1944,15 @@ export function getAllJobsStatus(opts?: Oazapfts.RequestOpts) {
         ...opts
     }));
 }
+export function createJob({ jobCreateDto }: {
+    jobCreateDto: JobCreateDto;
+}, opts?: Oazapfts.RequestOpts) {
+    return oazapfts.ok(oazapfts.fetchText("/jobs", oazapfts.json({
+        ...opts,
+        method: "POST",
+        body: jobCreateDto
+    })));
+}
 export function sendJobCommand({ id, jobCommandDto }: {
     id: JobName;
     jobCommandDto: JobCommandDto;
@@ -3364,6 +3376,11 @@ export enum EntityType {
     Asset = "ASSET",
     Album = "ALBUM"
 }
+export enum ManualJobName {
+    PersonCleanup = "person-cleanup",
+    TagCleanup = "tag-cleanup",
+    UserCleanup = "user-cleanup"
+}
 export enum JobName {
     ThumbnailGeneration = "thumbnailGeneration",
     MetadataExtraction = "metadataExtraction",
diff --git a/server/src/controllers/job.controller.ts b/server/src/controllers/job.controller.ts
index 2aa5920fab..7da19e207f 100644
--- a/server/src/controllers/job.controller.ts
+++ b/server/src/controllers/job.controller.ts
@@ -1,6 +1,6 @@
-import { Body, Controller, Get, Param, Put } from '@nestjs/common';
+import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common';
 import { ApiTags } from '@nestjs/swagger';
-import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto';
+import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto';
 import { Authenticated } from 'src/middleware/auth.guard';
 import { JobService } from 'src/services/job.service';
 
@@ -15,6 +15,12 @@ export class JobController {
     return this.service.getAllJobsStatus();
   }
 
+  @Post()
+  @Authenticated({ admin: true })
+  createJob(@Body() dto: JobCreateDto): Promise<void> {
+    return this.service.create(dto);
+  }
+
   @Put(':id')
   @Authenticated({ admin: true })
   sendJobCommand(@Param() { id }: JobIdParamDto, @Body() dto: JobCommandDto): Promise<JobStatusDto> {
diff --git a/server/src/dtos/job.dto.ts b/server/src/dtos/job.dto.ts
index b7d8cf59bf..895f710b7a 100644
--- a/server/src/dtos/job.dto.ts
+++ b/server/src/dtos/job.dto.ts
@@ -1,5 +1,6 @@
 import { ApiProperty } from '@nestjs/swagger';
 import { IsEnum, IsNotEmpty } from 'class-validator';
+import { ManualJobName } from 'src/enum';
 import { JobCommand, QueueName } from 'src/interfaces/job.interface';
 import { ValidateBoolean } from 'src/validation';
 
@@ -20,6 +21,12 @@ export class JobCommandDto {
   force!: boolean;
 }
 
+export class JobCreateDto {
+  @IsEnum(ManualJobName)
+  @ApiProperty({ type: 'string', enum: ManualJobName, enumName: 'ManualJobName' })
+  name!: ManualJobName;
+}
+
 export class JobCountsDto {
   @ApiProperty({ type: 'integer' })
   active!: number;
diff --git a/server/src/enum.ts b/server/src/enum.ts
index 32254854e4..d76d97371c 100644
--- a/server/src/enum.ts
+++ b/server/src/enum.ts
@@ -186,3 +186,9 @@ export enum SourceType {
   MACHINE_LEARNING = 'machine-learning',
   EXIF = 'exif',
 }
+
+export enum ManualJobName {
+  PERSON_CLEANUP = 'person-cleanup',
+  TAG_CLEANUP = 'tag-cleanup',
+  USER_CLEANUP = 'user-cleanup',
+}
diff --git a/server/src/interfaces/job.interface.ts b/server/src/interfaces/job.interface.ts
index a0533fa63f..d0a15bfa5d 100644
--- a/server/src/interfaces/job.interface.ts
+++ b/server/src/interfaces/job.interface.ts
@@ -60,6 +60,9 @@ export enum JobName {
   STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration',
   STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single',
 
+  // tags
+  TAG_CLEANUP = 'tag-cleanup',
+
   // migration
   QUEUE_MIGRATION = 'queue-migration',
   MIGRATE_ASSET = 'migrate-asset',
@@ -262,6 +265,9 @@ export type JobItem =
   | { name: JobName.CLEAN_OLD_AUDIT_LOGS; data?: IBaseJob }
   | { name: JobName.CLEAN_OLD_SESSION_TOKENS; data?: IBaseJob }
 
+  // Tags
+  | { name: JobName.TAG_CLEANUP; data?: IBaseJob }
+
   // Asset Deletion
   | { name: JobName.PERSON_CLEANUP; data?: IBaseJob }
   | { name: JobName.ASSET_DELETION; data: IAssetDeleteJob }
diff --git a/server/src/interfaces/tag.interface.ts b/server/src/interfaces/tag.interface.ts
index aca9c223d5..16a34d6ac4 100644
--- a/server/src/interfaces/tag.interface.ts
+++ b/server/src/interfaces/tag.interface.ts
@@ -17,4 +17,5 @@ export interface ITagRepository extends IBulkAsset {
 
   upsertAssetTags({ assetId, tagIds }: { assetId: string; tagIds: string[] }): Promise<void>;
   upsertAssetIds(items: AssetTagItem[]): Promise<AssetTagItem[]>;
+  deleteEmptyTags(): Promise<void>;
 }
diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts
index f64e5175e5..2981fa4bdd 100644
--- a/server/src/repositories/job.repository.ts
+++ b/server/src/repositories/job.repository.ts
@@ -41,6 +41,9 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
   [JobName.GENERATE_THUMBHASH]: QueueName.THUMBNAIL_GENERATION,
   [JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 
+  // tags
+  [JobName.TAG_CLEANUP]: QueueName.BACKGROUND_TASK,
+
   // metadata
   [JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION,
   [JobName.METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION,
diff --git a/server/src/repositories/tag.repository.ts b/server/src/repositories/tag.repository.ts
index 9389aeb13b..1a5415b8db 100644
--- a/server/src/repositories/tag.repository.ts
+++ b/server/src/repositories/tag.repository.ts
@@ -1,10 +1,11 @@
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
 import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
 import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
 import { TagEntity } from 'src/entities/tag.entity';
+import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { AssetTagItem, ITagRepository } from 'src/interfaces/tag.interface';
 import { Instrumentation } from 'src/utils/instrumentation';
-import { DataSource, In, Repository } from 'typeorm';
+import { DataSource, In, Repository, TreeRepository } from 'typeorm';
 
 @Instrumentation()
 @Injectable()
@@ -12,7 +13,11 @@ export class TagRepository implements ITagRepository {
   constructor(
     @InjectDataSource() private dataSource: DataSource,
     @InjectRepository(TagEntity) private repository: Repository<TagEntity>,
-  ) {}
+    @InjectRepository(TagEntity) private tree: TreeRepository<TagEntity>,
+    @Inject(ILoggerRepository) private logger: ILoggerRepository,
+  ) {
+    this.logger.setContext(TagRepository.name);
+  }
 
   get(id: string): Promise<TagEntity | null> {
     return this.repository.findOne({ where: { id } });
@@ -174,6 +179,34 @@ export class TagRepository implements ITagRepository {
     });
   }
 
+  async deleteEmptyTags() {
+    await this.dataSource.transaction(async (manager) => {
+      const ids = new Set<string>();
+      const tags = await manager.find(TagEntity);
+      for (const tag of tags) {
+        const count = await manager
+          .createQueryBuilder('assets', 'asset')
+          .innerJoin(
+            'asset.tags',
+            'asset_tags',
+            'asset_tags.id IN (SELECT id_descendant FROM tags_closure WHERE id_ancestor = :tagId)',
+            { tagId: tag.id },
+          )
+          .getCount();
+
+        if (count === 0) {
+          this.logger.debug(`Found empty tag: ${tag.id} - ${tag.value}`);
+          ids.add(tag.id);
+        }
+      }
+
+      if (ids.size > 0) {
+        await manager.delete(TagEntity, { id: In([...ids]) });
+        this.logger.log(`Deleted ${ids.size} empty tags`);
+      }
+    });
+  }
+
   private async save(partial: Partial<TagEntity>): Promise<TagEntity> {
     const { id } = await this.repository.save(partial);
     return this.repository.findOneOrFail({ where: { id } });
diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts
index aa61ccf3cb..03a6edf126 100644
--- a/server/src/services/job.service.ts
+++ b/server/src/services/job.service.ts
@@ -2,8 +2,8 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common';
 import { snakeCase } from 'lodash';
 import { SystemConfigCore } from 'src/cores/system-config.core';
 import { mapAsset } from 'src/dtos/asset-response.dto';
-import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from 'src/dtos/job.dto';
-import { AssetType } from 'src/enum';
+import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
+import { AssetType, ManualJobName } from 'src/enum';
 import { IAssetRepository } from 'src/interfaces/asset.interface';
 import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
 import {
@@ -22,6 +22,26 @@ import { IMetricRepository } from 'src/interfaces/metric.interface';
 import { IPersonRepository } from 'src/interfaces/person.interface';
 import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
 
+const asJobItem = (dto: JobCreateDto): JobItem => {
+  switch (dto.name) {
+    case ManualJobName.TAG_CLEANUP: {
+      return { name: JobName.TAG_CLEANUP };
+    }
+
+    case ManualJobName.PERSON_CLEANUP: {
+      return { name: JobName.PERSON_CLEANUP };
+    }
+
+    case ManualJobName.USER_CLEANUP: {
+      return { name: JobName.USER_DELETE_CHECK };
+    }
+
+    default: {
+      throw new BadRequestException('Invalid job name');
+    }
+  }
+};
+
 @Injectable()
 export class JobService {
   private configCore: SystemConfigCore;
@@ -39,6 +59,10 @@ export class JobService {
     this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
   }
 
+  async create(dto: JobCreateDto): Promise<void> {
+    await this.jobRepository.queue(asJobItem(dto));
+  }
+
   async handleCommand(queueName: QueueName, dto: JobCommandDto): Promise<JobStatusDto> {
     this.logger.debug(`Handling command: queue=${queueName},force=${dto.force}`);
 
diff --git a/server/src/services/microservices.service.ts b/server/src/services/microservices.service.ts
index 025400cc9b..df4b072d56 100644
--- a/server/src/services/microservices.service.ts
+++ b/server/src/services/microservices.service.ts
@@ -15,6 +15,7 @@ import { SessionService } from 'src/services/session.service';
 import { SmartInfoService } from 'src/services/smart-info.service';
 import { StorageTemplateService } from 'src/services/storage-template.service';
 import { StorageService } from 'src/services/storage.service';
+import { TagService } from 'src/services/tag.service';
 import { UserService } from 'src/services/user.service';
 import { VersionService } from 'src/services/version.service';
 import { otelShutdown } from 'src/utils/instrumentation';
@@ -34,6 +35,7 @@ export class MicroservicesService {
     private sessionService: SessionService,
     private storageTemplateService: StorageTemplateService,
     private storageService: StorageService,
+    private tagService: TagService,
     private userService: UserService,
     private duplicateService: DuplicateService,
     private versionService: VersionService,
@@ -93,6 +95,7 @@ export class MicroservicesService {
       [JobName.NOTIFY_ALBUM_INVITE]: (data) => this.notificationService.handleAlbumInvite(data),
       [JobName.NOTIFY_ALBUM_UPDATE]: (data) => this.notificationService.handleAlbumUpdate(data),
       [JobName.NOTIFY_SIGNUP]: (data) => this.notificationService.handleUserSignup(data),
+      [JobName.TAG_CLEANUP]: () => this.tagService.handleTagCleanup(),
       [JobName.VERSION_CHECK]: () => this.versionService.handleVersionCheck(),
     });
   }
diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts
index 97b0ef1be6..cc6d64f749 100644
--- a/server/src/services/tag.service.ts
+++ b/server/src/services/tag.service.ts
@@ -14,6 +14,7 @@ import { TagEntity } from 'src/entities/tag.entity';
 import { Permission } from 'src/enum';
 import { IAccessRepository } from 'src/interfaces/access.interface';
 import { IEventRepository } from 'src/interfaces/event.interface';
+import { JobStatus } from 'src/interfaces/job.interface';
 import { AssetTagItem, ITagRepository } from 'src/interfaces/tag.interface';
 import { checkAccess, requireAccess } from 'src/utils/access';
 import { addAssets, removeAssets } from 'src/utils/asset.util';
@@ -138,6 +139,11 @@ export class TagService {
     return results;
   }
 
+  async handleTagCleanup() {
+    await this.repository.deleteEmptyTags();
+    return JobStatus.SUCCESS;
+  }
+
   private async findOrFail(id: string) {
     const tag = await this.repository.get(id);
     if (!tag) {
diff --git a/server/test/repositories/tag.repository.mock.ts b/server/test/repositories/tag.repository.mock.ts
index a3fc0e77e0..acc2b59f6d 100644
--- a/server/test/repositories/tag.repository.mock.ts
+++ b/server/test/repositories/tag.repository.mock.ts
@@ -17,5 +17,6 @@ export const newTagRepositoryMock = (): Mocked<ITagRepository> => {
     addAssetIds: vitest.fn(),
     removeAssetIds: vitest.fn(),
     upsertAssetIds: vitest.fn(),
+    deleteEmptyTags: vitest.fn(),
   };
 };
diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte
index d3e022a759..7c71fe8aea 100644
--- a/web/src/lib/components/shared-components/combobox.svelte
+++ b/web/src/lib/components/shared-components/combobox.svelte
@@ -220,7 +220,7 @@
     role="listbox"
     id={listboxId}
     transition:fly={{ duration: 250 }}
-    class="absolute text-left text-sm w-full max-h-64 overflow-y-auto bg-white dark:bg-gray-800 border-t-0 border-gray-300 dark:border-gray-900 rounded-b-xl z-10"
+    class="absolute text-left text-sm w-full max-h-64 overflow-y-auto bg-white dark:bg-gray-800 border-t-0 border-gray-300 dark:border-gray-900 rounded-b-xl z-[10000]"
     class:border={isOpen}
     tabindex="-1"
   >
diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json
index f880dab347..a788666050 100644
--- a/web/src/lib/i18n/en.json
+++ b/web/src/lib/i18n/en.json
@@ -41,6 +41,7 @@
     "confirm_email_below": "To confirm, type \"{email}\" below",
     "confirm_reprocess_all_faces": "Are you sure you want to reprocess all faces? This will also clear named people.",
     "confirm_user_password_reset": "Are you sure you want to reset {user}'s password?",
+    "create_job": "Create job",
     "disable_login": "Disable login",
     "duplicate_detection_job_description": "Run machine learning on assets to detect similar images. Relies on Smart Search",
     "exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.",
@@ -68,6 +69,7 @@
     "image_thumbnail_resolution": "Thumbnail resolution",
     "image_thumbnail_resolution_description": "Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.",
     "job_concurrency": "{job} concurrency",
+    "job_created": "Job created",
     "job_not_concurrency_safe": "This job is not concurrency-safe.",
     "job_settings": "Job Settings",
     "job_settings_description": "Manage job concurrency",
@@ -196,6 +198,7 @@
     "password_settings": "Password Login",
     "password_settings_description": "Manage password login settings",
     "paths_validated_successfully": "All paths validated successfully",
+    "person_cleanup_job": "Person cleanup",
     "quota_size_gib": "Quota Size (GiB)",
     "refreshing_all_libraries": "Refreshing all libraries",
     "registration": "Admin Registration",
@@ -209,6 +212,7 @@
     "reset_settings_to_recent_saved": "Reset settings to the recent saved settings",
     "scanning_library_for_changed_files": "Scanning library for changed files",
     "scanning_library_for_new_files": "Scanning library for new files",
+    "search_jobs": "Search jobs...",
     "send_welcome_email": "Send welcome email",
     "server_external_domain_settings": "External domain",
     "server_external_domain_settings_description": "Domain for public shared links, including http(s)://",
@@ -236,6 +240,7 @@
     "storage_template_settings_description": "Manage the folder structure and file name of the upload asset",
     "storage_template_user_label": "<code>{label}</code> is the user's Storage Label",
     "system_settings": "System Settings",
+    "tag_cleanup_job": "Tag cleanup",
     "theme_custom_css_settings": "Custom CSS",
     "theme_custom_css_settings_description": "Cascading Style Sheets allow the design of Immich to be customized.",
     "theme_settings": "Theme Settings",
@@ -309,6 +314,7 @@
     "trash_settings_description": "Manage trash settings",
     "untracked_files": "Untracked Files",
     "untracked_files_description": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug",
+    "user_cleanup_job": "User cleanup",
     "user_delete_delay": "<b>{user}</b>'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.",
     "user_delete_delay_settings": "Delete delay",
     "user_delete_delay_settings_description": "Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.",
diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte
index dcd6630a01..16c2541e61 100644
--- a/web/src/routes/admin/jobs-status/+page.svelte
+++ b/web/src/routes/admin/jobs-status/+page.svelte
@@ -3,10 +3,17 @@
   import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
+  import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte';
+  import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
+  import {
+    notificationController,
+    NotificationType,
+  } from '$lib/components/shared-components/notification/notification';
   import { AppRoute } from '$lib/constants';
   import { asyncTimeout } from '$lib/utils';
-  import { getAllJobsStatus, type AllJobStatusResponseDto } from '@immich/sdk';
-  import { mdiCog } from '@mdi/js';
+  import { handleError } from '$lib/utils/handle-error';
+  import { createJob, getAllJobsStatus, ManualJobName, type AllJobStatusResponseDto } from '@immich/sdk';
+  import { mdiCog, mdiPlus } from '@mdi/js';
   import { onDestroy, onMount } from 'svelte';
   import { t } from 'svelte-i18n';
   import type { PageData } from './$types';
@@ -16,6 +23,8 @@
   let jobs: AllJobStatusResponseDto;
 
   let running = true;
+  let isOpen = false;
+  let selectedJob: ComboBoxOption | undefined = undefined;
 
   onMount(async () => {
     while (running) {
@@ -27,10 +36,38 @@
   onDestroy(() => {
     running = false;
   });
+
+  const options = [
+    { title: $t('admin.person_cleanup_job'), value: ManualJobName.PersonCleanup },
+    { title: $t('admin.tag_cleanup_job'), value: ManualJobName.TagCleanup },
+    { title: $t('admin.user_cleanup_job'), value: ManualJobName.UserCleanup },
+  ].map(({ value, title }) => ({ id: value, label: title, value }));
+
+  const handleCancel = () => (isOpen = false);
+
+  const handleCreate = async () => {
+    if (!selectedJob) {
+      return;
+    }
+
+    try {
+      await createJob({ jobCreateDto: { name: selectedJob.value as ManualJobName } });
+      notificationController.show({ message: $t('admin.job_created'), type: NotificationType.Info });
+      handleCancel();
+    } catch (error) {
+      handleError(error, $t('errors.unable_to_submit_job'));
+    }
+  };
 </script>
 
 <UserPageLayout title={data.meta.title} admin>
   <div class="flex justify-end" slot="buttons">
+    <LinkButton on:click={() => (isOpen = true)}>
+      <div class="flex place-items-center gap-2 text-sm">
+        <Icon path={mdiPlus} size="18" />
+        {$t('admin.create_job')}
+      </div>
+    </LinkButton>
     <LinkButton href="{AppRoute.ADMIN_SETTINGS}?isOpen=job">
       <div class="flex place-items-center gap-2 text-sm">
         <Icon path={mdiCog} size="18" />
@@ -46,3 +83,24 @@
     </section>
   </section>
 </UserPageLayout>
+
+{#if isOpen}
+  <ConfirmDialog
+    confirmColor="primary"
+    title={$t('admin.create_job')}
+    disabled={!selectedJob}
+    onConfirm={handleCreate}
+    onCancel={handleCancel}
+  >
+    <form on:submit|preventDefault={handleCreate} autocomplete="off" id="create-tag-form" slot="prompt" class="w-full">
+      <div class="flex flex-col gap-1 text-left">
+        <Combobox
+          bind:selectedOption={selectedJob}
+          label={$t('jobs')}
+          {options}
+          placeholder={$t('admin.search_jobs')}
+        />
+      </div>
+    </form>
+  </ConfirmDialog>
+{/if}