diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs
index fc5e35ce6d..f855a99c53 100644
--- a/web/eslint.config.mjs
+++ b/web/eslint.config.mjs
@@ -2,6 +2,8 @@ import { FlatCompat } from '@eslint/eslintrc';
 import js from '@eslint/js';
 import typescriptEslint from '@typescript-eslint/eslint-plugin';
 import tsParser from '@typescript-eslint/parser';
+import eslintPluginSvelte from 'eslint-plugin-svelte';
+import eslintPluginUnicorn from 'eslint-plugin-unicorn';
 import globals from 'globals';
 import path from 'node:path';
 import { fileURLToPath } from 'node:url';
@@ -11,11 +13,12 @@ const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
 const compat = new FlatCompat({
   baseDirectory: __dirname,
-  recommendedConfig: js.configs.recommended,
-  allConfig: js.configs.all,
 });
 
 export default [
+  ...eslintPluginSvelte.configs.recommended,
+  eslintPluginUnicorn.configs.recommended,
+  js.configs.recommended,
   {
     ignores: [
       '**/.DS_Store',
@@ -36,15 +39,11 @@ export default [
       'coverage',
     ],
   },
-  ...compat.extends(
-    'eslint:recommended',
-    'plugin:@typescript-eslint/recommended',
-    'plugin:svelte/recommended',
-    'plugin:unicorn/recommended',
-  ),
+  ...compat.extends('plugin:@typescript-eslint/recommended'),
   {
     plugins: {
       '@typescript-eslint': typescriptEslint,
+      svelte: eslintPluginSvelte,
     },
 
     languageOptions: {
diff --git a/web/package-lock.json b/web/package-lock.json
index e181a5a9ba..6c9ceba459 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -57,8 +57,8 @@
         "dotenv": "^16.4.7",
         "eslint": "^9.18.0",
         "eslint-config-prettier": "^10.0.0",
-        "eslint-plugin-svelte": "^2.46.1",
-        "eslint-plugin-unicorn": "^56.0.1",
+        "eslint-plugin-svelte": "^3.0.0",
+        "eslint-plugin-unicorn": "^57.0.0",
         "factory.ts": "^1.4.1",
         "globals": "^16.0.0",
         "postcss": "^8.5.0",
@@ -663,16 +663,20 @@
       }
     },
     "node_modules/@eslint-community/eslint-utils": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
-      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
+      "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "eslint-visitor-keys": "^3.3.0"
+        "eslint-visitor-keys": "^3.4.3"
       },
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      },
       "peerDependencies": {
         "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
       }
@@ -739,37 +743,6 @@
         "url": "https://opencollective.com/eslint"
       }
     },
-    "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
-      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
-      "dev": true,
-      "license": "Apache-2.0",
-      "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
-    "node_modules/@eslint/eslintrc/node_modules/espree": {
-      "version": "10.3.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
-      "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
-      "dev": true,
-      "license": "BSD-2-Clause",
-      "dependencies": {
-        "acorn": "^8.14.0",
-        "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^4.2.0"
-      },
-      "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
     "node_modules/@eslint/eslintrc/node_modules/globals": {
       "version": "14.0.0",
       "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
@@ -2561,7 +2534,8 @@
       "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
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/@types/pbf": {
       "version": "3.0.5",
@@ -3305,9 +3279,9 @@
       }
     },
     "node_modules/browserslist": {
-      "version": "4.24.3",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
-      "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
+      "version": "4.24.4",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
+      "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
       "dev": true,
       "funding": [
         {
@@ -3344,12 +3318,13 @@
       "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==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz",
+      "integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
-        "node": ">=6"
+        "node": ">=18.20"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
@@ -3523,9 +3498,9 @@
       }
     },
     "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==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz",
+      "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==",
       "dev": true,
       "funding": [
         {
@@ -3533,6 +3508,7 @@
           "url": "https://github.com/sponsors/sibiraj-s"
         }
       ],
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
@@ -3710,13 +3686,13 @@
       }
     },
     "node_modules/core-js-compat": {
-      "version": "3.39.0",
-      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz",
-      "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==",
+      "version": "3.41.0",
+      "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
+      "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "browserslist": "^4.24.2"
+        "browserslist": "^4.24.4"
       },
       "funding": {
         "type": "opencollective",
@@ -4043,15 +4019,6 @@
         "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-module-lexer": {
       "version": "1.6.0",
       "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
@@ -4250,9 +4217,9 @@
       }
     },
     "node_modules/eslint-compat-utils": {
-      "version": "0.5.1",
-      "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz",
-      "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==",
+      "version": "0.6.4",
+      "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.6.4.tgz",
+      "integrity": "sha512-/u+GQt8NMfXO8w17QendT4gvO5acfxQsAKirAt0LVxDnr2N8YLCVbregaNc/Yhp7NM128DwCaRvr8PLDfeNkQw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -4279,32 +4246,31 @@
       }
     },
     "node_modules/eslint-plugin-svelte": {
-      "version": "2.46.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz",
-      "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==",
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.0.2.tgz",
+      "integrity": "sha512-+0QglmWNryvXXxRQKzLF3i+AreTsueCw7PBb0nGVBq+F9HoYqAjQeJ/9N6vFAtjMjK3wgsETrLVyBKPdeufN6Q==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@eslint-community/eslint-utils": "^4.4.0",
-        "@jridgewell/sourcemap-codec": "^1.4.15",
-        "eslint-compat-utils": "^0.5.1",
+        "@eslint-community/eslint-utils": "^4.4.1",
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "eslint-compat-utils": "^0.6.4",
         "esutils": "^2.0.3",
         "known-css-properties": "^0.35.0",
-        "postcss": "^8.4.38",
+        "postcss": "^8.4.49",
         "postcss-load-config": "^3.1.4",
-        "postcss-safe-parser": "^6.0.0",
-        "postcss-selector-parser": "^6.1.0",
-        "semver": "^7.6.2",
-        "svelte-eslint-parser": "^0.43.0"
+        "postcss-safe-parser": "^7.0.0",
+        "semver": "^7.6.3",
+        "svelte-eslint-parser": "^1.0.0"
       },
       "engines": {
-        "node": "^14.17.0 || >=16.0.0"
+        "node": "^18.20.4 || ^20.18.0 || >=22.10.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/ota-meshi"
       },
       "peerDependencies": {
-        "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0",
+        "eslint": "^8.57.1 || ^9.0.0",
         "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0"
       },
       "peerDependenciesMeta": {
@@ -4314,28 +4280,28 @@
       }
     },
     "node_modules/eslint-plugin-unicorn": {
-      "version": "56.0.1",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz",
-      "integrity": "sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==",
+      "version": "57.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-57.0.0.tgz",
+      "integrity": "sha512-zUYYa6zfNdTeG9BISWDlcLmz16c+2Ck2o5ZDHh0UzXJz3DEP7xjmlVDTzbyV0W+XksgZ0q37WEWzN2D2Ze+g9Q==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@babel/helper-validator-identifier": "^7.24.7",
-        "@eslint-community/eslint-utils": "^4.4.0",
-        "ci-info": "^4.0.0",
+        "@babel/helper-validator-identifier": "^7.25.9",
+        "@eslint-community/eslint-utils": "^4.4.1",
+        "ci-info": "^4.1.0",
         "clean-regexp": "^1.0.0",
-        "core-js-compat": "^3.38.1",
+        "core-js-compat": "^3.40.0",
         "esquery": "^1.6.0",
-        "globals": "^15.9.0",
-        "indent-string": "^4.0.0",
-        "is-builtin-module": "^3.2.1",
-        "jsesc": "^3.0.2",
+        "globals": "^15.15.0",
+        "indent-string": "^5.0.0",
+        "is-builtin-module": "^4.0.0",
+        "jsesc": "^3.1.0",
         "pluralize": "^8.0.0",
-        "read-pkg-up": "^7.0.1",
+        "read-package-up": "^11.0.0",
         "regexp-tree": "^0.1.27",
-        "regjsparser": "^0.10.0",
-        "semver": "^7.6.3",
-        "strip-indent": "^3.0.0"
+        "regjsparser": "^0.12.0",
+        "semver": "^7.7.1",
+        "strip-indent": "^4.0.0"
       },
       "engines": {
         "node": ">=18.18"
@@ -4344,7 +4310,7 @@
         "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
       },
       "peerDependencies": {
-        "eslint": ">=8.56.0"
+        "eslint": ">=9.20.0"
       }
     },
     "node_modules/eslint-plugin-unicorn/node_modules/globals": {
@@ -4360,30 +4326,47 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "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==",
+    "node_modules/eslint-plugin-unicorn/node_modules/indent-string": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
+      "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
       "dev": true,
       "license": "MIT",
-      "bin": {
-        "jsesc": "bin/jsesc"
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/eslint-plugin-unicorn/node_modules/strip-indent": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz",
+      "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "min-indent": "^1.0.1"
       },
       "engines": {
-        "node": ">=6"
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/eslint-scope": {
-      "version": "7.2.2",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
-      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
+      "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
         "esrecurse": "^4.3.0",
         "estraverse": "^5.2.0"
       },
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       },
       "funding": {
         "url": "https://opencollective.com/eslint"
@@ -4494,23 +4477,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/eslint/node_modules/eslint-scope": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
-      "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
-      "dev": true,
-      "license": "BSD-2-Clause",
-      "dependencies": {
-        "esrecurse": "^4.3.0",
-        "estraverse": "^5.2.0"
-      },
-      "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
     "node_modules/eslint/node_modules/eslint-visitor-keys": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
@@ -4524,24 +4490,6 @@
         "url": "https://opencollective.com/eslint"
       }
     },
-    "node_modules/eslint/node_modules/espree": {
-      "version": "10.3.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
-      "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
-      "dev": true,
-      "license": "BSD-2-Clause",
-      "dependencies": {
-        "acorn": "^8.14.0",
-        "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^4.2.0"
-      },
-      "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
-      },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
-      }
-    },
     "node_modules/eslint/node_modules/has-flag": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -4586,17 +4534,31 @@
       }
     },
     "node_modules/espree": {
-      "version": "9.6.1",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
-      "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+      "version": "10.3.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
+      "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
-        "acorn": "^8.9.0",
+        "acorn": "^8.14.0",
         "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^3.4.1"
+        "eslint-visitor-keys": "^4.2.0"
       },
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/espree/node_modules/eslint-visitor-keys": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       },
       "funding": {
         "url": "https://opencollective.com/eslint"
@@ -5051,6 +5013,19 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/find-up-simple": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz",
+      "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/flat-cache": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
@@ -5395,10 +5370,17 @@
       "optional": true
     },
     "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
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
+      "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "lru-cache": "^10.0.1"
+      },
+      "engines": {
+        "node": "^16.14.0 || >=18.0.0"
+      }
     },
     "node_modules/html-encoding-sniffer": {
       "version": "4.0.0",
@@ -5556,6 +5538,19 @@
         "node": ">=8"
       }
     },
+    "node_modules/index-to-position": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-0.1.2.tgz",
+      "integrity": "sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/inflight": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -5606,12 +5601,6 @@
         "tslib": "2"
       }
     },
-    "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-binary-path": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -5624,15 +5613,16 @@
       }
     },
     "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==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-4.0.0.tgz",
+      "integrity": "sha512-rWP3AMAalQSesXO8gleROyL2iKU73SX5Er66losQn9rWOWL4Gef0a/xOEOVqjWGMuR2vHG3FJ8UUmT700O8oFg==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "builtin-modules": "^3.3.0"
+        "builtin-modules": "^4.0.0"
       },
       "engines": {
-        "node": ">=6"
+        "node": ">=18.20"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
@@ -5918,6 +5908,19 @@
         }
       }
     },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/json-buffer": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -5925,12 +5928,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "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",
@@ -6073,6 +6070,12 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "license": "ISC"
+    },
     "node_modules/lru-queue": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
@@ -6546,24 +6549,18 @@
       }
     },
     "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==",
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz",
+      "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==",
       "dev": true,
+      "license": "BSD-2-Clause",
       "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"
+        "hosted-git-info": "^7.0.0",
+        "semver": "^7.3.5",
+        "validate-npm-package-license": "^3.0.4"
+      },
+      "engines": {
+        "node": "^16.14.0 || >=18.0.0"
       }
     },
     "node_modules/normalize-path": {
@@ -6693,15 +6690,6 @@
         "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/package-json-from-dist": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
@@ -6720,18 +6708,18 @@
       }
     },
     "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==",
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.1.0.tgz",
+      "integrity": "sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==",
       "dev": true,
+      "license": "MIT",
       "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"
+        "@babel/code-frame": "^7.22.13",
+        "index-to-position": "^0.1.2",
+        "type-fest": "^4.7.1"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=18"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
@@ -6796,11 +6784,6 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/path-scurry/node_modules/lru-cache": {
-      "version": "10.4.3",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
-      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
-    },
     "node_modules/pathe": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz",
@@ -7008,19 +6991,30 @@
       }
     },
     "node_modules/postcss-safe-parser": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz",
-      "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==",
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz",
+      "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==",
       "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
       "engines": {
-        "node": ">=12.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/postcss/"
+        "node": ">=18.0"
       },
       "peerDependencies": {
-        "postcss": "^8.3.3"
+        "postcss": "^8.4.31"
       }
     },
     "node_modules/postcss-scss": {
@@ -7229,108 +7223,44 @@
         "pify": "^2.3.0"
       }
     },
+    "node_modules/read-package-up": {
+      "version": "11.0.0",
+      "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz",
+      "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "find-up-simple": "^1.0.0",
+        "read-pkg": "^9.0.0",
+        "type-fest": "^4.6.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "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==",
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz",
+      "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "@types/normalize-package-data": "^2.4.0",
-        "normalize-package-data": "^2.5.0",
-        "parse-json": "^5.0.0",
-        "type-fest": "^0.6.0"
+        "@types/normalize-package-data": "^2.4.3",
+        "normalize-package-data": "^6.0.0",
+        "parse-json": "^8.0.0",
+        "type-fest": "^4.6.0",
+        "unicorn-magic": "^0.1.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"
+        "node": ">=18"
       },
       "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",
@@ -7386,24 +7316,29 @@
       }
     },
     "node_modules/regjsparser": {
-      "version": "0.10.0",
-      "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz",
-      "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==",
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz",
+      "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==",
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
-        "jsesc": "~0.5.0"
+        "jsesc": "~3.0.2"
       },
       "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==",
+      "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,
+      "license": "MIT",
       "bin": {
         "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
       }
     },
     "node_modules/require-directory": {
@@ -7687,9 +7622,9 @@
       }
     },
     "node_modules/semver": {
-      "version": "7.6.3",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
-      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+      "version": "7.7.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
+      "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
       "devOptional": true,
       "license": "ISC",
       "bin": {
@@ -7968,32 +7903,36 @@
       "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
       "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
       "dev": true,
+      "license": "Apache-2.0",
       "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
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+      "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+      "dev": true,
+      "license": "CC-BY-3.0"
     },
     "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,
+      "license": "MIT",
       "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
+      "version": "3.0.21",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz",
+      "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==",
+      "dev": true,
+      "license": "CC0-1.0"
     },
     "node_modules/split-string": {
       "version": "3.1.0",
@@ -8298,20 +8237,21 @@
       }
     },
     "node_modules/svelte-eslint-parser": {
-      "version": "0.43.0",
-      "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
-      "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==",
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.0.0.tgz",
+      "integrity": "sha512-diZzpeeFhAxormeIhmRS4vXx98GG6T7Dq5y1a6qffqs/5MBrBqqDg8bj88iEohp6bvhU4MIABJmOTa0gXWcbSQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "eslint-scope": "^7.2.2",
-        "eslint-visitor-keys": "^3.4.3",
-        "espree": "^9.6.1",
-        "postcss": "^8.4.39",
-        "postcss-scss": "^4.0.9"
+        "eslint-scope": "^8.2.0",
+        "eslint-visitor-keys": "^4.0.0",
+        "espree": "^10.0.0",
+        "postcss": "^8.4.49",
+        "postcss-scss": "^4.0.9",
+        "postcss-selector-parser": "^7.0.0"
       },
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^18.20.4 || ^20.18.0 || >=22.10.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/ota-meshi"
@@ -8325,6 +8265,33 @@
         }
       }
     },
+    "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/svelte-eslint-parser/node_modules/postcss-selector-parser": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
+      "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/svelte-gestures": {
       "version": "5.1.3",
       "resolved": "https://registry.npmjs.org/svelte-gestures/-/svelte-gestures-5.1.3.tgz",
@@ -9236,6 +9203,19 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/type-fest": {
+      "version": "4.36.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.36.0.tgz",
+      "integrity": "sha512-3T/PUdKTCnkUmhQU6FFJEHsLwadsRegktX3TNHk+2JJB9HlA8gp1/VXblXVDI93kSnXF2rdPx0GMbHtJIV2LPg==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/typescript": {
       "version": "5.7.3",
       "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
@@ -9275,6 +9255,19 @@
         "node": ">=0.8.0"
       }
     },
+    "node_modules/unicorn-magic": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
+      "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/union-value": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@@ -9358,6 +9351,7 @@
       "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,
+      "license": "Apache-2.0",
       "dependencies": {
         "spdx-correct": "^3.0.0",
         "spdx-expression-parse": "^3.0.0"
diff --git a/web/package.json b/web/package.json
index 21266c5dea..23a6b5da0d 100644
--- a/web/package.json
+++ b/web/package.json
@@ -45,8 +45,8 @@
     "dotenv": "^16.4.7",
     "eslint": "^9.18.0",
     "eslint-config-prettier": "^10.0.0",
-    "eslint-plugin-svelte": "^2.46.1",
-    "eslint-plugin-unicorn": "^56.0.1",
+    "eslint-plugin-svelte": "^3.0.0",
+    "eslint-plugin-unicorn": "^57.0.0",
     "factory.ts": "^1.4.1",
     "globals": "^16.0.0",
     "postcss": "^8.5.0",
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 4eb0bf6bb0..2c59f59416 100644
--- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
+++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
@@ -169,7 +169,7 @@
 </script>
 
 <div class="flex flex-col gap-7">
-  {#each jobList as [jobName, { title, subtitle, description, disabled, allText, refreshText, missingText, icon, handleCommand: handleCommandOverride }]}
+  {#each jobList as [jobName, { title, subtitle, description, disabled, allText, refreshText, missingText, icon, handleCommand: handleCommandOverride }] (jobName)}
     {@const { jobCounts, queueStatus } = jobs[jobName]}
     <JobTile
       {icon}
diff --git a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte
index 356de6ae86..5a95dbea30 100644
--- a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte
@@ -46,7 +46,7 @@
 <div>
   <div in:fade={{ duration: 500 }}>
     <form autocomplete="off" {onsubmit}>
-      {#each jobNames as jobName}
+      {#each jobNames as jobName (jobName)}
         <div class="ml-4 mt-4 flex flex-col gap-4">
           {#if isSystemConfigJobDto(jobName)}
             <SettingInputField
diff --git a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte
index 80c8904376..2a95e7a9f5 100644
--- a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte
@@ -46,7 +46,7 @@
         <hr />
 
         <div>
-          {#each config.machineLearning.urls as _, i}
+          {#each config.machineLearning.urls as _, i (i)}
             {#snippet removeButton()}
               {#if config.machineLearning.urls.length > 1}
                 <CircleIconButton
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 2a40156eac..9b4aa5e934 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
@@ -225,7 +225,7 @@
                   bind:value={selectedPreset}
                   onchange={handlePresetSelection}
                 >
-                  {#each templateOptions.presetOptions as preset}
+                  {#each templateOptions.presetOptions as preset (preset)}
                     <option value={preset}>{renderTemplate(preset)}</option>
                   {/each}
                 </select>
@@ -246,7 +246,7 @@
                 <SettingInputField
                   label={$t('extension')}
                   inputType={SettingInputFieldType.TEXT}
-                  value={'.jpg'}
+                  value=".jpg"
                   disabled
                 />
               </div>
diff --git a/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte b/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte
index 379e366df6..9d8ff51cc0 100644
--- a/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte
+++ b/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte
@@ -19,6 +19,7 @@
   <h4>{$t('date_and_time').toUpperCase()}</h4>
 </div>
 
+<!-- eslint-disable svelte/no-useless-mustaches -->
 <div class="mt-2 rounded-lg bg-gray-200 p-4 text-xs dark:bg-gray-700 dark:text-immich-dark-fg">
   <div class="mb-2 text-gray-600 dark:text-immich-dark-fg">
     <p>{$t('admin.storage_template_date_time_description')}</p>
@@ -28,7 +29,7 @@
     <div>
       <p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('year').toUpperCase()}</p>
       <ul>
-        {#each options.yearOptions as yearFormat}
+        {#each options.yearOptions as yearFormat, index (index)}
           <li>{'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}</li>
         {/each}
       </ul>
@@ -37,7 +38,7 @@
     <div>
       <p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('month').toUpperCase()}</p>
       <ul>
-        {#each options.monthOptions as monthFormat}
+        {#each options.monthOptions as monthFormat, index (index)}
           <li>{'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}</li>
         {/each}
       </ul>
@@ -46,7 +47,7 @@
     <div>
       <p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('week').toUpperCase()}</p>
       <ul>
-        {#each options.weekOptions as weekFormat}
+        {#each options.weekOptions as weekFormat, index (index)}
           <li>{'{{'}{weekFormat}{'}}'} - {getLuxonExample(weekFormat)}</li>
         {/each}
       </ul>
@@ -55,7 +56,7 @@
     <div>
       <p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('day').toUpperCase()}</p>
       <ul>
-        {#each options.dayOptions as dayFormat}
+        {#each options.dayOptions as dayFormat, index (index)}
           <li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
         {/each}
       </ul>
@@ -64,7 +65,7 @@
     <div>
       <p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('hour').toUpperCase()}</p>
       <ul>
-        {#each options.hourOptions as dayFormat}
+        {#each options.hourOptions as dayFormat, index (index)}
           <li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
         {/each}
       </ul>
@@ -73,7 +74,7 @@
     <div>
       <p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('minute').toUpperCase()}</p>
       <ul>
-        {#each options.minuteOptions as dayFormat}
+        {#each options.minuteOptions as dayFormat, index (index)}
           <li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
         {/each}
       </ul>
@@ -82,7 +83,7 @@
     <div>
       <p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('second').toUpperCase()}</p>
       <ul>
-        {#each options.secondOptions as dayFormat}
+        {#each options.secondOptions as dayFormat, index (index)}
           <li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
         {/each}
       </ul>
diff --git a/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte b/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte
index c27df817c2..f23289d1e5 100644
--- a/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte
@@ -92,7 +92,7 @@
               <LoadingSpinner />
             {/if}
 
-            {#each templateConfigs as { label, templateKey, descriptionTags, templateName }}
+            {#each templateConfigs as { label, templateKey, descriptionTags, templateName } (templateKey)}
               <SettingTextarea
                 {label}
                 description={$t('admin.template_email_available_tags', { values: { tags: descriptionTags } })}
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 778943af3a..35d8a84412 100644
--- a/web/src/lib/components/album-page/share-info-modal.svelte
+++ b/web/src/lib/components/album-page/share-info-modal.svelte
@@ -95,7 +95,7 @@
           <p class="text-sm">{$t('owner')}</p>
         </div>
       </div>
-      {#each album.albumUsers as { user, role }}
+      {#each album.albumUsers as { user, role } (user.id)}
         <div
           class="flex w-full place-items-center justify-between gap-4 p-5 rounded-xl transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
         >
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 acbced70a0..cd454f515f 100644
--- a/web/src/lib/components/album-page/user-selection-modal.svelte
+++ b/web/src/lib/components/album-page/user-selection-modal.svelte
@@ -73,7 +73,7 @@
     <div class="mb-2 py-2 sticky">
       <p class="text-xs font-medium">{$t('selected')}</p>
       <div class="my-2">
-        {#each Object.values(selectedUsers) as { user }}
+        {#each Object.values(selectedUsers) as { user } (user.id)}
           {#key user.id}
             <div class="flex place-items-center gap-4 p-4">
               <div
@@ -116,7 +116,7 @@
       <Text>{$t('users')}</Text>
 
       <div class="my-2">
-        {#each users as user}
+        {#each users as user (user.id)}
           {#if !Object.keys(selectedUsers).includes(user.id)}
             <div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl">
               <button type="button" onclick={() => handleToggle(user)} class="flex w-full place-items-center gap-4 p-4">
@@ -161,7 +161,7 @@
       </div>
 
       <Stack gap={4}>
-        {#each sharedLinks as sharedLink}
+        {#each sharedLinks as sharedLink (sharedLink.id)}
           <AlbumSharedLink {album} {sharedLink} />
         {/each}
       </Stack>
diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte
index a83c5edd1e..b96a4660ee 100644
--- a/web/src/lib/components/asset-viewer/detail-panel.svelte
+++ b/web/src/lib/components/asset-viewer/detail-panel.svelte
@@ -536,7 +536,7 @@
 {#if albums.length > 0}
   <section class="px-6 pt-6 dark:text-immich-dark-fg">
     <p class="pb-4 text-sm">{$t('appears_in').toUpperCase()}</p>
-    {#each albums as album}
+    {#each albums as album (album.id)}
       <a href="{AppRoute.ALBUMS}/{album.id}">
         <div class="flex gap-4 pt-2 hover:cursor-pointer items-center">
           <div>
diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte
index 363bec7c1f..daeccd9074 100644
--- a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte
+++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte
@@ -137,7 +137,7 @@
   <div class="flex h-10 w-full items-center justify-between text-sm">
     <h2>{$t('editor_crop_tool_h2_aspect_ratios').toUpperCase()}</h2>
   </div>
-  {#each sizesRows as sizesRow}
+  {#each sizesRows as sizesRow, index (index)}
     <ul class="flex-wrap flex-row flex gap-x-6 py-2 justify-evenly">
       {#each sizesRow as size (size.name)}
         <CropPreset {size} {selectedSize} {rotateHorizontal} {selectType} />
diff --git a/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte b/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte
index e7709cd9cc..c50c21bef8 100644
--- a/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte
+++ b/web/src/lib/components/asset-viewer/face-editor/face-editor.svelte
@@ -325,7 +325,7 @@
     <div class="h-[250px] overflow-y-auto mt-2">
       {#if filteredCandidates.length > 0}
         <div class="mt-2 rounded-lg">
-          {#each filteredCandidates as person}
+          {#each filteredCandidates as person (person.id)}
             <button
               onclick={() => tagFace(person)}
               type="button"
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte
index 0d79b9e5fc..24701685ef 100644
--- a/web/src/lib/components/asset-viewer/photo-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte
@@ -225,6 +225,7 @@
           : slideshowLookCssMapping[$slideshowLook]}"
         draggable="false"
       />
+      <!-- eslint-disable-next-line svelte/require-each-key -->
       {#each getBoundingBox($boundingBoxesArray, $photoZoomState, $photoViewerImgElement) as boundingbox}
         <div
           class="absolute border-solid border-white border-[3px] rounded-lg"
diff --git a/web/src/lib/components/elements/buttons/skip-link.svelte b/web/src/lib/components/elements/buttons/skip-link.svelte
index 858d296c30..b80e7d1a44 100644
--- a/web/src/lib/components/elements/buttons/skip-link.svelte
+++ b/web/src/lib/components/elements/buttons/skip-link.svelte
@@ -22,7 +22,7 @@
 
 <div class="absolute z-50 top-2 left-2 transition-transform {isFocused ? 'translate-y-0' : '-translate-y-10 sr-only'}">
   <Button
-    size={'sm'}
+    size="sm"
     rounded="none"
     onclick={moveFocus}
     onfocus={() => (isFocused = true)}
diff --git a/web/src/lib/components/elements/group-tab.svelte b/web/src/lib/components/elements/group-tab.svelte
index 950c26f3f5..a751dd0fa1 100644
--- a/web/src/lib/components/elements/group-tab.svelte
+++ b/web/src/lib/components/elements/group-tab.svelte
@@ -18,7 +18,7 @@
   class="dark:bg-immich-dark-gray flex h-full rounded-2xl bg-gray-200 ring-gray-400 has-[:focus-visible]:ring dark:ring-gray-600"
 >
   <legend class="sr-only">{label}</legend>
-  {#each filters as filter, index}
+  {#each filters as filter, index (`${id}-${index}`)}
     <div class="group">
       <input
         type="radio"
diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte
index 26ca99d863..88576d2845 100644
--- a/web/src/lib/components/faces-page/merge-face-selector.svelte
+++ b/web/src/lib/components/faces-page/merge-face-selector.svelte
@@ -111,7 +111,7 @@
       <div></div>
     {/snippet}
     {#snippet trailing()}
-      <Button size={'sm'} disabled={!hasSelection} onclick={handleMerge}>
+      <Button size="sm" disabled={!hasSelection} onclick={handleMerge}>
         <Icon path={mdiMerge} size={18} />
         <span class="ml-2">{$t('merge')}</span></Button
       >
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 7bf87fc67b..b0e0f8fcc4 100644
--- a/web/src/lib/components/faces-page/person-side-panel.svelte
+++ b/web/src/lib/components/faces-page/person-side-panel.svelte
@@ -221,7 +221,7 @@
           <LoadingSpinner />
         </div>
       {:else}
-        {#each peopleWithFaces as face, index}
+        {#each peopleWithFaces as face, index (face.id)}
           {@const personName = face.person ? face.person?.name : $t('face_unassigned')}
           <div class="relative z-[20001] h-[115px] w-[95px]">
             <div
diff --git a/web/src/lib/components/faces-page/unmerge-face-selector.svelte b/web/src/lib/components/faces-page/unmerge-face-selector.svelte
index 06c53f3618..e808c98748 100644
--- a/web/src/lib/components/faces-page/unmerge-face-selector.svelte
+++ b/web/src/lib/components/faces-page/unmerge-face-selector.svelte
@@ -131,7 +131,7 @@
       <div class="flex gap-4">
         <Button
           title={$t('create_new_person_hint')}
-          size={'sm'}
+          size="sm"
           disabled={disableButtons || hasSelection}
           onclick={handleCreate}
         >
@@ -143,7 +143,7 @@
           <span class="ml-2"> {$t('create_new_person')}</span></Button
         >
         <Button
-          size={'sm'}
+          size="sm"
           title={$t('reassing_hint')}
           disabled={disableButtons || !hasSelection}
           onclick={handleReassign}
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 3acd46520f..6ad5b2598d 100644
--- a/web/src/lib/components/forms/library-import-paths-form.svelte
+++ b/web/src/lib/components/forms/library-import-paths-form.svelte
@@ -175,7 +175,7 @@
 <form {onsubmit} autocomplete="off" class="m-4 flex flex-col gap-4">
   <table class="text-left">
     <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
-      {#each validatedPaths as validatedPath, listIndex}
+      {#each validatedPaths as validatedPath, listIndex (validatedPath.importPath)}
         <tr
           class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
             listIndex % 2 == 0
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 68e99641e8..0ce414c10d 100644
--- a/web/src/lib/components/forms/library-scan-settings-form.svelte
+++ b/web/src/lib/components/forms/library-scan-settings-form.svelte
@@ -125,7 +125,7 @@
 <form {onsubmit} autocomplete="off" class="m-4 flex flex-col gap-4">
   <table class="w-full text-left">
     <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
-      {#each exclusionPatterns as exclusionPattern, listIndex}
+      {#each exclusionPatterns as exclusionPattern, listIndex (exclusionPattern)}
         <tr
           class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
             listIndex % 2 == 0
diff --git a/web/src/lib/components/i18n/format-message.svelte b/web/src/lib/components/i18n/format-message.svelte
index d65e1096fe..bb8a066c6e 100644
--- a/web/src/lib/components/i18n/format-message.svelte
+++ b/web/src/lib/components/i18n/format-message.svelte
@@ -129,6 +129,7 @@ Used for every occurrence of an HTML tag in a message
 Result: Visit <a href="">docs</a> <strong>now</strong>
 ```
 -->
+<!-- eslint-disable-next-line svelte/require-each-key -->
 {#each parts as { tag, message }}
   {#if tag}
     {#if children}{@render children({ tag, message })}{:else}{message}{/if}
diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte
index 3e8da972e1..c1ec31470f 100644
--- a/web/src/lib/components/memory-page/memory-viewer.svelte
+++ b/web/src/lib/components/memory-page/memory-viewer.svelte
@@ -183,6 +183,7 @@
     if (!current) {
       return;
     }
+    // eslint-disable-next-line no-self-assign
     current.memory.assets = current.memory.assets;
   };
 
@@ -222,6 +223,7 @@
 
     current.memory.assets = current.memory.assets.filter((asset) => asset.id !== current.asset.id);
 
+    // eslint-disable-next-line no-self-assign
     $memoryStore = $memoryStore;
 
     await removeMemoryAssets({ id: current.memory.id, bulkIdsDto: { ids: [current.asset.id] } });
@@ -351,7 +353,7 @@
           class="hover:text-black"
         />
 
-        {#each current.memory.assets as asset, index}
+        {#each current.memory.assets as asset, index (asset.id)}
           <a class="relative w-full py-2" href={asHref(asset)}>
             <span class="absolute left-0 h-[2px] w-full bg-gray-500"></span>
             {#await resetPromise}
@@ -475,14 +477,10 @@
                   align="bottom-right"
                   class="text-white dark:text-white"
                 >
-                  <MenuOption
-                    onClick={() => handleDeleteMemory(current)}
-                    text={'Remove memory'}
-                    icon={mdiCardsOutline}
-                  />
+                  <MenuOption onClick={() => handleDeleteMemory(current)} text="Remove memory" icon={mdiCardsOutline} />
                   <MenuOption
                     onClick={() => handleDeleteMemoryAsset(current)}
-                    text={'Remove photo from this memory'}
+                    text="Remove photo from this memory"
                     icon={mdiImageMinusOutline}
                   />
                   <!-- shortcut={{ key: 'l', shift: shared }} -->
diff --git a/web/src/lib/components/onboarding-page/onboarding-privacy.svelte b/web/src/lib/components/onboarding-page/onboarding-privacy.svelte
index 8ff8a9200d..5bf750af96 100644
--- a/web/src/lib/components/onboarding-page/onboarding-privacy.svelte
+++ b/web/src/lib/components/onboarding-page/onboarding-privacy.svelte
@@ -32,41 +32,39 @@
 
   {#if config && $user}
     <AdminSettings bind:config bind:this={adminSettingsComponent}>
-      {#snippet children()}
-        {#if config}
-          <SettingSwitch
-            title={$t('admin.map_settings')}
-            subtitle={$t('admin.map_implications')}
-            bind:checked={config.map.enabled}
-          />
-          <SettingSwitch
-            title={$t('admin.version_check_settings')}
-            subtitle={$t('admin.version_check_implications')}
-            bind:checked={config.newVersionCheck.enabled}
-          />
-          <div class="flex pt-4">
-            <div class="w-full flex place-content-start">
-              <Button class="flex gap-2 place-content-center" onclick={() => onPrevious()}>
-                <Icon path={mdiArrowLeft} size="18" />
-                <p>{$t('theme')}</p>
-              </Button>
-            </div>
-            <div class="flex w-full place-content-end">
-              <Button
-                onclick={() => {
-                  adminSettingsComponent?.handleSave({ map: config?.map, newVersionCheck: config?.newVersionCheck });
-                  onDone();
-                }}
-              >
-                <span class="flex place-content-center place-items-center gap-2">
-                  {$t('admin.storage_template_settings')}
-                  <Icon path={mdiArrowRight} size="18" />
-                </span>
-              </Button>
-            </div>
+      {#if config}
+        <SettingSwitch
+          title={$t('admin.map_settings')}
+          subtitle={$t('admin.map_implications')}
+          bind:checked={config.map.enabled}
+        />
+        <SettingSwitch
+          title={$t('admin.version_check_settings')}
+          subtitle={$t('admin.version_check_implications')}
+          bind:checked={config.newVersionCheck.enabled}
+        />
+        <div class="flex pt-4">
+          <div class="w-full flex place-content-start">
+            <Button class="flex gap-2 place-content-center" onclick={() => onPrevious()}>
+              <Icon path={mdiArrowLeft} size="18" />
+              <p>{$t('theme')}</p>
+            </Button>
           </div>
-        {/if}
-      {/snippet}
+          <div class="flex w-full place-content-end">
+            <Button
+              onclick={() => {
+                adminSettingsComponent?.handleSave({ map: config?.map, newVersionCheck: config?.newVersionCheck });
+                onDone();
+              }}
+            >
+              <span class="flex place-content-center place-items-center gap-2">
+                {$t('admin.storage_template_settings')}
+                <Icon path={mdiArrowRight} size="18" />
+              </span>
+            </Button>
+          </div>
+        </div>
+      {/if}
     </AdminSettings>
   {/if}
 </OnboardingCard>
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 b383729ecd..89c0b42165 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
@@ -33,7 +33,7 @@
   };
 </script>
 
-{#each jobs as job}
+{#each jobs as job (job)}
   {#if isAllVideos || job !== AssetJobName.TranscodeVideo}
     <MenuOption text={$getAssetJobName(job)} icon={getAssetJobIcon(job)} onClick={() => handleRunJob(job)} />
   {/if}
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 586491ef47..52704c58ee 100644
--- a/web/src/lib/components/photos-page/asset-date-group.svelte
+++ b/web/src/lib/components/photos-page/asset-date-group.svelte
@@ -119,7 +119,7 @@
       data-date-group={dateGroup.date}
       style:height={dateGroup.height + 'px'}
       style:width={dateGroup.geometry.containerWidth + 'px'}
-      style:overflow={'clip'}
+      style:overflow="clip"
     >
       {#if !display}
         <Skeleton height={dateGroup.height + 'px'} title={dateGroup.groupTitle} />
diff --git a/web/src/lib/components/photos-page/measure-date-group.svelte b/web/src/lib/components/photos-page/measure-date-group.svelte
index 80ad7640fb..a435f89fb5 100644
--- a/web/src/lib/components/photos-page/measure-date-group.svelte
+++ b/web/src/lib/components/photos-page/measure-date-group.svelte
@@ -67,7 +67,7 @@
 </script>
 
 <section id="measure-asset-group-by-date" class="flex flex-wrap gap-x-12" use:measure>
-  {#each bucket.dateGroups as dateGroup}
+  {#each bucket.dateGroups as dateGroup (dateGroup.date)}
     <div id="date-group" data-date-group={dateGroup.date}>
       <div use:resizeObserver={({ height }) => $assetStore.updateBucketDateGroup(bucket, dateGroup, { height })}>
         <div
@@ -83,7 +83,7 @@
           class="relative overflow-clip"
           style:height={dateGroup.geometry.containerHeight + 'px'}
           style:width={dateGroup.geometry.containerWidth + 'px'}
-          style:visibility={'hidden'}
+          style:visibility="hidden"
         ></div>
       </div>
     </div>
diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte
index b2db2f1f04..e2be2818b6 100644
--- a/web/src/lib/components/photos-page/memory-lane.svelte
+++ b/web/src/lib/components/photos-page/memory-lane.svelte
@@ -69,7 +69,7 @@
       </div>
     {/if}
     <div class="inline-block" use:resizeObserver={({ width }) => (innerWidth = width)}>
-      {#each $memoryStore as memory}
+      {#each $memoryStore as memory (memory.id)}
         {#if memory.assets.length > 0}
           <a
             class="memory-card relative mr-8 inline-block aspect-[3/4] md:aspect-video h-[215px] rounded-xl"
diff --git a/web/src/lib/components/places-page/places-card-group.svelte b/web/src/lib/components/places-page/places-card-group.svelte
index 87b9870888..9bd863a95d 100644
--- a/web/src/lib/components/places-page/places-card-group.svelte
+++ b/web/src/lib/components/places-page/places-card-group.svelte
@@ -43,7 +43,7 @@
 <div class="mt-4">
   {#if !isCollapsed}
     <div class="flex flex-row flex-wrap gap-4">
-      {#each places as item}
+      {#each places as item (item.id)}
         {@const city = item.exifInfo?.city}
         <a class="relative" href="{AppRoute.SEARCH}?{getMetadataSearchQuery({ city })}" draggable="false">
           <div
diff --git a/web/src/lib/components/shared-components/album-selection/album-selection-modal.svelte b/web/src/lib/components/shared-components/album-selection/album-selection-modal.svelte
index 49b697b62a..50ffae0322 100644
--- a/web/src/lib/components/shared-components/album-selection/album-selection-modal.svelte
+++ b/web/src/lib/components/shared-components/album-selection/album-selection-modal.svelte
@@ -83,6 +83,7 @@
 <FullScreenModal title={shared ? $t('add_to_shared_album') : $t('add_to_album')} {onClose}>
   <div class="mb-2 flex max-h-[400px] flex-col">
     {#if loading}
+      <!-- eslint-disable-next-line svelte/require-each-key -->
       {#each { length: 3 } as _}
         <div class="flex animate-pulse gap-4 px-6 py-2">
           <div class="h-12 w-12 rounded-xl bg-slate-200"></div>
@@ -104,6 +105,7 @@
         use:initInput
       />
       <div class="immich-scrollbar overflow-y-auto">
+        <!-- eslint-disable-next-line svelte/require-each-key -->
         {#each albumModalRows as row}
           {#if row.type === AlbumModalRowType.NEW_ALBUM}
             <NewAlbumListItem selected={row.selected || false} {onNewAlbum} searchQuery={search} />
diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte
index d2dbeb5488..4d20d9df91 100644
--- a/web/src/lib/components/shared-components/change-location.svelte
+++ b/web/src/lib/components/shared-components/change-location.svelte
@@ -139,7 +139,7 @@
           use:clickOutside={{ onOutclick: () => (hideSuggestion = true) }}
         >
           {#if !hideSuggestion}
-            {#each suggestedPlaces as place, index}
+            {#each suggestedPlaces as place, index (place.latitude + place.longitude)}
               <button
                 type="button"
                 class=" flex w-full border-t border-gray-400 dark:border-immich-dark-gray h-14 place-items-center bg-gray-200 p-2 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-[#232932] focus:bg-gray-300 focus:dark:bg-[#232932] {index ===
diff --git a/web/src/lib/components/shared-components/control-app-bar.svelte b/web/src/lib/components/shared-components/control-app-bar.svelte
index 6f39536ef0..23bdaa8de3 100644
--- a/web/src/lib/components/shared-components/control-app-bar.svelte
+++ b/web/src/lib/components/shared-components/control-app-bar.svelte
@@ -75,7 +75,7 @@
   >
     <div class="flex place-items-center sm:gap-6 justify-self-start dark:text-immich-dark-fg">
       {#if showBackButton}
-        <CircleIconButton title={$t('close')} onclick={handleClose} icon={backIcon} size={'24'} class={buttonClass} />
+        <CircleIconButton title={$t('close')} onclick={handleClose} icon={backIcon} size="24" class={buttonClass} />
       {/if}
       {@render leading?.()}
     </div>
diff --git a/web/src/lib/components/shared-components/modal-header.svelte b/web/src/lib/components/shared-components/modal-header.svelte
index 53f3fbdabb..f2e4c960fa 100644
--- a/web/src/lib/components/shared-components/modal-header.svelte
+++ b/web/src/lib/components/shared-components/modal-header.svelte
@@ -37,5 +37,5 @@
     </h1>
   </div>
 
-  <CircleIconButton onclick={onClose} icon={mdiClose} size={'20'} title={$t('close')} />
+  <CircleIconButton onclick={onClose} icon={mdiClose} size="20" title={$t('close')} />
 </div>
diff --git a/web/src/lib/components/shared-components/navigation-bar/avatar-selector.svelte b/web/src/lib/components/shared-components/navigation-bar/avatar-selector.svelte
index d762c7ba88..ce441d553b 100644
--- a/web/src/lib/components/shared-components/navigation-bar/avatar-selector.svelte
+++ b/web/src/lib/components/shared-components/navigation-bar/avatar-selector.svelte
@@ -18,7 +18,7 @@
 <FullScreenModal title={$t('select_avatar_color')} width="auto" {onClose}>
   <div class="flex items-center justify-center mt-4">
     <div class="grid grid-cols-2 md:grid-cols-5 gap-4">
-      {#each colors as color}
+      {#each colors as color (color)}
         <button type="button" onclick={() => onChoose(color)}>
           <UserAvatar label={color} {user} {color} size="xl" showProfileImage={false} />
         </button>
diff --git a/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte b/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte
index 0334fb9e99..eaf6d14674 100644
--- a/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte
+++ b/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte
@@ -15,7 +15,7 @@
 </script>
 
 <Portal>
-  <FullScreenModal showLogo title={''} {onClose} width="wide">
+  <FullScreenModal showLogo title="" {onClose} width="wide">
     {#if showProductActivated}
       <PurchaseActivationSuccess onDone={onClose} />
     {:else}
diff --git a/web/src/lib/components/shared-components/scrubber/scrubber.svelte b/web/src/lib/components/shared-components/scrubber/scrubber.svelte
index bdcca509bb..c51c03dd0f 100644
--- a/web/src/lib/components/shared-components/scrubber/scrubber.svelte
+++ b/web/src/lib/components/shared-components/scrubber/scrubber.svelte
@@ -278,7 +278,7 @@
     {/if}
   </div>
   <!-- Time Segment -->
-  {#each segments as segment}
+  {#each segments as segment (segment.date)}
     <div
       id="time-segment"
       class="relative"
diff --git a/web/src/lib/components/shared-components/settings/setting-accordion.svelte b/web/src/lib/components/shared-components/settings/setting-accordion.svelte
index 0fe1c9dc14..bfa379edcd 100755
--- a/web/src/lib/components/shared-components/settings/setting-accordion.svelte
+++ b/web/src/lib/components/shared-components/settings/setting-accordion.svelte
@@ -44,6 +44,7 @@
       }
     } else {
       $accordionState.delete(key);
+      // eslint-disable-next-line no-self-assign
       $accordionState = $accordionState;
     }
   };
diff --git a/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte b/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte
index 09f0ea438b..048301442e 100644
--- a/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte
+++ b/web/src/lib/components/shared-components/settings/setting-checkboxes.svelte
@@ -30,7 +30,7 @@
 </script>
 
 <div class="mb-4 w-full">
-  <div class={`flex h-[26px] place-items-center gap-1`}>
+  <div class="flex h-[26px] place-items-center gap-1">
     <label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="{name}-select">
       {label}
     </label>
@@ -51,7 +51,7 @@
     </p>
   {/if}
   <div class="flex flex-col gap-2">
-    {#each options as option}
+    {#each options as option (option.value)}
       <Checkbox
         id="{option.value}-checkbox"
         label={option.text}
diff --git a/web/src/lib/components/shared-components/settings/setting-input-field.svelte b/web/src/lib/components/shared-components/settings/setting-input-field.svelte
index 5ef1a764f7..d904089731 100644
--- a/web/src/lib/components/shared-components/settings/setting-input-field.svelte
+++ b/web/src/lib/components/shared-components/settings/setting-input-field.svelte
@@ -70,7 +70,7 @@
 </script>
 
 <div class="mb-4 w-full">
-  <div class={`flex place-items-center gap-1`}>
+  <div class="flex place-items-center gap-1">
     <label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for={label}>{label}</label>
     {#if required}
       <div class="text-red-400">*</div>
diff --git a/web/src/lib/components/shared-components/settings/setting-select.svelte b/web/src/lib/components/shared-components/settings/setting-select.svelte
index 44f03075da..1cfd7123ac 100644
--- a/web/src/lib/components/shared-components/settings/setting-select.svelte
+++ b/web/src/lib/components/shared-components/settings/setting-select.svelte
@@ -39,7 +39,7 @@
 </script>
 
 <div class="mb-4 w-full">
-  <div class={`flex h-[26px] place-items-center gap-1`}>
+  <div class="flex h-[26px] place-items-center gap-1">
     <label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="{name}-select"
       >{label}</label
     >
@@ -63,7 +63,7 @@
   <div class="grid">
     <Icon
       path={mdiChevronDown}
-      size={'1.2em'}
+      size="1.2em"
       ariaHidden={true}
       class="pointer-events-none right-1 relative col-start-1 row-start-1 self-center justify-self-end {disabled
         ? 'text-immich-bg'
@@ -78,7 +78,7 @@
       bind:value
       onchange={handleChange}
     >
-      {#each options as option}
+      {#each options as option (option.value)}
         <option value={option.value}>{option.text}</option>
       {/each}
     </select>
diff --git a/web/src/lib/components/shared-components/settings/setting-textarea.svelte b/web/src/lib/components/shared-components/settings/setting-textarea.svelte
index 9f9f885263..be638d25d9 100644
--- a/web/src/lib/components/shared-components/settings/setting-textarea.svelte
+++ b/web/src/lib/components/shared-components/settings/setting-textarea.svelte
@@ -30,7 +30,7 @@
 </script>
 
 <div class="mb-4 w-full">
-  <div class={`flex h-[26px] place-items-center gap-1`}>
+  <div class="flex h-[26px] place-items-center gap-1">
     <label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for={label}>{label}</label>
     {#if required}
       <div class="text-red-400">*</div>
diff --git a/web/src/lib/components/shared-components/show-shortcuts.svelte b/web/src/lib/components/shared-components/show-shortcuts.svelte
index 9ca35c927e..5a940e6621 100644
--- a/web/src/lib/components/shared-components/show-shortcuts.svelte
+++ b/web/src/lib/components/shared-components/show-shortcuts.svelte
@@ -50,10 +50,10 @@
       <div class="p-4">
         <h2>{$t('general')}</h2>
         <div class="text-sm">
-          {#each shortcuts.general as shortcut}
+          {#each shortcuts.general as shortcut (shortcut.key.join('-'))}
             <div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
               <div class="flex justify-self-end">
-                {#each shortcut.key as key}
+                {#each shortcut.key as key (key)}
                   <p class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
                     {key}
                   </p>
@@ -69,10 +69,10 @@
       <div class="p-4">
         <h2>{$t('actions')}</h2>
         <div class="text-sm">
-          {#each shortcuts.actions as shortcut}
+          {#each shortcuts.actions as shortcut (shortcut.key.join('-'))}
             <div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
               <div class="flex justify-self-end">
-                {#each shortcut.key as key}
+                {#each shortcut.key as key (key)}
                   <p class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
                     {key}
                   </p>
diff --git a/web/src/lib/components/shared-components/side-bar/recent-albums.svelte b/web/src/lib/components/shared-components/side-bar/recent-albums.svelte
index b11935d643..9674ed9366 100644
--- a/web/src/lib/components/shared-components/side-bar/recent-albums.svelte
+++ b/web/src/lib/components/shared-components/side-bar/recent-albums.svelte
@@ -23,7 +23,7 @@
   });
 </script>
 
-{#each albums as album}
+{#each albums as album (album.id)}
   <a
     href={'/albums/' + album.id}
     title={album.albumName}
diff --git a/web/src/lib/components/shared-components/star-rating.svelte b/web/src/lib/components/shared-components/star-rating.svelte
index 333248c227..bf9e5fbcaf 100644
--- a/web/src/lib/components/shared-components/star-rating.svelte
+++ b/web/src/lib/components/shared-components/star-rating.svelte
@@ -71,7 +71,7 @@
 >
   <legend class="sr-only">{$t('rating')}</legend>
   <div class="flex flex-row" data-testid="star-container">
-    {#each { length: count } as _, index}
+    {#each { length: count } as _, index (index)}
       {@const value = index + 1}
       {@const filled = hoverRating >= value || (hoverRating === 0 && ratingSelection >= value)}
       {@const starId = `${id}-${value}`}
diff --git a/web/src/lib/components/shared-components/tree/breadcrumbs.svelte b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte
index c4ea635687..6ed915e683 100644
--- a/web/src/lib/components/shared-components/tree/breadcrumbs.svelte
+++ b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte
@@ -45,7 +45,7 @@
           onclick={() => {}}
         />
       </li>
-      {#each pathSegments as segment, index}
+      {#each pathSegments as segment, index (segment)}
         {@const isLastSegment = index === pathSegments.length - 1}
         <li
           class="flex gap-2 items-center font-mono text-sm text-nowrap text-immich-primary dark:text-immich-dark-primary"
diff --git a/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte b/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte
index 36ef781656..71d87acb8d 100644
--- a/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte
+++ b/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte
@@ -14,7 +14,7 @@
   <div
     class="w-full grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-6 2xl:grid-cols-8 gap-2 bg-gray-50 dark:bg-immich-dark-gray/50 rounded-2xl border border-gray-100 dark:border-gray-900"
   >
-    {#each items as item}
+    {#each items as item (item)}
       <button
         class="flex flex-col place-items-center gap-2 py-2 px-4 hover:bg-immich-primary/10 dark:hover:bg-immich-primary/40 rounded-xl"
         onclick={() => onClick(item)}
diff --git a/web/src/lib/components/shared-components/tree/tree-items.svelte b/web/src/lib/components/shared-components/tree/tree-items.svelte
index 3724ced6c9..3b770e47ef 100644
--- a/web/src/lib/components/shared-components/tree/tree-items.svelte
+++ b/web/src/lib/components/shared-components/tree/tree-items.svelte
@@ -15,6 +15,7 @@
 </script>
 
 <ul class="list-none ml-2">
+  <!-- eslint-disable-next-line svelte/require-each-key -->
   {#each Object.entries(items).sort() as [path, tree]}
     {@const value = normalizeTreePath(`${parent}/${path}`)}
     {@const key = value + getColor(value)}
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 bb202b3606..ab9159eae9 100644
--- a/web/src/lib/components/user-settings-page/device-list.svelte
+++ b/web/src/lib/components/user-settings-page/device-list.svelte
@@ -71,7 +71,7 @@
       <h3 class="mb-2 text-xs font-medium text-immich-primary dark:text-immich-dark-primary">
         {$t('other_devices').toUpperCase()}
       </h3>
-      {#each otherDevices as device, index}
+      {#each otherDevices as device, index (device.id)}
         <DeviceCard {device} onDelete={() => handleDelete(device)} />
         {#if index !== otherDevices.length - 1}
           <hr class="my-3" />
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 070246b612..d4080eb042 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
@@ -39,7 +39,7 @@
 <FullScreenModal title={$t('add_partner')} showLogo {onClose}>
   <div class="immich-scrollbar max-h-[300px] overflow-y-auto">
     {#if availableUsers.length > 0}
-      {#each availableUsers as user}
+      {#each availableUsers as user (user.id)}
         <button
           type="button"
           onclick={() => selectUser(user)}
diff --git a/web/src/lib/components/user-settings-page/partner-settings.svelte b/web/src/lib/components/user-settings-page/partner-settings.svelte
index a71266a76b..7db78082bb 100644
--- a/web/src/lib/components/user-settings-page/partner-settings.svelte
+++ b/web/src/lib/components/user-settings-page/partner-settings.svelte
@@ -144,7 +144,7 @@
             <CircleIconButton
               onclick={() => handleRemovePartner(partner.user)}
               icon={mdiClose}
-              size={'16'}
+              size="16"
               title={$t('stop_sharing_photos_with_user')}
             />
           {/if}
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 c5c6bae9e5..ffea572111 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
@@ -137,37 +137,35 @@
           </tr>
         </thead>
         <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
-          {#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 ${
-                  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>
-                <td class="w-1/3 text-ellipsis px-4 text-sm"
-                  >{new Date(key.createdAt).toLocaleDateString($locale, format)}
-                </td>
-                <td class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1 w-1/3">
-                  <CircleIconButton
-                    color="primary"
-                    icon={mdiPencilOutline}
-                    title={$t('edit_key')}
-                    size="16"
-                    onclick={() => (editKey = key)}
-                  />
-                  <CircleIconButton
-                    color="primary"
-                    icon={mdiTrashCanOutline}
-                    title={$t('delete_key')}
-                    size="16"
-                    onclick={() => handleDelete(key)}
-                  />
-                </td>
-              </tr>
-            {/key}
+          {#each keys as key, index (key.id)}
+            <tr
+              class={`flex h-[80px] w-full place-items-center text-center dark:text-immich-dark-fg ${
+                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>
+              <td class="w-1/3 text-ellipsis px-4 text-sm"
+                >{new Date(key.createdAt).toLocaleDateString($locale, format)}
+              </td>
+              <td class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1 w-1/3">
+                <CircleIconButton
+                  color="primary"
+                  icon={mdiPencilOutline}
+                  title={$t('edit_key')}
+                  size="16"
+                  onclick={() => (editKey = key)}
+                />
+                <CircleIconButton
+                  color="primary"
+                  icon={mdiTrashCanOutline}
+                  title={$t('delete_key')}
+                  size="16"
+                  onclick={() => handleDelete(key)}
+                />
+              </td>
+            </tr>
           {/each}
         </tbody>
       </table>
diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 5bb1ecce03..8d2808ac67 100644
--- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -137,7 +137,7 @@
     await loadNextPage();
   }
 
-  export const loadNextPage = async () => {
+  const loadNextPage = async () => {
     if (!nextPage || searchResultAssets.length >= MAX_ASSET_COUNT) {
       return;
     }
@@ -230,6 +230,7 @@
     return tagNames.join(', ');
   }
 
+  // eslint-disable-next-line no-self-assign
   const triggerAssetUpdate = () => (searchResultAssets = searchResultAssets);
 
   const onAddToAlbum = (assetIds: string[]) => {
diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte
index aadbf3e949..6fe8dea1f3 100644
--- a/web/src/routes/admin/user-management/+page.svelte
+++ b/web/src/routes/admin/user-management/+page.svelte
@@ -195,7 +195,7 @@
         </thead>
         <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
           {#if allUsers}
-            {#each allUsers as immichUser, index}
+            {#each allUsers as immichUser, index (immichUser.id)}
               <tr
                 class="flex h-[80px] overflow-hidden w-full place-items-center text-center dark:text-immich-dark-fg {immichUser.deletedAt
                   ? 'bg-red-300 dark:bg-red-900'