refactor: use respondWithCookie

refactor: merge start/end into one route
refactor: rename MaintenanceRepository to AppRepository
chore: use new ApiTag/Endpoint
refactor: apply other requested changes
This commit is contained in:
izzy 2025-11-14 14:54:06 +00:00
parent 15c0d5f27a
commit 1fc98990ab
No known key found for this signature in database
GPG key ID: 5059F398521BB0F6
22 changed files with 1041 additions and 10008 deletions

View file

@ -34,41 +34,41 @@ describe('/admin/maintenance', () => {
});
});
describe('POST /end', async () => {
it('should not work out of maintenance mode', async () => {
const { status, body } = await request(app)
.post('/admin/maintenance/end')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not in maintenance mode'));
});
});
// => enter maintenance mode
describe.sequential('POST /start', () => {
describe.sequential('POST /', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/admin/maintenance/start').send();
const { status, body } = await request(app).post('/admin/maintenance').send({
maintenanceMode: false
});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should only work for admins', async () => {
const { status, body } = await request(app)
.post('/admin/maintenance/start')
.post('/admin/maintenance')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
.send();
.send({ maintenanceMode: false });
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should be a no-op if try to exit maintenance mode', async () => {
const { status } = await request(app)
.post('/admin/maintenance')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ maintenanceMode: false });
expect(status).toBe(201);
});
it('should enter maintenance mode', async () => {
const { status, headers } = await request(app)
.post('/admin/maintenance/start')
.post('/admin/maintenance')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
.send({
maintenaceMode: true
});
expect(status).toBe(201);
cookie = headers['set-cookie'][0].split(';')[0];
@ -134,21 +134,25 @@ describe('/admin/maintenance', () => {
});
});
describe('POST /start', async () => {
it('should not work in maintenance mode', async () => {
const { status, body } = await request(app).post('/admin/maintenance/start').set('cookie', cookie!).send();
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Already in maintenance mode'));
describe('POST /', async () => {
it('should be a no-op if try to enter maintenance mode', async () => {
const { status } = await request(app)
.post('/admin/maintenance')
.set('cookie', cookie!)
.send({ maintenanceMode: true });
expect(status).toBe(201);
});
});
});
// => exit maintenance mode
describe.sequential('POST /end', () => {
describe.sequential('POST /', () => {
it('should exit maintenance mode', async () => {
console.info('using', cookie!);
const { status } = await request(app).post('/admin/maintenance/end').set('cookie', cookie!).send();
const { status } = await request(app).post('/admin/maintenance').set('cookie', cookie!).send({
maintenanceMode: false
});
expect(status).toBe(201);

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -17,11 +17,11 @@ import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter';
import { LoggingInterceptor } from 'src/middleware/logging.interceptor';
import { MaintenanceAuthGuard } from 'src/middleware/maintenance-auth.guard';
import { repositories } from 'src/repositories';
import { AppRepository } from 'src/repositories/app.repository';
import { ConfigRepository } from 'src/repositories/config.repository';
import { EventRepository } from 'src/repositories/event.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { MaintenanceWorkerRepository } from 'src/repositories/maintenance-worker.repository';
import { MaintenanceRepository } from 'src/repositories/maintenance.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository';
import { WebsocketRepository } from 'src/repositories/websocket.repository';
@ -104,7 +104,7 @@ export class ApiModule extends BaseModule {}
ConfigRepository,
LoggingRepository,
SystemMetadataRepository,
MaintenanceRepository,
AppRepository,
MaintenanceWorkerRepository,
MaintenanceWorkerService,
...commonMiddleware,

View file

@ -150,6 +150,7 @@ export const endpointTags: Record<ApiTag, string> = {
'Queues and background jobs are used for processing tasks asynchronously. Queues can be paused and resumed as needed.',
[ApiTag.Libraries]:
'An external library is made up of input file paths or expressions that are scanned for asset files. Discovered files are automatically imported. Assets much be unique within a library, but can be duplicated across libraries. Each user has a default upload library, and can have one or more external libraries.',
[ApiTag.Maintenance]: 'Maintenance mode allows you to put Immich in a read-only state to perform various operations.',
[ApiTag.Map]:
'Map endpoints include supplemental functionality related to geolocation, such as reverse geocoding and retrieving map markers for assets with geolocation data.',
[ApiTag.Memories]:

View file

@ -1,13 +1,12 @@
import { BadRequestException, Body, Controller, Get, Post, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Body, Controller, Get, Post, Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
import { MaintenanceAuthDto, MaintenanceLoginDto } from 'src/dtos/maintenance.dto';
import { MaintenanceAuthDto, MaintenanceLoginDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto';
import { ServerConfigDto } from 'src/dtos/server.dto';
import { ImmichCookie } from 'src/enum';
import { MaintenanceRoute } from 'src/middleware/maintenance-auth.guard';
import { MaintenanceWorkerService } from 'src/services/maintenance-worker.service';
import { respondWithCookie } from 'src/utils/response';
@ApiTags('Maintenance (admin)')
@Controller()
export class MaintenanceWorkerController {
constructor(private service: MaintenanceWorkerService) {}
@ -21,23 +20,21 @@ export class MaintenanceWorkerController {
async maintenanceLogin(
@Req() request: Request,
@Body() dto: MaintenanceLoginDto,
@Res({ passthrough: true }) response: Response,
@Res({ passthrough: true }) res: Response,
): Promise<MaintenanceAuthDto> {
const token = dto.token ?? request.cookies[ImmichCookie.MaintenanceToken];
const auth = await this.service.login(token);
response.cookie(ImmichCookie.MaintenanceToken, token);
return auth;
return respondWithCookie(res, auth, {
isSecure: true,
values: [{ key: ImmichCookie.MaintenanceToken, value: token }],
});
}
@Post('admin/maintenance/start')
@Post('admin/maintenance')
@MaintenanceRoute()
startMaintenance(): void {
throw new BadRequestException('Already in maintenance mode');
}
@Post('admin/maintenance/end')
@MaintenanceRoute()
async endMaintenance(): Promise<void> {
await this.service.endMaintenance();
async setMaintenanceMode(@Body() dto: SetMaintenanceModeDto): Promise<void> {
if (!dto.maintenanceMode) {
await this.service.endMaintenance();
}
}
}

View file

@ -1,32 +1,47 @@
import { BadRequestException, Body, Controller, Post, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Response } from 'express';
import { Endpoint, HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
import { MaintenanceAuthDto, MaintenanceLoginDto } from 'src/dtos/maintenance.dto';
import { ImmichCookie, Permission } from 'src/enum';
import { MaintenanceAuthDto, MaintenanceLoginDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.dto';
import { ApiTag, ImmichCookie, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { MaintenanceService } from 'src/services/maintenance.service';
import { respondWithCookie } from 'src/utils/response';
@ApiTags('Maintenance (admin)')
@ApiTags(ApiTag.Maintenance)
@Controller('admin/maintenance')
export class MaintenanceController {
constructor(private service: MaintenanceService) {}
@Post('login')
@Endpoint({
summary: 'Log into maintenance mode',
description: 'Login with maintenance token or cookie to receive current information and perform further actions.',
history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
})
maintenanceLogin(@Body() _dto: MaintenanceLoginDto): MaintenanceAuthDto {
throw new BadRequestException('Not in maintenance mode');
}
@Post('start')
@Post()
@Endpoint({
summary: 'Set maintenance mode',
description: 'Put Immich into or take it out of maintenance mode',
history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'),
})
@Authenticated({ permission: Permission.Maintenance, admin: true })
async startMaintenance(@Auth() auth: AuthDto, @Res({ passthrough: true }) response: Response): Promise<void> {
const { jwt } = await this.service.startMaintenance(auth.user.name);
response.cookie(ImmichCookie.MaintenanceToken, jwt);
}
@Post('end')
@Authenticated({ permission: Permission.Maintenance, admin: true })
endMaintenance(): void {
throw new BadRequestException('Not in maintenance mode');
async setMaintenanceMode(
@Auth() auth: AuthDto,
@Body() dto: SetMaintenanceModeDto,
@Res({ passthrough: true }) res: Response,
): Promise<void> {
if (dto.maintenanceMode) {
const { jwt } = await this.service.startMaintenance(auth.user.name);
return respondWithCookie(res, undefined, {
isSecure: true,
values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }],
});
}
}
}

View file

@ -1,4 +1,9 @@
import { ValidateString } from 'src/validation';
import { ValidateBoolean, ValidateString } from 'src/validation';
export class SetMaintenanceModeDto {
@ValidateBoolean()
maintenanceMode!: boolean;
}
export class MaintenanceLoginDto {
@ValidateString({ optional: true })

View file

@ -796,6 +796,7 @@ export enum ApiTag {
Faces = 'Faces',
Jobs = 'Jobs',
Libraries = 'Libraries',
Maintenance = 'Maintenance (admin)',
Map = 'Map',
Memories = 'Memories',
Notifications = 'Notifications',

View file

@ -18,17 +18,13 @@ class Workers {
/**
* Currently running workers
*/
workers: Partial<Record<ImmichWorker, Worker | ChildProcess>>;
workers: Partial<Record<ImmichWorker, { kill: () => void }>> = {};
/**
* Fail-safe in case anything dies during restart
*/
restarting = false;
constructor() {
this.workers = {};
}
/**
* Boot all enabled workers
*/
@ -80,17 +76,27 @@ class Workers {
const basePath = dirname(__filename);
const workerFile = join(basePath, 'workers', `${name}.js`);
const worker: Worker | ChildProcess =
name === ImmichWorker.Api
? fork(workerFile, [], {
execArgv: process.execArgv.map((arg) => (arg.startsWith('--inspect') ? '--inspect=0.0.0.0:9231' : arg)),
})
: new Worker(workerFile);
let anyWorker: Worker | ChildProcess;
let kill: () => void;
worker.on('error', (error) => this.onError(name, error));
worker.on('exit', (exitCode) => this.onExit(name, exitCode));
if (name === ImmichWorker.Api) {
const worker = fork(workerFile, [], {
execArgv: process.execArgv.map((arg) => (arg.startsWith('--inspect') ? '--inspect=0.0.0.0:9231' : arg)),
});
this.workers[name] = worker;
kill = () => worker.kill();
anyWorker = worker;
} else {
const worker = new Worker(workerFile);
kill = () => worker.terminate();
anyWorker = worker;
}
anyWorker.on('error', (error) => this.onError(name, error));
anyWorker.on('exit', (exitCode) => this.onExit(name, exitCode));
this.workers[name] = { kill };
}
onError(name: ImmichWorker, error: Error) {

View file

@ -51,7 +51,7 @@ export class MaintenanceAuthGuard implements CanActivate {
}
const request = context.switchToHttp().getRequest<MaintenanceAuthRequest>();
request['auth'] = await this.service.authenticate(request.headers);
request.auth = await this.service.authenticate(request.headers);
return true;
}

View file

@ -2,11 +2,9 @@ import { Injectable } from '@nestjs/common';
import { ExitCode } from 'src/enum';
@Injectable()
export class MaintenanceRepository {
export class AppRepository {
private closeFn?: () => Promise<void>;
constructor() {}
exitApp() {
/* eslint-disable unicorn/no-process-exit */
void this.closeFn?.().finally(() => process.exit(ExitCode.AppRestart));

View file

@ -3,6 +3,7 @@ import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
import { AlbumRepository } from 'src/repositories/album.repository';
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
import { AppRepository } from 'src/repositories/app.repository';
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
import { AssetRepository } from 'src/repositories/asset.repository';
import { AuditRepository } from 'src/repositories/audit.repository';
@ -18,7 +19,6 @@ import { JobRepository } from 'src/repositories/job.repository';
import { LibraryRepository } from 'src/repositories/library.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
import { MaintenanceRepository } from 'src/repositories/maintenance.repository';
import { MapRepository } from 'src/repositories/map.repository';
import { MediaRepository } from 'src/repositories/media.repository';
import { MemoryRepository } from 'src/repositories/memory.repository';
@ -55,6 +55,7 @@ export const repositories = [
AlbumUserRepository,
AuditRepository,
ApiKeyRepository,
AppRepository,
AssetRepository,
AssetJobRepository,
ConfigRepository,
@ -69,7 +70,6 @@ export const repositories = [
LibraryRepository,
LoggingRepository,
MachineLearningRepository,
MaintenanceRepository,
MapRepository,
MediaRepository,
MemoryRepository,

View file

@ -7,9 +7,9 @@ import {
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { AppRepository } from 'src/repositories/app.repository';
import { AppRestartEvent } from 'src/repositories/event.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { MaintenanceRepository } from 'src/repositories/maintenance.repository';
export const serverEvents = ['AppRestart'] as const;
export type ServerEvents = (typeof serverEvents)[number];
@ -30,7 +30,7 @@ export class MaintenanceWorkerRepository implements OnGatewayConnection, OnGatew
constructor(
private logger: LoggingRepository,
private maintenanceRepository: MaintenanceRepository,
private maintenanceRepository: AppRepository,
) {
this.logger.setContext(MaintenanceWorkerRepository.name);
}

View file

@ -10,6 +10,7 @@ import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
import { AlbumRepository } from 'src/repositories/album.repository';
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
import { AppRepository } from 'src/repositories/app.repository';
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
import { AssetRepository } from 'src/repositories/asset.repository';
import { AuditRepository } from 'src/repositories/audit.repository';
@ -25,7 +26,6 @@ import { JobRepository } from 'src/repositories/job.repository';
import { LibraryRepository } from 'src/repositories/library.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
import { MaintenanceRepository } from 'src/repositories/maintenance.repository';
import { MapRepository } from 'src/repositories/map.repository';
import { MediaRepository } from 'src/repositories/media.repository';
import { MemoryRepository } from 'src/repositories/memory.repository';
@ -65,6 +65,7 @@ export const BASE_SERVICE_DEPENDENCIES = [
AlbumRepository,
AlbumUserRepository,
ApiKeyRepository,
AppRepository,
AssetRepository,
AssetJobRepository,
AuditRepository,
@ -79,7 +80,6 @@ export const BASE_SERVICE_DEPENDENCIES = [
JobRepository,
LibraryRepository,
MachineLearningRepository,
MaintenanceRepository,
MapRepository,
MediaRepository,
MemoryRepository,
@ -120,6 +120,7 @@ export class BaseService {
protected albumRepository: AlbumRepository,
protected albumUserRepository: AlbumUserRepository,
protected apiKeyRepository: ApiKeyRepository,
protected appRepository: AppRepository,
protected assetRepository: AssetRepository,
protected assetJobRepository: AssetJobRepository,
protected auditRepository: AuditRepository,
@ -134,7 +135,6 @@ export class BaseService {
protected jobRepository: JobRepository,
protected libraryRepository: LibraryRepository,
protected machineLearningRepository: MachineLearningRepository,
protected maintenanceRepository: MaintenanceRepository,
protected mapRepository: MapRepository,
protected mediaRepository: MediaRepository,
protected memoryRepository: MemoryRepository,

View file

@ -1,4 +1,4 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { OnEvent } from 'src/decorators';
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
import { SystemMetadataKey } from 'src/enum';
@ -19,11 +19,6 @@ export class MaintenanceService extends BaseService {
}
async startMaintenance(username: string): Promise<{ jwt: string }> {
const { isMaintenanceMode } = await this.getMaintenanceMode();
if (isMaintenanceMode) {
throw new BadRequestException('Already in maintenance mode');
}
const secret = generateMaintenanceSecret();
await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, { isMaintenanceMode: true, secret });
await this.eventRepository.emit('AppRestart', { isMaintenanceMode: true });
@ -37,7 +32,7 @@ export class MaintenanceService extends BaseService {
@OnEvent({ name: 'AppRestart', server: true })
onRestart(): void {
this.maintenanceRepository.exitApp();
this.appRepository.exitApp();
}
async createLoginUrl(auth: MaintenanceAuthDto, secret?: string): Promise<string> {

View file

@ -2,7 +2,7 @@ import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { configureExpress, configureTelemetry } from 'src/app.common';
import { ApiModule } from 'src/app.module';
import { MaintenanceRepository } from 'src/repositories/maintenance.repository';
import { AppRepository } from 'src/repositories/app.repository';
import { ApiService } from 'src/services/api.service';
import { isStartUpError } from 'src/utils/misc';
@ -12,7 +12,7 @@ async function bootstrap() {
configureTelemetry();
const app = await NestFactory.create<NestExpressApplication>(ApiModule, { bufferLogs: true });
app.get(MaintenanceRepository).setCloseFn(() => app.close());
app.get(AppRepository).setCloseFn(() => app.close());
void configureExpress(app, {
ssr: ApiService,

View file

@ -2,7 +2,7 @@ import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { configureExpress, configureTelemetry } from 'src/app.common';
import { MaintenanceModule } from 'src/app.module';
import { MaintenanceRepository } from 'src/repositories/maintenance.repository';
import { AppRepository } from 'src/repositories/app.repository';
import { MaintenanceWorkerService } from 'src/services/maintenance-worker.service';
import { isStartUpError } from 'src/utils/misc';
@ -11,7 +11,7 @@ async function bootstrap() {
configureTelemetry();
const app = await NestFactory.create<NestExpressApplication>(MaintenanceModule, { bufferLogs: true });
app.get(MaintenanceRepository).setCloseFn(() => app.close());
app.get(AppRepository).setCloseFn(() => app.close());
void configureExpress(app, {
permitSwaggerWrite: false,
ssr: MaintenanceWorkerService,

View file

@ -3,9 +3,9 @@ import { isMainThread } from 'node:worker_threads';
import { MicroservicesModule } from 'src/app.module';
import { serverVersion } from 'src/constants';
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
import { AppRepository } from 'src/repositories/app.repository';
import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { MaintenanceRepository } from 'src/repositories/maintenance.repository';
import { bootstrapTelemetry } from 'src/repositories/telemetry.repository';
import { isStartUpError } from 'src/utils/misc';
@ -18,7 +18,7 @@ export async function bootstrap() {
const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true });
const logger = await app.resolve(LoggingRepository);
const configRepository = app.get(ConfigRepository);
app.get(MaintenanceRepository).setCloseFn(() => app.close());
app.get(AppRepository).setCloseFn(() => app.close());
const { environment, host } = configRepository.getEnv();

View file

@ -19,6 +19,7 @@ import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
import { AlbumRepository } from 'src/repositories/album.repository';
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
import { AppRepository } from 'src/repositories/app.repository';
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
import { AssetRepository } from 'src/repositories/asset.repository';
import { AuditRepository } from 'src/repositories/audit.repository';
@ -34,7 +35,6 @@ import { JobRepository } from 'src/repositories/job.repository';
import { LibraryRepository } from 'src/repositories/library.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
import { MaintenanceRepository } from 'src/repositories/maintenance.repository';
import { MapRepository } from 'src/repositories/map.repository';
import { MediaRepository } from 'src/repositories/media.repository';
import { MemoryRepository } from 'src/repositories/memory.repository';
@ -211,6 +211,7 @@ export type ServiceOverrides = {
album: AlbumRepository;
albumUser: AlbumUserRepository;
apiKey: ApiKeyRepository;
app: AppRepository;
audit: AuditRepository;
asset: AssetRepository;
assetJob: AssetJobRepository;
@ -226,7 +227,6 @@ export type ServiceOverrides = {
library: LibraryRepository;
logger: LoggingRepository;
machineLearning: MachineLearningRepository;
maintenance: MaintenanceRepository;
map: MapRepository;
media: MediaRepository;
memory: MemoryRepository;
@ -286,6 +286,7 @@ export const getMocks = () => {
albumUser: automock(AlbumUserRepository),
asset: newAssetRepositoryMock(),
assetJob: automock(AssetJobRepository),
app: automock(AppRepository, { strict: false }),
config: newConfigRepositoryMock(),
database: newDatabaseRepositoryMock(),
downloadRepository: automock(DownloadRepository, { strict: false }),
@ -297,7 +298,6 @@ export const getMocks = () => {
apiKey: automock(ApiKeyRepository),
library: automock(LibraryRepository, { strict: false }),
machineLearning: automock(MachineLearningRepository, { args: [loggerMock], strict: false }),
maintenance: automock(MaintenanceRepository, { strict: false }),
map: automock(MapRepository, { args: [undefined, undefined, { setContext: () => {} }] }),
media: newMediaRepositoryMock(),
memory: automock(MemoryRepository),
@ -348,6 +348,7 @@ export const newTestService = <T extends BaseService>(
overrides.album || (mocks.album as As<AlbumRepository>),
overrides.albumUser || (mocks.albumUser as As<AlbumUserRepository>),
overrides.apiKey || (mocks.apiKey as As<ApiKeyRepository>),
overrides.app || (mocks.app as As<AppRepository>),
overrides.asset || (mocks.asset as As<AssetRepository>),
overrides.assetJob || (mocks.assetJob as As<AssetJobRepository>),
overrides.audit || (mocks.audit as As<AuditRepository>),
@ -362,7 +363,6 @@ export const newTestService = <T extends BaseService>(
overrides.job || (mocks.job as As<JobRepository>),
overrides.library || (mocks.library as As<LibraryRepository>),
overrides.machineLearning || (mocks.machineLearning as As<MachineLearningRepository>),
overrides.maintenance || (mocks.maintenance as As<MaintenanceRepository>),
overrides.map || (mocks.map as As<MapRepository>),
overrides.media || (mocks.media as As<MediaRepository>),
overrides.memory || (mocks.memory as As<MemoryRepository>),

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { handleError } from '$lib/utils/handle-error';
import { startMaintenance } from '@immich/sdk';
import { setMaintenanceMode } from '@immich/sdk';
import { Button } from '@immich/ui';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
@ -13,7 +13,11 @@
async function start() {
try {
await startMaintenance();
await setMaintenanceMode({
setMaintenanceModeDto: {
maintenanceMode: true,
},
});
} catch (error) {
handleError(error, $t('admin.maintenance_start_error'));
}

View file

@ -3,7 +3,7 @@
import FormatMessage from '$lib/elements/FormatMessage.svelte';
import { maintenanceAuth } from '$lib/stores/maintenance.store';
import { handleError } from '$lib/utils/handle-error';
import { endMaintenance } from '@immich/sdk';
import { setMaintenanceMode } from '@immich/sdk';
import { Button, Heading, Link } from '@immich/ui';
import { t } from 'svelte-i18n';
@ -16,7 +16,11 @@
async function end() {
try {
await endMaintenance();
await setMaintenanceMode({
setMaintenanceModeDto: {
maintenanceMode: false,
},
});
} catch (error) {
handleError(error, $t('maintenance_end_error'));
}