From f44fa45aa0d9874725b57fad244d5aa7d3480e19 Mon Sep 17 00:00:00 2001
From: Jonathan Jogenfors <jonathan@jogenfors.se>
Date: Fri, 2 Feb 2024 04:18:00 +0100
Subject: [PATCH] chore(server,cli,web): housekeeping and stricter code style
 (#6751)

* add unicorn to eslint

* fix lint errors for cli

* fix merge

* fix album name extraction

* Update cli/src/commands/upload.command.ts

Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>

* es2k23

* use lowercase os

* return undefined album name

* fix bug in asset response dto

* auto fix issues

* fix server code style

* es2022 and formatting

* fix compilation error

* fix test

* fix config load

* fix last lint errors

* set string type

* bump ts

* start work on web

* web formatting

* Fix UUIDParamDto as UUIDParamDto

* fix library service lint

* fix web errors

* fix errors

* formatting

* wip

* lints fixed

* web can now start

* alphabetical package json

* rename error

* chore: clean up

---------

Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
---
 cli/.eslintrc.cjs                             |  10 +-
 cli/package-lock.json                         |  30 +-
 cli/package.json                              |  14 +-
 cli/src/commands/upload.command.ts            |  92 ++-
 cli/src/index.ts                              |   8 +-
 cli/src/services/crawl.service.ts             |  10 +-
 cli/src/services/session.service.ts           |  16 +-
 cli/src/version.ts                            |   6 +-
 cli/test/e2e/login-key.e2e-spec.ts            |   6 +-
 cli/test/e2e/setup.ts                         |  14 +-
 cli/test/e2e/vitest.config.ts                 |   2 +-
 cli/tsconfig.json                             |   4 +-
 server/.eslintrc.js                           |   8 +-
 server/package-lock.json                      | 700 +++++++++++++++++-
 server/package.json                           |   9 +-
 server/src/domain/access/access.core.ts       |  91 ++-
 .../src/domain/activity/activity.service.ts   |   2 +-
 server/src/domain/album/album-response.dto.ts |  13 +-
 server/src/domain/album/album.service.ts      |  30 +-
 server/src/domain/api-key/api-key.service.ts  |   2 +-
 server/src/domain/asset/asset.service.spec.ts |   4 +-
 server/src/domain/asset/asset.service.ts      |  57 +-
 .../asset/response-dto/asset-response.dto.ts  |   5 +-
 .../asset/response-dto/exif-response.dto.ts   |   4 +-
 server/src/domain/audit/audit.service.ts      |  32 +-
 server/src/domain/auth/auth.service.spec.ts   |   2 +-
 server/src/domain/auth/auth.service.ts        |  19 +-
 .../src/domain/database/database.service.ts   |   4 +-
 server/src/domain/domain.constant.spec.ts     |  24 +-
 server/src/domain/domain.constant.ts          |  13 +-
 server/src/domain/domain.util.ts              |  25 +-
 .../src/domain/download/download.service.ts   |  18 +-
 server/src/domain/job/job.service.spec.ts     |   2 +-
 server/src/domain/job/job.service.ts          |  66 +-
 .../domain/library/library.service.spec.ts    |  22 +-
 server/src/domain/library/library.service.ts  |  36 +-
 server/src/domain/media/media.service.ts      |  66 +-
 server/src/domain/media/media.util.ts         |  40 +-
 .../domain/metadata/metadata.service.spec.ts  |  12 +-
 .../src/domain/metadata/metadata.service.ts   |  34 +-
 server/src/domain/partner/partner.service.ts  |   8 +-
 .../src/domain/person/person.service.spec.ts  |   8 +-
 server/src/domain/person/person.service.ts    |   6 +-
 .../repositories/database.repository.ts       |   2 +-
 .../domain/repositories/media.repository.ts   |   2 +-
 .../domain/repositories/storage.repository.ts |   6 +-
 server/src/domain/search/search.service.ts    |  11 +-
 .../server-info/server-info.service.spec.ts   |  44 +-
 .../domain/server-info/server-info.service.ts |   2 +-
 .../domain/shared-link/shared-link.service.ts |  20 +-
 .../domain/smart-info/smart-info.constant.ts  |   8 +-
 .../storage-template.service.spec.ts          |   8 +-
 .../storage-template.service.ts               |  38 +-
 server/src/domain/storage/storage.core.ts     |  69 +-
 .../system-config/system-config.core.ts       |  43 +-
 .../system-config.service.spec.ts             |   2 +-
 .../system-config/system-config.service.ts    |   2 +-
 server/src/domain/tag/tag.service.ts          |   8 +-
 .../user/response-dto/user-response.dto.ts    |   5 +-
 server/src/domain/user/user.core.ts           |   4 +-
 server/src/domain/user/user.service.spec.ts   |   2 +-
 server/src/domain/user/user.service.ts        |  11 +-
 .../commands/reset-admin-password.command.ts  |  18 +-
 server/src/immich/api-v1/asset/asset.core.ts  |   2 +-
 .../immich/api-v1/asset/asset.service.spec.ts |   4 +-
 .../src/immich/api-v1/asset/asset.service.ts  |  16 +-
 server/src/immich/app.guard.ts                |  28 +-
 server/src/immich/app.service.ts              |  14 +-
 server/src/immich/app.utils.ts                |  38 +-
 .../src/immich/controllers/auth.controller.ts |   4 +-
 .../immich/controllers/oauth.controller.ts    |   4 +-
 .../controllers/shared-link.controller.ts     |   4 +-
 .../interceptors/file-upload.interceptor.ts   |  51 +-
 server/src/infra/database.config.ts           |   5 +-
 server/src/infra/infra.config.ts              |   4 +-
 server/src/infra/infra.util.ts                |   2 +-
 server/src/infra/infra.utils.ts               |  20 +-
 .../1688392120838-AddLibraryTable.ts          |   3 +-
 .../migrations/1700713871511-UsePgVectors.ts  |   2 +-
 .../infra/repositories/access.repository.ts   |   2 +-
 .../infra/repositories/album.repository.ts    |   2 +-
 .../infra/repositories/asset.repository.ts    |  38 +-
 .../repositories/communication.repository.ts  |   6 +-
 .../infra/repositories/crypto.repository.ts   |   6 +-
 .../infra/repositories/filesystem.provider.ts |  14 +-
 .../src/infra/repositories/job.repository.ts  |  20 +-
 .../machine-learning.repository.ts            |   2 +-
 .../infra/repositories/media.repository.ts    |  14 +-
 .../infra/repositories/metadata.repository.ts |  21 +-
 .../infra/repositories/person.repository.ts   |   7 +-
 .../repositories/smart-info.repository.ts     |  18 +-
 .../repositories/system-config.repository.ts  |   4 +-
 .../src/infra/repositories/user.repository.ts |   6 +-
 server/src/infra/sql-generator/index.ts       |   7 +-
 server/src/infra/sql-generator/sql.logger.ts  |   4 +-
 .../src/infra/subscribers/audit.subscriber.ts |   6 +-
 server/src/main.ts                            |  15 +-
 .../utils/exif/coordinates.spec.ts            |  12 +-
 .../src/microservices/utils/numbers.spec.ts   |  13 +-
 server/src/microservices/utils/numbers.ts     |  12 +-
 server/src/test-utils/utils.ts                |  12 +-
 server/test/fixtures/asset.stub.ts            |  12 +-
 server/test/fixtures/media.stub.ts            |   2 +-
 .../repositories/database.repository.mock.ts  |   2 +-
 server/tsconfig.json                          |  10 +-
 web/.eslintrc.cjs                             |  15 +-
 web/package-lock.json                         | 418 ++++++++++-
 web/package.json                              |   9 +-
 web/src/api/api.ts                            |  24 +-
 web/src/api/types.ts                          |   2 +-
 .../admin-page/delete-confirm-dialoge.svelte  |   6 +-
 .../admin-page/jobs/job-tile.svelte           |   2 +-
 .../admin-page/jobs/jobs-panel.svelte         |   3 +-
 .../admin-page/restore-dialoge.svelte         |   2 +-
 .../admin-page/settings/admin-settings.svelte |  15 +-
 .../settings/setting-checkboxes.svelte        |   6 +-
 .../admin-page/settings/setting-select.svelte |   2 +-
 .../storage-template-settings.svelte          |   2 +-
 .../album-page/__tests__/album-card.spec.ts   |   6 +-
 .../components/album-page/album-card.svelte   |   2 +-
 .../components/album-page/album-viewer.svelte |   3 +-
 .../album-page/share-info-modal.svelte        |   8 +-
 .../album-page/thumbnail-selection.svelte     |   6 +-
 .../album-page/user-selection-modal.svelte    |  14 +-
 .../asset-viewer/activity-viewer.svelte       |   6 +-
 .../asset-viewer/asset-viewer.svelte          |  48 +-
 .../asset-viewer/detail-panel.svelte          |   5 +-
 .../asset-viewer/panorama-viewer.svelte       |   4 +-
 .../asset-viewer/photo-viewer.svelte          |   6 +-
 .../assets/thumbnail/image-thumbnail.svelte   |   1 +
 .../assets/thumbnail/thumbnail.svelte         |   2 +-
 .../lib/components/elements/dropdown.svelte   |   8 +-
 .../faces-page/assign-face-side-panel.svelte  |  31 +-
 .../faces-page/merge-suggestion-modal.svelte  |   2 +-
 .../components/faces-page/people-list.svelte  |   6 +-
 .../faces-page/person-side-panel.svelte       |  16 +-
 .../components/forms/create-user-form.svelte  |   7 +-
 .../components/forms/edit-user-form.svelte    |   4 +-
 .../forms/library-import-paths-form.svelte    |   2 +-
 .../forms/library-scan-settings-form.svelte   |   2 +-
 .../lib/components/forms/login-form.svelte    |   6 +-
 .../memory-page/memory-viewer.svelte          |  12 +-
 .../photos-page/actions/add-to-album.svelte   |   4 +-
 .../photos-page/actions/archive-action.svelte |   2 +-
 .../actions/asset-job-actions.svelte          |   4 +-
 .../actions/create-shared-link.svelte         |   2 +-
 .../photos-page/actions/delete-assets.svelte  |   4 +-
 .../actions/download-action.svelte            |   2 +-
 .../actions/favorite-action.svelte            |   2 +-
 .../actions/remove-from-album.svelte          |   8 +-
 .../actions/remove-from-shared-link.svelte    |   2 +-
 .../photos-page/actions/restore-assets.svelte |   8 +-
 .../actions/select-all-assets.svelte          |   4 +-
 .../photos-page/actions/stack-action.svelte   |   6 +-
 .../photos-page/asset-date-group.svelte       |  17 +-
 .../components/photos-page/asset-grid.svelte  |  31 +-
 .../asset-select-control-bar.svelte           |   2 +-
 .../components/photos-page/memory-lane.svelte |   4 +-
 .../individual-shared-viewer.svelte           |  12 +-
 .../album-selection-modal.svelte              |  13 +-
 .../shared-components/base-modal.svelte       |   6 +-
 .../shared-components/change-location.svelte  |   6 +-
 .../create-shared-link-modal.svelte           |  33 +-
 .../empty-placeholder.svelte                  |   2 +-
 .../asset-selection-viewer.svelte             |   8 +-
 .../gallery-viewer/gallery-viewer.svelte      |  16 +-
 .../shared-components/map/map.svelte          |   3 +-
 .../navigation-bar/navigation-bar.svelte      |   6 +-
 .../notification/notification-card.svelte     |   2 +-
 .../notification/notification.ts              |   2 +-
 .../shared-components/portal/portal.svelte    |  22 +-
 .../profile-image-cropper.svelte              |  16 +-
 .../scrollbar/scrollbar.svelte                |   8 +-
 .../search-bar/search-bar.svelte              |   6 +-
 .../version-announcement-box.svelte           |   4 +-
 .../sharedlinks-page/shared-link-card.svelte  |   2 +-
 .../user-settings-page/device-card.svelte     |   4 +-
 .../user-settings-page/device-list.svelte     |   4 +-
 .../user-settings-page/library-list.svelte    |  10 +-
 .../partner-selection-modal.svelte            |  12 +-
 .../user-api-key-list.svelte                  |   6 +-
 web/src/lib/stores/assets.store.ts            |  43 +-
 web/src/lib/stores/download.ts                |   2 +-
 web/src/lib/stores/preferences.store.ts       |  16 +-
 web/src/lib/stores/upload.ts                  |   4 +-
 web/src/lib/stores/websocket.ts               |   4 +-
 web/src/lib/utils/actions.ts                  |   4 +-
 web/src/lib/utils/asset-utils.spec.ts         |   6 +-
 web/src/lib/utils/asset-utils.ts              |  47 +-
 web/src/lib/utils/byte-converter.ts           |   4 +-
 web/src/lib/utils/byte-units.ts               |   2 +-
 web/src/lib/utils/context-menu.ts             |   9 +-
 web/src/lib/utils/executor-queue.ts           |   8 +-
 web/src/lib/utils/file-uploader.ts            |  14 +-
 web/src/lib/utils/person.ts                   |  22 +-
 web/src/lib/utils/time-to-seconds.spec.ts     |   2 +-
 web/src/routes/(user)/albums/+page.svelte     |  13 +-
 .../(user)/albums/[albumId]/+page.svelte      |  23 +-
 .../albums/__tests__/albums.bloc.spec.ts      |   4 +-
 web/src/routes/(user)/albums/albums.bloc.ts   |   6 +-
 web/src/routes/(user)/archive/+page.svelte    |   2 +-
 web/src/routes/(user)/favorites/+page.svelte  |   2 +-
 .../(user)/partners/[userId]/+page.svelte     |   4 +-
 web/src/routes/(user)/people/+page.svelte     |  19 +-
 .../(user)/people/[personId]/+page.svelte     |  10 +-
 web/src/routes/(user)/photos/+page.svelte     |   2 +-
 web/src/routes/(user)/search/+page.svelte     |  13 +-
 .../routes/(user)/share/[key]/+page.svelte    |   4 +-
 web/src/routes/(user)/share/[key]/+page.ts    |   8 +-
 web/src/routes/(user)/sharing/+page.svelte    |   8 +-
 web/src/routes/(user)/trash/+page.svelte      |   8 +-
 web/src/routes/admin/jobs-status/+page.svelte |   2 +-
 web/src/routes/admin/repair/+page.svelte      |  16 +-
 .../routes/admin/system-settings/+page.svelte |   2 +-
 .../routes/admin/user-management/+page.svelte |   8 +-
 web/src/routes/auth/onboarding/+page.svelte   |   4 +-
 web/src/test-data/factories/album-factory.ts  |   2 +-
 web/vite.config.js                            |   3 +-
 218 files changed, 2471 insertions(+), 1244 deletions(-)

diff --git a/cli/.eslintrc.cjs b/cli/.eslintrc.cjs
index 17a0a2dd6c..ca36d31bf7 100644
--- a/cli/.eslintrc.cjs
+++ b/cli/.eslintrc.cjs
@@ -6,7 +6,7 @@ module.exports = {
     tsconfigRootDir: __dirname,
   },
   plugins: ['@typescript-eslint/eslint-plugin'],
-  extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
+  extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'],
   root: true,
   env: {
     node: true,
@@ -19,6 +19,14 @@ module.exports = {
     '@typescript-eslint/explicit-module-boundary-types': 'off',
     '@typescript-eslint/no-explicit-any': 'off',
     '@typescript-eslint/no-floating-promises': 'error',
+    'unicorn/prefer-module': 'off',
+    curly: 2,
     'prettier/prettier': 0,
+    'unicorn/prevent-abbreviations': [
+      'error',
+      {
+        ignore: ['\\.e2e-spec$', /^ignore/i],
+      },
+    ],
   },
 };
diff --git a/cli/package-lock.json b/cli/package-lock.json
index c9afe6e467..9c218f78b9 100644
--- a/cli/package-lock.json
+++ b/cli/package-lock.json
@@ -30,15 +30,15 @@
         "@typescript-eslint/eslint-plugin": "^6.0.0",
         "@typescript-eslint/parser": "^6.0.0",
         "@vitest/coverage-v8": "^1.2.2",
-        "eslint": "^8.43.0",
-        "eslint-config-prettier": "^9.0.0",
-        "eslint-plugin-prettier": "^5.0.0",
-        "eslint-plugin-unicorn": "^50.0.0",
+        "eslint": "^8.56.0",
+        "eslint-config-prettier": "^9.1.0",
+        "eslint-plugin-prettier": "^5.1.3",
+        "eslint-plugin-unicorn": "^50.0.1",
         "immich": "file:../server",
         "mock-fs": "^5.2.0",
         "ts-node": "^10.9.1",
         "tslib": "^2.5.3",
-        "typescript": "^5.0.0",
+        "typescript": "^5.3.3",
         "vitest": "^1.2.1"
       }
     },
@@ -73,6 +73,7 @@
         "@nestjs/typeorm": "^10.0.0",
         "@nestjs/websockets": "^10.2.2",
         "@socket.io/postgres-adapter": "^0.3.1",
+        "@types/picomatch": "^2.3.3",
         "archiver": "^6.0.0",
         "async-lock": "^1.4.0",
         "axios": "^1.5.0",
@@ -96,6 +97,7 @@
         "node-addon-api": "^7.0.0",
         "openid-client": "^5.4.3",
         "pg": "^8.11.3",
+        "picomatch": "^3.0.1",
         "reflect-metadata": "^0.1.13",
         "rxjs": "^7.8.1",
         "sanitize-filename": "^1.6.3",
@@ -127,10 +129,12 @@
         "@types/ua-parser-js": "^0.7.36",
         "@typescript-eslint/eslint-plugin": "^6.4.1",
         "@typescript-eslint/parser": "^6.4.1",
+        "chokidar": "^3.5.3",
         "dotenv": "^16.3.1",
-        "eslint": "^8.48.0",
-        "eslint-config-prettier": "^9.0.0",
-        "eslint-plugin-prettier": "^5.0.0",
+        "eslint": "^8.56.0",
+        "eslint-config-prettier": "^9.1.0",
+        "eslint-plugin-prettier": "^5.1.3",
+        "eslint-plugin-unicorn": "^50.0.1",
         "jest": "^29.6.4",
         "jest-when": "^3.6.0",
         "mock-fs": "^5.2.0",
@@ -8768,6 +8772,7 @@
         "@types/mock-fs": "^4.13.1",
         "@types/multer": "^1.4.7",
         "@types/node": "^20.5.7",
+        "@types/picomatch": "^2.3.3",
         "@types/sharp": "^0.31.1",
         "@types/supertest": "^6.0.0",
         "@types/ua-parser-js": "^0.7.36",
@@ -8778,13 +8783,15 @@
         "axios": "^1.5.0",
         "bcrypt": "^5.1.1",
         "bullmq": "^4.8.0",
+        "chokidar": "^3.5.3",
         "class-transformer": "^0.5.1",
         "class-validator": "^0.14.0",
         "cookie-parser": "^1.4.6",
         "dotenv": "^16.3.1",
-        "eslint": "^8.48.0",
-        "eslint-config-prettier": "^9.0.0",
-        "eslint-plugin-prettier": "^5.0.0",
+        "eslint": "^8.56.0",
+        "eslint-config-prettier": "^9.1.0",
+        "eslint-plugin-prettier": "^5.1.3",
+        "eslint-plugin-unicorn": "^50.0.1",
         "exiftool-vendored": "~24.4.0",
         "exiftool-vendored.pl": "12.73",
         "fluent-ffmpeg": "^2.1.2",
@@ -8803,6 +8810,7 @@
         "node-addon-api": "^7.0.0",
         "openid-client": "^5.4.3",
         "pg": "^8.11.3",
+        "picomatch": "^3.0.1",
         "prettier": "^3.0.2",
         "prettier-plugin-organize-imports": "^3.2.3",
         "reflect-metadata": "^0.1.13",
diff --git a/cli/package.json b/cli/package.json
index 6c18605661..9e32061e3d 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -28,18 +28,18 @@
     "@types/cli-progress": "^3.11.0",
     "@types/mock-fs": "^4.13.1",
     "@types/node": "^20.3.1",
-    "@typescript-eslint/eslint-plugin": "^6.0.0",
-    "@typescript-eslint/parser": "^6.0.0",
+    "@typescript-eslint/eslint-plugin": "^6.4.1",
+    "@typescript-eslint/parser": "^6.4.1",
     "@vitest/coverage-v8": "^1.2.2",
-    "eslint": "^8.43.0",
-    "eslint-config-prettier": "^9.0.0",
-    "eslint-plugin-prettier": "^5.0.0",
-    "eslint-plugin-unicorn": "^50.0.0",
+    "eslint": "^8.56.0",
+    "eslint-config-prettier": "^9.1.0",
+    "eslint-plugin-prettier": "^5.1.3",
+    "eslint-plugin-unicorn": "^50.0.1",
     "immich": "file:../server",
     "mock-fs": "^5.2.0",
     "ts-node": "^10.9.1",
     "tslib": "^2.5.3",
-    "typescript": "^5.0.0",
+    "typescript": "^5.3.3",
     "vitest": "^1.2.1"
   },
   "scripts": {
diff --git a/cli/src/commands/upload.command.ts b/cli/src/commands/upload.command.ts
index 17fc6541bc..f026e374df 100644
--- a/cli/src/commands/upload.command.ts
+++ b/cli/src/commands/upload.command.ts
@@ -8,7 +8,7 @@ import { BaseCommand } from './base-command';
 import { basename } from 'node:path';
 import { access, constants, stat, unlink } from 'node:fs/promises';
 import { createHash } from 'node:crypto';
-import Os from 'os';
+import os from 'node:os';
 
 class Asset {
   readonly path: string;
@@ -27,7 +27,7 @@ class Asset {
 
   async prepare() {
     const stats = await stat(this.path);
-    this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, '');
+    this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replaceAll(/\s+/g, '');
     this.fileCreatedAt = stats.mtime.toISOString();
     this.fileModifiedAt = stats.mtime.toISOString();
     this.fileSize = stats.size;
@@ -35,9 +35,15 @@ class Asset {
   }
 
   async getUploadFormData(): Promise<FormData> {
-    if (!this.deviceAssetId) throw new Error('Device asset id not set');
-    if (!this.fileCreatedAt) throw new Error('File created at not set');
-    if (!this.fileModifiedAt) throw new Error('File modified at not set');
+    if (!this.deviceAssetId) {
+      throw new Error('Device asset id not set');
+    }
+    if (!this.fileCreatedAt) {
+      throw new Error('File created at not set');
+    }
+    if (!this.fileModifiedAt) {
+      throw new Error('File modified at not set');
+    }
 
     // TODO: doesn't xmp replace the file extension? Will need investigation
     const sideCarPath = `${this.path}.xmp`;
@@ -45,7 +51,7 @@ class Asset {
     try {
       await access(sideCarPath, constants.R_OK);
       sidecarData = createReadStream(sideCarPath);
-    } catch (error) {}
+    } catch {}
 
     const data: any = {
       assetData: createReadStream(this.path),
@@ -57,8 +63,8 @@ class Asset {
     };
     const formData = new FormData();
 
-    for (const prop in data) {
-      formData.append(prop, data[prop]);
+    for (const property in data) {
+      formData.append(property, data[property]);
     }
 
     if (sidecarData) {
@@ -86,12 +92,8 @@ class Asset {
     return await sha1(this.path);
   }
 
-  private extractAlbumName(): string {
-    if (Os.platform() === 'win32') {
-      return this.path.split('\\').slice(-2)[0];
-    } else {
-      return this.path.split('/').slice(-2)[0];
-    }
+  private extractAlbumName(): string | undefined {
+    return os.platform() === 'win32' ? this.path.split('\\').at(-2) : this.path.split('/').at(-2);
   }
 }
 
@@ -162,7 +164,7 @@ export class UploadCommand extends BaseCommand {
       }
     }
 
-    const existingAlbums = (await this.immichApi.albumApi.getAllAlbums()).data;
+    const { data: existingAlbums } = await this.immichApi.albumApi.getAllAlbums();
 
     uploadProgress.start(totalSize, 0);
     uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
@@ -195,32 +197,30 @@ export class UploadCommand extends BaseCommand {
           skipAsset = skipUpload && !isDuplicate;
         }
 
-        if (!skipAsset) {
-          if (!options.dryRun) {
-            if (!skipUpload) {
-              const formData = await asset.getUploadFormData();
-              const res = await this.uploadAsset(formData);
-              existingAssetId = res.data.id;
-              uploadCounter++;
-              totalSizeUploaded += asset.fileSize;
+        if (!skipAsset && !options.dryRun) {
+          if (!skipUpload) {
+            const formData = await asset.getUploadFormData();
+            const { data } = await this.uploadAsset(formData);
+            existingAssetId = data.id;
+            uploadCounter++;
+            totalSizeUploaded += asset.fileSize;
+          }
+
+          if ((options.album || options.albumName) && asset.albumName !== undefined) {
+            let album = existingAlbums.find((album) => album.albumName === asset.albumName);
+            if (!album) {
+              const { data } = await this.immichApi.albumApi.createAlbum({
+                createAlbumDto: { albumName: asset.albumName },
+              });
+              album = data;
+              existingAlbums.push(album);
             }
 
-            if ((options.album || options.albumName) && asset.albumName !== undefined) {
-              let album = existingAlbums.find((album) => album.albumName === asset.albumName);
-              if (!album) {
-                const res = await this.immichApi.albumApi.createAlbum({
-                  createAlbumDto: { albumName: asset.albumName },
-                });
-                album = res.data;
-                existingAlbums.push(album);
-              }
-
-              if (existingAssetId) {
-                await this.immichApi.albumApi.addAssetsToAlbum({
-                  id: album.id,
-                  bulkIdsDto: { ids: [existingAssetId] },
-                });
-              }
+            if (existingAssetId) {
+              await this.immichApi.albumApi.addAssetsToAlbum({
+                id: album.id,
+                bulkIdsDto: { ids: [existingAssetId] },
+              });
             }
           }
         }
@@ -233,12 +233,7 @@ export class UploadCommand extends BaseCommand {
       uploadProgress.stop();
     }
 
-    let messageStart;
-    if (options.dryRun) {
-      messageStart = 'Would have';
-    } else {
-      messageStart = 'Successfully';
-    }
+    const messageStart = options.dryRun ? 'Would have' : 'Successfully';
 
     if (uploadCounter === 0) {
       console.log('All assets were already uploaded, nothing to do.');
@@ -276,12 +271,11 @@ export class UploadCommand extends BaseCommand {
         'x-api-key': this.immichApi.apiKey,
         ...data.getHeaders(),
       },
-      maxContentLength: Infinity,
-      maxBodyLength: Infinity,
+      maxContentLength: Number.POSITIVE_INFINITY,
+      maxBodyLength: Number.POSITIVE_INFINITY,
       data,
     };
 
-    const res = await axios(config);
-    return res;
+    return axios(config);
   }
 }
diff --git a/cli/src/index.ts b/cli/src/index.ts
index 8369bff934..6582b37956 100644
--- a/cli/src/index.ts
+++ b/cli/src/index.ts
@@ -1,21 +1,21 @@
 #! /usr/bin/env node
 import { Command, Option } from 'commander';
 import path from 'node:path';
-import os from 'os';
+import os from 'node:os';
 import { version } from '../package.json';
 import { LoginCommand } from './commands/login';
 import { LogoutCommand } from './commands/logout.command';
 import { ServerInfoCommand } from './commands/server-info.command';
 import { UploadCommand } from './commands/upload.command';
 
-const userHomeDir = os.homedir();
-const configDir = path.join(userHomeDir, '.config/immich/');
+const homeDirectory = os.homedir();
+const configDirectory = path.join(homeDirectory, '.config/immich/');
 
 const program = new Command()
   .name('immich')
   .version(version)
   .description('Command line interface for Immich')
-  .addOption(new Option('-d, --config', 'Configuration directory').env('IMMICH_CONFIG_DIR').default(configDir));
+  .addOption(new Option('-d, --config', 'Configuration directory').env('IMMICH_CONFIG_DIR').default(configDirectory));
 
 program
   .command('upload')
diff --git a/cli/src/services/crawl.service.ts b/cli/src/services/crawl.service.ts
index bfe94a8992..3ad0fcf3b8 100644
--- a/cli/src/services/crawl.service.ts
+++ b/cli/src/services/crawl.service.ts
@@ -1,5 +1,5 @@
 import { glob } from 'glob';
-import * as fs from 'fs';
+import * as fs from 'node:fs';
 
 export class CrawlOptions {
   pathsToCrawl!: string[];
@@ -12,14 +12,14 @@ export class CrawlService {
   private readonly extensions!: string[];
 
   constructor(image: string[], video: string[]) {
-    this.extensions = image.concat(video).map((extension) => extension.replace('.', ''));
+    this.extensions = [...image, ...video].map((extension) => extension.replace('.', ''));
   }
 
   async crawl(options: CrawlOptions): Promise<string[]> {
     const { recursive, pathsToCrawl, exclusionPatterns, includeHidden } = options;
 
     if (!pathsToCrawl) {
-      return Promise.resolve([]);
+      return [];
     }
 
     const patterns: string[] = [];
@@ -65,8 +65,6 @@ export class CrawlService {
       ignore: exclusionPatterns,
     });
 
-    const returnedFiles = crawledFiles.concat(globbedFiles);
-    returnedFiles.sort();
-    return returnedFiles;
+    return [...crawledFiles, ...globbedFiles].sort();
   }
 }
diff --git a/cli/src/services/session.service.ts b/cli/src/services/session.service.ts
index ee49a7074a..9276a47210 100644
--- a/cli/src/services/session.service.ts
+++ b/cli/src/services/session.service.ts
@@ -1,4 +1,4 @@
-import { existsSync } from 'fs';
+import { existsSync } from 'node:fs';
 import { access, constants, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
 import path from 'node:path';
 import yaml from 'yaml';
@@ -15,12 +15,12 @@ class LoginError extends Error {
 }
 
 export class SessionService {
-  readonly configDir!: string;
+  readonly configDirectory!: string;
   readonly authPath!: string;
 
-  constructor(configDir: string) {
-    this.configDir = configDir;
-    this.authPath = path.join(configDir, '/auth.yml');
+  constructor(configDirectory: string) {
+    this.configDirectory = configDirectory;
+    this.authPath = path.join(configDirectory, '/auth.yml');
   }
 
   async connect(): Promise<ImmichApi> {
@@ -74,11 +74,11 @@ export class SessionService {
 
     console.log(`Logged in as ${userInfo.email}`);
 
-    if (!existsSync(this.configDir)) {
+    if (!existsSync(this.configDirectory)) {
       // Create config folder if it doesn't exist
-      const created = await mkdir(this.configDir, { recursive: true });
+      const created = await mkdir(this.configDirectory, { recursive: true });
       if (!created) {
-        throw new Error(`Failed to create config folder ${this.configDir}`);
+        throw new Error(`Failed to create config folder ${this.configDirectory}`);
       }
     }
 
diff --git a/cli/src/version.ts b/cli/src/version.ts
index e33764693a..6899251eea 100644
--- a/cli/src/version.ts
+++ b/cli/src/version.ts
@@ -1,4 +1,4 @@
-import pkg from '../package.json';
+import { version } from '../package.json';
 
 export interface ICliVersion {
   major: number;
@@ -23,7 +23,7 @@ export class CliVersion implements ICliVersion {
   }
 
   static fromString(version: string): CliVersion {
-    const regex = /(?:v)?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i;
+    const regex = /v?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i;
     const matchResult = version.match(regex);
     if (matchResult) {
       const [, major, minor, patch] = matchResult.map(Number);
@@ -34,4 +34,4 @@ export class CliVersion implements ICliVersion {
   }
 }
 
-export const cliVersion = CliVersion.fromString(pkg.version);
+export const cliVersion = CliVersion.fromString(version);
diff --git a/cli/test/e2e/login-key.e2e-spec.ts b/cli/test/e2e/login-key.e2e-spec.ts
index b9761c6bd3..d1e7f780e3 100644
--- a/cli/test/e2e/login-key.e2e-spec.ts
+++ b/cli/test/e2e/login-key.e2e-spec.ts
@@ -10,10 +10,10 @@ describe(`login-key (e2e)`, () => {
 
   beforeAll(async () => {
     await testApp.create();
-    if (!process.env.IMMICH_INSTANCE_URL) {
-      throw new Error('IMMICH_INSTANCE_URL environment variable not set');
-    } else {
+    if (process.env.IMMICH_INSTANCE_URL) {
       instanceUrl = process.env.IMMICH_INSTANCE_URL;
+    } else {
+      throw new Error('IMMICH_INSTANCE_URL environment variable not set');
     }
   });
 
diff --git a/cli/test/e2e/setup.ts b/cli/test/e2e/setup.ts
index 09872b3adf..52b2ae082c 100644
--- a/cli/test/e2e/setup.ts
+++ b/cli/test/e2e/setup.ts
@@ -1,6 +1,11 @@
-import path from 'path';
+import path from 'node:path';
 import { PostgreSqlContainer } from '@testcontainers/postgresql';
-import { access } from 'fs/promises';
+import { access } from 'node:fs/promises';
+
+export const directoryExists = (directory: string) =>
+  access(directory)
+    .then(() => true)
+    .catch(() => false);
 
 export default async () => {
   let IMMICH_TEST_ASSET_PATH: string = '';
@@ -12,11 +17,6 @@ export default async () => {
     IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH;
   }
 
-  const directoryExists = async (dirPath: string) =>
-    await access(dirPath)
-      .then(() => true)
-      .catch(() => false);
-
   if (!(await directoryExists(`${IMMICH_TEST_ASSET_PATH}/albums`))) {
     throw new Error(
       `Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${IMMICH_TEST_ASSET_PATH} before testing`,
diff --git a/cli/test/e2e/vitest.config.ts b/cli/test/e2e/vitest.config.ts
index ddb161c7ec..1657938765 100644
--- a/cli/test/e2e/vitest.config.ts
+++ b/cli/test/e2e/vitest.config.ts
@@ -17,6 +17,6 @@ export default defineConfig({
         minForks: 1,
       },
     },
-    testTimeout: 10000,
+    testTimeout: 10_000,
   },
 });
diff --git a/cli/tsconfig.json b/cli/tsconfig.json
index 4576ca4b6f..3742f4c192 100644
--- a/cli/tsconfig.json
+++ b/cli/tsconfig.json
@@ -9,7 +9,7 @@
     "experimentalDecorators": true,
     "allowSyntheticDefaultImports": true,
     "resolveJsonModule": true,
-    "target": "es2021",
+    "target": "es2022",
     "sourceMap": true,
     "outDir": "./dist",
     "incremental": true,
@@ -30,5 +30,5 @@
     },
     "types": ["vitest/globals"]
   },
-  "exclude": ["dist", "node_modules", "upload"]
+  "exclude": ["dist", "node_modules"]
 }
diff --git a/server/.eslintrc.js b/server/.eslintrc.js
index 2e46281fe6..f1e6564d87 100644
--- a/server/.eslintrc.js
+++ b/server/.eslintrc.js
@@ -6,7 +6,7 @@ module.exports = {
     tsconfigRootDir: __dirname,
   },
   plugins: ['@typescript-eslint/eslint-plugin'],
-  extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
+  extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'],
   root: true,
   env: {
     node: true,
@@ -19,6 +19,12 @@ module.exports = {
     '@typescript-eslint/explicit-module-boundary-types': 'off',
     '@typescript-eslint/no-explicit-any': 'off',
     '@typescript-eslint/no-floating-promises': 'error',
+    'unicorn/prevent-abbreviations': 'off',
+    'unicorn/filename-case': 'off',
+    'unicorn/no-null': 'off',
+    'unicorn/prefer-top-level-await': 'off',
+    'unicorn/prefer-event-target': 'off',
+    'unicorn/no-thenable': 'off',
     curly: 2,
     'prettier/prettier': 0,
   },
diff --git a/server/package-lock.json b/server/package-lock.json
index 8470fb9c68..c01dc1567d 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -80,9 +80,10 @@
         "@typescript-eslint/eslint-plugin": "^6.4.1",
         "@typescript-eslint/parser": "^6.4.1",
         "dotenv": "^16.3.1",
-        "eslint": "^8.48.0",
-        "eslint-config-prettier": "^9.0.0",
-        "eslint-plugin-prettier": "^5.0.0",
+        "eslint": "^8.56.0",
+        "eslint-config-prettier": "^9.1.0",
+        "eslint-plugin-prettier": "^5.1.3",
+        "eslint-plugin-unicorn": "^50.0.1",
         "jest": "^29.6.4",
         "jest-when": "^3.6.0",
         "mock-fs": "^5.2.0",
@@ -97,7 +98,7 @@
         "ts-loader": "^9.4.4",
         "ts-node": "^10.9.1",
         "tsconfig-paths": "^4.2.0",
-        "typescript": "^5.2.2",
+        "typescript": "^5.3.3",
         "utimes": "^5.2.1"
       }
     },
@@ -3233,6 +3234,12 @@
         "undici-types": "~5.26.4"
       }
     },
+    "node_modules/@types/normalize-package-data": {
+      "version": "2.4.4",
+      "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
+      "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
+      "dev": true
+    },
     "node_modules/@types/pg": {
       "version": "8.10.9",
       "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.9.tgz",
@@ -4439,9 +4446,9 @@
       }
     },
     "node_modules/browserslist": {
-      "version": "4.22.1",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
-      "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
+      "version": "4.22.3",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz",
+      "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==",
       "dev": true,
       "funding": [
         {
@@ -4458,9 +4465,9 @@
         }
       ],
       "dependencies": {
-        "caniuse-lite": "^1.0.30001541",
-        "electron-to-chromium": "^1.4.535",
-        "node-releases": "^2.0.13",
+        "caniuse-lite": "^1.0.30001580",
+        "electron-to-chromium": "^1.4.648",
+        "node-releases": "^2.0.14",
         "update-browserslist-db": "^1.0.13"
       },
       "bin": {
@@ -4545,6 +4552,18 @@
         "node": ">=10.0.0"
       }
     },
+    "node_modules/builtin-modules": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
+      "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/bullmq": {
       "version": "4.17.0",
       "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.17.0.tgz",
@@ -4664,9 +4683,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001542",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001542.tgz",
-      "integrity": "sha512-UrtAXVcj1mvPBFQ4sKd38daP8dEcXXr5sQe6QNNinaPd0iA/cxg9/l3VrSdL73jgw5sKyuQ6jNgiKO12W3SsVA==",
+      "version": "1.0.30001581",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz",
+      "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==",
       "dev": true,
       "funding": [
         {
@@ -4791,6 +4810,27 @@
         "validator": "^13.9.0"
       }
     },
+    "node_modules/clean-regexp": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
+      "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==",
+      "dev": true,
+      "dependencies": {
+        "escape-string-regexp": "^1.0.5"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/clean-regexp/node_modules/escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
     "node_modules/cli-cursor": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -5111,6 +5151,19 @@
       "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
       "dev": true
     },
+    "node_modules/core-js-compat": {
+      "version": "3.35.1",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz",
+      "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==",
+      "dev": true,
+      "dependencies": {
+        "browserslist": "^4.22.2"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
     "node_modules/core-util-is": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -5535,9 +5588,9 @@
       "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.4.538",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.538.tgz",
-      "integrity": "sha512-1a2m63NEookb1beNFTGDihgF3CKL7ksZ7PSA0VloON5DpTEhnOVgaDes8xkrDhkXRxlcN8JymQDGnv+Nn+uvhg==",
+      "version": "1.4.650",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.650.tgz",
+      "integrity": "sha512-sYSQhJCJa4aGA1wYol5cMQgekDBlbVfTRavlGZVr3WZpDdOPcp6a6xUnFfrt8TqZhsBYYbDxJZCjGfHuGupCRQ==",
       "dev": true
     },
     "node_modules/emittery": {
@@ -5751,6 +5804,66 @@
         }
       }
     },
+    "node_modules/eslint-plugin-unicorn": {
+      "version": "50.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz",
+      "integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.22.20",
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@eslint/eslintrc": "^2.1.4",
+        "ci-info": "^4.0.0",
+        "clean-regexp": "^1.0.0",
+        "core-js-compat": "^3.34.0",
+        "esquery": "^1.5.0",
+        "indent-string": "^4.0.0",
+        "is-builtin-module": "^3.2.1",
+        "jsesc": "^3.0.2",
+        "pluralize": "^8.0.0",
+        "read-pkg-up": "^7.0.1",
+        "regexp-tree": "^0.1.27",
+        "regjsparser": "^0.10.0",
+        "semver": "^7.5.4",
+        "strip-indent": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
+      },
+      "peerDependencies": {
+        "eslint": ">=8.56.0"
+      }
+    },
+    "node_modules/eslint-plugin-unicorn/node_modules/ci-info": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz",
+      "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/sibiraj-s"
+        }
+      ],
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/eslint-plugin-unicorn/node_modules/jsesc": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
+      "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
+      "dev": true,
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/eslint-scope": {
       "version": "7.2.2",
       "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -6884,6 +6997,12 @@
         "node": "*"
       }
     },
+    "node_modules/hosted-git-info": {
+      "version": "2.8.9",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+      "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+      "dev": true
+    },
     "node_modules/html-escaper": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -7019,6 +7138,15 @@
         "node": ">=0.8.19"
       }
     },
+    "node_modules/indent-string": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/inflight": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -7114,6 +7242,21 @@
         "node": ">=8"
       }
     },
+    "node_modules/is-builtin-module": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
+      "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
+      "dev": true,
+      "dependencies": {
+        "builtin-modules": "^3.3.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/is-core-module": {
       "version": "2.13.0",
       "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
@@ -8475,6 +8618,15 @@
         "node": ">=6"
       }
     },
+    "node_modules/min-indent": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+      "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -8818,9 +8970,9 @@
       "dev": true
     },
     "node_modules/node-releases": {
-      "version": "2.0.13",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
-      "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
+      "version": "2.0.14",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
+      "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
       "dev": true
     },
     "node_modules/nopt": {
@@ -8837,6 +8989,27 @@
         "node": ">=6"
       }
     },
+    "node_modules/normalize-package-data": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+      "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+      "dev": true,
+      "dependencies": {
+        "hosted-git-info": "^2.1.4",
+        "resolve": "^1.10.0",
+        "semver": "2 || 3 || 4 || 5",
+        "validate-npm-package-license": "^3.0.1"
+      }
+    },
+    "node_modules/normalize-package-data/node_modules/semver": {
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver"
+      }
+    },
     "node_modules/normalize-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -9735,6 +9908,108 @@
       "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
       "dev": true
     },
+    "node_modules/read-pkg": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
+      "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
+      "dev": true,
+      "dependencies": {
+        "@types/normalize-package-data": "^2.4.0",
+        "normalize-package-data": "^2.5.0",
+        "parse-json": "^5.0.0",
+        "type-fest": "^0.6.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
+      "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
+      "dev": true,
+      "dependencies": {
+        "find-up": "^4.1.0",
+        "read-pkg": "^5.2.0",
+        "type-fest": "^0.8.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "dev": true,
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "dev": true,
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "dev": true,
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "dev": true,
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/type-fest": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+      "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg/node_modules/type-fest": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
+      "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/readable-stream": {
       "version": "3.6.2",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -9838,6 +10113,36 @@
       "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
       "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
     },
+    "node_modules/regexp-tree": {
+      "version": "0.1.27",
+      "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
+      "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
+      "dev": true,
+      "bin": {
+        "regexp-tree": "bin/regexp-tree"
+      }
+    },
+    "node_modules/regjsparser": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz",
+      "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==",
+      "dev": true,
+      "dependencies": {
+        "jsesc": "~0.5.0"
+      },
+      "bin": {
+        "regjsparser": "bin/parser"
+      }
+    },
+    "node_modules/regjsparser/node_modules/jsesc": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+      "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==",
+      "dev": true,
+      "bin": {
+        "jsesc": "bin/jsesc"
+      }
+    },
     "node_modules/repeat-string": {
       "version": "1.6.1",
       "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
@@ -10465,6 +10770,38 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/spdx-correct": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+      "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+      "dev": true,
+      "dependencies": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/spdx-exceptions": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz",
+      "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==",
+      "dev": true
+    },
+    "node_modules/spdx-expression-parse": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+      "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+      "dev": true,
+      "dependencies": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/spdx-license-ids": {
+      "version": "3.0.16",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz",
+      "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==",
+      "dev": true
+    },
     "node_modules/split-ca": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
@@ -10672,6 +11009,18 @@
         "node": ">=6"
       }
     },
+    "node_modules/strip-indent": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+      "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+      "dev": true,
+      "dependencies": {
+        "min-indent": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/strip-json-comments": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -11901,6 +12250,16 @@
       "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
       "dev": true
     },
+    "node_modules/validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "dev": true,
+      "dependencies": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
     "node_modules/validator": {
       "version": "13.11.0",
       "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
@@ -14439,6 +14798,12 @@
         "undici-types": "~5.26.4"
       }
     },
+    "@types/normalize-package-data": {
+      "version": "2.4.4",
+      "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
+      "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
+      "dev": true
+    },
     "@types/pg": {
       "version": "8.10.9",
       "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.9.tgz",
@@ -15402,14 +15767,14 @@
       }
     },
     "browserslist": {
-      "version": "4.22.1",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
-      "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
+      "version": "4.22.3",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz",
+      "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==",
       "dev": true,
       "requires": {
-        "caniuse-lite": "^1.0.30001541",
-        "electron-to-chromium": "^1.4.535",
-        "node-releases": "^2.0.13",
+        "caniuse-lite": "^1.0.30001580",
+        "electron-to-chromium": "^1.4.648",
+        "node-releases": "^2.0.14",
         "update-browserslist-db": "^1.0.13"
       }
     },
@@ -15462,6 +15827,12 @@
       "dev": true,
       "optional": true
     },
+    "builtin-modules": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
+      "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
+      "dev": true
+    },
     "bullmq": {
       "version": "4.17.0",
       "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.17.0.tgz",
@@ -15553,9 +15924,9 @@
       "dev": true
     },
     "caniuse-lite": {
-      "version": "1.0.30001542",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001542.tgz",
-      "integrity": "sha512-UrtAXVcj1mvPBFQ4sKd38daP8dEcXXr5sQe6QNNinaPd0iA/cxg9/l3VrSdL73jgw5sKyuQ6jNgiKO12W3SsVA==",
+      "version": "1.0.30001581",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz",
+      "integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==",
       "dev": true
     },
     "chalk": {
@@ -15631,6 +16002,23 @@
         "validator": "^13.9.0"
       }
     },
+    "clean-regexp": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
+      "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==",
+      "dev": true,
+      "requires": {
+        "escape-string-regexp": "^1.0.5"
+      },
+      "dependencies": {
+        "escape-string-regexp": {
+          "version": "1.0.5",
+          "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+          "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+          "dev": true
+        }
+      }
+    },
     "cli-cursor": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -15874,6 +16262,15 @@
       "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
       "dev": true
     },
+    "core-js-compat": {
+      "version": "3.35.1",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz",
+      "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==",
+      "dev": true,
+      "requires": {
+        "browserslist": "^4.22.2"
+      }
+    },
     "core-util-is": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -16185,9 +16582,9 @@
       "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
     },
     "electron-to-chromium": {
-      "version": "1.4.538",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.538.tgz",
-      "integrity": "sha512-1a2m63NEookb1beNFTGDihgF3CKL7ksZ7PSA0VloON5DpTEhnOVgaDes8xkrDhkXRxlcN8JymQDGnv+Nn+uvhg==",
+      "version": "1.4.650",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.650.tgz",
+      "integrity": "sha512-sYSQhJCJa4aGA1wYol5cMQgekDBlbVfTRavlGZVr3WZpDdOPcp6a6xUnFfrt8TqZhsBYYbDxJZCjGfHuGupCRQ==",
       "dev": true
     },
     "emittery": {
@@ -16369,6 +16766,44 @@
         "synckit": "^0.8.6"
       }
     },
+    "eslint-plugin-unicorn": {
+      "version": "50.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz",
+      "integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-validator-identifier": "^7.22.20",
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@eslint/eslintrc": "^2.1.4",
+        "ci-info": "^4.0.0",
+        "clean-regexp": "^1.0.0",
+        "core-js-compat": "^3.34.0",
+        "esquery": "^1.5.0",
+        "indent-string": "^4.0.0",
+        "is-builtin-module": "^3.2.1",
+        "jsesc": "^3.0.2",
+        "pluralize": "^8.0.0",
+        "read-pkg-up": "^7.0.1",
+        "regexp-tree": "^0.1.27",
+        "regjsparser": "^0.10.0",
+        "semver": "^7.5.4",
+        "strip-indent": "^3.0.0"
+      },
+      "dependencies": {
+        "ci-info": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz",
+          "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==",
+          "dev": true
+        },
+        "jsesc": {
+          "version": "3.0.2",
+          "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
+          "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
+          "dev": true
+        }
+      }
+    },
     "eslint-scope": {
       "version": "7.2.2",
       "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -17192,6 +17627,12 @@
       "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
       "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="
     },
+    "hosted-git-info": {
+      "version": "2.8.9",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+      "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+      "dev": true
+    },
     "html-escaper": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -17277,6 +17718,12 @@
       "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
       "dev": true
     },
+    "indent-string": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+      "dev": true
+    },
     "inflight": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -17353,6 +17800,15 @@
         "binary-extensions": "^2.0.0"
       }
     },
+    "is-builtin-module": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
+      "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
+      "dev": true,
+      "requires": {
+        "builtin-modules": "^3.3.0"
+      }
+    },
     "is-core-module": {
       "version": "2.13.0",
       "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
@@ -18401,6 +18857,12 @@
       "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
       "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
     },
+    "min-indent": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+      "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+      "dev": true
+    },
     "minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -18676,9 +19138,9 @@
       "dev": true
     },
     "node-releases": {
-      "version": "2.0.13",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
-      "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
+      "version": "2.0.14",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
+      "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
       "dev": true
     },
     "nopt": {
@@ -18689,6 +19151,26 @@
         "abbrev": "1"
       }
     },
+    "normalize-package-data": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+      "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+      "dev": true,
+      "requires": {
+        "hosted-git-info": "^2.1.4",
+        "resolve": "^1.10.0",
+        "semver": "2 || 3 || 4 || 5",
+        "validate-npm-package-license": "^3.0.1"
+      },
+      "dependencies": {
+        "semver": {
+          "version": "5.7.2",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+          "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+          "dev": true
+        }
+      }
+    },
     "normalize-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -19338,6 +19820,82 @@
       "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
       "dev": true
     },
+    "read-pkg": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
+      "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
+      "dev": true,
+      "requires": {
+        "@types/normalize-package-data": "^2.4.0",
+        "normalize-package-data": "^2.5.0",
+        "parse-json": "^5.0.0",
+        "type-fest": "^0.6.0"
+      },
+      "dependencies": {
+        "type-fest": {
+          "version": "0.6.0",
+          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
+          "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
+          "dev": true
+        }
+      }
+    },
+    "read-pkg-up": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
+      "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
+      "dev": true,
+      "requires": {
+        "find-up": "^4.1.0",
+        "read-pkg": "^5.2.0",
+        "type-fest": "^0.8.1"
+      },
+      "dependencies": {
+        "find-up": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+          "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+          "dev": true,
+          "requires": {
+            "locate-path": "^5.0.0",
+            "path-exists": "^4.0.0"
+          }
+        },
+        "locate-path": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+          "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+          "dev": true,
+          "requires": {
+            "p-locate": "^4.1.0"
+          }
+        },
+        "p-limit": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+          "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+          "dev": true,
+          "requires": {
+            "p-try": "^2.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+          "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+          "dev": true,
+          "requires": {
+            "p-limit": "^2.2.0"
+          }
+        },
+        "type-fest": {
+          "version": "0.8.1",
+          "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+          "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+          "dev": true
+        }
+      }
+    },
     "readable-stream": {
       "version": "3.6.2",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -19421,6 +19979,29 @@
       "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
       "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
     },
+    "regexp-tree": {
+      "version": "0.1.27",
+      "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
+      "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
+      "dev": true
+    },
+    "regjsparser": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz",
+      "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==",
+      "dev": true,
+      "requires": {
+        "jsesc": "~0.5.0"
+      },
+      "dependencies": {
+        "jsesc": {
+          "version": "0.5.0",
+          "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+          "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==",
+          "dev": true
+        }
+      }
+    },
     "repeat-string": {
       "version": "1.6.1",
       "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
@@ -19901,6 +20482,38 @@
         }
       }
     },
+    "spdx-correct": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+      "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+      "dev": true,
+      "requires": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-exceptions": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz",
+      "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==",
+      "dev": true
+    },
+    "spdx-expression-parse": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+      "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+      "dev": true,
+      "requires": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "spdx-license-ids": {
+      "version": "3.0.16",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz",
+      "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==",
+      "dev": true
+    },
     "split-ca": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
@@ -20063,6 +20676,15 @@
       "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
       "dev": true
     },
+    "strip-indent": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+      "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+      "dev": true,
+      "requires": {
+        "min-indent": "^1.0.0"
+      }
+    },
     "strip-json-comments": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -20892,6 +21514,16 @@
         }
       }
     },
+    "validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "dev": true,
+      "requires": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
     "validator": {
       "version": "13.11.0",
       "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
diff --git a/server/package.json b/server/package.json
index 12c36e3462..481c878481 100644
--- a/server/package.json
+++ b/server/package.json
@@ -105,9 +105,10 @@
     "@typescript-eslint/eslint-plugin": "^6.4.1",
     "@typescript-eslint/parser": "^6.4.1",
     "dotenv": "^16.3.1",
-    "eslint": "^8.48.0",
-    "eslint-config-prettier": "^9.0.0",
-    "eslint-plugin-prettier": "^5.0.0",
+    "eslint": "^8.56.0",
+    "eslint-config-prettier": "^9.1.0",
+    "eslint-plugin-prettier": "^5.1.3",
+    "eslint-plugin-unicorn": "^50.0.1",
     "jest": "^29.6.4",
     "jest-when": "^3.6.0",
     "mock-fs": "^5.2.0",
@@ -122,7 +123,7 @@
     "ts-loader": "^9.4.4",
     "ts-node": "^10.9.1",
     "tsconfig-paths": "^4.2.0",
-    "typescript": "^5.2.2",
+    "typescript": "^5.3.3",
     "utimes": "^5.2.1"
   },
   "jest": {
diff --git a/server/src/domain/access/access.core.ts b/server/src/domain/access/access.core.ts
index fe9d972239..8602701072 100644
--- a/server/src/domain/access/access.core.ts
+++ b/server/src/domain/access/access.core.ts
@@ -107,42 +107,51 @@ export class AccessCore {
     const sharedLinkId = sharedLink.id;
 
     switch (permission) {
-      case Permission.ASSET_READ:
+      case Permission.ASSET_READ: {
         return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
+      }
 
-      case Permission.ASSET_VIEW:
+      case Permission.ASSET_VIEW: {
         return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
+      }
 
-      case Permission.ASSET_DOWNLOAD:
-        return !!sharedLink.allowDownload
+      case Permission.ASSET_DOWNLOAD: {
+        return sharedLink.allowDownload
           ? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids)
           : new Set();
+      }
 
-      case Permission.ASSET_UPLOAD:
+      case Permission.ASSET_UPLOAD: {
         return sharedLink.allowUpload ? ids : new Set();
+      }
 
-      case Permission.ASSET_SHARE:
+      case Permission.ASSET_SHARE: {
         // TODO: fix this to not use sharedLink.userId for access control
         return await this.repository.asset.checkOwnerAccess(sharedLink.userId, ids);
+      }
 
-      case Permission.ALBUM_READ:
+      case Permission.ALBUM_READ: {
         return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids);
+      }
 
-      case Permission.ALBUM_DOWNLOAD:
-        return !!sharedLink.allowDownload
+      case Permission.ALBUM_DOWNLOAD: {
+        return sharedLink.allowDownload
           ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids)
           : new Set();
+      }
 
-      default:
+      default: {
         return new Set();
+      }
     }
   }
 
   private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set<string>) {
     switch (permission) {
       // uses album id
-      case Permission.ACTIVITY_CREATE:
+      case Permission.ACTIVITY_CREATE: {
         return await this.repository.activity.checkCreateAccess(auth.user.id, ids);
+      }
 
       // uses activity id
       case Permission.ACTIVITY_DELETE: {
@@ -190,14 +199,17 @@ export class AccessCore {
         return setUnion(isOwner, isAlbum, isPartner);
       }
 
-      case Permission.ASSET_UPDATE:
+      case Permission.ASSET_UPDATE: {
         return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
+      }
 
-      case Permission.ASSET_DELETE:
+      case Permission.ASSET_DELETE: {
         return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
+      }
 
-      case Permission.ASSET_RESTORE:
+      case Permission.ASSET_RESTORE: {
         return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
+      }
 
       case Permission.ALBUM_READ: {
         const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
@@ -205,14 +217,17 @@ export class AccessCore {
         return setUnion(isOwner, isShared);
       }
 
-      case Permission.ALBUM_UPDATE:
+      case Permission.ALBUM_UPDATE: {
         return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
+      }
 
-      case Permission.ALBUM_DELETE:
+      case Permission.ALBUM_DELETE: {
         return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
+      }
 
-      case Permission.ALBUM_SHARE:
+      case Permission.ALBUM_SHARE: {
         return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
+      }
 
       case Permission.ALBUM_DOWNLOAD: {
         const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
@@ -220,17 +235,21 @@ export class AccessCore {
         return setUnion(isOwner, isShared);
       }
 
-      case Permission.ALBUM_REMOVE_ASSET:
+      case Permission.ALBUM_REMOVE_ASSET: {
         return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
+      }
 
-      case Permission.ASSET_UPLOAD:
+      case Permission.ASSET_UPLOAD: {
         return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
+      }
 
-      case Permission.ARCHIVE_READ:
+      case Permission.ARCHIVE_READ: {
         return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
+      }
 
-      case Permission.AUTH_DEVICE_DELETE:
+      case Permission.AUTH_DEVICE_DELETE: {
         return await this.repository.authDevice.checkOwnerAccess(auth.user.id, ids);
+      }
 
       case Permission.TIMELINE_READ: {
         const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
@@ -238,8 +257,9 @@ export class AccessCore {
         return setUnion(isOwner, isPartner);
       }
 
-      case Permission.TIMELINE_DOWNLOAD:
+      case Permission.TIMELINE_DOWNLOAD: {
         return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
+      }
 
       case Permission.LIBRARY_READ: {
         const isOwner = await this.repository.library.checkOwnerAccess(auth.user.id, ids);
@@ -247,32 +267,41 @@ export class AccessCore {
         return setUnion(isOwner, isPartner);
       }
 
-      case Permission.LIBRARY_UPDATE:
+      case Permission.LIBRARY_UPDATE: {
         return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
+      }
 
-      case Permission.LIBRARY_DELETE:
+      case Permission.LIBRARY_DELETE: {
         return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
+      }
 
-      case Permission.PERSON_READ:
+      case Permission.PERSON_READ: {
         return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
+      }
 
-      case Permission.PERSON_WRITE:
+      case Permission.PERSON_WRITE: {
         return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
+      }
 
-      case Permission.PERSON_MERGE:
+      case Permission.PERSON_MERGE: {
         return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
+      }
 
-      case Permission.PERSON_CREATE:
+      case Permission.PERSON_CREATE: {
         return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids);
+      }
 
-      case Permission.PERSON_REASSIGN:
+      case Permission.PERSON_REASSIGN: {
         return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids);
+      }
 
-      case Permission.PARTNER_UPDATE:
+      case Permission.PARTNER_UPDATE: {
         return await this.repository.partner.checkUpdateAccess(auth.user.id, ids);
+      }
 
-      default:
+      default: {
         return new Set();
+      }
     }
   }
 }
diff --git a/server/src/domain/activity/activity.service.ts b/server/src/domain/activity/activity.service.ts
index 15482acaeb..69386f561e 100644
--- a/server/src/domain/activity/activity.service.ts
+++ b/server/src/domain/activity/activity.service.ts
@@ -35,7 +35,7 @@ export class ActivityService {
       isLiked: dto.type && dto.type === ReactionType.LIKE,
     });
 
-    return activities.map(mapActivity);
+    return activities.map((activity) => mapActivity(activity));
   }
 
   async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
diff --git a/server/src/domain/album/album-response.dto.ts b/server/src/domain/album/album-response.dto.ts
index 671922408e..1a266abac4 100644
--- a/server/src/domain/album/album-response.dto.ts
+++ b/server/src/domain/album/album-response.dto.ts
@@ -27,10 +27,11 @@ export class AlbumResponseDto {
 export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumResponseDto => {
   const sharedUsers: UserResponseDto[] = [];
 
-  entity.sharedUsers?.forEach((user) => {
-    const userDto = mapUser(user);
-    sharedUsers.push(userDto);
-  });
+  if (entity.sharedUsers) {
+    for (const user of entity.sharedUsers) {
+      sharedUsers.push(mapUser(user));
+    }
+  }
 
   const assets = entity.assets || [];
 
@@ -41,9 +42,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons
   let endDate = assets.at(-1)?.fileCreatedAt || undefined;
   // Swap dates if start date is greater than end date.
   if (startDate && endDate && startDate > endDate) {
-    const temp = startDate;
-    startDate = endDate;
-    endDate = temp;
+    [startDate, endDate] = [endDate, startDate];
   }
 
   return {
diff --git a/server/src/domain/album/album.service.ts b/server/src/domain/album/album.service.ts
index 898e3f5263..b14779a802 100644
--- a/server/src/domain/album/album.service.ts
+++ b/server/src/domain/album/album.service.ts
@@ -69,19 +69,17 @@ export class AlbumService {
 
     // Get asset count for each album. Then map the result to an object:
     // { [albumId]: assetCount }
-    const albumMetadataForIds = await this.albumRepository.getMetadataForIds(albums.map((album) => album.id));
-    const albumMetadataForIdsObj: Record<string, AlbumAssetCount> = albumMetadataForIds.reduce(
-      (obj: Record<string, AlbumAssetCount>, { albumId, assetCount, startDate, endDate }) => {
-        obj[albumId] = {
-          albumId,
-          assetCount,
-          startDate,
-          endDate,
-        };
-        return obj;
-      },
-      {},
-    );
+    const results = await this.albumRepository.getMetadataForIds(albums.map((album) => album.id));
+    const albumMetadata: Record<string, AlbumAssetCount> = {};
+    for (const metadata of results) {
+      const { albumId, assetCount, startDate, endDate } = metadata;
+      albumMetadata[albumId] = {
+        albumId,
+        assetCount,
+        startDate,
+        endDate,
+      };
+    }
 
     return Promise.all(
       albums.map(async (album) => {
@@ -89,9 +87,9 @@ export class AlbumService {
         return {
           ...mapAlbumWithoutAssets(album),
           sharedLinks: undefined,
-          startDate: albumMetadataForIdsObj[album.id].startDate,
-          endDate: albumMetadataForIdsObj[album.id].endDate,
-          assetCount: albumMetadataForIdsObj[album.id].assetCount,
+          startDate: albumMetadata[album.id].startDate,
+          endDate: albumMetadata[album.id].endDate,
+          assetCount: albumMetadata[album.id].assetCount,
           lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt,
         };
       }),
diff --git a/server/src/domain/api-key/api-key.service.ts b/server/src/domain/api-key/api-key.service.ts
index 2c4b6147af..0eef1981ce 100644
--- a/server/src/domain/api-key/api-key.service.ts
+++ b/server/src/domain/api-key/api-key.service.ts
@@ -12,7 +12,7 @@ export class APIKeyService {
   ) {}
 
   async create(auth: AuthDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
-    const secret = this.crypto.randomBytes(32).toString('base64').replace(/\W/g, '');
+    const secret = this.crypto.randomBytes(32).toString('base64').replaceAll(/\W/g, '');
     const entity = await this.repository.create({
       key: this.crypto.hashSha256(secret),
       name: dto.name || 'API Key',
diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts
index 3cbe2068b6..a6b2cde3e8 100644
--- a/server/src/domain/asset/asset.service.spec.ts
+++ b/server/src/domain/asset/asset.service.spec.ts
@@ -1009,9 +1009,7 @@ describe(AssetService.name, () => {
   it('get assets by device id', async () => {
     const assets = [assetStub.image, assetStub.image1];
 
-    assetMock.getAllByDeviceId.mockImplementation(() =>
-      Promise.resolve<string[]>(Array.from(assets.map((asset) => asset.deviceAssetId))),
-    );
+    assetMock.getAllByDeviceId.mockResolvedValue(assets.map((asset) => asset.deviceAssetId));
 
     const deviceId = 'device-id';
     const result = await sut.getUserAssetsByDeviceId(authStub.user1, deviceId);
diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts
index 087a5ebcf2..e73858c311 100644
--- a/server/src/domain/asset/asset.service.ts
+++ b/server/src/domain/asset/asset.service.ts
@@ -3,7 +3,7 @@ import { ImmichLogger } from '@app/infra/logger';
 import { BadRequestException, Inject } from '@nestjs/common';
 import _ from 'lodash';
 import { DateTime, Duration } from 'luxon';
-import { extname } from 'path';
+import { extname } from 'node:path';
 import sanitize from 'sanitize-filename';
 import { AccessCore, Permission } from '../access';
 import { AuthDto } from '../auth';
@@ -93,7 +93,7 @@ export class AssetService {
   }
 
   search(auth: AuthDto, dto: AssetSearchDto) {
-    let checksum: Buffer | undefined = undefined;
+    let checksum: Buffer | undefined;
 
     if (dto.checksum) {
       const encoding = dto.checksum.length === 28 ? 'base64' : 'hex';
@@ -126,29 +126,33 @@ export class AssetService {
     const filename = file.originalName;
 
     switch (fieldName) {
-      case UploadFieldName.ASSET_DATA:
+      case UploadFieldName.ASSET_DATA: {
         if (mimeTypes.isAsset(filename)) {
           return true;
         }
         break;
+      }
 
-      case UploadFieldName.LIVE_PHOTO_DATA:
+      case UploadFieldName.LIVE_PHOTO_DATA: {
         if (mimeTypes.isVideo(filename)) {
           return true;
         }
         break;
+      }
 
-      case UploadFieldName.SIDECAR_DATA:
+      case UploadFieldName.SIDECAR_DATA: {
         if (mimeTypes.isSidecar(filename)) {
           return true;
         }
         break;
+      }
 
-      case UploadFieldName.PROFILE_DATA:
+      case UploadFieldName.PROFILE_DATA: {
         if (mimeTypes.isProfile(filename)) {
           return true;
         }
         break;
+      }
     }
 
     this.logger.error(`Unsupported file type ${filename}`);
@@ -158,13 +162,13 @@ export class AssetService {
   getUploadFilename({ auth, fieldName, file }: UploadRequest): string {
     this.access.requireUploadAccess(auth);
 
-    const originalExt = extname(file.originalName);
+    const originalExtension = extname(file.originalName);
 
     const lookup = {
-      [UploadFieldName.ASSET_DATA]: originalExt,
+      [UploadFieldName.ASSET_DATA]: originalExtension,
       [UploadFieldName.LIVE_PHOTO_DATA]: '.mov',
       [UploadFieldName.SIDECAR_DATA]: '.xmp',
-      [UploadFieldName.PROFILE_DATA]: originalExt,
+      [UploadFieldName.PROFILE_DATA]: originalExtension,
     };
 
     return sanitize(`${file.uuid}${lookup[fieldName]}`);
@@ -247,11 +251,9 @@ export class AssetService {
     await this.timeBucketChecks(auth, dto);
     const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
     const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions);
-    if (!auth.sharedLink || auth.sharedLink?.showExif) {
-      return assets.map((asset) => mapAsset(asset, { withStack: true }));
-    } else {
-      return assets.map((asset) => mapAsset(asset, { stripMetadata: true }));
-    }
+    return !auth.sharedLink || auth.sharedLink?.showExif
+      ? assets.map((asset) => mapAsset(asset, { withStack: true }))
+      : assets.map((asset) => mapAsset(asset, { stripMetadata: true }));
   }
 
   async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
@@ -371,14 +373,14 @@ export class AssetService {
       const assetsWithChildren = assets.filter((a) => a.stack && a.stack.assets.length > 0);
       ids.push(...assetsWithChildren.flatMap((child) => child.stack!.assets.map((gChild) => gChild.id)));
 
-      if (!stack) {
-        stack = await this.assetStackRepository.create({
+      if (stack) {
+        await this.assetStackRepository.update({
+          id: stack.id,
           primaryAssetId: primaryAsset.id,
           assets: ids.map((id) => ({ id }) as AssetEntity),
         });
       } else {
-        await this.assetStackRepository.update({
-          id: stack.id,
+        stack = await this.assetStackRepository.create({
           primaryAssetId: primaryAsset.id,
           assets: ids.map((id) => ({ id }) as AssetEntity),
         });
@@ -394,9 +396,10 @@ export class AssetService {
     }
 
     await this.assetRepository.updateAll(ids, options);
-    const stacksToDelete = (
-      await Promise.all(stackIdsToCheckForDelete.map((id) => this.assetStackRepository.getById(id)))
-    )
+    const stackIdsToDelete = await Promise.all(
+      stackIdsToCheckForDelete.map((id) => this.assetStackRepository.getById(id)),
+    );
+    const stacksToDelete = stackIdsToDelete
       .flatMap((stack) => (stack ? [stack] : []))
       .filter((stack) => stack.assets.length < 2);
     await Promise.all(stacksToDelete.map((as) => this.assetStackRepository.delete(as.id)));
@@ -510,9 +513,8 @@ export class AssetService {
       throw new Error('Asset not found or not in a stack');
     }
     if (oldParent != null) {
-      childIds.push(oldParent.id);
       // Get all children of old parent
-      childIds.push(...(oldParent.stack?.assets.map((a) => a.id) ?? []));
+      childIds.push(oldParent.id, ...(oldParent.stack?.assets.map((a) => a.id) ?? []));
     }
     await this.assetStackRepository.update({
       id: oldParent.stackId,
@@ -530,17 +532,20 @@ export class AssetService {
 
     for (const id of dto.assetIds) {
       switch (dto.name) {
-        case AssetJobName.REFRESH_METADATA:
+        case AssetJobName.REFRESH_METADATA: {
           jobs.push({ name: JobName.METADATA_EXTRACTION, data: { id } });
           break;
+        }
 
-        case AssetJobName.REGENERATE_THUMBNAIL:
+        case AssetJobName.REGENERATE_THUMBNAIL: {
           jobs.push({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id } });
           break;
+        }
 
-        case AssetJobName.TRANSCODE_VIDEO:
+        case AssetJobName.TRANSCODE_VIDEO: {
           jobs.push({ name: JobName.VIDEO_CONVERSION, data: { id } });
           break;
+        }
       }
     }
 
diff --git a/server/src/domain/asset/response-dto/asset-response.dto.ts b/server/src/domain/asset/response-dto/asset-response.dto.ts
index d70e5963c4..94a9f8a42d 100644
--- a/server/src/domain/asset/response-dto/asset-response.dto.ts
+++ b/server/src/domain/asset/response-dto/asset-response.dto.ts
@@ -26,7 +26,6 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
   libraryId!: string;
   originalPath!: string;
   originalFileName!: string;
-  resized!: boolean;
   fileCreatedAt!: Date;
   fileModifiedAt!: Date;
   updatedAt!: Date;
@@ -56,7 +55,7 @@ export type AssetMapOptions = {
 const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[] => {
   const result: PersonWithFacesResponseDto[] = [];
   if (faces) {
-    faces.forEach((face) => {
+    for (const face of faces) {
       if (face.person) {
         const existingPersonEntry = result.find((item) => item.id === face.person!.id);
         if (existingPersonEntry) {
@@ -65,7 +64,7 @@ const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[]
           result.push({ ...mapPerson(face.person!), faces: [mapFacesWithoutPerson(face)] });
         }
       }
-    });
+    }
   }
 
   return result;
diff --git a/server/src/domain/asset/response-dto/exif-response.dto.ts b/server/src/domain/asset/response-dto/exif-response.dto.ts
index cb0f8399a1..f4d0226b47 100644
--- a/server/src/domain/asset/response-dto/exif-response.dto.ts
+++ b/server/src/domain/asset/response-dto/exif-response.dto.ts
@@ -33,7 +33,7 @@ export function mapExif(entity: ExifEntity): ExifResponseDto {
     model: entity.model,
     exifImageWidth: entity.exifImageWidth,
     exifImageHeight: entity.exifImageHeight,
-    fileSizeInByte: entity.fileSizeInByte ? parseInt(entity.fileSizeInByte.toString()) : null,
+    fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
     orientation: entity.orientation,
     dateTimeOriginal: entity.dateTimeOriginal,
     modifyDate: entity.modifyDate,
@@ -55,7 +55,7 @@ export function mapExif(entity: ExifEntity): ExifResponseDto {
 
 export function mapSanitizedExif(entity: ExifEntity): ExifResponseDto {
   return {
-    fileSizeInByte: entity.fileSizeInByte ? parseInt(entity.fileSizeInByte.toString()) : null,
+    fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
     orientation: entity.orientation,
     dateTimeOriginal: entity.dateTimeOriginal,
     timeZone: entity.timeZone,
diff --git a/server/src/domain/audit/audit.service.ts b/server/src/domain/audit/audit.service.ts
index 1466d5fdb2..887b72e2cd 100644
--- a/server/src/domain/audit/audit.service.ts
+++ b/server/src/domain/audit/audit.service.ts
@@ -91,40 +91,50 @@ export class AuditService {
       }
 
       switch (pathType) {
-        case AssetPathType.ENCODED_VIDEO:
+        case AssetPathType.ENCODED_VIDEO: {
           await this.assetRepository.save({ id, encodedVideoPath: pathValue });
           break;
+        }
 
-        case AssetPathType.JPEG_THUMBNAIL:
+        case AssetPathType.JPEG_THUMBNAIL: {
           await this.assetRepository.save({ id, resizePath: pathValue });
           break;
+        }
 
-        case AssetPathType.WEBP_THUMBNAIL:
+        case AssetPathType.WEBP_THUMBNAIL: {
           await this.assetRepository.save({ id, webpPath: pathValue });
           break;
+        }
 
-        case AssetPathType.ORIGINAL:
+        case AssetPathType.ORIGINAL: {
           await this.assetRepository.save({ id, originalPath: pathValue });
           break;
+        }
 
-        case AssetPathType.SIDECAR:
+        case AssetPathType.SIDECAR: {
           await this.assetRepository.save({ id, sidecarPath: pathValue });
           break;
+        }
 
-        case PersonPathType.FACE:
+        case PersonPathType.FACE: {
           await this.personRepository.update({ id, thumbnailPath: pathValue });
           break;
+        }
 
-        case UserPathType.PROFILE:
+        case UserPathType.PROFILE: {
           await this.userRepository.update(id, { profileImagePath: pathValue });
           break;
+        }
       }
     }
   }
 
+  private fullPath(filename: string) {
+    return resolve(filename);
+  }
+
   async getFileReport() {
-    const fullPath = (filename: string) => resolve(filename);
-    const hasFile = (items: Set<string>, filename: string) => items.has(filename) || items.has(fullPath(filename));
+    const hasFile = (items: Set<string>, filename: string) => items.has(filename) || items.has(this.fullPath(filename));
     const crawl = async (folder: StorageFolder) =>
       new Set(
         await this.storageRepository.crawl({
@@ -150,7 +160,7 @@ export class AuditService {
         return;
       }
       allFiles.delete(filename);
-      allFiles.delete(fullPath(filename));
+      allFiles.delete(this.fullPath(filename));
     };
 
     this.logger.log(
@@ -226,7 +236,7 @@ export class AuditService {
 
     // send as absolute paths
     for (const orphan of orphans) {
-      orphan.pathValue = fullPath(orphan.pathValue);
+      orphan.pathValue = this.fullPath(orphan.pathValue);
     }
 
     return { orphans, extras };
diff --git a/server/src/domain/auth/auth.service.spec.ts b/server/src/domain/auth/auth.service.spec.ts
index ef3ee64b98..c04bbc2630 100644
--- a/server/src/domain/auth/auth.service.spec.ts
+++ b/server/src/domain/auth/auth.service.spec.ts
@@ -18,7 +18,7 @@ import {
   userStub,
   userTokenStub,
 } from '@test';
-import { IncomingHttpHeaders } from 'http';
+import { IncomingHttpHeaders } from 'node:http';
 import { Issuer, generators } from 'openid-client';
 import { Socket } from 'socket.io';
 import {
diff --git a/server/src/domain/auth/auth.service.ts b/server/src/domain/auth/auth.service.ts
index fd527ee0d9..ff4ea43032 100644
--- a/server/src/domain/auth/auth.service.ts
+++ b/server/src/domain/auth/auth.service.ts
@@ -8,8 +8,8 @@ import {
   UnauthorizedException,
 } from '@nestjs/common';
 import cookieParser from 'cookie';
-import { IncomingHttpHeaders } from 'http';
 import { DateTime } from 'luxon';
+import { IncomingHttpHeaders } from 'node:http';
 import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client';
 import { AccessCore, Permission } from '../access';
 import {
@@ -85,7 +85,7 @@ export class AuthService {
     this.configCore = SystemConfigCore.create(configRepository);
     this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository);
 
-    custom.setHttpOptionsDefaults({ timeout: 30000 });
+    custom.setHttpOptionsDefaults({ timeout: 30_000 });
   }
 
   async login(dto: LoginCredentialDto, details: LoginDetails): Promise<LoginResponse> {
@@ -213,7 +213,8 @@ export class AuthService {
     }
 
     const { scope, buttonText, autoLaunch } = config.oauth;
-    const url = (await this.getOAuthClient(config)).authorizationUrl({
+    const oauthClient = await this.getOAuthClient(config);
+    const url = oauthClient.authorizationUrl({
       redirect_uri: this.normalize(config, dto.redirectUri),
       scope,
       state: generators.state(),
@@ -376,12 +377,10 @@ export class AuthService {
 
     const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
     const sharedLink = await this.sharedLinkRepository.getByKey(bytes);
-    if (sharedLink) {
-      if (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date()) {
-        const user = sharedLink.user;
-        if (user) {
-          return { user, sharedLink };
-        }
+    if (sharedLink && (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date())) {
+      const user = sharedLink.user;
+      if (user) {
+        return { user, sharedLink };
       }
     }
     throw new UnauthorizedException('Invalid share key');
@@ -423,7 +422,7 @@ export class AuthService {
   }
 
   private async createLoginResponse(user: UserEntity, authType: AuthType, loginDetails: LoginDetails) {
-    const key = this.cryptoRepository.randomBytes(32).toString('base64').replace(/\W/g, '');
+    const key = this.cryptoRepository.randomBytes(32).toString('base64').replaceAll(/\W/g, '');
     const token = this.cryptoRepository.hashSha256(key);
 
     await this.userTokenRepository.create({
diff --git a/server/src/domain/database/database.service.ts b/server/src/domain/database/database.service.ts
index 4c490ffc55..5af576a73b 100644
--- a/server/src/domain/database/database.service.ts
+++ b/server/src/domain/database/database.service.ts
@@ -50,14 +50,14 @@ export class DatabaseService {
   }
 
   private async createVectors() {
-    await this.databaseRepository.createExtension(DatabaseExtension.VECTORS).catch(async (err: QueryFailedError) => {
+    await this.databaseRepository.createExtension(DatabaseExtension.VECTORS).catch(async (error: QueryFailedError) => {
       const image = await this.getVectorsImage();
       this.logger.fatal(`
         Failed to create pgvecto.rs extension.
         If you have not updated your Postgres instance to a docker image that supports pgvecto.rs (such as '${image}'), please do so.
         See the v1.91.0 release notes for more info: https://github.com/immich-app/immich/releases/tag/v1.91.0'
       `);
-      throw err;
+      throw error;
     });
   }
 
diff --git a/server/src/domain/domain.constant.spec.ts b/server/src/domain/domain.constant.spec.ts
index 84cd4d8ee7..4ec4b1124c 100644
--- a/server/src/domain/domain.constant.spec.ts
+++ b/server/src/domain/domain.constant.spec.ts
@@ -108,9 +108,9 @@ describe('mimeTypes', () => {
       expect(keys).toEqual([...keys].sort());
     });
 
-    for (const [ext, v] of Object.entries(mimeTypes.profile)) {
-      it(`should lookup ${ext}`, () => {
-        expect(mimeTypes.lookup(`test.${ext}`)).toEqual(v[0]);
+    for (const [extension, v] of Object.entries(mimeTypes.profile)) {
+      it(`should lookup ${extension}`, () => {
+        expect(mimeTypes.lookup(`test.${extension}`)).toEqual(v[0]);
       });
     }
   });
@@ -135,9 +135,9 @@ describe('mimeTypes', () => {
       expect(values).toEqual(values.filter((mimeType) => mimeType.startsWith('image/')));
     });
 
-    for (const [ext, v] of Object.entries(mimeTypes.image)) {
-      it(`should lookup ${ext}`, () => {
-        expect(mimeTypes.lookup(`test.${ext}`)).toEqual(v[0]);
+    for (const [extension, v] of Object.entries(mimeTypes.image)) {
+      it(`should lookup ${extension}`, () => {
+        expect(mimeTypes.lookup(`test.${extension}`)).toEqual(v[0]);
       });
     }
   });
@@ -162,9 +162,9 @@ describe('mimeTypes', () => {
       expect(values).toEqual(values.filter((mimeType) => mimeType.startsWith('video/')));
     });
 
-    for (const [ext, v] of Object.entries(mimeTypes.video)) {
-      it(`should lookup ${ext}`, () => {
-        expect(mimeTypes.lookup(`test.${ext}`)).toEqual(v[0]);
+    for (const [extension, v] of Object.entries(mimeTypes.video)) {
+      it(`should lookup ${extension}`, () => {
+        expect(mimeTypes.lookup(`test.${extension}`)).toEqual(v[0]);
       });
     }
   });
@@ -188,9 +188,9 @@ describe('mimeTypes', () => {
       expect(Object.values(mimeTypes.sidecar).flat()).toEqual(['application/xml', 'text/xml']);
     });
 
-    for (const [ext, v] of Object.entries(mimeTypes.sidecar)) {
-      it(`should lookup ${ext}`, () => {
-        expect(mimeTypes.lookup(`it.${ext}`)).toEqual(v[0]);
+    for (const [extension, v] of Object.entries(mimeTypes.sidecar)) {
+      it(`should lookup ${extension}`, () => {
+        expect(mimeTypes.lookup(`it.${extension}`)).toEqual(v[0]);
       });
     }
   });
diff --git a/server/src/domain/domain.constant.ts b/server/src/domain/domain.constant.ts
index 5589eb15a6..227595e04f 100644
--- a/server/src/domain/domain.constant.ts
+++ b/server/src/domain/domain.constant.ts
@@ -3,8 +3,6 @@ import { Duration } from 'luxon';
 import { readFileSync } from 'node:fs';
 import { extname, join } from 'node:path';
 
-const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'));
-
 export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
 export const ONE_HOUR = Duration.fromObject({ hours: 1 });
 
@@ -31,7 +29,7 @@ export class Version implements IVersion {
   }
 
   static fromString(version: string): Version {
-    const regex = /(?:v)?(?<major>\d+)(?:\.(?<minor>\d+))?(?:[\.-](?<patch>\d+))?/i;
+    const regex = /v?(?<major>\d+)(?:\.(?<minor>\d+))?(?:[.-](?<patch>\d+))?/i;
     const matchResult = version.match(regex);
     if (matchResult) {
       const { major, minor = '0', patch = '0' } = matchResult.groups as { [K in keyof IVersion]: string };
@@ -68,7 +66,8 @@ export class Version implements IVersion {
 export const envName = (process.env.NODE_ENV || 'development').toUpperCase();
 export const isDev = process.env.NODE_ENV === 'development';
 
-export const serverVersion = Version.fromString(pkg.version);
+const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
+export const serverVersion = Version.fromString(version);
 
 export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
 
@@ -129,9 +128,9 @@ const image: Record<string, string[]> = {
   '.x3f': ['image/x3f', 'image/x-sigma-x3f'],
 };
 
-const profileExtensions = ['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp'];
+const profileExtensions = new Set(['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp']);
 const profile: Record<string, string[]> = Object.fromEntries(
-  Object.entries(image).filter(([key]) => profileExtensions.includes(key)),
+  Object.entries(image).filter(([key]) => profileExtensions.has(key)),
 );
 
 const video: Record<string, string[]> = {
@@ -180,5 +179,5 @@ export const mimeTypes = {
     }
     return AssetType.OTHER;
   },
-  getSupportedFileExtensions: () => Object.keys(image).concat(Object.keys(video)),
+  getSupportedFileExtensions: () => [...Object.keys(image), ...Object.keys(video)],
 };
diff --git a/server/src/domain/domain.util.ts b/server/src/domain/domain.util.ts
index 79a1913b8e..ba5942cea4 100644
--- a/server/src/domain/domain.util.ts
+++ b/server/src/domain/domain.util.ts
@@ -46,7 +46,8 @@ export type Options = {
 
 export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED';
 
-export function ValidateUUID({ optional, each }: Options = { optional: false, each: false }) {
+export function ValidateUUID(options?: Options) {
+  const { optional, each } = { optional: false, each: false, ...options };
   return applyDecorators(
     IsUUID('4', { each }),
     ApiProperty({ format: 'uuid' }),
@@ -58,7 +59,7 @@ export function ValidateUUID({ optional, each }: Options = { optional: false, ea
 export function validateCronExpression(expression: string) {
   try {
     new CronJob(expression, () => {});
-  } catch (error) {
+  } catch {
     return false;
   }
 
@@ -96,7 +97,7 @@ export const toBoolean = ({ value }: IValue) => {
 
 export const toEmail = ({ value }: IValue) => value?.toLowerCase();
 
-export const toSanitized = ({ value }: IValue) => sanitize((value || '').replace(/\./g, ''));
+export const toSanitized = ({ value }: IValue) => sanitize((value || '').replaceAll('.', ''));
 
 export function getFileNameWithoutExtension(path: string): string {
   return basename(path, extname(path));
@@ -173,7 +174,7 @@ export function Optional({ nullable, ...validationOptions }: OptionalOptions = {
     return IsOptional(validationOptions);
   }
 
-  return ValidateIf((obj: any, v: any) => v !== undefined, validationOptions);
+  return ValidateIf((object: any, v: any) => v !== undefined, validationOptions);
 }
 
 /**
@@ -186,8 +187,8 @@ export function chunks<T>(collection: Array<T> | Set<T>, size: number): T[][] {
   if (collection instanceof Set) {
     const result = [];
     let chunk = [];
-    for (const elem of collection) {
-      chunk.push(elem);
+    for (const element of collection) {
+      chunk.push(element);
       if (chunk.length === size) {
         result.push(chunk);
         chunk = [];
@@ -209,8 +210,8 @@ export function chunks<T>(collection: Array<T> | Set<T>, size: number): T[][] {
 export const setUnion = <T>(...sets: Set<T>[]): Set<T> => {
   const union = new Set(sets[0]);
   for (const set of sets.slice(1)) {
-    for (const elem of set) {
-      union.add(elem);
+    for (const element of set) {
+      union.add(element);
     }
   }
   return union;
@@ -219,16 +220,16 @@ export const setUnion = <T>(...sets: Set<T>[]): Set<T> => {
 export const setDifference = <T>(setA: Set<T>, ...sets: Set<T>[]): Set<T> => {
   const difference = new Set(setA);
   for (const set of sets) {
-    for (const elem of set) {
-      difference.delete(elem);
+    for (const element of set) {
+      difference.delete(element);
     }
   }
   return difference;
 };
 
 export const setIsSuperset = <T>(set: Set<T>, subset: Set<T>): boolean => {
-  for (const elem of subset) {
-    if (!set.has(elem)) {
+  for (const element of subset) {
+    if (!set.has(element)) {
       return false;
     }
   }
diff --git a/server/src/domain/download/download.service.ts b/server/src/domain/download/download.service.ts
index 0b28942705..03bd6fee60 100644
--- a/server/src/domain/download/download.service.ts
+++ b/server/src/domain/download/download.service.ts
@@ -1,6 +1,6 @@
 import { AssetEntity } from '@app/infra/entities';
 import { BadRequestException, Inject, Injectable } from '@nestjs/common';
-import { extname } from 'path';
+import { extname } from 'node:path';
 import { AccessCore, Permission } from '../access';
 import { AssetIdsDto } from '../asset';
 import { AuthDto } from '../auth';
@@ -68,10 +68,12 @@ export class DownloadService {
       }
     }
 
-    return {
-      totalSize: archives.reduce((total, item) => (total += item.size), 0),
-      archives,
-    };
+    let totalSize = 0;
+    for (const archive of archives) {
+      totalSize += archive.size;
+    }
+
+    return { totalSize, archives };
   }
 
   async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
@@ -82,12 +84,12 @@ export class DownloadService {
     const paths: Record<string, number> = {};
 
     for (const { originalPath, originalFileName } of assets) {
-      const ext = extname(originalPath);
-      let filename = `${originalFileName}${ext}`;
+      const extension = extname(originalPath);
+      let filename = `${originalFileName}${extension}`;
       const count = paths[filename] || 0;
       paths[filename] = count + 1;
       if (count !== 0) {
-        filename = `${originalFileName}+${count}${ext}`;
+        filename = `${originalFileName}+${count}${extension}`;
       }
 
       zip.addFile(originalPath, filename);
diff --git a/server/src/domain/job/job.service.spec.ts b/server/src/domain/job/job.service.spec.ts
index 4abeb309b6..58cda724f8 100644
--- a/server/src/domain/job/job.service.spec.ts
+++ b/server/src/domain/job/job.service.spec.ts
@@ -23,7 +23,7 @@ import { JobService } from './job.service';
 
 const makeMockHandlers = (success: boolean) => {
   const mock = jest.fn().mockResolvedValue(success);
-  return Object.values(JobName).reduce((map, jobName) => ({ ...map, [jobName]: mock }), {}) as Record<
+  return Object.fromEntries(Object.values(JobName).map((jobName) => [jobName, mock])) as unknown as Record<
     JobName,
     JobHandler
   >;
diff --git a/server/src/domain/job/job.service.ts b/server/src/domain/job/job.service.ts
index a804cf658b..25055f1f31 100644
--- a/server/src/domain/job/job.service.ts
+++ b/server/src/domain/job/job.service.ts
@@ -36,26 +36,31 @@ export class JobService {
     this.logger.debug(`Handling command: queue=${queueName},force=${dto.force}`);
 
     switch (dto.command) {
-      case JobCommand.START:
+      case JobCommand.START: {
         await this.start(queueName, dto);
         break;
+      }
 
-      case JobCommand.PAUSE:
+      case JobCommand.PAUSE: {
         await this.jobRepository.pause(queueName);
         break;
+      }
 
-      case JobCommand.RESUME:
+      case JobCommand.RESUME: {
         await this.jobRepository.resume(queueName);
         break;
+      }
 
-      case JobCommand.EMPTY:
+      case JobCommand.EMPTY: {
         await this.jobRepository.empty(queueName);
         break;
+      }
 
-      case JobCommand.CLEAR_FAILED:
+      case JobCommand.CLEAR_FAILED: {
         const failedJobs = await this.jobRepository.clear(queueName, QueueCleanType.FAILED);
         this.logger.debug(`Cleared failed jobs: ${failedJobs}`);
         break;
+      }
     }
 
     return this.getJobStatus(queueName);
@@ -85,42 +90,53 @@ export class JobService {
     }
 
     switch (name) {
-      case QueueName.VIDEO_CONVERSION:
+      case QueueName.VIDEO_CONVERSION: {
         return this.jobRepository.queue({ name: JobName.QUEUE_VIDEO_CONVERSION, data: { force } });
+      }
 
-      case QueueName.STORAGE_TEMPLATE_MIGRATION:
+      case QueueName.STORAGE_TEMPLATE_MIGRATION: {
         return this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION });
+      }
 
-      case QueueName.MIGRATION:
+      case QueueName.MIGRATION: {
         return this.jobRepository.queue({ name: JobName.QUEUE_MIGRATION });
+      }
 
-      case QueueName.SMART_SEARCH:
+      case QueueName.SMART_SEARCH: {
         await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH);
         return this.jobRepository.queue({ name: JobName.QUEUE_SMART_SEARCH, data: { force } });
+      }
 
-      case QueueName.METADATA_EXTRACTION:
+      case QueueName.METADATA_EXTRACTION: {
         return this.jobRepository.queue({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force } });
+      }
 
-      case QueueName.SIDECAR:
+      case QueueName.SIDECAR: {
         await this.configCore.requireFeature(FeatureFlag.SIDECAR);
         return this.jobRepository.queue({ name: JobName.QUEUE_SIDECAR, data: { force } });
+      }
 
-      case QueueName.THUMBNAIL_GENERATION:
+      case QueueName.THUMBNAIL_GENERATION: {
         return this.jobRepository.queue({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force } });
+      }
 
-      case QueueName.FACE_DETECTION:
+      case QueueName.FACE_DETECTION: {
         await this.configCore.requireFeature(FeatureFlag.FACIAL_RECOGNITION);
         return this.jobRepository.queue({ name: JobName.QUEUE_FACE_DETECTION, data: { force } });
+      }
 
-      case QueueName.FACIAL_RECOGNITION:
+      case QueueName.FACIAL_RECOGNITION: {
         await this.configCore.requireFeature(FeatureFlag.FACIAL_RECOGNITION);
         return this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force } });
+      }
 
-      case QueueName.LIBRARY:
+      case QueueName.LIBRARY: {
         return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force } });
+      }
 
-      default:
+      default: {
         throw new BadRequestException(`Invalid job name: ${name}`);
+      }
     }
   }
 
@@ -184,17 +200,19 @@ export class JobService {
   private async onDone(item: JobItem) {
     switch (item.name) {
       case JobName.SIDECAR_SYNC:
-      case JobName.SIDECAR_DISCOVERY:
+      case JobName.SIDECAR_DISCOVERY: {
         await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: item.data });
         break;
+      }
 
-      case JobName.SIDECAR_WRITE:
+      case JobName.SIDECAR_WRITE: {
         await this.jobRepository.queue({
           name: JobName.METADATA_EXTRACTION,
           data: { id: item.data.id, source: 'sidecar-write' },
         });
+      }
 
-      case JobName.METADATA_EXTRACTION:
+      case JobName.METADATA_EXTRACTION: {
         if (item.data.source === 'sidecar-write') {
           const [asset] = await this.assetRepository.getByIds([item.data.id]);
           if (asset) {
@@ -203,24 +221,28 @@ export class JobService {
         }
         await this.jobRepository.queue({ name: JobName.LINK_LIVE_PHOTOS, data: item.data });
         break;
+      }
 
-      case JobName.LINK_LIVE_PHOTOS:
+      case JobName.LINK_LIVE_PHOTOS: {
         await this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: item.data });
         break;
+      }
 
-      case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE:
+      case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: {
         if (item.data.source === 'upload') {
           await this.jobRepository.queue({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: item.data });
         }
         break;
+      }
 
-      case JobName.GENERATE_PERSON_THUMBNAIL:
+      case JobName.GENERATE_PERSON_THUMBNAIL: {
         const { id } = item.data;
         const person = await this.personRepository.getById(id);
         if (person) {
           this.communicationRepository.send(ClientEvent.PERSON_THUMBNAIL, person.ownerId, person.id);
         }
         break;
+      }
 
       case JobName.GENERATE_JPEG_THUMBNAIL: {
         const jobs: JobItem[] = [
diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts
index 4117f4129e..1af343318b 100644
--- a/server/src/domain/library/library.service.spec.ts
+++ b/server/src/domain/library/library.service.spec.ts
@@ -19,7 +19,7 @@ import {
 } from '@test';
 
 import { newFSWatcherMock } from '@test/mocks';
-import { Stats } from 'fs';
+import { Stats } from 'node:fs';
 import { ILibraryFileJob, ILibraryRefreshJob, JobName } from '../job';
 import {
   IAssetRepository,
@@ -116,12 +116,15 @@ describe(LibraryService.name, () => {
 
       libraryMock.get.mockImplementation(async (id) => {
         switch (id) {
-          case libraryStub.externalLibraryWithImportPaths1.id:
+          case libraryStub.externalLibraryWithImportPaths1.id: {
             return libraryStub.externalLibraryWithImportPaths1;
-          case libraryStub.externalLibraryWithImportPaths2.id:
+          }
+          case libraryStub.externalLibraryWithImportPaths2.id: {
             return libraryStub.externalLibraryWithImportPaths2;
-          default:
+          }
+          default: {
             return null;
+          }
         }
       });
 
@@ -532,7 +535,7 @@ describe(LibraryService.name, () => {
     });
 
     it('should set a missing asset to offline', async () => {
-      storageMock.stat.mockRejectedValue(new Error());
+      storageMock.stat.mockRejectedValue(new Error('Path not found'));
 
       const mockLibraryJob: ILibraryFileJob = {
         id: assetStub.image.id,
@@ -1430,12 +1433,15 @@ describe(LibraryService.name, () => {
 
       libraryMock.get.mockImplementation(async (id) => {
         switch (id) {
-          case libraryStub.externalLibraryWithImportPaths1.id:
+          case libraryStub.externalLibraryWithImportPaths1.id: {
             return libraryStub.externalLibraryWithImportPaths1;
-          case libraryStub.externalLibraryWithImportPaths2.id:
+          }
+          case libraryStub.externalLibraryWithImportPaths2.id: {
             return libraryStub.externalLibraryWithImportPaths2;
-          default:
+          }
+          default: {
             return null;
+          }
         }
       });
 
diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts
index c064cd1b13..dc36c9d23b 100644
--- a/server/src/domain/library/library.service.ts
+++ b/server/src/domain/library/library.service.ts
@@ -1,17 +1,17 @@
 import { AssetType, LibraryType } from '@app/infra/entities';
 import { ImmichLogger } from '@app/infra/logger';
 import { BadRequestException, Inject, Injectable } from '@nestjs/common';
-import { EventEmitter } from 'events';
 import { R_OK } from 'node:constants';
+import { EventEmitter } from 'node:events';
 import { Stats } from 'node:fs';
-import path from 'node:path';
-import { basename, parse } from 'path';
+import path, { basename, parse } from 'node:path';
 import picomatch from 'picomatch';
 import { AccessCore, Permission } from '../access';
 import { AuthDto } from '../auth';
 import { mimeTypes } from '../domain.constant';
 import { usePagination, validateCronExpression } from '../domain.util';
 import { IBaseJob, IEntityJob, ILibraryFileJob, ILibraryRefreshJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
+
 import {
   IAccessRepository,
   IAssetRepository,
@@ -84,11 +84,7 @@ export class LibraryService extends EventEmitter {
 
       if (library.watch.enabled !== this.watchLibraries) {
         this.watchLibraries = library.watch.enabled;
-        if (this.watchLibraries) {
-          await this.watchAll();
-        } else {
-          await this.unwatchAll();
-        }
+        await (this.watchLibraries ? this.watchAll() : this.unwatchAll());
       }
     });
   }
@@ -227,12 +223,13 @@ export class LibraryService extends EventEmitter {
 
   async create(auth: AuthDto, dto: CreateLibraryDto): Promise<LibraryResponseDto> {
     switch (dto.type) {
-      case LibraryType.EXTERNAL:
+      case LibraryType.EXTERNAL: {
         if (!dto.name) {
           dto.name = 'New External Library';
         }
         break;
-      case LibraryType.UPLOAD:
+      }
+      case LibraryType.UPLOAD: {
         if (!dto.name) {
           dto.name = 'New Upload Library';
         }
@@ -246,6 +243,7 @@ export class LibraryService extends EventEmitter {
           throw new BadRequestException('Upload libraries cannot be watched');
         }
         break;
+      }
     }
 
     const library = await this.repository.create({
@@ -401,7 +399,7 @@ export class LibraryService extends EventEmitter {
       sidecarPath = `${assetPath}.xmp`;
     }
 
-    const deviceAssetId = `${basename(assetPath)}`.replace(/\s+/g, '');
+    const deviceAssetId = `${basename(assetPath)}`.replaceAll(/\s+/g, '');
 
     let assetId;
     if (doImport) {
@@ -533,17 +531,17 @@ export class LibraryService extends EventEmitter {
     }
 
     this.logger.verbose(`Refreshing library: ${job.id}`);
-    const crawledAssetPaths = (
-      await this.storageRepository.crawl({
-        pathsToCrawl: library.importPaths,
-        exclusionPatterns: library.exclusionPatterns,
-      })
-    )
-      .map(path.normalize)
+    const rawPaths = await this.storageRepository.crawl({
+      pathsToCrawl: library.importPaths,
+      exclusionPatterns: library.exclusionPatterns,
+    });
+
+    const crawledAssetPaths = rawPaths
+      .map((filePath) => path.normalize(filePath))
       .filter((assetPath) =>
         // Filter out paths that are not within the user's external path
         assetPath.match(new RegExp(`^${user.externalPath}`)),
-      );
+      ) as string[];
 
     this.logger.debug(`Found ${crawledAssetPaths.length} asset(s) when crawling import paths ${library.importPaths}`);
     const assetsInLibrary = await this.assetRepository.getByLibraryId([job.id]);
diff --git a/server/src/domain/media/media.service.ts b/server/src/domain/media/media.service.ts
index 6108ebf8da..68f861d7e2 100644
--- a/server/src/domain/media/media.service.ts
+++ b/server/src/domain/media/media.service.ts
@@ -181,13 +181,14 @@ export class MediaService {
     this.storageCore.ensureFolders(path);
 
     switch (asset.type) {
-      case AssetType.IMAGE:
+      case AssetType.IMAGE: {
         const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : thumbnail.colorspace;
         const thumbnailOptions = { format, size, colorspace, quality: thumbnail.quality };
         await this.mediaRepository.resize(asset.originalPath, path, thumbnailOptions);
         break;
+      }
 
-      case AssetType.VIDEO:
+      case AssetType.VIDEO: {
         const { audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath);
         const mainVideoStream = this.getMainStream(videoStreams);
         if (!mainVideoStream) {
@@ -199,9 +200,11 @@ export class MediaService {
         const options = new ThumbnailConfig(config).getOptions(mainVideoStream, mainAudioStream);
         await this.mediaRepository.transcode(asset.originalPath, path, options);
         break;
+      }
 
-      default:
+      default: {
         throw new UnsupportedMediaTypeException(`Unsupported asset type for thumbnail generation: ${asset.type}`);
+      }
     }
     this.logger.log(
       `Successfully generated ${format.toUpperCase()} ${asset.type.toLowerCase()} thumbnail for asset ${asset.id}`,
@@ -297,16 +300,16 @@ export class MediaService {
     let transcodeOptions;
     try {
       transcodeOptions = await this.getCodecConfig(config).then((c) => c.getOptions(mainVideoStream, mainAudioStream));
-    } catch (err) {
-      this.logger.error(`An error occurred while configuring transcoding options: ${err}`);
+    } catch (error) {
+      this.logger.error(`An error occurred while configuring transcoding options: ${error}`);
       return false;
     }
 
     this.logger.log(`Start encoding video ${asset.id} ${JSON.stringify(transcodeOptions)}`);
     try {
       await this.mediaRepository.transcode(input, output, transcodeOptions);
-    } catch (err) {
-      this.logger.error(err);
+    } catch (error) {
+      this.logger.error(error);
       if (config.accel !== TranscodeHWAccel.DISABLED) {
         this.logger.error(
           `Error occurred during transcoding. Retrying with ${config.accel.toUpperCase()} acceleration disabled.`,
@@ -354,23 +357,29 @@ export class MediaService {
     const isLargerThanTargetBitrate = bitrate > this.parseBitrateToBps(ffmpegConfig.maxBitrate);
 
     switch (ffmpegConfig.transcode) {
-      case TranscodePolicy.DISABLED:
+      case TranscodePolicy.DISABLED: {
         return false;
+      }
 
-      case TranscodePolicy.ALL:
+      case TranscodePolicy.ALL: {
         return true;
+      }
 
-      case TranscodePolicy.REQUIRED:
+      case TranscodePolicy.REQUIRED: {
         return !allTargetsMatching || videoStream.isHDR;
+      }
 
-      case TranscodePolicy.OPTIMAL:
+      case TranscodePolicy.OPTIMAL: {
         return !allTargetsMatching || isLargerThanTargetRes || videoStream.isHDR;
+      }
 
-      case TranscodePolicy.BITRATE:
+      case TranscodePolicy.BITRATE: {
         return !allTargetsMatching || isLargerThanTargetBitrate || videoStream.isHDR;
+      }
 
-      default:
+      default: {
         return false;
+      }
     }
   }
 
@@ -383,14 +392,18 @@ export class MediaService {
 
   private getSWCodecConfig(config: SystemConfigFFmpegDto) {
     switch (config.targetVideoCodec) {
-      case VideoCodec.H264:
+      case VideoCodec.H264: {
         return new H264Config(config);
-      case VideoCodec.HEVC:
+      }
+      case VideoCodec.HEVC: {
         return new HEVCConfig(config);
-      case VideoCodec.VP9:
+      }
+      case VideoCodec.VP9: {
         return new VP9Config(config);
-      default:
+      }
+      default: {
         throw new UnsupportedMediaTypeException(`Codec '${config.targetVideoCodec}' is unsupported`);
+      }
     }
   }
 
@@ -398,23 +411,28 @@ export class MediaService {
     let handler: VideoCodecHWConfig;
     let devices: string[];
     switch (config.accel) {
-      case TranscodeHWAccel.NVENC:
+      case TranscodeHWAccel.NVENC: {
         handler = new NVENCConfig(config);
         break;
-      case TranscodeHWAccel.QSV:
+      }
+      case TranscodeHWAccel.QSV: {
         devices = await this.storageRepository.readdir('/dev/dri');
         handler = new QSVConfig(config, devices);
         break;
-      case TranscodeHWAccel.VAAPI:
+      }
+      case TranscodeHWAccel.VAAPI: {
         devices = await this.storageRepository.readdir('/dev/dri');
         handler = new VAAPIConfig(config, devices);
         break;
-      case TranscodeHWAccel.RKMPP:
+      }
+      case TranscodeHWAccel.RKMPP: {
         devices = await this.storageRepository.readdir('/dev/dri');
         handler = new RKMPPConfig(config, devices);
         break;
-      default:
+      }
+      default: {
         throw new UnsupportedMediaTypeException(`${config.accel.toUpperCase()} acceleration is unsupported`);
+      }
     }
     if (!handler.getSupportedCodecs().includes(config.targetVideoCodec)) {
       throw new UnsupportedMediaTypeException(
@@ -441,14 +459,14 @@ export class MediaService {
   parseBitrateToBps(bitrateString: string) {
     const bitrateValue = Number.parseInt(bitrateString);
 
-    if (isNaN(bitrateValue)) {
+    if (Number.isNaN(bitrateValue)) {
       return 0;
     }
 
     if (bitrateString.toLowerCase().endsWith('k')) {
       return bitrateValue * 1000; // Kilobits per second to bits per second
     } else if (bitrateString.toLowerCase().endsWith('m')) {
-      return bitrateValue * 1000000; // Megabits per second to bits per second
+      return bitrateValue * 1_000_000; // Megabits per second to bits per second
     } else {
       return bitrateValue;
     }
diff --git a/server/src/domain/media/media.util.ts b/server/src/domain/media/media.util.ts
index 6166a6d5cf..ab3e43ec9f 100644
--- a/server/src/domain/media/media.util.ts
+++ b/server/src/domain/media/media.util.ts
@@ -15,16 +15,14 @@ class BaseConfig implements VideoCodecSWConfig {
   getOptions(videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
     const options = {
       inputOptions: this.getBaseInputOptions(),
-      outputOptions: this.getBaseOutputOptions(videoStream, audioStream).concat('-v verbose'),
+      outputOptions: [...this.getBaseOutputOptions(videoStream, audioStream), '-v verbose'],
       twoPass: this.eligibleForTwoPass(),
     } as TranscodeOptions;
     const filters = this.getFilterOptions(videoStream);
     if (filters.length > 0) {
       options.outputOptions.push(`-vf ${filters.join(',')}`);
     }
-    options.outputOptions.push(...this.getPresetOptions());
-    options.outputOptions.push(...this.getThreadOptions());
-    options.outputOptions.push(...this.getBitrateOptions());
+    options.outputOptions.push(...this.getPresetOptions(), ...this.getThreadOptions(), ...this.getBitrateOptions());
 
     return options;
   }
@@ -129,11 +127,10 @@ class BaseConfig implements VideoCodecSWConfig {
 
   getTargetResolution(videoStream: VideoStreamInfo) {
     let target;
-    if (this.config.targetResolution === 'original') {
-      target = Math.min(videoStream.height, videoStream.width);
-    } else {
-      target = Number.parseInt(this.config.targetResolution);
-    }
+    target =
+      this.config.targetResolution === 'original'
+        ? Math.min(videoStream.height, videoStream.width)
+        : Number.parseInt(this.config.targetResolution);
 
     if (target % 2 !== 0) {
       target -= 1;
@@ -182,7 +179,7 @@ class BaseConfig implements VideoCodecSWConfig {
 
   getBitrateUnit() {
     const maxBitrate = this.getMaxBitrateValue();
-    return this.config.maxBitrate.trim().substring(maxBitrate.toString().length); // use inputted unit if provided
+    return this.config.maxBitrate.trim().slice(maxBitrate.toString().length); // use inputted unit if provided
   }
 
   getMaxBitrateValue() {
@@ -411,8 +408,7 @@ export class NVENCConfig extends BaseHWConfig {
       ...super.getBaseOutputOptions(videoStream, audioStream),
     ];
     if (this.getBFrames() > 0) {
-      options.push('-b_ref_mode middle');
-      options.push('-b_qfactor 1.1');
+      options.push('-b_ref_mode middle', '-b_qfactor 1.1');
     }
     if (this.config.temporalAQ) {
       options.push('-temporal-aq 1');
@@ -474,8 +470,8 @@ export class NVENCConfig extends BaseHWConfig {
 
 export class QSVConfig extends BaseHWConfig {
   getBaseInputOptions() {
-    if (!this.devices.length) {
-      throw Error('No QSV device found');
+    if (this.devices.length === 0) {
+      throw new Error('No QSV device found');
     }
 
     let qsvString = '';
@@ -519,8 +515,7 @@ export class QSVConfig extends BaseHWConfig {
     options.push(`-${this.useCQP() ? 'q:v' : 'global_quality'} ${this.config.crf}`);
     const bitrates = this.getBitrateDistribution();
     if (bitrates.max > 0) {
-      options.push(`-maxrate ${bitrates.max}${bitrates.unit}`);
-      options.push(`-bufsize ${bitrates.max * 2}${bitrates.unit}`);
+      options.push(`-maxrate ${bitrates.max}${bitrates.unit}`, `-bufsize ${bitrates.max * 2}${bitrates.unit}`);
     }
     return options;
   }
@@ -623,7 +618,7 @@ export class RKMPPConfig extends BaseHWConfig {
 
   getBaseInputOptions() {
     if (this.devices.length === 0) {
-      throw Error('No RKMPP device found');
+      throw new Error('No RKMPP device found');
     }
     return [];
   }
@@ -642,14 +637,17 @@ export class RKMPPConfig extends BaseHWConfig {
 
   getPresetOptions() {
     switch (this.config.targetVideoCodec) {
-      case VideoCodec.H264:
+      case VideoCodec.H264: {
         // from ffmpeg_mpp help, commonly referred to as H264 level 5.1
         return ['-level 51'];
-      case VideoCodec.HEVC:
+      }
+      case VideoCodec.HEVC: {
         // from ffmpeg_mpp help, commonly referred to as HEVC level 5.1
         return ['-level 153'];
-      default:
-        throw Error(`Incompatible video codec for RKMPP: ${this.config.targetVideoCodec}`);
+      }
+      default: {
+        throw new Error(`Incompatible video codec for RKMPP: ${this.config.targetVideoCodec}`);
+      }
     }
   }
 
diff --git a/server/src/domain/metadata/metadata.service.spec.ts b/server/src/domain/metadata/metadata.service.spec.ts
index 9a1e11893f..9a3a4bfed1 100644
--- a/server/src/domain/metadata/metadata.service.spec.ts
+++ b/server/src/domain/metadata/metadata.service.spec.ts
@@ -16,11 +16,11 @@ import {
   newSystemConfigRepositoryMock,
   probeStub,
 } from '@test';
-import { randomBytes } from 'crypto';
 import { BinaryField } from 'exiftool-vendored';
-import { Stats } from 'fs';
-import { constants } from 'fs/promises';
 import { when } from 'jest-when';
+import { randomBytes } from 'node:crypto';
+import { Stats } from 'node:fs';
+import { constants } from 'node:fs/promises';
 import { JobName } from '../job';
 import {
   ClientEvent,
@@ -234,7 +234,7 @@ describe(MetadataService.name, () => {
 
   describe('handleMetadataExtraction', () => {
     beforeEach(() => {
-      storageMock.stat.mockResolvedValue({ size: 123456 } as Stats);
+      storageMock.stat.mockResolvedValue({ size: 123_456 } as Stats);
     });
 
     it('should handle an asset that could not be found', async () => {
@@ -507,7 +507,7 @@ describe(MetadataService.name, () => {
         exifImageWidth: null,
         exposureTime: tags.ExposureTime,
         fNumber: null,
-        fileSizeInByte: 123456,
+        fileSizeInByte: 123_456,
         focalLength: tags.FocalLength,
         fps: null,
         iso: tags.ISO,
@@ -565,7 +565,7 @@ describe(MetadataService.name, () => {
 
     it('should handle duration with scale', async () => {
       assetMock.getByIds.mockResolvedValue([assetStub.image]);
-      metadataMock.readTags.mockResolvedValue({ Duration: { Scale: 1.11111111111111e-5, Value: 558720 } });
+      metadataMock.readTags.mockResolvedValue({ Duration: { Scale: 1.111_111_111_111_11e-5, Value: 558_720 } });
 
       await sut.handleMetadataExtraction({ id: assetStub.image.id });
 
diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts
index 00d8412c8d..12ad57006d 100644
--- a/server/src/domain/metadata/metadata.service.ts
+++ b/server/src/domain/metadata/metadata.service.ts
@@ -3,9 +3,9 @@ import { ImmichLogger } from '@app/infra/logger';
 import { Inject, Injectable } from '@nestjs/common';
 import { ExifDateTime, Tags } from 'exiftool-vendored';
 import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
-import { constants } from 'fs/promises';
 import _ from 'lodash';
 import { Duration } from 'luxon';
+import { constants } from 'node:fs/promises';
 import { Subscription } from 'rxjs';
 import { usePagination } from '../domain.util';
 import { IBaseJob, IEntityJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job';
@@ -85,7 +85,7 @@ const validate = <T>(value: T): NonNullable<T> | null => {
     return null;
   }
 
-  if (typeof value === 'number' && (isNaN(value) || !isFinite(value))) {
+  if (typeof value === 'number' && (Number.isNaN(value) || !Number.isFinite(value))) {
     return null;
   }
 
@@ -217,18 +217,22 @@ export class MetadataService {
 
       if (videoStreams[0]) {
         switch (videoStreams[0].rotation) {
-          case -90:
+          case -90: {
             exifData.orientation = Orientation.Rotate90CW;
             break;
-          case 0:
+          }
+          case 0: {
             exifData.orientation = Orientation.Horizontal;
             break;
-          case 90:
+          }
+          case 90: {
             exifData.orientation = Orientation.Rotate270CW;
             break;
-          case 180:
+          }
+          case 180: {
             exifData.orientation = Orientation.Rotate180;
             break;
+          }
         }
       }
     }
@@ -243,7 +247,7 @@ export class MetadataService {
     const timeZoneOffset = tzOffset(firstDateTime(tags as Tags)) ?? 0;
 
     if (dateTimeOriginal && timeZoneOffset) {
-      localDateTime = new Date(dateTimeOriginal.getTime() + timeZoneOffset * 60000);
+      localDateTime = new Date(dateTimeOriginal.getTime() + timeZoneOffset * 60_000);
     }
     await this.assetRepository.save({
       id: asset.id,
@@ -413,7 +417,13 @@ export class MetadataService {
       const checksum = this.cryptoRepository.hashSha1(video);
 
       let motionAsset = await this.assetRepository.getByChecksum(asset.ownerId, checksum);
-      if (!motionAsset) {
+      if (motionAsset) {
+        this.logger.debug(
+          `Asset ${asset.id}'s motion photo video with checksum ${checksum.toString(
+            'base64',
+          )} already exists in the repository`,
+        );
+      } else {
         // We create a UUID in advance so that each extracted video can have a unique filename
         // (allowing us to delete old ones if necessary)
         const motionAssetId = this.cryptoRepository.randomUUID();
@@ -448,12 +458,6 @@ export class MetadataService {
           await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } });
           this.logger.log(`Removed old motion photo video asset (${asset.livePhotoVideoId})`);
         }
-      } else {
-        this.logger.debug(
-          `Asset ${asset.id}'s motion photo video with checksum ${checksum.toString(
-            'base64',
-          )} already exists in the repository`,
-        );
       }
 
       this.logger.debug(`Finished motion photo video extraction (${asset.id})`);
@@ -494,7 +498,7 @@ export class MetadataService {
       fileSizeInByte: stats.size,
       fNumber: validate(tags.FNumber),
       focalLength: validate(tags.FocalLength),
-      fps: validate(parseFloat(tags.VideoFrameRate!)),
+      fps: validate(Number.parseFloat(tags.VideoFrameRate!)),
       iso: validate(tags.ISO),
       latitude: validate(tags.GPSLatitude),
       lensModel: tags.LensModel ?? null,
diff --git a/server/src/domain/partner/partner.service.ts b/server/src/domain/partner/partner.service.ts
index 7a9cf182b4..a3f9a9f3df 100644
--- a/server/src/domain/partner/partner.service.ts
+++ b/server/src/domain/partner/partner.service.ts
@@ -24,7 +24,7 @@ export class PartnerService {
     }
 
     const partner = await this.repository.create(partnerId);
-    return this.map(partner, PartnerDirection.SharedBy);
+    return this.mapToPartnerEntity(partner, PartnerDirection.SharedBy);
   }
 
   async remove(auth: AuthDto, sharedWithId: string): Promise<void> {
@@ -43,7 +43,7 @@ export class PartnerService {
     return partners
       .filter((partner) => partner.sharedBy && partner.sharedWith) // Filter out soft deleted users
       .filter((partner) => partner[key] === auth.user.id)
-      .map((partner) => this.map(partner, direction));
+      .map((partner) => this.mapToPartnerEntity(partner, direction));
   }
 
   async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> {
@@ -51,10 +51,10 @@ export class PartnerService {
     const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id };
 
     const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline });
-    return this.map(entity, PartnerDirection.SharedWith);
+    return this.mapToPartnerEntity(entity, PartnerDirection.SharedWith);
   }
 
-  private map(partner: PartnerEntity, direction: PartnerDirection): PartnerResponseDto {
+  private mapToPartnerEntity(partner: PartnerEntity, direction: PartnerDirection): PartnerResponseDto {
     // this is opposite to return the non-me user of the "partner"
     const user = mapUser(
       direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy,
diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts
index d5b7a27d7a..e1937524af 100644
--- a/server/src/domain/person/person.service.spec.ts
+++ b/server/src/domain/person/person.service.spec.ts
@@ -814,7 +814,7 @@ describe(PersonService.name, () => {
       }
 
       const faces = [
-        { face: faceStub.noPerson1, distance: 0.0 },
+        { face: faceStub.noPerson1, distance: 0 },
         { face: faceStub.primaryFace1, distance: 0.2 },
         { face: faceStub.noPerson2, distance: 0.3 },
         { face: faceStub.face1, distance: 0.4 },
@@ -843,7 +843,7 @@ describe(PersonService.name, () => {
 
     it('should create a new person if the face is a core point with no person', async () => {
       const faces = [
-        { face: faceStub.noPerson1, distance: 0.0 },
+        { face: faceStub.noPerson1, distance: 0 },
         { face: faceStub.noPerson2, distance: 0.3 },
       ] as FaceSearchResult[];
 
@@ -867,7 +867,7 @@ describe(PersonService.name, () => {
     });
 
     it('should defer non-core faces to end of queue', async () => {
-      const faces = [{ face: faceStub.noPerson1, distance: 0.0 }] as FaceSearchResult[];
+      const faces = [{ face: faceStub.noPerson1, distance: 0 }] as FaceSearchResult[];
 
       configMock.load.mockResolvedValue([
         { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 2 },
@@ -888,7 +888,7 @@ describe(PersonService.name, () => {
     });
 
     it('should not assign person to non-core face with no matching person', async () => {
-      const faces = [{ face: faceStub.noPerson1, distance: 0.0 }] as FaceSearchResult[];
+      const faces = [{ face: faceStub.noPerson1, distance: 0 }] as FaceSearchResult[];
 
       configMock.load.mockResolvedValue([
         { key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 2 },
diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts
index 39c7eb9439..576f94c491 100644
--- a/server/src/domain/person/person.service.ts
+++ b/server/src/domain/person/person.service.ts
@@ -122,7 +122,7 @@ export class PersonService {
     }
     if (changeFeaturePhoto.length > 0) {
       // Remove duplicates
-      await this.createNewFeaturePhoto(Array.from(new Set(changeFeaturePhoto)));
+      await this.createNewFeaturePhoto([...new Set(changeFeaturePhoto)]);
     }
     return result;
   }
@@ -332,7 +332,7 @@ export class PersonService {
     this.logger.debug(`${faces.length} faces detected in ${asset.resizePath}`);
     this.logger.verbose(faces.map((face) => ({ ...face, embedding: `vector(${face.embedding.length})` })));
 
-    if (faces.length) {
+    if (faces.length > 0) {
       await this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } });
 
       const mappedFaces = faces.map((face) => ({
@@ -417,7 +417,7 @@ export class PersonService {
       numResults: machineLearning.facialRecognition.minFaces,
     });
 
-    this.logger.debug(`Face ${id} has ${matches.length} match${matches.length != 1 ? 'es' : ''}`);
+    this.logger.debug(`Face ${id} has ${matches.length} match${matches.length == 1 ? '' : 'es'}`);
 
     const isCore = matches.length >= machineLearning.facialRecognition.minFaces;
     if (!isCore && !deferred) {
diff --git a/server/src/domain/repositories/database.repository.ts b/server/src/domain/repositories/database.repository.ts
index 496081ddb2..07d0afca6b 100644
--- a/server/src/domain/repositories/database.repository.ts
+++ b/server/src/domain/repositories/database.repository.ts
@@ -15,7 +15,7 @@ export enum DatabaseLock {
 export const IDatabaseRepository = 'IDatabaseRepository';
 
 export interface IDatabaseRepository {
-  getExtensionVersion(extName: string): Promise<Version | null>;
+  getExtensionVersion(extensionName: string): Promise<Version | null>;
   getPostgresVersion(): Promise<Version>;
   createExtension(extension: DatabaseExtension): Promise<void>;
   runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise<void>;
diff --git a/server/src/domain/repositories/media.repository.ts b/server/src/domain/repositories/media.repository.ts
index 72da627bf5..60135e62dc 100644
--- a/server/src/domain/repositories/media.repository.ts
+++ b/server/src/domain/repositories/media.repository.ts
@@ -1,5 +1,5 @@
 import { VideoCodec } from '@app/infra/entities';
-import { Writable } from 'stream';
+import { Writable } from 'node:stream';
 
 export const IMediaRepository = 'IMediaRepository';
 
diff --git a/server/src/domain/repositories/storage.repository.ts b/server/src/domain/repositories/storage.repository.ts
index 8a01c73d51..c55aaf7ecd 100644
--- a/server/src/domain/repositories/storage.repository.ts
+++ b/server/src/domain/repositories/storage.repository.ts
@@ -1,7 +1,7 @@
 import { FSWatcher, WatchOptions } from 'chokidar';
-import { Stats } from 'fs';
-import { FileReadOptions } from 'fs/promises';
-import { Readable } from 'stream';
+import { Stats } from 'node:fs';
+import { FileReadOptions } from 'node:fs/promises';
+import { Readable } from 'node:stream';
 import { CrawlOptionsDto } from '../library';
 
 export interface ImmichReadStream {
diff --git a/server/src/domain/search/search.service.ts b/server/src/domain/search/search.service.ts
index 8695c26e0d..932a865d04 100644
--- a/server/src/domain/search/search.service.ts
+++ b/server/src/domain/search/search.service.ts
@@ -46,7 +46,7 @@ export class SearchService {
       this.assetRepository.getAssetIdByTag(auth.user.id, options),
     ]);
     const assetIds = new Set<string>(results.flatMap((field) => field.items.map((item) => item.data)));
-    const assets = await this.assetRepository.getByIds(Array.from(assetIds));
+    const assets = await this.assetRepository.getByIds([...assetIds]);
     const assetMap = new Map<string, AssetResponseDto>(assets.map((asset) => [asset.id, mapAsset(asset)]));
 
     return results.map(({ fieldName, items }) => ({
@@ -75,7 +75,7 @@ export class SearchService {
     let assets: AssetEntity[] = [];
 
     switch (strategy) {
-      case SearchStrategy.SMART:
+      case SearchStrategy.SMART: {
         const embedding = await this.machineLearning.encodeText(
           machineLearning.url,
           { text: query },
@@ -88,10 +88,13 @@ export class SearchService {
           withArchived,
         });
         break;
-      case SearchStrategy.TEXT:
+      }
+      case SearchStrategy.TEXT: {
         assets = await this.assetRepository.searchMetadata(query, userIds, { numResults: 250 });
-      default:
+      }
+      default: {
         break;
+      }
     }
 
     return {
diff --git a/server/src/domain/server-info/server-info.service.spec.ts b/server/src/domain/server-info/server-info.service.spec.ts
index 1f1b51055b..e097509e6a 100644
--- a/server/src/domain/server-info/server-info.service.spec.ts
+++ b/server/src/domain/server-info/server-info.service.spec.ts
@@ -71,12 +71,12 @@ describe(ServerInfoService.name, () => {
 
       await expect(sut.getInfo()).resolves.toEqual({
         diskAvailable: '293.0 KiB',
-        diskAvailableRaw: 300000,
+        diskAvailableRaw: 300_000,
         diskSize: '488.3 KiB',
-        diskSizeRaw: 500000,
+        diskSizeRaw: 500_000,
         diskUsagePercentage: 60,
         diskUse: '293.0 KiB',
-        diskUseRaw: 300000,
+        diskUseRaw: 300_000,
       });
 
       expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
@@ -87,12 +87,12 @@ describe(ServerInfoService.name, () => {
 
       await expect(sut.getInfo()).resolves.toEqual({
         diskAvailable: '286.1 MiB',
-        diskAvailableRaw: 300000000,
+        diskAvailableRaw: 300_000_000,
         diskSize: '476.8 MiB',
-        diskSizeRaw: 500000000,
+        diskSizeRaw: 500_000_000,
         diskUsagePercentage: 60,
         diskUse: '286.1 MiB',
-        diskUseRaw: 300000000,
+        diskUseRaw: 300_000_000,
       });
 
       expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
@@ -107,12 +107,12 @@ describe(ServerInfoService.name, () => {
 
       await expect(sut.getInfo()).resolves.toEqual({
         diskAvailable: '279.4 GiB',
-        diskAvailableRaw: 300000000000,
+        diskAvailableRaw: 300_000_000_000,
         diskSize: '465.7 GiB',
-        diskSizeRaw: 500000000000,
+        diskSizeRaw: 500_000_000_000,
         diskUsagePercentage: 60,
         diskUse: '279.4 GiB',
-        diskUseRaw: 300000000000,
+        diskUseRaw: 300_000_000_000,
       });
 
       expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
@@ -127,12 +127,12 @@ describe(ServerInfoService.name, () => {
 
       await expect(sut.getInfo()).resolves.toEqual({
         diskAvailable: '272.8 TiB',
-        diskAvailableRaw: 300000000000000,
+        diskAvailableRaw: 300_000_000_000_000,
         diskSize: '454.7 TiB',
-        diskSizeRaw: 500000000000000,
+        diskSizeRaw: 500_000_000_000_000,
         diskUsagePercentage: 60,
         diskUse: '272.8 TiB',
-        diskUseRaw: 300000000000000,
+        diskUseRaw: 300_000_000_000_000,
       });
 
       expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
@@ -147,12 +147,12 @@ describe(ServerInfoService.name, () => {
 
       await expect(sut.getInfo()).resolves.toEqual({
         diskAvailable: '266.5 PiB',
-        diskAvailableRaw: 300000000000000000,
+        diskAvailableRaw: 300_000_000_000_000_000,
         diskSize: '444.1 PiB',
-        diskSizeRaw: 500000000000000000,
+        diskSizeRaw: 500_000_000_000_000_000,
         diskUsagePercentage: 60,
         diskUse: '266.5 PiB',
-        diskUseRaw: 300000000000000000,
+        diskUseRaw: 300_000_000_000_000_000,
       });
 
       expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
@@ -219,7 +219,7 @@ describe(ServerInfoService.name, () => {
           userName: '1 User',
           photos: 10,
           videos: 11,
-          usage: 12345,
+          usage: 12_345,
           quotaSizeInBytes: 0,
         },
         {
@@ -227,7 +227,7 @@ describe(ServerInfoService.name, () => {
           userName: '2 User',
           photos: 10,
           videos: 20,
-          usage: 123456,
+          usage: 123_456,
           quotaSizeInBytes: 0,
         },
         {
@@ -235,7 +235,7 @@ describe(ServerInfoService.name, () => {
           userName: '3 User',
           photos: 100,
           videos: 0,
-          usage: 987654,
+          usage: 987_654,
           quotaSizeInBytes: 0,
         },
       ]);
@@ -243,12 +243,12 @@ describe(ServerInfoService.name, () => {
       await expect(sut.getStatistics()).resolves.toEqual({
         photos: 120,
         videos: 31,
-        usage: 1123455,
+        usage: 1_123_455,
         usageByUser: [
           {
             photos: 10,
             quotaSizeInBytes: 0,
-            usage: 12345,
+            usage: 12_345,
             userName: '1 User',
             userId: 'user1',
             videos: 11,
@@ -256,7 +256,7 @@ describe(ServerInfoService.name, () => {
           {
             photos: 10,
             quotaSizeInBytes: 0,
-            usage: 123456,
+            usage: 123_456,
             userName: '2 User',
             userId: 'user2',
             videos: 20,
@@ -264,7 +264,7 @@ describe(ServerInfoService.name, () => {
           {
             photos: 100,
             quotaSizeInBytes: 0,
-            usage: 987654,
+            usage: 987_654,
             userName: '3 User',
             userId: 'user3',
             videos: 0,
diff --git a/server/src/domain/server-info/server-info.service.ts b/server/src/domain/server-info/server-info.service.ts
index 7da045a18a..51d26b2c3d 100644
--- a/server/src/domain/server-info/server-info.service.ts
+++ b/server/src/domain/server-info/server-info.service.ts
@@ -67,7 +67,7 @@ export class ServerInfoService {
     serverInfo.diskAvailableRaw = diskInfo.available;
     serverInfo.diskSizeRaw = diskInfo.total;
     serverInfo.diskUseRaw = diskInfo.total - diskInfo.free;
-    serverInfo.diskUsagePercentage = parseFloat(usagePercentage);
+    serverInfo.diskUsagePercentage = Number.parseFloat(usagePercentage);
     return serverInfo;
   }
 
diff --git a/server/src/domain/shared-link/shared-link.service.ts b/server/src/domain/shared-link/shared-link.service.ts
index b2b488138f..54e6f60521 100644
--- a/server/src/domain/shared-link/shared-link.service.ts
+++ b/server/src/domain/shared-link/shared-link.service.ts
@@ -21,7 +21,7 @@ export class SharedLinkService {
   }
 
   getAll(auth: AuthDto): Promise<SharedLinkResponseDto[]> {
-    return this.repository.getAll(auth.user.id).then((links) => links.map(mapSharedLink));
+    return this.repository.getAll(auth.user.id).then((links) => links.map((link) => mapSharedLink(link)));
   }
 
   async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise<SharedLinkResponseDto> {
@@ -30,7 +30,7 @@ export class SharedLinkService {
     }
 
     const sharedLink = await this.findOrFail(auth.user.id, auth.sharedLink.id);
-    const response = this.map(sharedLink, { withExif: sharedLink.showExif });
+    const response = this.mapToSharedLink(sharedLink, { withExif: sharedLink.showExif });
     if (sharedLink.password) {
       response.token = this.validateAndRefreshToken(sharedLink, dto);
     }
@@ -40,19 +40,20 @@ export class SharedLinkService {
 
   async get(auth: AuthDto, id: string): Promise<SharedLinkResponseDto> {
     const sharedLink = await this.findOrFail(auth.user.id, id);
-    return this.map(sharedLink, { withExif: true });
+    return this.mapToSharedLink(sharedLink, { withExif: true });
   }
 
   async create(auth: AuthDto, dto: SharedLinkCreateDto): Promise<SharedLinkResponseDto> {
     switch (dto.type) {
-      case SharedLinkType.ALBUM:
+      case SharedLinkType.ALBUM: {
         if (!dto.albumId) {
           throw new BadRequestException('Invalid albumId');
         }
         await this.access.requirePermission(auth, Permission.ALBUM_SHARE, dto.albumId);
         break;
+      }
 
-      case SharedLinkType.INDIVIDUAL:
+      case SharedLinkType.INDIVIDUAL: {
         if (!dto.assetIds || dto.assetIds.length === 0) {
           throw new BadRequestException('Invalid assetIds');
         }
@@ -60,6 +61,7 @@ export class SharedLinkService {
         await this.access.requirePermission(auth, Permission.ASSET_SHARE, dto.assetIds);
 
         break;
+      }
     }
 
     const sharedLink = await this.repository.create({
@@ -76,7 +78,7 @@ export class SharedLinkService {
       showExif: dto.showMetadata ?? true,
     });
 
-    return this.map(sharedLink, { withExif: true });
+    return this.mapToSharedLink(sharedLink, { withExif: true });
   }
 
   async update(auth: AuthDto, id: string, dto: SharedLinkEditDto) {
@@ -91,7 +93,7 @@ export class SharedLinkService {
       allowDownload: dto.allowDownload,
       showExif: dto.showMetadata,
     });
-    return this.map(sharedLink, { withExif: true });
+    return this.mapToSharedLink(sharedLink, { withExif: true });
   }
 
   async remove(auth: AuthDto, id: string): Promise<void> {
@@ -173,7 +175,7 @@ export class SharedLinkService {
 
     const sharedLink = await this.findOrFail(auth.sharedLink.userId, auth.sharedLink.id);
     const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
-    const assetCount = sharedLink.assets.length || sharedLink.album?.assets.length || 0;
+    const assetCount = sharedLink.assets.length ?? sharedLink.album?.assets.length ?? 0;
 
     return {
       title: sharedLink.album ? sharedLink.album.albumName : 'Public Share',
@@ -184,7 +186,7 @@ export class SharedLinkService {
     };
   }
 
-  private map(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) {
+  private mapToSharedLink(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) {
     return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithoutMetadata(sharedLink);
   }
 
diff --git a/server/src/domain/smart-info/smart-info.constant.ts b/server/src/domain/smart-info/smart-info.constant.ts
index 3867d0885a..66c31b9851 100644
--- a/server/src/domain/smart-info/smart-info.constant.ts
+++ b/server/src/domain/smart-info/smart-info.constant.ts
@@ -111,8 +111,12 @@ export const CLIP_MODEL_INFO: Record<string, ModelInfo> = {
 };
 
 export function cleanModelName(modelName: string): string {
-  const tokens = modelName.split('/');
-  return tokens[tokens.length - 1].replace(/:/g, '_');
+  const token = modelName.split('/').at(-1);
+  if (!token) {
+    throw new Error(`Invalid model name: ${modelName}`);
+  }
+
+  return token.replaceAll(':', '_');
 }
 
 export function getCLIPModelInfo(modelName: string): ModelInfo {
diff --git a/server/src/domain/storage-template/storage-template.service.spec.ts b/server/src/domain/storage-template/storage-template.service.spec.ts
index a49f0347aa..6e17ca64e9 100644
--- a/server/src/domain/storage-template/storage-template.service.spec.ts
+++ b/server/src/domain/storage-template/storage-template.service.spec.ts
@@ -269,7 +269,7 @@ describe(StorageTemplateService.name, () => {
       when(storageMock.stat)
         .calledWith(newPath)
         .mockResolvedValue({ size: 5000 } as Stats);
-      when(cryptoMock.hashFile).calledWith(newPath).mockResolvedValue(Buffer.from('different-hash', 'utf-8'));
+      when(cryptoMock.hashFile).calledWith(newPath).mockResolvedValue(Buffer.from('different-hash', 'utf8'));
 
       when(assetMock.save)
         .calledWith({ id: assetStub.image.id, originalPath: newPath })
@@ -311,9 +311,9 @@ describe(StorageTemplateService.name, () => {
     });
 
     it.each`
-      failedPathChecksum                      | failedPathSize                              | reason
-      ${assetStub.image.checksum}             | ${500}                                      | ${'file size'}
-      ${Buffer.from('bad checksum', 'utf-8')} | ${assetStub.image.exifInfo?.fileSizeInByte} | ${'checksum'}
+      failedPathChecksum                     | failedPathSize                              | reason
+      ${assetStub.image.checksum}            | ${500}                                      | ${'file size'}
+      ${Buffer.from('bad checksum', 'utf8')} | ${assetStub.image.exifInfo?.fileSizeInByte} | ${'checksum'}
     `(
       'should fail to migrate previously failed move from previous new path when old path no longer exists if $reason validation fails',
       async ({ failedPathChecksum, failedPathSize }) => {
diff --git a/server/src/domain/storage-template/storage-template.service.ts b/server/src/domain/storage-template/storage-template.service.ts
index cbed4a06c9..d696982540 100644
--- a/server/src/domain/storage-template/storage-template.service.ts
+++ b/server/src/domain/storage-template/storage-template.service.ts
@@ -86,7 +86,8 @@ export class StorageTemplateService {
   }
 
   async handleMigrationSingle({ id }: IEntityJob) {
-    const storageTemplateEnabled = (await this.configCore.getConfig()).storageTemplate.enabled;
+    const config = await this.configCore.getConfig();
+    const storageTemplateEnabled = config.storageTemplate.enabled;
     if (!storageTemplateEnabled) {
       return true;
     }
@@ -109,8 +110,9 @@ export class StorageTemplateService {
 
   async handleMigration() {
     this.logger.log('Starting storage template migration');
-    const storageTemplateEnabled = (await this.configCore.getConfig()).storageTemplate.enabled;
-    if (!storageTemplateEnabled) {
+    const { storageTemplate } = await this.configCore.getConfig();
+    const { enabled } = storageTemplate;
+    if (!enabled) {
       this.logger.log('Storage template migration disabled, skipping');
       return true;
     }
@@ -145,7 +147,7 @@ export class StorageTemplateService {
     }
 
     return this.databaseRepository.withLock(DatabaseLock.StorageTemplateMigration, async () => {
-      const { id, sidecarPath, originalPath, exifInfo } = asset;
+      const { id, sidecarPath, originalPath, exifInfo, checksum } = asset;
       const oldPath = originalPath;
       const newPath = await this.getTemplatePath(asset, metadata);
 
@@ -160,7 +162,7 @@ export class StorageTemplateService {
           pathType: AssetPathType.ORIGINAL,
           oldPath,
           newPath,
-          assetInfo: { sizeInBytes: exifInfo.fileSizeInByte, checksum: asset.checksum },
+          assetInfo: { sizeInBytes: exifInfo.fileSizeInByte, checksum },
         });
         if (sidecarPath) {
           await this.storageCore.moveFile({
@@ -171,7 +173,7 @@ export class StorageTemplateService {
           });
         }
       } catch (error: any) {
-        this.logger.error(`Problem applying storage template`, error?.stack, { id: asset.id, oldPath, newPath });
+        this.logger.error(`Problem applying storage template`, error?.stack, { id, oldPath, newPath });
       }
     });
   }
@@ -181,8 +183,8 @@ export class StorageTemplateService {
 
     try {
       const source = asset.originalPath;
-      const ext = path.extname(source).split('.').pop() as string;
-      const sanitized = sanitize(path.basename(filename, `.${ext}`));
+      const extension = path.extname(source).split('.').pop() as string;
+      const sanitized = sanitize(path.basename(filename, `.${extension}`));
       const rootPath = StorageCore.getLibraryFolder({ id: asset.ownerId, storageLabel });
 
       let albumName = null;
@@ -194,11 +196,11 @@ export class StorageTemplateService {
       const storagePath = this.render(this.template.compiled, {
         asset,
         filename: sanitized,
-        extension: ext,
+        extension: extension,
         albumName,
       });
       const fullPath = path.normalize(path.join(rootPath, storagePath));
-      let destination = `${fullPath}.${ext}`;
+      let destination = `${fullPath}.${extension}`;
 
       if (!fullPath.startsWith(rootPath)) {
         this.logger.warn(`Skipped attempt to access an invalid path: ${fullPath}. Path should start with ${rootPath}`);
@@ -223,8 +225,8 @@ export class StorageTemplateService {
        * The lines below will be used to check if the differences between the source and destination is only the
        * +7 suffix, and if so, it will be considered as already migrated.
        */
-      if (source.startsWith(fullPath) && source.endsWith(`.${ext}`)) {
-        const diff = source.replace(fullPath, '').replace(`.${ext}`, '');
+      if (source.startsWith(fullPath) && source.endsWith(`.${extension}`)) {
+        const diff = source.replace(fullPath, '').replace(`.${extension}`, '');
         const hasDuplicationAnnotation = /^\+\d+$/.test(diff);
         if (hasDuplicationAnnotation) {
           return source;
@@ -240,7 +242,7 @@ export class StorageTemplateService {
         }
 
         duplicateCount++;
-        destination = `${fullPath}+${duplicateCount}.${ext}`;
+        destination = `${fullPath}+${duplicateCount}.${extension}`;
       }
 
       return destination;
@@ -264,9 +266,9 @@ export class StorageTemplateService {
         extension: 'jpg',
         albumName: 'album',
       });
-    } catch (e) {
-      this.logger.warn(`Storage template validation failed: ${JSON.stringify(e)}`);
-      throw new Error(`Invalid storage template: ${e}`);
+    } catch (error) {
+      this.logger.warn(`Storage template validation failed: ${JSON.stringify(error)}`);
+      throw new Error(`Invalid storage template: ${error}`);
     }
   }
 
@@ -282,7 +284,7 @@ export class StorageTemplateService {
     return {
       raw: template,
       compiled: handlebar.compile(template, { knownHelpers: undefined, strict: true }),
-      needsAlbum: template.indexOf('{{album}}') !== -1,
+      needsAlbum: template.includes('{{album}}'),
     };
   }
 
@@ -295,7 +297,7 @@ export class StorageTemplateService {
       filetypefull: asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO',
       assetId: asset.id,
       //just throw into the root if it doesn't belong to an album
-      album: (albumName && sanitize(albumName.replace(/\.+/g, ''))) || '.',
+      album: (albumName && sanitize(albumName.replaceAll(/\.+/g, ''))) || '.',
     };
 
     const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
diff --git a/server/src/domain/storage/storage.core.ts b/server/src/domain/storage/storage.core.ts
index fef954baa0..9456fd66b1 100644
--- a/server/src/domain/storage/storage.core.ts
+++ b/server/src/domain/storage/storage.core.ts
@@ -118,40 +118,44 @@ export class StorageCore {
   async moveAssetFile(asset: AssetEntity, pathType: GeneratedAssetPath) {
     const { id: entityId, resizePath, webpPath, encodedVideoPath } = asset;
     switch (pathType) {
-      case AssetPathType.JPEG_THUMBNAIL:
+      case AssetPathType.JPEG_THUMBNAIL: {
         return this.moveFile({
           entityId,
           pathType,
           oldPath: resizePath,
           newPath: StorageCore.getLargeThumbnailPath(asset),
         });
-      case AssetPathType.WEBP_THUMBNAIL:
+      }
+      case AssetPathType.WEBP_THUMBNAIL: {
         return this.moveFile({
           entityId,
           pathType,
           oldPath: webpPath,
           newPath: StorageCore.getSmallThumbnailPath(asset),
         });
-      case AssetPathType.ENCODED_VIDEO:
+      }
+      case AssetPathType.ENCODED_VIDEO: {
         return this.moveFile({
           entityId,
           pathType,
           oldPath: encodedVideoPath,
           newPath: StorageCore.getEncodedVideoPath(asset),
         });
+      }
     }
   }
 
   async movePersonFile(person: PersonEntity, pathType: PersonPathType) {
     const { id: entityId, thumbnailPath } = person;
     switch (pathType) {
-      case PersonPathType.FACE:
+      case PersonPathType.FACE: {
         await this.moveFile({
           entityId,
           pathType,
           oldPath: thumbnailPath,
           newPath: StorageCore.getPersonThumbnailPath(person),
         });
+      }
     }
   }
 
@@ -168,7 +172,8 @@ export class StorageCore {
       this.logger.log(`Attempting to finish incomplete move: ${move.oldPath} => ${move.newPath}`);
       const oldPathExists = await this.repository.checkFileExists(move.oldPath);
       const newPathExists = await this.repository.checkFileExists(move.newPath);
-      const actualPath = oldPathExists ? move.oldPath : newPathExists ? move.newPath : null;
+      const newPathCheck = newPathExists ? move.newPath : null;
+      const actualPath = oldPathExists ? move.oldPath : newPathCheck;
       if (!actualPath) {
         this.logger.warn('Unable to complete move. File does not exist at either location.');
         return;
@@ -177,13 +182,14 @@ export class StorageCore {
       const fileAtNewLocation = actualPath === move.newPath;
       this.logger.log(`Found file at ${fileAtNewLocation ? 'new' : 'old'} location`);
 
-      if (fileAtNewLocation) {
-        if (!(await this.verifyNewPathContentsMatchesExpected(move.oldPath, move.newPath, assetInfo))) {
-          this.logger.fatal(
-            `Skipping move as file verification failed, old file is missing and new file is different to what was expected`,
-          );
-          return;
-        }
+      if (
+        fileAtNewLocation &&
+        !(await this.verifyNewPathContentsMatchesExpected(move.oldPath, move.newPath, assetInfo))
+      ) {
+        this.logger.fatal(
+          `Skipping move as file verification failed, old file is missing and new file is different to what was expected`,
+        );
+        return;
       }
 
       move = await this.moveRepository.update({ id: move.id, oldPath: actualPath, newPath });
@@ -200,10 +206,10 @@ export class StorageCore {
       try {
         this.logger.debug(`Attempting to rename file: ${move.oldPath} => ${newPath}`);
         await this.repository.rename(move.oldPath, newPath);
-      } catch (err: any) {
-        if (err.code !== 'EXDEV') {
+      } catch (error: any) {
+        if (error.code !== 'EXDEV') {
           this.logger.warn(
-            `Unable to complete move. Error renaming file with code ${err.code} and message: ${err.message}`,
+            `Unable to complete move. Error renaming file with code ${error.code} and message: ${error.message}`,
           );
           return;
         }
@@ -218,8 +224,8 @@ export class StorageCore {
 
         try {
           await this.repository.unlink(move.oldPath);
-        } catch (err: any) {
-          this.logger.warn(`Unable to delete old file, it will now no longer be tracked by Immich: ${err.message}`);
+        } catch (error: any) {
+          this.logger.warn(`Unable to delete old file, it will now no longer be tracked by Immich: ${error.message}`);
         }
       }
     }
@@ -233,14 +239,17 @@ export class StorageCore {
     newPath: string,
     assetInfo?: { sizeInBytes: number; checksum: Buffer },
   ) {
-    const oldPathSize = assetInfo ? assetInfo.sizeInBytes : (await this.repository.stat(oldPath)).size;
-    const newPathSize = (await this.repository.stat(newPath)).size;
+    const oldStat = await this.repository.stat(oldPath);
+    const newStat = await this.repository.stat(newPath);
+    const oldPathSize = assetInfo ? assetInfo.sizeInBytes : oldStat.size;
+    const newPathSize = newStat.size;
     this.logger.debug(`File size check: ${newPathSize} === ${oldPathSize}`);
     if (newPathSize !== oldPathSize) {
       this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`);
       return false;
     }
-    if (assetInfo && (await this.configCore.getConfig()).storageTemplate.hashVerificationEnabled) {
+    const config = await this.configCore.getConfig();
+    if (assetInfo && config.storageTemplate.hashVerificationEnabled) {
       const { checksum } = assetInfo;
       const newChecksum = await this.cryptoRepository.hashFile(newPath);
       if (!newChecksum.equals(checksum)) {
@@ -266,23 +275,29 @@ export class StorageCore {
 
   private savePath(pathType: PathType, id: string, newPath: string) {
     switch (pathType) {
-      case AssetPathType.ORIGINAL:
+      case AssetPathType.ORIGINAL: {
         return this.assetRepository.save({ id, originalPath: newPath });
-      case AssetPathType.JPEG_THUMBNAIL:
+      }
+      case AssetPathType.JPEG_THUMBNAIL: {
         return this.assetRepository.save({ id, resizePath: newPath });
-      case AssetPathType.WEBP_THUMBNAIL:
+      }
+      case AssetPathType.WEBP_THUMBNAIL: {
         return this.assetRepository.save({ id, webpPath: newPath });
-      case AssetPathType.ENCODED_VIDEO:
+      }
+      case AssetPathType.ENCODED_VIDEO: {
         return this.assetRepository.save({ id, encodedVideoPath: newPath });
-      case AssetPathType.SIDECAR:
+      }
+      case AssetPathType.SIDECAR: {
         return this.assetRepository.save({ id, sidecarPath: newPath });
-      case PersonPathType.FACE:
+      }
+      case PersonPathType.FACE: {
         return this.personRepository.update({ id, thumbnailPath: newPath });
+      }
     }
   }
 
   static getNestedFolder(folder: StorageFolder, ownerId: string, filename: string): string {
-    return join(StorageCore.getFolderLocation(folder, ownerId), filename.substring(0, 2), filename.substring(2, 4));
+    return join(StorageCore.getFolderLocation(folder, ownerId), filename.slice(0, 2), filename.slice(2, 4));
   }
 
   static getNestedPath(folder: StorageFolder, ownerId: string, filename: string): string {
diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts
index 8a33c7061a..0a20e5cc2a 100644
--- a/server/src/domain/system-config/system-config.core.ts
+++ b/server/src/domain/system-config/system-config.core.ts
@@ -132,7 +132,7 @@ export const defaults = Object.freeze<SystemConfig>({
     watch: {
       enabled: false,
       usePolling: false,
-      interval: 10000,
+      interval: 10_000,
     },
   },
   server: {
@@ -184,22 +184,30 @@ export class SystemConfigCore {
     const hasFeature = await this.hasFeature(feature);
     if (!hasFeature) {
       switch (feature) {
-        case FeatureFlag.SMART_SEARCH:
+        case FeatureFlag.SMART_SEARCH: {
           throw new BadRequestException('Smart search is not enabled');
-        case FeatureFlag.FACIAL_RECOGNITION:
+        }
+        case FeatureFlag.FACIAL_RECOGNITION: {
           throw new BadRequestException('Facial recognition is not enabled');
-        case FeatureFlag.SIDECAR:
+        }
+        case FeatureFlag.SIDECAR: {
           throw new BadRequestException('Sidecar is not enabled');
-        case FeatureFlag.SEARCH:
+        }
+        case FeatureFlag.SEARCH: {
           throw new BadRequestException('Search is not enabled');
-        case FeatureFlag.OAUTH:
+        }
+        case FeatureFlag.OAUTH: {
           throw new BadRequestException('OAuth is not enabled');
-        case FeatureFlag.PASSWORD_LOGIN:
+        }
+        case FeatureFlag.PASSWORD_LOGIN: {
           throw new BadRequestException('Password login is not enabled');
-        case FeatureFlag.CONFIG_FILE:
+        }
+        case FeatureFlag.CONFIG_FILE: {
           throw new BadRequestException('Config file is not set');
-        default:
+        }
+        default: {
           throw new ForbiddenException(`Missing required feature: ${feature}`);
+        }
       }
     }
   }
@@ -278,9 +286,9 @@ export class SystemConfigCore {
       for (const validator of this.validators) {
         await validator(newConfig, oldConfig);
       }
-    } catch (e) {
-      this.logger.warn(`Unable to save system config due to a validation error: ${e}`);
-      throw new BadRequestException(e instanceof Error ? e.message : e);
+    } catch (error) {
+      this.logger.warn(`Unable to save system config due to a validation error: ${error}`);
+      throw new BadRequestException(error instanceof Error ? error.message : error);
     }
 
     const updates: SystemConfigEntity[] = [];
@@ -330,19 +338,20 @@ export class SystemConfigCore {
   private async loadFromFile(filepath: string, force = false) {
     if (force || !this.configCache) {
       try {
-        const file = JSON.parse((await this.repository.readFile(filepath)).toString());
+        const file = await this.repository.readFile(filepath);
+        const json = JSON.parse(file.toString());
         const overrides: SystemConfigEntity<SystemConfigValue>[] = [];
 
         for (const key of Object.values(SystemConfigKey)) {
-          const value = _.get(file, key);
-          this.unsetDeep(file, key);
+          const value = _.get(json, key);
+          this.unsetDeep(json, key);
           if (value !== undefined) {
             overrides.push({ key, value });
           }
         }
 
-        if (!_.isEmpty(file)) {
-          this.logger.warn(`Unknown keys found: ${JSON.stringify(file, null, 2)}`);
+        if (!_.isEmpty(json)) {
+          this.logger.warn(`Unknown keys found: ${JSON.stringify(json, null, 2)}`);
         }
 
         this.configCache = overrides;
diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts
index 9a5862db05..191480b2b7 100644
--- a/server/src/domain/system-config/system-config.service.spec.ts
+++ b/server/src/domain/system-config/system-config.service.spec.ts
@@ -136,7 +136,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
     watch: {
       enabled: false,
       usePolling: false,
-      interval: 10000,
+      interval: 10_000,
     },
   },
 });
diff --git a/server/src/domain/system-config/system-config.service.ts b/server/src/domain/system-config/system-config.service.ts
index 261f8f9a16..5bf597e35d 100644
--- a/server/src/domain/system-config/system-config.service.ts
+++ b/server/src/domain/system-config/system-config.service.ts
@@ -121,7 +121,7 @@ export class SystemConfigService {
   private async setLogLevel({ logging }: SystemConfig) {
     const envLevel = this.getEnvLogLevel();
     const configLevel = logging.enabled ? logging.level : false;
-    const level = envLevel ? envLevel : configLevel;
+    const level = envLevel ?? configLevel;
     ImmichLogger.setLogLevel(level);
     this.logger.log(`LogLevel=${level} ${envLevel ? '(set via LOG_LEVEL)' : '(set via system config)'}`);
   }
diff --git a/server/src/domain/tag/tag.service.ts b/server/src/domain/tag/tag.service.ts
index f7f06c4177..38f1de1bcb 100644
--- a/server/src/domain/tag/tag.service.ts
+++ b/server/src/domain/tag/tag.service.ts
@@ -10,7 +10,7 @@ export class TagService {
   constructor(@Inject(ITagRepository) private repository: ITagRepository) {}
 
   getAll(auth: AuthDto) {
-    return this.repository.getAll(auth.user.id).then((tags) => tags.map(mapTag));
+    return this.repository.getAll(auth.user.id).then((tags) => tags.map((tag) => mapTag(tag)));
   }
 
   async getById(auth: AuthDto, id: string): Promise<TagResponseDto> {
@@ -78,10 +78,10 @@ export class TagService {
     const results: AssetIdsResponseDto[] = [];
     for (const assetId of dto.assetIds) {
       const hasAsset = await this.repository.hasAsset(auth.user.id, id, assetId);
-      if (!hasAsset) {
-        results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND });
-      } else {
+      if (hasAsset) {
         results.push({ assetId, success: true });
+      } else {
+        results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND });
       }
     }
 
diff --git a/server/src/domain/user/response-dto/user-response.dto.ts b/server/src/domain/user/response-dto/user-response.dto.ts
index e6dff1655c..15800b9933 100644
--- a/server/src/domain/user/response-dto/user-response.dto.ts
+++ b/server/src/domain/user/response-dto/user-response.dto.ts
@@ -5,10 +5,7 @@ import { IsEnum } from 'class-validator';
 export const getRandomAvatarColor = (user: UserEntity): UserAvatarColor => {
   const values = Object.values(UserAvatarColor);
   const randomIndex = Math.floor(
-    user.email
-      .split('')
-      .map((letter) => letter.charCodeAt(0))
-      .reduce((a, b) => a + b, 0) % values.length,
+    [...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
   );
   return values[randomIndex] as UserAvatarColor;
 };
diff --git a/server/src/domain/user/user.core.ts b/server/src/domain/user/user.core.ts
index b19ee5e84f..691bc7de49 100644
--- a/server/src/domain/user/user.core.ts
+++ b/server/src/domain/user/user.core.ts
@@ -1,6 +1,6 @@
 import { LibraryType, UserEntity } from '@app/infra/entities';
 import { BadRequestException, ForbiddenException } from '@nestjs/common';
-import path from 'path';
+import path from 'node:path';
 import sanitize from 'sanitize-filename';
 import { ICryptoRepository, ILibraryRepository, IUserRepository } from '../repositories';
 import { UserResponseDto } from './response-dto';
@@ -97,7 +97,7 @@ export class UserCore {
       payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS);
     }
     if (payload.storageLabel) {
-      payload.storageLabel = sanitize(payload.storageLabel.replace(/\./g, ''));
+      payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
     }
     const userEntity = await this.userRepository.create(payload);
     await this.libraryRepository.create({
diff --git a/server/src/domain/user/user.service.spec.ts b/server/src/domain/user/user.service.spec.ts
index 78743a0439..13ae149b4e 100644
--- a/server/src/domain/user/user.service.spec.ts
+++ b/server/src/domain/user/user.service.spec.ts
@@ -418,7 +418,7 @@ describe(UserService.name, () => {
 
     it('should default to a random password', async () => {
       userMock.getAdmin.mockResolvedValue(userStub.admin);
-      const ask = jest.fn().mockResolvedValue(undefined);
+      const ask = jest.fn().mockImplementation(() => {});
 
       const response = await sut.resetAdminPassword(ask);
 
diff --git a/server/src/domain/user/user.service.ts b/server/src/domain/user/user.service.ts
index bdb8f74ed7..7855b1ed6b 100644
--- a/server/src/domain/user/user.service.ts
+++ b/server/src/domain/user/user.service.ts
@@ -1,7 +1,7 @@
 import { UserEntity } from '@app/infra/entities';
 import { ImmichLogger } from '@app/infra/logger';
 import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common';
-import { randomBytes } from 'crypto';
+import { randomBytes } from 'node:crypto';
 import { AuthDto } from '../auth';
 import { CacheControl, ImmichFileResponse } from '../domain.util';
 import { IEntityJob, JobName } from '../job';
@@ -39,7 +39,7 @@ export class UserService {
 
   async getAll(auth: AuthDto, isAll: boolean): Promise<UserResponseDto[]> {
     const users = await this.userRepository.getList({ withDeleted: !isAll });
-    return users.map(mapUser);
+    return users.map((user) => mapUser(user));
   }
 
   async get(userId: string): Promise<UserResponseDto> {
@@ -125,7 +125,7 @@ export class UserService {
     }
 
     const providedPassword = await ask(mapUser(admin));
-    const password = providedPassword || randomBytes(24).toString('base64').replace(/\W/g, '');
+    const password = providedPassword || randomBytes(24).toString('base64').replaceAll(/\W/g, '');
 
     await this.userCore.updateUser(admin, admin.id, { password });
 
@@ -188,9 +188,10 @@ export class UserService {
       return false;
     }
 
-    const msInDay = 86400000;
+    // TODO use luxon for date calculation
+    const msInDay = 86_400_000;
     const msDeleteWait = msInDay * 7;
-    const msSinceDelete = new Date().getTime() - (Date.parse(user.deletedAt.toString()) || 0);
+    const msSinceDelete = Date.now() - (Date.parse(user.deletedAt.toString()) || 0);
 
     return msSinceDelete >= msDeleteWait;
   }
diff --git a/server/src/immich-admin/commands/reset-admin-password.command.ts b/server/src/immich-admin/commands/reset-admin-password.command.ts
index af36c590c9..d19ddf4338 100644
--- a/server/src/immich-admin/commands/reset-admin-password.command.ts
+++ b/server/src/immich-admin/commands/reset-admin-password.command.ts
@@ -13,20 +13,20 @@ export class ResetAdminPasswordCommand extends CommandRunner {
     super();
   }
 
-  async run(): Promise<void> {
-    const ask = (admin: UserResponseDto) => {
-      const { id, oauthId, email, name } = admin;
-      console.log(`Found Admin:
+  ask = (admin: UserResponseDto) => {
+    const { id, oauthId, email, name } = admin;
+    console.log(`Found Admin:
 - ID=${id}
 - OAuth ID=${oauthId}
 - Email=${email}
 - Name=${name}`);
 
-      return this.inquirer.ask<{ password: string }>('prompt-password', undefined).then(({ password }) => password);
-    };
+    return this.inquirer.ask<{ password: string }>('prompt-password', {}).then(({ password }) => password);
+  };
 
+  async run(): Promise<void> {
     try {
-      const { password, provided } = await this.userService.resetAdminPassword(ask);
+      const { password, provided } = await this.userService.resetAdminPassword(this.ask);
 
       if (provided) {
         console.log(`The admin password has been updated.`);
@@ -46,7 +46,7 @@ export class PromptPasswordQuestions {
     message: 'Please choose a new password (optional)',
     name: 'password',
   })
-  parsePassword(val: string) {
-    return val;
+  parsePassword(value: string) {
+    return value;
   }
 }
diff --git a/server/src/immich/api-v1/asset/asset.core.ts b/server/src/immich/api-v1/asset/asset.core.ts
index ec68c98a1e..0688a65dd6 100644
--- a/server/src/immich/api-v1/asset/asset.core.ts
+++ b/server/src/immich/api-v1/asset/asset.core.ts
@@ -38,7 +38,7 @@ export class AssetCore {
       isArchived: dto.isArchived ?? false,
       duration: dto.duration || null,
       isVisible: dto.isVisible ?? true,
-      livePhotoVideo: livePhotoAssetId != null ? ({ id: livePhotoAssetId } as AssetEntity) : null,
+      livePhotoVideo: livePhotoAssetId === null ? null : ({ id: livePhotoAssetId } as AssetEntity),
       resizePath: null,
       webpPath: null,
       thumbhash: null,
diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/immich/api-v1/asset/asset.service.spec.ts
index 8d3046c3a3..d5fde4a625 100644
--- a/server/src/immich/api-v1/asset/asset.service.spec.ts
+++ b/server/src/immich/api-v1/asset/asset.service.spec.ts
@@ -51,8 +51,8 @@ const _getAsset_1 = () => {
   asset_1.encodedVideoPath = '';
   asset_1.duration = '0:00:00.000000';
   asset_1.exifInfo = new ExifEntity();
-  asset_1.exifInfo.latitude = 49.533547;
-  asset_1.exifInfo.longitude = 10.703075;
+  asset_1.exifInfo.latitude = 49.533_547;
+  asset_1.exifInfo.longitude = 10.703_075;
   return asset_1;
 };
 
diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts
index efad71dc89..6d59647cbf 100644
--- a/server/src/immich/api-v1/asset/asset.service.ts
+++ b/server/src/immich/api-v1/asset/asset.service.ts
@@ -27,7 +27,6 @@ import { AssetSearchDto } from './dto/asset-search.dto';
 import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
 import { CreateAssetDto } from './dto/create-asset.dto';
 import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
-import { SearchPropertiesDto } from './dto/search-properties.dto';
 import { ServeFileDto } from './dto/serve-file.dto';
 import {
   AssetBulkUploadCheckResponseDto,
@@ -163,7 +162,8 @@ export class AssetService {
     const possibleSearchTerm = new Set<string>();
 
     const rows = await this.assetRepositoryV1.getSearchPropertiesByUserId(auth.user.id);
-    rows.forEach((row: SearchPropertiesDto) => {
+
+    for (const row of rows) {
       // tags
       row.tags?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase()));
 
@@ -187,9 +187,9 @@ export class AssetService {
       possibleSearchTerm.add(row.city?.toLowerCase() || '');
       possibleSearchTerm.add(row.state?.toLowerCase() || '');
       possibleSearchTerm.add(row.country?.toLowerCase() || '');
-    });
+    }
 
-    return Array.from(possibleSearchTerm).filter((x) => x != null && x != '');
+    return [...possibleSearchTerm].filter((x) => x != null && x != '');
   }
 
   async getCuratedLocation(auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
@@ -249,18 +249,18 @@ export class AssetService {
 
   private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) {
     switch (format) {
-      case GetAssetThumbnailFormatEnum.WEBP:
+      case GetAssetThumbnailFormatEnum.WEBP: {
         if (asset.webpPath) {
           return asset.webpPath;
         }
         this.logger.warn(`WebP thumbnail requested but not found for asset ${asset.id}, falling back to JPEG`);
-
-      case GetAssetThumbnailFormatEnum.JPEG:
-      default:
+      }
+      case GetAssetThumbnailFormatEnum.JPEG: {
         if (!asset.resizePath) {
           throw new NotFoundException(`No thumbnail found for asset ${asset.id}`);
         }
         return asset.resizePath;
+      }
     }
   }
 
diff --git a/server/src/immich/app.guard.ts b/server/src/immich/app.guard.ts
index 85f0689a8c..bd07d107b1 100644
--- a/server/src/immich/app.guard.ts
+++ b/server/src/immich/app.guard.ts
@@ -50,8 +50,8 @@ export const SharedLinkRoute = () =>
   applyDecorators(SetMetadata(Metadata.SHARED_ROUTE, true), ApiQuery({ name: 'key', type: String, required: false }));
 export const AdminRoute = (value = true) => SetMetadata(Metadata.ADMIN_ROUTE, value);
 
-export const Auth = createParamDecorator((data, ctx: ExecutionContext): AuthDto => {
-  return ctx.switchToHttp().getRequest<{ user: AuthDto }>().user;
+export const Auth = createParamDecorator((data, context: ExecutionContext): AuthDto => {
+  return context.switchToHttp().getRequest<{ user: AuthDto }>().user;
 });
 
 export const FileResponse = () =>
@@ -59,15 +59,15 @@ export const FileResponse = () =>
     content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } },
   });
 
-export const GetLoginDetails = createParamDecorator((data, ctx: ExecutionContext): LoginDetails => {
-  const req = ctx.switchToHttp().getRequest<Request>();
-  const userAgent = UAParser(req.headers['user-agent']);
+export const GetLoginDetails = createParamDecorator((data, context: ExecutionContext): LoginDetails => {
+  const request = context.switchToHttp().getRequest<Request>();
+  const userAgent = UAParser(request.headers['user-agent']);
 
   return {
-    clientIp: req.ip,
-    isSecure: req.secure,
-    deviceType: userAgent.browser.name || userAgent.device.type || (req.headers.devicemodel as string) || '',
-    deviceOS: userAgent.os.name || (req.headers.devicetype as string) || '',
+    clientIp: request.ip,
+    isSecure: request.secure,
+    deviceType: userAgent.browser.name || userAgent.device.type || (request.headers.devicemodel as string) || '',
+    deviceOS: userAgent.os.name || (request.headers.devicetype as string) || '',
   };
 });
 
@@ -95,20 +95,20 @@ export class AppGuard implements CanActivate {
       return true;
     }
 
-    const req = context.switchToHttp().getRequest<AuthRequest>();
+    const request = context.switchToHttp().getRequest<AuthRequest>();
 
-    const authDto = await this.authService.validate(req.headers, req.query as Record<string, string>);
+    const authDto = await this.authService.validate(request.headers, request.query as Record<string, string>);
     if (authDto.sharedLink && !isSharedRoute) {
-      this.logger.warn(`Denied access to non-shared route: ${req.path}`);
+      this.logger.warn(`Denied access to non-shared route: ${request.path}`);
       return false;
     }
 
     if (isAdminRoute && !authDto.user.isAdmin) {
-      this.logger.warn(`Denied access to admin only route: ${req.path}`);
+      this.logger.warn(`Denied access to admin only route: ${request.path}`);
       return false;
     }
 
-    req.user = authDto;
+    request.user = authDto;
 
     return true;
   }
diff --git a/server/src/immich/app.service.ts b/server/src/immich/app.service.ts
index 0b3a18577c..be82ae4dc8 100644
--- a/server/src/immich/app.service.ts
+++ b/server/src/immich/app.service.ts
@@ -15,7 +15,7 @@ import { ImmichLogger } from '@app/infra/logger';
 import { Injectable } from '@nestjs/common';
 import { Cron, CronExpression, Interval } from '@nestjs/schedule';
 import { NextFunction, Request, Response } from 'express';
-import { readFileSync } from 'fs';
+import { readFileSync } from 'node:fs';
 
 const render = (index: string, meta: OpenGraphTags) => {
   const tags = `
@@ -79,15 +79,15 @@ export class AppService {
     let index = '';
     try {
       index = readFileSync(WEB_ROOT_PATH).toString();
-    } catch (error: Error | any) {
+    } catch {
       this.logger.warn('Unable to open `www/index.html, skipping SSR.');
     }
 
-    return async (req: Request, res: Response, next: NextFunction) => {
+    return async (request: Request, res: Response, next: NextFunction) => {
       if (
-        req.url.startsWith('/api') ||
-        req.method.toLowerCase() !== 'get' ||
-        excludePaths.find((item) => req.url.startsWith(item))
+        request.url.startsWith('/api') ||
+        request.method.toLowerCase() !== 'get' ||
+        excludePaths.some((item) => request.url.startsWith(item))
       ) {
         return next();
       }
@@ -107,7 +107,7 @@ export class AppService {
 
       try {
         for (const { regex, onMatch } of targets) {
-          const matches = req.url.match(regex);
+          const matches = request.url.match(regex);
           if (matches) {
             const meta = await onMatch(matches);
             if (meta) {
diff --git a/server/src/immich/app.utils.ts b/server/src/immich/app.utils.ts
index 0dd984a02d..938f6f4708 100644
--- a/server/src/immich/app.utils.ts
+++ b/server/src/immich/app.utils.ts
@@ -18,11 +18,11 @@ import {
   SwaggerModule,
 } from '@nestjs/swagger';
 import { NextFunction, Response } from 'express';
-import { writeFileSync } from 'fs';
-import { access, constants } from 'fs/promises';
 import _ from 'lodash';
-import path, { isAbsolute } from 'path';
-import { promisify } from 'util';
+import { writeFileSync } from 'node:fs';
+import { access, constants } from 'node:fs/promises';
+import path, { isAbsolute } from 'node:path';
+import { promisify } from 'node:util';
 
 import { applyDecorators, UsePipes, ValidationPipe } from '@nestjs/common';
 import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
@@ -55,13 +55,15 @@ export const sendFile = async (
   try {
     const file = await handler();
     switch (file.cacheControl) {
-      case CacheControl.PRIVATE_WITH_CACHE:
+      case CacheControl.PRIVATE_WITH_CACHE: {
         res.set('Cache-Control', 'private, max-age=86400, no-transform');
         break;
+      }
 
-      case CacheControl.PRIVATE_WITHOUT_CACHE:
+      case CacheControl.PRIVATE_WITHOUT_CACHE: {
         res.set('Cache-Control', 'private, no-cache, no-transform');
         break;
+      }
     }
 
     res.header('Content-Type', file.contentType);
@@ -94,21 +96,21 @@ export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) =>
   return new StreamableFile(stream, { type, length });
 };
 
-function sortKeys<T>(obj: T): T {
-  if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
-    return obj;
+function sortKeys<T>(target: T): T {
+  if (!target || typeof target !== 'object' || Array.isArray(target)) {
+    return target;
   }
 
   const result: Partial<T> = {};
-  const keys = Object.keys(obj).sort() as Array<keyof T>;
+  const keys = Object.keys(target).sort() as Array<keyof T>;
   for (const key of keys) {
-    result[key] = sortKeys(obj[key]);
+    result[key] = sortKeys(target[key]);
   }
   return result as T;
 }
 
 export const routeToErrorMessage = (methodName: string) =>
-  'Failed to ' + methodName.replace(/[A-Z]+/g, (letter) => ` ${letter.toLowerCase()}`);
+  'Failed to ' + methodName.replaceAll(/[A-Z]+/g, (letter) => ` ${letter.toLowerCase()}`);
 
 const patchOpenAPI = (document: OpenAPIObject) => {
   document.paths = sortKeys(document.paths);
@@ -152,7 +154,7 @@ const patchOpenAPI = (document: OpenAPIObject) => {
         continue;
       }
 
-      if ((operation.security || []).find((item) => !!item[Metadata.PUBLIC_SECURITY])) {
+      if ((operation.security || []).some((item) => !!item[Metadata.PUBLIC_SECURITY])) {
         delete operation.security;
       }
 
@@ -177,7 +179,7 @@ const patchOpenAPI = (document: OpenAPIObject) => {
   return document;
 };
 
-export const useSwagger = (app: INestApplication, isDev: boolean) => {
+export const useSwagger = (app: INestApplication, isDevelopment: boolean) => {
   const config = new DocumentBuilder()
     .setTitle('Immich')
     .setDescription('Immich API')
@@ -203,7 +205,7 @@ export const useSwagger = (app: INestApplication, isDev: boolean) => {
     operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
   };
 
-  const doc = SwaggerModule.createDocument(app, config, options);
+  const specification = SwaggerModule.createDocument(app, config, options);
 
   const customOptions: SwaggerCustomOptions = {
     swaggerOptions: {
@@ -212,11 +214,11 @@ export const useSwagger = (app: INestApplication, isDev: boolean) => {
     customSiteTitle: 'Immich API Documentation',
   };
 
-  SwaggerModule.setup('doc', app, doc, customOptions);
+  SwaggerModule.setup('doc', app, specification, customOptions);
 
-  if (isDev) {
+  if (isDevelopment) {
     // Generate API Documentation only in development mode
     const outputPath = path.resolve(process.cwd(), '../open-api/immich-openapi-specs.json');
-    writeFileSync(outputPath, JSON.stringify(patchOpenAPI(doc), null, 2), { encoding: 'utf8' });
+    writeFileSync(outputPath, JSON.stringify(patchOpenAPI(specification), null, 2), { encoding: 'utf8' });
   }
 };
diff --git a/server/src/immich/controllers/auth.controller.ts b/server/src/immich/controllers/auth.controller.ts
index 38cf8f23dc..15018c10de 100644
--- a/server/src/immich/controllers/auth.controller.ts
+++ b/server/src/immich/controllers/auth.controller.ts
@@ -78,13 +78,13 @@ export class AuthController {
   @Post('logout')
   @HttpCode(HttpStatus.OK)
   logout(
-    @Req() req: Request,
+    @Req() request: Request,
     @Res({ passthrough: true }) res: Response,
     @Auth() auth: AuthDto,
   ): Promise<LogoutResponseDto> {
     res.clearCookie(IMMICH_ACCESS_COOKIE);
     res.clearCookie(IMMICH_AUTH_TYPE_COOKIE);
 
-    return this.service.logout(auth, (req.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]);
+    return this.service.logout(auth, (request.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]);
   }
 }
diff --git a/server/src/immich/controllers/oauth.controller.ts b/server/src/immich/controllers/oauth.controller.ts
index b7fd0fe021..678e4a4f3c 100644
--- a/server/src/immich/controllers/oauth.controller.ts
+++ b/server/src/immich/controllers/oauth.controller.ts
@@ -25,9 +25,9 @@ export class OAuthController {
   @PublicRoute()
   @Get('mobile-redirect')
   @Redirect()
-  redirectOAuthToMobile(@Req() req: Request) {
+  redirectOAuthToMobile(@Req() request: Request) {
     return {
-      url: this.service.getMobileRedirect(req.url),
+      url: this.service.getMobileRedirect(request.url),
       statusCode: HttpStatus.TEMPORARY_REDIRECT,
     };
   }
diff --git a/server/src/immich/controllers/shared-link.controller.ts b/server/src/immich/controllers/shared-link.controller.ts
index 25d4bdca46..86045433d5 100644
--- a/server/src/immich/controllers/shared-link.controller.ts
+++ b/server/src/immich/controllers/shared-link.controller.ts
@@ -33,10 +33,10 @@ export class SharedLinkController {
   async getMySharedLink(
     @Auth() auth: AuthDto,
     @Query() dto: SharedLinkPasswordDto,
-    @Req() req: Request,
+    @Req() request: Request,
     @Res({ passthrough: true }) res: Response,
   ): Promise<SharedLinkResponseDto> {
-    const sharedLinkToken = req.cookies?.[IMMICH_SHARED_LINK_ACCESS_COOKIE];
+    const sharedLinkToken = request.cookies?.[IMMICH_SHARED_LINK_ACCESS_COOKIE];
     if (sharedLinkToken) {
       dto.token = sharedLinkToken;
     }
diff --git a/server/src/immich/interceptors/file-upload.interceptor.ts b/server/src/immich/interceptors/file-upload.interceptor.ts
index d94761d44a..52cc447e8e 100644
--- a/server/src/immich/interceptors/file-upload.interceptor.ts
+++ b/server/src/immich/interceptors/file-upload.interceptor.ts
@@ -4,9 +4,9 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nes
 import { PATH_METADATA } from '@nestjs/common/constants';
 import { Reflector } from '@nestjs/core';
 import { transformException } from '@nestjs/platform-express/multer/multer/multer.utils';
-import { createHash, randomUUID } from 'crypto';
 import { NextFunction, RequestHandler } from 'express';
 import multer, { StorageEngine, diskStorage } from 'multer';
+import { createHash, randomUUID } from 'node:crypto';
 import { Observable } from 'rxjs';
 import { AuthRequest } from '../app.guard';
 
@@ -40,17 +40,17 @@ interface Callback<T> {
   (error: null, result: T): void;
 }
 
-const callbackify = async <T>(fn: (...args: any[]) => T, callback: Callback<T>) => {
+const callbackify = async <T>(target: (...arguments_: any[]) => T, callback: Callback<T>) => {
   try {
-    return callback(null, await fn());
+    return callback(null, await target());
   } catch (error: Error | any) {
     return callback(error);
   }
 };
 
-const asRequest = (req: AuthRequest, file: Express.Multer.File) => {
+const asRequest = (request: AuthRequest, file: Express.Multer.File) => {
   return {
-    auth: req.user || null,
+    auth: request.user || null,
     fieldName: file.fieldname as UploadFieldName,
     file: mapToUploadFile(file as ImmichFile),
   };
@@ -94,14 +94,14 @@ export class FileUploadInterceptor implements NestInterceptor {
   }
 
   async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<any>> {
-    const ctx = context.switchToHttp();
+    const context_ = context.switchToHttp();
     const route = this.reflect.get<string>(PATH_METADATA, context.getClass());
 
     const handler: RequestHandler | null = this.getHandler(route as Route);
     if (handler) {
       await new Promise<void>((resolve, reject) => {
         const next: NextFunction = (error) => (error ? reject(transformException(error)) : resolve());
-        handler(ctx.getRequest(), ctx.getResponse(), next);
+        handler(context_.getRequest(), context_.getResponse(), next);
       });
     } else {
       this.logger.warn(`Skipping invalid file upload route: ${route}`);
@@ -110,28 +110,31 @@ export class FileUploadInterceptor implements NestInterceptor {
     return next.handle();
   }
 
-  private fileFilter(req: AuthRequest, file: Express.Multer.File, callback: multer.FileFilterCallback) {
-    return callbackify(() => this.assetService.canUploadFile(asRequest(req, file)), callback);
+  private fileFilter(request: AuthRequest, file: Express.Multer.File, callback: multer.FileFilterCallback) {
+    return callbackify(() => this.assetService.canUploadFile(asRequest(request, file)), callback);
   }
 
-  private filename(req: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
-    return callbackify(() => this.assetService.getUploadFilename(asRequest(req, file)), callback as Callback<string>);
+  private filename(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
+    return callbackify(
+      () => this.assetService.getUploadFilename(asRequest(request, file)),
+      callback as Callback<string>,
+    );
   }
 
-  private destination(req: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
-    return callbackify(() => this.assetService.getUploadFolder(asRequest(req, file)), callback as Callback<string>);
+  private destination(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
+    return callbackify(() => this.assetService.getUploadFolder(asRequest(request, file)), callback as Callback<string>);
   }
 
-  private handleFile(req: AuthRequest, file: Express.Multer.File, callback: Callback<Partial<ImmichFile>>) {
+  private handleFile(request: AuthRequest, file: Express.Multer.File, callback: Callback<Partial<ImmichFile>>) {
     (file as ImmichMulterFile).uuid = randomUUID();
     if (!this.isAssetUploadFile(file)) {
-      this.defaultStorage._handleFile(req, file, callback);
+      this.defaultStorage._handleFile(request, file, callback);
       return;
     }
 
     const hash = createHash('sha1');
     file.stream.on('data', (chunk) => hash.update(chunk));
-    this.defaultStorage._handleFile(req, file, (error, info) => {
+    this.defaultStorage._handleFile(request, file, (error, info) => {
       if (error) {
         hash.destroy();
         callback(error);
@@ -141,15 +144,16 @@ export class FileUploadInterceptor implements NestInterceptor {
     });
   }
 
-  private removeFile(req: AuthRequest, file: Express.Multer.File, callback: (error: Error | null) => void) {
-    this.defaultStorage._removeFile(req, file, callback);
+  private removeFile(request: AuthRequest, file: Express.Multer.File, callback: (error: Error | null) => void) {
+    this.defaultStorage._removeFile(request, file, callback);
   }
 
   private isAssetUploadFile(file: Express.Multer.File) {
     switch (file.fieldname as UploadFieldName) {
       case UploadFieldName.ASSET_DATA:
-      case UploadFieldName.LIVE_PHOTO_DATA:
+      case UploadFieldName.LIVE_PHOTO_DATA: {
         return true;
+      }
     }
 
     return false;
@@ -157,14 +161,17 @@ export class FileUploadInterceptor implements NestInterceptor {
 
   private getHandler(route: Route) {
     switch (route) {
-      case Route.ASSET:
+      case Route.ASSET: {
         return this.handlers.assetUpload;
+      }
 
-      case Route.USER:
+      case Route.USER: {
         return this.handlers.userProfile;
+      }
 
-      default:
+      default: {
         return null;
+      }
     }
   }
 }
diff --git a/server/src/infra/database.config.ts b/server/src/infra/database.config.ts
index 5cad312570..9e6cccd198 100644
--- a/server/src/infra/database.config.ts
+++ b/server/src/infra/database.config.ts
@@ -6,12 +6,13 @@ const urlOrParts = url
   ? { url }
   : {
       host: process.env.DB_HOSTNAME || 'localhost',
-      port: parseInt(process.env.DB_PORT || '5432'),
+      port: Number.parseInt(process.env.DB_PORT || '5432'),
       username: process.env.DB_USERNAME || 'postgres',
       password: process.env.DB_PASSWORD || 'postgres',
       database: process.env.DB_DATABASE_NAME || 'immich',
     };
 
+/* eslint unicorn/prefer-module: "off" -- We can fix this when migrating to ESM*/
 export const databaseConfig: PostgresConnectionOptions = {
   type: 'postgres',
   entities: [__dirname + '/entities/*.entity.{js,ts}'],
@@ -19,7 +20,7 @@ export const databaseConfig: PostgresConnectionOptions = {
   migrations: [__dirname + '/migrations/*.{js,ts}'],
   subscribers: [__dirname + '/subscribers/*.{js,ts}'],
   migrationsRun: false,
-  connectTimeoutMS: 10000, // 10 seconds
+  connectTimeoutMS: 10_000, // 10 seconds
   parseInt8: true,
   ...urlOrParts,
 };
diff --git a/server/src/infra/infra.config.ts b/server/src/infra/infra.config.ts
index 90ca9fc818..f72f333344 100644
--- a/server/src/infra/infra.config.ts
+++ b/server/src/infra/infra.config.ts
@@ -15,8 +15,8 @@ function parseRedisConfig(): RedisOptions {
   }
   return {
     host: process.env.REDIS_HOSTNAME || 'immich_redis',
-    port: parseInt(process.env.REDIS_PORT || '6379'),
-    db: parseInt(process.env.REDIS_DBINDEX || '0'),
+    port: Number.parseInt(process.env.REDIS_PORT || '6379'),
+    db: Number.parseInt(process.env.REDIS_DBINDEX || '0'),
     username: process.env.REDIS_USERNAME || undefined,
     password: process.env.REDIS_PASSWORD || undefined,
     path: process.env.REDIS_SOCKET || undefined,
diff --git a/server/src/infra/infra.util.ts b/server/src/infra/infra.util.ts
index 4dc821cd57..585d058e03 100644
--- a/server/src/infra/infra.util.ts
+++ b/server/src/infra/infra.util.ts
@@ -27,4 +27,4 @@ export const DummyValue = {
 // maximum number of parameters is 65535. Any query that tries to bind more than that (e.g. searching
 // by a list of IDs) requires splitting the query into multiple chunks.
 // We are rounding down this limit, as queries commonly include other filters and parameters.
-export const DATABASE_PARAMETER_CHUNK_SIZE = 65500;
+export const DATABASE_PARAMETER_CHUNK_SIZE = 65_500;
diff --git a/server/src/infra/infra.utils.ts b/server/src/infra/infra.utils.ts
index 91608472f7..1036df2afa 100644
--- a/server/src/infra/infra.utils.ts
+++ b/server/src/infra/infra.utils.ts
@@ -59,21 +59,25 @@ export const isValidInteger = (value: number, options: { min?: number; max?: num
 export function Chunked(options: { paramIndex?: number; mergeFn?: (results: any) => any } = {}): MethodDecorator {
   return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
     const originalMethod = descriptor.value;
-    const paramIndex = options.paramIndex ?? 0;
-    descriptor.value = async function (...args: any[]) {
-      const arg = args[paramIndex];
+    const parameterIndex = options.paramIndex ?? 0;
+    descriptor.value = async function (...arguments_: any[]) {
+      const argument = arguments_[parameterIndex];
 
       // Early return if argument length is less than or equal to the chunk size.
       if (
-        (arg instanceof Array && arg.length <= DATABASE_PARAMETER_CHUNK_SIZE) ||
-        (arg instanceof Set && arg.size <= DATABASE_PARAMETER_CHUNK_SIZE)
+        (Array.isArray(argument) && argument.length <= DATABASE_PARAMETER_CHUNK_SIZE) ||
+        (argument instanceof Set && argument.size <= DATABASE_PARAMETER_CHUNK_SIZE)
       ) {
-        return await originalMethod.apply(this, args);
+        return await originalMethod.apply(this, arguments_);
       }
 
       return Promise.all(
-        chunks(arg, DATABASE_PARAMETER_CHUNK_SIZE).map(async (chunk) => {
-          await originalMethod.apply(this, [...args.slice(0, paramIndex), chunk, ...args.slice(paramIndex + 1)]);
+        chunks(argument, DATABASE_PARAMETER_CHUNK_SIZE).map(async (chunk) => {
+          await Reflect.apply(originalMethod, this, [
+            ...arguments_.slice(0, parameterIndex),
+            chunk,
+            ...arguments_.slice(parameterIndex + 1),
+          ]);
         }),
       ).then((results) => (options.mergeFn ? options.mergeFn(results) : results));
     };
diff --git a/server/src/infra/migrations/1688392120838-AddLibraryTable.ts b/server/src/infra/migrations/1688392120838-AddLibraryTable.ts
index 53a6f780bf..4d394adaf1 100644
--- a/server/src/infra/migrations/1688392120838-AddLibraryTable.ts
+++ b/server/src/infra/migrations/1688392120838-AddLibraryTable.ts
@@ -24,7 +24,8 @@ export class AddLibraries1688392120838 implements MigrationInterface {
     );
 
     // Create default library for each user and assign all assets to it
-    const userIds: string[] = (await queryRunner.query(`SELECT id FROM "users"`)).map((user: any) => user.id);
+    const users = await queryRunner.query(`SELECT id FROM "users"`);
+    const userIds: string[] = users.map((user: any) => user.id);
 
     for (const userId of userIds) {
       await queryRunner.query(
diff --git a/server/src/infra/migrations/1700713871511-UsePgVectors.ts b/server/src/infra/migrations/1700713871511-UsePgVectors.ts
index 9f8a72cff3..a952f1646d 100644
--- a/server/src/infra/migrations/1700713871511-UsePgVectors.ts
+++ b/server/src/infra/migrations/1700713871511-UsePgVectors.ts
@@ -14,7 +14,7 @@ export class UsePgVectors1700713871511 implements MigrationInterface {
 
     const clipModelNameQuery = await queryRunner.query(`SELECT value FROM system_config WHERE key = 'machineLearning.clip.modelName'`);
     const clipModelName: string = clipModelNameQuery?.[0]?.['value'] ?? 'ViT-B-32__openai';
-    const clipDimSize = getCLIPModelInfo(clipModelName.replace(/"/g, '')).dimSize;
+    const clipDimSize = getCLIPModelInfo(clipModelName.replaceAll('"', '')).dimSize;
 
     await queryRunner.query(`
         ALTER TABLE asset_faces 
diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts
index f275b51713..cb6469195e 100644
--- a/server/src/infra/repositories/access.repository.ts
+++ b/server/src/infra/repositories/access.repository.ts
@@ -167,7 +167,7 @@ class AlbumAccess implements IAlbumAccess {
           })
           .then(
             (sharedLinks) =>
-              new Set(sharedLinks.flatMap((sharedLink) => (!!sharedLink.albumId ? [sharedLink.albumId] : []))),
+              new Set(sharedLinks.flatMap((sharedLink) => (sharedLink.albumId ? [sharedLink.albumId] : []))),
           ),
       ),
     ).then((results) => setUnion(...results));
diff --git a/server/src/infra/repositories/album.repository.ts b/server/src/infra/repositories/album.repository.ts
index aa66ba2dc8..2d3fd795db 100644
--- a/server/src/infra/repositories/album.repository.ts
+++ b/server/src/infra/repositories/album.repository.ts
@@ -71,7 +71,7 @@ export class AlbumRepository implements IAlbumRepository {
   @ChunkedArray()
   async getMetadataForIds(ids: string[]): Promise<AlbumAssetCount[]> {
     // Guard against running invalid query when ids list is empty.
-    if (!ids.length) {
+    if (ids.length === 0) {
       return [];
     }
 
diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts
index 226803cfb9..95a227b693 100644
--- a/server/src/infra/repositories/asset.repository.ts
+++ b/server/src/infra/repositories/asset.repository.ts
@@ -24,7 +24,7 @@ import { Injectable } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import _ from 'lodash';
 import { DateTime } from 'luxon';
-import path from 'path';
+import path from 'node:path';
 import {
   And,
   Brackets,
@@ -471,7 +471,7 @@ export class AssetRepository implements IAssetRepository {
     let where: FindOptionsWhere<AssetEntity> | FindOptionsWhere<AssetEntity>[] = {};
 
     switch (property) {
-      case WithoutProperty.THUMBNAIL:
+      case WithoutProperty.THUMBNAIL: {
         where = [
           { resizePath: IsNull(), isVisible: true },
           { resizePath: '', isVisible: true },
@@ -480,15 +480,17 @@ export class AssetRepository implements IAssetRepository {
           { thumbhash: IsNull(), isVisible: true },
         ];
         break;
+      }
 
-      case WithoutProperty.ENCODED_VIDEO:
+      case WithoutProperty.ENCODED_VIDEO: {
         where = [
           { type: AssetType.VIDEO, encodedVideoPath: IsNull() },
           { type: AssetType.VIDEO, encodedVideoPath: '' },
         ];
         break;
+      }
 
-      case WithoutProperty.EXIF:
+      case WithoutProperty.EXIF: {
         relations = {
           exifInfo: true,
           jobStatus: true,
@@ -500,8 +502,9 @@ export class AssetRepository implements IAssetRepository {
           },
         };
         break;
+      }
 
-      case WithoutProperty.SMART_SEARCH:
+      case WithoutProperty.SMART_SEARCH: {
         relations = {
           smartSearch: true,
         };
@@ -513,8 +516,9 @@ export class AssetRepository implements IAssetRepository {
           },
         };
         break;
+      }
 
-      case WithoutProperty.OBJECT_TAGS:
+      case WithoutProperty.OBJECT_TAGS: {
         relations = {
           smartInfo: true,
         };
@@ -526,8 +530,9 @@ export class AssetRepository implements IAssetRepository {
           },
         };
         break;
+      }
 
-      case WithoutProperty.FACES:
+      case WithoutProperty.FACES: {
         relations = {
           faces: true,
           jobStatus: true,
@@ -544,8 +549,9 @@ export class AssetRepository implements IAssetRepository {
           },
         };
         break;
+      }
 
-      case WithoutProperty.PERSON:
+      case WithoutProperty.PERSON: {
         relations = {
           faces: true,
         };
@@ -558,16 +564,19 @@ export class AssetRepository implements IAssetRepository {
           },
         };
         break;
+      }
 
-      case WithoutProperty.SIDECAR:
+      case WithoutProperty.SIDECAR: {
         where = [
           { sidecarPath: IsNull(), isVisible: true },
           { sidecarPath: '', isVisible: true },
         ];
         break;
+      }
 
-      default:
+      default: {
         throw new Error(`Invalid getWithout property: ${property}`);
+      }
     }
 
     return paginate(this.repository, pagination, {
@@ -584,18 +593,21 @@ export class AssetRepository implements IAssetRepository {
     let where: FindOptionsWhere<AssetEntity> | FindOptionsWhere<AssetEntity>[] = {};
 
     switch (property) {
-      case WithProperty.SIDECAR:
+      case WithProperty.SIDECAR: {
         where = [{ sidecarPath: Not(IsNull()), isVisible: true }];
         break;
-      case WithProperty.IS_OFFLINE:
+      }
+      case WithProperty.IS_OFFLINE: {
         if (!libraryId) {
           throw new Error('Library id is required when finding offline assets');
         }
         where = [{ isOffline: true, libraryId: libraryId }];
         break;
+      }
 
-      default:
+      default: {
         throw new Error(`Invalid getWith property: ${property}`);
+      }
     }
 
     return paginate(this.repository, pagination, {
diff --git a/server/src/infra/repositories/communication.repository.ts b/server/src/infra/repositories/communication.repository.ts
index 23edf85411..ec9eb005bf 100644
--- a/server/src/infra/repositories/communication.repository.ts
+++ b/server/src/infra/repositories/communication.repository.ts
@@ -51,13 +51,15 @@ export class CommunicationRepository
 
   on(event: 'connect' | ServerEvent, callback: OnConnectCallback | OnServerEventCallback) {
     switch (event) {
-      case 'connect':
+      case 'connect': {
         this.onConnectCallbacks.push(callback);
         break;
+      }
 
-      default:
+      default: {
         this.onServerEventCallbacks[event].push(callback as OnServerEventCallback);
         break;
+      }
     }
   }
 
diff --git a/server/src/infra/repositories/crypto.repository.ts b/server/src/infra/repositories/crypto.repository.ts
index a21bf6253e..f445ed850b 100644
--- a/server/src/infra/repositories/crypto.repository.ts
+++ b/server/src/infra/repositories/crypto.repository.ts
@@ -1,8 +1,8 @@
 import { ICryptoRepository } from '@app/domain';
 import { Injectable } from '@nestjs/common';
 import { compareSync, hash } from 'bcrypt';
-import { createHash, randomBytes, randomUUID } from 'crypto';
-import { createReadStream } from 'fs';
+import { createHash, randomBytes, randomUUID } from 'node:crypto';
+import { createReadStream } from 'node:fs';
 
 @Injectable()
 export class CryptoRepository implements ICryptoRepository {
@@ -24,7 +24,7 @@ export class CryptoRepository implements ICryptoRepository {
     return new Promise<Buffer>((resolve, reject) => {
       const hash = createHash('sha1');
       const stream = createReadStream(filepath);
-      stream.on('error', (err) => reject(err));
+      stream.on('error', (error) => reject(error));
       stream.on('data', (chunk) => hash.update(chunk));
       stream.on('end', () => resolve(hash.digest()));
     });
diff --git a/server/src/infra/repositories/filesystem.provider.ts b/server/src/infra/repositories/filesystem.provider.ts
index c9b44845d4..2ae18432b2 100644
--- a/server/src/infra/repositories/filesystem.provider.ts
+++ b/server/src/infra/repositories/filesystem.provider.ts
@@ -10,10 +10,10 @@ import {
 import { ImmichLogger } from '@app/infra/logger';
 import archiver from 'archiver';
 import chokidar, { WatchOptions } from 'chokidar';
-import { constants, createReadStream, existsSync, mkdirSync } from 'fs';
-import fs, { copyFile, readdir, rename, writeFile } from 'fs/promises';
 import { glob } from 'glob';
-import path from 'path';
+import { constants, createReadStream, existsSync, mkdirSync } from 'node:fs';
+import fs, { copyFile, readdir, rename, writeFile } from 'node:fs/promises';
+import path from 'node:path';
 
 export class FilesystemProvider implements IStorageRepository {
   private logger = new ImmichLogger(FilesystemProvider.name);
@@ -60,7 +60,7 @@ export class FilesystemProvider implements IStorageRepository {
     try {
       await fs.access(filepath, mode);
       return true;
-    } catch (_) {
+    } catch {
       return false;
     }
   }
@@ -68,11 +68,11 @@ export class FilesystemProvider implements IStorageRepository {
   async unlink(file: string) {
     try {
       await fs.unlink(file);
-    } catch (err) {
-      if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') {
+    } catch (error) {
+      if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
         this.logger.warn(`File ${file} does not exist.`);
       } else {
-        throw err;
+        throw error;
       }
     }
   }
diff --git a/server/src/infra/repositories/job.repository.ts b/server/src/infra/repositories/job.repository.ts
index 88f3a316ef..8160ff8440 100644
--- a/server/src/infra/repositories/job.repository.ts
+++ b/server/src/infra/repositories/job.repository.ts
@@ -15,7 +15,7 @@ import { ModuleRef } from '@nestjs/core';
 import { SchedulerRegistry } from '@nestjs/schedule';
 import { Job, JobsOptions, Processor, Queue, Worker, WorkerOptions } from 'bullmq';
 import { CronJob, CronTime } from 'cron';
-import { setTimeout } from 'timers/promises';
+import { setTimeout } from 'node:timers/promises';
 import { bullConfig } from '../infra.config';
 
 @Injectable()
@@ -24,7 +24,7 @@ export class JobRepository implements IJobRepository {
   private logger = new ImmichLogger(JobRepository.name);
 
   constructor(
-    private moduleRef: ModuleRef,
+    private moduleReference: ModuleRef,
     private schedulerReqistry: SchedulerRegistry,
   ) {}
 
@@ -118,7 +118,7 @@ export class JobRepository implements IJobRepository {
   }
 
   async queueAll(items: JobItem[]): Promise<void> {
-    if (!items.length) {
+    if (items.length === 0) {
       return;
     }
 
@@ -167,19 +167,23 @@ export class JobRepository implements IJobRepository {
 
   private getJobOptions(item: JobItem): JobsOptions | null {
     switch (item.name) {
-      case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE:
+      case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: {
         return { jobId: item.data.id };
-      case JobName.GENERATE_PERSON_THUMBNAIL:
+      }
+      case JobName.GENERATE_PERSON_THUMBNAIL: {
         return { priority: 1 };
-      case JobName.QUEUE_FACIAL_RECOGNITION:
+      }
+      case JobName.QUEUE_FACIAL_RECOGNITION: {
         return { jobId: JobName.QUEUE_FACIAL_RECOGNITION };
+      }
 
-      default:
+      default: {
         return null;
+      }
     }
   }
 
   private getQueue(queue: QueueName): Queue {
-    return this.moduleRef.get<Queue>(getQueueToken(queue), { strict: false });
+    return this.moduleReference.get<Queue>(getQueueToken(queue), { strict: false });
   }
 }
diff --git a/server/src/infra/repositories/machine-learning.repository.ts b/server/src/infra/repositories/machine-learning.repository.ts
index 71a6995188..4542c65779 100644
--- a/server/src/infra/repositories/machine-learning.repository.ts
+++ b/server/src/infra/repositories/machine-learning.repository.ts
@@ -10,7 +10,7 @@ import {
   VisionModelInput,
 } from '@app/domain';
 import { Injectable } from '@nestjs/common';
-import { readFile } from 'fs/promises';
+import { readFile } from 'node:fs/promises';
 
 const errorPrefix = 'Machine learning request';
 
diff --git a/server/src/infra/repositories/media.repository.ts b/server/src/infra/repositories/media.repository.ts
index 884c24bf9b..bb65dd25c8 100644
--- a/server/src/infra/repositories/media.repository.ts
+++ b/server/src/infra/repositories/media.repository.ts
@@ -2,10 +2,10 @@ import { CropOptions, IMediaRepository, ResizeOptions, TranscodeOptions, VideoIn
 import { Colorspace } from '@app/infra/entities';
 import { ImmichLogger } from '@app/infra/logger';
 import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
-import fs from 'fs/promises';
+import fs from 'node:fs/promises';
+import { Writable } from 'node:stream';
+import { promisify } from 'node:util';
 import sharp from 'sharp';
-import { Writable } from 'stream';
-import { promisify } from 'util';
 
 const probe = promisify<string, FfprobeData>(ffmpeg.ffprobe);
 sharp.concurrency(0);
@@ -91,7 +91,7 @@ export class MediaRepository implements IMediaRepository {
     }
 
     if (typeof output !== 'string') {
-      throw new Error('Two-pass transcoding does not support writing to a stream');
+      throw new TypeError('Two-pass transcoding does not support writing to a stream');
     }
 
     // two-pass allows for precise control of bitrate at the cost of running twice
@@ -124,12 +124,12 @@ export class MediaRepository implements IMediaRepository {
       .inputOptions(options.inputOptions)
       .outputOptions(options.outputOptions)
       .output(output)
-      .on('error', (err, stdout, stderr) => this.logger.error(stderr || err));
+      .on('error', (error, stdout, stderr) => this.logger.error(stderr || error));
   }
 
   chainPath(existing: string, path: string) {
-    const sep = existing.endsWith(':') ? '' : ':';
-    return `${existing}${sep}${path}`;
+    const separator = existing.endsWith(':') ? '' : ':';
+    return `${existing}${separator}${path}`;
   }
 
   async generateThumbhash(imagePath: string): Promise<Buffer> {
diff --git a/server/src/infra/repositories/metadata.repository.ts b/server/src/infra/repositories/metadata.repository.ts
index d916795bcb..83c05597a2 100644
--- a/server/src/infra/repositories/metadata.repository.ts
+++ b/server/src/infra/repositories/metadata.repository.ts
@@ -15,11 +15,11 @@ import { ImmichLogger } from '@app/infra/logger';
 import { Inject } from '@nestjs/common';
 import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
 import { DefaultReadTaskOptions, exiftool, Tags } from 'exiftool-vendored';
-import { createReadStream, existsSync } from 'fs';
-import { readFile } from 'fs/promises';
 import * as geotz from 'geo-tz';
 import { getName } from 'i18n-iso-countries';
-import * as readLine from 'readline';
+import { createReadStream, existsSync } from 'node:fs';
+import { readFile } from 'node:fs/promises';
+import * as readLine from 'node:readline';
 import { DataSource, DeepPartial, QueryRunner, Repository } from 'typeorm';
 
 type GeoEntity = GeodataPlacesEntity | GeodataAdmin1Entity | GeodataAdmin2Entity;
@@ -69,10 +69,10 @@ export class MetadataRepository implements IMetadataRepository {
       await this.loadAdmin2(queryRunner);
 
       await queryRunner.commitTransaction();
-    } catch (e) {
-      this.logger.fatal('Error importing geodata', e);
+    } catch (error) {
+      this.logger.fatal('Error importing geodata', error);
       await queryRunner.rollbackTransaction();
-      throw e;
+      throw error;
     } finally {
       await queryRunner.release();
     }
@@ -110,10 +110,10 @@ export class MetadataRepository implements IMetadataRepository {
       queryRunner,
       (lineSplit: string[]) =>
         this.geodataPlacesRepository.create({
-          id: parseInt(lineSplit[0]),
+          id: Number.parseInt(lineSplit[0]),
           name: lineSplit[1],
-          latitude: parseFloat(lineSplit[4]),
-          longitude: parseFloat(lineSplit[5]),
+          latitude: Number.parseFloat(lineSplit[4]),
+          longitude: Number.parseFloat(lineSplit[5]),
           countryCode: lineSplit[8],
           admin1Code: lineSplit[10],
           admin2Code: lineSplit[11],
@@ -192,7 +192,8 @@ export class MetadataRepository implements IMetadataRepository {
         backfillTimezones: true,
         inferTimezoneFromDatestamps: true,
         useMWG: true,
-        numericTags: DefaultReadTaskOptions.numericTags.concat(['FocalLength']),
+        numericTags: [...DefaultReadTaskOptions.numericTags, 'FocalLength'],
+        /* eslint unicorn/no-array-callback-reference: off, unicorn/no-array-method-this-argument: off */
         geoTz: (lat, lon) => geotz.find(lat, lon)[0],
       })
       .catch((error) => {
diff --git a/server/src/infra/repositories/person.repository.ts b/server/src/infra/repositories/person.repository.ts
index 195fe5a5b4..85423b74dd 100644
--- a/server/src/infra/repositories/person.repository.ts
+++ b/server/src/infra/repositories/person.repository.ts
@@ -28,12 +28,7 @@ export class PersonRepository implements IPersonRepository {
       .createQueryBuilder()
       .update()
       .set({ personId: newPersonId })
-      .where(
-        _.omitBy(
-          { personId: oldPersonId ? oldPersonId : undefined, id: faceIds ? In(faceIds) : undefined },
-          _.isUndefined,
-        ),
-      )
+      .where(_.omitBy({ personId: oldPersonId ?? undefined, id: faceIds ? In(faceIds) : undefined }, _.isUndefined))
       .execute();
 
     return result.affected ?? 0;
diff --git a/server/src/infra/repositories/smart-info.repository.ts b/server/src/infra/repositories/smart-info.repository.ts
index 4f9c52a66e..ab43ff6f91 100644
--- a/server/src/infra/repositories/smart-info.repository.ts
+++ b/server/src/infra/repositories/smart-info.repository.ts
@@ -31,11 +31,11 @@ export class SmartInfoRepository implements ISmartInfoRepository {
       throw new Error(`Invalid CLIP model name: ${modelName}`);
     }
 
-    const curDimSize = await this.getDimSize();
-    this.logger.verbose(`Current database CLIP dimension size is ${curDimSize}`);
+    const currentDimSize = await this.getDimSize();
+    this.logger.verbose(`Current database CLIP dimension size is ${currentDimSize}`);
 
-    if (dimSize != curDimSize) {
-      this.logger.log(`Dimension size of model ${modelName} is ${dimSize}, but database expects ${curDimSize}.`);
+    if (dimSize != currentDimSize) {
+      this.logger.log(`Dimension size of model ${modelName} is ${dimSize}, but database expects ${currentDimSize}.`);
       await this.updateDimSize(dimSize);
     }
   }
@@ -119,7 +119,9 @@ export class SmartInfoRepository implements ISmartInfoRepository {
         cte = cte.andWhere('faces."personId" IS NOT NULL');
       }
 
-      this.faceColumns.forEach((col) => cte.addSelect(`faces.${col}`, col));
+      for (const col of this.faceColumns) {
+        cte.addSelect(`faces.${col}`, col);
+      }
 
       results = await manager
         .createQueryBuilder()
@@ -157,8 +159,8 @@ export class SmartInfoRepository implements ISmartInfoRepository {
       throw new Error(`Invalid CLIP dimension size: ${dimSize}`);
     }
 
-    const curDimSize = await this.getDimSize();
-    if (curDimSize === dimSize) {
+    const currentDimSize = await this.getDimSize();
+    if (currentDimSize === dimSize) {
       return;
     }
 
@@ -181,7 +183,7 @@ export class SmartInfoRepository implements ISmartInfoRepository {
           $$)`);
     });
 
-    this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`);
+    this.logger.log(`Successfully updated database CLIP dimension size from ${currentDimSize} to ${dimSize}.`);
   }
 
   private async getDimSize(): Promise<number> {
diff --git a/server/src/infra/repositories/system-config.repository.ts b/server/src/infra/repositories/system-config.repository.ts
index 4ab35b4d63..82d0b8c8be 100644
--- a/server/src/infra/repositories/system-config.repository.ts
+++ b/server/src/infra/repositories/system-config.repository.ts
@@ -1,7 +1,7 @@
 import { ISystemConfigRepository } from '@app/domain';
 import { InjectRepository } from '@nestjs/typeorm';
 import axios from 'axios';
-import { readFile } from 'fs/promises';
+import { readFile } from 'node:fs/promises';
 import { In, Repository } from 'typeorm';
 import { SystemConfigEntity } from '../entities';
 import { DummyValue, GenerateSql } from '../infra.util';
@@ -22,7 +22,7 @@ export class SystemConfigRepository implements ISystemConfigRepository {
   }
 
   readFile(filename: string): Promise<string> {
-    return readFile(filename, { encoding: 'utf-8' });
+    return readFile(filename, { encoding: 'utf8' });
   }
 
   saveAll(items: SystemConfigEntity[]): Promise<SystemConfigEntity[]> {
diff --git a/server/src/infra/repositories/user.repository.ts b/server/src/infra/repositories/user.repository.ts
index 5d55eea1f6..640eda0ee4 100644
--- a/server/src/infra/repositories/user.repository.ts
+++ b/server/src/infra/repositories/user.repository.ts
@@ -74,11 +74,7 @@ export class UserRepository implements IUserRepository {
   }
 
   async delete(user: UserEntity, hard?: boolean): Promise<UserEntity> {
-    if (hard) {
-      return this.userRepository.remove(user);
-    } else {
-      return this.userRepository.softRemove(user);
-    }
+    return hard ? this.userRepository.remove(user) : this.userRepository.softRemove(user);
   }
 
   async restore(user: UserEntity): Promise<UserEntity> {
diff --git a/server/src/infra/sql-generator/index.ts b/server/src/infra/sql-generator/index.ts
index b4b0978a23..348762d957 100644
--- a/server/src/infra/sql-generator/index.ts
+++ b/server/src/infra/sql-generator/index.ts
@@ -1,10 +1,11 @@
+#!/usr/bin/env node
 import { ISystemConfigRepository } from '@app/domain';
 import { INestApplication } from '@nestjs/common';
 import { Reflector } from '@nestjs/core';
 import { Test } from '@nestjs/testing';
 import { TypeOrmModule } from '@nestjs/typeorm';
-import { mkdir, rm, writeFile } from 'fs/promises';
-import { join } from 'path';
+import { mkdir, rm, writeFile } from 'node:fs/promises';
+import { join } from 'node:path';
 import { databaseConfig } from '../database.config';
 import { databaseEntities } from '../entities';
 import { GENERATE_SQL_KEY, GenerateSqlQueries } from '../infra.util';
@@ -157,7 +158,7 @@ class SqlGenerator {
 
   private async write() {
     for (const [repoName, data] of Object.entries(this.results)) {
-      const filename = repoName.replace(/[A-Z]/g, (letter) => `.${letter.toLowerCase()}`).replace('.', '');
+      const filename = repoName.replaceAll(/[A-Z]/g, (letter) => `.${letter.toLowerCase()}`).replace('.', '');
       const file = join(this.options.targetDir, `${filename}.sql`);
       await writeFile(file, data.join('\n\n') + '\n');
     }
diff --git a/server/src/infra/sql-generator/sql.logger.ts b/server/src/infra/sql-generator/sql.logger.ts
index 78c3df148e..6f3c298c08 100644
--- a/server/src/infra/sql-generator/sql.logger.ts
+++ b/server/src/infra/sql-generator/sql.logger.ts
@@ -1,8 +1,6 @@
+import { format } from 'sql-formatter';
 import { Logger } from 'typeorm';
 
-// eslint-disable-next-line @typescript-eslint/no-var-requires
-const { format } = require('sql-formatter');
-
 export class SqlLogger implements Logger {
   queries: string[] = [];
   errors: Array<{ error: string | Error; query: string }> = [];
diff --git a/server/src/infra/subscribers/audit.subscriber.ts b/server/src/infra/subscribers/audit.subscriber.ts
index c0e8313077..896f9ae5e0 100644
--- a/server/src/infra/subscribers/audit.subscriber.ts
+++ b/server/src/infra/subscribers/audit.subscriber.ts
@@ -16,21 +16,23 @@ export class AuditSubscriber implements EntitySubscriberInterface<AssetEntity |
 
   private getAudit(entityName: string, entity: any): Partial<AuditEntity> | null {
     switch (entityName) {
-      case AssetEntity.name:
+      case AssetEntity.name: {
         const asset = entity as AssetEntity;
         return {
           entityType: EntityType.ASSET,
           entityId: asset.id,
           ownerId: asset.ownerId,
         };
+      }
 
-      case AlbumEntity.name:
+      case AlbumEntity.name: {
         const album = entity as AlbumEntity;
         return {
           entityType: EntityType.ALBUM,
           entityId: album.id,
           ownerId: album.ownerId,
         };
+      }
     }
 
     return null;
diff --git a/server/src/main.ts b/server/src/main.ts
index c43d6ea461..198b0f0877 100644
--- a/server/src/main.ts
+++ b/server/src/main.ts
@@ -10,18 +10,21 @@ if (process.argv[2] === immichApp) {
 
 function bootstrap() {
   switch (immichApp) {
-    case 'immich':
+    case 'immich': {
       process.title = 'immich_server';
       return server();
-    case 'microservices':
+    }
+    case 'microservices': {
       process.title = 'immich_microservices';
       return microservices();
-    case 'immich-admin':
+    }
+    case 'immich-admin': {
       process.title = 'immich_admin_cli';
       return admin();
-    default:
-      console.log(`Invalid app name: ${immichApp}. Expected one of immich|microservices|cli`);
-      process.exit(1);
+    }
+    default: {
+      throw new Error(`Invalid app name: ${immichApp}. Expected one of immich|microservices|cli`);
+    }
   }
 }
 void bootstrap();
diff --git a/server/src/microservices/utils/exif/coordinates.spec.ts b/server/src/microservices/utils/exif/coordinates.spec.ts
index fd9ffd5d58..b9644fb49a 100644
--- a/server/src/microservices/utils/exif/coordinates.spec.ts
+++ b/server/src/microservices/utils/exif/coordinates.spec.ts
@@ -7,7 +7,7 @@ describe('parsing latitude from string input', () => {
     expect(parseLatitude('Infinity')).toBeNull();
     expect(parseLatitude('-Infinity')).toBeNull();
     expect(parseLatitude('90.001')).toBeNull();
-    expect(parseLatitude(-90.000001)).toBeNull();
+    expect(parseLatitude(-90.000_001)).toBeNull();
     expect(parseLatitude('1000')).toBeNull();
     expect(parseLatitude(-1000)).toBeNull();
   });
@@ -15,10 +15,10 @@ describe('parsing latitude from string input', () => {
   it('returns the numeric coordinate for valid inputs', () => {
     expect(parseLatitude('90')).toBeCloseTo(90);
     expect(parseLatitude('-90')).toBeCloseTo(-90);
-    expect(parseLatitude(89.999999)).toBeCloseTo(89.999999);
+    expect(parseLatitude(89.999_999)).toBeCloseTo(89.999_999);
     expect(parseLatitude('-89.9')).toBeCloseTo(-89.9);
     expect(parseLatitude(0)).toBeCloseTo(0);
-    expect(parseLatitude('-0.0')).toBeCloseTo(-0.0);
+    expect(parseLatitude('-0.0')).toBeCloseTo(-0);
   });
 });
 
@@ -32,7 +32,7 @@ describe('parsing longitude from string input', () => {
   it('returns null for invalid inputs', () => {
     expect(parseLongitude('')).toBeNull();
     expect(parseLongitude('NaN')).toBeNull();
-    expect(parseLongitude(Infinity)).toBeNull();
+    expect(parseLongitude(Number.POSITIVE_INFINITY)).toBeNull();
     expect(parseLongitude('-Infinity')).toBeNull();
     expect(parseLongitude('180.001')).toBeNull();
     expect(parseLongitude('-180.000001')).toBeNull();
@@ -43,10 +43,10 @@ describe('parsing longitude from string input', () => {
   it('returns the numeric coordinate for valid inputs', () => {
     expect(parseLongitude(180)).toBeCloseTo(180);
     expect(parseLongitude('-180')).toBeCloseTo(-180);
-    expect(parseLongitude('179.999999')).toBeCloseTo(179.999999);
+    expect(parseLongitude('179.999999')).toBeCloseTo(179.999_999);
     expect(parseLongitude(-179.9)).toBeCloseTo(-179.9);
     expect(parseLongitude('0')).toBeCloseTo(0);
-    expect(parseLongitude('-0.0')).toBeCloseTo(-0.0);
+    expect(parseLongitude('-0.0')).toBeCloseTo(-0);
   });
 });
 
diff --git a/server/src/microservices/utils/numbers.spec.ts b/server/src/microservices/utils/numbers.spec.ts
index 19aba8f76a..47f95b8aab 100644
--- a/server/src/microservices/utils/numbers.spec.ts
+++ b/server/src/microservices/utils/numbers.spec.ts
@@ -2,15 +2,15 @@ import { isDecimalNumber, isNumberInRange, toNumberOrNull } from './numbers';
 
 describe('checks if a number is a decimal number', () => {
   it('returns false for non-decimal numbers', () => {
-    expect(isDecimalNumber(NaN)).toBe(false);
-    expect(isDecimalNumber(Infinity)).toBe(false);
-    expect(isDecimalNumber(-Infinity)).toBe(false);
+    expect(isDecimalNumber(Number.NaN)).toBe(false);
+    expect(isDecimalNumber(Number.POSITIVE_INFINITY)).toBe(false);
+    expect(isDecimalNumber(Number.NEGATIVE_INFINITY)).toBe(false);
   });
 
   it('returns true for decimal numbers', () => {
     expect(isDecimalNumber(0)).toBe(true);
     expect(isDecimalNumber(-0)).toBe(true);
-    expect(isDecimalNumber(10.12345)).toBe(true);
+    expect(isDecimalNumber(10.123_45)).toBe(true);
     expect(isDecimalNumber(Number.MAX_VALUE)).toBe(true);
     expect(isDecimalNumber(Number.MIN_VALUE)).toBe(true);
   });
@@ -26,16 +26,17 @@ describe('checks if a number is within a range', () => {
   it('returns true for numbers inside the range', () => {
     expect(isNumberInRange(0, 0, 50)).toBe(true);
     expect(isNumberInRange(50, 0, 50)).toBe(true);
-    expect(isNumberInRange(-50.12345, -50.12345, 0)).toBe(true);
+    expect(isNumberInRange(-50.123_45, -50.123_45, 0)).toBe(true);
   });
 });
 
 describe('converts input to a number or null', () => {
   it('returns null for invalid inputs', () => {
     expect(toNumberOrNull(null)).toBeNull();
+    // eslint-disable-next-line unicorn/no-useless-undefined
     expect(toNumberOrNull(undefined)).toBeNull();
     expect(toNumberOrNull('')).toBeNull();
-    expect(toNumberOrNull(NaN)).toBeNull();
+    expect(toNumberOrNull(Number.NaN)).toBeNull();
   });
 
   it('returns a number for valid inputs', () => {
diff --git a/server/src/microservices/utils/numbers.ts b/server/src/microservices/utils/numbers.ts
index 4eb8884b1a..cd6e81d2a2 100644
--- a/server/src/microservices/utils/numbers.ts
+++ b/server/src/microservices/utils/numbers.ts
@@ -1,12 +1,12 @@
-export function isDecimalNumber(num: number): boolean {
-  return !Number.isNaN(num) && Number.isFinite(num);
+export function isDecimalNumber(number_: number): boolean {
+  return !Number.isNaN(number_) && Number.isFinite(number_);
 }
 
 /**
  * Check if `num` is a valid number and is between `start` and `end` (inclusive)
  */
-export function isNumberInRange(num: number, start: number, end: number): boolean {
-  return isDecimalNumber(num) && num >= start && num <= end;
+export function isNumberInRange(number_: number, start: number, end: number): boolean {
+  return isDecimalNumber(number_) && number_ >= start && number_ <= end;
 }
 
 export function toNumberOrNull(input: number | string | null | undefined): number | null {
@@ -14,6 +14,6 @@ export function toNumberOrNull(input: number | string | null | undefined): numbe
     return null;
   }
 
-  const num = typeof input === 'string' ? Number.parseFloat(input) : input;
-  return isDecimalNumber(num) ? num : null;
+  const number_ = typeof input === 'string' ? Number.parseFloat(input) : input;
+  return isDecimalNumber(number_) ? number_ : null;
 }
diff --git a/server/src/test-utils/utils.ts b/server/src/test-utils/utils.ts
index 67ad5fff34..077239d8a3 100644
--- a/server/src/test-utils/utils.ts
+++ b/server/src/test-utils/utils.ts
@@ -7,8 +7,8 @@ import { Test } from '@nestjs/testing';
 import { DateTime } from 'luxon';
 import * as fs from 'node:fs';
 import path from 'node:path';
+import { EventEmitter } from 'node:stream';
 import { Server } from 'node:tls';
-import { EventEmitter } from 'stream';
 import { EntityTarget, ObjectLiteral } from 'typeorm';
 import { AppService } from '../immich/app.service';
 import { AppService as MicroAppService } from '../microservices/app.service';
@@ -69,7 +69,7 @@ class JobMock implements IJobRepository {
     return this._handler(item);
   }
   queueAll(items: JobItem[]) {
-    return Promise.all(items.map(this._handler)).then(() => Promise.resolve());
+    return Promise.all(items.map((arg) => this._handler(arg))).then(() => {});
   }
   async resume() {}
   async empty() {}
@@ -140,13 +140,13 @@ export const testApp = {
 
 export function waitForEvent<T>(emitter: EventEmitter, event: string): Promise<T> {
   return new Promise((resolve, reject) => {
-    const success = (val: T) => {
+    const success = (value: T) => {
       emitter.off('error', fail);
-      resolve(val);
+      resolve(value);
     };
-    const fail = (err: Error) => {
+    const fail = (error: Error) => {
       emitter.off(event, success);
-      reject(err);
+      reject(error);
     };
     emitter.once(event, success);
     emitter.once('error', fail);
diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts
index fcc52df8f4..36f646af63 100644
--- a/server/test/fixtures/asset.stub.ts
+++ b/server/test/fixtures/asset.stub.ts
@@ -164,7 +164,7 @@ export const assetStub = {
     deletedAt: null,
     sidecarPath: null,
     exifInfo: {
-      fileSizeInByte: 5_000,
+      fileSizeInByte: 5000,
     } as ExifEntity,
     stack: assetStackStub('stack-1', [
       { id: 'primary-asset-id' } as AssetEntity,
@@ -209,7 +209,7 @@ export const assetStub = {
     deletedAt: null,
     sidecarPath: null,
     exifInfo: {
-      fileSizeInByte: 5_000,
+      fileSizeInByte: 5000,
     } as ExifEntity,
   }),
 
@@ -249,7 +249,7 @@ export const assetStub = {
     deletedAt: null,
     sidecarPath: null,
     exifInfo: {
-      fileSizeInByte: 5_000,
+      fileSizeInByte: 5000,
     } as ExifEntity,
   }),
 
@@ -288,7 +288,7 @@ export const assetStub = {
     faces: [],
     sidecarPath: null,
     exifInfo: {
-      fileSizeInByte: 5_000,
+      fileSizeInByte: 5000,
     } as ExifEntity,
     deletedAt: null,
   }),
@@ -329,7 +329,7 @@ export const assetStub = {
     faces: [],
     sidecarPath: null,
     exifInfo: {
-      fileSizeInByte: 5_000,
+      fileSizeInByte: 5000,
     } as ExifEntity,
   }),
 
@@ -368,7 +368,7 @@ export const assetStub = {
     faces: [],
     sidecarPath: null,
     exifInfo: {
-      fileSizeInByte: 5_000,
+      fileSizeInByte: 5000,
     } as ExifEntity,
     deletedAt: null,
   }),
diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts
index ee6b767ef0..30dfec1669 100644
--- a/server/test/fixtures/media.stub.ts
+++ b/server/test/fixtures/media.stub.ts
@@ -94,7 +94,7 @@ export const probeStub = {
       formatName: 'mov,mp4,m4a,3gp,3g2,mj2',
       formatLongName: 'QuickTime / MOV',
       duration: 0,
-      bitrate: 40000000,
+      bitrate: 40_000_000,
     },
   }),
   videoStreamHDR: Object.freeze<VideoInfo>({
diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts
index d37a4af6e0..f34e6b06b5 100644
--- a/server/test/repositories/database.repository.mock.ts
+++ b/server/test/repositories/database.repository.mock.ts
@@ -6,7 +6,7 @@ export const newDatabaseRepositoryMock = (): jest.Mocked<IDatabaseRepository> =>
     getPostgresVersion: jest.fn().mockResolvedValue(new Version(14, 0, 0)),
     createExtension: jest.fn().mockImplementation(() => Promise.resolve()),
     runMigrations: jest.fn(),
-    withLock: jest.fn().mockImplementation((_, func: <R>() => Promise<R>) => func()),
+    withLock: jest.fn().mockImplementation((_, function_: <R>() => Promise<R>) => function_()),
     isBusy: jest.fn(),
     wait: jest.fn(),
   };
diff --git a/server/tsconfig.json b/server/tsconfig.json
index d86cba04c5..6d89fe7088 100644
--- a/server/tsconfig.json
+++ b/server/tsconfig.json
@@ -1,6 +1,6 @@
 {
   "compilerOptions": {
-    "module": "Node16",
+    "module": "node16",
     "strict": true,
     "declaration": true,
     "removeComments": true,
@@ -8,7 +8,7 @@
     "experimentalDecorators": true,
     "allowSyntheticDefaultImports": true,
     "resolveJsonModule": true,
-    "target": "es2021",
+    "target": "es2022",
     "moduleResolution": "node16",
     "sourceMap": true,
     "outDir": "./dist",
@@ -25,8 +25,8 @@
       "@app/infra": ["src/infra"],
       "@app/infra/*": ["src/infra/*"],
       "@app/domain": ["src/domain"],
-      "@app/domain/*": ["src/domain/*"]
-    }
+      "@app/domain/*": ["src/domain/*"],
+    },
   },
-  "exclude": ["dist", "node_modules", "upload"]
+  "exclude": ["dist", "node_modules", "upload"],
 }
diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs
index 9277676ac5..de62060e0f 100644
--- a/web/.eslintrc.cjs
+++ b/web/.eslintrc.cjs
@@ -1,12 +1,17 @@
 /** @type {import('eslint').Linter.Config} */
 module.exports = {
   root: true,
-  extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:svelte/recommended'],
+  extends: [
+    'eslint:recommended',
+    'plugin:@typescript-eslint/recommended',
+    'plugin:svelte/recommended',
+    'plugin:unicorn/recommended',
+  ],
   parser: '@typescript-eslint/parser',
   plugins: ['@typescript-eslint'],
   parserOptions: {
     sourceType: 'module',
-    ecmaVersion: 2020,
+    ecmaVersion: 2022,
     extraFileExtensions: ['.svelte'],
   },
   env: {
@@ -27,6 +32,12 @@ module.exports = {
     NodeJS: true,
   },
   rules: {
+    'unicorn/no-useless-undefined': 'off',
+    'unicorn/prefer-spread': 'off',
+    'unicorn/no-null': 'off',
+    'unicorn/prevent-abbreviations': 'off',
+    'unicorn/no-nested-ternary': 'off',
+    'unicorn/consistent-function-scoping': 'off',
     '@typescript-eslint/no-unused-vars': [
       'warn',
       {
diff --git a/web/package-lock.json b/web/package-lock.json
index 835b0451e3..93c04c47b6 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -38,13 +38,14 @@
         "@types/justified-layout": "^4.1.0",
         "@types/lodash-es": "^4.17.6",
         "@types/luxon": "^3.2.0",
-        "@typescript-eslint/eslint-plugin": "^6.0.0",
-        "@typescript-eslint/parser": "^6.0.0",
+        "@typescript-eslint/eslint-plugin": "^6.4.1",
+        "@typescript-eslint/parser": "^6.4.1",
         "@vitest/coverage-v8": "^1.0.4",
         "autoprefixer": "^10.4.13",
         "eslint": "^8.34.0",
-        "eslint-config-prettier": "^9.0.0",
+        "eslint-config-prettier": "^9.1.0",
         "eslint-plugin-svelte": "^2.30.0",
+        "eslint-plugin-unicorn": "^50.0.1",
         "factory.ts": "^1.3.0",
         "identity-obj-proxy": "^3.0.0",
         "postcss": "^8.4.21",
@@ -55,7 +56,7 @@
         "svelte-preprocess": "^5.0.3",
         "tailwindcss": "^3.2.7",
         "tslib": "^2.5.0",
-        "typescript": "^5.0.0",
+        "typescript": "^5.3.3",
         "vite": "^5.0.10",
         "vitest": "^1.0.4"
       }
@@ -1774,6 +1775,12 @@
       "optional": true,
       "peer": true
     },
+    "node_modules/@types/normalize-package-data": {
+      "version": "2.4.4",
+      "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
+      "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
+      "dev": true
+    },
     "node_modules/@types/pbf": {
       "version": "3.0.5",
       "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
@@ -2734,6 +2741,18 @@
       "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
       "dev": true
     },
+    "node_modules/builtin-modules": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
+      "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/bytewise": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz",
@@ -2894,6 +2913,33 @@
         "node": ">= 6"
       }
     },
+    "node_modules/ci-info": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz",
+      "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/sibiraj-s"
+        }
+      ],
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/clean-regexp": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
+      "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==",
+      "dev": true,
+      "dependencies": {
+        "escape-string-regexp": "^1.0.5"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/clone-deep": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
@@ -2981,6 +3027,19 @@
       "resolved": "https://registry.npmjs.org/copy-image-clipboard/-/copy-image-clipboard-2.1.2.tgz",
       "integrity": "sha512-3VCXVl2IpFfOyD8drv9DozcNlwmqBqxOlsgkEGyVAzadjlPk1go8YNZyy8QmTnwHPxSFpeCR9OdsStEdVK7qDA=="
     },
+    "node_modules/core-js-compat": {
+      "version": "3.35.1",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz",
+      "integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==",
+      "dev": true,
+      "dependencies": {
+        "browserslist": "^4.22.2"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -3339,6 +3398,15 @@
         "url": "https://github.com/fb55/entities?sponsor=1"
       }
     },
+    "node_modules/error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "dev": true,
+      "dependencies": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
     "node_modules/es-get-iterator": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
@@ -3568,6 +3636,84 @@
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
       "dev": true
     },
+    "node_modules/eslint-plugin-unicorn": {
+      "version": "50.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz",
+      "integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==",
+      "dev": true,
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.22.20",
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@eslint/eslintrc": "^2.1.4",
+        "ci-info": "^4.0.0",
+        "clean-regexp": "^1.0.0",
+        "core-js-compat": "^3.34.0",
+        "esquery": "^1.5.0",
+        "indent-string": "^4.0.0",
+        "is-builtin-module": "^3.2.1",
+        "jsesc": "^3.0.2",
+        "pluralize": "^8.0.0",
+        "read-pkg-up": "^7.0.1",
+        "regexp-tree": "^0.1.27",
+        "regjsparser": "^0.10.0",
+        "semver": "^7.5.4",
+        "strip-indent": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
+      },
+      "peerDependencies": {
+        "eslint": ">=8.56.0"
+      }
+    },
+    "node_modules/eslint-plugin-unicorn/node_modules/jsesc": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
+      "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
+      "dev": true,
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/eslint-plugin-unicorn/node_modules/lru-cache": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+      "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+      "dev": true,
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/eslint-plugin-unicorn/node_modules/semver": {
+      "version": "7.5.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+      "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+      "dev": true,
+      "dependencies": {
+        "lru-cache": "^6.0.0"
+      },
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/eslint-plugin-unicorn/node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "dev": true
+    },
     "node_modules/eslint-scope": {
       "version": "7.2.2",
       "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -4328,6 +4474,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/hosted-git-info": {
+      "version": "2.8.9",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+      "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+      "dev": true
+    },
     "node_modules/html-encoding-sniffer": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
@@ -4567,6 +4719,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+      "dev": true
+    },
     "node_modules/is-bigint": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
@@ -4607,6 +4765,21 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-builtin-module": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
+      "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
+      "dev": true,
+      "dependencies": {
+        "builtin-modules": "^3.3.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/is-callable": {
       "version": "1.2.7",
       "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
@@ -5039,6 +5212,12 @@
       "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
       "dev": true
     },
+    "node_modules/json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+      "dev": true
+    },
     "node_modules/json-schema-traverse": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -5546,6 +5725,27 @@
       "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
       "dev": true
     },
+    "node_modules/normalize-package-data": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+      "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+      "dev": true,
+      "dependencies": {
+        "hosted-git-info": "^2.1.4",
+        "resolve": "^1.10.0",
+        "semver": "2 || 3 || 4 || 5",
+        "validate-npm-package-license": "^3.0.1"
+      }
+    },
+    "node_modules/normalize-package-data/node_modules/semver": {
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+      "dev": true,
+      "bin": {
+        "semver": "bin/semver"
+      }
+    },
     "node_modules/normalize-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -5740,6 +5940,15 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/parent-module": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -5752,6 +5961,24 @@
         "node": ">=6"
       }
     },
+    "node_modules/parse-json": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "dev": true,
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/parse5": {
       "version": "7.1.2",
       "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@@ -5892,6 +6119,15 @@
         "pathe": "^1.1.0"
       }
     },
+    "node_modules/pluralize": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
+      "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/pmtiles": {
       "version": "2.11.0",
       "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-2.11.0.tgz",
@@ -6213,6 +6449,108 @@
         "pify": "^2.3.0"
       }
     },
+    "node_modules/read-pkg": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
+      "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
+      "dev": true,
+      "dependencies": {
+        "@types/normalize-package-data": "^2.4.0",
+        "normalize-package-data": "^2.5.0",
+        "parse-json": "^5.0.0",
+        "type-fest": "^0.6.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
+      "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
+      "dev": true,
+      "dependencies": {
+        "find-up": "^4.1.0",
+        "read-pkg": "^5.2.0",
+        "type-fest": "^0.8.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "dev": true,
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "dev": true,
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "dev": true,
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "dev": true,
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg-up/node_modules/type-fest": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+      "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/read-pkg/node_modules/type-fest": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
+      "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
+      "dev": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/readdirp": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -6244,6 +6582,15 @@
       "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==",
       "dev": true
     },
+    "node_modules/regexp-tree": {
+      "version": "0.1.27",
+      "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
+      "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
+      "dev": true,
+      "bin": {
+        "regexp-tree": "bin/regexp-tree"
+      }
+    },
     "node_modules/regexp.prototype.flags": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
@@ -6261,6 +6608,27 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/regjsparser": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz",
+      "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==",
+      "dev": true,
+      "dependencies": {
+        "jsesc": "~0.5.0"
+      },
+      "bin": {
+        "regjsparser": "bin/parser"
+      }
+    },
+    "node_modules/regjsparser/node_modules/jsesc": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+      "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==",
+      "dev": true,
+      "bin": {
+        "jsesc": "bin/jsesc"
+      }
+    },
     "node_modules/requires-port": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -6677,6 +7045,38 @@
         "source-map": "^0.6.0"
       }
     },
+    "node_modules/spdx-correct": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+      "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+      "dev": true,
+      "dependencies": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/spdx-exceptions": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz",
+      "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==",
+      "dev": true
+    },
+    "node_modules/spdx-expression-parse": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+      "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+      "dev": true,
+      "dependencies": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/spdx-license-ids": {
+      "version": "3.0.16",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz",
+      "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==",
+      "dev": true
+    },
     "node_modules/split-string": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@@ -7498,6 +7898,16 @@
         "node": ">=10.12.0"
       }
     },
+    "node_modules/validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "dev": true,
+      "dependencies": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
     "node_modules/vite": {
       "version": "5.0.12",
       "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz",
diff --git a/web/package.json b/web/package.json
index fa7934d1a7..9e4ccf460a 100644
--- a/web/package.json
+++ b/web/package.json
@@ -32,13 +32,14 @@
     "@types/justified-layout": "^4.1.0",
     "@types/lodash-es": "^4.17.6",
     "@types/luxon": "^3.2.0",
-    "@typescript-eslint/eslint-plugin": "^6.0.0",
-    "@typescript-eslint/parser": "^6.0.0",
+    "@typescript-eslint/eslint-plugin": "^6.4.1",
+    "@typescript-eslint/parser": "^6.4.1",
     "@vitest/coverage-v8": "^1.0.4",
     "autoprefixer": "^10.4.13",
     "eslint": "^8.34.0",
-    "eslint-config-prettier": "^9.0.0",
+    "eslint-config-prettier": "^9.1.0",
     "eslint-plugin-svelte": "^2.30.0",
+    "eslint-plugin-unicorn": "^50.0.1",
     "factory.ts": "^1.3.0",
     "identity-obj-proxy": "^3.0.0",
     "postcss": "^8.4.21",
@@ -49,7 +50,7 @@
     "svelte-preprocess": "^5.0.3",
     "tailwindcss": "^3.2.7",
     "tslib": "^2.5.0",
-    "typescript": "^5.0.0",
+    "typescript": "^5.3.3",
     "vite": "^5.0.10",
     "vitest": "^1.0.4"
   },
diff --git a/web/src/api/api.ts b/web/src/api/api.ts
index 387c754b25..78228aee1f 100644
--- a/web/src/api/api.ts
+++ b/web/src/api/api.ts
@@ -26,7 +26,7 @@ import {
   common,
   configuration,
 } from '@immich/sdk';
-import type { ApiParams } from './types';
+import type { ApiParams as ApiParameters } from './types';
 
 class ImmichApi {
   public activityApi: ActivityApi;
@@ -56,8 +56,8 @@ class ImmichApi {
     return !!this.key;
   }
 
-  constructor(params: configuration.ConfigurationParameters) {
-    this.config = new configuration.Configuration(params);
+  constructor(parameters: configuration.ConfigurationParameters) {
+    this.config = new configuration.Configuration(parameters);
 
     this.activityApi = new ActivityApi(this.config);
     this.albumApi = new AlbumApi(this.config);
@@ -80,17 +80,17 @@ class ImmichApi {
     this.trashApi = new TrashApi(this.config);
   }
 
-  private createUrl(path: string, params?: Record<string, unknown>) {
-    const searchParams = new URLSearchParams();
-    for (const key in params) {
-      const value = params[key];
+  private createUrl(path: string, parameters?: Record<string, unknown>) {
+    const searchParameters = new URLSearchParams();
+    for (const key in parameters) {
+      const value = parameters[key];
       if (value !== undefined && value !== null) {
-        searchParams.set(key, value.toString());
+        searchParameters.set(key, value.toString());
       }
     }
 
     const url = new URL(path, common.DUMMY_BASE_URL);
-    url.search = searchParams.toString();
+    url.search = searchParameters.toString();
 
     return (this.config.basePath || base.BASE_PATH) + common.toPathString(url);
   }
@@ -115,17 +115,17 @@ class ImmichApi {
     this.config.basePath = baseUrl;
   }
 
-  public getAssetFileUrl(...[assetId, isThumb, isWeb]: ApiParams<typeof AssetApiFp, 'serveFile'>) {
+  public getAssetFileUrl(...[assetId, isThumb, isWeb]: ApiParameters<typeof AssetApiFp, 'serveFile'>) {
     const path = `/asset/file/${assetId}`;
     return this.createUrl(path, { isThumb, isWeb, key: this.getKey() });
   }
 
-  public getAssetThumbnailUrl(...[assetId, format]: ApiParams<typeof AssetApiFp, 'getAssetThumbnail'>) {
+  public getAssetThumbnailUrl(...[assetId, format]: ApiParameters<typeof AssetApiFp, 'getAssetThumbnail'>) {
     const path = `/asset/thumbnail/${assetId}`;
     return this.createUrl(path, { format, key: this.getKey() });
   }
 
-  public getProfileImageUrl(...[userId]: ApiParams<typeof UserApiFp, 'getProfileImage'>) {
+  public getProfileImageUrl(...[userId]: ApiParameters<typeof UserApiFp, 'getProfileImage'>) {
     const path = `/user/profile-image/${userId}`;
     return this.createUrl(path);
   }
diff --git a/web/src/api/types.ts b/web/src/api/types.ts
index a830c947d0..96baf2f3aa 100644
--- a/web/src/api/types.ts
+++ b/web/src/api/types.ts
@@ -1,7 +1,7 @@
 import type { Configuration } from '@immich/sdk';
 
 /* eslint-disable @typescript-eslint/no-explicit-any */
-export type ApiFp = (configuration: Configuration) => Record<any, (...args: any) => any>;
+export type ApiFp = (configuration: Configuration) => Record<any, (...arguments_: any) => any>;
 
 export type OmitLast<T extends readonly unknown[]> = T extends readonly [...infer U, any?] ? U : [...T];
 
diff --git a/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte b/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte
index 20444bb29c..dfb93f7033 100644
--- a/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte
+++ b/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte
@@ -14,10 +14,10 @@
   const deleteUser = async () => {
     try {
       const deletedUser = await api.userApi.deleteUser({ id: user.id });
-      if (deletedUser.data.deletedAt != null) {
-        dispatch('success');
-      } else {
+      if (deletedUser.data.deletedAt == undefined) {
         dispatch('fail');
+      } else {
+        dispatch('success');
       }
     } catch (error) {
       handleError(error, 'Unable to delete user');
diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte
index d0904ad94d..75d8ab6b81 100644
--- a/web/src/lib/components/admin-page/jobs/job-tile.svelte
+++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte
@@ -18,7 +18,7 @@
   } from '@mdi/js';
 
   export let title: string;
-  export let subtitle: string | undefined = undefined;
+  export let subtitle: string | undefined;
   export let jobCounts: JobCountsDto;
   export let queueStatus: QueueStatusDto;
   export let allowForceCommand = true;
diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
index 34604e852c..2efd2c1bf6 100644
--- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
+++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
@@ -131,12 +131,13 @@
       jobs[jobId] = data;
 
       switch (jobCommand.command) {
-        case JobCommand.Empty:
+        case JobCommand.Empty: {
           notificationController.show({
             message: `Cleared jobs for: ${title}`,
             type: NotificationType.Info,
           });
           break;
+        }
       }
     } catch (error) {
       handleError(error, `Command '${jobCommand.command}' failed for job: ${title}`);
diff --git a/web/src/lib/components/admin-page/restore-dialoge.svelte b/web/src/lib/components/admin-page/restore-dialoge.svelte
index 19227a3abd..95525ed9d3 100644
--- a/web/src/lib/components/admin-page/restore-dialoge.svelte
+++ b/web/src/lib/components/admin-page/restore-dialoge.svelte
@@ -12,7 +12,7 @@
 
   const restoreUser = async () => {
     const restoredUser = await api.userApi.restoreUser({ id: user.id });
-    if (restoredUser.data.deletedAt == null) {
+    if (restoredUser.data.deletedAt == undefined) {
       dispatch('success');
     } else {
       dispatch('fail');
diff --git a/web/src/lib/components/admin-page/settings/admin-settings.svelte b/web/src/lib/components/admin-page/settings/admin-settings.svelte
index 98e202336a..3f7ddf7614 100644
--- a/web/src/lib/components/admin-page/settings/admin-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/admin-settings.svelte
@@ -19,11 +19,7 @@
   const dispatch = createEventDispatcher<{ save: void }>();
 
   const handleReset = async (detail: SettingsEventType['reset']) => {
-    if (detail.default) {
-      await resetToDefault(detail.configKeys);
-    } else {
-      await reset(detail.configKeys);
-    }
+    await (detail.default ? resetToDefault(detail.configKeys) : reset(detail.configKeys));
   };
 
   const handleSave = async (update: Partial<SystemConfigDto>) => {
@@ -47,7 +43,10 @@
 
   const reset = async (configKeys: Array<keyof SystemConfigDto>) => {
     const { data: resetConfig } = await api.systemConfigApi.getConfig();
-    config = configKeys.reduce((acc, key) => ({ ...acc, [key]: resetConfig[key] }), config);
+
+    for (const key of configKeys) {
+      config = { ...config, [key]: resetConfig[key] };
+    }
 
     notificationController.show({
       message: 'Reset settings to the recent saved settings',
@@ -56,7 +55,9 @@
   };
 
   const resetToDefault = async (configKeys: Array<keyof SystemConfigDto>) => {
-    config = configKeys.reduce((acc, key) => ({ ...acc, [key]: defaultConfig[key] }), config);
+    for (const key of configKeys) {
+      config = { ...config, [key]: defaultConfig[key] };
+    }
 
     notificationController.show({
       message: 'Reset settings to default',
diff --git a/web/src/lib/components/admin-page/settings/setting-checkboxes.svelte b/web/src/lib/components/admin-page/settings/setting-checkboxes.svelte
index ba5d0b2408..506cd042ba 100644
--- a/web/src/lib/components/admin-page/settings/setting-checkboxes.svelte
+++ b/web/src/lib/components/admin-page/settings/setting-checkboxes.svelte
@@ -11,11 +11,7 @@
   export let disabled = false;
 
   function handleCheckboxChange(option: string) {
-    if (value.includes(option)) {
-      value = value.filter((item) => item !== option);
-    } else {
-      value = [...value, option];
-    }
+    value = value.includes(option) ? value.filter((item) => item !== option) : [...value, option];
   }
 </script>
 
diff --git a/web/src/lib/components/admin-page/settings/setting-select.svelte b/web/src/lib/components/admin-page/settings/setting-select.svelte
index 5b080c2328..d79f60a2ac 100644
--- a/web/src/lib/components/admin-page/settings/setting-select.svelte
+++ b/web/src/lib/components/admin-page/settings/setting-select.svelte
@@ -17,7 +17,7 @@
   const handleChange = (e: Event) => {
     value = (e.target as HTMLInputElement).value;
     if (number) {
-      value = parseInt(value);
+      value = Number.parseInt(value);
     }
     dispatch('select', value);
   };
diff --git a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte
index 026a5b4788..5e2d7b781b 100644
--- a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte
@@ -38,7 +38,7 @@
   $: parsedTemplate = () => {
     try {
       return renderTemplate(config.storageTemplate.template);
-    } catch (error) {
+    } catch {
       return 'error';
     }
   };
diff --git a/web/src/lib/components/album-page/__tests__/album-card.spec.ts b/web/src/lib/components/album-page/__tests__/album-card.spec.ts
index 9bdf97a211..bbcd2f5c6a 100644
--- a/web/src/lib/components/album-page/__tests__/album-card.spec.ts
+++ b/web/src/lib/components/album-page/__tests__/album-card.spec.ts
@@ -122,10 +122,10 @@ describe('AlbumCard component', () => {
       const onClickHandler = vi.fn();
       sut.component.$on('showalbumcontextmenu', onClickHandler);
 
-      const contextMenuBtnParent = sut.getByTestId('context-button-parent');
+      const contextMenuButtonParent = sut.getByTestId('context-button-parent');
 
       // Mock getBoundingClientRect to return a bounding rectangle that will result in the expected position
-      contextMenuBtnParent.getBoundingClientRect = () => ({
+      contextMenuButtonParent.getBoundingClientRect = () => ({
         x: 123,
         y: 456,
         width: 0,
@@ -138,7 +138,7 @@ describe('AlbumCard component', () => {
       });
 
       await fireEvent(
-        contextMenuBtnParent,
+        contextMenuButtonParent,
         new MouseEvent('click', {
           clientX: 123,
           clientY: 456,
diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte
index a9e94c4dd8..34b87e8df4 100644
--- a/web/src/lib/components/album-page/album-card.svelte
+++ b/web/src/lib/components/album-page/album-card.svelte
@@ -25,7 +25,7 @@
   const dispatchShowContextMenu = createEventDispatcher<OnShowContextMenu>();
 
   const loadHighQualityThumbnail = async (thubmnailId: string | null) => {
-    if (thubmnailId == null) {
+    if (thubmnailId == undefined) {
       return;
     }
 
diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte
index 23cf5959c5..9777cd1f5b 100644
--- a/web/src/lib/components/album-page/album-viewer.svelte
+++ b/web/src/lib/components/album-page/album-viewer.svelte
@@ -83,11 +83,12 @@
     }
     if (!$showAssetViewer) {
       switch (event.key) {
-        case 'Escape':
+        case 'Escape': {
           if ($isMultiSelectState) {
             assetInteractionStore.clearMultiselect();
           }
           return;
+        }
       }
     }
   };
diff --git a/web/src/lib/components/album-page/share-info-modal.svelte b/web/src/lib/components/album-page/share-info-modal.svelte
index 144c2dcce5..d05760c390 100644
--- a/web/src/lib/components/album-page/share-info-modal.svelte
+++ b/web/src/lib/components/album-page/share-info-modal.svelte
@@ -30,8 +30,8 @@
     try {
       const { data } = await api.userApi.getMyUserInfo();
       currentUser = data;
-    } catch (e) {
-      handleError(e, 'Unable to refresh user');
+    } catch (error) {
+      handleError(error, 'Unable to refresh user');
     }
   });
 
@@ -58,8 +58,8 @@
       dispatch('remove', userId);
       const message = userId === 'me' ? `Left ${album.albumName}` : `Removed ${selectedRemoveUser.name}`;
       notificationController.show({ type: NotificationType.Info, message });
-    } catch (e) {
-      handleError(e, 'Unable to remove user');
+    } catch (error) {
+      handleError(error, 'Unable to remove user');
     } finally {
       selectedRemoveUser = null;
     }
diff --git a/web/src/lib/components/album-page/thumbnail-selection.svelte b/web/src/lib/components/album-page/thumbnail-selection.svelte
index e2c1968b29..f098fbf69a 100644
--- a/web/src/lib/components/album-page/thumbnail-selection.svelte
+++ b/web/src/lib/components/album-page/thumbnail-selection.svelte
@@ -16,11 +16,7 @@
   }>();
 
   $: isSelected = (id: string): boolean | undefined => {
-    if (!selectedThumbnail && album.albumThumbnailAssetId == id) {
-      return true;
-    } else {
-      return selectedThumbnail?.id == id;
-    }
+    return !selectedThumbnail && album.albumThumbnailAssetId == id ? true : selectedThumbnail?.id == id;
   };
 </script>
 
diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/components/album-page/user-selection-modal.svelte
index 8e77df9d95..60e082d439 100644
--- a/web/src/lib/components/album-page/user-selection-modal.svelte
+++ b/web/src/lib/components/album-page/user-selection-modal.svelte
@@ -28,9 +28,9 @@
     users = data.filter((user) => !(user.deletedAt || user.id === album.ownerId));
 
     // Remove the existed shared users from the album
-    album.sharedUsers.forEach((sharedUser) => {
+    for (const sharedUser of album.sharedUsers) {
       users = users.filter((user) => user.id !== sharedUser.id);
-    });
+    }
   });
 
   const getSharedLinks = async () => {
@@ -40,11 +40,9 @@
   };
 
   const handleSelect = (user: UserResponseDto) => {
-    if (selectedUsers.includes(user)) {
-      selectedUsers = selectedUsers.filter((selectedUser) => selectedUser.id !== user.id);
-    } else {
-      selectedUsers = [...selectedUsers, user];
-    }
+    selectedUsers = selectedUsers.includes(user)
+      ? selectedUsers.filter((selectedUser) => selectedUser.id !== user.id)
+      : [...selectedUsers, user];
   };
 
   const handleUnselect = (user: UserResponseDto) => {
@@ -122,7 +120,7 @@
         size="sm"
         fullwidth
         rounded="full"
-        disabled={!selectedUsers.length}
+        disabled={selectedUsers.length === 0}
         on:click={() => dispatch('select', selectedUsers)}>Add</Button
       >
     </div>
diff --git a/web/src/lib/components/asset-viewer/activity-viewer.svelte b/web/src/lib/components/asset-viewer/activity-viewer.svelte
index b9c280ff92..723a2fa7fb 100644
--- a/web/src/lib/components/asset-viewer/activity-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/activity-viewer.svelte
@@ -66,7 +66,7 @@
     close: void;
   }>();
 
-  $: showDeleteReaction = Array(reactions.length).fill(false);
+  $: showDeleteReaction = Array.from({ length: reactions.length }).fill(false);
   $: {
     if (innerHeight && activityHeight) {
       divHeight = innerHeight - activityHeight;
@@ -198,7 +198,7 @@
               {/if}
               {#if reaction.user.id === user.id || albumOwnerId === user.id}
                 <div class="flex items-start w-fit pt-[5px]" title="Delete comment">
-                  <button on:click={() => (!showDeleteReaction[index] ? showOptionsMenu(index) : '')}>
+                  <button on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))}>
                     <Icon path={mdiDotsVertical} />
                   </button>
                 </div>
@@ -244,7 +244,7 @@
                 {/if}
                 {#if reaction.user.id === user.id || albumOwnerId === user.id}
                   <div class="flex items-start w-fit" title="Delete like">
-                    <button on:click={() => (!showDeleteReaction[index] ? showOptionsMenu(index) : '')}>
+                    <button on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))}>
                       <Icon path={mdiDotsVertical} />
                     </button>
                   </div>
diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte
index 058b5f42e1..0218ee7754 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte
@@ -145,11 +145,7 @@
           albumId: album.id,
           type: ReactionType.Like,
         });
-        if (data.length > 0) {
-          isLiked = data[0];
-        } else {
-          isLiked = null;
-        }
+        isLiked = data.length > 0 ? data[0] : null;
       } catch (error) {
         handleError(error, "Can't get Favorite");
       }
@@ -238,8 +234,8 @@
     try {
       const { data } = await api.albumApi.getAllAlbums({ assetId: asset.id });
       appearsInAlbums = data;
-    } catch (e) {
-      console.error('Error getting album that asset belong to', e);
+    } catch (error) {
+      console.error('Error getting album that asset belong to', error);
     }
   };
 
@@ -260,40 +256,48 @@
 
     switch (key) {
       case 'a':
-      case 'A':
+      case 'A': {
         if (shiftKey) {
           toggleArchive();
         }
         return;
-      case 'ArrowLeft':
+      }
+      case 'ArrowLeft': {
         navigateAssetBackward();
         return;
-      case 'ArrowRight':
+      }
+      case 'ArrowRight': {
         navigateAssetForward();
         return;
+      }
       case 'd':
-      case 'D':
+      case 'D': {
         if (shiftKey) {
           downloadFile(asset);
         }
         return;
-      case 'Delete':
+      }
+      case 'Delete': {
         trashOrDelete(shiftKey);
         return;
-      case 'Escape':
+      }
+      case 'Escape': {
         if (isShowDeleteConfirmation) {
           isShowDeleteConfirmation = false;
           return;
         }
         closeViewer();
         return;
-      case 'f':
+      }
+      case 'f': {
         toggleFavorite();
         return;
-      case 'i':
+      }
+      case 'i': {
         isShowActivity = false;
         $isShowDetail = !$isShowDetail;
         return;
+      }
     }
   };
 
@@ -383,8 +387,8 @@
         message: 'Moved to trash',
         type: NotificationType.Info,
       });
-    } catch (e) {
-      handleError(e, 'Unable to trash asset');
+    } catch (error) {
+      handleError(error, 'Unable to trash asset');
     }
   };
 
@@ -398,8 +402,8 @@
         message: 'Permanently deleted asset',
         type: NotificationType.Info,
       });
-    } catch (e) {
-      handleError(e, 'Unable to delete asset');
+    } catch (error) {
+      handleError(error, 'Unable to delete asset');
     } finally {
       isShowDeleteConfirmation = false;
     }
@@ -537,11 +541,7 @@
   const handleStackedAssetMouseEvent = (e: CustomEvent<{ isMouseOver: boolean }>, asset: AssetResponseDto) => {
     const { isMouseOver } = e.detail;
 
-    if (isMouseOver) {
-      previewStackedAsset = asset;
-    } else {
-      previewStackedAsset = undefined;
-    }
+    previewStackedAsset = isMouseOver ? asset : undefined;
   };
 
   const handleUnstack = async () => {
diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte
index 30f3ed0cdb..fea5676b54 100644
--- a/web/src/lib/components/asset-viewer/detail-panel.svelte
+++ b/web/src/lib/components/asset-viewer/detail-panel.svelte
@@ -108,10 +108,11 @@
     }
     const ctrl = event.ctrlKey;
     switch (event.key) {
-      case 'Enter':
+      case 'Enter': {
         if (ctrl && event.target === textArea) {
           handleFocusOut();
         }
+      }
     }
   };
 
@@ -222,7 +223,7 @@
           bind:this={textArea}
           class="max-h-[500px]
       w-full resize-none overflow-hidden border-b border-gray-500 bg-transparent text-base text-black outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:text-white dark:focus:border-immich-dark-primary"
-          placeholder={!isOwner ? '' : 'Add a description'}
+          placeholder={isOwner ? 'Add a description' : ''}
           on:focusin={handleFocusIn}
           on:focusout={handleFocusOut}
           on:input={() => autoGrowHeight(textArea)}
diff --git a/web/src/lib/components/asset-viewer/panorama-viewer.svelte b/web/src/lib/components/asset-viewer/panorama-viewer.svelte
index 4013568287..be398ee07b 100644
--- a/web/src/lib/components/asset-viewer/panorama-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/panorama-viewer.svelte
@@ -20,9 +20,9 @@
         dataUrl = URL.createObjectURL(data);
         return dataUrl;
       } else {
-        throw new Error('Invalid data format');
+        throw new TypeError('Invalid data format');
       }
-    } catch (error) {
+    } catch {
       errorMessage = 'Failed to load asset';
       return '';
     }
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte
index 177c5ba151..cf7c16b3d8 100644
--- a/web/src/lib/components/asset-viewer/photo-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte
@@ -20,7 +20,7 @@
   let assetData: string;
   let abortController: AbortController;
   let hasZoomed = false;
-  let copyImageToClipboard: (src: string) => Promise<Blob>;
+  let copyImageToClipboard: (source: string) => Promise<Blob>;
   let canCopyImagesToClipboard: () => boolean;
 
   $: if (imgElement) {
@@ -90,8 +90,8 @@
         message: 'Copied image to clipboard.',
         timeout: 3000,
       });
-    } catch (err) {
-      console.error('Error [photo-viewer]:', err);
+    } catch (error) {
+      console.error('Error [photo-viewer]:', error);
       notificationController.show({
         type: NotificationType.Error,
         message: 'Copying image to clipboard failed.',
diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte
index 1be7e8ad21..c7aa0b6d83 100644
--- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte
+++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte
@@ -2,6 +2,7 @@
   import { onMount, tick } from 'svelte';
   import { fade } from 'svelte/transition';
   import { thumbHashToDataURL } from 'thumbhash';
+  // eslint-disable-next-line unicorn/prefer-node-protocol
   import { Buffer } from 'buffer';
   import { mdiEyeOffOutline } from '@mdi/js';
   import Icon from '$lib/components/elements/icon.svelte';
diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte
index 0cea5be9fb..de540b3208 100644
--- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte
+++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte
@@ -161,7 +161,7 @@
 
         {#if asset.stackCount && showStackedIcon}
           <div
-            class="absolute {asset.type == AssetTypeEnum.Image && asset.livePhotoVideoId == null
+            class="absolute {asset.type == AssetTypeEnum.Image && asset.livePhotoVideoId == undefined
               ? 'top-0 right-0'
               : 'top-7 right-1'} z-20 flex place-items-center gap-1 text-xs font-medium text-white"
           >
diff --git a/web/src/lib/components/elements/dropdown.svelte b/web/src/lib/components/elements/dropdown.svelte
index b69b191f25..57757dafdb 100644
--- a/web/src/lib/components/elements/dropdown.svelte
+++ b/web/src/lib/components/elements/dropdown.svelte
@@ -26,7 +26,7 @@
   export let options: T[];
   export let selectedOption = options[0];
 
-  export let render: (item: T) => string | RenderedOption = (item) => String(item);
+  export let render: (item: T) => string | RenderedOption = String;
 
   type RenderedOption = {
     title: string;
@@ -54,13 +54,15 @@
   const renderOption = (option: T): RenderedOption => {
     const renderedOption = render(option);
     switch (typeof renderedOption) {
-      case 'string':
+      case 'string': {
         return { title: renderedOption };
-      default:
+      }
+      default: {
         return {
           title: renderedOption.title,
           icon: renderedOption.icon,
         };
+      }
     }
   };
 
diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte
index ec1b614332..cb02ce5bb2 100644
--- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte
+++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte
@@ -47,8 +47,8 @@
       img.src = data;
 
       await new Promise<void>((resolve) => {
-        img.onload = () => resolve();
-        img.onerror = () => resolve();
+        img.addEventListener('load', () => resolve());
+        img.addEventListener('error', () => resolve());
       });
 
       image = img;
@@ -56,13 +56,20 @@
     if (image === null) {
       return null;
     }
-    const { boundingBoxX1: x1, boundingBoxX2: x2, boundingBoxY1: y1, boundingBoxY2: y2 } = face;
+    const {
+      boundingBoxX1: x1,
+      boundingBoxX2: x2,
+      boundingBoxY1: y1,
+      boundingBoxY2: y2,
+      imageWidth,
+      imageHeight,
+    } = face;
 
     const coordinates = {
-      x1: (image.naturalWidth / face.imageWidth) * x1,
-      x2: (image.naturalWidth / face.imageWidth) * x2,
-      y1: (image.naturalHeight / face.imageHeight) * y1,
-      y2: (image.naturalHeight / face.imageHeight) * y2,
+      x1: (image.naturalWidth / imageWidth) * x1,
+      x2: (image.naturalWidth / imageWidth) * x2,
+      y1: (image.naturalHeight / imageHeight) * y1,
+      y2: (image.naturalHeight / imageHeight) * y2,
     };
 
     const faceWidth = coordinates.x2 - coordinates.x1;
@@ -72,17 +79,17 @@
     faceImage.src = image.src;
 
     await new Promise((resolve) => {
-      faceImage.onload = resolve;
-      faceImage.onerror = () => resolve(null);
+      faceImage.addEventListener('load', resolve);
+      faceImage.addEventListener('error', () => resolve(null));
     });
 
     const canvas = document.createElement('canvas');
     canvas.width = faceWidth;
     canvas.height = faceHeight;
 
-    const ctx = canvas.getContext('2d');
-    if (ctx) {
-      ctx.drawImage(faceImage, coordinates.x1, coordinates.y1, faceWidth, faceHeight, 0, 0, faceWidth, faceHeight);
+    const context = canvas.getContext('2d');
+    if (context) {
+      context.drawImage(faceImage, coordinates.x1, coordinates.y1, faceWidth, faceHeight, 0, 0, faceWidth, faceHeight);
 
       return canvas.toDataURL();
     } else {
diff --git a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte
index cb5022d23d..ab77a0df03 100644
--- a/web/src/lib/components/faces-page/merge-suggestion-modal.svelte
+++ b/web/src/lib/components/faces-page/merge-suggestion-modal.svelte
@@ -71,7 +71,7 @@
             }}
           >
             <ImageThumbnail
-              border={potentialMergePeople.length !== 0}
+              border={potentialMergePeople.length > 0}
               circle
               shadow
               url={api.getPeopleThumbnailUrl(personMerge2.id)}
diff --git a/web/src/lib/components/faces-page/people-list.svelte b/web/src/lib/components/faces-page/people-list.svelte
index 5ae4bcc775..5794e0c67d 100644
--- a/web/src/lib/components/faces-page/people-list.svelte
+++ b/web/src/lib/components/faces-page/people-list.svelte
@@ -34,10 +34,8 @@
       people = peopleCopy;
       return;
     }
-    if (!force) {
-      if (people.length < maximumLengthSearchPeople && name.startsWith(searchWord)) {
-        return;
-      }
+    if (!force && people.length < maximumLengthSearchPeople && name.startsWith(searchWord)) {
+      return;
     }
 
     const timeout = setTimeout(() => (isSearchingPeople = true), timeBeforeShowLoadingSpinner);
diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte
index 94aba63f5b..04fd47c273 100644
--- a/web/src/lib/components/faces-page/person-side-panel.svelte
+++ b/web/src/lib/components/faces-page/person-side-panel.svelte
@@ -72,8 +72,8 @@
       allPeople = data.people;
       const result = await api.faceApi.getFaces({ id: assetId });
       peopleWithFaces = result.data;
-      selectedPersonToCreate = new Array<string | null>(peopleWithFaces.length);
-      selectedPersonToReassign = new Array<PersonResponseDto | null>(peopleWithFaces.length);
+      selectedPersonToCreate = Array.from({ length: peopleWithFaces.length });
+      selectedPersonToReassign = Array.from({ length: peopleWithFaces.length });
     } catch (error) {
       handleError(error, "Can't get faces");
     } finally {
@@ -106,20 +106,20 @@
       selectedPersonToReassign.filter((person) => person !== null).length;
     if (numberOfChanges > 0) {
       try {
-        for (let i = 0; i < peopleWithFaces.length; i++) {
-          const personId = selectedPersonToReassign[i]?.id;
+        for (const [index, peopleWithFace] of peopleWithFaces.entries()) {
+          const personId = selectedPersonToReassign[index]?.id;
 
           if (personId) {
             await api.faceApi.reassignFacesById({
               id: personId,
-              faceDto: { id: peopleWithFaces[i].id },
+              faceDto: { id: peopleWithFace.id },
             });
-          } else if (selectedPersonToCreate[i]) {
+          } else if (selectedPersonToCreate[index]) {
             const { data } = await api.personApi.createPerson();
             numberOfPersonToCreate.push(data.id);
             await api.faceApi.reassignFacesById({
               id: data.id,
-              faceDto: { id: peopleWithFaces[i].id },
+              faceDto: { id: peopleWithFace.id },
             });
           }
         }
@@ -138,7 +138,7 @@
       clearTimeout(loaderLoadingDoneTimeout);
       dispatch('refresh');
     } else {
-      automaticRefreshTimeout = setTimeout(() => dispatch('refresh'), 15000);
+      automaticRefreshTimeout = setTimeout(() => dispatch('refresh'), 15_000);
     }
   };
 
diff --git a/web/src/lib/components/forms/create-user-form.svelte b/web/src/lib/components/forms/create-user-form.svelte
index 2d3ab93410..b0434648c0 100644
--- a/web/src/lib/components/forms/create-user-form.svelte
+++ b/web/src/lib/components/forms/create-user-form.svelte
@@ -14,7 +14,7 @@
   let confirmPassowrd = '';
 
   let canCreateUser = false;
-  let quotaSize: number | undefined = undefined;
+  let quotaSize: number | undefined;
   let isCreatingUser = false;
 
   $: quotaSizeWarning = quotaSize && convertToBytes(Number(quotaSize), 'GiB') > $serverInfo.diskSizeRaw;
@@ -69,11 +69,10 @@
           error = 'Error create user account';
           isCreatingUser = false;
         }
-      } catch (e) {
-        error = 'Error create user account';
+      } catch (error) {
         isCreatingUser = false;
 
-        console.log('[ERROR] registerUser', e);
+        console.log('[ERROR] registerUser', error);
 
         notificationController.show({
           message: `Error create new user, check console for more detail`,
diff --git a/web/src/lib/components/forms/edit-user-form.svelte b/web/src/lib/components/forms/edit-user-form.svelte
index 218cd427fb..1116b87cf6 100644
--- a/web/src/lib/components/forms/edit-user-form.svelte
+++ b/web/src/lib/components/forms/edit-user-form.svelte
@@ -70,8 +70,8 @@
       if (status == 200) {
         dispatch('resetPasswordSuccess');
       }
-    } catch (e) {
-      console.error('Error reseting user password', e);
+    } catch (error) {
+      console.error('Error reseting user password', error);
       notificationController.show({
         message: 'Error reseting user password, check console for more details',
         type: NotificationType.Error,
diff --git a/web/src/lib/components/forms/library-import-paths-form.svelte b/web/src/lib/components/forms/library-import-paths-form.svelte
index 3d31573499..8659cdcd05 100644
--- a/web/src/lib/components/forms/library-import-paths-form.svelte
+++ b/web/src/lib/components/forms/library-import-paths-form.svelte
@@ -110,7 +110,7 @@
   />
 {/if}
 
-{#if editImportPath != null}
+{#if editImportPath != undefined}
   <LibraryImportPathForm
     title="Edit Import Path"
     submitText="Save"
diff --git a/web/src/lib/components/forms/library-scan-settings-form.svelte b/web/src/lib/components/forms/library-scan-settings-form.svelte
index 1a07839a9d..dcff0bb0fb 100644
--- a/web/src/lib/components/forms/library-scan-settings-form.svelte
+++ b/web/src/lib/components/forms/library-scan-settings-form.svelte
@@ -109,7 +109,7 @@
   />
 {/if}
 
-{#if editExclusionPattern != null}
+{#if editExclusionPattern != undefined}
   <LibraryExclusionPatternForm
     submitText="Save"
     canDelete={true}
diff --git a/web/src/lib/components/forms/login-form.svelte b/web/src/lib/components/forms/login-form.svelte
index e8ad3817bf..eedc3370b8 100644
--- a/web/src/lib/components/forms/login-form.svelte
+++ b/web/src/lib/components/forms/login-form.svelte
@@ -33,9 +33,9 @@
         await oauth.login(window.location);
         dispatch('success');
         return;
-      } catch (e) {
-        console.error('Error [login-form] [oauth.callback]', e);
-        oauthError = (await getServerErrorMessage(e)) || 'Unable to complete OAuth login';
+      } catch (error) {
+        console.error('Error [login-form] [oauth.callback]', error);
+        oauthError = (await getServerErrorMessage(error)) || 'Unable to complete OAuth login';
         oauthLoading = false;
       }
     }
diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte
index 96d56fec88..dbab3dc520 100644
--- a/web/src/lib/components/memory-page/memory-viewer.svelte
+++ b/web/src/lib/components/memory-page/memory-viewer.svelte
@@ -16,7 +16,8 @@
   import { tweened } from 'svelte/motion';
   import { mdiChevronDown, mdiChevronLeft, mdiChevronRight, mdiChevronUp, mdiPause, mdiPlay } from '@mdi/js';
 
-  const parseIndex = (s: string | null, max: number | null) => Math.max(Math.min(parseInt(s ?? '') || 0, max ?? 0), 0);
+  const parseIndex = (s: string | null, max: number | null) =>
+    Math.max(Math.min(Number.parseInt(s ?? '') || 0, max ?? 0), 0);
 
   $: memoryIndex = parseIndex($page.url.searchParams.get(QueryParameter.MEMORY_INDEX), $memoryStore?.length - 1);
   $: assetIndex = parseIndex($page.url.searchParams.get(QueryParameter.ASSET_INDEX), currentMemory?.assets.length - 1);
@@ -114,18 +115,19 @@
         <div class="flex place-content-center place-items-center gap-2 overflow-hidden">
           <CircleIconButton icon={paused ? mdiPlay : mdiPause} forceDark on:click={() => (paused = !paused)} />
 
-          {#each currentMemory.assets as _, i}
+          {#each currentMemory.assets as _, index}
             <button
               class="relative w-full py-2"
-              on:click={() => goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex}&${QueryParameter.ASSET_INDEX}=${i}`)}
+              on:click={() =>
+                goto(`?${QueryParameter.MEMORY_INDEX}=${memoryIndex}&${QueryParameter.ASSET_INDEX}=${index}`)}
             >
               <span class="absolute left-0 h-[2px] w-full bg-gray-500" />
               {#await resetPromise}
-                <span class="absolute left-0 h-[2px] bg-white" style:width={`${i < assetIndex ? 100 : 0}%`} />
+                <span class="absolute left-0 h-[2px] bg-white" style:width={`${index < assetIndex ? 100 : 0}%`} />
               {:then}
                 <span
                   class="absolute left-0 h-[2px] bg-white"
-                  style:width={`${i < assetIndex ? 100 : i > assetIndex ? 0 : $progress * 100}%`}
+                  style:width={`${index < assetIndex ? 100 : index > assetIndex ? 0 : $progress * 100}%`}
                 />
               {/await}
             </button>
diff --git a/web/src/lib/components/photos-page/actions/add-to-album.svelte b/web/src/lib/components/photos-page/actions/add-to-album.svelte
index 8b4bded170..e89e6376d8 100644
--- a/web/src/lib/components/photos-page/actions/add-to-album.svelte
+++ b/web/src/lib/components/photos-page/actions/add-to-album.svelte
@@ -26,7 +26,7 @@
   const handleAddToNewAlbum = (albumName: string) => {
     showAlbumPicker = false;
 
-    const assetIds = Array.from(getAssets()).map((asset) => asset.id);
+    const assetIds = [...getAssets()].map((asset) => asset.id);
     api.albumApi.createAlbum({ createAlbumDto: { albumName, assetIds } }).then((response) => {
       const { id, albumName } = response.data;
 
@@ -43,7 +43,7 @@
 
   const handleAddToAlbum = async (album: AlbumResponseDto) => {
     showAlbumPicker = false;
-    const assetIds = Array.from(getAssets()).map((asset) => asset.id);
+    const assetIds = [...getAssets()].map((asset) => asset.id);
     await addAssetsToAlbum(album.id, assetIds);
     clearSelect();
   };
diff --git a/web/src/lib/components/photos-page/actions/archive-action.svelte b/web/src/lib/components/photos-page/actions/archive-action.svelte
index fc3739c4e6..731856212e 100644
--- a/web/src/lib/components/photos-page/actions/archive-action.svelte
+++ b/web/src/lib/components/photos-page/actions/archive-action.svelte
@@ -28,7 +28,7 @@
     loading = true;
 
     try {
-      const assets = Array.from(getOwnedAssets()).filter((asset) => asset.isArchived !== isArchived);
+      const assets = [...getOwnedAssets()].filter((asset) => asset.isArchived !== isArchived);
       const ids = assets.map(({ id }) => id);
 
       if (ids.length > 0) {
diff --git a/web/src/lib/components/photos-page/actions/asset-job-actions.svelte b/web/src/lib/components/photos-page/actions/asset-job-actions.svelte
index 296197a710..28d683363e 100644
--- a/web/src/lib/components/photos-page/actions/asset-job-actions.svelte
+++ b/web/src/lib/components/photos-page/actions/asset-job-actions.svelte
@@ -16,11 +16,11 @@
 
   const { clearSelect, getOwnedAssets } = getAssetControlContext();
 
-  $: isAllVideos = Array.from(getOwnedAssets()).every((asset) => asset.type === AssetTypeEnum.Video);
+  $: isAllVideos = [...getOwnedAssets()].every((asset) => asset.type === AssetTypeEnum.Video);
 
   const handleRunJob = async (name: AssetJobName) => {
     try {
-      const ids = Array.from(getOwnedAssets()).map(({ id }) => id);
+      const ids = [...getOwnedAssets()].map(({ id }) => id);
       await api.assetApi.runAssetJobs({ assetJobsDto: { assetIds: ids, name } });
       notificationController.show({ message: api.getAssetJobMessage(name), type: NotificationType.Info });
       clearSelect();
diff --git a/web/src/lib/components/photos-page/actions/create-shared-link.svelte b/web/src/lib/components/photos-page/actions/create-shared-link.svelte
index 6e700807af..b3e68d3034 100644
--- a/web/src/lib/components/photos-page/actions/create-shared-link.svelte
+++ b/web/src/lib/components/photos-page/actions/create-shared-link.svelte
@@ -20,7 +20,7 @@
 
 {#if showModal}
   <CreateSharedLinkModal
-    assetIds={Array.from(getAssets()).map(({ id }) => id)}
+    assetIds={[...getAssets()].map(({ id }) => id)}
     on:close={() => (showModal = false)}
     on:escape={escape}
   />
diff --git a/web/src/lib/components/photos-page/actions/delete-assets.svelte b/web/src/lib/components/photos-page/actions/delete-assets.svelte
index ac44514498..667de9682a 100644
--- a/web/src/lib/components/photos-page/actions/delete-assets.svelte
+++ b/web/src/lib/components/photos-page/actions/delete-assets.svelte
@@ -32,9 +32,7 @@
 
   const handleDelete = async () => {
     loading = true;
-    const ids = Array.from(getOwnedAssets())
-      .filter((a) => !a.isExternal)
-      .map((a) => a.id);
+    const ids = [...getOwnedAssets()].filter((a) => !a.isExternal).map((a) => a.id);
     await deleteAssets(force, onAssetDelete, ids);
     clearSelect();
     isShowConfirmation = false;
diff --git a/web/src/lib/components/photos-page/actions/download-action.svelte b/web/src/lib/components/photos-page/actions/download-action.svelte
index f4e7f685ee..3619db950e 100644
--- a/web/src/lib/components/photos-page/actions/download-action.svelte
+++ b/web/src/lib/components/photos-page/actions/download-action.svelte
@@ -11,7 +11,7 @@
   const { getAssets, clearSelect } = getAssetControlContext();
 
   const handleDownloadFiles = async () => {
-    const assets = Array.from(getAssets());
+    const assets = [...getAssets()];
     if (assets.length === 1) {
       clearSelect();
       await downloadFile(assets[0]);
diff --git a/web/src/lib/components/photos-page/actions/favorite-action.svelte b/web/src/lib/components/photos-page/actions/favorite-action.svelte
index 8ca73958a3..2a70a0f476 100644
--- a/web/src/lib/components/photos-page/actions/favorite-action.svelte
+++ b/web/src/lib/components/photos-page/actions/favorite-action.svelte
@@ -28,7 +28,7 @@
     loading = true;
 
     try {
-      const assets = Array.from(getOwnedAssets()).filter((asset) => asset.isFavorite !== isFavorite);
+      const assets = [...getOwnedAssets()].filter((asset) => asset.isFavorite !== isFavorite);
 
       const ids = assets.map(({ id }) => id);
 
diff --git a/web/src/lib/components/photos-page/actions/remove-from-album.svelte b/web/src/lib/components/photos-page/actions/remove-from-album.svelte
index cf9c32818e..48b33719e5 100644
--- a/web/src/lib/components/photos-page/actions/remove-from-album.svelte
+++ b/web/src/lib/components/photos-page/actions/remove-from-album.svelte
@@ -11,7 +11,7 @@
   import { mdiDeleteOutline } from '@mdi/js';
 
   export let album: AlbumResponseDto;
-  export let onRemove: ((assetIds: string[]) => void) | undefined = undefined;
+  export let onRemove: ((assetIds: string[]) => void) | undefined;
   export let menuItem = false;
 
   const { getAssets, clearSelect } = getAssetControlContext();
@@ -20,7 +20,7 @@
 
   const removeFromAlbum = async () => {
     try {
-      const ids = Array.from(getAssets()).map((a) => a.id);
+      const ids = [...getAssets()].map((a) => a.id);
       const { data: results } = await api.albumApi.removeAssetFromAlbum({
         id: album.id,
         bulkIdsDto: { ids },
@@ -38,8 +38,8 @@
       });
 
       clearSelect();
-    } catch (e) {
-      console.error('Error [album-viewer] [removeAssetFromAlbum]', e);
+    } catch (error) {
+      console.error('Error [album-viewer] [removeAssetFromAlbum]', error);
       notificationController.show({
         type: NotificationType.Error,
         message: 'Error removing assets from album, check console for more details',
diff --git a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte
index f37d021c9a..1389cb76d5 100644
--- a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte
+++ b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte
@@ -18,7 +18,7 @@
       const { data: results } = await api.sharedLinkApi.removeSharedLinkAssets({
         id: sharedLink.id,
         assetIdsDto: {
-          assetIds: Array.from(getAssets()).map((asset) => asset.id),
+          assetIds: [...getAssets()].map((asset) => asset.id),
         },
         key: api.getKey(),
       });
diff --git a/web/src/lib/components/photos-page/actions/restore-assets.svelte b/web/src/lib/components/photos-page/actions/restore-assets.svelte
index 4efbbda532..5121be9ce3 100644
--- a/web/src/lib/components/photos-page/actions/restore-assets.svelte
+++ b/web/src/lib/components/photos-page/actions/restore-assets.svelte
@@ -11,7 +11,7 @@
   import { mdiHistory } from '@mdi/js';
   import type { OnRestore } from '$lib/utils/actions';
 
-  export let onRestore: OnRestore | undefined = undefined;
+  export let onRestore: OnRestore | undefined;
 
   const { getAssets, clearSelect } = getAssetControlContext();
 
@@ -21,7 +21,7 @@
     loading = true;
 
     try {
-      const ids = Array.from(getAssets()).map((a) => a.id);
+      const ids = [...getAssets()].map((a) => a.id);
       await api.trashApi.restoreAssets({ bulkIdsDto: { ids } });
       onRestore?.(ids);
 
@@ -31,8 +31,8 @@
       });
 
       clearSelect();
-    } catch (e) {
-      handleError(e, 'Error restoring assets');
+    } catch (error) {
+      handleError(error, 'Error restoring assets');
     } finally {
       loading = false;
     }
diff --git a/web/src/lib/components/photos-page/actions/select-all-assets.svelte b/web/src/lib/components/photos-page/actions/select-all-assets.svelte
index 1cd3e0abab..c14ea37882 100644
--- a/web/src/lib/components/photos-page/actions/select-all-assets.svelte
+++ b/web/src/lib/components/photos-page/actions/select-all-assets.svelte
@@ -28,8 +28,8 @@
       }
 
       selecting = false;
-    } catch (e) {
-      handleError(e, 'Error selecting all assets');
+    } catch (error) {
+      handleError(error, 'Error selecting all assets');
     }
   };
 </script>
diff --git a/web/src/lib/components/photos-page/actions/stack-action.svelte b/web/src/lib/components/photos-page/actions/stack-action.svelte
index ef50f28c89..ceaaec8cb4 100644
--- a/web/src/lib/components/photos-page/actions/stack-action.svelte
+++ b/web/src/lib/components/photos-page/actions/stack-action.svelte
@@ -9,13 +9,13 @@
   import { handleError } from '$lib/utils/handle-error';
   import type { OnStack } from '$lib/utils/actions';
 
-  export let onStack: OnStack | undefined = undefined;
+  export let onStack: OnStack | undefined;
 
   const { clearSelect, getOwnedAssets } = getAssetControlContext();
 
   const handleStack = async () => {
     try {
-      const assets = Array.from(getOwnedAssets());
+      const assets = [...getOwnedAssets()];
       const parent = assets.at(0);
 
       if (parent == undefined) {
@@ -33,7 +33,7 @@
       for (const asset of children) {
         asset.stackParentId = parent?.id;
         // Add grand-children's count to new parent
-        childrenCount += asset.stackCount == null ? 1 : asset.stackCount + 1;
+        childrenCount += asset.stackCount == undefined ? 1 : asset.stackCount + 1;
         // Reset children stack info
         asset.stackCount = null;
         asset.stack = [];
diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte
index 07e759fcd7..cf560dbbec 100644
--- a/web/src/lib/components/photos-page/asset-date-group.svelte
+++ b/web/src/lib/components/photos-page/asset-date-group.svelte
@@ -48,13 +48,16 @@
   $: geometry = (() => {
     const geometry = [];
     for (let group of assetsGroupByDate) {
-      const justifiedLayoutResult = justifiedLayout(group.map(getAssetRatio), {
-        boxSpacing: 2,
-        containerWidth: Math.floor(viewport.width),
-        containerPadding: 0,
-        targetRowHeightTolerance: 0.15,
-        targetRowHeight: 235,
-      });
+      const justifiedLayoutResult = justifiedLayout(
+        group.map((assetGroup) => getAssetRatio(assetGroup)),
+        {
+          boxSpacing: 2,
+          containerWidth: Math.floor(viewport.width),
+          containerPadding: 0,
+          targetRowHeightTolerance: 0.15,
+          targetRowHeight: 235,
+        },
+      );
       geometry.push({
         ...justifiedLayoutResult,
         containerWidth: calculateWidth(justifiedLayoutResult.boxes),
diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte
index 53bc5440d4..059ce61ed3 100644
--- a/web/src/lib/components/photos-page/asset-grid.svelte
+++ b/web/src/lib/components/photos-page/asset-grid.svelte
@@ -44,9 +44,7 @@
 
   $: timelineY = element?.scrollTop || 0;
   $: isEmpty = $assetStore.initialized && $assetStore.buckets.length === 0;
-  $: idsSelectedAssets = Array.from($selectedAssets)
-    .filter((a) => !a.isExternal)
-    .map((a) => a.id);
+  $: idsSelectedAssets = [...$selectedAssets].filter((a) => !a.isExternal).map((a) => a.id);
 
   const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
   const dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>();
@@ -86,20 +84,23 @@
 
     if (!$showAssetViewer) {
       switch (key) {
-        case 'Escape':
+        case 'Escape': {
           dispatch('escape');
           return;
-        case '?':
+        }
+        case '?': {
           if (event.shiftKey) {
             event.preventDefault();
             showShortcuts = !showShortcuts;
           }
           return;
-        case '/':
+        }
+        case '/': {
           event.preventDefault();
           goto(AppRoute.EXPLORE);
           return;
-        case 'Delete':
+        }
+        case 'Delete': {
           if ($isMultiSelectState) {
             let force = false;
             if (shiftKey || !isTrashEnabled) {
@@ -113,6 +114,7 @@
             trashOrDelete(force);
           }
           return;
+        }
       }
     }
   };
@@ -124,8 +126,8 @@
   };
 
   function intersectedHandler(event: CustomEvent) {
-    const el = event.detail.container as HTMLElement;
-    const target = el.firstChild as HTMLElement;
+    const element_ = event.detail.container as HTMLElement;
+    const target = element_.firstChild as HTMLElement;
     if (target) {
       const bucketDate = target.id.split('_')[1];
       assetStore.loadBucket(bucketDate, event.detail.position);
@@ -160,24 +162,27 @@
     switch (action) {
       case removeAction:
       case AssetAction.TRASH:
-      case AssetAction.DELETE:
+      case AssetAction.DELETE: {
         // find the next asset to show or close the viewer
         (await handleNext()) || (await handlePrevious()) || handleClose();
 
         // delete after find the next one
         assetStore.removeAsset(asset.id);
         break;
+      }
 
       case AssetAction.ARCHIVE:
       case AssetAction.UNARCHIVE:
       case AssetAction.FAVORITE:
-      case AssetAction.UNFAVORITE:
+      case AssetAction.UNFAVORITE: {
         assetStore.updateAsset(asset);
         break;
+      }
 
-      case AssetAction.ADD:
+      case AssetAction.ADD: {
         assetStore.addAsset(asset);
         break;
+      }
     }
   };
 
@@ -392,7 +397,7 @@
     <div class="mt-8 animate-pulse">
       <div class="mb-2 h-4 w-24 rounded-full bg-immich-primary/20 dark:bg-immich-dark-primary/20" />
       <div class="flex w-[120%] flex-wrap">
-        {#each Array(100) as _}
+        {#each Array.from({ length: 100 }) as _}
           <div class="m-[1px] h-[10em] w-[16em] bg-immich-primary/20 dark:bg-immich-dark-primary/20" />
         {/each}
       </div>
diff --git a/web/src/lib/components/photos-page/asset-select-control-bar.svelte b/web/src/lib/components/photos-page/asset-select-control-bar.svelte
index cb9def8ab5..8410f34aa9 100644
--- a/web/src/lib/components/photos-page/asset-select-control-bar.svelte
+++ b/web/src/lib/components/photos-page/asset-select-control-bar.svelte
@@ -25,7 +25,7 @@
   setContext({
     getAssets: () => assets,
     getOwnedAssets: () =>
-      ownerId !== undefined ? new Set(Array.from(assets).filter((asset) => asset.ownerId === ownerId)) : assets,
+      ownerId === undefined ? assets : new Set([...assets].filter((asset) => asset.ownerId === ownerId)),
     clearSelect,
   });
 </script>
diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte
index 5ddab9eaa3..be48dced22 100644
--- a/web/src/lib/components/photos-page/memory-lane.svelte
+++ b/web/src/lib/components/photos-page/memory-lane.svelte
@@ -69,10 +69,10 @@
     {/if}
 
     <div class="inline-block" bind:offsetWidth={innerWidth}>
-      {#each $memoryStore as memory, i (memory.title)}
+      {#each $memoryStore as memory, index (memory.title)}
         <button
           class="memory-card relative mr-8 inline-block aspect-video h-[215px] rounded-xl"
-          on:click={() => goto(`${AppRoute.MEMORY}?${QueryParameter.MEMORY_INDEX}=${i}`)}
+          on:click={() => goto(`${AppRoute.MEMORY}?${QueryParameter.MEMORY_INDEX}=${index}`)}
         >
           <img
             class="h-full w-full rounded-xl object-cover"
diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte
index b966144c21..045a58ca79 100644
--- a/web/src/lib/components/share-page/individual-shared-viewer.svelte
+++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte
@@ -38,11 +38,9 @@
   const handleUploadAssets = async (files: File[] = []) => {
     try {
       let results: (string | undefined)[] = [];
-      if (!files || files.length === 0 || !Array.isArray(files)) {
-        results = await openFileUploadDialog(undefined);
-      } else {
-        results = await fileUploadHandler(files, undefined);
-      }
+      results = await (!files || files.length === 0 || !Array.isArray(files)
+        ? openFileUploadDialog()
+        : fileUploadHandler(files));
       const { data } = await api.sharedLinkApi.addSharedLinkAssets({
         id: sharedLink.id,
         assetIdsDto: {
@@ -57,8 +55,8 @@
         message: `Added ${added} assets`,
         type: NotificationType.Info,
       });
-    } catch (e) {
-      await handleError(e, 'Unable to add assets to shared link');
+    } catch (error) {
+      await handleError(error, 'Unable to add assets to shared link');
     }
   };
 
diff --git a/web/src/lib/components/shared-components/album-selection-modal.svelte b/web/src/lib/components/shared-components/album-selection-modal.svelte
index 77dd753c25..188bdd5c06 100644
--- a/web/src/lib/components/shared-components/album-selection-modal.svelte
+++ b/web/src/lib/components/shared-components/album-selection-modal.svelte
@@ -30,13 +30,12 @@
   });
 
   $: {
-    if (search.length > 0 && albums.length > 0) {
-      filteredAlbums = albums.filter((album) => {
-        return album.albumName.toLowerCase().includes(search.toLowerCase());
-      });
-    } else {
-      filteredAlbums = albums;
-    }
+    filteredAlbums =
+      search.length > 0 && albums.length > 0
+        ? albums.filter((album) => {
+            return album.albumName.toLowerCase().includes(search.toLowerCase());
+          })
+        : albums;
   }
 
   const handleSelect = (album: AlbumResponseDto) => {
diff --git a/web/src/lib/components/shared-components/base-modal.svelte b/web/src/lib/components/shared-components/base-modal.svelte
index a1aae869f7..09d22646ca 100644
--- a/web/src/lib/components/shared-components/base-modal.svelte
+++ b/web/src/lib/components/shared-components/base-modal.svelte
@@ -18,15 +18,15 @@
     if (browser) {
       const scrollTop = document.documentElement.scrollTop;
       const scrollLeft = document.documentElement.scrollLeft;
-      window.onscroll = function () {
+      window.addEventListener('scroll', function () {
         window.scrollTo(scrollLeft, scrollTop);
-      };
+      });
     }
   });
 
   onDestroy(() => {
     if (browser) {
-      window.onscroll = null;
+      window.addEventListener('scroll', () => {});
     }
   });
 </script>
diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte
index 9af69232b8..2186986c56 100644
--- a/web/src/lib/components/shared-components/change-location.svelte
+++ b/web/src/lib/components/shared-components/change-location.svelte
@@ -29,10 +29,10 @@
   };
 
   const handleConfirm = () => {
-    if (!point) {
-      dispatch('cancel');
-    } else {
+    if (point) {
       dispatch('confirm', point);
+    } else {
+      dispatch('cancel');
     }
   };
 </script>
diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte
index e446c1ff6f..5d85e8bc85 100644
--- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte
+++ b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte
@@ -66,7 +66,7 @@
 
   const handleCreateSharedLink = async () => {
     const expirationTime = getExpirationTimeInMillisecond();
-    const currentTime = new Date().getTime();
+    const currentTime = Date.now();
     const expirationDate = expirationTime ? new Date(currentTime + expirationTime).toISOString() : undefined;
 
     try {
@@ -84,8 +84,8 @@
         },
       });
       sharedLink = makeSharedLinkUrl($serverConfig.externalDomain, data.key);
-    } catch (e) {
-      handleError(e, 'Failed to create shared link');
+    } catch (error) {
+      handleError(error, 'Failed to create shared link');
     }
   };
 
@@ -99,20 +99,27 @@
 
   const getExpirationTimeInMillisecond = () => {
     switch (expirationTime) {
-      case '30 minutes':
+      case '30 minutes': {
         return 30 * 60 * 1000;
-      case '1 hour':
+      }
+      case '1 hour': {
         return 60 * 60 * 1000;
-      case '6 hours':
+      }
+      case '6 hours': {
         return 6 * 60 * 60 * 1000;
-      case '1 day':
+      }
+      case '1 day': {
         return 24 * 60 * 60 * 1000;
-      case '7 days':
+      }
+      case '7 days': {
         return 7 * 24 * 60 * 60 * 1000;
-      case '30 days':
+      }
+      case '30 days': {
         return 30 * 24 * 60 * 60 * 1000;
-      default:
+      }
+      default: {
         return 0;
+      }
     }
   };
 
@@ -123,7 +130,7 @@
 
     try {
       const expirationTime = getExpirationTimeInMillisecond();
-      const currentTime = new Date().getTime();
+      const currentTime = Date.now();
       const expirationDate: string | null = expirationTime
         ? new Date(currentTime + expirationTime).toISOString()
         : null;
@@ -146,8 +153,8 @@
       });
 
       dispatch('close');
-    } catch (e) {
-      handleError(e, 'Failed to edit shared link');
+    } catch (error) {
+      handleError(error, 'Failed to edit shared link');
     }
   };
 </script>
diff --git a/web/src/lib/components/shared-components/empty-placeholder.svelte b/web/src/lib/components/shared-components/empty-placeholder.svelte
index 1ba673181c..fc234aa4da 100644
--- a/web/src/lib/components/shared-components/empty-placeholder.svelte
+++ b/web/src/lib/components/shared-components/empty-placeholder.svelte
@@ -7,7 +7,7 @@
   export let fullWidth = false;
   export let src = empty1Url;
 
-  const noop = () => undefined;
+  const noop = () => {};
 
   $: handler = actionHandler || noop;
   $: width = fullWidth ? 'w-full' : 'w-[50%]';
diff --git a/web/src/lib/components/shared-components/gallery-viewer/asset-selection-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/asset-selection-viewer.svelte
index 36347495a8..1b2f80da80 100644
--- a/web/src/lib/components/shared-components/gallery-viewer/asset-selection-viewer.svelte
+++ b/web/src/lib/components/shared-components/gallery-viewer/asset-selection-viewer.svelte
@@ -17,14 +17,14 @@
 
   const selectAssetHandler = (event: CustomEvent) => {
     const { asset }: { asset: AssetResponseDto } = event.detail;
-    let temp = new Set(selectedAssets);
+    let temporary = new Set(selectedAssets);
     if (selectedAssets.has(asset)) {
-      temp.delete(asset);
+      temporary.delete(asset);
     } else {
-      temp.add(asset);
+      temporary.add(asset);
     }
 
-    selectedAssets = temp;
+    selectedAssets = temporary;
     dispatch('select', { asset, selectedAssets });
   };
 </script>
diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
index 449188582f..669d3ec81f 100644
--- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
+++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
@@ -35,15 +35,15 @@
 
   const selectAssetHandler = (event: CustomEvent) => {
     const { asset }: { asset: AssetResponseDto } = event.detail;
-    let temp = new Set(selectedAssets);
+    let temporary = new Set(selectedAssets);
 
     if (selectedAssets.has(asset)) {
-      temp.delete(asset);
+      temporary.delete(asset);
     } else {
-      temp.add(asset);
+      temporary.add(asset);
     }
 
-    selectedAssets = temp;
+    selectedAssets = temporary;
   };
 
   const navigateAssetForward = () => {
@@ -53,8 +53,8 @@
         selectedAsset = assets[currentViewAssetIndex];
         pushState(selectedAsset.id);
       }
-    } catch (e) {
-      handleError(e, 'Cannot navigate to the next asset');
+    } catch (error) {
+      handleError(error, 'Cannot navigate to the next asset');
     }
   };
 
@@ -65,8 +65,8 @@
         selectedAsset = assets[currentViewAssetIndex];
         pushState(selectedAsset.id);
       }
-    } catch (e) {
-      handleError(e, 'Cannot navigate to previous asset');
+    } catch (error) {
+      handleError(error, 'Cannot navigate to previous asset');
     }
   };
 
diff --git a/web/src/lib/components/shared-components/map/map.svelte b/web/src/lib/components/shared-components/map/map.svelte
index ebbd6797a9..a74a51dc09 100644
--- a/web/src/lib/components/shared-components/map/map.svelte
+++ b/web/src/lib/components/shared-components/map/map.svelte
@@ -35,6 +35,7 @@
   let map: maplibregl.Map;
   let marker: maplibregl.Marker | null = null;
 
+  // eslint-disable-next-line unicorn/prefer-top-level-await
   $: style = (async () => {
     const { data } = await api.systemConfigApi.getMapStyle({
       theme: $mapSettings.allowDarkMode ? $colorTheme.value : Theme.LIGHT,
@@ -60,7 +61,7 @@
     }
 
     const mapSource = map?.getSource('geojson') as GeoJSONSource;
-    mapSource.getClusterLeaves(clusterId, 10000, 0, (error, leaves) => {
+    mapSource.getClusterLeaves(clusterId, 10_000, 0, (error, leaves) => {
       if (error) {
         return;
       }
diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
index 3a56148dd6..8a83c0cb32 100644
--- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
+++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
@@ -30,10 +30,10 @@
   const logOut = async () => {
     resetSavedUser();
     const { data } = await api.authenticationApi.logout();
-    if (!data.redirectUri.startsWith('/')) {
-      window.location.href = data.redirectUri;
-    } else {
+    if (data.redirectUri.startsWith('/')) {
       goto(data.redirectUri);
+    } else {
+      window.location.href = data.redirectUri;
     }
   };
 </script>
diff --git a/web/src/lib/components/shared-components/notification/notification-card.svelte b/web/src/lib/components/shared-components/notification/notification-card.svelte
index 558c80769b..a031b76810 100644
--- a/web/src/lib/components/shared-components/notification/notification-card.svelte
+++ b/web/src/lib/components/shared-components/notification/notification-card.svelte
@@ -59,7 +59,7 @@
     }
   };
 
-  let removeNotificationTimeout: NodeJS.Timeout | undefined = undefined;
+  let removeNotificationTimeout: NodeJS.Timeout | undefined;
 
   onMount(() => {
     removeNotificationTimeout = setTimeout(discard, notificationInfo.timeout);
diff --git a/web/src/lib/components/shared-components/notification/notification.ts b/web/src/lib/components/shared-components/notification/notification.ts
index 815ac89e28..f1a2140461 100644
--- a/web/src/lib/components/shared-components/notification/notification.ts
+++ b/web/src/lib/components/shared-components/notification/notification.ts
@@ -7,7 +7,7 @@ export enum NotificationType {
 }
 
 export class ImmichNotification {
-  id = new Date().getTime() + Math.random();
+  id = Date.now() + Math.random();
   type!: NotificationType;
   message!: string;
   action!: NotificationAction;
diff --git a/web/src/lib/components/shared-components/portal/portal.svelte b/web/src/lib/components/shared-components/portal/portal.svelte
index 431950fd3b..c3dee4212c 100644
--- a/web/src/lib/components/shared-components/portal/portal.svelte
+++ b/web/src/lib/components/shared-components/portal/portal.svelte
@@ -4,21 +4,21 @@
   /**
    * Usage: <div use:portal={'css selector'}> or <div use:portal={document.body}>
    */
-  export function portal(el: HTMLElement, target: HTMLElement | string = 'body') {
-    let targetEl;
+  export function portal(element: HTMLElement, target: HTMLElement | string = 'body') {
+    let targetElement;
     async function update(newTarget: HTMLElement | string) {
       target = newTarget;
       if (typeof target === 'string') {
-        targetEl = document.querySelector(target);
-        if (targetEl === null) {
+        targetElement = document.querySelector(target);
+        if (targetElement === null) {
           await tick();
-          targetEl = document.querySelector(target);
+          targetElement = document.querySelector(target);
         }
-        if (targetEl === null) {
+        if (targetElement === null) {
           throw new Error(`No element found matching css selector: "${target}"`);
         }
       } else if (target instanceof HTMLElement) {
-        targetEl = target;
+        targetElement = target;
       } else {
         throw new TypeError(
           `Unknown portal target type: ${
@@ -26,13 +26,13 @@
           }. Allowed types: string (CSS selector) or HTMLElement.`,
         );
       }
-      targetEl.appendChild(el);
-      el.hidden = false;
+      targetElement.append(element);
+      element.hidden = false;
     }
 
     function destroy() {
-      if (el.parentNode) {
-        el.parentNode.removeChild(el);
+      if (element.parentNode) {
+        element.remove();
       }
     }
 
diff --git a/web/src/lib/components/shared-components/profile-image-cropper.svelte b/web/src/lib/components/shared-components/profile-image-cropper.svelte
index 0bac6ee7f1..6f71898311 100644
--- a/web/src/lib/components/shared-components/profile-image-cropper.svelte
+++ b/web/src/lib/components/shared-components/profile-image-cropper.svelte
@@ -26,18 +26,18 @@
     const canvas = document.createElement('canvas');
     canvas.width = img.width;
     canvas.height = img.height;
-    const ctx = canvas.getContext('2d');
-    if (!ctx) {
+    const context = canvas.getContext('2d');
+    if (!context) {
       throw new Error('Could not get canvas context.');
     }
-    ctx.drawImage(img, 0, 0);
-    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+    context.drawImage(img, 0, 0);
+    const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
     const data = imageData?.data;
     if (!data) {
       throw new Error('Could not get image data.');
     }
-    for (let i = 0; i < data.length; i += 4) {
-      if (data[i + 3] < 255) {
+    for (let index = 0; index < data.length; index += 4) {
+      if (data[index + 3] < 255) {
         return true;
       }
     }
@@ -62,8 +62,8 @@
         message: 'Profile picture set.',
         timeout: 3000,
       });
-    } catch (err) {
-      handleError(err, 'Error setting profile picture.');
+    } catch (error) {
+      handleError(error, 'Error setting profile picture.');
     }
     dispatch('close');
   };
diff --git a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte b/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte
index ad9e205779..bef58fe250 100644
--- a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte
+++ b/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte
@@ -34,7 +34,7 @@
 
   const calculateSegments = (buckets: AssetBucket[]) => {
     let height = 0;
-    let prev: Segment;
+    let previous: Segment;
     return buckets.map((bucket) => {
       const segment = new Segment();
       segment.count = bucket.assets.length;
@@ -42,13 +42,13 @@
       segment.timeGroup = bucket.bucketDate;
       segment.date = fromLocalDateTime(segment.timeGroup);
 
-      if (prev?.date.year !== segment.date.year && height > MIN_YEAR_LABEL_DISTANCE) {
-        prev.hasLabel = true;
+      if (previous?.date.year !== segment.date.year && height > MIN_YEAR_LABEL_DISTANCE) {
+        previous.hasLabel = true;
         height = 0;
       }
 
       height += segment.height;
-      prev = segment;
+      previous = segment;
       return segment;
     });
   };
diff --git a/web/src/lib/components/shared-components/search-bar/search-bar.svelte b/web/src/lib/components/shared-components/search-bar/search-bar.svelte
index 1d26a368fc..bc0b370cb5 100644
--- a/web/src/lib/components/shared-components/search-bar/search-bar.svelte
+++ b/web/src/lib/components/shared-components/search-bar/search-bar.svelte
@@ -26,14 +26,14 @@
     $savedSearchTerms = $savedSearchTerms.filter((item) => item !== value);
     saveSearchTerm(value);
 
-    const params = new URLSearchParams({
+    const parameters = new URLSearchParams({
       q: searchValue,
       smart: smartSearch,
     });
 
     showBigSearchBar = false;
     $isSearchEnabled = false;
-    goto(`${AppRoute.SEARCH}?${params}`, { invalidateAll: true });
+    goto(`${AppRoute.SEARCH}?${parameters}`, { invalidateAll: true });
   }
 
   const clearSearchTerm = (searchTerm: string) => {
@@ -140,7 +140,7 @@
           </div>
         {/if}
 
-        {#each $savedSearchTerms as savedSearchTerm, i (i)}
+        {#each $savedSearchTerms as savedSearchTerm, index (index)}
           <div
             class="flex w-full items-center justify-between text-sm text-black hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-500/10"
           >
diff --git a/web/src/lib/components/shared-components/version-announcement-box.svelte b/web/src/lib/components/shared-components/version-announcement-box.svelte
index b2c535cd65..d0415f54ba 100644
--- a/web/src/lib/components/shared-components/version-announcement-box.svelte
+++ b/web/src/lib/components/shared-components/version-announcement-box.svelte
@@ -26,8 +26,8 @@
       }
 
       showModal = true;
-    } catch (err) {
-      console.error('Error [VersionAnnouncementBox]:', err);
+    } catch (error) {
+      console.error('Error [VersionAnnouncementBox]:', error);
     }
   };
 </script>
diff --git a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte
index 43eafd6b5e..df24ee7d53 100644
--- a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte
+++ b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte
@@ -55,7 +55,7 @@
   };
 
   const isExpired = (expiresAt: string) => {
-    const now = new Date().getTime();
+    const now = Date.now();
     const expiration = new Date(expiresAt).getTime();
 
     return now > expiration;
diff --git a/web/src/lib/components/user-settings-page/device-card.svelte b/web/src/lib/components/user-settings-page/device-card.svelte
index eec3a9e2cd..cb6f39e798 100644
--- a/web/src/lib/components/user-settings-page/device-card.svelte
+++ b/web/src/lib/components/user-settings-page/device-card.svelte
@@ -34,9 +34,9 @@
       <Icon path={mdiAndroid} size="40" />
     {:else if device.deviceOS === 'iOS' || device.deviceOS === 'Mac OS'}
       <Icon path={mdiApple} size="40" />
-    {:else if device.deviceOS.indexOf('Safari') !== -1}
+    {:else if device.deviceOS.includes('Safari')}
       <Icon path={mdiAppleSafari} size="40" />
-    {:else if device.deviceOS.indexOf('Windows') !== -1}
+    {:else if device.deviceOS.includes('Windows')}
       <Icon path={mdiMicrosoftWindows} size="40" />
     {:else if device.deviceOS === 'Linux'}
       <Icon path={mdiLinux} size="40" />
diff --git a/web/src/lib/components/user-settings-page/device-list.svelte b/web/src/lib/components/user-settings-page/device-list.svelte
index 7cef9985a9..b0f1be38c0 100644
--- a/web/src/lib/components/user-settings-page/device-list.svelte
+++ b/web/src/lib/components/user-settings-page/device-list.svelte
@@ -73,9 +73,9 @@
   {#if otherDevices.length > 0}
     <div class="mb-6">
       <h3 class="mb-2 text-xs font-medium text-immich-primary dark:text-immich-dark-primary">OTHER DEVICES</h3>
-      {#each otherDevices as device, i}
+      {#each otherDevices as device, index}
         <DeviceCard {device} on:delete={() => (deleteDevice = device)} />
-        {#if i !== otherDevices.length - 1}
+        {#if index !== otherDevices.length - 1}
           <hr class="my-3" />
         {/if}
       {/each}
diff --git a/web/src/lib/components/user-settings-page/library-list.svelte b/web/src/lib/components/user-settings-page/library-list.svelte
index fd287fde16..ead581ddaf 100644
--- a/web/src/lib/components/user-settings-page/library-list.svelte
+++ b/web/src/lib/components/user-settings-page/library-list.svelte
@@ -56,8 +56,8 @@
     updateLibraryIndex = null;
     showContextMenu = false;
 
-    for (let i = 0; i < dropdownOpen.length; i++) {
-      dropdownOpen[i] = false;
+    for (let index = 0; index < dropdownOpen.length; index++) {
+      dropdownOpen[index] = false;
     }
   };
 
@@ -87,9 +87,9 @@
 
     dropdownOpen.length = libraries.length;
 
-    for (let i = 0; i < libraries.length; i++) {
-      await refreshStats(i);
-      dropdownOpen[i] = false;
+    for (let index = 0; index < libraries.length; index++) {
+      await refreshStats(index);
+      dropdownOpen[index] = false;
     }
   }
 
diff --git a/web/src/lib/components/user-settings-page/partner-selection-modal.svelte b/web/src/lib/components/user-settings-page/partner-selection-modal.svelte
index 2d9600646f..09beeb78c5 100644
--- a/web/src/lib/components/user-settings-page/partner-selection-modal.svelte
+++ b/web/src/lib/components/user-settings-page/partner-selection-modal.svelte
@@ -22,16 +22,14 @@
 
     // exclude partners from the list of users available for selection
     const { data: partners } = await api.partnerApi.getPartners({ direction: 'shared-by' });
-    const partnerIds = partners.map((partner) => partner.id);
-    availableUsers = users.filter((user) => !partnerIds.includes(user.id));
+    const partnerIds = new Set(partners.map((partner) => partner.id));
+    availableUsers = users.filter((user) => !partnerIds.has(user.id));
   });
 
   const selectUser = (user: UserResponseDto) => {
-    if (selectedUsers.includes(user)) {
-      selectedUsers = selectedUsers.filter((selectedUser) => selectedUser.id !== user.id);
-    } else {
-      selectedUsers = [...selectedUsers, user];
-    }
+    selectedUsers = selectedUsers.includes(user)
+      ? selectedUsers.filter((selectedUser) => selectedUser.id !== user.id)
+      : [...selectedUsers, user];
   };
 </script>
 
diff --git a/web/src/lib/components/user-settings-page/user-api-key-list.svelte b/web/src/lib/components/user-settings-page/user-api-key-list.svelte
index 2555554119..90a776cc09 100644
--- a/web/src/lib/components/user-settings-page/user-api-key-list.svelte
+++ b/web/src/lib/components/user-settings-page/user-api-key-list.svelte
@@ -129,11 +129,13 @@
           </tr>
         </thead>
         <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
-          {#each keys as key, i}
+          {#each keys as key, index}
             {#key key.id}
               <tr
                 class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
-                  i % 2 == 0 ? 'bg-immich-gray dark:bg-immich-dark-gray/75' : 'bg-immich-bg dark:bg-immich-dark-gray/50'
+                  index % 2 == 0
+                    ? 'bg-immich-gray dark:bg-immich-dark-gray/75'
+                    : 'bg-immich-bg dark:bg-immich-dark-gray/50'
                 }`}
               >
                 <td class="w-1/3 text-ellipsis px-4 text-sm">{key.name}</td>
diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts
index 6d4fe6db4f..00c9195608 100644
--- a/web/src/lib/stores/assets.store.ts
+++ b/web/src/lib/stores/assets.store.ts
@@ -90,7 +90,7 @@ export class AssetStore {
     setTimeout(() => {
       this.pendingChanges.push(...changes);
       this.processPendingChanges();
-    }, 1_000);
+    }, 1000);
   }
 
   connect() {
@@ -124,19 +124,22 @@ export class AssetStore {
   processPendingChanges = throttle(() => {
     for (const { type, value } of this.pendingChanges) {
       switch (type) {
-        case 'add':
+        case 'add': {
           this.addAsset(value);
           break;
+        }
 
-        case 'trash':
+        case 'trash': {
           if (!this.options.isTrashed) {
             this.removeAsset(value);
           }
           break;
+        }
 
-        case 'delete':
+        case 'delete': {
           this.removeAsset(value);
           break;
+        }
       }
     }
 
@@ -174,7 +177,7 @@ export class AssetStore {
       };
     });
 
-    this.timelineHeight = this.buckets.reduce((acc, b) => acc + b.bucketHeight, 0);
+    this.timelineHeight = this.buckets.reduce((accumulator, b) => accumulator + b.bucketHeight, 0);
 
     this.emit(false);
 
@@ -199,7 +202,7 @@ export class AssetStore {
 
       bucket.position = position;
 
-      if (bucket.assets.length !== 0) {
+      if (bucket.assets.length > 0) {
         this.emit(false);
         return;
       }
@@ -324,7 +327,9 @@ export class AssetStore {
   }
 
   async getRandomAsset(): Promise<AssetResponseDto | null> {
-    let index = Math.floor(Math.random() * this.buckets.reduce((acc, bucket) => acc + bucket.bucketCount, 0));
+    let index = Math.floor(
+      Math.random() * this.buckets.reduce((accumulator, bucket) => accumulator + bucket.bucketCount, 0),
+    );
     for (const bucket of this.buckets) {
       if (index < bucket.bucketCount) {
         await this.loadBucket(bucket.bucketDate, BucketPosition.Unknown);
@@ -356,17 +361,17 @@ export class AssetStore {
   }
 
   removeAsset(id: string) {
-    for (let i = 0; i < this.buckets.length; i++) {
-      const bucket = this.buckets[i];
-      for (let j = 0; j < bucket.assets.length; j++) {
-        const asset = bucket.assets[j];
+    for (let index = 0; index < this.buckets.length; index++) {
+      const bucket = this.buckets[index];
+      for (let index_ = 0; index_ < bucket.assets.length; index_++) {
+        const asset = bucket.assets[index_];
         if (asset.id !== id) {
           continue;
         }
 
-        bucket.assets.splice(j, 1);
+        bucket.assets.splice(index_, 1);
         if (bucket.assets.length === 0) {
-          this.buckets.splice(i, 1);
+          this.buckets.splice(index, 1);
         }
 
         this.emit(true);
@@ -422,14 +427,14 @@ export class AssetStore {
       this.assets = this.buckets.flatMap(({ assets }) => assets);
 
       const assetToBucket: Record<string, AssetLookup> = {};
-      for (let i = 0; i < this.buckets.length; i++) {
-        const bucket = this.buckets[i];
-        if (bucket.assets.length !== 0) {
+      for (let index = 0; index < this.buckets.length; index++) {
+        const bucket = this.buckets[index];
+        if (bucket.assets.length > 0) {
           bucket.bucketCount = bucket.assets.length;
         }
-        for (let j = 0; j < bucket.assets.length; j++) {
-          const asset = bucket.assets[j];
-          assetToBucket[asset.id] = { bucket, bucketIndex: i, assetIndex: j };
+        for (let index_ = 0; index_ < bucket.assets.length; index_++) {
+          const asset = bucket.assets[index_];
+          assetToBucket[asset.id] = { bucket, bucketIndex: index, assetIndex: index_ };
         }
       }
       this.assetToBucket = assetToBucket;
diff --git a/web/src/lib/stores/download.ts b/web/src/lib/stores/download.ts
index 7dd13b18cf..a37b351b44 100644
--- a/web/src/lib/stores/download.ts
+++ b/web/src/lib/stores/download.ts
@@ -10,7 +10,7 @@ export interface DownloadProgress {
 export const downloadAssets = writable<Record<string, DownloadProgress>>({});
 
 export const isDownloading = derived(downloadAssets, ($downloadAssets) => {
-  if (Object.keys($downloadAssets).length == 0) {
+  if (Object.keys($downloadAssets).length === 0) {
     return false;
   }
 
diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts
index 6ce3a791d2..09154f5b12 100644
--- a/web/src/lib/stores/preferences.store.ts
+++ b/web/src/lib/stores/preferences.store.ts
@@ -15,10 +15,8 @@ export const handleToggleTheme = () => {
 };
 
 const initTheme = (): ThemeSetting => {
-  if (browser) {
-    if (!window.matchMedia('(prefers-color-scheme: dark)').matches) {
-      return { value: Theme.LIGHT, system: false };
-    }
+  if (browser && !window.matchMedia('(prefers-color-scheme: dark)').matches) {
+    return { value: Theme.LIGHT, system: false };
   }
   return { value: Theme.DARK, system: false };
 };
@@ -30,13 +28,9 @@ export const colorTheme = persisted<ThemeSetting>('color-theme', initialTheme, {
   serializer: {
     parse: (text: string): ThemeSetting => {
       const parsedText: ThemeSetting = JSON.parse(text);
-      if (Object.values(Theme).includes(parsedText.value)) {
-        return parsedText;
-      } else {
-        return initTheme();
-      }
+      return Object.values(Theme).includes(parsedText.value) ? parsedText : initTheme();
     },
-    stringify: (obj) => JSON.stringify(obj),
+    stringify: (object) => JSON.stringify(object),
   },
 });
 
@@ -44,7 +38,7 @@ export const colorTheme = persisted<ThemeSetting>('color-theme', initialTheme, {
 export const locale = persisted<string | undefined>('locale', undefined, {
   serializer: {
     parse: (text) => text,
-    stringify: (obj) => obj ?? '',
+    stringify: (object) => object ?? '',
   },
 });
 
diff --git a/web/src/lib/stores/upload.ts b/web/src/lib/stores/upload.ts
index 37733fd9c5..09031a9169 100644
--- a/web/src/lib/stores/upload.ts
+++ b/web/src/lib/stores/upload.ts
@@ -65,8 +65,8 @@ function createUploadStore() {
     });
   };
 
-  const updateAsset = (id: string, partialObj: Partial<UploadAsset>) => {
-    updateAssetMap(id, (v) => ({ ...v, ...partialObj }));
+  const updateAsset = (id: string, partialObject: Partial<UploadAsset>) => {
+    updateAssetMap(id, (v) => ({ ...v, ...partialObject }));
   };
 
   const removeUploadAsset = (id: string) => {
diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts
index cc8ebdad0c..2062eddb34 100644
--- a/web/src/lib/stores/websocket.ts
+++ b/web/src/lib/stores/websocket.ts
@@ -55,8 +55,8 @@ export const openWebsocketConnection = async () => {
       .on('on_config_update', () => loadConfig())
       .on('on_new_release', (data) => websocketStore.onRelease.set(data))
       .on('error', (e) => console.log('Websocket Error', e));
-  } catch (e) {
-    console.log('Cannot connect to websocket ', e);
+  } catch (error) {
+    console.log('Cannot connect to websocket', error);
   }
 };
 
diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts
index 3148143c0e..28b01252c8 100644
--- a/web/src/lib/utils/actions.ts
+++ b/web/src/lib/utils/actions.ts
@@ -19,7 +19,7 @@ export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids:
       message: `${force ? 'Permanently deleted' : 'Trashed'} ${ids.length} assets`,
       type: NotificationType.Info,
     });
-  } catch (e) {
-    handleError(e, 'Error deleting assets');
+  } catch (error) {
+    handleError(error, 'Error deleting assets');
   }
 };
diff --git a/web/src/lib/utils/asset-utils.spec.ts b/web/src/lib/utils/asset-utils.spec.ts
index 5b68fd6995..8f753ca625 100644
--- a/web/src/lib/utils/asset-utils.spec.ts
+++ b/web/src/lib/utils/asset-utils.spec.ts
@@ -29,7 +29,7 @@ describe('get file extension from filename', () => {
 
 describe('get asset filename', () => {
   it('returns the filename including file extension', () => {
-    [
+    for (const { asset, result } of [
       {
         asset: {
           originalFileName: 'filename',
@@ -51,8 +51,8 @@ describe('get asset filename', () => {
         },
         result: 'new-filename.txt.jpg',
       },
-    ].forEach(({ asset, result }) => {
+    ]) {
       expect(getAssetFilename(asset as AssetResponseDto)).toEqual(result);
-    });
+    }
   });
 });
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts
index f41dd790b4..04bbf0b04d 100644
--- a/web/src/lib/utils/asset-utils.ts
+++ b/web/src/lib/utils/asset-utils.ts
@@ -36,9 +36,9 @@ export const downloadBlob = (data: Blob, filename: string) => {
   anchor.href = url;
   anchor.download = filename;
 
-  document.body.appendChild(anchor);
+  document.body.append(anchor);
   anchor.click();
-  document.body.removeChild(anchor);
+  anchor.remove();
 
   URL.revokeObjectURL(url);
 };
@@ -57,14 +57,14 @@ export const downloadArchive = async (fileName: string, options: DownloadInfoDto
   // TODO: prompt for big download
   // const total = downloadInfo.totalSize;
 
-  for (let i = 0; i < downloadInfo.archives.length; i++) {
-    const archive = downloadInfo.archives[i];
-    const suffix = downloadInfo.archives.length === 1 ? '' : `+${i + 1}`;
+  for (let index = 0; index < downloadInfo.archives.length; index++) {
+    const archive = downloadInfo.archives[index];
+    const suffix = downloadInfo.archives.length === 1 ? '' : `+${index + 1}`;
     const archiveName = fileName.replace('.zip', `${suffix}-${DateTime.now().toFormat('yyyy-LL-dd-HH-mm-ss')}.zip`);
 
     let downloadKey = `${archiveName} `;
     if (downloadInfo.archives.length > 1) {
-      downloadKey = `${archiveName} (${i + 1}/${downloadInfo.archives.length})`;
+      downloadKey = `${archiveName} (${index + 1}/${downloadInfo.archives.length})`;
     }
 
     const abort = new AbortController();
@@ -81,12 +81,12 @@ export const downloadArchive = async (fileName: string, options: DownloadInfoDto
       );
 
       downloadBlob(data, archiveName);
-    } catch (e) {
-      handleError(e, 'Unable to download files');
+    } catch (error) {
+      handleError(error, 'Unable to download files');
       downloadManager.clear(downloadKey);
       return;
     } finally {
-      setTimeout(() => downloadManager.clear(downloadKey), 5_000);
+      setTimeout(() => downloadManager.clear(downloadKey), 5000);
     }
   }
 };
@@ -140,11 +140,11 @@ export const downloadFile = async (asset: AssetResponseDto) => {
       });
 
       downloadBlob(data, filename);
-    } catch (e) {
-      handleError(e, `Error downloading ${filename}`);
+    } catch (error) {
+      handleError(error, `Error downloading ${filename}`);
       downloadManager.clear(downloadKey);
     } finally {
-      setTimeout(() => downloadManager.clear(downloadKey), 5_000);
+      setTimeout(() => downloadManager.clear(downloadKey), 5000);
     }
   }
 };
@@ -155,7 +155,7 @@ export const downloadFile = async (asset: AssetResponseDto) => {
  */
 export function getFilenameExtension(filename: string): string {
   const lastIndex = Math.max(0, filename.lastIndexOf('.'));
-  const startIndex = (lastIndex || Infinity) + 1;
+  const startIndex = (lastIndex || Number.POSITIVE_INFINITY) + 1;
   return filename.slice(startIndex).toLowerCase();
 }
 
@@ -182,10 +182,8 @@ export function getAssetRatio(asset: AssetResponseDto) {
   let height = asset.exifInfo?.exifImageHeight || 235;
   let width = asset.exifInfo?.exifImageWidth || 235;
   const orientation = Number(asset.exifInfo?.orientation);
-  if (orientation) {
-    if (isRotated90CW(orientation) || isRotated270CW(orientation)) {
-      [width, height] = [height, width];
-    }
+  if (orientation && (isRotated90CW(orientation) || isRotated270CW(orientation))) {
+    [width, height] = [height, width];
   }
   return { width, height };
 }
@@ -204,21 +202,22 @@ export function isWebCompatibleImage(asset: AssetResponseDto): boolean {
 
 export const getAssetType = (type: AssetTypeEnum) => {
   switch (type) {
-    case 'IMAGE':
+    case 'IMAGE': {
       return 'Photo';
-    case 'VIDEO':
+    }
+    case 'VIDEO': {
       return 'Video';
-    default:
+    }
+    default: {
       return 'Asset';
+    }
   }
 };
 
 export const getSelectedAssets = (assets: Set<AssetResponseDto>, user: UserResponseDto | null): string[] => {
-  const ids = Array.from(assets)
-    .filter((a) => !a.isExternal && user && a.ownerId === user.id)
-    .map((a) => a.id);
+  const ids = [...assets].filter((a) => !a.isExternal && user && a.ownerId === user.id).map((a) => a.id);
 
-  const numberOfIssues = Array.from(assets).filter((a) => a.isExternal || (user && a.ownerId !== user.id)).length;
+  const numberOfIssues = [...assets].filter((a) => a.isExternal || (user && a.ownerId !== user.id)).length;
   if (numberOfIssues > 0) {
     notificationController.show({
       message: `Can't change metadata of ${numberOfIssues} asset${numberOfIssues > 1 ? 's' : ''}`,
diff --git a/web/src/lib/utils/byte-converter.ts b/web/src/lib/utils/byte-converter.ts
index 15d775dc1d..9fc5eb6471 100644
--- a/web/src/lib/utils/byte-converter.ts
+++ b/web/src/lib/utils/byte-converter.ts
@@ -11,7 +11,7 @@ export function convertToBytes(size: number, unit: string): number {
   let bytes = 0;
 
   if (unit === 'GiB') {
-    bytes = size * 1073741824;
+    bytes = size * 1_073_741_824;
   }
 
   return bytes;
@@ -30,7 +30,7 @@ export function convertFromBytes(bytes: number, unit: string): number {
   let size = 0;
 
   if (unit === 'GiB') {
-    size = bytes / 1073741824;
+    size = bytes / 1_073_741_824;
   }
 
   return size;
diff --git a/web/src/lib/utils/byte-units.ts b/web/src/lib/utils/byte-units.ts
index 4a41a23bc5..c30fede2c7 100644
--- a/web/src/lib/utils/byte-units.ts
+++ b/web/src/lib/utils/byte-units.ts
@@ -22,7 +22,7 @@ export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, stri
     }
   }
 
-  remainder = parseFloat(remainder.toFixed(maxPrecision));
+  remainder = Number.parseFloat(remainder.toFixed(maxPrecision));
 
   return [remainder, units[magnitude]];
 }
diff --git a/web/src/lib/utils/context-menu.ts b/web/src/lib/utils/context-menu.ts
index 364beeaf82..e4c26d962f 100644
--- a/web/src/lib/utils/context-menu.ts
+++ b/web/src/lib/utils/context-menu.ts
@@ -5,12 +5,15 @@ export const getContextMenuPosition = (event: MouseEvent, align: Align = 'middle
   const box = ((currentTarget || target) as HTMLElement)?.getBoundingClientRect();
   if (box) {
     switch (align) {
-      case 'middle':
+      case 'middle': {
         return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
-      case 'top-left':
+      }
+      case 'top-left': {
         return { x: box.x, y: box.y };
-      case 'top-right':
+      }
+      case 'top-right': {
         return { x: box.x + box.width, y: box.y };
+      }
     }
   }
 
diff --git a/web/src/lib/utils/executor-queue.ts b/web/src/lib/utils/executor-queue.ts
index 0816befd6d..0744427cfc 100644
--- a/web/src/lib/utils/executor-queue.ts
+++ b/web/src/lib/utils/executor-queue.ts
@@ -26,7 +26,9 @@ export class ExecutorQueue {
 
     const v = concurrency - this.running;
     if (v > 0) {
-      [...new Array(this._concurrency)].forEach(() => this.tryRun());
+      for (let i = 0; i < v; i++) {
+        this.tryRun();
+      }
     }
   }
 
@@ -38,8 +40,8 @@ export class ExecutorQueue {
           this.running++;
           const result = task();
           resolve(await result);
-        } catch (e) {
-          reject(e);
+        } catch (error) {
+          reject(error);
         } finally {
           this.taskFinished();
         }
diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts
index 5e47b156f0..2e039abbd4 100644
--- a/web/src/lib/utils/file-uploader.ts
+++ b/web/src/lib/utils/file-uploader.ts
@@ -17,7 +17,7 @@ const getExtensions = async () => {
   return _extensions;
 };
 
-export const openFileUploadDialog = async (albumId: string | undefined = undefined) => {
+export const openFileUploadDialog = async (albumId?: string | undefined) => {
   const extensions = await getExtensions();
 
   return new Promise<(string | undefined)[]>((resolve, reject) => {
@@ -27,7 +27,7 @@ export const openFileUploadDialog = async (albumId: string | undefined = undefin
       fileSelector.type = 'file';
       fileSelector.multiple = true;
       fileSelector.accept = extensions.join(',');
-      fileSelector.onchange = async (e: Event) => {
+      fileSelector.addEventListener('change', async (e: Event) => {
         const target = e.target as HTMLInputElement;
         if (!target.files) {
           return;
@@ -35,12 +35,12 @@ export const openFileUploadDialog = async (albumId: string | undefined = undefin
         const files = Array.from(target.files);
 
         resolve(fileUploadHandler(files, albumId));
-      };
+      });
 
       fileSelector.click();
-    } catch (e) {
-      console.log('Error selecting file', e);
-      reject(e);
+    } catch (error) {
+      console.log('Error selecting file', error);
+      reject(error);
     }
   });
 };
@@ -50,7 +50,7 @@ export const fileUploadHandler = async (files: File[], albumId: string | undefin
   const promises = [];
   for (const file of files) {
     const name = file.name.toLowerCase();
-    if (extensions.some((ext) => name.endsWith(ext))) {
+    if (extensions.some((extension) => name.endsWith(extension))) {
       uploadAssetsStore.addNewUploadAsset({ id: getDeviceAssetId(file), file, albumId });
       promises.push(uploadExecutionQueue.addTask(() => fileUploader(file, albumId)));
     }
diff --git a/web/src/lib/utils/person.ts b/web/src/lib/utils/person.ts
index e26d6c6936..e608a06a0c 100644
--- a/web/src/lib/utils/person.ts
+++ b/web/src/lib/utils/person.ts
@@ -6,27 +6,21 @@ export const searchNameLocal = (
   slice: number,
   personId?: string,
 ): PersonResponseDto[] => {
-  return name.indexOf(' ') >= 0
+  return name.includes(' ')
     ? people
         .filter((person: PersonResponseDto) => {
-          if (personId) {
-            return person.name.toLowerCase().startsWith(name.toLowerCase()) && person.id !== personId;
-          } else {
-            return person.name.toLowerCase().startsWith(name.toLowerCase());
-          }
+          return personId
+            ? person.name.toLowerCase().startsWith(name.toLowerCase()) && person.id !== personId
+            : person.name.toLowerCase().startsWith(name.toLowerCase());
         })
         .slice(0, slice)
     : people
         .filter((person: PersonResponseDto) => {
           const nameParts = person.name.split(' ');
-          if (personId) {
-            return (
-              nameParts.some((splitName) => splitName.toLowerCase().startsWith(name.toLowerCase())) &&
-              person.id !== personId
-            );
-          } else {
-            return nameParts.some((splitName) => splitName.toLowerCase().startsWith(name.toLowerCase()));
-          }
+          return personId
+            ? nameParts.some((splitName) => splitName.toLowerCase().startsWith(name.toLowerCase())) &&
+                person.id !== personId
+            : nameParts.some((splitName) => splitName.toLowerCase().startsWith(name.toLowerCase()));
         })
         .slice(0, slice);
 };
diff --git a/web/src/lib/utils/time-to-seconds.spec.ts b/web/src/lib/utils/time-to-seconds.spec.ts
index 2b0f31edba..3e6cb0839a 100644
--- a/web/src/lib/utils/time-to-seconds.spec.ts
+++ b/web/src/lib/utils/time-to-seconds.spec.ts
@@ -14,7 +14,7 @@ describe('converting time to seconds', () => {
   });
 
   it('parses hhh:mm:ss.SSS correctly', () => {
-    expect(timeToSeconds('100:02:03.456')).toBeCloseTo(360123.456);
+    expect(timeToSeconds('100:02:03.456')).toBeCloseTo(360_123.456);
   });
 
   it('ignores ignores double milliseconds hh:mm:ss.SSS.SSSSSS', () => {
diff --git a/web/src/routes/(user)/albums/+page.svelte b/web/src/routes/(user)/albums/+page.svelte
index 55f64be50f..77b9efeafb 100644
--- a/web/src/routes/(user)/albums/+page.svelte
+++ b/web/src/routes/(user)/albums/+page.svelte
@@ -227,11 +227,8 @@
   };
 
   const handleChangeListMode = () => {
-    if ($albumViewSettings.view === AlbumViewMode.Cover) {
-      $albumViewSettings.view = AlbumViewMode.List;
-    } else {
-      $albumViewSettings.view = AlbumViewMode.Cover;
-    }
+    $albumViewSettings.view =
+      $albumViewSettings.view === AlbumViewMode.Cover ? AlbumViewMode.List : AlbumViewMode.Cover;
   };
 </script>
 
@@ -285,14 +282,14 @@
       </div>
     </LinkButton>
   </div>
-  {#if $albums.length !== 0}
+  {#if $albums.length > 0}
     <!-- Album Card -->
     {#if $albumViewSettings.view === AlbumViewMode.Cover}
       <div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))]">
-        {#each $albums as album, idx (album.id)}
+        {#each $albums as album, index (album.id)}
           <a data-sveltekit-preload-data="hover" href="{AppRoute.ALBUMS}/{album.id}" animate:flip={{ duration: 200 }}>
             <AlbumCard
-              preload={idx < 20}
+              preload={index < 20}
               {album}
               on:showalbumcontextmenu={(e) => showAlbumContextMenu(e.detail, album)}
             />
diff --git a/web/src/routes/(user)/albums/[albumId]/+page.svelte b/web/src/routes/(user)/albums/[albumId]/+page.svelte
index f15ff6411c..2769aeb806 100644
--- a/web/src/routes/(user)/albums/[albumId]/+page.svelte
+++ b/web/src/routes/(user)/albums/[albumId]/+page.svelte
@@ -111,14 +111,10 @@
   const { selectedAssets: timelineSelected } = timelineInteractionStore;
 
   $: isOwned = $user.id == album.ownerId;
-  $: isAllUserOwned = Array.from($selectedAssets).every((asset) => asset.ownerId === $user.id);
-  $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
+  $: isAllUserOwned = [...$selectedAssets].every((asset) => asset.ownerId === $user.id);
+  $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite);
   $: {
-    if (isShowActivity) {
-      assetGridWidth = globalWidth - (globalWidth < 768 ? 360 : 460);
-    } else {
-      assetGridWidth = globalWidth;
-    }
+    assetGridWidth = isShowActivity ? globalWidth - (globalWidth < 768 ? 360 : 460) : globalWidth;
   }
   $: showActivityStatus =
     album.sharedUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0);
@@ -157,7 +153,7 @@
         message: `Activity is ${album.isActivityEnabled ? 'enabled' : 'disabled'}`,
       });
     } catch (error) {
-      handleError(error, `Can't ${!album.isActivityEnabled ? 'enable' : 'disable'} activity`);
+      handleError(error, `Can't ${album.isActivityEnabled ? 'disable' : 'enable'} activity`);
     }
   };
 
@@ -224,10 +220,11 @@
     }
     const ctrl = event.ctrlKey;
     switch (event.key) {
-      case 'Enter':
+      case 'Enter': {
         if (ctrl && event.target === textArea) {
           textArea.blur();
         }
+      }
     }
   };
 
@@ -302,7 +299,7 @@
   };
 
   const handleAddAssets = async () => {
-    const assetIds = Array.from($timelineSelected).map((asset) => asset.id);
+    const assetIds = [...$timelineSelected].map((asset) => asset.id);
 
     try {
       const { data: results } = await api.albumApi.addAssetsToAlbum({
@@ -352,7 +349,7 @@
       const { data } = await api.albumApi.addUsersToAlbum({
         id: album.id,
         addUsersDto: {
-          sharedUserIds: Array.from(users).map(({ id }) => id),
+          sharedUserIds: [...users].map(({ id }) => id),
         },
       });
 
@@ -373,8 +370,8 @@
     try {
       await refreshAlbum();
       viewMode = album.sharedUsers.length > 1 ? ViewMode.SELECT_USERS : ViewMode.VIEW;
-    } catch (e) {
-      handleError(e, 'Error deleting share users');
+    } catch (error) {
+      handleError(error, 'Error deleting share users');
     }
   };
 
diff --git a/web/src/routes/(user)/albums/__tests__/albums.bloc.spec.ts b/web/src/routes/(user)/albums/__tests__/albums.bloc.spec.ts
index 0aa48d532e..d95cc297fe 100644
--- a/web/src/routes/(user)/albums/__tests__/albums.bloc.spec.ts
+++ b/web/src/routes/(user)/albums/__tests__/albums.bloc.spec.ts
@@ -20,7 +20,9 @@ describe('Albums BLoC', () => {
   afterEach(() => {
     const notifications = get(notificationController.notificationList);
 
-    notifications.forEach((notification) => notificationController.removeNotificationById(notification.id));
+    for (const notification of notifications) {
+      notificationController.removeNotificationById(notification.id);
+    }
   });
 
   it('inits with provided albums', () => {
diff --git a/web/src/routes/(user)/albums/albums.bloc.ts b/web/src/routes/(user)/albums/albums.bloc.ts
index f1860ceeb7..11bea9e11d 100644
--- a/web/src/routes/(user)/albums/albums.bloc.ts
+++ b/web/src/routes/(user)/albums/albums.bloc.ts
@@ -3,10 +3,10 @@ import { notificationController, NotificationType } from '$lib/components/shared
 import { type AlbumResponseDto, api } from '@api';
 import { derived, get, writable } from 'svelte/store';
 
-type AlbumsProps = { albums: AlbumResponseDto[] };
+type AlbumsProperties = { albums: AlbumResponseDto[] };
 
-export const useAlbums = (props: AlbumsProps) => {
-  const albums = writable([...props.albums]);
+export const useAlbums = (properties: AlbumsProperties) => {
+  const albums = writable([...properties.albums]);
   const contextMenuPosition = writable<OnShowContextMenuDetail>({ x: 0, y: 0 });
   const contextMenuTargetAlbum = writable<AlbumResponseDto | undefined>();
   const isShowContextMenu = derived(contextMenuTargetAlbum, ($selectedAlbum) => !!$selectedAlbum);
diff --git a/web/src/routes/(user)/archive/+page.svelte b/web/src/routes/(user)/archive/+page.svelte
index 0d3714a021..6505ea111e 100644
--- a/web/src/routes/(user)/archive/+page.svelte
+++ b/web/src/routes/(user)/archive/+page.svelte
@@ -24,7 +24,7 @@
   const assetInteractionStore = createAssetInteractionStore();
   const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 
-  $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
+  $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite);
 </script>
 
 {#if $isMultiSelectState}
diff --git a/web/src/routes/(user)/favorites/+page.svelte b/web/src/routes/(user)/favorites/+page.svelte
index 4005a5d81c..3f49b95c60 100644
--- a/web/src/routes/(user)/favorites/+page.svelte
+++ b/web/src/routes/(user)/favorites/+page.svelte
@@ -26,7 +26,7 @@
   const assetInteractionStore = createAssetInteractionStore();
   const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 
-  $: isAllArchive = Array.from($selectedAssets).every((asset) => asset.isArchived);
+  $: isAllArchive = [...$selectedAssets].every((asset) => asset.isArchived);
 </script>
 
 <!-- Multiselection mode app bar -->
diff --git a/web/src/routes/(user)/partners/[userId]/+page.svelte b/web/src/routes/(user)/partners/[userId]/+page.svelte
index ef60e7f24e..6d867adab6 100644
--- a/web/src/routes/(user)/partners/[userId]/+page.svelte
+++ b/web/src/routes/(user)/partners/[userId]/+page.svelte
@@ -19,7 +19,7 @@
 
   const assetStore = new AssetStore({ userId: data.partner.id, isArchived: false, withStacked: true });
   const assetInteractionStore = createAssetInteractionStore();
-  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
+  const { isMultiSelectState, selectedAssets, clearMultiselect } = assetInteractionStore;
 
   onDestroy(() => {
     assetInteractionStore.clearMultiselect();
@@ -28,7 +28,7 @@
 
 <main class="grid h-screen bg-immich-bg pt-18 dark:bg-immich-dark-bg">
   {#if $isMultiSelectState}
-    <AssetSelectControlBar assets={$selectedAssets} clearSelect={assetInteractionStore.clearMultiselect}>
+    <AssetSelectControlBar assets={$selectedAssets} clearSelect={clearMultiselect}>
       <CreateSharedLink />
       <AssetSelectContextMenu icon={mdiPlus} title="Add">
         <AddToAlbum />
diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte
index 16a9be0f1a..004742946c 100644
--- a/web/src/routes/(user)/people/+page.svelte
+++ b/web/src/routes/(user)/people/+page.svelte
@@ -90,9 +90,10 @@
       return;
     }
     switch (event.key) {
-      case 'Escape':
+      case 'Escape': {
         handleCloseClick();
         return;
+      }
     }
   };
 
@@ -288,10 +289,8 @@
       }
       return;
     }
-    if (!force) {
-      if (people.length < maximumLengthSearchPeople && searchName.startsWith(searchWord)) {
-        return;
-      }
+    if (!force && people.length < maximumLengthSearchPeople && searchName.startsWith(searchWord)) {
+      return;
     }
 
     const timeout = setTimeout(() => (isSearchingPeople = true), timeBeforeShowLoadingSpinner);
@@ -417,7 +416,7 @@
   </FullScreenModal>
 {/if}
 
-<UserPageLayout title="People" description={countTotalPeople !== 0 ? `(${countTotalPeople.toString()})` : undefined}>
+<UserPageLayout title="People" description={countTotalPeople === 0 ? undefined : `(${countTotalPeople.toString()})`}>
   <svelte:fragment slot="buttons">
     {#if countTotalPeople > 0}
       <div class="flex gap-2 items-center justify-center">
@@ -445,11 +444,11 @@
 
   {#if countVisiblePeople > 0}
     <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-9 gap-1">
-      {#each people as person, idx (person.id)}
+      {#each people as person, index (person.id)}
         {#if !person.isHidden && (searchName ? searchedPeopleLocal.some((searchedPerson) => searchedPerson.id === person.id) : true)}
           <PeopleCard
             {person}
-            preload={idx < 20}
+            preload={index < 20}
             on:change-name={() => handleChangeName(person)}
             on:set-birth-date={() => handleSetBirthDate(person)}
             on:merge-people={() => handleMergePeople(person)}
@@ -519,7 +518,7 @@
     screenHeight={innerHeight}
   >
     <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-9 gap-1">
-      {#each people as person, idx (person.id)}
+      {#each people as person, index (person.id)}
         <button
           class="relative"
           on:click={() => (person.isHidden = !person.isHidden)}
@@ -527,7 +526,7 @@
           on:mouseleave={() => (eyeColorMap[person.id] = 'white')}
         >
           <ImageThumbnail
-            preload={searchName !== '' || idx < 20}
+            preload={searchName !== '' || index < 20}
             bind:hidden={person.isHidden}
             shadow
             url={api.getPeopleThumbnailUrl(person.id)}
diff --git a/web/src/routes/(user)/people/[personId]/+page.svelte b/web/src/routes/(user)/people/[personId]/+page.svelte
index 18fc06db0f..3d96387659 100644
--- a/web/src/routes/(user)/people/[personId]/+page.svelte
+++ b/web/src/routes/(user)/people/[personId]/+page.svelte
@@ -108,14 +108,14 @@
     isSearchingPeople = false;
   };
 
-  $: isAllArchive = Array.from($selectedAssets).every((asset) => asset.isArchived);
-  $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
+  $: isAllArchive = [...$selectedAssets].every((asset) => asset.isArchived);
+  $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite);
   $: $onPersonThumbnail === data.person.id &&
     (thumbnailData = api.getPeopleThumbnailUrl(data.person.id) + `?now=${Date.now()}`);
 
   $: {
     if (people) {
-      suggestedPeople = !name ? [] : searchNameLocal(name, people, 5, data.person.id);
+      suggestedPeople = name ? searchNameLocal(name, people, 5, data.person.id) : [];
     }
   }
 
@@ -158,7 +158,7 @@
   });
 
   const handleUnmerge = () => {
-    $assetStore.removeAssets(Array.from($selectedAssets).map((a) => a.id));
+    $assetStore.removeAssets([...$selectedAssets].map((a) => a.id));
     assetInteractionStore.clearMultiselect();
     viewMode = ViewMode.VIEW_ASSETS;
   };
@@ -352,7 +352,7 @@
 
 {#if viewMode === ViewMode.UNASSIGN_ASSETS}
   <UnMergeFaceSelector
-    assetIds={Array.from($selectedAssets).map((a) => a.id)}
+    assetIds={[...$selectedAssets].map((a) => a.id)}
     personAssets={data.person}
     on:close={() => (viewMode = ViewMode.VIEW_ASSETS)}
     on:confirm={handleUnmerge}
diff --git a/web/src/routes/(user)/photos/+page.svelte b/web/src/routes/(user)/photos/+page.svelte
index 913c4d038e..f1dc364624 100644
--- a/web/src/routes/(user)/photos/+page.svelte
+++ b/web/src/routes/(user)/photos/+page.svelte
@@ -31,7 +31,7 @@
   const assetInteractionStore = createAssetInteractionStore();
   const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 
-  $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
+  $: isAllFavorite = [...$selectedAssets].every((asset) => asset.isFavorite);
 
   const handleEscape = () => {
     if ($showAssetViewer) {
diff --git a/web/src/routes/(user)/search/+page.svelte b/web/src/routes/(user)/search/+page.svelte
index f49c7a0c58..42f9af0f0f 100644
--- a/web/src/routes/(user)/search/+page.svelte
+++ b/web/src/routes/(user)/search/+page.svelte
@@ -56,7 +56,7 @@
     }
     if (!$showAssetViewer) {
       switch (event.key) {
-        case 'Escape':
+        case 'Escape': {
           if (isMultiSelectionMode) {
             selectedAssets = new Set();
             return;
@@ -66,6 +66,7 @@
           }
           $preventRaceConditionSearchBar = false;
           return;
+        }
       }
     }
   };
@@ -96,8 +97,8 @@
 
   let selectedAssets: Set<AssetResponseDto> = new Set();
   $: isMultiSelectionMode = selectedAssets.size > 0;
-  $: isAllArchived = Array.from(selectedAssets).every((asset) => asset.isArchived);
-  $: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite);
+  $: isAllArchived = [...selectedAssets].every((asset) => asset.isArchived);
+  $: isAllFavorite = [...selectedAssets].every((asset) => asset.isFavorite);
   $: searchResultAssets = data.results?.assets.items;
 
   const onAssetDelete = (assetId: string) => {
@@ -140,14 +141,14 @@
 
 <section class="relative mb-12 bg-immich-bg pt-32 dark:bg-immich-dark-bg">
   <section class="immich-scrollbar relative overflow-y-auto">
-    {#if albums && albums.length}
+    {#if albums && albums.length > 0}
       <section>
         <div class="ml-6 text-4xl font-medium text-black/70 dark:text-white/80">ALBUMS</div>
         <div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))]">
-          {#each albums as album, idx (album.id)}
+          {#each albums as album, index (album.id)}
             <a data-sveltekit-preload-data="hover" href={`albums/${album.id}`} animate:flip={{ duration: 200 }}>
               <AlbumCard
-                preload={idx < 20}
+                preload={index < 20}
                 {album}
                 isSharingView={false}
                 showItemCount={false}
diff --git a/web/src/routes/(user)/share/[key]/+page.svelte b/web/src/routes/(user)/share/[key]/+page.svelte
index d45d1b06fc..d7bdb4d1c0 100644
--- a/web/src/routes/(user)/share/[key]/+page.svelte
+++ b/web/src/routes/(user)/share/[key]/+page.svelte
@@ -11,8 +11,8 @@
   import { user } from '$lib/stores/user.store';
 
   export let data: PageData;
-  let { sharedLink, passwordRequired, sharedLinkKey: key } = data;
-  let { title, description } = data.meta;
+  let { sharedLink, passwordRequired, sharedLinkKey: key, meta } = data;
+  let { title, description } = meta;
   let isOwned = $user ? $user.id === sharedLink?.userId : false;
   let password = '';
 
diff --git a/web/src/routes/(user)/share/[key]/+page.ts b/web/src/routes/(user)/share/[key]/+page.ts
index 938e46d149..47b52f1f88 100644
--- a/web/src/routes/(user)/share/[key]/+page.ts
+++ b/web/src/routes/(user)/share/[key]/+page.ts
@@ -1,8 +1,8 @@
 import { getAuthUser } from '$lib/utils/auth';
 import { api, ThumbnailFormat } from '@api';
-import { error } from '@sveltejs/kit';
 import type { AxiosError } from 'axios';
 import type { PageLoad } from './$types';
+import { error as throwError } from '@sveltejs/kit';
 
 export const load = (async ({ params }) => {
   const { key } = params;
@@ -24,10 +24,10 @@ export const load = (async ({ params }) => {
           : '/feature-panel.png',
       },
     };
-  } catch (e) {
+  } catch (error) {
     // handle unauthorized error
     // TODO this doesn't allow for 404 shared links anymore
-    if ((e as AxiosError).response?.status === 401) {
+    if ((error as AxiosError).response?.status === 401) {
       return {
         passwordRequired: true,
         sharedLinkKey: key,
@@ -37,7 +37,7 @@ export const load = (async ({ params }) => {
       };
     }
 
-    error(404, {
+    throwError(404, {
       message: 'Invalid shared link',
     });
   }
diff --git a/web/src/routes/(user)/sharing/+page.svelte b/web/src/routes/(user)/sharing/+page.svelte
index 3471fadca9..c244d5b815 100644
--- a/web/src/routes/(user)/sharing/+page.svelte
+++ b/web/src/routes/(user)/sharing/+page.svelte
@@ -28,13 +28,13 @@
       });
 
       goto(`${AppRoute.ALBUMS}/${newAlbum.id}`);
-    } catch (e) {
+    } catch (error) {
       notificationController.show({
         message: 'Error creating album, check console for more details',
         type: NotificationType.Error,
       });
 
-      console.log('Error [createAlbum] ', e);
+      console.log('Error [createAlbum]', error);
     }
   };
 </script>
@@ -94,9 +94,9 @@
       <div>
         <!-- Share Album List -->
         <div class="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))]">
-          {#each data.sharedAlbums as album, idx (album.id)}
+          {#each data.sharedAlbums as album, index (album.id)}
             <a data-sveltekit-preload-data="hover" href={`albums/${album.id}`} animate:flip={{ duration: 200 }}>
-              <AlbumCard preload={idx < 20} {album} isSharingView showContextMenu={false} />
+              <AlbumCard preload={index < 20} {album} isSharingView showContextMenu={false} />
             </a>
           {/each}
         </div>
diff --git a/web/src/routes/(user)/trash/+page.svelte b/web/src/routes/(user)/trash/+page.svelte
index d5e22a79ca..7cd37e328f 100644
--- a/web/src/routes/(user)/trash/+page.svelte
+++ b/web/src/routes/(user)/trash/+page.svelte
@@ -43,8 +43,8 @@
         message: `Empty trash initiated. Refresh the page to see the changes`,
         type: NotificationType.Info,
       });
-    } catch (e) {
-      handleError(e, 'Error emptying trash');
+    } catch (error) {
+      handleError(error, 'Error emptying trash');
     }
   };
 
@@ -56,8 +56,8 @@
         message: `Restore trash initiated. Refresh the page to see the changes`,
         type: NotificationType.Info,
       });
-    } catch (e) {
-      handleError(e, 'Error restoring trash');
+    } catch (error) {
+      handleError(error, 'Error restoring trash');
     }
   };
 </script>
diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte
index b7cdcb0dad..e71278fd42 100644
--- a/web/src/routes/admin/jobs-status/+page.svelte
+++ b/web/src/routes/admin/jobs-status/+page.svelte
@@ -22,7 +22,7 @@
 
   onMount(async () => {
     await load();
-    timer = setInterval(load, 5_000);
+    timer = setInterval(load, 5000);
   });
 
   onDestroy(() => {
diff --git a/web/src/routes/admin/repair/+page.svelte b/web/src/routes/admin/repair/+page.svelte
index 4295c60941..35666aa202 100644
--- a/web/src/routes/admin/repair/+page.svelte
+++ b/web/src/routes/admin/repair/+page.svelte
@@ -44,7 +44,7 @@
       downloadManager.add(downloadKey, blob.size);
       downloadManager.update(downloadKey, blob.size);
       downloadBlob(blob, downloadKey);
-      setTimeout(() => downloadManager.clear(downloadKey), 5_000);
+      setTimeout(() => downloadManager.clear(downloadKey), 5000);
     }
 
     if (orphans.length > 0) {
@@ -53,7 +53,7 @@
       downloadManager.add(downloadKey, blob.size);
       downloadManager.update(downloadKey, blob.size);
       downloadBlob(blob, downloadKey);
-      setTimeout(() => downloadManager.clear(downloadKey), 5_000);
+      setTimeout(() => downloadManager.clear(downloadKey), 5000);
     }
   };
 
@@ -130,9 +130,9 @@
 
     try {
       const chunkSize = 10;
-      const filenames = [...extras.filter(({ checksum }) => !checksum).map(({ filename }) => filename)];
-      for (let i = 0; i < filenames.length; i += chunkSize) {
-        count += await loadAndMatch(filenames.slice(i, i + chunkSize));
+      const filenames = extras.filter(({ checksum }) => !checksum).map(({ filename }) => filename);
+      for (let index = 0; index < filenames.length; index += chunkSize) {
+        count += await loadAndMatch(filenames.slice(index, index + chunkSize));
       }
     } catch (error) {
       handleError(error, 'Unable to check items');
@@ -218,7 +218,7 @@
               <tr class="flex w-full place-items-center p-2 md:p-5">
                 <th class="w-full text-sm place-items-center font-medium flex justify-between" colspan="2">
                   <div class="px-3">
-                    <p>MATCHES {matches.length ? `(${matches.length})` : ''}</p>
+                    <p>MATCHES {matches.length > 0 ? `(${matches.length})` : ''}</p>
                     <p class="text-gray-600 dark:text-gray-300 mt-1">These files are matched by their checksums</p>
                   </div>
                 </th>
@@ -252,7 +252,7 @@
               <tr class="flex w-full place-items-center p-1 md:p-5">
                 <th class="w-full text-sm font-medium justify-between place-items-center flex" colspan="2">
                   <div class="px-3">
-                    <p>OFFLINE PATHS {orphans.length ? `(${orphans.length})` : ''}</p>
+                    <p>OFFLINE PATHS {orphans.length > 0 ? `(${orphans.length})` : ''}</p>
                     <p class="text-gray-600 dark:text-gray-300 mt-1">
                       These files are the results of manually deletion of the default upload library
                     </p>
@@ -290,7 +290,7 @@
               <tr class="flex w-full place-items-center p-2 md:p-5">
                 <th class="w-full text-sm font-medium place-items-center flex justify-between" colspan="2">
                   <div class="px-3">
-                    <p>UNTRACKS FILES {extras.length ? `(${extras.length})` : ''}</p>
+                    <p>UNTRACKS FILES {extras.length > 0 ? `(${extras.length})` : ''}</p>
                     <p class="text-gray-600 dark:text-gray-300 mt-1">
                       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
diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte
index c13fd96d30..e91809c063 100644
--- a/web/src/routes/admin/system-settings/+page.svelte
+++ b/web/src/routes/admin/system-settings/+page.svelte
@@ -38,7 +38,7 @@
     downloadManager.add(downloadKey, blob.size);
     downloadManager.update(downloadKey, blob.size);
     downloadBlob(blob, downloadKey);
-    setTimeout(() => downloadManager.clear(downloadKey), 5_000);
+    setTimeout(() => downloadManager.clear(downloadKey), 5000);
   };
 
   const settings = [
diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte
index 792838b6d4..812814c835 100644
--- a/web/src/routes/admin/user-management/+page.svelte
+++ b/web/src/routes/admin/user-management/+page.svelte
@@ -31,7 +31,7 @@
   });
 
   const isDeleted = (user: UserResponseDto): boolean => {
-    return user.deletedAt != null;
+    return user.deletedAt != undefined;
   };
 
   const deleteDateFormat: Intl.DateTimeFormatOptions = {
@@ -41,7 +41,7 @@
   };
 
   const getDeleteDate = (user: UserResponseDto): string => {
-    let deletedAt = new Date(user.deletedAt ? user.deletedAt : Date.now());
+    let deletedAt = new Date(user.deletedAt ?? Date.now());
     deletedAt.setDate(deletedAt.getDate() + 7);
     return deletedAt.toLocaleString($locale, deleteDateFormat);
   };
@@ -188,13 +188,13 @@
         </thead>
         <tbody class="block max-h-[320px] w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
           {#if allUsers}
-            {#each allUsers as immichUser, i}
+            {#each allUsers as immichUser, index}
               <tr
                 class="flex h-[80px] overflow-hidden w-full place-items-center text-center dark:text-immich-dark-fg {isDeleted(
                   immichUser,
                 )
                   ? 'bg-red-300 dark:bg-red-900'
-                  : i % 2 == 0
+                  : index % 2 == 0
                     ? 'bg-immich-gray dark:bg-immich-dark-gray/75'
                     : 'bg-immich-bg dark:bg-immich-dark-gray/50'}"
               >
diff --git a/web/src/routes/auth/onboarding/+page.svelte b/web/src/routes/auth/onboarding/+page.svelte
index f8f91b679e..fbdd24fcc9 100644
--- a/web/src/routes/auth/onboarding/+page.svelte
+++ b/web/src/routes/auth/onboarding/+page.svelte
@@ -22,8 +22,8 @@
 
   $: {
     const stepState = $page.url.searchParams.get('step');
-    const tempIndex = onboardingSteps.findIndex((step) => step.name === stepState);
-    index = tempIndex >= 0 ? tempIndex : 0;
+    const temporaryIndex = onboardingSteps.findIndex((step) => step.name === stepState);
+    index = temporaryIndex >= 0 ? temporaryIndex : 0;
   }
 
   const handleDoneClicked = async () => {
diff --git a/web/src/test-data/factories/album-factory.ts b/web/src/test-data/factories/album-factory.ts
index 8cb92b50a2..37e838287b 100644
--- a/web/src/test-data/factories/album-factory.ts
+++ b/web/src/test-data/factories/album-factory.ts
@@ -7,7 +7,7 @@ export const albumFactory = Sync.makeFactory<AlbumResponseDto>({
   albumName: Sync.each(() => faker.commerce.product()),
   description: '',
   albumThumbnailAssetId: null,
-  assetCount: Sync.each((i) => i % 5),
+  assetCount: Sync.each((index) => index % 5),
   assets: [],
   createdAt: Sync.each(() => faker.date.past().toISOString()),
   updatedAt: Sync.each(() => faker.date.past().toISOString()),
diff --git a/web/vite.config.js b/web/vite.config.js
index e1cf9ea75e..ea49b8cc47 100644
--- a/web/vite.config.js
+++ b/web/vite.config.js
@@ -1,5 +1,5 @@
 import { sveltekit } from '@sveltejs/kit/vite';
-import path from 'path';
+import path from 'node:path';
 
 const upstream = {
   target: process.env.IMMICH_SERVER_URL || 'http://immich-server:3001/',
@@ -14,6 +14,7 @@ const config = {
   resolve: {
     alias: {
       'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js',
+      // eslint-disable-next-line unicorn/prefer-module
       '@test-data': path.resolve(__dirname, './src/test-data'),
       '@api': path.resolve('./src/api'),
     },