diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 64c084fa2e..d52ca4e6f7 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -593,8 +593,8 @@ jobs:
           echo "Changed files: ${CHANGED_FILES}"
           exit 1
 
-  generated-typeorm-migrations-up-to-date:
-    name: TypeORM Checks
+  sql-schema-up-to-date:
+    name: SQL Schema Checks
     runs-on: ubuntu-latest
     permissions:
       contents: read
@@ -641,7 +641,7 @@ jobs:
 
       - name: Generate new migrations
         continue-on-error: true
-        run: npm run migrations:generate TestMigration
+        run: npm run migrations:generate src/TestMigration
 
       - name: Find file changes
         uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20
diff --git a/i18n/en.json b/i18n/en.json
index c17d55872c..d2e109dec0 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -8,6 +8,8 @@
   "unable_to_create_pincode": "Unable to create PIN code",
   "pincode_changed_successfully": "PIN code changed successfully",
   "pincode_created_successfully": "PIN code created successfully",
+  "pincode_reset_successfully": "PIN code reset successfully",
+  "reset_pincode": "Reset PIN code",
   "create_pincode": "Create PIN code",
   "about": "About",
   "account": "Account",
@@ -63,6 +65,7 @@
     "confirm_email_below": "To confirm, type \"{email}\" below",
     "confirm_reprocess_all_faces": "Are you sure you want to reprocess all faces? This will also clear named people.",
     "confirm_user_password_reset": "Are you sure you want to reset {user}'s password?",
+    "confirm_user_pincode_reset": "Are you sure you want to reset {user}'s PIN code?",
     "create_job": "Create job",
     "cron_expression": "Cron expression",
     "cron_expression_description": "Set the scanning interval using the cron format. For more information please refer to e.g. <link>Crontab Guru</link>",
@@ -932,6 +935,7 @@
     "unable_to_remove_reaction": "Unable to remove reaction",
     "unable_to_repair_items": "Unable to repair items",
     "unable_to_reset_password": "Unable to reset password",
+    "unable_to_reset_pincode": "Unable to reset PIN code",
     "unable_to_resolve_duplicate": "Unable to resolve duplicate",
     "unable_to_restore_assets": "Unable to restore assets",
     "unable_to_restore_trash": "Unable to restore trash",
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index c83971b0de..2244dc4c5e 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -114,6 +114,7 @@ Class | Method | HTTP request | Description
 *AuthenticationApi* | [**getAuthStatus**](doc//AuthenticationApi.md#getauthstatus) | **GET** /auth/status | 
 *AuthenticationApi* | [**login**](doc//AuthenticationApi.md#login) | **POST** /auth/login | 
 *AuthenticationApi* | [**logout**](doc//AuthenticationApi.md#logout) | **POST** /auth/logout | 
+*AuthenticationApi* | [**resetPincode**](doc//AuthenticationApi.md#resetpincode) | **POST** /auth/reset-pincode | 
 *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | 
 *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | 
 *DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | 
@@ -398,6 +399,7 @@ Class | Method | HTTP request | Description
  - [RatingsUpdate](doc//RatingsUpdate.md)
  - [ReactionLevel](doc//ReactionLevel.md)
  - [ReactionType](doc//ReactionType.md)
+ - [ResetPincodeDto](doc//ResetPincodeDto.md)
  - [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
  - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
  - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md)
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 03f4b4ae27..fc4ab98c80 100644
--- a/mobile/openapi/lib/api.dart
+++ b/mobile/openapi/lib/api.dart
@@ -199,6 +199,7 @@ part 'model/ratings_response.dart';
 part 'model/ratings_update.dart';
 part 'model/reaction_level.dart';
 part 'model/reaction_type.dart';
+part 'model/reset_pincode_dto.dart';
 part 'model/reverse_geocoding_state_response_dto.dart';
 part 'model/search_album_response_dto.dart';
 part 'model/search_asset_response_dto.dart';
diff --git a/mobile/openapi/lib/api/authentication_api.dart b/mobile/openapi/lib/api/authentication_api.dart
index a8809aa63b..3e3d2f5b62 100644
--- a/mobile/openapi/lib/api/authentication_api.dart
+++ b/mobile/openapi/lib/api/authentication_api.dart
@@ -286,6 +286,53 @@ class AuthenticationApi {
     return null;
   }
 
+  /// Performs an HTTP 'POST /auth/reset-pincode' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [ResetPincodeDto] resetPincodeDto (required):
+  Future<Response> resetPincodeWithHttpInfo(ResetPincodeDto resetPincodeDto,) async {
+    // ignore: prefer_const_declarations
+    final apiPath = r'/auth/reset-pincode';
+
+    // ignore: prefer_final_locals
+    Object? postBody = resetPincodeDto;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    const contentTypes = <String>['application/json'];
+
+
+    return apiClient.invokeAPI(
+      apiPath,
+      'POST',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [ResetPincodeDto] resetPincodeDto (required):
+  Future<UserAdminResponseDto?> resetPincode(ResetPincodeDto resetPincodeDto,) async {
+    final response = await resetPincodeWithHttpInfo(resetPincodeDto,);
+    if (response.statusCode >= HttpStatus.badRequest) {
+      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+    }
+    // When a remote server returns no body with a status of 204, we shall not decode it.
+    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+    // FormatException when trying to decode an empty string.
+    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserAdminResponseDto',) as UserAdminResponseDto;
+    
+    }
+    return null;
+  }
+
   /// Performs an HTTP 'POST /auth/admin-sign-up' operation and returns the [Response].
   /// Parameters:
   ///
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index 621a298253..4caee90715 100644
--- a/mobile/openapi/lib/api_client.dart
+++ b/mobile/openapi/lib/api_client.dart
@@ -454,6 +454,8 @@ class ApiClient {
           return ReactionLevelTypeTransformer().decode(value);
         case 'ReactionType':
           return ReactionTypeTypeTransformer().decode(value);
+        case 'ResetPincodeDto':
+          return ResetPincodeDto.fromJson(value);
         case 'ReverseGeocodingStateResponseDto':
           return ReverseGeocodingStateResponseDto.fromJson(value);
         case 'SearchAlbumResponseDto':
diff --git a/mobile/openapi/lib/model/reset_pincode_dto.dart b/mobile/openapi/lib/model/reset_pincode_dto.dart
new file mode 100644
index 0000000000..29a3a75b30
--- /dev/null
+++ b/mobile/openapi/lib/model/reset_pincode_dto.dart
@@ -0,0 +1,99 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.18
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class ResetPincodeDto {
+  /// Returns a new [ResetPincodeDto] instance.
+  ResetPincodeDto({
+    required this.userId,
+  });
+
+  String userId;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is ResetPincodeDto &&
+    other.userId == userId;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (userId.hashCode);
+
+  @override
+  String toString() => 'ResetPincodeDto[userId=$userId]';
+
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+      json[r'userId'] = this.userId;
+    return json;
+  }
+
+  /// Returns a new [ResetPincodeDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static ResetPincodeDto? fromJson(dynamic value) {
+    upgradeDto(value, "ResetPincodeDto");
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      return ResetPincodeDto(
+        userId: mapValueOfType<String>(json, r'userId')!,
+      );
+    }
+    return null;
+  }
+
+  static List<ResetPincodeDto> listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <ResetPincodeDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = ResetPincodeDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, ResetPincodeDto> mapFromJson(dynamic json) {
+    final map = <String, ResetPincodeDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = ResetPincodeDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of ResetPincodeDto-objects as value to a dart map
+  static Map<String, List<ResetPincodeDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<ResetPincodeDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      // ignore: parameter_assignments
+      json = json.cast<String, dynamic>();
+      for (final entry in json.entries) {
+        map[entry.key] = ResetPincodeDto.listFromJson(entry.value, growable: growable,);
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+    'userId',
+  };
+}
+
diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts
index 25393ccab6..cfe3bec365 100644
--- a/open-api/typescript-sdk/src/fetch-client.ts
+++ b/open-api/typescript-sdk/src/fetch-client.ts
@@ -517,6 +517,9 @@ export type LogoutResponseDto = {
     redirectUri: string;
     successful: boolean;
 };
+export type ResetPincodeDto = {
+    userId: string;
+};
 export type AuthStatusResponseDto = {
     hasPincode: boolean;
 };
@@ -2051,6 +2054,18 @@ export function logout(opts?: Oazapfts.RequestOpts) {
         method: "POST"
     }));
 }
+export function resetPincode({ resetPincodeDto }: {
+    resetPincodeDto: ResetPincodeDto;
+}, opts?: Oazapfts.RequestOpts) {
+    return oazapfts.ok(oazapfts.fetchJson<{
+        status: 200;
+        data: UserAdminResponseDto;
+    }>("/auth/reset-pincode", oazapfts.json({
+        ...opts,
+        method: "POST",
+        body: resetPincodeDto
+    })));
+}
 export function getAuthStatus(opts?: Oazapfts.RequestOpts) {
     return oazapfts.ok(oazapfts.fetchJson<{
         status: 200;
diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts
index 9c080039ac..ed45637890 100644
--- a/server/src/repositories/user.repository.ts
+++ b/server/src/repositories/user.repository.ts
@@ -90,13 +90,13 @@ export class UserRepository {
   }
 
   @GenerateSql({ params: [DummyValue.EMAIL] })
-  getByEmail(email: string, withPassword?: boolean, withPincode?: boolean) {
+  getByEmail(email: string, options?: { withPassword?: boolean; withPincode?: boolean }) {
     return this.db
       .selectFrom('users')
       .select(columns.userAdmin)
       .select(withMetadata)
-      .$if(!!withPassword, (eb) => eb.select('password'))
-      .$if(!!withPincode, (eb) => eb.select('pincode'))
+      .$if(!!options?.withPassword, (eb) => eb.select('password'))
+      .$if(!!options?.withPincode, (eb) => eb.select('pincode'))
       .where('email', '=', email)
       .where('users.deletedAt', 'is', null)
       .executeTakeFirst();
diff --git a/server/src/schema/enums.ts b/server/src/schema/enums.ts
index 100b92aa63..a1134df6bc 100644
--- a/server/src/schema/enums.ts
+++ b/server/src/schema/enums.ts
@@ -1,4 +1,4 @@
-import { AssetStatus, SourceType } from 'src/enum';
+import { AssetStatus, AssetVisibility, SourceType } from 'src/enum';
 import { registerEnum } from 'src/sql-tools';
 
 export const assets_status_enum = registerEnum({
@@ -10,3 +10,8 @@ export const asset_face_source_type = registerEnum({
   name: 'sourcetype',
   values: Object.values(SourceType),
 });
+
+export const asset_visibility_enum = registerEnum({
+  name: 'asset_visibility_enum',
+  values: Object.values(AssetVisibility),
+});
diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts
index 1800f08c13..735dfd3ae9 100644
--- a/server/src/schema/index.ts
+++ b/server/src/schema/index.ts
@@ -1,5 +1,4 @@
-import { AssetVisibility } from 'src/enum';
-import { asset_face_source_type, assets_status_enum } from 'src/schema/enums';
+import { asset_face_source_type, asset_visibility_enum, assets_status_enum } from 'src/schema/enums';
 import {
   assets_delete_audit,
   f_concat_ws,
@@ -46,12 +45,7 @@ import { UserAuditTable } from 'src/schema/tables/user-audit.table';
 import { UserMetadataTable } from 'src/schema/tables/user-metadata.table';
 import { UserTable } from 'src/schema/tables/user.table';
 import { VersionHistoryTable } from 'src/schema/tables/version-history.table';
-import { ConfigurationParameter, Database, Extensions, registerEnum } from 'src/sql-tools';
-
-export const asset_visibility_enum = registerEnum({
-  name: 'asset_visibility_enum',
-  values: Object.values(AssetVisibility),
-});
+import { ConfigurationParameter, Database, Extensions } from 'src/sql-tools';
 
 @Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql'])
 @ConfigurationParameter({ name: 'search_path', value: () => '"$user", public, vectors', scope: 'database' })
@@ -107,5 +101,5 @@ export class ImmichDatabase {
     assets_delete_audit,
   ];
 
-  enum = [assets_status_enum, asset_face_source_type];
+  enum = [assets_status_enum, asset_face_source_type, asset_visibility_enum];
 }
diff --git a/server/src/schema/migrations/1746636476623-DropExtraIndexes.ts b/server/src/schema/migrations/1746636476623-DropExtraIndexes.ts
new file mode 100644
index 0000000000..aae52829a5
--- /dev/null
+++ b/server/src/schema/migrations/1746636476623-DropExtraIndexes.ts
@@ -0,0 +1,21 @@
+import { Kysely, sql } from 'kysely';
+
+export async function up(db: Kysely<any>): Promise<void> {
+  const { rows } = await sql<{ db: string }>`SELECT current_database() as db`.execute(db);
+  const databaseName = rows[0].db;
+  await sql.raw(`ALTER DATABASE "${databaseName}" SET search_path TO "$user", public, vectors`).execute(db);
+  await sql`ALTER TABLE "naturalearth_countries" DROP CONSTRAINT IF EXISTS "PK_21a6d86d1ab5d841648212e5353";`.execute(db);
+  await sql`ALTER TABLE "naturalearth_countries" DROP CONSTRAINT IF EXISTS "naturalearth_countries_pkey";`.execute(db);
+  await sql`ALTER TABLE "naturalearth_countries" ADD CONSTRAINT "naturalearth_countries_pkey" PRIMARY KEY ("id") WITH (FILLFACTOR = 100);`.execute(db);
+  await sql`DROP INDEX IF EXISTS "IDX_02a43fd0b3c50fb6d7f0cb7282";`.execute(db);
+  await sql`DROP INDEX IF EXISTS "IDX_95ad7106dd7b484275443f580f";`.execute(db);
+  await sql`DROP INDEX IF EXISTS "IDX_7e077a8b70b3530138610ff5e0";`.execute(db);
+  await sql`DROP INDEX IF EXISTS "IDX_92e67dc508c705dd66c9461557";`.execute(db);
+  await sql`DROP INDEX IF EXISTS "IDX_6afb43681a21cf7815932bc38a";`.execute(db);
+}
+
+export async function down(db: Kysely<any>): Promise<void> {
+  const { rows } = await sql<{ db: string }>`SELECT current_database() as db`.execute(db);
+  const databaseName = rows[0].db;
+  await sql.raw(`ALTER DATABASE "${databaseName}" RESET "search_path"`).execute(db);
+}
diff --git a/server/src/schema/tables/asset.table.ts b/server/src/schema/tables/asset.table.ts
index 4552ac158d..d337984a46 100644
--- a/server/src/schema/tables/asset.table.ts
+++ b/server/src/schema/tables/asset.table.ts
@@ -1,7 +1,6 @@
 import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
 import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
-import { asset_visibility_enum } from 'src/schema';
-import { assets_status_enum } from 'src/schema/enums';
+import { asset_visibility_enum, assets_status_enum } from 'src/schema/enums';
 import { assets_delete_audit } from 'src/schema/functions';
 import { LibraryTable } from 'src/schema/tables/library.table';
 import { StackTable } from 'src/schema/tables/stack.table';
diff --git a/server/src/schema/tables/natural-earth-countries.table.ts b/server/src/schema/tables/natural-earth-countries.table.ts
index df1132d17d..e5e6ead772 100644
--- a/server/src/schema/tables/natural-earth-countries.table.ts
+++ b/server/src/schema/tables/natural-earth-countries.table.ts
@@ -1,6 +1,6 @@
 import { Column, PrimaryGeneratedColumn, Table } from 'src/sql-tools';
 
-@Table({ name: 'naturalearth_countries' })
+@Table({ name: 'naturalearth_countries', primaryConstraintName: 'naturalearth_countries_pkey' })
 export class NaturalEarthCountriesTable {
   @PrimaryGeneratedColumn({ strategy: 'identity' })
   id!: number;
diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts
index fb4499e0fc..113691d477 100644
--- a/server/src/services/auth.service.spec.ts
+++ b/server/src/services/auth.service.spec.ts
@@ -119,7 +119,7 @@ describe(AuthService.name, () => {
 
       await sut.changePassword(auth, dto);
 
-      expect(mocks.user.getByEmail).toHaveBeenCalledWith(auth.user.email, true);
+      expect(mocks.user.getByEmail).toHaveBeenCalledWith(auth.user.email, { withPassword: true });
       expect(mocks.crypto.compareBcrypt).toHaveBeenCalledWith('old-password', 'hash-password');
     });
 
@@ -872,7 +872,7 @@ describe(AuthService.name, () => {
 
       await sut.createPincode(auth, dto);
 
-      expect(mocks.user.getByEmail).toHaveBeenCalledWith(auth.user.email, false, true);
+      expect(mocks.user.getByEmail).toHaveBeenCalledWith(auth.user.email, { withPincode: true });
       expect(mocks.crypto.hashBcrypt).toHaveBeenCalledWith('new-pincode', SALT_ROUNDS);
       expect(mocks.user.update).toHaveBeenCalledWith(user.id, { pincode: expect.any(String) });
     });
@@ -900,7 +900,7 @@ describe(AuthService.name, () => {
 
       await sut.changePincode(auth, dto);
 
-      expect(mocks.user.getByEmail).toHaveBeenCalledWith(auth.user.email, false, true);
+      expect(mocks.user.getByEmail).toHaveBeenCalledWith(auth.user.email, { withPincode: true });
       expect(mocks.crypto.compareBcrypt).toHaveBeenCalledWith('old-pincode', 'hash-pincode');
     });
 
diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts
index 848a813e29..705a29a334 100644
--- a/server/src/services/auth.service.ts
+++ b/server/src/services/auth.service.ts
@@ -60,9 +60,9 @@ export class AuthService extends BaseService {
       throw new UnauthorizedException('Password login has been disabled');
     }
 
-    let user = await this.userRepository.getByEmail(dto.email, true);
+    let user = await this.userRepository.getByEmail(dto.email, { withPassword: true });
     if (user) {
-      const isAuthenticated = this.validatePassword(dto.password, user);
+      const isAuthenticated = this.validateSecrect(dto.password, user.password);
       if (!isAuthenticated) {
         user = undefined;
       }
@@ -90,12 +90,12 @@ export class AuthService extends BaseService {
 
   async changePassword(auth: AuthDto, dto: ChangePasswordDto): Promise<UserAdminResponseDto> {
     const { password, newPassword } = dto;
-    const user = await this.userRepository.getByEmail(auth.user.email, true);
+    const user = await this.userRepository.getByEmail(auth.user.email, { withPassword: true });
     if (!user) {
       throw new UnauthorizedException();
     }
 
-    const valid = this.validatePassword(password, user);
+    const valid = this.validateSecrect(password, user.password);
     if (!valid) {
       throw new BadRequestException('Wrong password');
     }
@@ -108,7 +108,7 @@ export class AuthService extends BaseService {
   }
 
   async createPincode(auth: AuthDto, { pincode }: createPincodeDto): Promise<UserAdminResponseDto> {
-    const user = await this.userRepository.getByEmail(auth.user.email, false, true);
+    const user = await this.userRepository.getByEmail(auth.user.email, { withPincode: true });
     if (!user) {
       throw new UnauthorizedException();
     }
@@ -127,18 +127,18 @@ export class AuthService extends BaseService {
   async changePincode(auth: AuthDto, dto: ChangePincodeDto): Promise<UserAdminResponseDto> {
     const { pincode, newPincode } = dto;
 
-    const user = await this.userRepository.getByEmail(auth.user.email, false, true);
+    const user = await this.userRepository.getByEmail(auth.user.email, { withPincode: true });
     if (!user) {
       throw new UnauthorizedException();
     }
 
-    const valid = this.validatePincode(pincode, user);
+    const valid = this.validateSecrect(pincode, user.pincode);
     if (!valid) {
       throw new BadRequestException('Wrong pincode');
     }
 
-    const hasedPincode = await this.cryptoRepository.hashBcrypt(newPincode.toString(), SALT_ROUNDS);
-    const updatedUser = await this.userRepository.update(user.id, { pincode: hasedPincode });
+    const hashedPincode = await this.cryptoRepository.hashBcrypt(newPincode.toString(), SALT_ROUNDS);
+    const updatedUser = await this.userRepository.update(user.id, { pincode: hashedPincode });
 
     return mapUserAdmin(updatedUser);
   }
@@ -411,18 +411,12 @@ export class AuthService extends BaseService {
     throw new UnauthorizedException('Invalid API key');
   }
 
-  private validatePassword(inputPassword: string, user: { password?: string }): boolean {
-    if (!user || !user.password) {
+  private validateSecrect(inputSecret: string, existingHash?: string | null): boolean {
+    if (!existingHash) {
       return false;
     }
-    return this.cryptoRepository.compareBcrypt(inputPassword, user.password);
-  }
 
-  private validatePincode(inputPincode: string, user: { pincode?: string | null }): boolean {
-    if (!user || !user.pincode) {
-      return false;
-    }
-    return this.cryptoRepository.compareBcrypt(inputPincode, user.pincode);
+    return this.cryptoRepository.compareBcrypt(inputSecret, existingHash);
   }
 
   private async validateSession(tokenValue: string): Promise<AuthDto> {
@@ -477,20 +471,14 @@ export class AuthService extends BaseService {
   }
 
   async getAuthStatus(auth: AuthDto): Promise<AuthStatusResponseDto> {
-    const hasPincode = await this.hasPincode(auth);
-
-    return {
-      hasPincode,
-    };
-  }
-
-  private async hasPincode(auth: AuthDto): Promise<boolean> {
-    const user = await this.userRepository.getByEmail(auth.user.email, false, true);
+    const user = await this.userRepository.getByEmail(auth.user.email, { withPincode: true });
     if (!user) {
       throw new UnauthorizedException();
     }
 
-    return !!user.pincode;
+    return {
+      hasPincode: !!user.pincode,
+    };
   }
 
   async resetPincode(auth: AuthDto, dto: ResetPincodeDto): Promise<UserAdminResponseDto> {
diff --git a/web/src/lib/components/album-page/album-map.svelte b/web/src/lib/components/album-page/album-map.svelte
index 8230dea92e..871e26b4f9 100644
--- a/web/src/lib/components/album-page/album-map.svelte
+++ b/web/src/lib/components/album-page/album-map.svelte
@@ -130,6 +130,7 @@
               clickable={false}
               bind:mapMarkers
               onSelect={onViewAssets}
+              showSettings={false}
             />
           {/await}
         </div>
diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte
index 15bc42d001..d672b1a8b0 100644
--- a/web/src/lib/components/asset-viewer/detail-panel.svelte
+++ b/web/src/lib/components/asset-viewer/detail-panel.svelte
@@ -494,6 +494,7 @@
           },
         ]}
         center={latlng}
+        showSettings={false}
         zoom={12.5}
         simplified
         useLocationPin
diff --git a/web/src/lib/components/forms/api-key-form.svelte b/web/src/lib/components/forms/api-key-form.svelte
deleted file mode 100644
index bba7398655..0000000000
--- a/web/src/lib/components/forms/api-key-form.svelte
+++ /dev/null
@@ -1,55 +0,0 @@
-<script lang="ts">
-  import { mdiKeyVariant } from '@mdi/js';
-  import { t } from 'svelte-i18n';
-  import FullScreenModal from '../shared-components/full-screen-modal.svelte';
-  import { NotificationType, notificationController } from '../shared-components/notification/notification';
-  import { Button } from '@immich/ui';
-
-  interface Props {
-    apiKey: { name: string };
-    title: string;
-    cancelText?: string;
-    submitText?: string;
-    onSubmit: (apiKey: { name: string }) => void;
-    onCancel: () => void;
-  }
-
-  let {
-    apiKey = $bindable(),
-    title,
-    cancelText = $t('cancel'),
-    submitText = $t('save'),
-    onSubmit,
-    onCancel,
-  }: Props = $props();
-
-  const handleSubmit = () => {
-    if (apiKey.name) {
-      onSubmit({ name: apiKey.name });
-    } else {
-      notificationController.show({
-        message: $t('api_key_empty'),
-        type: NotificationType.Warning,
-      });
-    }
-  };
-
-  const onsubmit = (event: Event) => {
-    event.preventDefault();
-    handleSubmit();
-  };
-</script>
-
-<FullScreenModal {title} icon={mdiKeyVariant} onClose={() => onCancel()}>
-  <form {onsubmit} autocomplete="off" id="api-key-form">
-    <div class="mb-4 flex flex-col gap-2">
-      <label class="immich-form-label" for="name">{$t('name')}</label>
-      <input class="immich-form-input" id="name" name="name" type="text" bind:value={apiKey.name} />
-    </div>
-  </form>
-
-  {#snippet stickyBottom()}
-    <Button shape="round" color="secondary" fullWidth onclick={() => onCancel()}>{cancelText}</Button>
-    <Button shape="round" type="submit" fullWidth form="api-key-form">{submitText}</Button>
-  {/snippet}
-</FullScreenModal>
diff --git a/web/src/lib/components/forms/api-key-secret.svelte b/web/src/lib/components/forms/api-key-secret.svelte
deleted file mode 100644
index 0d3b88d85a..0000000000
--- a/web/src/lib/components/forms/api-key-secret.svelte
+++ /dev/null
@@ -1,32 +0,0 @@
-<script lang="ts">
-  import { copyToClipboard } from '$lib/utils';
-  import { Button } from '@immich/ui';
-  import { mdiKeyVariant } from '@mdi/js';
-  import { t } from 'svelte-i18n';
-  import FullScreenModal from '../shared-components/full-screen-modal.svelte';
-
-  interface Props {
-    secret?: string;
-    onDone: () => void;
-  }
-
-  let { secret = '', onDone }: Props = $props();
-</script>
-
-<FullScreenModal title={$t('api_key')} icon={mdiKeyVariant} onClose={onDone}>
-  <div class="text-immich-primary dark:text-immich-dark-primary">
-    <p class="text-sm dark:text-immich-dark-fg">
-      {$t('api_key_description')}
-    </p>
-  </div>
-
-  <div class="my-4 flex flex-col gap-2">
-    <!-- <label class="immich-form-label" for="secret">{ $t("api_key") }</label> -->
-    <textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret}></textarea>
-  </div>
-
-  {#snippet stickyBottom()}
-    <Button shape="round" onclick={() => copyToClipboard(secret)} fullWidth>{$t('copy_to_clipboard')}</Button>
-    <Button shape="round" onclick={onDone} fullWidth>{$t('done')}</Button>
-  {/snippet}
-</FullScreenModal>
diff --git a/web/src/lib/components/map-page/map-settings-modal.svelte b/web/src/lib/components/map-page/map-settings-modal.svelte
deleted file mode 100644
index c1ee1e0b80..0000000000
--- a/web/src/lib/components/map-page/map-settings-modal.svelte
+++ /dev/null
@@ -1,133 +0,0 @@
-<script lang="ts">
-  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
-  import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
-  import type { MapSettings } from '$lib/stores/preferences.store';
-  import { Button, Field, Stack, Switch } from '@immich/ui';
-  import { Duration } from 'luxon';
-  import { t } from 'svelte-i18n';
-  import { fly } from 'svelte/transition';
-  import DateInput from '../elements/date-input.svelte';
-
-  interface Props {
-    settings: MapSettings;
-    onClose: () => void;
-    onSave: (settings: MapSettings) => void;
-  }
-
-  let { settings: initialValues, onClose, onSave }: Props = $props();
-  let settings = $state(initialValues);
-
-  let customDateRange = $state(!!settings.dateAfter || !!settings.dateBefore);
-
-  const onsubmit = (event: Event) => {
-    event.preventDefault();
-    onSave(settings);
-  };
-</script>
-
-<form {onsubmit}>
-  <FullScreenModal title={$t('map_settings')} {onClose}>
-    <Stack gap={4}>
-      <Field label={$t('allow_dark_mode')}>
-        <Switch bind:checked={settings.allowDarkMode} />
-      </Field>
-      <Field label={$t('only_favorites')}>
-        <Switch bind:checked={settings.onlyFavorites} />
-      </Field>
-      <Field label={$t('include_archived')}>
-        <Switch bind:checked={settings.includeArchived} />
-      </Field>
-      <Field label={$t('include_shared_partner_assets')}>
-        <Switch bind:checked={settings.withPartners} />
-      </Field>
-      <Field label={$t('include_shared_albums')}>
-        <Switch bind:checked={settings.withSharedAlbums} />
-      </Field>
-
-      {#if customDateRange}
-        <div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
-          <div class="flex items-center justify-between gap-8">
-            <label class="immich-form-label shrink-0 text-sm" for="date-after">{$t('date_after')}</label>
-            <DateInput
-              class="immich-form-input w-40"
-              type="date"
-              id="date-after"
-              max={settings.dateBefore}
-              bind:value={settings.dateAfter}
-            />
-          </div>
-          <div class="flex items-center justify-between gap-8">
-            <label class="immich-form-label shrink-0 text-sm" for="date-before">{$t('date_before')}</label>
-            <DateInput class="immich-form-input w-40" type="date" id="date-before" bind:value={settings.dateBefore} />
-          </div>
-          <div class="flex justify-center text-xs">
-            <Button
-              color="primary"
-              size="small"
-              variant="ghost"
-              onclick={() => {
-                customDateRange = false;
-                settings.dateAfter = '';
-                settings.dateBefore = '';
-              }}
-            >
-              {$t('remove_custom_date_range')}
-            </Button>
-          </div>
-        </div>
-      {:else}
-        <div in:fly={{ y: -10, duration: 200 }} class="flex flex-col gap-1">
-          <SettingSelect
-            label={$t('date_range')}
-            name="date-range"
-            bind:value={settings.relativeDate}
-            options={[
-              {
-                value: '',
-                text: $t('all'),
-              },
-              {
-                value: Duration.fromObject({ hours: 24 }).toISO() || '',
-                text: $t('past_durations.hours', { values: { hours: 24 } }),
-              },
-              {
-                value: Duration.fromObject({ days: 7 }).toISO() || '',
-                text: $t('past_durations.days', { values: { days: 7 } }),
-              },
-              {
-                value: Duration.fromObject({ days: 30 }).toISO() || '',
-                text: $t('past_durations.days', { values: { days: 30 } }),
-              },
-              {
-                value: Duration.fromObject({ years: 1 }).toISO() || '',
-                text: $t('past_durations.years', { values: { years: 1 } }),
-              },
-              {
-                value: Duration.fromObject({ years: 3 }).toISO() || '',
-                text: $t('past_durations.years', { values: { years: 3 } }),
-              },
-            ]}
-          />
-          <div class="text-xs">
-            <Button
-              color="primary"
-              size="small"
-              variant="ghost"
-              onclick={() => {
-                customDateRange = true;
-                settings.relativeDate = '';
-              }}
-            >
-              {$t('use_custom_date_range')}
-            </Button>
-          </div>
-        </div>
-      {/if}
-    </Stack>
-
-    {#snippet stickyBottom()}
-      <Button color="secondary" shape="round" fullWidth onclick={onClose}>{$t('cancel')}</Button>
-      <Button type="submit" shape="round" fullWidth>{$t('save')}</Button>
-    {/snippet}
-  </FullScreenModal>
-</form>
diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte
index 3539945911..e502d9aeda 100644
--- a/web/src/lib/components/shared-components/change-location.svelte
+++ b/web/src/lib/components/shared-components/change-location.svelte
@@ -190,6 +190,7 @@
             simplified={true}
             clickable={true}
             onClickPoint={(selected) => (point = selected)}
+            showSettings={false}
           />
         {/await}
       </div>
diff --git a/web/src/lib/components/shared-components/map/map.svelte b/web/src/lib/components/shared-components/map/map.svelte
index 5adeb2f00f..bf66018eb6 100644
--- a/web/src/lib/components/shared-components/map/map.svelte
+++ b/web/src/lib/components/shared-components/map/map.svelte
@@ -9,15 +9,20 @@
 <script lang="ts">
   import Icon from '$lib/components/elements/icon.svelte';
   import { Theme } from '$lib/constants';
+  import { modalManager } from '$lib/managers/modal-manager.svelte';
   import { themeManager } from '$lib/managers/theme-manager.svelte';
+  import MapSettingsModal from '$lib/modals/MapSettingsModal.svelte';
   import { mapSettings } from '$lib/stores/preferences.store';
   import { serverConfig } from '$lib/stores/server-config.store';
   import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
-  import { type MapMarkerResponseDto } from '@immich/sdk';
+  import { getMapMarkers, type MapMarkerResponseDto } from '@immich/sdk';
   import mapboxRtlUrl from '@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js?url';
   import { mdiCog, mdiMap, mdiMapMarker } from '@mdi/js';
   import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson';
+  import { isEqual, omit } from 'lodash-es';
+  import { DateTime, Duration } from 'luxon';
   import maplibregl, { GlobeControl, type GeoJSONSource, type LngLatLike } from 'maplibre-gl';
+  import { onDestroy, onMount } from 'svelte';
   import { t } from 'svelte-i18n';
   import {
     AttributionControl,
@@ -36,8 +41,8 @@
   } from 'svelte-maplibre';
 
   interface Props {
-    mapMarkers: MapMarkerResponseDto[];
-    showSettingsModal?: boolean | undefined;
+    mapMarkers?: MapMarkerResponseDto[];
+    showSettings?: boolean;
     zoom?: number | undefined;
     center?: LngLatLike | undefined;
     hash?: boolean;
@@ -51,8 +56,8 @@
   }
 
   let {
-    mapMarkers = $bindable(),
-    showSettingsModal = $bindable(undefined),
+    mapMarkers = $bindable([]),
+    showSettings = true,
     zoom = undefined,
     center = $bindable(undefined),
     hash = false,
@@ -67,6 +72,7 @@
 
   let map: maplibregl.Map | undefined = $state();
   let marker: maplibregl.Marker | null = null;
+  let abortController: AbortController;
 
   const theme = $derived($mapSettings.allowDarkMode ? themeManager.value : Theme.LIGHT);
   const styleUrl = $derived(theme === Theme.DARK ? $serverConfig.mapDarkStyleUrl : $serverConfig.mapLightStyleUrl);
@@ -143,6 +149,72 @@
     };
   };
 
+  function getFileCreatedDates() {
+    const { relativeDate, dateAfter, dateBefore } = $mapSettings;
+
+    if (relativeDate) {
+      const duration = Duration.fromISO(relativeDate);
+      return {
+        fileCreatedAfter: duration.isValid ? DateTime.now().minus(duration).toISO() : undefined,
+      };
+    }
+
+    try {
+      return {
+        fileCreatedAfter: dateAfter ? new Date(dateAfter).toISOString() : undefined,
+        fileCreatedBefore: dateBefore ? new Date(dateBefore).toISOString() : undefined,
+      };
+    } catch {
+      $mapSettings.dateAfter = '';
+      $mapSettings.dateBefore = '';
+      return {};
+    }
+  }
+
+  async function loadMapMarkers() {
+    if (abortController) {
+      abortController.abort();
+    }
+    abortController = new AbortController();
+
+    const { includeArchived, onlyFavorites, withPartners, withSharedAlbums } = $mapSettings;
+    const { fileCreatedAfter, fileCreatedBefore } = getFileCreatedDates();
+
+    return await getMapMarkers(
+      {
+        isArchived: includeArchived && undefined,
+        isFavorite: onlyFavorites || undefined,
+        fileCreatedAfter: fileCreatedAfter || undefined,
+        fileCreatedBefore,
+        withPartners: withPartners || undefined,
+        withSharedAlbums: withSharedAlbums || undefined,
+      },
+      {
+        signal: abortController.signal,
+      },
+    );
+  }
+
+  const handleSettingsClick = async () => {
+    const settings = await modalManager.show(MapSettingsModal, { settings: { ...$mapSettings } });
+    if (settings) {
+      const shouldUpdate = !isEqual(omit(settings, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode'));
+      $mapSettings = settings;
+
+      if (shouldUpdate) {
+        mapMarkers = await loadMapMarkers();
+      }
+    }
+  };
+
+  onMount(async () => {
+    mapMarkers = await loadMapMarkers();
+  });
+
+  onDestroy(() => {
+    abortController.abort();
+  });
+
   $effect(() => {
     map?.setStyle(styleUrl, {
       transformStyle: (previousStyle, nextStyle) => {
@@ -199,10 +271,10 @@
       <AttributionControl compact={false} />
     {/if}
 
-    {#if showSettingsModal !== undefined}
+    {#if showSettings}
       <Control>
         <ControlGroup>
-          <ControlButton onclick={() => (showSettingsModal = true)}><Icon path={mdiCog} size="100%" /></ControlButton>
+          <ControlButton onclick={handleSettingsClick}><Icon path={mdiCog} size="100%" /></ControlButton>
         </ControlGroup>
       </Control>
     {/if}
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 53b90798d9..8cf2fb9dfc 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
@@ -6,12 +6,13 @@
   import { page } from '$app/state';
   import { clickOutside } from '$lib/actions/click-outside';
   import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
-  import HelpAndFeedbackModal from '$lib/components/shared-components/help-and-feedback-modal.svelte';
   import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
   import NotificationPanel from '$lib/components/shared-components/navigation-bar/notification-panel.svelte';
   import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
   import { AppRoute } from '$lib/constants';
   import { authManager } from '$lib/managers/auth-manager.svelte';
+  import { modalManager } from '$lib/managers/modal-manager.svelte';
+  import HelpAndFeedbackModal from '$lib/modals/HelpAndFeedbackModal.svelte';
   import { mobileDevice } from '$lib/stores/mobile-device.svelte';
   import { notificationManager } from '$lib/stores/notification-manager.svelte';
   import { featureFlags } from '$lib/stores/server-config.store';
@@ -35,7 +36,6 @@
   let { showUploadButton = true, onUploadClick }: Props = $props();
 
   let shouldShowAccountInfoPanel = $state(false);
-  let shouldShowHelpPanel = $state(false);
   let shouldShowNotificationPanel = $state(false);
   let innerWidth: number = $state(0);
   const hasUnreadNotifications = $derived(notificationManager.notifications.length > 0);
@@ -49,10 +49,6 @@
 
 <svelte:window bind:innerWidth />
 
-{#if shouldShowHelpPanel && info}
-  <HelpAndFeedbackModal onClose={() => (shouldShowHelpPanel = false)} {info} />
-{/if}
-
 <nav id="dashboard-navbar" class="z-auto max-md:h-[var(--navbar-height-md)] h-[var(--navbar-height)] w-dvw text-sm">
   <SkipLink text={$t('skip_to_content')} />
   <div
@@ -129,18 +125,14 @@
 
         <ThemeButton padding="2" />
 
-        <div
-          use:clickOutside={{
-            onEscape: () => (shouldShowHelpPanel = false),
-          }}
-        >
+        <div>
           <IconButton
             shape="round"
             color="secondary"
             variant="ghost"
             size="medium"
             icon={mdiHelpCircleOutline}
-            onclick={() => (shouldShowHelpPanel = !shouldShowHelpPanel)}
+            onclick={() => info && modalManager.show(HelpAndFeedbackModal, { info })}
             aria-label={$t('support_and_feedback')}
           />
         </div>
diff --git a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte
index fe48a68009..aafb430046 100644
--- a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte
+++ b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte
@@ -28,7 +28,7 @@
   const { isPurchased } = purchaseStore;
 
   const openPurchaseModal = async () => {
-    await modalManager.open(PurchaseModal);
+    await modalManager.show(PurchaseModal, {});
     showMessage = false;
   };
 
diff --git a/web/src/lib/components/shared-components/side-bar/server-status.svelte b/web/src/lib/components/shared-components/side-bar/server-status.svelte
index 665decc44f..0a9f3d9d8c 100644
--- a/web/src/lib/components/shared-components/side-bar/server-status.svelte
+++ b/web/src/lib/components/shared-components/side-bar/server-status.svelte
@@ -56,7 +56,7 @@
     {#if $connected && version}
       <button
         type="button"
-        onclick={() => info && modalManager.open(ServerAboutModal, { versions, info })}
+        onclick={() => info && modalManager.show(ServerAboutModal, { versions, info })}
         class="dark:text-immich-gray flex gap-1"
       >
         {#if isMain}
diff --git a/web/src/lib/components/user-settings-page/PincodeInput.svelte b/web/src/lib/components/user-settings-page/PincodeInput.svelte
index 70deef0044..a14e28335f 100644
--- a/web/src/lib/components/user-settings-page/PincodeInput.svelte
+++ b/web/src/lib/components/user-settings-page/PincodeInput.svelte
@@ -11,10 +11,11 @@
   let pinValues = $state(Array.from({ length: pinLength }).fill(''));
   let pincodeInputElements: HTMLInputElement[] = $state([]);
 
-  export function reset() {
-    pinValues = Array.from({ length: pinLength }).fill('');
-    value = '';
-  }
+  $effect(() => {
+    if (value === '') {
+      pinValues = Array.from({ length: pinLength }).fill('');
+    }
+  });
 
   const focusNext = (index: number) => {
     if (index < pinLength - 1) {
@@ -55,33 +56,37 @@
     const target = event.currentTarget as HTMLInputElement;
     const index = pincodeInputElements.indexOf(target);
 
-    if (event.key === 'Tab') {
-      return;
-    }
-
-    if (event.key === 'Backspace') {
-      if (target.value === '' && index > 0) {
-        focusPrev(index);
-        pinValues[index - 1] = '';
-      } else if (target.value !== '') {
-        pinValues[index] = '';
+    switch (event.key) {
+      case 'Tab': {
+        return;
+      }
+      case 'Backspace': {
+        if (target.value === '' && index > 0) {
+          focusPrev(index);
+          pinValues[index - 1] = '';
+        } else if (target.value !== '') {
+          pinValues[index] = '';
+        }
+        return;
+      }
+      case 'ArrowLeft': {
+        if (index > 0) {
+          focusPrev(index);
+        }
+        return;
+      }
+      case 'ArrowRight': {
+        if (index < pinLength - 1) {
+          focusNext(index);
+        }
+        return;
+      }
+      default: {
+        if (!/^\d$/.test(event.key)) {
+          event.preventDefault();
+        }
+        break;
       }
-
-      return;
-    }
-
-    if (event.key === 'ArrowLeft' && index > 0) {
-      focusPrev(index);
-      return;
-    }
-    if (event.key === 'ArrowRight' && index < pinLength - 1) {
-      focusNext(index);
-      return;
-    }
-
-    if (!/^\d$/.test(event.key) && event.key !== 'Backspace') {
-      event.preventDefault();
-      return;
     }
   }
 </script>
diff --git a/web/src/lib/components/user-settings-page/PincodeSettings.svelte b/web/src/lib/components/user-settings-page/PincodeSettings.svelte
index b3fd9cf950..fc7f8a5955 100644
--- a/web/src/lib/components/user-settings-page/PincodeSettings.svelte
+++ b/web/src/lib/components/user-settings-page/PincodeSettings.svelte
@@ -11,7 +11,6 @@
   import { t } from 'svelte-i18n';
   import { fade } from 'svelte/transition';
 
-  let pincodeFormElement = $state<HTMLFormElement | null>(null);
   let hasPincode = $state(false);
   let currentPincode = $state('');
   let newPincode = $state('');
@@ -99,13 +98,12 @@
     currentPincode = '';
     newPincode = '';
     confirmPincode = '';
-    pincodeFormElement?.reset();
   };
 </script>
 
 <section class="my-4">
   <div in:fade={{ duration: 200 }}>
-    <form bind:this={pincodeFormElement} autocomplete="off" onsubmit={onSubmit} class="mt-6">
+    <form autocomplete="off" onsubmit={onSubmit} class="mt-6">
       <div class="flex flex-col gap-6 place-items-center place-content-center">
         {#if hasPincode}
           <p class="text-dark">Change PIN code</p>
@@ -127,7 +125,10 @@
         {/if}
       </div>
 
-      <div class="flex justify-end">
+      <div class="flex justify-end gap-2 mt-4">
+        <Button shape="round" color="secondary" type="button" size="small" onclick={resetForm}>
+          {$t('clear')}
+        </Button>
         <Button shape="round" type="submit" size="small" loading={isLoading} disabled={!canSubmit}>
           {hasPincode ? $t('save') : $t('create_pincode')}
         </Button>
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
deleted file mode 100644
index 37c6580429..0000000000
--- a/web/src/lib/components/user-settings-page/partner-selection-modal.svelte
+++ /dev/null
@@ -1,79 +0,0 @@
-<script lang="ts">
-  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
-  import { getPartners, PartnerDirection, searchUsers, type UserResponseDto } from '@immich/sdk';
-  import { onMount } from 'svelte';
-  import { t } from 'svelte-i18n';
-  import UserAvatar from '../shared-components/user-avatar.svelte';
-  import { Button } from '@immich/ui';
-
-  interface Props {
-    user: UserResponseDto;
-    onClose: () => void;
-    onAddUsers: (users: UserResponseDto[]) => void;
-  }
-
-  let { user, onClose, onAddUsers }: Props = $props();
-
-  let availableUsers: UserResponseDto[] = $state([]);
-  let selectedUsers: UserResponseDto[] = $state([]);
-
-  onMount(async () => {
-    let users = await searchUsers();
-
-    // remove current user
-    users = users.filter((_user) => _user.id !== user.id);
-
-    // exclude partners from the list of users available for selection
-    const partners = await getPartners({ direction: PartnerDirection.SharedBy });
-    const partnerIds = new Set(partners.map((partner) => partner.id));
-    availableUsers = users.filter((user) => !partnerIds.has(user.id));
-  });
-
-  const selectUser = (user: UserResponseDto) => {
-    selectedUsers = selectedUsers.includes(user)
-      ? selectedUsers.filter((selectedUser) => selectedUser.id !== user.id)
-      : [...selectedUsers, user];
-  };
-</script>
-
-<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 (user.id)}
-        <button
-          type="button"
-          onclick={() => selectUser(user)}
-          class="flex w-full place-items-center gap-4 px-5 py-4 transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
-        >
-          {#if selectedUsers.includes(user)}
-            <span
-              class="flex h-12 w-12 place-content-center place-items-center rounded-full border bg-immich-primary text-3xl text-white dark:border-immich-dark-gray dark:bg-immich-dark-primary dark:text-immich-dark-bg"
-              >✓</span
-            >
-          {:else}
-            <UserAvatar {user} size="lg" />
-          {/if}
-
-          <div class="text-start">
-            <p class="text-immich-fg dark:text-immich-dark-fg">
-              {user.name}
-            </p>
-            <p class="text-xs">
-              {user.email}
-            </p>
-          </div>
-        </button>
-      {/each}
-    {:else}
-      <p class="py-5 text-sm">
-        {$t('photo_shared_all_users')}
-      </p>
-    {/if}
-
-    {#if selectedUsers.length > 0}
-      <div class="pt-5">
-        <Button shape="round" fullWidth onclick={() => onAddUsers(selectedUsers)}>{$t('add')}</Button>
-      </div>
-    {/if}
-  </div>
-</FullScreenModal>
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 b2238b84e2..b18390e28f 100644
--- a/web/src/lib/components/user-settings-page/partner-settings.svelte
+++ b/web/src/lib/components/user-settings-page/partner-settings.svelte
@@ -2,6 +2,8 @@
   import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
   import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
+  import { modalManager } from '$lib/managers/modal-manager.svelte';
+  import PartnerSelectionModal from '$lib/modals/PartnerSelectionModal.svelte';
   import {
     createPartner,
     getPartners,
@@ -18,7 +20,6 @@
   import { handleError } from '../../utils/handle-error';
   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
   import Icon from '../elements/icon.svelte';
-  import PartnerSelectionModal from './partner-selection-modal.svelte';
 
   interface PartnerSharing {
     user: UserResponseDto;
@@ -33,8 +34,6 @@
 
   let { user }: Props = $props();
 
-  let createPartnerFlag = $state(false);
-  // let removePartnerDto: PartnerResponseDto | null = null;
   let partners: Array<PartnerSharing> = $state([]);
 
   onMount(async () => {
@@ -99,14 +98,19 @@
     }
   };
 
-  const handleCreatePartners = async (users: UserResponseDto[]) => {
+  const handleCreatePartners = async () => {
+    const users = await modalManager.show(PartnerSelectionModal, { user });
+
+    if (!users) {
+      return;
+    }
+
     try {
       for (const user of users) {
         await createPartner({ id: user.id });
       }
 
       await refreshPartners();
-      createPartnerFlag = false;
     } catch (error) {
       handleError(error, $t('errors.unable_to_add_partners'));
     }
@@ -189,10 +193,6 @@
   {/if}
 
   <div class="flex justify-end mt-5">
-    <Button shape="round" size="small" onclick={() => (createPartnerFlag = true)}>{$t('add_partner')}</Button>
+    <Button shape="round" size="small" onclick={() => handleCreatePartners()}>{$t('add_partner')}</Button>
   </div>
 </section>
-
-{#if createPartnerFlag}
-  <PartnerSelectionModal {user} onClose={() => (createPartnerFlag = false)} onAddUsers={handleCreatePartners} />
-{/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 6fbc28a776..ab9ffb294c 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
@@ -1,6 +1,9 @@
 <script lang="ts">
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
   import { dialogController } from '$lib/components/shared-components/dialog/dialog';
+  import { modalManager } from '$lib/managers/modal-manager.svelte';
+  import ApiKeyModal from '$lib/modals/ApiKeyModal.svelte';
+  import ApiKeySecretModal from '$lib/modals/ApiKeySecretModal.svelte';
   import { locale } from '$lib/stores/preferences.store';
   import {
     createApiKey,
@@ -15,8 +18,6 @@
   import { t } from 'svelte-i18n';
   import { fade } from 'svelte/transition';
   import { handleError } from '../../utils/handle-error';
-  import APIKeyForm from '../forms/api-key-form.svelte';
-  import APIKeySecret from '../forms/api-key-secret.svelte';
   import { notificationController, NotificationType } from '../shared-components/notification/notification';
 
   interface Props {
@@ -25,10 +26,6 @@
 
   let { keys = $bindable() }: Props = $props();
 
-  let newKey: { name: string } | null = $state(null);
-  let editKey: ApiKeyResponseDto | null = $state(null);
-  let secret = $state('');
-
   const format: Intl.DateTimeFormatOptions = {
     month: 'short',
     day: 'numeric',
@@ -39,30 +36,46 @@
     keys = await getApiKeys();
   }
 
-  const handleCreate = async ({ name }: { name: string }) => {
-    try {
-      const data = await createApiKey({
-        apiKeyCreateDto: {
-          name,
-          permissions: [Permission.All],
-        },
-      });
-      secret = data.secret;
-    } catch (error) {
-      handleError(error, $t('errors.unable_to_create_api_key'));
-    } finally {
-      await refreshKeys();
-      newKey = null;
-    }
-  };
+  const handleCreate = async () => {
+    const result = await modalManager.show(ApiKeyModal, {
+      title: $t('new_api_key'),
+      apiKey: { name: 'API Key' },
+      submitText: $t('create'),
+    });
 
-  const handleUpdate = async (detail: Partial<ApiKeyResponseDto>) => {
-    if (!editKey || !detail.name) {
+    if (!result) {
       return;
     }
 
     try {
-      await updateApiKey({ id: editKey.id, apiKeyUpdateDto: { name: detail.name } });
+      const { secret } = await createApiKey({
+        apiKeyCreateDto: {
+          name: result.name,
+          permissions: [Permission.All],
+        },
+      });
+
+      await modalManager.show(ApiKeySecretModal, { secret });
+    } catch (error) {
+      handleError(error, $t('errors.unable_to_create_api_key'));
+    } finally {
+      await refreshKeys();
+    }
+  };
+
+  const handleUpdate = async (key: ApiKeyResponseDto) => {
+    const result = await modalManager.show(ApiKeyModal, {
+      title: $t('api_key'),
+      submitText: $t('save'),
+      apiKey: key,
+    });
+
+    if (!result) {
+      return;
+    }
+
+    try {
+      await updateApiKey({ id: key.id, apiKeyUpdateDto: { name: result.name } });
       notificationController.show({
         message: $t('saved_api_key'),
         type: NotificationType.Info,
@@ -71,7 +84,6 @@
       handleError(error, $t('errors.unable_to_save_api_key'));
     } finally {
       await refreshKeys();
-      editKey = null;
     }
   };
 
@@ -95,34 +107,10 @@
   };
 </script>
 
-{#if newKey}
-  <APIKeyForm
-    title={$t('new_api_key')}
-    submitText={$t('create')}
-    apiKey={newKey}
-    onSubmit={(key) => handleCreate(key)}
-    onCancel={() => (newKey = null)}
-  />
-{/if}
-
-{#if secret}
-  <APIKeySecret {secret} onDone={() => (secret = '')} />
-{/if}
-
-{#if editKey}
-  <APIKeyForm
-    title={$t('api_key')}
-    submitText={$t('save')}
-    apiKey={editKey}
-    onSubmit={(key) => handleUpdate(key)}
-    onCancel={() => (editKey = null)}
-  />
-{/if}
-
 <section class="my-4">
   <div class="flex flex-col gap-2" in:fade={{ duration: 500 }}>
     <div class="mb-2 flex justify-end">
-      <Button shape="round" size="small" onclick={() => (newKey = { name: $t('api_key') })}>{$t('new_api_key')}</Button>
+      <Button shape="round" size="small" onclick={() => handleCreate()}>{$t('new_api_key')}</Button>
     </div>
 
     {#if keys.length > 0}
@@ -153,7 +141,7 @@
                   icon={mdiPencilOutline}
                   title={$t('edit_key')}
                   size="16"
-                  onclick={() => (editKey = key)}
+                  onclick={() => handleUpdate(key)}
                 />
                 <CircleIconButton
                   color="primary"
diff --git a/web/src/lib/managers/modal-manager.svelte.ts b/web/src/lib/managers/modal-manager.svelte.ts
index 055df14502..12f9224018 100644
--- a/web/src/lib/managers/modal-manager.svelte.ts
+++ b/web/src/lib/managers/modal-manager.svelte.ts
@@ -1,19 +1,20 @@
 import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
 import { mount, unmount, type Component, type ComponentProps } from 'svelte';
 
-type OnCloseData<T> = T extends { onClose: (data: infer R) => void | Promise<void> } ? R : never;
+type OnCloseData<T> = T extends { onClose: (data?: infer R) => void } ? R : never;
+type ExtendsEmptyObject<T> = keyof T extends never ? Record<string, never> : T;
 
 class ModalManager {
-  open<T = { onClose: (data: unknown) => void }, K = OnCloseData<T>>(
-    Component: Component<{ onClose: T }>,
-    props?: Record<string, never>,
-  ): Promise<K>;
-  open<T extends object, K = OnCloseData<T>>(Component: Component<T>, props: Omit<T, 'onClose'>): Promise<K>;
-  open<T extends object, K = OnCloseData<T>>(Component: Component<T>, props?: Omit<T, 'onClose'>) {
-    return new Promise<K>((resolve) => {
-      let modal: object = {};
+  show<T extends object>(Component: Component<T>, props: ExtendsEmptyObject<Omit<T, 'onClose'>>) {
+    return this.open(Component, props).onClose;
+  }
 
-      const onClose = async (data: K) => {
+  open<T extends object, K = OnCloseData<T>>(Component: Component<T>, props: ExtendsEmptyObject<Omit<T, 'onClose'>>) {
+    let modal: object = {};
+    let onClose: () => Promise<void>;
+
+    const deferred = new Promise<K | undefined>((resolve) => {
+      onClose = async (data?: K) => {
         await unmount(modal);
         resolve(data);
       };
@@ -21,15 +22,20 @@ class ModalManager {
       modal = mount(Component, {
         target: document.body,
         props: {
-          ...((props ?? {}) as T),
+          ...(props as T),
           onClose,
         },
       });
     });
+
+    return {
+      onClose: deferred,
+      close: () => onClose(),
+    };
   }
 
   openDialog(options: Omit<ComponentProps<typeof ConfirmDialog>, 'onClose'>) {
-    return this.open(ConfirmDialog, options);
+    return this.show(ConfirmDialog, options);
   }
 }
 
diff --git a/web/src/lib/modals/ApiKeyModal.svelte b/web/src/lib/modals/ApiKeyModal.svelte
new file mode 100644
index 0000000000..f5e1fb2a7e
--- /dev/null
+++ b/web/src/lib/modals/ApiKeyModal.svelte
@@ -0,0 +1,53 @@
+<script lang="ts">
+  import {
+    notificationController,
+    NotificationType,
+  } from '$lib/components/shared-components/notification/notification';
+  import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
+  import { mdiKeyVariant } from '@mdi/js';
+  import { t } from 'svelte-i18n';
+
+  interface Props {
+    apiKey: { name: string };
+    title: string;
+    cancelText?: string;
+    submitText?: string;
+    onClose: (apiKey?: { name: string }) => void;
+  }
+
+  let { apiKey = $bindable(), title, cancelText = $t('cancel'), submitText = $t('save'), onClose }: Props = $props();
+
+  const handleSubmit = () => {
+    if (apiKey.name) {
+      onClose({ name: apiKey.name });
+    } else {
+      notificationController.show({
+        message: $t('api_key_empty'),
+        type: NotificationType.Warning,
+      });
+    }
+  };
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+    handleSubmit();
+  };
+</script>
+
+<Modal {title} icon={mdiKeyVariant} {onClose} size="small">
+  <ModalBody>
+    <form {onsubmit} autocomplete="off" id="api-key-form">
+      <div class="mb-4 flex flex-col gap-2">
+        <label class="immich-form-label" for="name">{$t('name')}</label>
+        <input class="immich-form-input" id="name" name="name" type="text" bind:value={apiKey.name} />
+      </div>
+    </form>
+  </ModalBody>
+
+  <ModalFooter>
+    <div class="flex gap-3 w-full">
+      <Button shape="round" color="secondary" fullWidth onclick={() => onClose()}>{cancelText}</Button>
+      <Button shape="round" type="submit" fullWidth form="api-key-form">{submitText}</Button>
+    </div>
+  </ModalFooter>
+</Modal>
diff --git a/web/src/lib/modals/ApiKeySecretModal.svelte b/web/src/lib/modals/ApiKeySecretModal.svelte
new file mode 100644
index 0000000000..88d34341d9
--- /dev/null
+++ b/web/src/lib/modals/ApiKeySecretModal.svelte
@@ -0,0 +1,35 @@
+<script lang="ts">
+  import { copyToClipboard } from '$lib/utils';
+  import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
+  import { mdiKeyVariant } from '@mdi/js';
+  import { t } from 'svelte-i18n';
+
+  interface Props {
+    secret?: string;
+    onClose: () => void;
+  }
+
+  let { secret = '', onClose }: Props = $props();
+</script>
+
+<Modal title={$t('api_key')} icon={mdiKeyVariant} {onClose} size="small">
+  <ModalBody>
+    <div class="text-immich-primary dark:text-immich-dark-primary">
+      <p class="text-sm dark:text-immich-dark-fg">
+        {$t('api_key_description')}
+      </p>
+    </div>
+
+    <div class="my-4 flex flex-col gap-2">
+      <!-- <label class="immich-form-label" for="secret">{ $t("api_key") }</label> -->
+      <textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret}></textarea>
+    </div>
+  </ModalBody>
+
+  <ModalFooter>
+    <div class="flex gap-3 w-full">
+      <Button shape="round" onclick={() => copyToClipboard(secret)} fullWidth>{$t('copy_to_clipboard')}</Button>
+      <Button shape="round" onclick={onClose} fullWidth>{$t('done')}</Button>
+    </div>
+  </ModalFooter>
+</Modal>
diff --git a/web/src/lib/components/shared-components/help-and-feedback-modal.svelte b/web/src/lib/modals/HelpAndFeedbackModal.svelte
similarity index 94%
rename from web/src/lib/components/shared-components/help-and-feedback-modal.svelte
rename to web/src/lib/modals/HelpAndFeedbackModal.svelte
index 1dcb021d78..edc78b3bf4 100644
--- a/web/src/lib/components/shared-components/help-and-feedback-modal.svelte
+++ b/web/src/lib/modals/HelpAndFeedbackModal.svelte
@@ -1,11 +1,10 @@
 <script lang="ts">
-  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
-  import Portal from '$lib/components/shared-components/portal/portal.svelte';
-  import { type ServerAboutResponseDto } from '@immich/sdk';
-  import { t } from 'svelte-i18n';
-  import Icon from '$lib/components/elements/icon.svelte';
-  import { mdiBugOutline, mdiFaceAgent, mdiGit, mdiGithub, mdiInformationOutline } from '@mdi/js';
   import { discordPath, discordViewBox } from '$lib/assets/svg-paths';
+  import Icon from '$lib/components/elements/icon.svelte';
+  import { type ServerAboutResponseDto } from '@immich/sdk';
+  import { Modal, ModalBody } from '@immich/ui';
+  import { mdiBugOutline, mdiFaceAgent, mdiGit, mdiGithub, mdiInformationOutline } from '@mdi/js';
+  import { t } from 'svelte-i18n';
 
   interface Props {
     onClose: () => void;
@@ -15,8 +14,8 @@
   let { onClose, info }: Props = $props();
 </script>
 
-<Portal>
-  <FullScreenModal title={$t('support_and_feedback')} {onClose}>
+<Modal title={$t('support_and_feedback')} {onClose} size="small">
+  <ModalBody>
     <p>{$t('official_immich_resources')}</p>
     <div class="flex flex-col sm:grid sm:grid-cols-2 gap-2 mt-5">
       <div>
@@ -130,5 +129,5 @@
         {/if}
       </div>
     {/if}
-  </FullScreenModal>
-</Portal>
+  </ModalBody>
+</Modal>
diff --git a/web/src/lib/modals/MapSettingsModal.svelte b/web/src/lib/modals/MapSettingsModal.svelte
new file mode 100644
index 0000000000..e7bef2ecaf
--- /dev/null
+++ b/web/src/lib/modals/MapSettingsModal.svelte
@@ -0,0 +1,135 @@
+<script lang="ts">
+  import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
+  import type { MapSettings } from '$lib/stores/preferences.store';
+  import { Button, Field, Modal, ModalBody, ModalFooter, Stack, Switch } from '@immich/ui';
+  import { Duration } from 'luxon';
+  import { t } from 'svelte-i18n';
+  import { fly } from 'svelte/transition';
+  import DateInput from '../components/elements/date-input.svelte';
+
+  interface Props {
+    settings: MapSettings;
+    onClose: (settings?: MapSettings) => void;
+  }
+
+  let { settings: initialValues, onClose }: Props = $props();
+  let settings = $state(initialValues);
+
+  let customDateRange = $state(!!settings.dateAfter || !!settings.dateBefore);
+
+  const onsubmit = (event: Event) => {
+    event.preventDefault();
+    onClose(settings);
+  };
+</script>
+
+<Modal title={$t('map_settings')} {onClose} size="small">
+  <ModalBody>
+    <form {onsubmit} id="map-settings-form">
+      <Stack gap={4}>
+        <Field label={$t('allow_dark_mode')}>
+          <Switch bind:checked={settings.allowDarkMode} />
+        </Field>
+        <Field label={$t('only_favorites')}>
+          <Switch bind:checked={settings.onlyFavorites} />
+        </Field>
+        <Field label={$t('include_archived')}>
+          <Switch bind:checked={settings.includeArchived} />
+        </Field>
+        <Field label={$t('include_shared_partner_assets')}>
+          <Switch bind:checked={settings.withPartners} />
+        </Field>
+        <Field label={$t('include_shared_albums')}>
+          <Switch bind:checked={settings.withSharedAlbums} />
+        </Field>
+
+        {#if customDateRange}
+          <div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
+            <div class="flex items-center justify-between gap-8">
+              <label class="immich-form-label shrink-0 text-sm" for="date-after">{$t('date_after')}</label>
+              <DateInput
+                class="immich-form-input w-40"
+                type="date"
+                id="date-after"
+                max={settings.dateBefore}
+                bind:value={settings.dateAfter}
+              />
+            </div>
+            <div class="flex items-center justify-between gap-8">
+              <label class="immich-form-label shrink-0 text-sm" for="date-before">{$t('date_before')}</label>
+              <DateInput class="immich-form-input w-40" type="date" id="date-before" bind:value={settings.dateBefore} />
+            </div>
+            <div class="flex justify-center text-xs">
+              <Button
+                color="primary"
+                size="small"
+                variant="ghost"
+                onclick={() => {
+                  customDateRange = false;
+                  settings.dateAfter = '';
+                  settings.dateBefore = '';
+                }}
+              >
+                {$t('remove_custom_date_range')}
+              </Button>
+            </div>
+          </div>
+        {:else}
+          <div in:fly={{ y: -10, duration: 200 }} class="flex flex-col gap-1">
+            <SettingSelect
+              label={$t('date_range')}
+              name="date-range"
+              bind:value={settings.relativeDate}
+              options={[
+                {
+                  value: '',
+                  text: $t('all'),
+                },
+                {
+                  value: Duration.fromObject({ hours: 24 }).toISO() || '',
+                  text: $t('past_durations.hours', { values: { hours: 24 } }),
+                },
+                {
+                  value: Duration.fromObject({ days: 7 }).toISO() || '',
+                  text: $t('past_durations.days', { values: { days: 7 } }),
+                },
+                {
+                  value: Duration.fromObject({ days: 30 }).toISO() || '',
+                  text: $t('past_durations.days', { values: { days: 30 } }),
+                },
+                {
+                  value: Duration.fromObject({ years: 1 }).toISO() || '',
+                  text: $t('past_durations.years', { values: { years: 1 } }),
+                },
+                {
+                  value: Duration.fromObject({ years: 3 }).toISO() || '',
+                  text: $t('past_durations.years', { values: { years: 3 } }),
+                },
+              ]}
+            />
+            <div class="text-xs">
+              <Button
+                color="primary"
+                size="small"
+                variant="ghost"
+                onclick={() => {
+                  customDateRange = true;
+                  settings.relativeDate = '';
+                }}
+              >
+                {$t('use_custom_date_range')}
+              </Button>
+            </div>
+          </div>
+        {/if}
+      </Stack>
+    </form>
+  </ModalBody>
+
+  <ModalFooter>
+    <div class="flex gap-3 w-full">
+      <Button color="secondary" shape="round" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
+      <Button type="submit" shape="round" fullWidth form="map-settings-form">{$t('save')}</Button>
+    </div>
+  </ModalFooter>
+</Modal>
diff --git a/web/src/lib/modals/PartnerSelectionModal.svelte b/web/src/lib/modals/PartnerSelectionModal.svelte
new file mode 100644
index 0000000000..729a035ef1
--- /dev/null
+++ b/web/src/lib/modals/PartnerSelectionModal.svelte
@@ -0,0 +1,79 @@
+<script lang="ts">
+  import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
+  import { getPartners, PartnerDirection, searchUsers, type UserResponseDto } from '@immich/sdk';
+  import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
+  import { onMount } from 'svelte';
+  import { t } from 'svelte-i18n';
+
+  interface Props {
+    user: UserResponseDto;
+    onClose: (users?: UserResponseDto[]) => void;
+  }
+
+  let { user, onClose }: Props = $props();
+
+  let availableUsers: UserResponseDto[] = $state([]);
+  let selectedUsers: UserResponseDto[] = $state([]);
+
+  onMount(async () => {
+    let users = await searchUsers();
+
+    // remove current user
+    users = users.filter((_user) => _user.id !== user.id);
+
+    // exclude partners from the list of users available for selection
+    const partners = await getPartners({ direction: PartnerDirection.SharedBy });
+    const partnerIds = new Set(partners.map((partner) => partner.id));
+    availableUsers = users.filter((user) => !partnerIds.has(user.id));
+  });
+
+  const selectUser = (user: UserResponseDto) => {
+    selectedUsers = selectedUsers.includes(user)
+      ? selectedUsers.filter((selectedUser) => selectedUser.id !== user.id)
+      : [...selectedUsers, user];
+  };
+</script>
+
+<Modal title={$t('add_partner')} {onClose} size="small">
+  <ModalBody>
+    <div class="immich-scrollbar max-h-[300px] overflow-y-auto">
+      {#if availableUsers.length > 0}
+        {#each availableUsers as user (user.id)}
+          <button
+            type="button"
+            onclick={() => selectUser(user)}
+            class="flex w-full place-items-center gap-4 px-5 py-4 transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
+          >
+            {#if selectedUsers.includes(user)}
+              <span
+                class="flex h-12 w-12 place-content-center place-items-center rounded-full border bg-immich-primary text-3xl text-white dark:border-immich-dark-gray dark:bg-immich-dark-primary dark:text-immich-dark-bg"
+                >✓</span
+              >
+            {:else}
+              <UserAvatar {user} size="lg" />
+            {/if}
+
+            <div class="text-start">
+              <p class="text-immich-fg dark:text-immich-dark-fg">
+                {user.name}
+              </p>
+              <p class="text-xs">
+                {user.email}
+              </p>
+            </div>
+          </button>
+        {/each}
+      {:else}
+        <p class="py-5 text-sm">
+          {$t('photo_shared_all_users')}
+        </p>
+      {/if}
+
+      <ModalFooter>
+        {#if selectedUsers.length > 0}
+          <Button shape="round" fullWidth onclick={() => onClose(selectedUsers)}>{$t('add')}</Button>
+        {/if}
+      </ModalFooter>
+    </div>
+  </ModalBody>
+</Modal>
diff --git a/web/src/lib/modals/UserEditModal.svelte b/web/src/lib/modals/UserEditModal.svelte
index 9981aed3ad..7f5ac89681 100644
--- a/web/src/lib/modals/UserEditModal.svelte
+++ b/web/src/lib/modals/UserEditModal.svelte
@@ -4,16 +4,19 @@
   import { userInteraction } from '$lib/stores/user.svelte';
   import { ByteUnit, convertFromBytes, convertToBytes } from '$lib/utils/byte-units';
   import { handleError } from '$lib/utils/handle-error';
-  import { updateUserAdmin, type UserAdminResponseDto } from '@immich/sdk';
+  import { resetPincode, updateUserAdmin, type UserAdminResponseDto } from '@immich/sdk';
   import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
-  import { mdiAccountEditOutline } from '@mdi/js';
+  import { mdiAccountEditOutline, mdiLockSmart, mdiOnepassword } from '@mdi/js';
   import { t } from 'svelte-i18n';
 
   interface Props {
     user: UserAdminResponseDto;
     canResetPassword?: boolean;
     onClose: (
-      data?: { action: 'update'; data: UserAdminResponseDto } | { action: 'resetPassword'; data: string },
+      data?:
+        | { action: 'update'; data: UserAdminResponseDto }
+        | { action: 'resetPassword'; data: string }
+        | { action: 'resetPincode' },
     ) => void;
   }
 
@@ -76,6 +79,28 @@
     }
   };
 
+  const resetUserPincode = async () => {
+    const isConfirmed = await modalManager.openDialog({
+      prompt: $t('admin.confirm_user_pincode_reset', { values: { user: user.name } }),
+    });
+
+    if (!isConfirmed) {
+      return;
+    }
+
+    try {
+      await resetPincode({
+        resetPincodeDto: {
+          userId: user.id,
+        },
+      });
+
+      onClose({ action: 'resetPincode' });
+    } catch (error) {
+      handleError(error, $t('errors.unable_to_reset_pincode'));
+    }
+  };
+
   // TODO move password reset server-side
   function generatePassword(length: number = 16) {
     let generatedPassword = '';
@@ -151,13 +176,34 @@
   </ModalBody>
 
   <ModalFooter>
-    <div class="flex gap-3 w-full">
-      {#if canResetPassword}
-        <Button shape="round" color="warning" variant="filled" fullWidth onclick={resetPassword}
-          >{$t('reset_password')}</Button
+    <div class="w-full">
+      <div class="flex gap-3 w-full">
+        {#if canResetPassword}
+          <Button
+            shape="round"
+            color="warning"
+            variant="filled"
+            fullWidth
+            onclick={resetPassword}
+            leadingIcon={mdiOnepassword}
+          >
+            {$t('reset_password')}</Button
+          >
+        {/if}
+
+        <Button
+          shape="round"
+          color="warning"
+          variant="filled"
+          fullWidth
+          onclick={resetUserPincode}
+          leadingIcon={mdiLockSmart}>{$t('reset_pincode')}</Button
         >
-      {/if}
-      <Button type="submit" shape="round" fullWidth form="edit-user-form">{$t('confirm')}</Button>
+      </div>
+
+      <div class="w-full mt-4">
+        <Button type="submit" shape="round" fullWidth form="edit-user-form">{$t('confirm')}</Button>
+      </div>
     </div>
   </ModalFooter>
 </Modal>
diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 0a71f35ff2..b1fff3a0cd 100644
--- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -3,21 +3,15 @@
 
   import { goto } from '$app/navigation';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
-  import MapSettingsModal from '$lib/components/map-page/map-settings-modal.svelte';
   import Map from '$lib/components/shared-components/map/map.svelte';
   import Portal from '$lib/components/shared-components/portal/portal.svelte';
   import { AppRoute } from '$lib/constants';
   import { assetViewingStore } from '$lib/stores/asset-viewing.store';
-  import type { MapSettings } from '$lib/stores/preferences.store';
-  import { mapSettings } from '$lib/stores/preferences.store';
   import { featureFlags } from '$lib/stores/server-config.store';
-  import { getMapMarkers, type MapMarkerResponseDto } from '@immich/sdk';
-  import { isEqual } from 'lodash-es';
-  import { DateTime, Duration } from 'luxon';
-  import { onDestroy, onMount } from 'svelte';
-  import type { PageData } from './$types';
   import { handlePromiseError } from '$lib/utils';
   import { navigate } from '$lib/utils/navigation';
+  import { onDestroy } from 'svelte';
+  import type { PageData } from './$types';
 
   interface Props {
     data: PageData;
@@ -27,18 +21,10 @@
 
   let { isViewing: showAssetViewer, asset: viewingAsset, setAssetId } = assetViewingStore;
 
-  let abortController: AbortController;
-  let mapMarkers: MapMarkerResponseDto[] = $state([]);
   let viewingAssets: string[] = $state([]);
   let viewingAssetCursor = 0;
-  let showSettingsModal = $state(false);
-
-  onMount(async () => {
-    mapMarkers = await loadMapMarkers();
-  });
 
   onDestroy(() => {
-    abortController?.abort();
     assetViewingStore.showAssetViewer(false);
   });
 
@@ -47,55 +33,6 @@
       handlePromiseError(goto(AppRoute.PHOTOS));
     }
   });
-  const omit = (obj: MapSettings, key: string) => {
-    return Object.fromEntries(Object.entries(obj).filter(([k]) => k !== key));
-  };
-
-  async function loadMapMarkers() {
-    if (abortController) {
-      abortController.abort();
-    }
-    abortController = new AbortController();
-
-    const { includeArchived, onlyFavorites, withPartners, withSharedAlbums } = $mapSettings;
-    const { fileCreatedAfter, fileCreatedBefore } = getFileCreatedDates();
-
-    return await getMapMarkers(
-      {
-        isArchived: includeArchived && undefined,
-        isFavorite: onlyFavorites || undefined,
-        fileCreatedAfter: fileCreatedAfter || undefined,
-        fileCreatedBefore,
-        withPartners: withPartners || undefined,
-        withSharedAlbums: withSharedAlbums || undefined,
-      },
-      {
-        signal: abortController.signal,
-      },
-    );
-  }
-
-  function getFileCreatedDates() {
-    const { relativeDate, dateAfter, dateBefore } = $mapSettings;
-
-    if (relativeDate) {
-      const duration = Duration.fromISO(relativeDate);
-      return {
-        fileCreatedAfter: duration.isValid ? DateTime.now().minus(duration).toISO() : undefined,
-      };
-    }
-
-    try {
-      return {
-        fileCreatedAfter: dateAfter ? new Date(dateAfter).toISOString() : undefined,
-        fileCreatedBefore: dateBefore ? new Date(dateBefore).toISOString() : undefined,
-      };
-    } catch {
-      $mapSettings.dateAfter = '';
-      $mapSettings.dateBefore = '';
-      return {};
-    }
-  }
 
   async function onViewAssets(assetIds: string[]) {
     viewingAssets = assetIds;
@@ -135,7 +72,7 @@
 {#if $featureFlags.loaded && $featureFlags.map}
   <UserPageLayout title={data.meta.title}>
     <div class="isolate h-full w-full">
-      <Map hash bind:mapMarkers bind:showSettingsModal onSelect={onViewAssets} />
+      <Map hash onSelect={onViewAssets} />
     </div>
   </UserPageLayout>
   <Portal target="body">
@@ -156,20 +93,4 @@
       {/await}
     {/if}
   </Portal>
-
-  {#if showSettingsModal}
-    <MapSettingsModal
-      settings={{ ...$mapSettings }}
-      onClose={() => (showSettingsModal = false)}
-      onSave={async (settings) => {
-        const shouldUpdate = !isEqual(omit(settings, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode'));
-        showSettingsModal = false;
-        $mapSettings = settings;
-
-        if (shouldUpdate) {
-          mapMarkers = await loadMapMarkers();
-        }
-      }}
-    />
-  {/if}
 {/if}
diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte
index c5327a4271..6636d748cf 100644
--- a/web/src/routes/admin/jobs-status/+page.svelte
+++ b/web/src/routes/admin/jobs-status/+page.svelte
@@ -39,7 +39,7 @@
     <HStack gap={0}>
       <Button
         leadingIcon={mdiPlus}
-        onclick={() => modalManager.open(JobCreateModal)}
+        onclick={() => modalManager.show(JobCreateModal, {})}
         size="small"
         variant="ghost"
         color="secondary"
diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte
index 8ac3793046..bfe12f0a48 100644
--- a/web/src/routes/admin/user-management/+page.svelte
+++ b/web/src/routes/admin/user-management/+page.svelte
@@ -59,33 +59,40 @@
   };
 
   const handleCreate = async () => {
-    await modalManager.open(UserCreateModal);
+    await modalManager.show(UserCreateModal, {});
     await refresh();
   };
 
   const handleEdit = async (dto: UserAdminResponseDto) => {
-    const result = await modalManager.open(UserEditModal, { user: dto, canResetPassword: dto.id !== $user.id });
+    const result = await modalManager.show(UserEditModal, { user: dto, canResetPassword: dto.id !== $user.id });
     switch (result?.action) {
       case 'resetPassword': {
-        await modalManager.open(PasswordResetSuccess, { newPassword: result.data });
+        await modalManager.show(PasswordResetSuccess, { newPassword: result.data });
         break;
       }
       case 'update': {
         await refresh();
         break;
       }
+      case 'resetPincode': {
+        notificationController.show({
+          type: NotificationType.Info,
+          message: $t('pincode_reset_successfully'),
+        });
+        break;
+      }
     }
   };
 
   const handleDelete = async (user: UserAdminResponseDto) => {
-    const result = await modalManager.open(UserDeleteConfirmModal, { user });
+    const result = await modalManager.show(UserDeleteConfirmModal, { user });
     if (result) {
       await refresh();
     }
   };
 
   const handleRestore = async (user: UserAdminResponseDto) => {
-    const result = await modalManager.open(UserRestoreConfirmModal, { user });
+    const result = await modalManager.show(UserRestoreConfirmModal, { user });
     if (result) {
       await refresh();
     }
@@ -137,7 +144,7 @@
                   {#if !immichUser.deletedAt}
                     <IconButton
                       shape="round"
-                      size="small"
+                      size="medium"
                       icon={mdiPencilOutline}
                       title={$t('edit_user')}
                       onclick={() => handleEdit(immichUser)}
@@ -146,7 +153,7 @@
                     {#if immichUser.id !== $user.id}
                       <IconButton
                         shape="round"
-                        size="small"
+                        size="medium"
                         icon={mdiTrashCanOutline}
                         title={$t('delete_user')}
                         onclick={() => handleDelete(immichUser)}