diff --git a/i18n/en.json b/i18n/en.json index 5edbd87973..34bad1de7d 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1326,6 +1326,8 @@ "maintenance_end": "End maintenance mode", "maintenance_end_error": "Failed to end maintenance mode.", "maintenance_logged_in_as": "Currently logged in as {user}", + "maintenance_task_backup": "Creating a backup of the existing database...", + "maintenance_task_restore": "Restoring the chosen backup...", "maintenance_title": "Temporarily Unavailable", "make": "Make", "manage_geolocation": "Manage location", diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index ebb38ac907..fd94b993ce 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -159,10 +159,10 @@ Class | Method | HTTP request | Description *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* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | Validate library settings -*MaintenanceAdminApi* | [**deleteBackup**](doc//MaintenanceAdminApi.md#deletebackup) | **DELETE** /admin/maintenance/admin/maintenance/backups/{filename} | Delete backup -*MaintenanceAdminApi* | [**listBackups**](doc//MaintenanceAdminApi.md#listbackups) | **GET** /admin/maintenance/admin/maintenance/backups/list | List backups +*MaintenanceAdminApi* | [**deleteBackup**](doc//MaintenanceAdminApi.md#deletebackup) | **DELETE** /admin/maintenance/backups/{filename} | Delete backup +*MaintenanceAdminApi* | [**listBackups**](doc//MaintenanceAdminApi.md#listbackups) | **GET** /admin/maintenance/backups/list | List backups *MaintenanceAdminApi* | [**maintenanceLogin**](doc//MaintenanceAdminApi.md#maintenancelogin) | **POST** /admin/maintenance/login | Log into maintenance mode -*MaintenanceAdminApi* | [**maintenanceStatus**](doc//MaintenanceAdminApi.md#maintenancestatus) | **GET** /admin/maintenance/admin/maintenance/status | Get maintenance mode status +*MaintenanceAdminApi* | [**maintenanceStatus**](doc//MaintenanceAdminApi.md#maintenancestatus) | **GET** /admin/maintenance/status | Get maintenance mode status *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* | [**reverseGeocode**](doc//MapApi.md#reversegeocode) | **GET** /map/reverse-geocode | Reverse geocode coordinates diff --git a/mobile/openapi/lib/api/maintenance_admin_api.dart b/mobile/openapi/lib/api/maintenance_admin_api.dart index ffa6517e62..50c025ebec 100644 --- a/mobile/openapi/lib/api/maintenance_admin_api.dart +++ b/mobile/openapi/lib/api/maintenance_admin_api.dart @@ -27,7 +27,7 @@ class MaintenanceAdminApi { /// * [String] filename (required): Future deleteBackupWithHttpInfo(String filename,) async { // ignore: prefer_const_declarations - final apiPath = r'/admin/maintenance/admin/maintenance/backups/{filename}' + final apiPath = r'/admin/maintenance/backups/{filename}' .replaceAll('{filename}', filename); // ignore: prefer_final_locals @@ -72,7 +72,7 @@ class MaintenanceAdminApi { /// Note: This method returns the HTTP [Response]. Future listBackupsWithHttpInfo() async { // ignore: prefer_const_declarations - final apiPath = r'/admin/maintenance/admin/maintenance/backups/list'; + final apiPath = r'/admin/maintenance/backups/list'; // ignore: prefer_final_locals Object? postBody; @@ -176,7 +176,7 @@ class MaintenanceAdminApi { /// Note: This method returns the HTTP [Response]. Future maintenanceStatusWithHttpInfo() async { // ignore: prefer_const_declarations - final apiPath = r'/admin/maintenance/admin/maintenance/status'; + final apiPath = r'/admin/maintenance/status'; // ignore: prefer_final_locals Object? postBody; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 915b6e6b0d..5f8cfee38b 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -372,7 +372,7 @@ "x-immich-state": "Alpha" } }, - "/admin/maintenance/admin/maintenance/backups/list": { + "/admin/maintenance/backups/list": { "get": { "description": "Get the list of the successful and failed backups", "operationId": "listBackups", @@ -419,7 +419,7 @@ "x-immich-state": "Alpha" } }, - "/admin/maintenance/admin/maintenance/backups/{filename}": { + "/admin/maintenance/backups/{filename}": { "delete": { "description": "Delete a backup by its filename", "operationId": "deleteBackup", @@ -469,40 +469,6 @@ "x-immich-state": "Alpha" } }, - "/admin/maintenance/admin/maintenance/status": { - "get": { - "description": "Fetch information about the currently running maintenance action.", - "operationId": "maintenanceStatus", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MaintenanceStatusResponseDto" - } - } - }, - "description": "" - } - }, - "summary": "Get maintenance mode status", - "tags": [ - "Maintenance (admin)" - ], - "x-immich-history": [ - { - "version": "v9.9.9", - "state": "Added" - }, - { - "version": "v9.9.9", - "state": "Alpha" - } - ], - "x-immich-state": "Alpha" - } - }, "/admin/maintenance/login": { "post": { "description": "Login with maintenance token or cookie to receive current information and perform further actions.", @@ -547,6 +513,40 @@ "x-immich-state": "Alpha" } }, + "/admin/maintenance/status": { + "get": { + "description": "Fetch information about the currently running maintenance action.", + "operationId": "maintenanceStatus", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MaintenanceStatusResponseDto" + } + } + }, + "description": "" + } + }, + "summary": "Get maintenance mode status", + "tags": [ + "Maintenance (admin)" + ], + "x-immich-history": [ + { + "version": "v9.9.9", + "state": "Added" + }, + { + "version": "v9.9.9", + "state": "Alpha" + } + ], + "x-immich-state": "Alpha" + } + }, "/admin/notifications": { "post": { "description": "Create a new notification for a specific user.", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index fc0d379f23..609ae150c8 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -48,18 +48,18 @@ export type MaintenanceListBackupsResponseDto = { backups: string[]; failedBackups: string[]; }; -export type MaintenanceStatusResponseDto = { - action: MaintenanceAction; - error?: string; - progress?: number; - task?: string; -}; export type MaintenanceLoginDto = { token?: string; }; export type MaintenanceAuthDto = { username: string; }; +export type MaintenanceStatusResponseDto = { + action: MaintenanceAction; + error?: string; + progress?: number; + task?: string; +}; export type NotificationCreateDto = { data?: object; description?: string | null; @@ -1862,7 +1862,7 @@ export function listBackups(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: MaintenanceListBackupsResponseDto; - }>("/admin/maintenance/admin/maintenance/backups/list", { + }>("/admin/maintenance/backups/list", { ...opts })); } @@ -1872,22 +1872,11 @@ export function listBackups(opts?: Oazapfts.RequestOpts) { export function deleteBackup({ filename }: { filename: string; }, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText(`/admin/maintenance/admin/maintenance/backups/${encodeURIComponent(filename)}`, { + return oazapfts.ok(oazapfts.fetchText(`/admin/maintenance/backups/${encodeURIComponent(filename)}`, { ...opts, method: "DELETE" })); } -/** - * Get maintenance mode status - */ -export function maintenanceStatus(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: MaintenanceStatusResponseDto; - }>("/admin/maintenance/admin/maintenance/status", { - ...opts - })); -} /** * Log into maintenance mode */ @@ -1903,6 +1892,17 @@ export function maintenanceLogin({ maintenanceLoginDto }: { body: maintenanceLoginDto }))); } +/** + * Get maintenance mode status + */ +export function maintenanceStatus(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MaintenanceStatusResponseDto; + }>("/admin/maintenance/status", { + ...opts + })); +} /** * Create a notification */ diff --git a/server/src/controllers/maintenance.controller.ts b/server/src/controllers/maintenance.controller.ts index c4b1724529..83a79abc6b 100644 --- a/server/src/controllers/maintenance.controller.ts +++ b/server/src/controllers/maintenance.controller.ts @@ -22,7 +22,7 @@ import { FilenameParamDto } from 'src/validation'; export class MaintenanceController { constructor(private service: MaintenanceService) {} - @Get('admin/maintenance/status') + @Get('status') @Endpoint({ summary: 'Get maintenance mode status', description: 'Fetch information about the currently running maintenance action.', @@ -66,7 +66,7 @@ export class MaintenanceController { } } - @Get('admin/maintenance/backups/list') + @Get('backups/list') @Endpoint({ summary: 'List backups', description: 'Get the list of the successful and failed backups', @@ -77,7 +77,7 @@ export class MaintenanceController { return this.service.listBackups(); } - @Delete('admin/maintenance/backups/:filename') + @Delete('backups/:filename') @Endpoint({ summary: 'Delete backup', description: 'Delete a backup by its filename', diff --git a/server/src/maintenance/maintenance-worker.service.spec.ts b/server/src/maintenance/maintenance-worker.service.spec.ts index 8bac07eeec..400c08509d 100644 --- a/server/src/maintenance/maintenance-worker.service.spec.ts +++ b/server/src/maintenance/maintenance-worker.service.spec.ts @@ -245,6 +245,7 @@ describe(MaintenanceWorkerService.name, () => { expect(maintenanceEphemeralStateRepositoryMock.setStatus).toHaveBeenCalledWith({ action: MaintenanceAction.RestoreDatabase, + task: 'ready', progress: expect.any(Number), }); diff --git a/server/src/maintenance/maintenance-worker.service.ts b/server/src/maintenance/maintenance-worker.service.ts index abcdcea33b..f94fd4a510 100644 --- a/server/src/maintenance/maintenance-worker.service.ts +++ b/server/src/maintenance/maintenance-worker.service.ts @@ -250,6 +250,7 @@ export class MaintenanceWorkerService { private async restoreBackup(filename: string): Promise { this.setStatus({ action: MaintenanceAction.RestoreDatabase, + task: 'ready', progress: 0, }); diff --git a/web/src/lib/components/maintenance/MaintenanceBackupsList.svelte b/web/src/lib/components/maintenance/MaintenanceBackupsList.svelte new file mode 100644 index 0000000000..2e553f0c28 --- /dev/null +++ b/web/src/lib/components/maintenance/MaintenanceBackupsList.svelte @@ -0,0 +1,120 @@ + + + + {#each backups as backup (backup.filename)} + + + + + {backup.filename} + {#if typeof backup.hoursAgo === 'number'} + {#if backup.hoursAgo <= 24} + Created {backup.hoursAgo} hours ago + {:else if backup.hoursAgo <= 48} + Created 1 day ago + {:else} + Created {Math.floor(backup.hoursAgo / 24)} days ago + {/if} + {/if} + + + {#if props.showDelete} + + {/if} + + + + {/each} + diff --git a/web/src/lib/sidebars/AdminSidebar.svelte b/web/src/lib/sidebars/AdminSidebar.svelte index 2fecaebf49..4a107dcde7 100644 --- a/web/src/lib/sidebars/AdminSidebar.svelte +++ b/web/src/lib/sidebars/AdminSidebar.svelte @@ -11,6 +11,7 @@ + diff --git a/web/src/lib/stores/maintenance.store.ts b/web/src/lib/stores/maintenance.store.ts index 29d11038ed..ff0f770d49 100644 --- a/web/src/lib/stores/maintenance.store.ts +++ b/web/src/lib/stores/maintenance.store.ts @@ -3,5 +3,5 @@ import { writable } from 'svelte/store'; export const maintenanceStore = { auth: writable(), - status: writable(), + status: writable(), }; diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index b936ce36ae..35ab31674f 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -72,16 +72,17 @@ afterNavigate(() => { showNavigationLoadingBar = false; }); + + const { release, serverRestarting } = websocketStore; + run(() => { - if ($user || page.url.pathname.startsWith(AppRoute.MAINTENANCE)) { + if ($user || $serverRestarting || page.url.pathname.startsWith(AppRoute.MAINTENANCE)) { openWebsocketConnection(); } else { closeWebsocketConnection(); } }); - const { release, serverRestarting } = websocketStore; - const handleRelease = async (release?: ReleaseEvent) => { if (!release?.isAvailable || !$user.isAdmin) { return; diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index a414702cfd..34e8f0ad31 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -1,17 +1,39 @@
{$t('welcome_to_immich')} -
+ -
+ +
diff --git a/web/src/routes/admin/maintenance/+page.svelte b/web/src/routes/admin/maintenance/+page.svelte new file mode 100644 index 0000000000..6e441176cb --- /dev/null +++ b/web/src/routes/admin/maintenance/+page.svelte @@ -0,0 +1,62 @@ + + + + {#snippet buttons()} + + + + {/snippet} + +
+
+ + + + + +
+
+
diff --git a/web/src/routes/admin/maintenance/+page.ts b/web/src/routes/admin/maintenance/+page.ts new file mode 100644 index 0000000000..38aa69aa37 --- /dev/null +++ b/web/src/routes/admin/maintenance/+page.ts @@ -0,0 +1,17 @@ +import { authenticate } from '$lib/utils/auth'; +import { getFormatter } from '$lib/utils/i18n'; +import { listBackups } from '@immich/sdk'; +import type { PageLoad } from './$types'; + +export const load = (async ({ url }) => { + await authenticate(url, { admin: true }); + const { backups } = await listBackups(); + const $t = await getFormatter(); + + return { + backups, + meta: { + title: $t('admin.maintenance_settings'), + }, + }; +}) satisfies PageLoad; diff --git a/web/src/routes/maintenance/+page.svelte b/web/src/routes/maintenance/+page.svelte index 7328342b1a..ec54bb5181 100644 --- a/web/src/routes/maintenance/+page.svelte +++ b/web/src/routes/maintenance/+page.svelte @@ -1,13 +1,14 @@
- {$t('maintenance_title')} -

- - {#snippet children({ tag, message })} - {#if tag === 'link'} - - {message} - + {#if $status?.action === MaintenanceAction.RestoreDatabase} + {#if $status.task} + Restoring Database + {#if $status.error} + +

{error}
+ + {:else} +
+
+
+ {#if $status.task !== 'ready'} + {$t(`maintenance_task_${$status.task as 'backup' | 'restore'}`)} {/if} - {/snippet} - -

- {#if $auth} + {/if} + {:else} + Restore From Backup + + + + + {/if} + {:else} + {$t('maintenance_title')}

- {$t('maintenance_logged_in_as', { - values: { - user: $auth.username, - }, - })} + + {#snippet children({ tag, message })} + {#if tag === 'link'} + + {message} + + {/if} + {/snippet} +

+ {#if $auth} +

+ {$t('maintenance_logged_in_as', { + values: { + user: $auth.username, + }, + })} +

+ {/if} + {/if} + {#if $auth && ($status?.action === MaintenanceAction.Start || $status?.error)} {/if}