mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 13:57:39 +09:00 
			
		
		
		
	feat: track upgrade history (#13097)
This commit is contained in:
		
							
								
								
									
										2
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @@ -183,6 +183,7 @@ Class | Method | HTTP request | Description | |||||||
| *ServerApi* | [**getStorage**](doc//ServerApi.md#getstorage) | **GET** /server/storage |  | *ServerApi* | [**getStorage**](doc//ServerApi.md#getstorage) | **GET** /server/storage |  | ||||||
| *ServerApi* | [**getSupportedMediaTypes**](doc//ServerApi.md#getsupportedmediatypes) | **GET** /server/media-types |  | *ServerApi* | [**getSupportedMediaTypes**](doc//ServerApi.md#getsupportedmediatypes) | **GET** /server/media-types |  | ||||||
| *ServerApi* | [**getTheme**](doc//ServerApi.md#gettheme) | **GET** /server/theme |  | *ServerApi* | [**getTheme**](doc//ServerApi.md#gettheme) | **GET** /server/theme |  | ||||||
|  | *ServerApi* | [**getVersionHistory**](doc//ServerApi.md#getversionhistory) | **GET** /server/version-history |  | ||||||
| *ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping |  | *ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping |  | ||||||
| *ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license |  | *ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license |  | ||||||
| *SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |  | *SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |  | ||||||
| @@ -400,6 +401,7 @@ Class | Method | HTTP request | Description | |||||||
|  - [ServerStatsResponseDto](doc//ServerStatsResponseDto.md) |  - [ServerStatsResponseDto](doc//ServerStatsResponseDto.md) | ||||||
|  - [ServerStorageResponseDto](doc//ServerStorageResponseDto.md) |  - [ServerStorageResponseDto](doc//ServerStorageResponseDto.md) | ||||||
|  - [ServerThemeDto](doc//ServerThemeDto.md) |  - [ServerThemeDto](doc//ServerThemeDto.md) | ||||||
|  |  - [ServerVersionHistoryResponseDto](doc//ServerVersionHistoryResponseDto.md) | ||||||
|  - [ServerVersionResponseDto](doc//ServerVersionResponseDto.md) |  - [ServerVersionResponseDto](doc//ServerVersionResponseDto.md) | ||||||
|  - [SessionResponseDto](doc//SessionResponseDto.md) |  - [SessionResponseDto](doc//SessionResponseDto.md) | ||||||
|  - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) |  - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @@ -213,6 +213,7 @@ part 'model/server_ping_response.dart'; | |||||||
| part 'model/server_stats_response_dto.dart'; | part 'model/server_stats_response_dto.dart'; | ||||||
| part 'model/server_storage_response_dto.dart'; | part 'model/server_storage_response_dto.dart'; | ||||||
| part 'model/server_theme_dto.dart'; | part 'model/server_theme_dto.dart'; | ||||||
|  | part 'model/server_version_history_response_dto.dart'; | ||||||
| part 'model/server_version_response_dto.dart'; | part 'model/server_version_response_dto.dart'; | ||||||
| part 'model/session_response_dto.dart'; | part 'model/session_response_dto.dart'; | ||||||
| part 'model/shared_link_create_dto.dart'; | part 'model/shared_link_create_dto.dart'; | ||||||
|   | |||||||
							
								
								
									
										44
									
								
								mobile/openapi/lib/api/server_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										44
									
								
								mobile/openapi/lib/api/server_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -418,6 +418,50 @@ class ServerApi { | |||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /// Performs an HTTP 'GET /server/version-history' operation and returns the [Response]. | ||||||
|  |   Future<Response> getVersionHistoryWithHttpInfo() async { | ||||||
|  |     // ignore: prefer_const_declarations | ||||||
|  |     final path = r'/server/version-history'; | ||||||
|  | 
 | ||||||
|  |     // ignore: prefer_final_locals | ||||||
|  |     Object? postBody; | ||||||
|  | 
 | ||||||
|  |     final queryParams = <QueryParam>[]; | ||||||
|  |     final headerParams = <String, String>{}; | ||||||
|  |     final formParams = <String, String>{}; | ||||||
|  | 
 | ||||||
|  |     const contentTypes = <String>[]; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     return apiClient.invokeAPI( | ||||||
|  |       path, | ||||||
|  |       'GET', | ||||||
|  |       queryParams, | ||||||
|  |       postBody, | ||||||
|  |       headerParams, | ||||||
|  |       formParams, | ||||||
|  |       contentTypes.isEmpty ? null : contentTypes.first, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<List<ServerVersionHistoryResponseDto>?> getVersionHistory() async { | ||||||
|  |     final response = await getVersionHistoryWithHttpInfo(); | ||||||
|  |     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) { | ||||||
|  |       final responseBody = await _decodeBodyBytes(response); | ||||||
|  |       return (await apiClient.deserializeAsync(responseBody, 'List<ServerVersionHistoryResponseDto>') as List) | ||||||
|  |         .cast<ServerVersionHistoryResponseDto>() | ||||||
|  |         .toList(growable: false); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   /// Performs an HTTP 'GET /server/ping' operation and returns the [Response]. |   /// Performs an HTTP 'GET /server/ping' operation and returns the [Response]. | ||||||
|   Future<Response> pingServerWithHttpInfo() async { |   Future<Response> pingServerWithHttpInfo() async { | ||||||
|     // ignore: prefer_const_declarations |     // ignore: prefer_const_declarations | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @@ -480,6 +480,8 @@ class ApiClient { | |||||||
|           return ServerStorageResponseDto.fromJson(value); |           return ServerStorageResponseDto.fromJson(value); | ||||||
|         case 'ServerThemeDto': |         case 'ServerThemeDto': | ||||||
|           return ServerThemeDto.fromJson(value); |           return ServerThemeDto.fromJson(value); | ||||||
|  |         case 'ServerVersionHistoryResponseDto': | ||||||
|  |           return ServerVersionHistoryResponseDto.fromJson(value); | ||||||
|         case 'ServerVersionResponseDto': |         case 'ServerVersionResponseDto': | ||||||
|           return ServerVersionResponseDto.fromJson(value); |           return ServerVersionResponseDto.fromJson(value); | ||||||
|         case 'SessionResponseDto': |         case 'SessionResponseDto': | ||||||
|   | |||||||
							
								
								
									
										115
									
								
								mobile/openapi/lib/model/server_version_history_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								mobile/openapi/lib/model/server_version_history_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | |||||||
|  | // | ||||||
|  | // 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 ServerVersionHistoryResponseDto { | ||||||
|  |   /// Returns a new [ServerVersionHistoryResponseDto] instance. | ||||||
|  |   ServerVersionHistoryResponseDto({ | ||||||
|  |     required this.createdAt, | ||||||
|  |     required this.id, | ||||||
|  |     required this.version, | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   DateTime createdAt; | ||||||
|  | 
 | ||||||
|  |   String id; | ||||||
|  | 
 | ||||||
|  |   String version; | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   bool operator ==(Object other) => identical(this, other) || other is ServerVersionHistoryResponseDto && | ||||||
|  |     other.createdAt == createdAt && | ||||||
|  |     other.id == id && | ||||||
|  |     other.version == version; | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   int get hashCode => | ||||||
|  |     // ignore: unnecessary_parenthesis | ||||||
|  |     (createdAt.hashCode) + | ||||||
|  |     (id.hashCode) + | ||||||
|  |     (version.hashCode); | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   String toString() => 'ServerVersionHistoryResponseDto[createdAt=$createdAt, id=$id, version=$version]'; | ||||||
|  | 
 | ||||||
|  |   Map<String, dynamic> toJson() { | ||||||
|  |     final json = <String, dynamic>{}; | ||||||
|  |       json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); | ||||||
|  |       json[r'id'] = this.id; | ||||||
|  |       json[r'version'] = this.version; | ||||||
|  |     return json; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// Returns a new [ServerVersionHistoryResponseDto] instance and imports its values from | ||||||
|  |   /// [value] if it's a [Map], null otherwise. | ||||||
|  |   // ignore: prefer_constructors_over_static_methods | ||||||
|  |   static ServerVersionHistoryResponseDto? fromJson(dynamic value) { | ||||||
|  |     upgradeDto(value, "ServerVersionHistoryResponseDto"); | ||||||
|  |     if (value is Map) { | ||||||
|  |       final json = value.cast<String, dynamic>(); | ||||||
|  | 
 | ||||||
|  |       return ServerVersionHistoryResponseDto( | ||||||
|  |         createdAt: mapDateTime(json, r'createdAt', r'')!, | ||||||
|  |         id: mapValueOfType<String>(json, r'id')!, | ||||||
|  |         version: mapValueOfType<String>(json, r'version')!, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static List<ServerVersionHistoryResponseDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||||
|  |     final result = <ServerVersionHistoryResponseDto>[]; | ||||||
|  |     if (json is List && json.isNotEmpty) { | ||||||
|  |       for (final row in json) { | ||||||
|  |         final value = ServerVersionHistoryResponseDto.fromJson(row); | ||||||
|  |         if (value != null) { | ||||||
|  |           result.add(value); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return result.toList(growable: growable); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   static Map<String, ServerVersionHistoryResponseDto> mapFromJson(dynamic json) { | ||||||
|  |     final map = <String, ServerVersionHistoryResponseDto>{}; | ||||||
|  |     if (json is Map && json.isNotEmpty) { | ||||||
|  |       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||||
|  |       for (final entry in json.entries) { | ||||||
|  |         final value = ServerVersionHistoryResponseDto.fromJson(entry.value); | ||||||
|  |         if (value != null) { | ||||||
|  |           map[entry.key] = value; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return map; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // maps a json object with a list of ServerVersionHistoryResponseDto-objects as value to a dart map | ||||||
|  |   static Map<String, List<ServerVersionHistoryResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||||
|  |     final map = <String, List<ServerVersionHistoryResponseDto>>{}; | ||||||
|  |     if (json is Map && json.isNotEmpty) { | ||||||
|  |       // ignore: parameter_assignments | ||||||
|  |       json = json.cast<String, dynamic>(); | ||||||
|  |       for (final entry in json.entries) { | ||||||
|  |         map[entry.key] = ServerVersionHistoryResponseDto.listFromJson(entry.value, growable: growable,); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return map; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /// The list of required keys that must be present in a JSON. | ||||||
|  |   static const requiredKeys = <String>{ | ||||||
|  |     'createdAt', | ||||||
|  |     'id', | ||||||
|  |     'version', | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @@ -5088,6 +5088,30 @@ | |||||||
|         ] |         ] | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "/server/version-history": { | ||||||
|  |       "get": { | ||||||
|  |         "operationId": "getVersionHistory", | ||||||
|  |         "parameters": [], | ||||||
|  |         "responses": { | ||||||
|  |           "200": { | ||||||
|  |             "content": { | ||||||
|  |               "application/json": { | ||||||
|  |                 "schema": { | ||||||
|  |                   "items": { | ||||||
|  |                     "$ref": "#/components/schemas/ServerVersionHistoryResponseDto" | ||||||
|  |                   }, | ||||||
|  |                   "type": "array" | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|  |             "description": "" | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         "tags": [ | ||||||
|  |           "Server" | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "/sessions": { |     "/sessions": { | ||||||
|       "delete": { |       "delete": { | ||||||
|         "operationId": "deleteAllSessions", |         "operationId": "deleteAllSessions", | ||||||
| @@ -11042,6 +11066,26 @@ | |||||||
|         ], |         ], | ||||||
|         "type": "object" |         "type": "object" | ||||||
|       }, |       }, | ||||||
|  |       "ServerVersionHistoryResponseDto": { | ||||||
|  |         "properties": { | ||||||
|  |           "createdAt": { | ||||||
|  |             "format": "date-time", | ||||||
|  |             "type": "string" | ||||||
|  |           }, | ||||||
|  |           "id": { | ||||||
|  |             "type": "string" | ||||||
|  |           }, | ||||||
|  |           "version": { | ||||||
|  |             "type": "string" | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         "required": [ | ||||||
|  |           "createdAt", | ||||||
|  |           "id", | ||||||
|  |           "version" | ||||||
|  |         ], | ||||||
|  |         "type": "object" | ||||||
|  |       }, | ||||||
|       "ServerVersionResponseDto": { |       "ServerVersionResponseDto": { | ||||||
|         "properties": { |         "properties": { | ||||||
|           "major": { |           "major": { | ||||||
|   | |||||||
| @@ -1000,6 +1000,11 @@ export type ServerVersionResponseDto = { | |||||||
|     minor: number; |     minor: number; | ||||||
|     patch: number; |     patch: number; | ||||||
| }; | }; | ||||||
|  | export type ServerVersionHistoryResponseDto = { | ||||||
|  |     createdAt: string; | ||||||
|  |     id: string; | ||||||
|  |     version: string; | ||||||
|  | }; | ||||||
| export type SessionResponseDto = { | export type SessionResponseDto = { | ||||||
|     createdAt: string; |     createdAt: string; | ||||||
|     current: boolean; |     current: boolean; | ||||||
| @@ -2667,6 +2672,14 @@ export function getServerVersion(opts?: Oazapfts.RequestOpts) { | |||||||
|         ...opts |         ...opts | ||||||
|     })); |     })); | ||||||
| } | } | ||||||
|  | export function getVersionHistory(opts?: Oazapfts.RequestOpts) { | ||||||
|  |     return oazapfts.ok(oazapfts.fetchJson<{ | ||||||
|  |         status: 200; | ||||||
|  |         data: ServerVersionHistoryResponseDto[]; | ||||||
|  |     }>("/server/version-history", { | ||||||
|  |         ...opts | ||||||
|  |     })); | ||||||
|  | } | ||||||
| export function deleteAllSessions(opts?: Oazapfts.RequestOpts) { | export function deleteAllSessions(opts?: Oazapfts.RequestOpts) { | ||||||
|     return oazapfts.ok(oazapfts.fetchText("/sessions", { |     return oazapfts.ok(oazapfts.fetchText("/sessions", { | ||||||
|         ...opts, |         ...opts, | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import { | |||||||
|   ServerStatsResponseDto, |   ServerStatsResponseDto, | ||||||
|   ServerStorageResponseDto, |   ServerStorageResponseDto, | ||||||
|   ServerThemeDto, |   ServerThemeDto, | ||||||
|  |   ServerVersionHistoryResponseDto, | ||||||
|   ServerVersionResponseDto, |   ServerVersionResponseDto, | ||||||
| } from 'src/dtos/server.dto'; | } from 'src/dtos/server.dto'; | ||||||
| import { Authenticated } from 'src/middleware/auth.guard'; | import { Authenticated } from 'src/middleware/auth.guard'; | ||||||
| @@ -46,6 +47,11 @@ export class ServerController { | |||||||
|     return this.versionService.getVersion(); |     return this.versionService.getVersion(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   @Get('version-history') | ||||||
|  |   getVersionHistory(): Promise<ServerVersionHistoryResponseDto[]> { | ||||||
|  |     return this.versionService.getVersionHistory(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @Get('features') |   @Get('features') | ||||||
|   getServerFeatures(): Promise<ServerFeaturesDto> { |   getServerFeatures(): Promise<ServerFeaturesDto> { | ||||||
|     return this.service.getFeatures(); |     return this.service.getFeatures(); | ||||||
|   | |||||||
| @@ -68,6 +68,12 @@ export class ServerVersionResponseDto { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export class ServerVersionHistoryResponseDto { | ||||||
|  |   id!: string; | ||||||
|  |   createdAt!: Date; | ||||||
|  |   version!: string; | ||||||
|  | } | ||||||
|  |  | ||||||
| export class UsageByUserDto { | export class UsageByUserDto { | ||||||
|   @ApiProperty({ type: 'string' }) |   @ApiProperty({ type: 'string' }) | ||||||
|   userId!: string; |   userId!: string; | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ import { SystemMetadataEntity } from 'src/entities/system-metadata.entity'; | |||||||
| import { TagEntity } from 'src/entities/tag.entity'; | import { TagEntity } from 'src/entities/tag.entity'; | ||||||
| import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; | import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; | ||||||
| import { UserEntity } from 'src/entities/user.entity'; | import { UserEntity } from 'src/entities/user.entity'; | ||||||
|  | import { VersionHistoryEntity } from 'src/entities/version-history.entity'; | ||||||
|  |  | ||||||
| export const entities = [ | export const entities = [ | ||||||
|   ActivityEntity, |   ActivityEntity, | ||||||
| @@ -54,4 +55,5 @@ export const entities = [ | |||||||
|   UserMetadataEntity, |   UserMetadataEntity, | ||||||
|   SessionEntity, |   SessionEntity, | ||||||
|   LibraryEntity, |   LibraryEntity, | ||||||
|  |   VersionHistoryEntity, | ||||||
| ]; | ]; | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								server/src/entities/version-history.entity.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								server/src/entities/version-history.entity.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm'; | ||||||
|  |  | ||||||
|  | @Entity('version_history') | ||||||
|  | export class VersionHistoryEntity { | ||||||
|  |   @PrimaryGeneratedColumn('uuid') | ||||||
|  |   id!: string; | ||||||
|  |  | ||||||
|  |   @CreateDateColumn({ type: 'timestamptz' }) | ||||||
|  |   createdAt!: Date; | ||||||
|  |  | ||||||
|  |   @Column() | ||||||
|  |   version!: string; | ||||||
|  | } | ||||||
| @@ -17,6 +17,7 @@ export enum DatabaseLock { | |||||||
|   Migrations = 200, |   Migrations = 200, | ||||||
|   SystemFileMounts = 300, |   SystemFileMounts = 300, | ||||||
|   StorageTemplateMigration = 420, |   StorageTemplateMigration = 420, | ||||||
|  |   VersionHistory = 500, | ||||||
|   CLIPDimSize = 512, |   CLIPDimSize = 512, | ||||||
|   LibraryWatch = 1337, |   LibraryWatch = 1337, | ||||||
|   GetSystemConfig = 69, |   GetSystemConfig = 69, | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								server/src/interfaces/version-history.interface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								server/src/interfaces/version-history.interface.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | import { VersionHistoryEntity } from 'src/entities/version-history.entity'; | ||||||
|  |  | ||||||
|  | export const IVersionHistoryRepository = 'IVersionHistoryRepository'; | ||||||
|  |  | ||||||
|  | export interface IVersionHistoryRepository { | ||||||
|  |   create(version: Omit<VersionHistoryEntity, 'id' | 'createdAt'>): Promise<VersionHistoryEntity>; | ||||||
|  |   getAll(): Promise<VersionHistoryEntity[]>; | ||||||
|  |   getLatest(): Promise<VersionHistoryEntity | null>; | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								server/src/migrations/1727797340951-AddVersionHistory.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								server/src/migrations/1727797340951-AddVersionHistory.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | import { MigrationInterface, QueryRunner } from "typeorm"; | ||||||
|  |  | ||||||
|  | export class AddVersionHistory1727797340951 implements MigrationInterface { | ||||||
|  |     name = 'AddVersionHistory1727797340951' | ||||||
|  |  | ||||||
|  |     public async up(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |         await queryRunner.query(`CREATE TABLE "version_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "version" character varying NOT NULL, CONSTRAINT "PK_5db259cbb09ce82c0d13cfd1b23" PRIMARY KEY ("id"))`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async down(queryRunner: QueryRunner): Promise<void> { | ||||||
|  |         await queryRunner.query(`DROP TABLE "version_history"`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -32,6 +32,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf | |||||||
| import { ITagRepository } from 'src/interfaces/tag.interface'; | import { ITagRepository } from 'src/interfaces/tag.interface'; | ||||||
| import { ITrashRepository } from 'src/interfaces/trash.interface'; | import { ITrashRepository } from 'src/interfaces/trash.interface'; | ||||||
| import { IUserRepository } from 'src/interfaces/user.interface'; | import { IUserRepository } from 'src/interfaces/user.interface'; | ||||||
|  | import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; | ||||||
| import { IViewRepository } from 'src/interfaces/view.interface'; | import { IViewRepository } from 'src/interfaces/view.interface'; | ||||||
| import { AccessRepository } from 'src/repositories/access.repository'; | import { AccessRepository } from 'src/repositories/access.repository'; | ||||||
| import { ActivityRepository } from 'src/repositories/activity.repository'; | import { ActivityRepository } from 'src/repositories/activity.repository'; | ||||||
| @@ -67,6 +68,7 @@ import { SystemMetadataRepository } from 'src/repositories/system-metadata.repos | |||||||
| import { TagRepository } from 'src/repositories/tag.repository'; | import { TagRepository } from 'src/repositories/tag.repository'; | ||||||
| import { TrashRepository } from 'src/repositories/trash.repository'; | import { TrashRepository } from 'src/repositories/trash.repository'; | ||||||
| import { UserRepository } from 'src/repositories/user.repository'; | import { UserRepository } from 'src/repositories/user.repository'; | ||||||
|  | import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; | ||||||
| import { ViewRepository } from 'src/repositories/view-repository'; | import { ViewRepository } from 'src/repositories/view-repository'; | ||||||
|  |  | ||||||
| export const repositories = [ | export const repositories = [ | ||||||
| @@ -104,5 +106,6 @@ export const repositories = [ | |||||||
|   { provide: ITagRepository, useClass: TagRepository }, |   { provide: ITagRepository, useClass: TagRepository }, | ||||||
|   { provide: ITrashRepository, useClass: TrashRepository }, |   { provide: ITrashRepository, useClass: TrashRepository }, | ||||||
|   { provide: IUserRepository, useClass: UserRepository }, |   { provide: IUserRepository, useClass: UserRepository }, | ||||||
|  |   { provide: IVersionHistoryRepository, useClass: VersionHistoryRepository }, | ||||||
|   { provide: IViewRepository, useClass: ViewRepository }, |   { provide: IViewRepository, useClass: ViewRepository }, | ||||||
| ]; | ]; | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								server/src/repositories/version-history.repository.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								server/src/repositories/version-history.repository.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | import { Injectable } from '@nestjs/common'; | ||||||
|  | import { InjectRepository } from '@nestjs/typeorm'; | ||||||
|  | import { VersionHistoryEntity } from 'src/entities/version-history.entity'; | ||||||
|  | import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; | ||||||
|  | import { Instrumentation } from 'src/utils/instrumentation'; | ||||||
|  | import { Repository } from 'typeorm'; | ||||||
|  |  | ||||||
|  | @Instrumentation() | ||||||
|  | @Injectable() | ||||||
|  | export class VersionHistoryRepository implements IVersionHistoryRepository { | ||||||
|  |   constructor(@InjectRepository(VersionHistoryEntity) private repository: Repository<VersionHistoryEntity>) {} | ||||||
|  |  | ||||||
|  |   async getAll(): Promise<VersionHistoryEntity[]> { | ||||||
|  |     return this.repository.find({ order: { createdAt: 'DESC' } }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async getLatest(): Promise<VersionHistoryEntity | null> { | ||||||
|  |     const results = await this.repository.find({ order: { createdAt: 'DESC' }, take: 1 }); | ||||||
|  |     return results[0] || null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   create(version: Omit<VersionHistoryEntity, 'id' | 'createdAt'>): Promise<VersionHistoryEntity> { | ||||||
|  |     return this.repository.save(version); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,17 +1,21 @@ | |||||||
| import { DateTime } from 'luxon'; | import { DateTime } from 'luxon'; | ||||||
| import { serverVersion } from 'src/constants'; | import { serverVersion } from 'src/constants'; | ||||||
| import { SystemMetadataKey } from 'src/enum'; | import { SystemMetadataKey } from 'src/enum'; | ||||||
|  | import { IDatabaseRepository } from 'src/interfaces/database.interface'; | ||||||
| import { IEventRepository } from 'src/interfaces/event.interface'; | import { IEventRepository } from 'src/interfaces/event.interface'; | ||||||
| import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; | import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; | ||||||
| import { ILoggerRepository } from 'src/interfaces/logger.interface'; | import { ILoggerRepository } from 'src/interfaces/logger.interface'; | ||||||
| import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; | import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; | ||||||
| import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; | import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; | ||||||
|  | import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; | ||||||
| import { VersionService } from 'src/services/version.service'; | import { VersionService } from 'src/services/version.service'; | ||||||
|  | import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; | ||||||
| import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; | import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; | ||||||
| import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; | import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; | ||||||
| import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; | import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; | ||||||
| import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock'; | import { newServerInfoRepositoryMock } from 'test/repositories/server-info.repository.mock'; | ||||||
| import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; | import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; | ||||||
|  | import { newVersionHistoryRepositoryMock } from 'test/repositories/version-history.repository.mock'; | ||||||
| import { Mocked } from 'vitest'; | import { Mocked } from 'vitest'; | ||||||
|  |  | ||||||
| const mockRelease = (version: string) => ({ | const mockRelease = (version: string) => ({ | ||||||
| @@ -26,26 +30,47 @@ const mockRelease = (version: string) => ({ | |||||||
|  |  | ||||||
| describe(VersionService.name, () => { | describe(VersionService.name, () => { | ||||||
|   let sut: VersionService; |   let sut: VersionService; | ||||||
|  |   let databaseMock: Mocked<IDatabaseRepository>; | ||||||
|   let eventMock: Mocked<IEventRepository>; |   let eventMock: Mocked<IEventRepository>; | ||||||
|   let jobMock: Mocked<IJobRepository>; |   let jobMock: Mocked<IJobRepository>; | ||||||
|   let serverMock: Mocked<IServerInfoRepository>; |   let serverMock: Mocked<IServerInfoRepository>; | ||||||
|   let systemMock: Mocked<ISystemMetadataRepository>; |   let systemMock: Mocked<ISystemMetadataRepository>; | ||||||
|  |   let versionMock: Mocked<IVersionHistoryRepository>; | ||||||
|   let loggerMock: Mocked<ILoggerRepository>; |   let loggerMock: Mocked<ILoggerRepository>; | ||||||
|  |  | ||||||
|   beforeEach(() => { |   beforeEach(() => { | ||||||
|  |     databaseMock = newDatabaseRepositoryMock(); | ||||||
|     eventMock = newEventRepositoryMock(); |     eventMock = newEventRepositoryMock(); | ||||||
|     jobMock = newJobRepositoryMock(); |     jobMock = newJobRepositoryMock(); | ||||||
|     serverMock = newServerInfoRepositoryMock(); |     serverMock = newServerInfoRepositoryMock(); | ||||||
|     systemMock = newSystemMetadataRepositoryMock(); |     systemMock = newSystemMetadataRepositoryMock(); | ||||||
|  |     versionMock = newVersionHistoryRepositoryMock(); | ||||||
|     loggerMock = newLoggerRepositoryMock(); |     loggerMock = newLoggerRepositoryMock(); | ||||||
|  |  | ||||||
|     sut = new VersionService(eventMock, jobMock, serverMock, systemMock, loggerMock); |     sut = new VersionService(databaseMock, eventMock, jobMock, serverMock, systemMock, versionMock, loggerMock); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   it('should work', () => { |   it('should work', () => { | ||||||
|     expect(sut).toBeDefined(); |     expect(sut).toBeDefined(); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   describe('onBootstrap', () => { | ||||||
|  |     it('should record a new version', async () => { | ||||||
|  |       await expect(sut.onBootstrap()).resolves.toBeUndefined(); | ||||||
|  |       expect(versionMock.create).toHaveBeenCalledWith({ version: expect.any(String) }); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should skip a duplicate version', async () => { | ||||||
|  |       versionMock.getLatest.mockResolvedValue({ | ||||||
|  |         id: 'version-1', | ||||||
|  |         createdAt: new Date(), | ||||||
|  |         version: serverVersion.toString(), | ||||||
|  |       }); | ||||||
|  |       await expect(sut.onBootstrap()).resolves.toBeUndefined(); | ||||||
|  |       expect(versionMock.create).not.toHaveBeenCalled(); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   describe('getVersion', () => { |   describe('getVersion', () => { | ||||||
|     it('should respond the server version', () => { |     it('should respond the server version', () => { | ||||||
|       expect(sut.getVersion()).toEqual({ |       expect(sut.getVersion()).toEqual({ | ||||||
| @@ -56,6 +81,14 @@ describe(VersionService.name, () => { | |||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   describe('getVersionHistory', () => { | ||||||
|  |     it('should respond the server version history', async () => { | ||||||
|  |       const upgrade = { id: 'upgrade-1', createdAt: new Date(), version: '1.0.0' }; | ||||||
|  |       versionMock.getAll.mockResolvedValue([upgrade]); | ||||||
|  |       await expect(sut.getVersionHistory()).resolves.toEqual([upgrade]); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   describe('handQueueVersionCheck', () => { |   describe('handQueueVersionCheck', () => { | ||||||
|     it('should queue a version check job', async () => { |     it('should queue a version check job', async () => { | ||||||
|       await expect(sut.handleQueueVersionCheck()).resolves.toBeUndefined(); |       await expect(sut.handleQueueVersionCheck()).resolves.toBeUndefined(); | ||||||
|   | |||||||
| @@ -6,11 +6,13 @@ import { OnEvent } from 'src/decorators'; | |||||||
| import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; | import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; | ||||||
| import { VersionCheckMetadata } from 'src/entities/system-metadata.entity'; | import { VersionCheckMetadata } from 'src/entities/system-metadata.entity'; | ||||||
| import { SystemMetadataKey } from 'src/enum'; | import { SystemMetadataKey } from 'src/enum'; | ||||||
|  | import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; | ||||||
| import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; | import { ArgOf, IEventRepository } from 'src/interfaces/event.interface'; | ||||||
| import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; | import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; | ||||||
| import { ILoggerRepository } from 'src/interfaces/logger.interface'; | import { ILoggerRepository } from 'src/interfaces/logger.interface'; | ||||||
| import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; | import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; | ||||||
| import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; | import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; | ||||||
|  | import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; | ||||||
| import { BaseService } from 'src/services/base.service'; | import { BaseService } from 'src/services/base.service'; | ||||||
|  |  | ||||||
| const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => { | const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => { | ||||||
| @@ -25,10 +27,12 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re | |||||||
| @Injectable() | @Injectable() | ||||||
| export class VersionService extends BaseService { | export class VersionService extends BaseService { | ||||||
|   constructor( |   constructor( | ||||||
|  |     @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, | ||||||
|     @Inject(IEventRepository) private eventRepository: IEventRepository, |     @Inject(IEventRepository) private eventRepository: IEventRepository, | ||||||
|     @Inject(IJobRepository) private jobRepository: IJobRepository, |     @Inject(IJobRepository) private jobRepository: IJobRepository, | ||||||
|     @Inject(IServerInfoRepository) private repository: IServerInfoRepository, |     @Inject(IServerInfoRepository) private repository: IServerInfoRepository, | ||||||
|     @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, |     @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, | ||||||
|  |     @Inject(IVersionHistoryRepository) private versionRepository: IVersionHistoryRepository, | ||||||
|     @Inject(ILoggerRepository) logger: ILoggerRepository, |     @Inject(ILoggerRepository) logger: ILoggerRepository, | ||||||
|   ) { |   ) { | ||||||
|     super(systemMetadataRepository, logger); |     super(systemMetadataRepository, logger); | ||||||
| @@ -38,12 +42,25 @@ export class VersionService extends BaseService { | |||||||
|   @OnEvent({ name: 'app.bootstrap' }) |   @OnEvent({ name: 'app.bootstrap' }) | ||||||
|   async onBootstrap(): Promise<void> { |   async onBootstrap(): Promise<void> { | ||||||
|     await this.handleVersionCheck(); |     await this.handleVersionCheck(); | ||||||
|  |  | ||||||
|  |     await this.databaseRepository.withLock(DatabaseLock.VersionHistory, async () => { | ||||||
|  |       const latest = await this.versionRepository.getLatest(); | ||||||
|  |       const current = serverVersion.toString(); | ||||||
|  |       if (!latest || latest.version !== current) { | ||||||
|  |         this.logger.log(`Version has changed, adding ${current} to history`); | ||||||
|  |         await this.versionRepository.create({ version: current }); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getVersion() { |   getVersion() { | ||||||
|     return ServerVersionResponseDto.fromSemVer(serverVersion); |     return ServerVersionResponseDto.fromSemVer(serverVersion); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   getVersionHistory() { | ||||||
|  |     return this.versionRepository.getAll(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   async handleQueueVersionCheck() { |   async handleQueueVersionCheck() { | ||||||
|     await this.jobRepository.queue({ name: JobName.VERSION_CHECK, data: {} }); |     await this.jobRepository.queue({ name: JobName.VERSION_CHECK, data: {} }); | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								server/test/repositories/version-history.repository.mock.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								server/test/repositories/version-history.repository.mock.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface'; | ||||||
|  | import { Mocked, vitest } from 'vitest'; | ||||||
|  |  | ||||||
|  | export const newVersionHistoryRepositoryMock = (): Mocked<IVersionHistoryRepository> => { | ||||||
|  |   return { | ||||||
|  |     getAll: vitest.fn().mockResolvedValue([]), | ||||||
|  |     getLatest: vitest.fn(), | ||||||
|  |     create: vitest.fn(), | ||||||
|  |   }; | ||||||
|  | }; | ||||||
| @@ -1,19 +1,19 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; |   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; | ||||||
|   import Portal from '$lib/components/shared-components/portal/portal.svelte'; |   import Portal from '$lib/components/shared-components/portal/portal.svelte'; | ||||||
|   import { type ServerAboutResponseDto } from '@immich/sdk'; |   import { type ServerAboutResponseDto, type ServerVersionHistoryResponseDto } from '@immich/sdk'; | ||||||
|  |   import { DateTime } from 'luxon'; | ||||||
|   import { t } from 'svelte-i18n'; |   import { t } from 'svelte-i18n'; | ||||||
|  |  | ||||||
|   export let onClose: () => void; |   export let onClose: () => void; | ||||||
|  |  | ||||||
|   export let info: ServerAboutResponseDto; |   export let info: ServerAboutResponseDto; | ||||||
|  |   export let versions: ServerVersionHistoryResponseDto[]; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <Portal> | <Portal> | ||||||
|   <FullScreenModal title={$t('about')} {onClose}> |   <FullScreenModal title={$t('about')} {onClose}> | ||||||
|     <div |     <div class="flex flex-col sm:grid sm:grid-cols-2 gap-1 text-immich-primary dark:text-immich-dark-primary"> | ||||||
|       class="immich-scrollbar max-h-[500px] overflow-y-auto flex flex-col sm:grid sm:grid-cols-2 gap-1 text-immich-primary dark:text-immich-dark-primary" |  | ||||||
|     > |  | ||||||
|       <div> |       <div> | ||||||
|         <label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="version-desc" |         <label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="version-desc" | ||||||
|           >Immich</label |           >Immich</label | ||||||
| @@ -151,6 +151,35 @@ | |||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       {/if} |       {/if} | ||||||
|  |  | ||||||
|  |       <div class="col-span-full"> | ||||||
|  |         <label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="version-history" | ||||||
|  |           >{$t('version_history')}</label | ||||||
|  |         > | ||||||
|  |         <ul id="version-history" class="list-none"> | ||||||
|  |           {#each versions.slice(0, 5) as item (item.id)} | ||||||
|  |             {@const createdAt = DateTime.fromISO(item.createdAt)} | ||||||
|  |             <li> | ||||||
|  |               <span | ||||||
|  |                 class="immich-form-label pb-2 text-xs" | ||||||
|  |                 id="version-history" | ||||||
|  |                 title={createdAt.toLocaleString(DateTime.DATETIME_SHORT_WITH_SECONDS)} | ||||||
|  |               > | ||||||
|  |                 {$t('version_history_item', { | ||||||
|  |                   values: { | ||||||
|  |                     version: item.version, | ||||||
|  |                     date: createdAt.toLocaleString({ | ||||||
|  |                       month: 'short', | ||||||
|  |                       day: 'numeric', | ||||||
|  |                       year: 'numeric', | ||||||
|  |                     }), | ||||||
|  |                   }, | ||||||
|  |                 })} | ||||||
|  |               </span> | ||||||
|  |             </li> | ||||||
|  |           {/each} | ||||||
|  |         </ul> | ||||||
|  |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </FullScreenModal> |   </FullScreenModal> | ||||||
| </Portal> | </Portal> | ||||||
|   | |||||||
| @@ -4,7 +4,12 @@ | |||||||
|   import { requestServerInfo } from '$lib/utils/auth'; |   import { requestServerInfo } from '$lib/utils/auth'; | ||||||
|   import { onMount } from 'svelte'; |   import { onMount } from 'svelte'; | ||||||
|   import { t } from 'svelte-i18n'; |   import { t } from 'svelte-i18n'; | ||||||
|   import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk'; |   import { | ||||||
|  |     getAboutInfo, | ||||||
|  |     getVersionHistory, | ||||||
|  |     type ServerAboutResponseDto, | ||||||
|  |     type ServerVersionHistoryResponseDto, | ||||||
|  |   } from '@immich/sdk'; | ||||||
|  |  | ||||||
|   const { serverVersion, connected } = websocketStore; |   const { serverVersion, connected } = websocketStore; | ||||||
|  |  | ||||||
| @@ -12,16 +17,17 @@ | |||||||
|  |  | ||||||
|   $: version = $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null; |   $: version = $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null; | ||||||
|  |  | ||||||
|   let aboutInfo: ServerAboutResponseDto; |   let info: ServerAboutResponseDto; | ||||||
|  |   let versions: ServerVersionHistoryResponseDto[] = []; | ||||||
|  |  | ||||||
|   onMount(async () => { |   onMount(async () => { | ||||||
|     await requestServerInfo(); |     await requestServerInfo(); | ||||||
|     aboutInfo = await getAboutInfo(); |     [info, versions] = await Promise.all([getAboutInfo(), getVersionHistory()]); | ||||||
|   }); |   }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| {#if isOpen} | {#if isOpen} | ||||||
|   <ServerAboutModal onClose={() => (isOpen = false)} info={aboutInfo} /> |   <ServerAboutModal onClose={() => (isOpen = false)} {info} {versions} /> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
| <div | <div | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import ServerAboutModal from '$lib/components/shared-components/server-about-modal.svelte'; |  | ||||||
|   import { locale } from '$lib/stores/preferences.store'; |   import { locale } from '$lib/stores/preferences.store'; | ||||||
|   import { serverInfo } from '$lib/stores/server-info.store'; |   import { serverInfo } from '$lib/stores/server-info.store'; | ||||||
|   import { user } from '$lib/stores/user.store'; |   import { user } from '$lib/stores/user.store'; | ||||||
| @@ -8,18 +7,14 @@ | |||||||
|   import { t } from 'svelte-i18n'; |   import { t } from 'svelte-i18n'; | ||||||
|   import { getByteUnitString } from '../../../utils/byte-units'; |   import { getByteUnitString } from '../../../utils/byte-units'; | ||||||
|   import LoadingSpinner from '../loading-spinner.svelte'; |   import LoadingSpinner from '../loading-spinner.svelte'; | ||||||
|   import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk'; |  | ||||||
|  |  | ||||||
|   let usageClasses = ''; |   let usageClasses = ''; | ||||||
|   let isOpen = false; |  | ||||||
|  |  | ||||||
|   $: hasQuota = $user?.quotaSizeInBytes !== null; |   $: hasQuota = $user?.quotaSizeInBytes !== null; | ||||||
|   $: availableBytes = (hasQuota ? $user?.quotaSizeInBytes : $serverInfo?.diskSizeRaw) || 0; |   $: availableBytes = (hasQuota ? $user?.quotaSizeInBytes : $serverInfo?.diskSizeRaw) || 0; | ||||||
|   $: usedBytes = (hasQuota ? $user?.quotaUsageInBytes : $serverInfo?.diskUseRaw) || 0; |   $: usedBytes = (hasQuota ? $user?.quotaUsageInBytes : $serverInfo?.diskUseRaw) || 0; | ||||||
|   $: usedPercentage = Math.min(Math.round((usedBytes / availableBytes) * 100), 100); |   $: usedPercentage = Math.min(Math.round((usedBytes / availableBytes) * 100), 100); | ||||||
|  |  | ||||||
|   let aboutInfo: ServerAboutResponseDto; |  | ||||||
|  |  | ||||||
|   const onUpdate = () => { |   const onUpdate = () => { | ||||||
|     usageClasses = getUsageClass(); |     usageClasses = getUsageClass(); | ||||||
|   }; |   }; | ||||||
| @@ -42,14 +37,9 @@ | |||||||
|  |  | ||||||
|   onMount(async () => { |   onMount(async () => { | ||||||
|     await requestServerInfo(); |     await requestServerInfo(); | ||||||
|     aboutInfo = await getAboutInfo(); |  | ||||||
|   }); |   }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| {#if isOpen} |  | ||||||
|   <ServerAboutModal onClose={() => (isOpen = false)} info={aboutInfo} /> |  | ||||||
| {/if} |  | ||||||
|  |  | ||||||
| <div | <div | ||||||
|   class="hidden md:block storage-status p-4 bg-gray-100 dark:bg-immich-dark-primary/10 ml-4 rounded-lg text-sm" |   class="hidden md:block storage-status p-4 bg-gray-100 dark:bg-immich-dark-primary/10 ml-4 rounded-lg text-sm" | ||||||
|   title={$t('storage_usage', { |   title={$t('storage_usage', { | ||||||
|   | |||||||
| @@ -1278,6 +1278,8 @@ | |||||||
|   "version": "Version", |   "version": "Version", | ||||||
|   "version_announcement_closing": "Your friend, Alex", |   "version_announcement_closing": "Your friend, Alex", | ||||||
|   "version_announcement_message": "Hi friend, there is a new version of the application please take your time to visit the <link>release notes</link> and ensure your <code>docker-compose.yml</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your application automatically.", |   "version_announcement_message": "Hi friend, there is a new version of the application please take your time to visit the <link>release notes</link> and ensure your <code>docker-compose.yml</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your application automatically.", | ||||||
|  |   "version_history": "Version History", | ||||||
|  |   "version_history_item": "Installed {version} on {date}", | ||||||
|   "video": "Video", |   "video": "Video", | ||||||
|   "video_hover_setting": "Play video thumbnail on hover", |   "video_hover_setting": "Play video thumbnail on hover", | ||||||
|   "video_hover_setting_description": "Play video thumbnail when mouse is hovering over item. Even when disabled, playback can be started by hovering over the play icon.", |   "video_hover_setting_description": "Play video thumbnail when mouse is hovering over item. Even when disabled, playback can be started by hovering over the play icon.", | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user