diff --git a/app/client/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts index 342f4a36..aa4dd6e1 100644 --- a/app/client/api-client/@tanstack/react-query.gen.ts +++ b/app/client/api-client/@tanstack/react-query.gen.ts @@ -5,6 +5,7 @@ import { type DefaultError, queryOptions, type UseMutationOptions } from "@tanst import { client } from "../client.gen"; import { browseFilesystem, + cancelDoctor, createBackupSchedule, createNotificationDestination, createRepository, @@ -15,7 +16,6 @@ import { deleteSnapshot, deleteSnapshots, deleteVolume, - doctorRepository, downloadResticPassword, getBackupSchedule, getBackupScheduleForVolume, @@ -44,6 +44,7 @@ import { restoreSnapshot, runBackupNow, runForget, + startDoctor, stopBackup, tagSnapshots, testConnection, @@ -59,6 +60,8 @@ import { import type { BrowseFilesystemData, BrowseFilesystemResponse, + CancelDoctorData, + CancelDoctorResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, @@ -79,8 +82,6 @@ import type { DeleteSnapshotsResponse, DeleteVolumeData, DeleteVolumeResponse, - DoctorRepositoryData, - DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, GetBackupScheduleData, @@ -135,6 +136,8 @@ import type { RunBackupNowResponse, RunForgetData, RunForgetResponse, + StartDoctorData, + StartDoctorResponse, StopBackupData, StopBackupResponse, TagSnapshotsData, @@ -685,14 +688,33 @@ export const restoreSnapshotMutation = ( }; /** - * Run doctor operations on a repository to fix common issues (unlock, check, repair index). Use this when the repository is locked or has errors. + * Cancel a running doctor operation on a repository */ -export const doctorRepositoryMutation = ( - options?: Partial>, -): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { +export const cancelDoctorMutation = ( + options?: Partial>, +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { - const { data } = await doctorRepository({ + const { data } = await cancelDoctor({ + ...options, + ...fnOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + +/** + * Start an asynchronous doctor operation on a repository to fix common issues (unlock, check, repair index). The operation runs in the background and sends results via SSE events. + */ +export const startDoctorMutation = ( + options?: Partial>, +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await startDoctor({ ...options, ...fnOptions, throwOnError: true, diff --git a/app/client/api-client/index.ts b/app/client/api-client/index.ts index bee3869e..5404543d 100644 --- a/app/client/api-client/index.ts +++ b/app/client/api-client/index.ts @@ -2,6 +2,7 @@ export { browseFilesystem, + cancelDoctor, createBackupSchedule, createNotificationDestination, createRepository, @@ -12,7 +13,6 @@ export { deleteSnapshot, deleteSnapshots, deleteVolume, - doctorRepository, downloadResticPassword, getBackupSchedule, getBackupScheduleForVolume, @@ -41,6 +41,7 @@ export { restoreSnapshot, runBackupNow, runForget, + startDoctor, stopBackup, tagSnapshots, testConnection, @@ -57,6 +58,10 @@ export type { BrowseFilesystemData, BrowseFilesystemResponse, BrowseFilesystemResponses, + CancelDoctorData, + CancelDoctorErrors, + CancelDoctorResponse, + CancelDoctorResponses, ClientOptions, CreateBackupScheduleData, CreateBackupScheduleResponse, @@ -89,9 +94,6 @@ export type { DeleteVolumeData, DeleteVolumeResponse, DeleteVolumeResponses, - DoctorRepositoryData, - DoctorRepositoryResponse, - DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponse, DownloadResticPasswordResponses, @@ -176,6 +178,10 @@ export type { RunForgetData, RunForgetResponse, RunForgetResponses, + StartDoctorData, + StartDoctorErrors, + StartDoctorResponse, + StartDoctorResponses, StopBackupData, StopBackupErrors, StopBackupResponse, diff --git a/app/client/api-client/sdk.gen.ts b/app/client/api-client/sdk.gen.ts index 835080bd..72a1022b 100644 --- a/app/client/api-client/sdk.gen.ts +++ b/app/client/api-client/sdk.gen.ts @@ -5,6 +5,9 @@ import { client } from "./client.gen"; import type { BrowseFilesystemData, BrowseFilesystemResponses, + CancelDoctorData, + CancelDoctorErrors, + CancelDoctorResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, @@ -26,8 +29,6 @@ import type { DeleteSnapshotsResponses, DeleteVolumeData, DeleteVolumeResponses, - DoctorRepositoryData, - DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, GetBackupScheduleData, @@ -85,6 +86,9 @@ import type { RunBackupNowResponses, RunForgetData, RunForgetResponses, + StartDoctorData, + StartDoctorErrors, + StartDoctorResponses, StopBackupData, StopBackupErrors, StopBackupResponses, @@ -405,12 +409,19 @@ export const restoreSnapshot = ( }); /** - * Run doctor operations on a repository to fix common issues (unlock, check, repair index). Use this when the repository is locked or has errors. + * Cancel a running doctor operation on a repository */ -export const doctorRepository = ( - options: Options, -) => - (options.client ?? client).post({ +export const cancelDoctor = (options: Options) => + (options.client ?? client).delete({ + url: "/api/v1/repositories/{id}/doctor", + ...options, + }); + +/** + * Start an asynchronous doctor operation on a repository to fix common issues (unlock, check, repair index). The operation runs in the background and sends results via SSE events. + */ +export const startDoctor = (options: Options) => + (options.client ?? client).post({ url: "/api/v1/repositories/{id}/doctor", ...options, }); diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 3cab252d..c2e39876 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -725,8 +725,18 @@ export type ListRepositoriesResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -736,8 +746,18 @@ export type ListRepositoriesResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -746,9 +766,19 @@ export type ListRepositoriesResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -757,17 +787,37 @@ export type ListRepositoriesResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -775,18 +825,38 @@ export type ListRepositoriesResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -799,17 +869,37 @@ export type ListRepositoriesResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; id: string; lastChecked: number | null; lastError: string | null; name: string; shortId: string; - status: "error" | "healthy" | "unknown" | null; + status: "doctor" | "error" | "healthy" | "unknown" | null; type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; updatedAt: number; }>; @@ -828,8 +918,18 @@ export type CreateRepositoryData = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -839,8 +939,18 @@ export type CreateRepositoryData = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -849,9 +959,19 @@ export type CreateRepositoryData = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -860,17 +980,37 @@ export type CreateRepositoryData = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -878,18 +1018,38 @@ export type CreateRepositoryData = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -902,9 +1062,19 @@ export type CreateRepositoryData = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; name: string; compressionMode?: "auto" | "max" | "off"; @@ -993,8 +1163,18 @@ export type GetRepositoryResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -1004,8 +1184,18 @@ export type GetRepositoryResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -1014,9 +1204,19 @@ export type GetRepositoryResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -1025,17 +1225,37 @@ export type GetRepositoryResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -1043,18 +1263,38 @@ export type GetRepositoryResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -1067,17 +1307,37 @@ export type GetRepositoryResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; id: string; lastChecked: number | null; lastError: string | null; name: string; shortId: string; - status: "error" | "healthy" | "unknown" | null; + status: "doctor" | "error" | "healthy" | "unknown" | null; type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; updatedAt: number; }; @@ -1123,8 +1383,18 @@ export type UpdateRepositoryResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -1134,8 +1404,18 @@ export type UpdateRepositoryResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -1144,9 +1424,19 @@ export type UpdateRepositoryResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -1155,17 +1445,37 @@ export type UpdateRepositoryResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -1173,18 +1483,38 @@ export type UpdateRepositoryResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -1197,17 +1527,37 @@ export type UpdateRepositoryResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; id: string; lastChecked: number | null; lastError: string | null; name: string; shortId: string; - status: "error" | "healthy" | "unknown" | null; + status: "doctor" | "error" | "healthy" | "unknown" | null; type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; updatedAt: number; }; @@ -1383,7 +1733,7 @@ export type RestoreSnapshotResponses = { export type RestoreSnapshotResponse = RestoreSnapshotResponses[keyof RestoreSnapshotResponses]; -export type DoctorRepositoryData = { +export type CancelDoctorData = { body?: never; path: { id: string; @@ -1392,22 +1742,51 @@ export type DoctorRepositoryData = { url: "/api/v1/repositories/{id}/doctor"; }; -export type DoctorRepositoryResponses = { +export type CancelDoctorErrors = { /** - * Doctor operation completed + * No doctor operation is currently running + */ + 409: unknown; +}; + +export type CancelDoctorResponses = { + /** + * Doctor operation cancelled */ 200: { - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; + message: string; }; }; -export type DoctorRepositoryResponse = DoctorRepositoryResponses[keyof DoctorRepositoryResponses]; +export type CancelDoctorResponse = CancelDoctorResponses[keyof CancelDoctorResponses]; + +export type StartDoctorData = { + body?: never; + path: { + id: string; + }; + query?: never; + url: "/api/v1/repositories/{id}/doctor"; +}; + +export type StartDoctorErrors = { + /** + * Doctor operation already in progress + */ + 409: unknown; +}; + +export type StartDoctorResponses = { + /** + * Doctor operation started + */ + 202: { + message: string; + repositoryId: string; + }; +}; + +export type StartDoctorResponse = StartDoctorResponses[keyof StartDoctorResponses]; export type TagSnapshotsData = { body?: { @@ -1470,8 +1849,18 @@ export type ListBackupSchedulesResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -1481,8 +1870,18 @@ export type ListBackupSchedulesResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -1491,9 +1890,19 @@ export type ListBackupSchedulesResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -1502,17 +1911,37 @@ export type ListBackupSchedulesResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -1520,18 +1949,38 @@ export type ListBackupSchedulesResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -1544,17 +1993,37 @@ export type ListBackupSchedulesResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; id: string; lastChecked: number | null; lastError: string | null; name: string; shortId: string; - status: "error" | "healthy" | "unknown" | null; + status: "doctor" | "error" | "healthy" | "unknown" | null; type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; updatedAt: number; }; @@ -1762,8 +2231,18 @@ export type GetBackupScheduleResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -1773,8 +2252,18 @@ export type GetBackupScheduleResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -1783,9 +2272,19 @@ export type GetBackupScheduleResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -1794,17 +2293,37 @@ export type GetBackupScheduleResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -1812,18 +2331,38 @@ export type GetBackupScheduleResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -1836,17 +2375,37 @@ export type GetBackupScheduleResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; id: string; lastChecked: number | null; lastError: string | null; name: string; shortId: string; - status: "error" | "healthy" | "unknown" | null; + status: "doctor" | "error" | "healthy" | "unknown" | null; type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; updatedAt: number; }; @@ -2035,8 +2594,18 @@ export type GetBackupScheduleForVolumeResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -2046,8 +2615,18 @@ export type GetBackupScheduleForVolumeResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -2056,9 +2635,19 @@ export type GetBackupScheduleForVolumeResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -2067,17 +2656,37 @@ export type GetBackupScheduleForVolumeResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -2085,18 +2694,38 @@ export type GetBackupScheduleForVolumeResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -2109,17 +2738,37 @@ export type GetBackupScheduleForVolumeResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; id: string; lastChecked: number | null; lastError: string | null; name: string; shortId: string; - status: "error" | "healthy" | "unknown" | null; + status: "doctor" | "error" | "healthy" | "unknown" | null; type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; updatedAt: number; }; @@ -2516,8 +3165,18 @@ export type GetScheduleMirrorsResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -2527,8 +3186,18 @@ export type GetScheduleMirrorsResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -2537,9 +3206,19 @@ export type GetScheduleMirrorsResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -2548,17 +3227,37 @@ export type GetScheduleMirrorsResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -2566,18 +3265,38 @@ export type GetScheduleMirrorsResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -2590,17 +3309,37 @@ export type GetScheduleMirrorsResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; id: string; lastChecked: number | null; lastError: string | null; name: string; shortId: string; - status: "error" | "healthy" | "unknown" | null; + status: "doctor" | "error" | "healthy" | "unknown" | null; type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; updatedAt: number; }; @@ -2646,8 +3385,18 @@ export type UpdateScheduleMirrorsResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -2657,8 +3406,18 @@ export type UpdateScheduleMirrorsResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -2667,9 +3426,19 @@ export type UpdateScheduleMirrorsResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -2678,17 +3447,37 @@ export type UpdateScheduleMirrorsResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -2696,18 +3485,38 @@ export type UpdateScheduleMirrorsResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -2720,17 +3529,37 @@ export type UpdateScheduleMirrorsResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; + doctorResult: { + completedAt: number; + steps: Array<{ + error: string | null; + output: string | null; + step: string; + success: boolean; + }>; + success: boolean; + } | null; id: string; lastChecked: number | null; lastError: string | null; name: string; shortId: string; - status: "error" | "healthy" | "unknown" | null; + status: "doctor" | "error" | "healthy" | "unknown" | null; type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; updatedAt: number; }; diff --git a/app/client/hooks/use-server-events.ts b/app/client/hooks/use-server-events.ts index fe3fc4bf..dd041398 100644 --- a/app/client/hooks/use-server-events.ts +++ b/app/client/hooks/use-server-events.ts @@ -11,7 +11,10 @@ type ServerEventType = | "volume:unmounted" | "volume:updated" | "mirror:started" - | "mirror:completed"; + | "mirror:completed" + | "doctor:started" + | "doctor:completed" + | "doctor:cancelled"; export interface BackupEvent { scheduleId: number; @@ -45,6 +48,21 @@ export interface MirrorEvent { error?: string; } +export interface DoctorEvent { + repositoryId: string; + repositoryName: string; +} + +export interface DoctorCompletedEvent extends DoctorEvent { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; +} + type EventHandler = (data: unknown) => void; /** @@ -156,6 +174,39 @@ export function useServerEvents() { }); }); + eventSource.addEventListener("doctor:started", (e) => { + const data = JSON.parse(e.data) as DoctorEvent; + console.info("[SSE] Doctor started:", data); + + void queryClient.invalidateQueries(); + + handlersRef.current.get("doctor:started")?.forEach((handler) => { + handler(data); + }); + }); + + eventSource.addEventListener("doctor:completed", (e) => { + const data = JSON.parse(e.data) as DoctorCompletedEvent; + console.info("[SSE] Doctor completed:", data); + + void queryClient.invalidateQueries(); + + handlersRef.current.get("doctor:completed")?.forEach((handler) => { + handler(data); + }); + }); + + eventSource.addEventListener("doctor:cancelled", (e) => { + const data = JSON.parse(e.data) as DoctorEvent; + console.info("[SSE] Doctor cancelled:", data); + + void queryClient.invalidateQueries(); + + handlersRef.current.get("doctor:cancelled")?.forEach((handler) => { + handler(data); + }); + }); + eventSource.onerror = (error) => { console.error("[SSE] Connection error:", error); }; diff --git a/app/client/modules/repositories/components/doctor-report.tsx b/app/client/modules/repositories/components/doctor-report.tsx new file mode 100644 index 00000000..66bd3ed4 --- /dev/null +++ b/app/client/modules/repositories/components/doctor-report.tsx @@ -0,0 +1,88 @@ +import { AlertCircle, CheckCircle2 } from "lucide-react"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "~/client/components/ui/collapsible"; +import { formatDateTime } from "~/client/lib/datetime"; +import { cn } from "~/client/lib/utils"; + +type DoctorStep = { + step: string; + success: boolean; + output: string | null; + error: string | null; +}; + +type DoctorResult = { + success: boolean; + steps: DoctorStep[]; + completedAt: number; +}; +export const DoctorReport = ({ + result, + repositoryStatus, +}: { + result?: DoctorResult | null; + repositoryStatus: string | null; +}) => { + return ( +
+

Doctor Report

+ {result && ( +
+ Completed {formatDateTime(result.completedAt)} +
+ {result.steps.map((step) => ( + + +
+ {step.step.replace("_", " ")} + {step.success ? ( + + ) : ( + + )} +
+
+ +
+ {step.output && ( +
+												{step.output.startsWith("{") ? JSON.stringify(JSON.parse(step.output), null, 2) : step.output}
+											
+ )} + {step.error && ( +
+
Error
+
+													{step.error}
+												
+
+ )} + {!step.output && !step.error && ( +
No output recorded
+ )} +
+
+
+ ))} +
+
+ )} +
+

No doctor report available.

+
+
+
+
+

Doctor operation is currently running...

+
+
+
+ ); +}; diff --git a/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx b/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx index e4512fcd..066997f2 100644 --- a/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx +++ b/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx @@ -29,7 +29,7 @@ export const AdvancedForm = ({ form }: Props) => { return ( - Advanced Settings + Advanced Settings
diff --git a/app/client/modules/repositories/routes/repository-details.tsx b/app/client/modules/repositories/routes/repository-details.tsx index 53f61a77..482f5d63 100644 --- a/app/client/modules/repositories/routes/repository-details.tsx +++ b/app/client/modules/repositories/routes/repository-details.tsx @@ -3,10 +3,11 @@ import { redirect, useNavigate, useSearchParams } from "react-router"; import { toast } from "sonner"; import { useState, useEffect } from "react"; import { + cancelDoctorMutation, deleteRepositoryMutation, - doctorRepositoryMutation, getRepositoryOptions, listSnapshotsOptions, + startDoctorMutation, } from "~/client/api-client/@tanstack/react-query.gen"; import { Button } from "~/client/components/ui/button"; import { @@ -25,7 +26,7 @@ import { cn } from "~/client/lib/utils"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs"; import { RepositoryInfoTabContent } from "../tabs/info"; import { RepositorySnapshotsTabContent } from "../tabs/snapshots"; -import { Loader2, Stethoscope, Trash2 } from "lucide-react"; +import { Square, Stethoscope, Trash2 } from "lucide-react"; export const handle = { breadcrumb: (match: Route.MetaArgs) => [ @@ -52,8 +53,6 @@ export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => { }; export default function RepositoryDetailsPage({ loaderData }: Route.ComponentProps) { - const [showDoctorResults, setShowDoctorResults] = useState(false); - const navigate = useNavigate(); const queryClient = useQueryClient(); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); @@ -83,24 +82,22 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro }, }); - const doctorMutation = useMutation({ - ...doctorRepositoryMutation(), - onSuccess: (data) => { - if (data) { - setShowDoctorResults(true); + const startDoctor = useMutation({ + ...startDoctorMutation(), + onError: (error) => { + toast.error("Failed to start doctor", { + description: parseError(error)?.message, + }); + }, + }); - if (data.success) { - toast.success("Repository doctor completed successfully"); - } else { - toast.warning("Doctor completed with some issues", { - description: "Check the details for more information", - richColors: true, - }); - } - } + const cancelDoctor = useMutation({ + ...cancelDoctorMutation(), + onSuccess: () => { + toast.info("Doctor operation cancelled"); }, onError: (error) => { - toast.error("Failed to run doctor", { + toast.error("Failed to cancel doctor", { description: parseError(error)?.message, }); }, @@ -111,21 +108,6 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro deleteRepo.mutate({ path: { id: data.id } }); }; - const getStepLabel = (step: string) => { - switch (step) { - case "unlock": - return "Unlock Repository"; - case "check": - return "Check Repository"; - case "repair_index": - return "Repair Index"; - case "recheck": - return "Re-check Repository"; - default: - return step; - } - }; - return ( <>
@@ -134,6 +116,7 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro className={cn("inline-flex items-center gap-2 px-2 py-1 rounded-md text-xs bg-gray-500/10 text-gray-500", { "bg-green-500/10 text-green-500": data.status === "healthy", "bg-red-500/10 text-red-500": data.status === "error", + "bg-blue-500/10 text-blue-500": data.status === "doctor", })} > {data.status || "unknown"} @@ -141,23 +124,17 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro {data.type}
- + {data.status === "doctor" ? ( + + ) : ( + + )}
- - - - - Doctor results - Repository doctor operation completed - - - {doctorMutation.data && ( -
- {doctorMutation.data.steps.map((step) => ( -
-
- {getStepLabel(step.step)} - - {step.success ? "Success" : "Warning"} - -
- {step.error &&

{step.error}

} -
- ))} -
- )} - -
- -
-
-
); } diff --git a/app/client/modules/repositories/tabs/info.tsx b/app/client/modules/repositories/tabs/info.tsx index 6b63330d..75d02954 100644 --- a/app/client/modules/repositories/tabs/info.tsx +++ b/app/client/modules/repositories/tabs/info.tsx @@ -22,6 +22,7 @@ import { REPOSITORY_BASE } from "~/client/lib/constants"; import { formatDateTime, formatTimeAgo } from "~/client/lib/datetime"; import { updateRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen"; import type { CompressionMode, RepositoryConfig } from "~/schemas/restic"; +import { DoctorReport } from "../components/doctor-report"; type Props = { repository: Repository; @@ -126,7 +127,7 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
{effectiveLocalPath && (
-
Effective Local Path
+
Local path

{effectiveLocalPath}

)} @@ -179,6 +180,8 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
+ +