feat: sub-pages for integrity reports

This commit is contained in:
izzy
2025-11-28 11:40:53 +00:00
parent d3abed3414
commit ca358f4dae
23 changed files with 785 additions and 179 deletions

View File

@@ -181,6 +181,10 @@
"maintenance_settings_description": "Put Immich into maintenance mode.", "maintenance_settings_description": "Put Immich into maintenance mode.",
"maintenance_start": "Start maintenance mode", "maintenance_start": "Start maintenance mode",
"maintenance_start_error": "Failed to start maintenance mode.", "maintenance_start_error": "Failed to start maintenance mode.",
"maintenance_integrity_report": "Integrity Report",
"maintenance_integrity_orphan_file": "Orphan Files",
"maintenance_integrity_missing_file": "Missing Files",
"maintenance_integrity_checksum_mismatch": "Checksum Mismatch",
"manage_concurrency": "Manage Concurrency", "manage_concurrency": "Manage Concurrency",
"manage_log_settings": "Manage log settings", "manage_log_settings": "Manage log settings",
"map_dark_style": "Dark style", "map_dark_style": "Dark style",

View File

@@ -161,7 +161,8 @@ Class | Method | HTTP request | Description
*LibrariesApi* | [**scanLibrary**](doc//LibrariesApi.md#scanlibrary) | **POST** /libraries/{id}/scan | Scan a library *LibrariesApi* | [**scanLibrary**](doc//LibrariesApi.md#scanlibrary) | **POST** /libraries/{id}/scan | Scan a library
*LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} | Update a library *LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} | Update a library
*LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | Validate library settings *LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | Validate library settings
*MaintenanceAdminApi* | [**getIntegrityReport**](doc//MaintenanceAdminApi.md#getintegrityreport) | **GET** /admin/maintenance | Get integrity report *MaintenanceAdminApi* | [**getIntegrityReport**](doc//MaintenanceAdminApi.md#getintegrityreport) | **POST** /admin/maintenance/integrity/report | Get integrity report by type
*MaintenanceAdminApi* | [**getIntegrityReportSummary**](doc//MaintenanceAdminApi.md#getintegrityreportsummary) | **GET** /admin/maintenance/integrity/summary | Get integrity report summary
*MaintenanceAdminApi* | [**maintenanceLogin**](doc//MaintenanceAdminApi.md#maintenancelogin) | **POST** /admin/maintenance/login | Log into maintenance mode *MaintenanceAdminApi* | [**maintenanceLogin**](doc//MaintenanceAdminApi.md#maintenancelogin) | **POST** /admin/maintenance/login | Log into maintenance mode
*MaintenanceAdminApi* | [**setMaintenanceMode**](doc//MaintenanceAdminApi.md#setmaintenancemode) | **POST** /admin/maintenance | Set maintenance mode *MaintenanceAdminApi* | [**setMaintenanceMode**](doc//MaintenanceAdminApi.md#setmaintenancemode) | **POST** /admin/maintenance | Set maintenance mode
*MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | Retrieve map markers *MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | Retrieve map markers
@@ -403,6 +404,7 @@ Class | Method | HTTP request | Description
- [FoldersResponse](doc//FoldersResponse.md) - [FoldersResponse](doc//FoldersResponse.md)
- [FoldersUpdate](doc//FoldersUpdate.md) - [FoldersUpdate](doc//FoldersUpdate.md)
- [ImageFormat](doc//ImageFormat.md) - [ImageFormat](doc//ImageFormat.md)
- [IntegrityReportType](doc//IntegrityReportType.md)
- [JobCreateDto](doc//JobCreateDto.md) - [JobCreateDto](doc//JobCreateDto.md)
- [JobName](doc//JobName.md) - [JobName](doc//JobName.md)
- [JobSettingsDto](doc//JobSettingsDto.md) - [JobSettingsDto](doc//JobSettingsDto.md)
@@ -417,8 +419,10 @@ Class | Method | HTTP request | Description
- [MachineLearningAvailabilityChecksDto](doc//MachineLearningAvailabilityChecksDto.md) - [MachineLearningAvailabilityChecksDto](doc//MachineLearningAvailabilityChecksDto.md)
- [MaintenanceAction](doc//MaintenanceAction.md) - [MaintenanceAction](doc//MaintenanceAction.md)
- [MaintenanceAuthDto](doc//MaintenanceAuthDto.md) - [MaintenanceAuthDto](doc//MaintenanceAuthDto.md)
- [MaintenanceGetIntegrityReportDto](doc//MaintenanceGetIntegrityReportDto.md)
- [MaintenanceIntegrityReportDto](doc//MaintenanceIntegrityReportDto.md) - [MaintenanceIntegrityReportDto](doc//MaintenanceIntegrityReportDto.md)
- [MaintenanceIntegrityReportResponseDto](doc//MaintenanceIntegrityReportResponseDto.md) - [MaintenanceIntegrityReportResponseDto](doc//MaintenanceIntegrityReportResponseDto.md)
- [MaintenanceIntegrityReportSummaryResponseDto](doc//MaintenanceIntegrityReportSummaryResponseDto.md)
- [MaintenanceLoginDto](doc//MaintenanceLoginDto.md) - [MaintenanceLoginDto](doc//MaintenanceLoginDto.md)
- [ManualJobName](doc//ManualJobName.md) - [ManualJobName](doc//ManualJobName.md)
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)

View File

@@ -154,6 +154,7 @@ part 'model/facial_recognition_config.dart';
part 'model/folders_response.dart'; part 'model/folders_response.dart';
part 'model/folders_update.dart'; part 'model/folders_update.dart';
part 'model/image_format.dart'; part 'model/image_format.dart';
part 'model/integrity_report_type.dart';
part 'model/job_create_dto.dart'; part 'model/job_create_dto.dart';
part 'model/job_name.dart'; part 'model/job_name.dart';
part 'model/job_settings_dto.dart'; part 'model/job_settings_dto.dart';
@@ -168,8 +169,10 @@ part 'model/logout_response_dto.dart';
part 'model/machine_learning_availability_checks_dto.dart'; part 'model/machine_learning_availability_checks_dto.dart';
part 'model/maintenance_action.dart'; part 'model/maintenance_action.dart';
part 'model/maintenance_auth_dto.dart'; part 'model/maintenance_auth_dto.dart';
part 'model/maintenance_get_integrity_report_dto.dart';
part 'model/maintenance_integrity_report_dto.dart'; part 'model/maintenance_integrity_report_dto.dart';
part 'model/maintenance_integrity_report_response_dto.dart'; part 'model/maintenance_integrity_report_response_dto.dart';
part 'model/maintenance_integrity_report_summary_response_dto.dart';
part 'model/maintenance_login_dto.dart'; part 'model/maintenance_login_dto.dart';
part 'model/manual_job_name.dart'; part 'model/manual_job_name.dart';
part 'model/map_marker_response_dto.dart'; part 'model/map_marker_response_dto.dart';

View File

@@ -16,14 +16,70 @@ class MaintenanceAdminApi {
final ApiClient apiClient; final ApiClient apiClient;
/// Get integrity report /// Get integrity report by type
/// ///
/// ... /// ...
/// ///
/// Note: This method returns the HTTP [Response]. /// Note: This method returns the HTTP [Response].
Future<Response> getIntegrityReportWithHttpInfo() async { ///
/// Parameters:
///
/// * [MaintenanceGetIntegrityReportDto] maintenanceGetIntegrityReportDto (required):
Future<Response> getIntegrityReportWithHttpInfo(MaintenanceGetIntegrityReportDto maintenanceGetIntegrityReportDto,) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final apiPath = r'/admin/maintenance'; final apiPath = r'/admin/maintenance/integrity/report';
// ignore: prefer_final_locals
Object? postBody = maintenanceGetIntegrityReportDto;
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,
);
}
/// Get integrity report by type
///
/// ...
///
/// Parameters:
///
/// * [MaintenanceGetIntegrityReportDto] maintenanceGetIntegrityReportDto (required):
Future<MaintenanceIntegrityReportResponseDto?> getIntegrityReport(MaintenanceGetIntegrityReportDto maintenanceGetIntegrityReportDto,) async {
final response = await getIntegrityReportWithHttpInfo(maintenanceGetIntegrityReportDto,);
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), 'MaintenanceIntegrityReportResponseDto',) as MaintenanceIntegrityReportResponseDto;
}
return null;
}
/// Get integrity report summary
///
/// ...
///
/// Note: This method returns the HTTP [Response].
Future<Response> getIntegrityReportSummaryWithHttpInfo() async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/maintenance/integrity/summary';
// ignore: prefer_final_locals // ignore: prefer_final_locals
Object? postBody; Object? postBody;
@@ -46,11 +102,11 @@ class MaintenanceAdminApi {
); );
} }
/// Get integrity report /// Get integrity report summary
/// ///
/// ... /// ...
Future<MaintenanceIntegrityReportResponseDto?> getIntegrityReport() async { Future<MaintenanceIntegrityReportSummaryResponseDto?> getIntegrityReportSummary() async {
final response = await getIntegrityReportWithHttpInfo(); final response = await getIntegrityReportSummaryWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
@@ -58,7 +114,7 @@ class MaintenanceAdminApi {
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string. // FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MaintenanceIntegrityReportResponseDto',) as MaintenanceIntegrityReportResponseDto; return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MaintenanceIntegrityReportSummaryResponseDto',) as MaintenanceIntegrityReportSummaryResponseDto;
} }
return null; return null;

View File

@@ -356,6 +356,8 @@ class ApiClient {
return FoldersUpdate.fromJson(value); return FoldersUpdate.fromJson(value);
case 'ImageFormat': case 'ImageFormat':
return ImageFormatTypeTransformer().decode(value); return ImageFormatTypeTransformer().decode(value);
case 'IntegrityReportType':
return IntegrityReportTypeTypeTransformer().decode(value);
case 'JobCreateDto': case 'JobCreateDto':
return JobCreateDto.fromJson(value); return JobCreateDto.fromJson(value);
case 'JobName': case 'JobName':
@@ -384,10 +386,14 @@ class ApiClient {
return MaintenanceActionTypeTransformer().decode(value); return MaintenanceActionTypeTransformer().decode(value);
case 'MaintenanceAuthDto': case 'MaintenanceAuthDto':
return MaintenanceAuthDto.fromJson(value); return MaintenanceAuthDto.fromJson(value);
case 'MaintenanceGetIntegrityReportDto':
return MaintenanceGetIntegrityReportDto.fromJson(value);
case 'MaintenanceIntegrityReportDto': case 'MaintenanceIntegrityReportDto':
return MaintenanceIntegrityReportDto.fromJson(value); return MaintenanceIntegrityReportDto.fromJson(value);
case 'MaintenanceIntegrityReportResponseDto': case 'MaintenanceIntegrityReportResponseDto':
return MaintenanceIntegrityReportResponseDto.fromJson(value); return MaintenanceIntegrityReportResponseDto.fromJson(value);
case 'MaintenanceIntegrityReportSummaryResponseDto':
return MaintenanceIntegrityReportSummaryResponseDto.fromJson(value);
case 'MaintenanceLoginDto': case 'MaintenanceLoginDto':
return MaintenanceLoginDto.fromJson(value); return MaintenanceLoginDto.fromJson(value);
case 'ManualJobName': case 'ManualJobName':

View File

@@ -94,6 +94,9 @@ String parameterToString(dynamic value) {
if (value is ImageFormat) { if (value is ImageFormat) {
return ImageFormatTypeTransformer().encode(value).toString(); return ImageFormatTypeTransformer().encode(value).toString();
} }
if (value is IntegrityReportType) {
return IntegrityReportTypeTypeTransformer().encode(value).toString();
}
if (value is JobName) { if (value is JobName) {
return JobNameTypeTransformer().encode(value).toString(); return JobNameTypeTransformer().encode(value).toString();
} }

View File

@@ -0,0 +1,88 @@
//
// 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 IntegrityReportType {
/// Instantiate a new enum with the provided [value].
const IntegrityReportType._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const orphanFile = IntegrityReportType._(r'orphan_file');
static const missingFile = IntegrityReportType._(r'missing_file');
static const checksumMismatch = IntegrityReportType._(r'checksum_mismatch');
/// List of all possible values in this [enum][IntegrityReportType].
static const values = <IntegrityReportType>[
orphanFile,
missingFile,
checksumMismatch,
];
static IntegrityReportType? fromJson(dynamic value) => IntegrityReportTypeTypeTransformer().decode(value);
static List<IntegrityReportType> listFromJson(dynamic json, {bool growable = false,}) {
final result = <IntegrityReportType>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = IntegrityReportType.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [IntegrityReportType] to String,
/// and [decode] dynamic data back to [IntegrityReportType].
class IntegrityReportTypeTypeTransformer {
factory IntegrityReportTypeTypeTransformer() => _instance ??= const IntegrityReportTypeTypeTransformer._();
const IntegrityReportTypeTypeTransformer._();
String encode(IntegrityReportType data) => data.value;
/// Decodes a [dynamic value][data] to a IntegrityReportType.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
IntegrityReportType? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'orphan_file': return IntegrityReportType.orphanFile;
case r'missing_file': return IntegrityReportType.missingFile;
case r'checksum_mismatch': return IntegrityReportType.checksumMismatch;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [IntegrityReportTypeTypeTransformer] instance.
static IntegrityReportTypeTypeTransformer? _instance;
}

View File

@@ -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 MaintenanceGetIntegrityReportDto {
/// Returns a new [MaintenanceGetIntegrityReportDto] instance.
MaintenanceGetIntegrityReportDto({
required this.type,
});
IntegrityReportType type;
@override
bool operator ==(Object other) => identical(this, other) || other is MaintenanceGetIntegrityReportDto &&
other.type == type;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(type.hashCode);
@override
String toString() => 'MaintenanceGetIntegrityReportDto[type=$type]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'type'] = this.type;
return json;
}
/// Returns a new [MaintenanceGetIntegrityReportDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static MaintenanceGetIntegrityReportDto? fromJson(dynamic value) {
upgradeDto(value, "MaintenanceGetIntegrityReportDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return MaintenanceGetIntegrityReportDto(
type: IntegrityReportType.fromJson(json[r'type'])!,
);
}
return null;
}
static List<MaintenanceGetIntegrityReportDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <MaintenanceGetIntegrityReportDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = MaintenanceGetIntegrityReportDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, MaintenanceGetIntegrityReportDto> mapFromJson(dynamic json) {
final map = <String, MaintenanceGetIntegrityReportDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = MaintenanceGetIntegrityReportDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of MaintenanceGetIntegrityReportDto-objects as value to a dart map
static Map<String, List<MaintenanceGetIntegrityReportDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<MaintenanceGetIntegrityReportDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = MaintenanceGetIntegrityReportDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'type',
};
}

View File

@@ -22,7 +22,7 @@ class MaintenanceIntegrityReportDto {
String path; String path;
MaintenanceIntegrityReportDtoTypeEnum type; IntegrityReportType type;
@override @override
bool operator ==(Object other) => identical(this, other) || other is MaintenanceIntegrityReportDto && bool operator ==(Object other) => identical(this, other) || other is MaintenanceIntegrityReportDto &&
@@ -59,7 +59,7 @@ class MaintenanceIntegrityReportDto {
return MaintenanceIntegrityReportDto( return MaintenanceIntegrityReportDto(
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
path: mapValueOfType<String>(json, r'path')!, path: mapValueOfType<String>(json, r'path')!,
type: MaintenanceIntegrityReportDtoTypeEnum.fromJson(json[r'type'])!, type: IntegrityReportType.fromJson(json[r'type'])!,
); );
} }
return null; return null;
@@ -113,80 +113,3 @@ class MaintenanceIntegrityReportDto {
}; };
} }
class MaintenanceIntegrityReportDtoTypeEnum {
/// Instantiate a new enum with the provided [value].
const MaintenanceIntegrityReportDtoTypeEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const orphanFile = MaintenanceIntegrityReportDtoTypeEnum._(r'orphan_file');
static const missingFile = MaintenanceIntegrityReportDtoTypeEnum._(r'missing_file');
static const checksumMismatch = MaintenanceIntegrityReportDtoTypeEnum._(r'checksum_mismatch');
/// List of all possible values in this [enum][MaintenanceIntegrityReportDtoTypeEnum].
static const values = <MaintenanceIntegrityReportDtoTypeEnum>[
orphanFile,
missingFile,
checksumMismatch,
];
static MaintenanceIntegrityReportDtoTypeEnum? fromJson(dynamic value) => MaintenanceIntegrityReportDtoTypeEnumTypeTransformer().decode(value);
static List<MaintenanceIntegrityReportDtoTypeEnum> listFromJson(dynamic json, {bool growable = false,}) {
final result = <MaintenanceIntegrityReportDtoTypeEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = MaintenanceIntegrityReportDtoTypeEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [MaintenanceIntegrityReportDtoTypeEnum] to String,
/// and [decode] dynamic data back to [MaintenanceIntegrityReportDtoTypeEnum].
class MaintenanceIntegrityReportDtoTypeEnumTypeTransformer {
factory MaintenanceIntegrityReportDtoTypeEnumTypeTransformer() => _instance ??= const MaintenanceIntegrityReportDtoTypeEnumTypeTransformer._();
const MaintenanceIntegrityReportDtoTypeEnumTypeTransformer._();
String encode(MaintenanceIntegrityReportDtoTypeEnum data) => data.value;
/// Decodes a [dynamic value][data] to a MaintenanceIntegrityReportDtoTypeEnum.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
MaintenanceIntegrityReportDtoTypeEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'orphan_file': return MaintenanceIntegrityReportDtoTypeEnum.orphanFile;
case r'missing_file': return MaintenanceIntegrityReportDtoTypeEnum.missingFile;
case r'checksum_mismatch': return MaintenanceIntegrityReportDtoTypeEnum.checksumMismatch;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [MaintenanceIntegrityReportDtoTypeEnumTypeTransformer] instance.
static MaintenanceIntegrityReportDtoTypeEnumTypeTransformer? _instance;
}

View 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 MaintenanceIntegrityReportSummaryResponseDto {
/// Returns a new [MaintenanceIntegrityReportSummaryResponseDto] instance.
MaintenanceIntegrityReportSummaryResponseDto({
required this.checksumMismatch,
required this.missingFile,
required this.orphanFile,
});
int checksumMismatch;
int missingFile;
int orphanFile;
@override
bool operator ==(Object other) => identical(this, other) || other is MaintenanceIntegrityReportSummaryResponseDto &&
other.checksumMismatch == checksumMismatch &&
other.missingFile == missingFile &&
other.orphanFile == orphanFile;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(checksumMismatch.hashCode) +
(missingFile.hashCode) +
(orphanFile.hashCode);
@override
String toString() => 'MaintenanceIntegrityReportSummaryResponseDto[checksumMismatch=$checksumMismatch, missingFile=$missingFile, orphanFile=$orphanFile]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'checksum_mismatch'] = this.checksumMismatch;
json[r'missing_file'] = this.missingFile;
json[r'orphan_file'] = this.orphanFile;
return json;
}
/// Returns a new [MaintenanceIntegrityReportSummaryResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static MaintenanceIntegrityReportSummaryResponseDto? fromJson(dynamic value) {
upgradeDto(value, "MaintenanceIntegrityReportSummaryResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return MaintenanceIntegrityReportSummaryResponseDto(
checksumMismatch: mapValueOfType<int>(json, r'checksum_mismatch')!,
missingFile: mapValueOfType<int>(json, r'missing_file')!,
orphanFile: mapValueOfType<int>(json, r'orphan_file')!,
);
}
return null;
}
static List<MaintenanceIntegrityReportSummaryResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <MaintenanceIntegrityReportSummaryResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = MaintenanceIntegrityReportSummaryResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, MaintenanceIntegrityReportSummaryResponseDto> mapFromJson(dynamic json) {
final map = <String, MaintenanceIntegrityReportSummaryResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = MaintenanceIntegrityReportSummaryResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of MaintenanceIntegrityReportSummaryResponseDto-objects as value to a dart map
static Map<String, List<MaintenanceIntegrityReportSummaryResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<MaintenanceIntegrityReportSummaryResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = MaintenanceIntegrityReportSummaryResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'checksum_mismatch',
'missing_file',
'orphan_file',
};
}

View File

@@ -323,51 +323,6 @@
} }
}, },
"/admin/maintenance": { "/admin/maintenance": {
"get": {
"description": "...",
"operationId": "getIntegrityReport",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MaintenanceIntegrityReportResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Get integrity report",
"tags": [
"Maintenance (admin)"
],
"x-immich-admin-only": true,
"x-immich-history": [
{
"version": "v9.9.9",
"state": "Added"
},
{
"version": "v9.9.9",
"state": "Alpha"
}
],
"x-immich-permission": "maintenance",
"x-immich-state": "Alpha"
},
"post": { "post": {
"description": "Put Immich into or take it out of maintenance mode", "description": "Put Immich into or take it out of maintenance mode",
"operationId": "setMaintenanceMode", "operationId": "setMaintenanceMode",
@@ -417,6 +372,110 @@
"x-immich-state": "Alpha" "x-immich-state": "Alpha"
} }
}, },
"/admin/maintenance/integrity/report": {
"post": {
"description": "...",
"operationId": "getIntegrityReport",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MaintenanceGetIntegrityReportDto"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MaintenanceIntegrityReportResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Get integrity report by type",
"tags": [
"Maintenance (admin)"
],
"x-immich-admin-only": true,
"x-immich-history": [
{
"version": "v9.9.9",
"state": "Added"
},
{
"version": "v9.9.9",
"state": "Alpha"
}
],
"x-immich-permission": "maintenance",
"x-immich-state": "Alpha"
}
},
"/admin/maintenance/integrity/summary": {
"get": {
"description": "...",
"operationId": "getIntegrityReportSummary",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MaintenanceIntegrityReportSummaryResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Get integrity report summary",
"tags": [
"Maintenance (admin)"
],
"x-immich-admin-only": true,
"x-immich-history": [
{
"version": "v9.9.9",
"state": "Added"
},
{
"version": "v9.9.9",
"state": "Alpha"
}
],
"x-immich-permission": "maintenance",
"x-immich-state": "Alpha"
}
},
"/admin/maintenance/login": { "/admin/maintenance/login": {
"post": { "post": {
"description": "Login with maintenance token or cookie to receive current information and perform further actions.", "description": "Login with maintenance token or cookie to receive current information and perform further actions.",
@@ -16634,6 +16693,14 @@
], ],
"type": "string" "type": "string"
}, },
"IntegrityReportType": {
"enum": [
"orphan_file",
"missing_file",
"checksum_mismatch"
],
"type": "string"
},
"JobCreateDto": { "JobCreateDto": {
"properties": { "properties": {
"name": { "name": {
@@ -16965,6 +17032,21 @@
], ],
"type": "object" "type": "object"
}, },
"MaintenanceGetIntegrityReportDto": {
"properties": {
"type": {
"allOf": [
{
"$ref": "#/components/schemas/IntegrityReportType"
}
]
}
},
"required": [
"type"
],
"type": "object"
},
"MaintenanceIntegrityReportDto": { "MaintenanceIntegrityReportDto": {
"properties": { "properties": {
"id": { "id": {
@@ -16974,12 +17056,11 @@
"type": "string" "type": "string"
}, },
"type": { "type": {
"enum": [ "allOf": [
"orphan_file", {
"missing_file", "$ref": "#/components/schemas/IntegrityReportType"
"checksum_mismatch" }
], ]
"type": "string"
} }
}, },
"required": [ "required": [
@@ -17003,6 +17084,25 @@
], ],
"type": "object" "type": "object"
}, },
"MaintenanceIntegrityReportSummaryResponseDto": {
"properties": {
"checksum_mismatch": {
"type": "integer"
},
"missing_file": {
"type": "integer"
},
"orphan_file": {
"type": "integer"
}
},
"required": [
"checksum_mismatch",
"missing_file",
"orphan_file"
],
"type": "object"
},
"MaintenanceLoginDto": { "MaintenanceLoginDto": {
"properties": { "properties": {
"token": { "token": {

View File

@@ -40,16 +40,24 @@ export type ActivityStatisticsResponseDto = {
comments: number; comments: number;
likes: number; likes: number;
}; };
export type SetMaintenanceModeDto = {
action: MaintenanceAction;
};
export type MaintenanceGetIntegrityReportDto = {
"type": IntegrityReportType;
};
export type MaintenanceIntegrityReportDto = { export type MaintenanceIntegrityReportDto = {
id: string; id: string;
path: string; path: string;
"type": Type; "type": IntegrityReportType;
}; };
export type MaintenanceIntegrityReportResponseDto = { export type MaintenanceIntegrityReportResponseDto = {
items: MaintenanceIntegrityReportDto[]; items: MaintenanceIntegrityReportDto[];
}; };
export type SetMaintenanceModeDto = { export type MaintenanceIntegrityReportSummaryResponseDto = {
action: MaintenanceAction; checksum_mismatch: number;
missing_file: number;
orphan_file: number;
}; };
export type MaintenanceLoginDto = { export type MaintenanceLoginDto = {
token?: string; token?: string;
@@ -1874,17 +1882,6 @@ export function unlinkAllOAuthAccountsAdmin(opts?: Oazapfts.RequestOpts) {
method: "POST" method: "POST"
})); }));
} }
/**
* Get integrity report
*/
export function getIntegrityReport(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: MaintenanceIntegrityReportResponseDto;
}>("/admin/maintenance", {
...opts
}));
}
/** /**
* Set maintenance mode * Set maintenance mode
*/ */
@@ -1897,6 +1894,32 @@ export function setMaintenanceMode({ setMaintenanceModeDto }: {
body: setMaintenanceModeDto body: setMaintenanceModeDto
}))); })));
} }
/**
* Get integrity report by type
*/
export function getIntegrityReport({ maintenanceGetIntegrityReportDto }: {
maintenanceGetIntegrityReportDto: MaintenanceGetIntegrityReportDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 201;
data: MaintenanceIntegrityReportResponseDto;
}>("/admin/maintenance/integrity/report", oazapfts.json({
...opts,
method: "POST",
body: maintenanceGetIntegrityReportDto
})));
}
/**
* Get integrity report summary
*/
export function getIntegrityReportSummary(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: MaintenanceIntegrityReportSummaryResponseDto;
}>("/admin/maintenance/integrity/summary", {
...opts
}));
}
/** /**
* Log into maintenance mode * Log into maintenance mode
*/ */
@@ -5173,15 +5196,15 @@ export enum UserAvatarColor {
Gray = "gray", Gray = "gray",
Amber = "amber" Amber = "amber"
} }
export enum Type {
OrphanFile = "orphan_file",
MissingFile = "missing_file",
ChecksumMismatch = "checksum_mismatch"
}
export enum MaintenanceAction { export enum MaintenanceAction {
Start = "start", Start = "start",
End = "end" End = "end"
} }
export enum IntegrityReportType {
OrphanFile = "orphan_file",
MissingFile = "missing_file",
ChecksumMismatch = "checksum_mismatch"
}
export enum NotificationLevel { export enum NotificationLevel {
Success = "success", Success = "success",
Error = "error", Error = "error",

View File

@@ -7,6 +7,7 @@ import {
MaintenanceAuthDto, MaintenanceAuthDto,
MaintenanceGetIntegrityReportDto, MaintenanceGetIntegrityReportDto,
MaintenanceIntegrityReportResponseDto, MaintenanceIntegrityReportResponseDto,
MaintenanceIntegrityReportSummaryResponseDto,
MaintenanceLoginDto, MaintenanceLoginDto,
SetMaintenanceModeDto, SetMaintenanceModeDto,
} from 'src/dtos/maintenance.dto'; } from 'src/dtos/maintenance.dto';
@@ -53,14 +54,25 @@ export class MaintenanceController {
} }
} }
@Get() @Get('integrity/summary')
@Endpoint({ @Endpoint({
summary: 'Get integrity report', summary: 'Get integrity report summary',
description: '...', description: '...',
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'), history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
}) })
@Authenticated({ permission: Permission.Maintenance, admin: true }) @Authenticated({ permission: Permission.Maintenance, admin: true })
getIntegrityReport(dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> { getIntegrityReportSummary(): Promise<MaintenanceIntegrityReportSummaryResponseDto> {
return this.service.getIntegrityReportSummary(); //
}
@Post('integrity/report')
@Endpoint({
summary: 'Get integrity report by type',
description: '...',
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
})
@Authenticated({ permission: Permission.Maintenance, admin: true })
getIntegrityReport(@Body() dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> {
return this.service.getIntegrityReport(dto); return this.service.getIntegrityReport(dto);
} }
} }

View File

@@ -1,4 +1,4 @@
import { IsEnum } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger';
import { IntegrityReportType, MaintenanceAction } from 'src/enum'; import { IntegrityReportType, MaintenanceAction } from 'src/enum';
import { ValidateEnum, ValidateString } from 'src/validation'; import { ValidateEnum, ValidateString } from 'src/validation';
@@ -16,7 +16,19 @@ export class MaintenanceAuthDto {
username!: string; username!: string;
} }
export class MaintenanceIntegrityReportSummaryResponseDto {
@ApiProperty({ type: 'integer' })
[IntegrityReportType.ChecksumFail]!: number;
@ApiProperty({ type: 'integer' })
[IntegrityReportType.MissingFile]!: number;
@ApiProperty({ type: 'integer' })
[IntegrityReportType.OrphanFile]!: number;
}
export class MaintenanceGetIntegrityReportDto { export class MaintenanceGetIntegrityReportDto {
@ValidateEnum({ enum: IntegrityReportType, name: 'IntegrityReportType' })
type!: IntegrityReportType;
// todo: paginate // todo: paginate
// @IsInt() // @IsInt()
// @Min(1) // @Min(1)
@@ -27,7 +39,7 @@ export class MaintenanceGetIntegrityReportDto {
class MaintenanceIntegrityReportDto { class MaintenanceIntegrityReportDto {
id!: string; id!: string;
@IsEnum(IntegrityReportType) @ValidateEnum({ enum: IntegrityReportType, name: 'IntegrityReportType' })
type!: IntegrityReportType; type!: IntegrityReportType;
path!: string; path!: string;
} }

View File

@@ -1,7 +1,12 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Insertable, Kysely } from 'kysely'; import { Insertable, Kysely } from 'kysely';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
import { MaintenanceGetIntegrityReportDto, MaintenanceIntegrityReportResponseDto } from 'src/dtos/maintenance.dto'; import {
MaintenanceGetIntegrityReportDto,
MaintenanceIntegrityReportResponseDto,
MaintenanceIntegrityReportSummaryResponseDto,
} from 'src/dtos/maintenance.dto';
import { IntegrityReportType } from 'src/enum';
import { DB } from 'src/schema'; import { DB } from 'src/schema';
import { IntegrityReportTable } from 'src/schema/tables/integrity-report.table'; import { IntegrityReportTable } from 'src/schema/tables/integrity-report.table';
@@ -18,11 +23,36 @@ export class IntegrityReportRepository {
.executeTakeFirst(); .executeTakeFirst();
} }
async getIntegrityReport(_dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> { async getIntegrityReportSummary(): Promise<MaintenanceIntegrityReportSummaryResponseDto> {
return await this.db
.selectFrom('integrity_report')
.select((eb) =>
eb.fn
.countAll<number>()
.filterWhere('type', '=', IntegrityReportType.ChecksumFail)
.as(IntegrityReportType.ChecksumFail),
)
.select((eb) =>
eb.fn
.countAll<number>()
.filterWhere('type', '=', IntegrityReportType.MissingFile)
.as(IntegrityReportType.MissingFile),
)
.select((eb) =>
eb.fn
.countAll<number>()
.filterWhere('type', '=', IntegrityReportType.OrphanFile)
.as(IntegrityReportType.OrphanFile),
)
.executeTakeFirstOrThrow();
}
async getIntegrityReport(dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> {
return { return {
items: await this.db items: await this.db
.selectFrom('integrity_report') .selectFrom('integrity_report')
.select(['id', 'type', 'path']) .select(['id', 'type', 'path'])
.where('type', '=', dto.type)
.orderBy('createdAt', 'desc') .orderBy('createdAt', 'desc')
.execute(), .execute(),
}; };

View File

@@ -4,6 +4,7 @@ import {
MaintenanceAuthDto, MaintenanceAuthDto,
MaintenanceGetIntegrityReportDto, MaintenanceGetIntegrityReportDto,
MaintenanceIntegrityReportResponseDto, MaintenanceIntegrityReportResponseDto,
MaintenanceIntegrityReportSummaryResponseDto,
} from 'src/dtos/maintenance.dto'; } from 'src/dtos/maintenance.dto';
import { SystemMetadataKey } from 'src/enum'; import { SystemMetadataKey } from 'src/enum';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
@@ -55,6 +56,10 @@ export class MaintenanceService extends BaseService {
return await createMaintenanceLoginUrl(baseUrl, auth, secret); return await createMaintenanceLoginUrl(baseUrl, auth, secret);
} }
getIntegrityReportSummary(): Promise<MaintenanceIntegrityReportSummaryResponseDto> {
return this.integrityReportRepository.getIntegrityReportSummary();
}
getIntegrityReport(dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> { getIntegrityReport(dto: MaintenanceGetIntegrityReportDto): Promise<MaintenanceIntegrityReportResponseDto> {
return this.integrityReportRepository.getIntegrityReport(dto); return this.integrityReportRepository.getIntegrityReport(dto);
} }

View File

@@ -1,15 +1,17 @@
<script lang="ts"> <script lang="ts">
import { ByteUnit } from '$lib/utils/byte-units'; import { ByteUnit } from '$lib/utils/byte-units';
import { Code, Icon, Text } from '@immich/ui'; import { Code, Icon, Text } from '@immich/ui';
import type { Snippet } from 'svelte';
interface Props { interface Props {
icon: string; icon?: string;
title: string; title: string;
value: number; value: number;
unit?: ByteUnit | undefined; unit?: ByteUnit | undefined;
footer?: Snippet<[]>;
} }
let { icon, title, value, unit = undefined }: Props = $props(); let { icon, title, value, unit = undefined, footer }: Props = $props();
const zeros = $derived(() => { const zeros = $derived(() => {
const maxLength = 13; const maxLength = 13;
@@ -22,7 +24,9 @@
<div class="flex h-35 w-full flex-col justify-between rounded-3xl bg-subtle text-primary p-5"> <div class="flex h-35 w-full flex-col justify-between rounded-3xl bg-subtle text-primary p-5">
<div class="flex place-items-center gap-4"> <div class="flex place-items-center gap-4">
<Icon {icon} size="40" /> {#if icon}
<Icon {icon} size="40" />
{/if}
<Text size="large" fontWeight="bold" class="uppercase">{title}</Text> <Text size="large" fontWeight="bold" class="uppercase">{title}</Text>
</div> </div>
@@ -32,4 +36,6 @@
<Code color="muted" class="absolute -top-5 end-1 font-light p-0">{unit}</Code> <Code color="muted" class="absolute -top-5 end-1 font-light p-0">{unit}</Code>
{/if} {/if}
</div> </div>
{@render footer?.()}
</div> </div>

View File

@@ -23,6 +23,7 @@ export enum AppRoute {
ADMIN_LIBRARY_MANAGEMENT = '/admin/library-management', ADMIN_LIBRARY_MANAGEMENT = '/admin/library-management',
ADMIN_SETTINGS = '/admin/system-settings', ADMIN_SETTINGS = '/admin/system-settings',
ADMIN_MAINTENANCE_SETTINGS = '/admin/maintenance', ADMIN_MAINTENANCE_SETTINGS = '/admin/maintenance',
ADMIN_MAINTENANCE_INTEGRITY_REPORT = '/admin/maintenance/integrity-report/',
ADMIN_STATS = '/admin/server-status', ADMIN_STATS = '/admin/server-status',
ADMIN_JOBS = '/admin/jobs-status', ADMIN_JOBS = '/admin/jobs-status',
ADMIN_REPAIR = '/admin/repair', ADMIN_REPAIR = '/admin/repair',

View File

@@ -2,7 +2,7 @@
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte'; import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { NavbarItem } from '@immich/ui'; import { NavbarItem } from '@immich/ui';
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync } from '@mdi/js'; import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiWrench } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
</script> </script>
@@ -11,7 +11,7 @@
<NavbarItem title={$t('users')} href={AppRoute.ADMIN_USERS} icon={mdiAccountMultipleOutline} /> <NavbarItem title={$t('users')} href={AppRoute.ADMIN_USERS} icon={mdiAccountMultipleOutline} />
<NavbarItem title={$t('jobs')} href={AppRoute.ADMIN_JOBS} icon={mdiSync} /> <NavbarItem title={$t('jobs')} href={AppRoute.ADMIN_JOBS} icon={mdiSync} />
<NavbarItem title={$t('settings')} href={AppRoute.ADMIN_SETTINGS} icon={mdiCog} /> <NavbarItem title={$t('settings')} href={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
<NavbarItem title={$t('admin.maintenance_settings')} href={AppRoute.ADMIN_MAINTENANCE_SETTINGS} icon={mdiCog} /> <NavbarItem title={$t('admin.maintenance_settings')} href={AppRoute.ADMIN_MAINTENANCE_SETTINGS} icon={mdiWrench} />
<NavbarItem title={$t('external_libraries')} href={AppRoute.ADMIN_LIBRARY_MANAGEMENT} icon={mdiBookshelf} /> <NavbarItem title={$t('external_libraries')} href={AppRoute.ADMIN_LIBRARY_MANAGEMENT} icon={mdiBookshelf} />
<NavbarItem title={$t('server_stats')} href={AppRoute.ADMIN_STATS} icon={mdiServer} /> <NavbarItem title={$t('server_stats')} href={AppRoute.ADMIN_STATS} icon={mdiServer} />
</div> </div>

View File

@@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte'; import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
import SettingAccordionState from '$lib/components/shared-components/settings/setting-accordion-state.svelte'; import ServerStatisticsCard from '$lib/components/server-statistics/ServerStatisticsCard.svelte';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte'; import { AppRoute } from '$lib/constants';
import { QueryParameter } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { MaintenanceAction, setMaintenanceMode } from '@immich/sdk'; import { MaintenanceAction, setMaintenanceMode } from '@immich/sdk';
import { Button, HStack, IconButton, Text } from '@immich/ui'; import { Button, HStack, Text } from '@immich/ui';
import { mdiDotsVertical, mdiProgressWrench, mdiRefresh } from '@mdi/js'; import { mdiProgressWrench } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@@ -46,7 +45,26 @@
<section id="setting-content" class="flex place-content-center sm:mx-4"> <section id="setting-content" class="flex place-content-center sm:mx-4">
<section class="w-full pb-28 sm:w-5/6 md:w-[850px]"> <section class="w-full pb-28 sm:w-5/6 md:w-[850px]">
<SettingAccordionState queryParam={QueryParameter.IS_OPEN}> <p class="text-sm dark:text-immich-dark-fg uppercase">{$t('admin.maintenance_integrity_report')}</p>
<div class="mt-5 hidden justify-between lg:flex gap-4">
{#each ['orphan_file', 'missing_file', 'checksum_mismatch'] as const as reportType (reportType)}
<ServerStatisticsCard
title={$t(`admin.maintenance_integrity_${reportType}`)}
value={data.integrityReport[reportType]}
>
{#snippet footer()}
<Button
href={`${AppRoute.ADMIN_MAINTENANCE_INTEGRITY_REPORT + reportType}`}
size="tiny"
class="self-end mt-1">View Report</Button
>
{/snippet}
</ServerStatisticsCard>
{/each}
</div>
<!-- <SettingAccordionState queryParam={QueryParameter.IS_OPEN}>
<SettingAccordion <SettingAccordion
title="Integrity Report" title="Integrity Report"
subtitle={`There are ${data.integrityReport.items.length} unresolved issues!`} subtitle={`There are ${data.integrityReport.items.length} unresolved issues!`}
@@ -86,7 +104,7 @@
</tbody> </tbody>
</table> </table>
</SettingAccordion> </SettingAccordion>
</SettingAccordionState> </SettingAccordionState> -->
</section> </section>
</section> </section>
</AdminPageLayout> </AdminPageLayout>

View File

@@ -1,17 +1,17 @@
import { authenticate } from '$lib/utils/auth'; import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { getIntegrityReport } from '@immich/sdk'; import { getIntegrityReportSummary } from '@immich/sdk';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
export const load = (async ({ url }) => { export const load = (async ({ url }) => {
await authenticate(url, { admin: true }); await authenticate(url, { admin: true });
const integrityReport = await getIntegrityReport(); const integrityReport = await getIntegrityReportSummary();
const $t = await getFormatter(); const $t = await getFormatter();
return { return {
integrityReport, integrityReport,
meta: { meta: {
title: $t('admin.system_settings'), title: $t('admin.maintenance_settings'),
}, },
}; };
}) satisfies PageLoad; }) satisfies PageLoad;

View File

@@ -0,0 +1,75 @@
<script lang="ts">
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
import { AppRoute } from '$lib/constants';
import { IconButton } from '@immich/ui';
import { mdiDotsVertical } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
interface Props {
data: PageData;
}
let { data }: Props = $props();
// async function switchToMaintenance() {
// try {
// await setMaintenanceMode({
// setMaintenanceModeDto: {
// action: MaintenanceAction.Start,
// },
// });
// } catch (error) {
// handleError(error, $t('admin.maintenance_start_error'));
// }
// }
</script>
<AdminPageLayout
breadcrumbs={[
{ title: $t('admin.maintenance_settings'), href: AppRoute.ADMIN_MAINTENANCE_SETTINGS },
{ title: $t('admin.maintenance_integrity_report') },
{ title: data.meta.title },
]}
>
<!-- {#snippet buttons()}
<HStack gap={1}>
<Button
leadingIcon={mdiProgressWrench}
size="small"
variant="ghost"
color="secondary"
onclick={switchToMaintenance}
>
<Text class="hidden md:block">{$t('admin.maintenance_start')}</Text>
</Button>
</HStack>
{/snippet} -->
<section id="setting-content" class="flex place-content-center sm:mx-4">
<section class="w-full pb-28 sm:w-5/6 md:w-[850px]">
<table class="mt-5 w-full text-start">
<thead
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray"
>
<tr class="flex w-full place-items-center">
<th class="w-7/8 text-left px-2 text-sm font-medium">{$t('filename')}</th>
<th class="w-1/8"></th>
</tr>
</thead>
<tbody
class="block max-h-80 w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg"
>
{#each data.integrityReport.items as { id, path } (id)}
<tr class="flex py-1 w-full place-items-center even:bg-subtle/20 odd:bg-subtle/80">
<td class="w-7/8 text-ellipsis text-left px-2 text-sm select-all">{path}</td>
<td class="w-1/8 text-ellipsis text-right flex justify-end px-2">
<IconButton aria-label="Open" color="secondary" icon={mdiDotsVertical} variant="ghost" /></td
>
</tr>
{/each}
</tbody>
</table>
</section>
</section>
</AdminPageLayout>

View File

@@ -0,0 +1,23 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getIntegrityReport, IntegrityReportType } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
const type = params.type as IntegrityReportType;
await authenticate(url, { admin: true });
const integrityReport = await getIntegrityReport({
maintenanceGetIntegrityReportDto: {
type,
},
});
const $t = await getFormatter();
return {
integrityReport,
meta: {
title: $t(`admin.maintenance_integrity_${type}`),
},
};
}) satisfies PageLoad;