diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 4b02bb57..9c7721a9 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -17,20 +17,20 @@ jobs: uses: actions/checkout@v6 - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 with: driver: cloud endpoint: "meienberger/runtipi-builder" install: true - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -38,14 +38,14 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ghcr.io/${{ github.repository_owner }}/zerobyte tags: | type=raw,value=nightly - name: Push images to GitHub Container Registry - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . target: production diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d63283b2..612e3a91 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,27 +49,27 @@ jobs: ref: ${{ github.ref }} - name: Log in to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 with: driver: cloud endpoint: "meienberger/runtipi-builder" install: true - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build docker image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . target: production @@ -98,7 +98,7 @@ jobs: - name: Docker meta id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ghcr.io/${{ github.repository_owner }}/zerobyte tags: | @@ -110,7 +110,7 @@ jobs: latest=${{ needs.determine-release-type.outputs.release_type == 'release' }} - name: Push images to GitHub Container Registry - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . target: production diff --git a/.gitignore b/.gitignore index 8122a126..644f0800 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ openapi-ts-error-*.log tmp/ qa-output +.worktrees/ diff --git a/.oxlintrc.json b/.oxlintrc.json index ae1c8af2..0bb6f8ba 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -50,6 +50,7 @@ "no-unused-vars": [ "warn", { + "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_", "argsIgnorePattern": "^_" } @@ -114,6 +115,7 @@ "unicorn/prefer-set-size": "warn", "unicorn/prefer-string-starts-ends-with": "warn", "import/no-cycle": "error", + "import/no-unused-modules": ["error", { "unusedExports": true }], "eslint/no-console": ["warn", { "allow": ["warn", "error", "info"] }] }, "settings": { diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..6386db09 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.defaultFormatter": "oxc.oxc-vscode" +} diff --git a/Dockerfile b/Dockerfile index 46b8ba01..b99f1957 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ ENV VITE_RESTIC_VERSION=${RESTIC_VERSION} \ RUN apk update --no-cache && \ apk upgrade --no-cache && \ - apk add --no-cache davfs2=1.6.1-r2 openssh-client fuse3 sshfs tini + apk add --no-cache davfs2=1.6.1-r2 openssh-client fuse3 sshfs tini tzdata ENTRYPOINT ["/sbin/tini", "-s", "--"] diff --git a/README.md b/README.md index e90106b8..5f652a48 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ In order to run Zerobyte, you need to have Docker and Docker Compose installed o ```yaml services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.29 + image: ghcr.io/nicotsx/zerobyte:v0.30 container_name: zerobyte restart: unless-stopped cap_add: @@ -50,7 +50,7 @@ services: devices: - /dev/fuse:/dev/fuse environment: - - TZ=Europe/Paris # Set your timezone here + - TZ=Europe/Zurich # Set your timezone here - BASE_URL=http://localhost:4096 # URL you will use to access Zerobyte - APP_SECRET=94bad46...c66e25d5c2b # Generate your own secret with `openssl rand -hex 32` volumes: @@ -95,7 +95,7 @@ Zerobyte can be customized using environment variables. Below are the available | `APP_SECRET` | **Required.** A random secret key (32+ chars) used to encrypt sensitive data in the database. Generate with `openssl rand -hex 32`. | (none) | | `PORT` | The port the web interface and API will listen on. | `4096` | | `RESTIC_HOSTNAME` | The hostname used by Restic when creating snapshots. Automatically detected if a custom hostname is set in Docker. | `zerobyte` | -| `TZ` | Timezone for the container (e.g., `Europe/Paris`). **Crucial for accurate backup scheduling.** | `UTC` | +| `TZ` | Timezone for the container (e.g., `Europe/Zurich`). **Crucial for accurate backup scheduling.** | `UTC` | | `TRUSTED_ORIGINS` | Comma-separated list of extra trusted origins for CORS (e.g., `http://localhost:3000,http://example.com`). | (none) | | `LOG_LEVEL` | Logging verbosity. Options: `debug`, `info`, `warn`, `error`. | `info` | | `SERVER_IDLE_TIMEOUT` | Idle timeout for the server in seconds. | `60` | @@ -118,13 +118,13 @@ If you only need to back up locally mounted folders and don't require remote sha ```yaml services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.29 + image: ghcr.io/nicotsx/zerobyte:v0.30 container_name: zerobyte restart: unless-stopped ports: - "4096:4096" environment: - - TZ=Europe/Paris # Set your timezone here + - TZ=Europe/Zurich # Set your timezone here - BASE_URL=http://localhost:4096 # Change this to your actual URL (use https:// for secure cookies) - APP_SECRET=94bad46...c66e25d5c2b # Generate your own secret with `openssl rand -hex 32` volumes: @@ -157,7 +157,7 @@ If you want to track a local directory on the same server where Zerobyte is runn ```diff services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.29 + image: ghcr.io/nicotsx/zerobyte:v0.30 container_name: zerobyte restart: unless-stopped cap_add: @@ -167,7 +167,7 @@ services: devices: - /dev/fuse:/dev/fuse environment: - - TZ=Europe/Paris + - TZ=Europe/Zurich - BASE_URL=http://localhost:4096 # URL you will use to access Zerobyte - APP_SECRET=94bad46...c66e25d5c2b # Generate your own secret with `openssl rand -hex 32` volumes: @@ -232,7 +232,7 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov ```diff services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.29 + image: ghcr.io/nicotsx/zerobyte:v0.30 container_name: zerobyte restart: unless-stopped cap_add: @@ -242,7 +242,7 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov devices: - /dev/fuse:/dev/fuse environment: - - TZ=Europe/Paris + - TZ=Europe/Zurich - BASE_URL=http://localhost:4096 # URL you will use to access Zerobyte volumes: - /etc/localtime:/etc/localtime:ro diff --git a/app/app.css b/app/app.css index 15a1e65c..19842c5a 100644 --- a/app/app.css +++ b/app/app.css @@ -37,7 +37,6 @@ body { } body { - @apply bg-[#131313]; min-height: 100dvh; } @@ -67,6 +66,8 @@ body { --color-accent: var(--accent); --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-success: var(--success); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); @@ -88,38 +89,40 @@ body { :root { --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --card-header: oklch(0.922 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); + --background: oklch(0.98 0.002 250); + --foreground: oklch(0.20 0.01 260); + --card: #ffffff; + --card-foreground: oklch(0.20 0.01 260); + --card-header: oklch(0.99 0.001 260); + --popover: #ffffff; + --popover-foreground: oklch(0.20 0.01 260); + --primary: oklch(0.20 0.01 260); + --primary-foreground: #ffffff; + --secondary: oklch(0.96 0.005 260); + --secondary-foreground: oklch(0.20 0.01 260); + --muted: oklch(0.95 0.005 260); + --muted-foreground: oklch(0.45 0.01 260); + --accent: oklch(0.96 0.005 260); + --accent-foreground: oklch(0.20 0.01 260); --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --destructive-foreground: #fff; + --success: oklch(0.57 0.16 148); + --border: oklch(0.90 0.01 260); + --input: oklch(0.90 0.01 260); + --ring: oklch(0.7 0.01 260); + --chart-1: oklch(0.55 0.22 260); + --chart-2: oklch(0.6 0.15 200); + --chart-3: oklch(0.45 0.12 280); + --chart-4: oklch(0.7 0.15 190); + --chart-5: oklch(0.55 0.18 300); + --sidebar: #ffffff; + --sidebar-foreground: oklch(0.35 0.01 260); + --sidebar-primary: oklch(0.20 0.01 260); + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: oklch(0.97 0.005 260); + --sidebar-accent-foreground: oklch(0.20 0.01 260); + --sidebar-border: oklch(0.90 0.01 260); + --sidebar-ring: oklch(0.7 0.01 260); --strong-accent: #ff543a; } @@ -144,6 +147,8 @@ body { --accent: oklch(0.269 0 0); --accent-foreground: oklch(0.985 0 0); --destructive: #ff543a; + --destructive-foreground: #fff; + --success: oklch(0.696 0.17 162.48); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.556 0 0); @@ -174,11 +179,11 @@ body { @layer base { :root { - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); + --chart-1: oklch(0.55 0.22 260); + --chart-2: oklch(0.6 0.15 200); + --chart-3: oklch(0.45 0.12 280); + --chart-4: oklch(0.7 0.15 190); + --chart-5: oklch(0.55 0.18 300); } .dark { @@ -190,6 +195,17 @@ body { } } +.snapshot-scrollable::-webkit-scrollbar { + height: 6px; +} +.snapshot-scrollable::-webkit-scrollbar-track { + background: transparent; +} +.snapshot-scrollable::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 999px; +} + /* Hide built-in password reveal/clear controls (notably in Edge on Windows) */ [data-secret-input] input[type="password"]::-ms-reveal, [data-secret-input] input[type="password"]::-ms-clear { diff --git a/app/client/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts index 182d913b..d6510ffe 100644 --- a/app/client/api-client/@tanstack/react-query.gen.ts +++ b/app/client/api-client/@tanstack/react-query.gen.ts @@ -4,8 +4,8 @@ import { type DefaultError, type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query'; import { client } from '../client.gen'; -import { browseFilesystem, cancelDoctor, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteSsoInvitation, deleteSsoProvider, deleteUserAccount, deleteVolume, devPanelExec, downloadResticPassword, dumpSnapshot, getAdminUsers, getBackupProgress, getBackupSchedule, getBackupScheduleForVolume, getDevPanel, getMirrorCompatibility, getNotificationDestination, getOrgMembers, getPublicSsoProviders, getRegistrationStatus, getRepository, getRepositoryStats, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getSsoSettings, getStatus, getSystemInfo, getUpdates, getUserDeletionImpact, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, mountVolume, type Options, refreshSnapshots, removeOrgMember, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, setRegistrationStatus, startDoctor, stopBackup, tagSnapshots, testConnection, testNotificationDestination, unlockRepository, unmountVolume, updateBackupSchedule, updateMemberRole, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateSsoProviderAutoLinking, updateVolume } from '../sdk.gen'; -import type { BrowseFilesystemData, BrowseFilesystemResponse, CancelDoctorData, CancelDoctorResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteSsoInvitationData, DeleteSsoProviderData, DeleteUserAccountData, DeleteVolumeData, DeleteVolumeResponse, DevPanelExecData, DevPanelExecResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, DumpSnapshotData, DumpSnapshotResponse, GetAdminUsersData, GetAdminUsersResponse, GetBackupProgressData, GetBackupProgressResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetDevPanelData, GetDevPanelResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetOrgMembersData, GetOrgMembersResponse, GetPublicSsoProvidersData, GetPublicSsoProvidersResponse, GetRegistrationStatusData, GetRegistrationStatusResponse, GetRepositoryData, GetRepositoryResponse, GetRepositoryStatsData, GetRepositoryStatsResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetSsoSettingsData, GetSsoSettingsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetUpdatesData, GetUpdatesResponse, GetUserDeletionImpactData, GetUserDeletionImpactResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, MountVolumeData, MountVolumeResponse, RefreshSnapshotsData, RefreshSnapshotsResponse, RemoveOrgMemberData, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, SetRegistrationStatusData, SetRegistrationStatusResponse, StartDoctorData, StartDoctorResponse, StopBackupData, StopBackupResponse, TagSnapshotsData, TagSnapshotsResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnlockRepositoryData, UnlockRepositoryResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateMemberRoleData, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateSsoProviderAutoLinkingData, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen'; +import { browseFilesystem, cancelDoctor, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteSsoInvitation, deleteSsoProvider, deleteUserAccount, deleteVolume, downloadResticPassword, dumpSnapshot, getAdminUsers, getBackupProgress, getBackupSchedule, getBackupScheduleForVolume, getDevPanel, getMirrorCompatibility, getNotificationDestination, getOrgMembers, getPublicSsoProviders, getRegistrationStatus, getRepository, getRepositoryStats, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getSsoSettings, getStatus, getSystemInfo, getUpdates, getUserDeletionImpact, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, mountVolume, type Options, refreshRepositoryStats, refreshSnapshots, removeOrgMember, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, setRegistrationStatus, startDoctor, stopBackup, tagSnapshots, testConnection, testNotificationDestination, unlockRepository, unmountVolume, updateBackupSchedule, updateMemberRole, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateSsoProviderAutoLinking, updateVolume } from '../sdk.gen'; +import type { BrowseFilesystemData, BrowseFilesystemResponse, CancelDoctorData, CancelDoctorResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteSsoInvitationData, DeleteSsoProviderData, DeleteUserAccountData, DeleteVolumeData, DeleteVolumeResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, DumpSnapshotData, DumpSnapshotResponse, GetAdminUsersData, GetAdminUsersResponse, GetBackupProgressData, GetBackupProgressResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetDevPanelData, GetDevPanelResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetOrgMembersData, GetOrgMembersResponse, GetPublicSsoProvidersData, GetPublicSsoProvidersResponse, GetRegistrationStatusData, GetRegistrationStatusResponse, GetRepositoryData, GetRepositoryResponse, GetRepositoryStatsData, GetRepositoryStatsResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetSsoSettingsData, GetSsoSettingsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetUpdatesData, GetUpdatesResponse, GetUserDeletionImpactData, GetUserDeletionImpactResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, MountVolumeData, MountVolumeResponse, RefreshRepositoryStatsData, RefreshRepositoryStatsResponse, RefreshSnapshotsData, RefreshSnapshotsResponse, RemoveOrgMemberData, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, SetRegistrationStatusData, SetRegistrationStatusResponse, StartDoctorData, StartDoctorResponse, StopBackupData, StopBackupResponse, TagSnapshotsData, TagSnapshotsResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnlockRepositoryData, UnlockRepositoryResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateMemberRoleData, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateSsoProviderAutoLinkingData, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen'; export type QueryKey = [ Pick & { @@ -58,93 +58,6 @@ export const getStatusOptions = (options?: Options) => queryOptio queryKey: getStatusQueryKey(options) }); -export const getPublicSsoProvidersQueryKey = (options?: Options) => createQueryKey('getPublicSsoProviders', options); - -/** - * Get public SSO providers for the instance - */ -export const getPublicSsoProvidersOptions = (options?: Options) => queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getPublicSsoProviders({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }); - return data; - }, - queryKey: getPublicSsoProvidersQueryKey(options) -}); - -export const getSsoSettingsQueryKey = (options?: Options) => createQueryKey('getSsoSettings', options); - -/** - * Get SSO providers and invitations for the active organization - */ -export const getSsoSettingsOptions = (options?: Options) => queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getSsoSettings({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }); - return data; - }, - queryKey: getSsoSettingsQueryKey(options) -}); - -/** - * Delete an SSO provider - */ -export const deleteSsoProviderMutation = (options?: Partial>): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await deleteSsoProvider({ - ...options, - ...fnOptions, - throwOnError: true - }); - return data; - } - }; - return mutationOptions; -}; - -/** - * Update whether SSO sign-in can auto-link existing accounts by email - */ -export const updateSsoProviderAutoLinkingMutation = (options?: Partial>): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await updateSsoProviderAutoLinking({ - ...options, - ...fnOptions, - throwOnError: true - }); - return data; - } - }; - return mutationOptions; -}; - -/** - * Delete an SSO invitation - */ -export const deleteSsoInvitationMutation = (options?: Partial>): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await deleteSsoInvitation({ - ...options, - ...fnOptions, - throwOnError: true - }); - return data; - } - }; - return mutationOptions; -}; - export const getAdminUsersQueryKey = (options?: Options) => createQueryKey('getAdminUsers', options); /** @@ -250,6 +163,93 @@ export const removeOrgMemberMutation = (options?: Partial) => createQueryKey('getPublicSsoProviders', options); + +/** + * Get public SSO providers for the instance + */ +export const getPublicSsoProvidersOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getPublicSsoProviders({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getPublicSsoProvidersQueryKey(options) +}); + +export const getSsoSettingsQueryKey = (options?: Options) => createQueryKey('getSsoSettings', options); + +/** + * Get SSO providers and invitations for the active organization + */ +export const getSsoSettingsOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getSsoSettings({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getSsoSettingsQueryKey(options) +}); + +/** + * Delete an SSO provider + */ +export const deleteSsoProviderMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteSsoProvider({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Update whether SSO sign-in can auto-link existing accounts by email + */ +export const updateSsoProviderAutoLinkingMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await updateSsoProviderAutoLinking({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Delete an SSO invitation + */ +export const deleteSsoInvitationMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteSsoInvitation({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + export const listVolumesQueryKey = (options?: Options) => createQueryKey('listVolumes', options); /** @@ -457,7 +457,7 @@ export const listFilesInfiniteQueryKey = (options: Options): Quer /** * List files in a volume directory */ -export const listFilesInfiniteOptions = (options: Options) => infiniteQueryOptions, QueryKey>, string | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( +export const listFilesInfiniteOptions = (options: Options) => infiniteQueryOptions, QueryKey>, number | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( // @ts-ignore { queryFn: async ({ pageParam, queryKey, signal }) => { @@ -620,6 +620,23 @@ export const getRepositoryStatsOptions = (options: Options>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await refreshRepositoryStats({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + /** * Delete multiple snapshots from a repository */ @@ -730,7 +747,7 @@ export const listSnapshotFilesInfiniteQueryKey = (options: Options) => infiniteQueryOptions, QueryKey>, string | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( +export const listSnapshotFilesInfiniteOptions = (options: Options) => infiniteQueryOptions, QueryKey>, number | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( // @ts-ignore { queryFn: async ({ pageParam, queryKey, signal }) => { @@ -855,23 +872,6 @@ export const tagSnapshotsMutation = (options?: Partial return mutationOptions; }; -/** - * Execute a restic command against a repository (dev panel only) - */ -export const devPanelExecMutation = (options?: Partial>): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await devPanelExec({ - ...options, - ...fnOptions, - throwOnError: true - }); - return data; - } - }; - return mutationOptions; -}; - export const listBackupSchedulesQueryKey = (options?: Options) => createQueryKey('listBackupSchedules', options); /** diff --git a/app/client/api-client/client/client.gen.ts b/app/client/api-client/client/client.gen.ts index d9a078b1..7bddcbe1 100644 --- a/app/client/api-client/client/client.gen.ts +++ b/app/client/api-client/client/client.gen.ts @@ -38,7 +38,7 @@ export const createClient = (config: Config = {}): Client => { ...options, fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined, + serializedBody: undefined as string | undefined, }; if (opts.security) { @@ -53,7 +53,7 @@ export const createClient = (config: Config = {}): Client => { } if (opts.body !== undefined && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body); + opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; } // remove Content-Type header if body is empty to avoid sending invalid requests @@ -259,8 +259,10 @@ export const createClient = (config: Config = {}): Client => { }); }; + const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); + return { - buildUrl, + buildUrl: _buildUrl, connect: makeMethodFn('CONNECT'), delete: makeMethodFn('DELETE'), get: makeMethodFn('GET'), diff --git a/app/client/api-client/core/bodySerializer.gen.ts b/app/client/api-client/core/bodySerializer.gen.ts index bbfd8727..2c7177c5 100644 --- a/app/client/api-client/core/bodySerializer.gen.ts +++ b/app/client/api-client/core/bodySerializer.gen.ts @@ -5,7 +5,7 @@ import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerialize export type QuerySerializer = (query: Record) => string; -export type BodySerializer = (body: any) => any; +export type BodySerializer = (body: unknown) => unknown; type QuerySerializerOptionsObject = { allowReserved?: boolean; @@ -40,12 +40,10 @@ const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: }; export const formDataBodySerializer = { - bodySerializer: | Array>>( - body: T, - ): FormData => { + bodySerializer: (body: unknown): FormData => { const data = new FormData(); - Object.entries(body).forEach(([key, value]) => { + Object.entries(body as Record).forEach(([key, value]) => { if (value === undefined || value === null) { return; } @@ -61,15 +59,15 @@ export const formDataBodySerializer = { }; export const jsonBodySerializer = { - bodySerializer: (body: T): string => + bodySerializer: (body: unknown): string => JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), }; export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>(body: T): string => { + bodySerializer: (body: unknown): string => { const data = new URLSearchParams(); - Object.entries(body).forEach(([key, value]) => { + Object.entries(body as Record).forEach(([key, value]) => { if (value === undefined || value === null) { return; } diff --git a/app/client/api-client/core/params.gen.ts b/app/client/api-client/core/params.gen.ts index 5c5cc4a3..598f2b72 100644 --- a/app/client/api-client/core/params.gen.ts +++ b/app/client/api-client/core/params.gen.ts @@ -97,7 +97,7 @@ interface Params { const stripEmptySlots = (params: Params) => { for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Object.keys(value).length) { + if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { delete params[slot as Slot]; } } diff --git a/app/client/api-client/index.ts b/app/client/api-client/index.ts index cef3e578..58bbc410 100644 --- a/app/client/api-client/index.ts +++ b/app/client/api-client/index.ts @@ -52,6 +52,7 @@ export { listVolumes, mountVolume, type Options, + refreshRepositoryStats, refreshSnapshots, removeOrgMember, reorderBackupSchedules, @@ -230,6 +231,9 @@ export type { MountVolumeData, MountVolumeResponse, MountVolumeResponses, + RefreshRepositoryStatsData, + RefreshRepositoryStatsResponse, + RefreshRepositoryStatsResponses, RefreshSnapshotsData, RefreshSnapshotsResponse, RefreshSnapshotsResponses, diff --git a/app/client/api-client/sdk.gen.ts b/app/client/api-client/sdk.gen.ts index 3f2d16ee..b1c29713 100644 --- a/app/client/api-client/sdk.gen.ts +++ b/app/client/api-client/sdk.gen.ts @@ -3,7 +3,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { BrowseFilesystemData, BrowseFilesystemResponses, CancelDoctorData, CancelDoctorErrors, CancelDoctorResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteSsoInvitationData, DeleteSsoInvitationErrors, DeleteSsoInvitationResponses, DeleteSsoProviderData, DeleteSsoProviderErrors, DeleteSsoProviderResponses, DeleteUserAccountData, DeleteUserAccountErrors, DeleteUserAccountResponses, DeleteVolumeData, DeleteVolumeResponses, DevPanelExecData, DevPanelExecErrors, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetBackupProgressData, GetBackupProgressResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetOrgMembersData, GetOrgMembersResponses, GetPublicSsoProvidersData, GetPublicSsoProvidersResponses, GetRegistrationStatusData, GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponses, GetRepositoryStatsData, GetRepositoryStatsResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetSsoSettingsData, GetSsoSettingsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetUserDeletionImpactData, GetUserDeletionImpactResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, MountVolumeData, MountVolumeResponses, RefreshSnapshotsData, RefreshSnapshotsResponses, RemoveOrgMemberData, RemoveOrgMemberErrors, RemoveOrgMemberResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, SetRegistrationStatusData, SetRegistrationStatusResponses, StartDoctorData, StartDoctorErrors, StartDoctorResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnlockRepositoryData, UnlockRepositoryResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateMemberRoleData, UpdateMemberRoleErrors, UpdateMemberRoleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateSsoProviderAutoLinkingData, UpdateSsoProviderAutoLinkingErrors, UpdateSsoProviderAutoLinkingResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen'; +import type { BrowseFilesystemData, BrowseFilesystemResponses, CancelDoctorData, CancelDoctorErrors, CancelDoctorResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteSsoInvitationData, DeleteSsoInvitationErrors, DeleteSsoInvitationResponses, DeleteSsoProviderData, DeleteSsoProviderErrors, DeleteSsoProviderResponses, DeleteUserAccountData, DeleteUserAccountErrors, DeleteUserAccountResponses, DeleteVolumeData, DeleteVolumeResponses, DevPanelExecData, DevPanelExecErrors, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetBackupProgressData, GetBackupProgressResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetOrgMembersData, GetOrgMembersResponses, GetPublicSsoProvidersData, GetPublicSsoProvidersResponses, GetRegistrationStatusData, GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponses, GetRepositoryStatsData, GetRepositoryStatsResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetSsoSettingsData, GetSsoSettingsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetUserDeletionImpactData, GetUserDeletionImpactResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, MountVolumeData, MountVolumeResponses, RefreshRepositoryStatsData, RefreshRepositoryStatsResponses, RefreshSnapshotsData, RefreshSnapshotsResponses, RemoveOrgMemberData, RemoveOrgMemberErrors, RemoveOrgMemberResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, SetRegistrationStatusData, SetRegistrationStatusResponses, StartDoctorData, StartDoctorErrors, StartDoctorResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnlockRepositoryData, UnlockRepositoryResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateMemberRoleData, UpdateMemberRoleErrors, UpdateMemberRoleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateSsoProviderAutoLinkingData, UpdateSsoProviderAutoLinkingErrors, UpdateSsoProviderAutoLinkingResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen'; export type Options = Options2 & { /** @@ -24,38 +24,6 @@ export type Options(options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/status', ...options }); -/** - * Get public SSO providers for the instance - */ -export const getPublicSsoProviders = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/sso-providers', ...options }); - -/** - * Get SSO providers and invitations for the active organization - */ -export const getSsoSettings = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/sso-settings', ...options }); - -/** - * Delete an SSO provider - */ -export const deleteSsoProvider = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/auth/sso-providers/{providerId}', ...options }); - -/** - * Update whether SSO sign-in can auto-link existing accounts by email - */ -export const updateSsoProviderAutoLinking = (options: Options) => (options.client ?? client).patch({ - url: '/api/v1/auth/sso-providers/{providerId}/auto-linking', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -/** - * Delete an SSO invitation - */ -export const deleteSsoInvitation = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/auth/sso-invitations/{invitationId}', ...options }); - /** * List admin users for settings management */ @@ -93,6 +61,38 @@ export const updateMemberRole = (options: */ export const removeOrgMember = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/auth/org-members/{memberId}', ...options }); +/** + * Get public SSO providers for the instance + */ +export const getPublicSsoProviders = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/sso-providers', ...options }); + +/** + * Get SSO providers and invitations for the active organization + */ +export const getSsoSettings = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/sso-settings', ...options }); + +/** + * Delete an SSO provider + */ +export const deleteSsoProvider = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/auth/sso-providers/{providerId}', ...options }); + +/** + * Update whether SSO sign-in can auto-link existing accounts by email + */ +export const updateSsoProviderAutoLinking = (options: Options) => (options.client ?? client).patch({ + url: '/api/v1/auth/sso-providers/{providerId}/auto-linking', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Delete an SSO invitation + */ +export const deleteSsoInvitation = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/auth/sso-invitations/{invitationId}', ...options }); + /** * List all volumes */ @@ -101,24 +101,24 @@ export const listVolumes = (options?: Opti /** * Create a new volume */ -export const createVolume = (options?: Options) => (options?.client ?? client).post({ +export const createVolume = (options: Options) => (options.client ?? client).post({ url: '/api/v1/volumes', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); /** * Test connection to backend */ -export const testConnection = (options?: Options) => (options?.client ?? client).post({ +export const testConnection = (options: Options) => (options.client ?? client).post({ url: '/api/v1/volumes/test-connection', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); @@ -177,12 +177,12 @@ export const listRepositories = (options?: /** * Create a new restic repository */ -export const createRepository = (options?: Options) => (options?.client ?? client).post({ +export const createRepository = (options: Options) => (options.client ?? client).post({ url: '/api/v1/repositories', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); @@ -218,6 +218,11 @@ export const updateRepository = (options: */ export const getRepositoryStats = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{shortId}/stats', ...options }); +/** + * Refresh repository storage and compression statistics + */ +export const refreshRepositoryStats = (options: Options) => (options.client ?? client).post({ url: '/api/v1/repositories/{shortId}/stats/refresh', ...options }); + /** * Delete multiple snapshots from a repository */ @@ -319,12 +324,12 @@ export const listBackupSchedules = (option /** * Create a new backup schedule for a volume */ -export const createBackupSchedule = (options?: Options) => (options?.client ?? client).post({ +export const createBackupSchedule = (options: Options) => (options.client ?? client).post({ url: '/api/v1/backups', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); @@ -412,12 +417,12 @@ export const getMirrorCompatibility = (opt /** * Reorder backup schedules by providing an array of schedule short IDs in the desired order */ -export const reorderBackupSchedules = (options?: Options) => (options?.client ?? client).post({ +export const reorderBackupSchedules = (options: Options) => (options.client ?? client).post({ url: '/api/v1/backups/reorder', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); @@ -434,12 +439,12 @@ export const listNotificationDestinations = (options?: Options) => (options?.client ?? client).post({ +export const createNotificationDestination = (options: Options) => (options.client ?? client).post({ url: '/api/v1/notifications/destinations', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); @@ -488,24 +493,24 @@ export const getRegistrationStatus = (opti /** * Update the registration status for new users. Requires global admin role. */ -export const setRegistrationStatus = (options?: Options) => (options?.client ?? client).put({ +export const setRegistrationStatus = (options: Options) => (options.client ?? client).put({ url: '/api/v1/system/registration-status', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); /** * Download the organization's Restic password for backup recovery. Requires organization owner or admin role and password re-authentication. */ -export const downloadResticPassword = (options?: Options) => (options?.client ?? client).post({ +export const downloadResticPassword = (options: Options) => (options.client ?? client).post({ url: '/api/v1/system/restic-password', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 1e8e5af4..c16fdd64 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -23,138 +23,6 @@ export type GetStatusResponses = { export type GetStatusResponse = GetStatusResponses[keyof GetStatusResponses]; -export type GetPublicSsoProvidersData = { - body?: never; - path?: never; - query?: never; - url: '/api/v1/auth/sso-providers'; -}; - -export type GetPublicSsoProvidersResponses = { - /** - * List of public SSO providers - */ - 200: { - providers: Array<{ - organizationSlug: string; - providerId: string; - }>; - }; -}; - -export type GetPublicSsoProvidersResponse = GetPublicSsoProvidersResponses[keyof GetPublicSsoProvidersResponses]; - -export type GetSsoSettingsData = { - body?: never; - path?: never; - query?: never; - url: '/api/v1/auth/sso-settings'; -}; - -export type GetSsoSettingsResponses = { - /** - * SSO settings for the active organization - */ - 200: { - invitations: Array<{ - email: string; - expiresAt: string; - id: string; - role: string; - status: string; - }>; - providers: Array<{ - autoLinkMatchingEmails: boolean; - domain: string; - issuer: string; - organizationId: string | null; - providerId: string; - type: string; - }>; - }; -}; - -export type GetSsoSettingsResponse = GetSsoSettingsResponses[keyof GetSsoSettingsResponses]; - -export type DeleteSsoProviderData = { - body?: never; - path: { - providerId: string; - }; - query?: never; - url: '/api/v1/auth/sso-providers/{providerId}'; -}; - -export type DeleteSsoProviderErrors = { - /** - * Forbidden - */ - 403: unknown; - /** - * Provider not found - */ - 404: unknown; -}; - -export type DeleteSsoProviderResponses = { - /** - * SSO provider deleted successfully - */ - 200: unknown; -}; - -export type UpdateSsoProviderAutoLinkingData = { - body?: { - enabled: boolean; - }; - path: { - providerId: string; - }; - query?: never; - url: '/api/v1/auth/sso-providers/{providerId}/auto-linking'; -}; - -export type UpdateSsoProviderAutoLinkingErrors = { - /** - * Forbidden - */ - 403: unknown; - /** - * Provider not found - */ - 404: unknown; -}; - -export type UpdateSsoProviderAutoLinkingResponses = { - /** - * SSO provider auto-linking setting updated successfully - */ - 200: unknown; -}; - -export type DeleteSsoInvitationData = { - body?: never; - path: { - invitationId: string; - }; - query?: never; - url: '/api/v1/auth/sso-invitations/{invitationId}'; -}; - -export type DeleteSsoInvitationErrors = { - /** - * Forbidden - */ - 403: unknown; -}; - -export type DeleteSsoInvitationResponses = { - /** - * SSO invitation deleted successfully - */ - 200: unknown; -}; - export type GetAdminUsersData = { body?: never; path?: never; @@ -167,18 +35,18 @@ export type GetAdminUsersResponses = { * List of users with roles and status */ 200: { - total: number; users: Array<{ + id: string; + name: string | null; + email: string; + role: string; + banned: boolean; accounts: Array<{ id: string; providerId: string; }>; - banned: boolean; - email: string; - id: string; - name: string | null; - role: string; }>; + total: number; }; }; @@ -196,9 +64,9 @@ export type DeleteUserAccountData = { export type DeleteUserAccountErrors = { /** - * Forbidden + * Account not found */ - 403: unknown; + 404: unknown; /** * Cannot delete the last account */ @@ -230,9 +98,9 @@ export type GetUserDeletionImpactResponses = { id: string; name: string; resources: { - backupSchedulesCount: number; - repositoriesCount: number; volumesCount: number; + repositoriesCount: number; + backupSchedulesCount: number; }; }>; }; @@ -253,14 +121,14 @@ export type GetOrgMembersResponses = { */ 200: { members: Array<{ - createdAt: string; id: string; - role: string; - user: { - email: string; - name: string | null; - }; userId: string; + role: string; + createdAt: string; + user: { + name: string | null; + email: string; + }; }>; }; }; @@ -268,8 +136,8 @@ export type GetOrgMembersResponses = { export type GetOrgMembersResponse = GetOrgMembersResponses[keyof GetOrgMembersResponses]; export type UpdateMemberRoleData = { - body?: { - role: 'admin' | 'member'; + body: { + role: 'member' | 'admin'; }; path: { memberId: string; @@ -323,6 +191,138 @@ export type RemoveOrgMemberResponses = { 200: unknown; }; +export type GetPublicSsoProvidersData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/auth/sso-providers'; +}; + +export type GetPublicSsoProvidersResponses = { + /** + * List of public SSO providers + */ + 200: { + providers: Array<{ + providerId: string; + organizationSlug: string; + }>; + }; +}; + +export type GetPublicSsoProvidersResponse = GetPublicSsoProvidersResponses[keyof GetPublicSsoProvidersResponses]; + +export type GetSsoSettingsData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/auth/sso-settings'; +}; + +export type GetSsoSettingsResponses = { + /** + * SSO settings for the active organization + */ + 200: { + providers: Array<{ + providerId: string; + type: string; + issuer: string; + domain: string; + autoLinkMatchingEmails: boolean; + organizationId: string | null; + }>; + invitations: Array<{ + id: string; + email: string; + role: string; + status: string; + expiresAt: string; + }>; + }; +}; + +export type GetSsoSettingsResponse = GetSsoSettingsResponses[keyof GetSsoSettingsResponses]; + +export type DeleteSsoProviderData = { + body?: never; + path: { + providerId: string; + }; + query?: never; + url: '/api/v1/auth/sso-providers/{providerId}'; +}; + +export type DeleteSsoProviderErrors = { + /** + * Forbidden + */ + 403: unknown; + /** + * Provider not found + */ + 404: unknown; +}; + +export type DeleteSsoProviderResponses = { + /** + * SSO provider deleted successfully + */ + 200: unknown; +}; + +export type UpdateSsoProviderAutoLinkingData = { + body: { + enabled: boolean; + }; + path: { + providerId: string; + }; + query?: never; + url: '/api/v1/auth/sso-providers/{providerId}/auto-linking'; +}; + +export type UpdateSsoProviderAutoLinkingErrors = { + /** + * Forbidden + */ + 403: unknown; + /** + * Provider not found + */ + 404: unknown; +}; + +export type UpdateSsoProviderAutoLinkingResponses = { + /** + * SSO provider auto-linking setting updated successfully + */ + 200: unknown; +}; + +export type DeleteSsoInvitationData = { + body?: never; + path: { + invitationId: string; + }; + query?: never; + url: '/api/v1/auth/sso-invitations/{invitationId}'; +}; + +export type DeleteSsoInvitationErrors = { + /** + * Forbidden + */ + 403: unknown; +}; + +export type DeleteSsoInvitationResponses = { + /** + * SSO invitation deleted successfully + */ + 200: unknown; +}; + export type ListVolumesData = { body?: never; path?: never; @@ -335,120 +335,120 @@ export type ListVolumesResponses = { * A list of volumes */ 200: Array<{ - autoRemount: boolean; + id: number; + shortId: string; + name: string; + type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp'; + status: 'mounted' | 'unmounted' | 'error'; + lastError: string | null; + createdAt: number; + updatedAt: number; + lastHealthCheck: number; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; - shortId: string; - status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; - updatedAt: number; + autoRemount: boolean; }>; }; export type ListVolumesResponse = ListVolumesResponses[keyof ListVolumesResponses]; export type CreateVolumeData = { - body?: { + body: { + name: string; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - name: string; }; path?: never; query?: never; @@ -460,118 +460,118 @@ export type CreateVolumeResponses = { * Volume created successfully */ 201: { - autoRemount: boolean; + id: number; + shortId: string; + name: string; + type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp'; + status: 'mounted' | 'unmounted' | 'error'; + lastError: string | null; + createdAt: number; + updatedAt: number; + lastHealthCheck: number; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; - shortId: string; - status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; - updatedAt: number; + autoRemount: boolean; }; }; export type CreateVolumeResponse = CreateVolumeResponses[keyof CreateVolumeResponses]; export type TestConnectionData = { - body?: { + body: { config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; }; path?: never; @@ -584,8 +584,8 @@ export type TestConnectionResponses = { * Connection test result */ 200: { - message: string; success: boolean; + message: string; }; }; @@ -632,70 +632,70 @@ export type GetVolumeResponses = { * Volume details */ 200: { - statfs: { - free: number; - total: number; - used: number; - }; volume: { - autoRemount: boolean; + id: number; + shortId: string; + name: string; + type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp'; + status: 'mounted' | 'unmounted' | 'error'; + lastError: string | null; + createdAt: number; + updatedAt: number; + lastHealthCheck: number; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; - shortId: string; - status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; - updatedAt: number; + autoRemount: boolean; + }; + statfs: { + total: number; + used: number; + free: number; }; }; }; @@ -703,57 +703,57 @@ export type GetVolumeResponses = { export type GetVolumeResponse = GetVolumeResponses[keyof GetVolumeResponses]; export type UpdateVolumeData = { - body?: { + body: { + name?: string; autoRemount?: boolean; config?: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - name?: string; }; path: { shortId: string; @@ -774,64 +774,64 @@ export type UpdateVolumeResponses = { * Volume updated successfully */ 200: { - autoRemount: boolean; + id: number; + shortId: string; + name: string; + type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp'; + status: 'mounted' | 'unmounted' | 'error'; + lastError: string | null; + createdAt: number; + updatedAt: number; + lastHealthCheck: number; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; - shortId: string; - status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; - updatedAt: number; + autoRemount: boolean; }; }; @@ -851,8 +851,8 @@ export type MountVolumeResponses = { * Volume mounted successfully */ 200: { - status: 'error' | 'mounted' | 'unmounted'; error?: string; + status: 'mounted' | 'unmounted' | 'error'; }; }; @@ -872,8 +872,8 @@ export type UnmountVolumeResponses = { * Volume unmounted successfully */ 200: { - status: 'error' | 'mounted' | 'unmounted'; error?: string; + status: 'mounted' | 'unmounted' | 'error'; }; }; @@ -900,8 +900,8 @@ export type HealthCheckVolumeResponses = { * Volume health check result */ 200: { - status: 'error' | 'mounted' | 'unmounted'; error?: string; + status: 'mounted' | 'unmounted' | 'error'; }; }; @@ -913,9 +913,9 @@ export type ListFilesData = { shortId: string; }; query?: { - limit?: string; - offset?: string; path?: string; + offset?: number; + limit?: number; }; url: '/api/v1/volumes/{shortId}/files'; }; @@ -928,15 +928,15 @@ export type ListFilesResponses = { files: Array<{ name: string; path: string; - type: 'directory' | 'file'; - modifiedAt?: number; + type: 'file' | 'directory'; size?: number; + modifiedAt?: number; }>; - hasMore: boolean; - limit: number; - offset: number; path: string; + offset: number; + limit: number; total: number; + hasMore: boolean; }; }; @@ -962,9 +962,9 @@ export type BrowseFilesystemResponses = { directories: Array<{ name: string; path: string; - type: 'directory' | 'file'; - modifiedAt?: number; + type: 'file' | 'directory'; size?: number; + modifiedAt?: number; }>; path: string; }; @@ -984,183 +984,183 @@ export type ListRepositoriesResponses = { * List of repositories */ 200: Array<{ - compressionMode: 'auto' | 'max' | 'off' | null; + id: string; + shortId: string; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - 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; backend: 's3'; - bucket: string; endpoint: string; - 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; - accountName: string; - backend: 'azure'; - 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'; bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + accessKeyId: string; + secretAccessKey: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'local'; path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rclone'; - path: string; remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + path: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rest'; url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; + username?: string; password?: string; path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; - username?: string; } | { backend: 'sftp'; host: string; + port?: string | number; + user: string; path: string; privateKey: string; - user: string; - port?: number; skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; lastChecked: number | null; lastError: string | null; - name: string; - shortId: string; - status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }>; }; @@ -1168,167 +1168,167 @@ export type ListRepositoriesResponses = { export type ListRepositoriesResponse = ListRepositoriesResponses[keyof ListRepositoriesResponses]; export type CreateRepositoryData = { - body?: { + body: { + name: string; + compressionMode?: 'off' | 'auto' | 'max'; config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - 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; backend: 's3'; - bucket: string; endpoint: string; - 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; - accountName: string; - backend: 'azure'; - 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'; bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + accessKeyId: string; + secretAccessKey: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'local'; path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rclone'; - path: string; remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + path: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rest'; url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; + username?: string; password?: string; path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; - username?: string; } | { backend: 'sftp'; host: string; + port?: string | number; + user: string; path: string; privateKey: string; - user: string; - port?: number; skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; }; - name: string; - compressionMode?: 'auto' | 'max' | 'off'; }; path?: never; query?: never; @@ -1343,8 +1343,8 @@ export type CreateRepositoryResponses = { message: string; repository: { id: string; - name: string; shortId: string; + name: string; }; }; }; @@ -1404,183 +1404,183 @@ export type GetRepositoryResponses = { * Repository details */ 200: { - compressionMode: 'auto' | 'max' | 'off' | null; + id: string; + shortId: string; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - 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; backend: 's3'; - bucket: string; endpoint: string; - 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; - accountName: string; - backend: 'azure'; - 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'; bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + accessKeyId: string; + secretAccessKey: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'local'; path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rclone'; - path: string; remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + path: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rest'; url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; + username?: string; password?: string; path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; - username?: string; } | { backend: 'sftp'; host: string; + port?: string | number; + user: string; path: string; privateKey: string; - user: string; - port?: number; skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; lastChecked: number | null; lastError: string | null; - name: string; - shortId: string; - status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }; }; @@ -1588,167 +1588,167 @@ export type GetRepositoryResponses = { export type GetRepositoryResponse = GetRepositoryResponses[keyof GetRepositoryResponses]; export type UpdateRepositoryData = { - body?: { - compressionMode?: 'auto' | 'max' | 'off'; + body: { + name?: string; + compressionMode?: 'off' | 'auto' | 'max'; config?: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - 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; backend: 's3'; - bucket: string; endpoint: string; - 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; - accountName: string; - backend: 'azure'; - 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'; bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + accessKeyId: string; + secretAccessKey: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'local'; path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rclone'; - path: string; remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + path: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rest'; url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; + username?: string; password?: string; path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; - username?: string; } | { backend: 'sftp'; host: string; + port?: string | number; + user: string; path: string; privateKey: string; - user: string; - port?: number; skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; }; - name?: string; }; path: { shortId: string; @@ -1777,183 +1777,183 @@ export type UpdateRepositoryResponses = { * Repository updated successfully */ 200: { - compressionMode: 'auto' | 'max' | 'off' | null; + id: string; + shortId: string; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - 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; backend: 's3'; - bucket: string; endpoint: string; - 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; - accountName: string; - backend: 'azure'; - 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'; bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + accessKeyId: string; + secretAccessKey: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'local'; path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rclone'; - path: string; remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + path: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rest'; url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; + username?: string; password?: string; path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; - username?: string; } | { backend: 'sftp'; host: string; + port?: string | number; + user: string; path: string; privateKey: string; - user: string; - port?: number; skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; lastChecked: number | null; lastError: string | null; - name: string; - shortId: string; - status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }; }; @@ -1974,19 +1974,44 @@ export type GetRepositoryStatsResponses = { * Repository statistics */ 200: { - compression_progress?: number; - compression_ratio?: number; - compression_space_saving?: number; - snapshots_count?: number; total_size?: number; total_uncompressed_size?: number; + compression_ratio?: number; + compression_progress?: number; + compression_space_saving?: number; + snapshots_count?: number; }; }; export type GetRepositoryStatsResponse = GetRepositoryStatsResponses[keyof GetRepositoryStatsResponses]; +export type RefreshRepositoryStatsData = { + body?: never; + path: { + shortId: string; + }; + query?: never; + url: '/api/v1/repositories/{shortId}/stats/refresh'; +}; + +export type RefreshRepositoryStatsResponses = { + /** + * Refreshed repository statistics + */ + 200: { + total_size?: number; + total_uncompressed_size?: number; + compression_ratio?: number; + compression_progress?: number; + compression_space_saving?: number; + snapshots_count?: number; + }; +}; + +export type RefreshRepositoryStatsResponse = RefreshRepositoryStatsResponses[keyof RefreshRepositoryStatsResponses]; + export type DeleteSnapshotsData = { - body?: { + body: { snapshotIds: Array; }; path: { @@ -2023,29 +2048,29 @@ export type ListSnapshotsResponses = { * List of snapshots */ 200: Array<{ - duration: number; - paths: Array; - retentionCategories: Array; short_id: string; - size: number; - tags: Array; time: number; + paths: Array; + size: number; + duration: number; + tags: Array; + retentionCategories: Array; hostname?: string; summary?: { - backup_end: string; - backup_start: string; - data_added: number; - data_blobs: number; - dirs_changed: number; - dirs_new: number; - dirs_unmodified: number; - files_changed: number; files_new: number; + files_changed: number; files_unmodified: number; - total_bytes_processed: number; - total_files_processed: number; + dirs_new: number; + dirs_changed: number; + dirs_unmodified: number; + data_blobs: number; tree_blobs: number; + data_added: number; data_added_packed?: number; + total_files_processed: number; + total_bytes_processed: number; + backup_start: string; + backup_end: string; }; }>; }; @@ -2066,8 +2091,8 @@ export type RefreshSnapshotsResponses = { * Snapshot cache cleared and refreshed */ 200: { - count: number; message: string; + count: number; }; }; @@ -2109,29 +2134,29 @@ export type GetSnapshotDetailsResponses = { * Snapshot details */ 200: { - duration: number; - paths: Array; - retentionCategories: Array; short_id: string; - size: number; - tags: Array; time: number; + paths: Array; + size: number; + duration: number; + tags: Array; + retentionCategories: Array; hostname?: string; summary?: { - backup_end: string; - backup_start: string; - data_added: number; - data_blobs: number; - dirs_changed: number; - dirs_new: number; - dirs_unmodified: number; - files_changed: number; files_new: number; + files_changed: number; files_unmodified: number; - total_bytes_processed: number; - total_files_processed: number; + dirs_new: number; + dirs_changed: number; + dirs_unmodified: number; + data_blobs: number; tree_blobs: number; + data_added: number; data_added_packed?: number; + total_files_processed: number; + total_bytes_processed: number; + backup_start: string; + backup_end: string; }; }; }; @@ -2145,9 +2170,9 @@ export type ListSnapshotFilesData = { snapshotId: string; }; query?: { - limit?: string; - offset?: string; path?: string; + offset?: number; + limit?: number; }; url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/files'; }; @@ -2157,29 +2182,29 @@ export type ListSnapshotFilesResponses = { * List of files and directories in the snapshot */ 200: { - files: Array<{ - name: string; - path: string; - type: string; - atime?: string; - ctime?: string; - gid?: number; - mode?: number; - mtime?: string; - size?: number; - uid?: number; - }>; - hasMore: boolean; - limit: number; - offset: number; snapshot: { - hostname: string; id: string; - paths: Array; short_id: string; time: string; + hostname?: string; + paths: Array; }; + files: Array<{ + name: string; + type: string; + path: string; + uid?: number; + gid?: number; + size?: number; + mode?: number; + mtime?: string; + atime?: string; + ctime?: string; + }>; + offset: number; + limit: number; total: number; + hasMore: boolean; }; }; @@ -2192,8 +2217,8 @@ export type DumpSnapshotData = { snapshotId: string; }; query?: { - kind?: 'dir' | 'file'; path?: string; + kind?: 'file' | 'dir'; }; url: '/api/v1/repositories/{shortId}/snapshots/{snapshotId}/dump'; }; @@ -2208,14 +2233,15 @@ export type DumpSnapshotResponses = { export type DumpSnapshotResponse = DumpSnapshotResponses[keyof DumpSnapshotResponses]; export type RestoreSnapshotData = { - body?: { + body: { snapshotId: string; - delete?: boolean; + include?: Array; + selectedItemKind?: 'file' | 'dir'; exclude?: Array; excludeXattr?: Array; - include?: Array; - overwrite?: 'always' | 'if-changed' | 'if-newer' | 'never'; + delete?: boolean; targetPath?: string; + overwrite?: 'always' | 'if-changed' | 'if-newer' | 'never'; }; path: { shortId: string; @@ -2229,10 +2255,10 @@ export type RestoreSnapshotResponses = { * Snapshot restored successfully */ 200: { + success: boolean; + message: string; filesRestored: number; filesSkipped: number; - message: string; - success: boolean; }; }; @@ -2307,15 +2333,15 @@ export type UnlockRepositoryResponses = { * Repository unlocked successfully */ 200: { - message: string; success: boolean; + message: string; }; }; export type UnlockRepositoryResponse = UnlockRepositoryResponses[keyof UnlockRepositoryResponses]; export type TagSnapshotsData = { - body?: { + body: { snapshotIds: Array; add?: Array; remove?: Array; @@ -2340,7 +2366,7 @@ export type TagSnapshotsResponses = { export type TagSnapshotsResponse = TagSnapshotsResponses[keyof TagSnapshotsResponses]; export type DevPanelExecData = { - body?: { + body: { command: string; args?: Array; }; @@ -2379,298 +2405,300 @@ export type ListBackupSchedulesResponses = { * List of backup schedules */ 200: Array<{ - createdAt: number; - cronExpression: string; - enabled: boolean; - excludeIfPresent: Array | null; - excludePatterns: Array | null; id: number; - includePatterns: Array | null; - lastBackupAt: number | null; - lastBackupError: string | null; - lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; - name: string; - nextBackupAt: number | null; - oneFileSystem: boolean; - repository: { - compressionMode: 'auto' | 'max' | 'off' | null; - config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - 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; - backend: 's3'; - bucket: string; - endpoint: string; - 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; - accountName: string; - backend: 'azure'; - 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'; - bucket: string; - credentialsJson: string; - 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'; - path: 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: 'rclone'; - path: string; - 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; - } | { - backend: 'sftp'; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - 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: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; - updatedAt: number; - }; - repositoryId: string; - retentionPolicy: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - } | null; shortId: string; + name: string; + volumeId: number; + repositoryId: string; + enabled: boolean; + cronExpression: string; + retentionPolicy: { + keepLast?: number; + keepHourly?: number; + keepDaily?: number; + keepWeekly?: number; + keepMonthly?: number; + keepYearly?: number; + keepWithinDuration?: string; + } | null; + excludePatterns: Array | null; + excludeIfPresent: Array | null; + includePatterns: Array | null; + oneFileSystem: boolean; + customResticParams: Array | null; + lastBackupAt: number | null; + lastBackupStatus: 'success' | 'error' | 'in_progress' | 'warning' | null; + lastBackupError: string | null; + nextBackupAt: number | null; + createdAt: number; updatedAt: number; volume: { - autoRemount: boolean; + id: number; + shortId: string; + name: string; + type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp'; + status: 'mounted' | 'unmounted' | 'error'; + lastError: string | null; + createdAt: number; + updatedAt: number; + lastHealthCheck: number; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; + autoRemount: boolean; + }; + repository: { + id: string; shortId: string; - status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; + config: { + backend: 's3'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'local'; + path: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'rclone'; + remote: string; + path: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'rest'; + url: string; + username?: string; + password?: string; + path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + user: string; + path: string; + privateKey: string; + skipHostKeyCheck?: boolean; + knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + }; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; + lastChecked: number | null; + lastError: string | null; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }; - volumeId: number; }>; }; export type ListBackupSchedulesResponse = ListBackupSchedulesResponses[keyof ListBackupSchedulesResponses]; export type CreateBackupScheduleData = { - body?: { - cronExpression: string; - enabled: boolean; + body: { name: string; + volumeId: string | number; repositoryId: string; - volumeId: number | string; - excludeIfPresent?: Array; + enabled: boolean; + cronExpression: string; + retentionPolicy?: { + keepLast?: number; + keepHourly?: number; + keepDaily?: number; + keepWeekly?: number; + keepMonthly?: number; + keepYearly?: number; + keepWithinDuration?: string; + }; excludePatterns?: Array; + excludeIfPresent?: Array; includePatterns?: Array; oneFileSystem?: boolean; - retentionPolicy?: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - }; tags?: Array; + customResticParams?: Array; }; path?: never; query?: never; @@ -2682,32 +2710,33 @@ export type CreateBackupScheduleResponses = { * Backup schedule created successfully */ 201: { - createdAt: number; - cronExpression: string; - enabled: boolean; - excludeIfPresent: Array | null; - excludePatterns: Array | null; id: number; - includePatterns: Array | null; - lastBackupAt: number | null; - lastBackupError: string | null; - lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; - name: string; - nextBackupAt: number | null; - oneFileSystem: boolean; - repositoryId: string; - retentionPolicy: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - } | null; shortId: string; - updatedAt: number; + name: string; volumeId: number; + repositoryId: string; + enabled: boolean; + cronExpression: string; + retentionPolicy: { + keepLast?: number; + keepHourly?: number; + keepDaily?: number; + keepWeekly?: number; + keepMonthly?: number; + keepYearly?: number; + keepWithinDuration?: string; + } | null; + excludePatterns: Array | null; + excludeIfPresent: Array | null; + includePatterns: Array | null; + oneFileSystem: boolean; + customResticParams: Array | null; + lastBackupAt: number | null; + lastBackupStatus: 'success' | 'error' | 'in_progress' | 'warning' | null; + lastBackupError: string | null; + nextBackupAt: number | null; + createdAt: number; + updatedAt: number; }; }; @@ -2747,297 +2776,299 @@ export type GetBackupScheduleResponses = { * Backup schedule details */ 200: { - createdAt: number; - cronExpression: string; - enabled: boolean; - excludeIfPresent: Array | null; - excludePatterns: Array | null; id: number; - includePatterns: Array | null; - lastBackupAt: number | null; - lastBackupError: string | null; - lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; - name: string; - nextBackupAt: number | null; - oneFileSystem: boolean; - repository: { - compressionMode: 'auto' | 'max' | 'off' | null; - config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - 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; - backend: 's3'; - bucket: string; - endpoint: string; - 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; - accountName: string; - backend: 'azure'; - 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'; - bucket: string; - credentialsJson: string; - 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'; - path: 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: 'rclone'; - path: string; - 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; - } | { - backend: 'sftp'; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - 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: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; - updatedAt: number; - }; - repositoryId: string; - retentionPolicy: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - } | null; shortId: string; + name: string; + volumeId: number; + repositoryId: string; + enabled: boolean; + cronExpression: string; + retentionPolicy: { + keepLast?: number; + keepHourly?: number; + keepDaily?: number; + keepWeekly?: number; + keepMonthly?: number; + keepYearly?: number; + keepWithinDuration?: string; + } | null; + excludePatterns: Array | null; + excludeIfPresent: Array | null; + includePatterns: Array | null; + oneFileSystem: boolean; + customResticParams: Array | null; + lastBackupAt: number | null; + lastBackupStatus: 'success' | 'error' | 'in_progress' | 'warning' | null; + lastBackupError: string | null; + nextBackupAt: number | null; + createdAt: number; updatedAt: number; volume: { - autoRemount: boolean; + id: number; + shortId: string; + name: string; + type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp'; + status: 'mounted' | 'unmounted' | 'error'; + lastError: string | null; + createdAt: number; + updatedAt: number; + lastHealthCheck: number; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; + autoRemount: boolean; + }; + repository: { + id: string; shortId: string; - status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; + config: { + backend: 's3'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'local'; + path: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'rclone'; + remote: string; + path: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'rest'; + url: string; + username?: string; + password?: string; + path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + user: string; + path: string; + privateKey: string; + skipHostKeyCheck?: boolean; + knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + }; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; + lastChecked: number | null; + lastError: string | null; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }; - volumeId: number; }; }; export type GetBackupScheduleResponse = GetBackupScheduleResponses[keyof GetBackupScheduleResponses]; export type UpdateBackupScheduleData = { - body?: { - cronExpression: string; + body: { + name?: string; repositoryId: string; enabled?: boolean; - excludeIfPresent?: Array; - excludePatterns?: Array; - includePatterns?: Array; - name?: string; - oneFileSystem?: boolean; + cronExpression: string; retentionPolicy?: { - keepDaily?: number; - keepHourly?: number; keepLast?: number; - keepMonthly?: number; + keepHourly?: number; + keepDaily?: number; keepWeekly?: number; - keepWithinDuration?: string; + keepMonthly?: number; keepYearly?: number; + keepWithinDuration?: string; }; + excludePatterns?: Array; + excludeIfPresent?: Array; + includePatterns?: Array; + oneFileSystem?: boolean; tags?: Array; + customResticParams?: Array; }; path: { shortId: string; @@ -3051,32 +3082,33 @@ export type UpdateBackupScheduleResponses = { * Backup schedule updated successfully */ 200: { - createdAt: number; - cronExpression: string; - enabled: boolean; - excludeIfPresent: Array | null; - excludePatterns: Array | null; id: number; - includePatterns: Array | null; - lastBackupAt: number | null; - lastBackupError: string | null; - lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; - name: string; - nextBackupAt: number | null; - oneFileSystem: boolean; - repositoryId: string; - retentionPolicy: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - } | null; shortId: string; - updatedAt: number; + name: string; volumeId: number; + repositoryId: string; + enabled: boolean; + cronExpression: string; + retentionPolicy: { + keepLast?: number; + keepHourly?: number; + keepDaily?: number; + keepWeekly?: number; + keepMonthly?: number; + keepYearly?: number; + keepWithinDuration?: string; + } | null; + excludePatterns: Array | null; + excludeIfPresent: Array | null; + includePatterns: Array | null; + oneFileSystem: boolean; + customResticParams: Array | null; + lastBackupAt: number | null; + lastBackupStatus: 'success' | 'error' | 'in_progress' | 'warning' | null; + lastBackupError: string | null; + nextBackupAt: number | null; + createdAt: number; + updatedAt: number; }; }; @@ -3096,272 +3128,273 @@ export type GetBackupScheduleForVolumeResponses = { * Backup schedule details for the volume */ 200: { - createdAt: number; - cronExpression: string; - enabled: boolean; - excludeIfPresent: Array | null; - excludePatterns: Array | null; id: number; - includePatterns: Array | null; - lastBackupAt: number | null; - lastBackupError: string | null; - lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; - name: string; - nextBackupAt: number | null; - oneFileSystem: boolean; - repository: { - compressionMode: 'auto' | 'max' | 'off' | null; - config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - 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; - backend: 's3'; - bucket: string; - endpoint: string; - 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; - accountName: string; - backend: 'azure'; - 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'; - bucket: string; - credentialsJson: string; - 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'; - path: 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: 'rclone'; - path: string; - 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; - } | { - backend: 'sftp'; - host: string; - path: string; - privateKey: string; - user: string; - port?: number; - 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: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; - updatedAt: number; - }; - repositoryId: string; - retentionPolicy: { - keepDaily?: number; - keepHourly?: number; - keepLast?: number; - keepMonthly?: number; - keepWeekly?: number; - keepWithinDuration?: string; - keepYearly?: number; - } | null; shortId: string; + name: string; + volumeId: number; + repositoryId: string; + enabled: boolean; + cronExpression: string; + retentionPolicy: { + keepLast?: number; + keepHourly?: number; + keepDaily?: number; + keepWeekly?: number; + keepMonthly?: number; + keepYearly?: number; + keepWithinDuration?: string; + } | null; + excludePatterns: Array | null; + excludeIfPresent: Array | null; + includePatterns: Array | null; + oneFileSystem: boolean; + customResticParams: Array | null; + lastBackupAt: number | null; + lastBackupStatus: 'success' | 'error' | 'in_progress' | 'warning' | null; + lastBackupError: string | null; + nextBackupAt: number | null; + createdAt: number; updatedAt: number; volume: { - autoRemount: boolean; + id: number; + shortId: string; + name: string; + type: 'nfs' | 'smb' | 'directory' | 'webdav' | 'rclone' | 'sftp'; + status: 'mounted' | 'unmounted' | 'error'; + lastError: string | null; + createdAt: number; + updatedAt: number; + lastHealthCheck: number; config: { - backend: 'directory'; - path: string; - readOnly?: false; - } | { backend: 'nfs'; - exportPath: string; server: string; + exportPath: string; + port?: string | number; version: '3' | '4' | '4.1'; - port?: number; - readOnly?: boolean; - } | { - backend: 'rclone'; - path: string; - remote: string; - readOnly?: boolean; - } | { - backend: 'sftp'; - host: string; - path: string; - username: string; - port?: number; - skipHostKeyCheck?: boolean; - knownHosts?: string; - password?: string; - privateKey?: string; readOnly?: boolean; } | { backend: 'smb'; server: string; share: string; - vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; - port?: number; - domain?: string; - guest?: boolean; - password?: string; - readOnly?: boolean; username?: string; + password?: string; + guest?: boolean; + vers?: '1.0' | '2.0' | '2.1' | '3.0' | 'auto'; + domain?: string; + port?: string | number; + readOnly?: boolean; } | { backend: 'webdav'; - path: string; server: string; - port?: number; + path: string; + username?: string; password?: string; + port?: string | number; readOnly?: boolean; ssl?: boolean; - username?: string; + } | { + backend: 'directory'; + path: string; + readOnly?: false; + } | { + backend: 'rclone'; + remote: string; + path: string; + readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + username: string; + password?: string; + privateKey?: string; + path: string; + readOnly?: boolean; + skipHostKeyCheck?: boolean; + knownHosts?: string; }; - createdAt: number; - id: number; - lastError: string | null; - lastHealthCheck: number; - name: string; + autoRemount: boolean; + }; + repository: { + id: string; shortId: string; - status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; + config: { + backend: 's3'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'local'; + path: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'rclone'; + remote: string; + path: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'rest'; + url: string; + username?: string; + password?: string; + path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'sftp'; + host: string; + port?: string | number; + user: string; + path: string; + privateKey: string; + skipHostKeyCheck?: boolean; + knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + }; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; + lastChecked: number | null; + lastError: string | null; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }; - volumeId: number; } | null; }; @@ -3448,94 +3481,94 @@ export type GetScheduleNotificationsResponses = { * List of notification assignments for the schedule */ 200: Array<{ + scheduleId: number; + destinationId: number; + notifyOnStart: boolean; + notifyOnSuccess: boolean; + notifyOnWarning: boolean; + notifyOnFailure: boolean; createdAt: number; destination: { + id: number; + name: string; + enabled: boolean; + type: 'email' | 'slack' | 'discord' | 'gotify' | 'ntfy' | 'pushover' | 'telegram' | 'generic' | 'custom'; config: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; createdAt: number; - enabled: boolean; - id: number; - name: string; - type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }; - destinationId: number; - notifyOnFailure: boolean; - notifyOnStart: boolean; - notifyOnSuccess: boolean; - notifyOnWarning: boolean; - scheduleId: number; }>; }; export type GetScheduleNotificationsResponse = GetScheduleNotificationsResponses[keyof GetScheduleNotificationsResponses]; export type UpdateScheduleNotificationsData = { - body?: { + body: { assignments: Array<{ destinationId: number; - notifyOnFailure: boolean; notifyOnStart: boolean; notifyOnSuccess: boolean; notifyOnWarning: boolean; + notifyOnFailure: boolean; }>; }; path: { @@ -3550,81 +3583,81 @@ export type UpdateScheduleNotificationsResponses = { * Notification assignments updated successfully */ 200: Array<{ + scheduleId: number; + destinationId: number; + notifyOnStart: boolean; + notifyOnSuccess: boolean; + notifyOnWarning: boolean; + notifyOnFailure: boolean; createdAt: number; destination: { + id: number; + name: string; + enabled: boolean; + type: 'email' | 'slack' | 'discord' | 'gotify' | 'ntfy' | 'pushover' | 'telegram' | 'generic' | 'custom'; config: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; createdAt: number; - enabled: boolean; - id: number; - name: string; - type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }; - destinationId: number; - notifyOnFailure: boolean; - notifyOnStart: boolean; - notifyOnSuccess: boolean; - notifyOnWarning: boolean; - scheduleId: number; }>; }; @@ -3644,203 +3677,203 @@ export type GetScheduleMirrorsResponses = { * List of mirror repository assignments for the schedule */ 200: Array<{ - createdAt: number; + scheduleId: string; + repositoryId: string; enabled: boolean; lastCopyAt: number | null; + lastCopyStatus: 'success' | 'error' | 'in_progress' | null; lastCopyError: string | null; - lastCopyStatus: 'error' | 'in_progress' | 'success' | null; + createdAt: number; repository: { - compressionMode: 'auto' | 'max' | 'off' | null; + id: string; + shortId: string; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - 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; backend: 's3'; - bucket: string; endpoint: string; - 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; - accountName: string; - backend: 'azure'; - 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'; bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + accessKeyId: string; + secretAccessKey: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'local'; path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rclone'; - path: string; remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + path: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rest'; url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; + username?: string; password?: string; path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; - username?: string; } | { backend: 'sftp'; host: string; + port?: string | number; + user: string; path: string; privateKey: string; - user: string; - port?: number; skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; lastChecked: number | null; lastError: string | null; - name: string; - shortId: string; - status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }; - repositoryId: string; - scheduleId: string; }>; }; export type GetScheduleMirrorsResponse = GetScheduleMirrorsResponses[keyof GetScheduleMirrorsResponses]; export type UpdateScheduleMirrorsData = { - body?: { + body: { mirrors: Array<{ - enabled: boolean; repositoryId: string; + enabled: boolean; }>; }; path: { @@ -3855,193 +3888,193 @@ export type UpdateScheduleMirrorsResponses = { * Mirror assignments updated successfully */ 200: Array<{ - createdAt: number; + scheduleId: string; + repositoryId: string; enabled: boolean; lastCopyAt: number | null; + lastCopyStatus: 'success' | 'error' | 'in_progress' | null; lastCopyError: string | null; - lastCopyStatus: 'error' | 'in_progress' | 'success' | null; + createdAt: number; repository: { - compressionMode: 'auto' | 'max' | 'off' | null; + id: string; + shortId: string; + name: string; + type: 'local' | 's3' | 'r2' | 'gcs' | 'azure' | 'rclone' | 'rest' | 'sftp'; config: { - accessKeyId: string; - backend: 'r2'; - bucket: string; - endpoint: string; - 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; backend: 's3'; - bucket: string; endpoint: string; - 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; - accountName: string; - backend: 'azure'; - 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'; bucket: string; - credentialsJson: string; - projectId: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + accessKeyId: string; + secretAccessKey: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'r2'; + endpoint: string; + bucket: string; + accessKeyId: string; + secretAccessKey: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'local'; path: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'gcs'; + bucket: string; + projectId: string; + credentialsJson: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + } | { + backend: 'azure'; + container: string; + accountName: string; + accountKey: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; + uploadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rclone'; - path: string; remote: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; + path: string; isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; } | { backend: 'rest'; url: string; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; + username?: string; password?: string; path?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; - username?: string; } | { backend: 'sftp'; host: string; + port?: string | number; + user: string; path: string; privateKey: string; - user: string; - port?: number; skipHostKeyCheck?: boolean; - cacert?: string; - customPassword?: string; - downloadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; - enabled?: boolean; - }; - insecureTls?: boolean; - isExistingRepository?: boolean; knownHosts?: string; + isExistingRepository?: boolean; + customPassword?: string; + cacert?: string; + insecureTls?: boolean; uploadLimit?: { - unit?: 'Gbps' | 'Kbps' | 'Mbps'; - value?: number; enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; + }; + downloadLimit?: { + enabled?: boolean; + value?: number; + unit?: 'Kbps' | 'Mbps' | 'Gbps'; }; }; - createdAt: number; - doctorResult: { - completedAt: number; - steps: Array<{ - error: string | null; - output: string | null; - step: string; - success: boolean; - }>; - success: boolean; - } | null; - id: string; + compressionMode: 'off' | 'auto' | 'max' | null; + status: 'healthy' | 'error' | 'unknown' | 'doctor' | 'cancelled' | null; lastChecked: number | null; lastError: string | null; - name: string; - shortId: string; - status: 'cancelled' | 'doctor' | 'error' | 'healthy' | 'unknown' | null; - type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + doctorResult: { + success: boolean; + steps: Array<{ + step: string; + success: boolean; + output: string | null; + error: string | null; + }>; + completedAt: number; + } | null; + createdAt: number; updatedAt: number; }; - repositoryId: string; - scheduleId: string; }>; }; @@ -4061,16 +4094,16 @@ export type GetMirrorCompatibilityResponses = { * List of repositories with their mirror compatibility status */ 200: Array<{ + repositoryId: string; compatible: boolean; reason: string | null; - repositoryId: string; }>; }; export type GetMirrorCompatibilityResponse = GetMirrorCompatibilityResponses[keyof GetMirrorCompatibilityResponses]; export type ReorderBackupSchedulesData = { - body?: { + body: { scheduleShortIds: Array; }; path?: never; @@ -4103,15 +4136,16 @@ export type GetBackupProgressResponses = { * Current backup progress or null if not yet available */ 200: { - bytes_done: number; - files_done: number; - percent_done: number; - repositoryName: string; scheduleId: string; - seconds_elapsed: number; - total_bytes: number; - total_files: number; volumeName: string; + repositoryName: string; + seconds_elapsed: number; + seconds_remaining?: number; + percent_done: number; + total_files: number; + files_done: number; + total_bytes: number; + bytes_done: number; current_files?: Array; } | null; }; @@ -4130,71 +4164,71 @@ export type ListNotificationDestinationsResponses = { * A list of notification destinations */ 200: Array<{ + id: number; + name: string; + enabled: boolean; + type: 'email' | 'slack' | 'discord' | 'gotify' | 'ntfy' | 'pushover' | 'telegram' | 'generic' | 'custom'; config: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; createdAt: number; - enabled: boolean; - id: number; - name: string; - type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }>; }; @@ -4202,68 +4236,68 @@ export type ListNotificationDestinationsResponses = { export type ListNotificationDestinationsResponse = ListNotificationDestinationsResponses[keyof ListNotificationDestinationsResponses]; export type CreateNotificationDestinationData = { - body?: { + body: { + name: string; config: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; - name: string; }; path?: never; query?: never; @@ -4275,71 +4309,71 @@ export type CreateNotificationDestinationResponses = { * Notification destination created successfully */ 201: { + id: number; + name: string; + enabled: boolean; + type: 'email' | 'slack' | 'discord' | 'gotify' | 'ntfy' | 'pushover' | 'telegram' | 'generic' | 'custom'; config: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; createdAt: number; - enabled: boolean; - id: number; - name: string; - type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }; }; @@ -4394,71 +4428,71 @@ export type GetNotificationDestinationResponses = { * Notification destination details */ 200: { + id: number; + name: string; + enabled: boolean; + type: 'email' | 'slack' | 'discord' | 'gotify' | 'ntfy' | 'pushover' | 'telegram' | 'generic' | 'custom'; config: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; createdAt: number; - enabled: boolean; - id: number; - name: string; - type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }; }; @@ -4466,69 +4500,69 @@ export type GetNotificationDestinationResponses = { export type GetNotificationDestinationResponse = GetNotificationDestinationResponses[keyof GetNotificationDestinationResponses]; export type UpdateNotificationDestinationData = { - body?: { + body: { + name?: string; + enabled?: boolean; config?: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; - enabled?: boolean; - name?: string; }; path: { id: string; @@ -4549,71 +4583,71 @@ export type UpdateNotificationDestinationResponses = { * Notification destination updated successfully */ 200: { + id: number; + name: string; + enabled: boolean; + type: 'email' | 'slack' | 'discord' | 'gotify' | 'ntfy' | 'pushover' | 'telegram' | 'generic' | 'custom'; config: { - apiToken: string; - priority: -1 | 0 | 1; - type: 'pushover'; - userKey: string; - devices?: string; - } | { - botToken: string; - chatId: string; - type: 'telegram'; - threadId?: string; - } | { - from: string; + type: 'email'; smtpHost: string; smtpPort: number; - to: Array; - type: 'email'; - useTLS: boolean; + username?: string; + password?: string; + from: string; fromName?: string; - password?: string; - username?: string; - } | { - method: 'GET' | 'POST'; - type: 'generic'; - url: string; - contentType?: string; - headers?: Array; - messageKey?: string; - titleKey?: string; - useJson?: boolean; - } | { - priority: 'default' | 'high' | 'low' | 'max' | 'min'; - topic: string; - type: 'ntfy'; - accessToken?: string; - password?: string; - serverUrl?: string; - username?: string; - } | { - priority: number; - serverUrl: string; - token: string; - type: 'gotify'; - path?: string; - } | { - shoutrrrUrl: string; - type: 'custom'; - } | { - type: 'discord'; - webhookUrl: string; - avatarUrl?: string; - threadId?: string; - username?: string; + to: Array; + useTLS: boolean; } | { type: 'slack'; webhookUrl: string; channel?: string; - iconEmoji?: string; username?: string; + iconEmoji?: string; + } | { + type: 'discord'; + webhookUrl: string; + username?: string; + avatarUrl?: string; + threadId?: string; + } | { + type: 'gotify'; + serverUrl: string; + token: string; + path?: string; + priority: number; + } | { + type: 'ntfy'; + serverUrl?: string; + topic: string; + priority: 'max' | 'high' | 'default' | 'low' | 'min'; + username?: string; + password?: string; + accessToken?: string; + } | { + type: 'pushover'; + userKey: string; + apiToken: string; + devices?: string; + priority: -1 | 0 | 1; + } | { + type: 'telegram'; + botToken: string; + chatId: string; + threadId?: string; + } | { + type: 'generic'; + url: string; + method: 'GET' | 'POST'; + contentType?: string; + headers?: Array; + useJson?: boolean; + titleKey?: string; + messageKey?: string; + } | { + type: 'custom'; + shoutrrrUrl: string; }; createdAt: number; - enabled: boolean; - id: number; - name: string; - type: 'custom' | 'discord' | 'email' | 'generic' | 'gotify' | 'ntfy' | 'pushover' | 'slack' | 'telegram'; updatedAt: number; }; }; @@ -4689,13 +4723,13 @@ export type GetUpdatesResponses = { */ 200: { currentVersion: string; - hasUpdate: boolean; latestVersion: string; + hasUpdate: boolean; missedReleases: Array<{ - body: string; - publishedAt: string; - url: string; version: string; + url: string; + publishedAt: string; + body: string; }>; }; }; @@ -4721,7 +4755,7 @@ export type GetRegistrationStatusResponses = { export type GetRegistrationStatusResponse = GetRegistrationStatusResponses[keyof GetRegistrationStatusResponses]; export type SetRegistrationStatusData = { - body?: { + body: { enabled: boolean; }; path?: never; @@ -4741,7 +4775,7 @@ export type SetRegistrationStatusResponses = { export type SetRegistrationStatusResponse = SetRegistrationStatusResponses[keyof SetRegistrationStatusResponses]; export type DownloadResticPasswordData = { - body?: { + body: { password: string; }; path?: never; diff --git a/app/client/components/app-sidebar.tsx b/app/client/components/app-sidebar.tsx index 5bb686ec..c27734d7 100644 --- a/app/client/components/app-sidebar.tsx +++ b/app/client/components/app-sidebar.tsx @@ -69,7 +69,7 @@ export function AppSidebar({ isInstanceAdmin }: Props) { return ( - + Zerobyte Logo @@ -171,7 +171,7 @@ export function AppSidebar({ isInstanceAdmin }: Props) { /> @@ -194,7 +194,7 @@ export function AppSidebar({ isInstanceAdmin }: Props) { )} - +
diff --git a/app/client/components/cron-input.tsx b/app/client/components/cron-input.tsx index c7e85e06..35e387ed 100644 --- a/app/client/components/cron-input.tsx +++ b/app/client/components/cron-input.tsx @@ -59,7 +59,7 @@ export function CronInput({ value, onChange, error }: CronInputProps) { {value && (
{isValid ? ( - + ) : ( )} diff --git a/app/client/components/file-browsers/snapshot-tree-browser.tsx b/app/client/components/file-browsers/snapshot-tree-browser.tsx index f9983dfd..d660c4c2 100644 --- a/app/client/components/file-browsers/snapshot-tree-browser.tsx +++ b/app/client/components/file-browsers/snapshot-tree-browser.tsx @@ -74,11 +74,7 @@ export const SnapshotTreeBrowser = ({ return await queryClient.ensureQueryData( listSnapshotFilesOptions({ path: { shortId: repositoryId, snapshotId }, - query: { - path, - offset: offset.toString(), - limit: pageSize.toString(), - }, + query: { path, offset: offset, limit: pageSize }, }), ); }, @@ -86,11 +82,7 @@ export const SnapshotTreeBrowser = ({ void queryClient.prefetchQuery( listSnapshotFilesOptions({ path: { shortId: repositoryId, snapshotId }, - query: { - path, - offset: "0", - limit: pageSize.toString(), - }, + query: { path, offset: 0, limit: pageSize }, }), ); }, diff --git a/app/client/components/file-browsers/volume-file-browser.tsx b/app/client/components/file-browsers/volume-file-browser.tsx index 2b39d03f..902b191a 100644 --- a/app/client/components/file-browsers/volume-file-browser.tsx +++ b/app/client/components/file-browsers/volume-file-browser.tsx @@ -24,7 +24,7 @@ export const VolumeFileBrowser = ({ volumeId, enabled = true, ...uiProps }: Volu return await queryClient.ensureQueryData( listFilesOptions({ path: { shortId: volumeId }, - query: { path, offset: offset?.toString() }, + query: { path, offset: offset }, }), ); }, diff --git a/app/client/components/grid-background.tsx b/app/client/components/grid-background.tsx index ec417f3a..9dbcd5d2 100644 --- a/app/client/components/grid-background.tsx +++ b/app/client/components/grid-background.tsx @@ -14,7 +14,7 @@ export function GridBackground({ children, className, containerClassName }: Grid className={cn( "pointer-events-none absolute inset-0 w-full h-full", "bg-[size:40px_40px]", - "bg-[linear-gradient(to_right,#e4e4e7_1px,transparent_1px),linear-gradient(to_bottom,#e4e4e7_1px,transparent_1px)]", + "bg-[linear-gradient(to_right,rgba(0,0,0,0.04)_1px,transparent_1px),linear-gradient(to_bottom,rgba(0,0,0,0.04)_1px,transparent_1px)]", "dark:bg-[linear-gradient(to_right,#262626_1px,transparent_1px),linear-gradient(to_bottom,#262626_1px,transparent_1px)]", "[mask-image:radial-gradient(ellipse_at_top,black_70%,transparent_100%)]", )} diff --git a/app/client/components/layout.tsx b/app/client/components/layout.tsx index 82600947..9bb084e2 100644 --- a/app/client/components/layout.tsx +++ b/app/client/components/layout.tsx @@ -10,6 +10,7 @@ import { DevPanelListener } from "./dev-panel-listener"; import { Outlet, useNavigate } from "@tanstack/react-router"; import { AppBreadcrumb } from "./app-breadcrumb"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; +import { ThemeToggle } from "./theme-toggle"; type Props = { loaderData: AppContext; @@ -35,23 +36,24 @@ export function Layout({ loaderData }: Props) {
-
+
{loaderData.user && ( -
+
{loaderData.user.name} + + + Toggle theme + + ); +} diff --git a/app/client/components/ui/button.tsx b/app/client/components/ui/button.tsx index 9f2411b3..6908fba4 100644 --- a/app/client/components/ui/button.tsx +++ b/app/client/components/ui/button.tsx @@ -11,13 +11,12 @@ const buttonVariants = cva( { variants: { variant: { - default: - "bg-transparent text-white hover:bg-[#3A3A3A]/80 border hover:border-white/40 dark:text-white dark:hover:bg-[#3A3A3A]/80", + default: "bg-transparent text-foreground hover:bg-muted border hover:border-border", primary: "bg-strong-accent text-white hover:bg-strong-accent/90 focus-visible:ring-strong-accent/50", destructive: - "border border-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/50 text-destructive hover:text-white", + "border border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground focus-visible:ring-destructive/50", outline: "border border-border bg-background hover:bg-accent hover:text-accent-foreground", - secondary: "bg-transparent text-white hover:bg-[#3A3A3A]/80 border dark:text-white dark:hover:bg-[#3A3A3A]/80", + secondary: "bg-transparent text-foreground hover:bg-muted border", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, diff --git a/app/client/components/ui/card.tsx b/app/client/components/ui/card.tsx index 00aa1e72..c31a6971 100644 --- a/app/client/components/ui/card.tsx +++ b/app/client/components/ui/card.tsx @@ -7,7 +7,7 @@ function Card({ className, children, interactive, ...props }: React.ComponentPro
- - - - - - - - + + + + + + + + {children}
diff --git a/app/client/components/ui/checkbox.tsx b/app/client/components/ui/checkbox.tsx index 0904f5d8..359968d6 100644 --- a/app/client/components/ui/checkbox.tsx +++ b/app/client/components/ui/checkbox.tsx @@ -1,10 +1,13 @@ -import type * as React from "react"; +import * as React from "react"; import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; import { CheckIcon } from "lucide-react"; +import { useWebHaptics } from "web-haptics/react"; import { cn } from "~/client/lib/utils"; function Checkbox({ className, ...props }: React.ComponentProps) { + const trigger = useWebHaptics().trigger; + return ( { + void trigger([{ duration: 8 }], { intensity: 0.3 }); + props.onCheckedChange?.(checked); + }} {...props} > = ({ code, filename }) => { return ( -
-
+
+
@@ -18,7 +18,7 @@ export const CodeBlock: React.FC = ({ code, filename }) => {
-				{code}
+				{code}
 			
); diff --git a/app/client/components/ui/sonner.tsx b/app/client/components/ui/sonner.tsx index 30810352..abcd1c27 100644 --- a/app/client/components/ui/sonner.tsx +++ b/app/client/components/ui/sonner.tsx @@ -1,4 +1,4 @@ -import { useTheme } from "next-themes"; +import { useTheme } from "~/client/components/theme-provider"; import { Toaster as Sonner, type ToasterProps } from "sonner"; const Toaster = ({ ...props }: ToasterProps) => { diff --git a/app/client/components/ui/switch.tsx b/app/client/components/ui/switch.tsx index e989fbf9..6a79a184 100644 --- a/app/client/components/ui/switch.tsx +++ b/app/client/components/ui/switch.tsx @@ -1,9 +1,12 @@ import * as SwitchPrimitive from "@radix-ui/react-switch"; -import type * as React from "react"; +import * as React from "react"; +import { useWebHaptics } from "web-haptics/react"; import { cn } from "~/client/lib/utils"; function Switch({ className, ...props }: React.ComponentProps) { + const trigger = useWebHaptics().trigger; + return ( { + void trigger([{ duration: 8 }], { intensity: 0.3 }); + props.onCheckedChange?.(checked); + }} {...props} > ) { @@ -17,7 +18,14 @@ function TabsList({ className, ...props }: React.ComponentProps) { +function TabsTrigger({ className, onClick, ...props }: React.ComponentProps) { + const { trigger } = useWebHaptics(); + + const handleClick: React.MouseEventHandler = (event) => { + void trigger([{ duration: 8 }], { intensity: 0.3 }); + onClick?.(event); + }; + return ( {props.children} {/* Left bracket - bottom tick */} - + {/* Right bracket - top tick */} - + {/* Right bracket - vertical line */} - + {/* Right bracket - bottom tick */} - + ); } diff --git a/app/client/hooks/__tests__/use-server-events.test.tsx b/app/client/hooks/__tests__/use-server-events.test.tsx new file mode 100644 index 00000000..f5a52a19 --- /dev/null +++ b/app/client/hooks/__tests__/use-server-events.test.tsx @@ -0,0 +1,132 @@ +import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { cleanup, render, screen } from "@testing-library/react"; +import { useEffect, useState } from "react"; +import { useServerEvents } from "../use-server-events"; + +class MockEventSource { + static instances: MockEventSource[] = []; + + public onerror: ((event: Event) => void) | null = null; + public close = mock(() => {}); + private listeners = new Map void>>(); + + constructor(public url: string) { + MockEventSource.instances.push(this); + } + + addEventListener(type: string, listener: EventListenerOrEventListenerObject) { + const listeners = this.listeners.get(type) ?? new Set<(event: Event) => void>(); + const callback = typeof listener === "function" ? listener : (event: Event) => listener.handleEvent(event); + listeners.add(callback); + this.listeners.set(type, listeners); + } + + emit(type: string, data: unknown) { + const event = new MessageEvent(type, { + data: JSON.stringify(data), + }); + for (const listener of this.listeners.get(type) ?? []) { + listener(event); + } + } + + static reset() { + MockEventSource.instances = []; + } +} + +const originalEventSource = globalThis.EventSource; +const originalConsoleInfo = console.info; +const originalConsoleError = console.error; + +const createTestQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + retry: false, + gcTime: Infinity, + }, + mutations: { + gcTime: Infinity, + }, + }, + }); + +const ConnectionConsumer = () => { + useServerEvents(); + return null; +}; + +const BackupCompletedListener = ({ scheduleId }: { scheduleId: string }) => { + const { addEventListener } = useServerEvents(); + const [status, setStatus] = useState("pending"); + + useEffect(() => { + const abortController = new AbortController(); + + addEventListener( + "backup:completed", + (event) => { + if (event.scheduleId === scheduleId) { + setStatus(event.status); + } + }, + { signal: abortController.signal }, + ); + + return () => abortController.abort(); + }, [addEventListener, scheduleId]); + + return
{status}
; +}; + +describe("useServerEvents", () => { + beforeEach(() => { + MockEventSource.reset(); + globalThis.EventSource = MockEventSource as unknown as typeof EventSource; + console.info = mock(() => {}); + console.error = mock(() => {}); + }); + + afterEach(() => { + cleanup(); + globalThis.EventSource = originalEventSource; + console.info = originalConsoleInfo; + console.error = originalConsoleError; + MockEventSource.reset(); + }); + + test("shares one EventSource across consumers and invalidates queries once on backup completion", async () => { + const queryClient = createTestQueryClient(); + const invalidateQueries = mock(async () => undefined); + const refetchQueries = mock(async () => undefined); + queryClient.invalidateQueries = invalidateQueries as typeof queryClient.invalidateQueries; + queryClient.refetchQueries = refetchQueries as typeof queryClient.refetchQueries; + + render( + + + + , + ); + + expect(MockEventSource.instances).toHaveLength(1); + + MockEventSource.instances[0]?.emit("backup:completed", { + organizationId: "default-org", + scheduleId: "0b9c940b", + volumeName: "synology", + repositoryName: "swiss-backup", + status: "success", + }); + + expect(await screen.findByText("success")).toBeTruthy(); + expect(invalidateQueries).toHaveBeenCalledTimes(1); + expect(refetchQueries).not.toHaveBeenCalled(); + + cleanup(); + + expect(MockEventSource.instances[0]?.close).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/client/hooks/use-mobile.ts b/app/client/hooks/use-mobile.ts index 21e5c1f0..8b812788 100644 --- a/app/client/hooks/use-mobile.ts +++ b/app/client/hooks/use-mobile.ts @@ -2,18 +2,20 @@ import * as React from "react"; const MOBILE_BREAKPOINT = 768; -export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState(undefined); - - React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - }; - mql.addEventListener("change", onChange); - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - return () => mql.removeEventListener("change", onChange); - }, []); - - return !!isMobile; +function subscribe(callback: () => void) { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + mql.addEventListener("change", callback); + return () => mql.removeEventListener("change", callback); +} + +function getSnapshot() { + return window.innerWidth < MOBILE_BREAKPOINT; +} + +function getServerSnapshot() { + return false; +} + +export function useIsMobile() { + return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); } diff --git a/app/client/hooks/use-server-events.ts b/app/client/hooks/use-server-events.ts index 88c789a7..62cb4ac8 100644 --- a/app/client/hooks/use-server-events.ts +++ b/app/client/hooks/use-server-events.ts @@ -1,4 +1,4 @@ -import { useQueryClient } from "@tanstack/react-query"; +import { type QueryClient, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useRef } from "react"; import { serverEventNames, type ServerEventPayloadMap } from "~/schemas/server-events"; @@ -16,6 +16,13 @@ type EventHandlerMap = { [K in ServerEventType]?: EventHandlerSet; }; +type SharedServerEventsState = { + eventSource: EventSource | null; + handlers: EventHandlerMap; + queryClient: QueryClient | null; + subscribers: number; +}; + const invalidatingEvents = new Set([ "backup:completed", "volume:updated", @@ -31,103 +38,151 @@ export type RestoreProgressEvent = ServerEventsPayloadMap["restore:progress"]; export type RestoreCompletedEvent = ServerEventsPayloadMap["restore:completed"]; export type BackupProgressEvent = ServerEventsPayloadMap["backup:progress"]; +const sharedState: SharedServerEventsState = { + eventSource: null, + handlers: {}, + queryClient: null, + subscribers: 0, +}; + const parseEventData = (event: Event): ServerEventsPayloadMap[T] => JSON.parse((event as MessageEvent).data) as ServerEventsPayloadMap[T]; +const isAbortError = (error: unknown): error is Error => error instanceof Error && error.name === "AbortError"; + +const emit = (eventName: T, data: ServerEventsPayloadMap[T]) => { + const handlers = sharedState.handlers[eventName] as EventHandlerSet | undefined; + handlers?.forEach((handler) => { + handler(data); + }); +}; + +const refreshQueriesForEvent = (eventName: ServerEventType) => { + if (!invalidatingEvents.has(eventName) || !sharedState.queryClient) { + return; + } + + void sharedState.queryClient.invalidateQueries().catch((error) => { + if (!isAbortError(error)) { + console.error(`[SSE] Failed to refresh queries after ${eventName}:`, error); + } + }); +}; + +const connectEventSource = (queryClient: QueryClient) => { + sharedState.queryClient = queryClient; + if (sharedState.eventSource) { + return; + } + + const eventSource = new EventSource("/api/v1/events"); + sharedState.eventSource = eventSource; + + eventSource.addEventListener("connected", (event) => { + const data = parseEventData<"connected">(event); + console.info("[SSE] Connected to server events"); + emit("connected", data); + }); + + eventSource.addEventListener("heartbeat", (event) => { + emit("heartbeat", parseEventData<"heartbeat">(event)); + }); + + for (const eventName of serverEventNames) { + eventSource.addEventListener(eventName, (event) => { + const data = parseEventData(event); + console.info(`[SSE] ${eventName}:`, data); + + refreshQueriesForEvent(eventName); + + if (eventName === "volume:status_changed") { + const statusData = data as ServerEventsPayloadMap["volume:status_changed"]; + emit("volume:status_changed", statusData); + emit("volume:updated", statusData); + return; + } + + emit(eventName, data); + }); + } + + eventSource.onerror = (error) => { + console.error("[SSE] Connection error:", error); + }; +}; + +const disconnectEventSource = () => { + if (!sharedState.eventSource) { + return; + } + + console.info("[SSE] Disconnecting from server events"); + sharedState.eventSource.close(); + sharedState.eventSource = null; + sharedState.queryClient = null; + sharedState.handlers = {}; +}; + +const addSharedEventListener = ( + eventName: T, + handler: EventHandler, + options?: { signal?: AbortSignal }, +) => { + if (options?.signal?.aborted) { + return () => {}; + } + + const existingHandlers = sharedState.handlers[eventName] as EventHandlerSet | undefined; + const eventHandlers = existingHandlers ?? new Set>(); + eventHandlers.add(handler); + sharedState.handlers[eventName] = eventHandlers as EventHandlerMap[T]; + + const unsubscribe = () => { + const handlers = sharedState.handlers[eventName] as EventHandlerSet | undefined; + handlers?.delete(handler); + if (handlers && handlers.size === 0) { + delete sharedState.handlers[eventName]; + } + if (options?.signal) { + options.signal.removeEventListener("abort", unsubscribe); + } + }; + + if (options?.signal) { + options.signal.addEventListener("abort", unsubscribe, { once: true }); + } + + return unsubscribe; +}; + /** * Hook to listen to Server-Sent Events (SSE) from the backend * Automatically handles cache invalidation for backup and volume events */ export function useServerEvents() { const queryClient = useQueryClient(); - const eventSourceRef = useRef(null); - const handlersRef = useRef({}); - const emit = useCallback((eventName: T, data: ServerEventsPayloadMap[T]) => { - const handlers = handlersRef.current[eventName] as EventHandlerSet | undefined; - handlers?.forEach((handler) => { - handler(data); - }); - }, []); + const addEventListener = useCallback(addSharedEventListener, []); + const hasMountedRef = useRef(false); useEffect(() => { - const eventSource = new EventSource("/api/v1/events"); - eventSourceRef.current = eventSource; - - eventSource.addEventListener("connected", (event) => { - const data = parseEventData<"connected">(event); - console.info("[SSE] Connected to server events"); - emit("connected", data); - }); - - eventSource.addEventListener("heartbeat", (event) => { - emit("heartbeat", parseEventData<"heartbeat">(event)); - }); - - for (const eventName of serverEventNames) { - eventSource.addEventListener(eventName, (event) => { - const data = parseEventData(event); - console.info(`[SSE] ${eventName}:`, data); - - if (invalidatingEvents.has(eventName)) { - void queryClient.invalidateQueries(); - } - - if (eventName === "backup:completed") { - void queryClient.refetchQueries(); - } - - if (eventName === "volume:status_changed") { - const statusData = data as ServerEventsPayloadMap["volume:status_changed"]; - emit("volume:status_changed", statusData); - emit("volume:updated", statusData); - return; - } - - emit(eventName, data); - }); + connectEventSource(queryClient); + if (!hasMountedRef.current) { + sharedState.subscribers += 1; + hasMountedRef.current = true; } - eventSource.onerror = (error) => { - console.error("[SSE] Connection error:", error); - }; - return () => { - console.info("[SSE] Disconnecting from server events"); - eventSource.close(); - eventSourceRef.current = null; + if (!hasMountedRef.current) { + return; + } + + hasMountedRef.current = false; + sharedState.subscribers = Math.max(0, sharedState.subscribers - 1); + if (sharedState.subscribers === 0) { + disconnectEventSource(); + } }; - }, [emit, queryClient]); - - const addEventListener = useCallback( - (eventName: T, handler: EventHandler, options?: { signal?: AbortSignal }) => { - if (options?.signal?.aborted) { - return () => {}; - } - - const existingHandlers = handlersRef.current[eventName] as EventHandlerSet | undefined; - const eventHandlers = existingHandlers ?? new Set>(); - eventHandlers.add(handler); - handlersRef.current[eventName] = eventHandlers as EventHandlerMap[T]; - - const unsubscribe = () => { - const handlers = handlersRef.current[eventName] as EventHandlerSet | undefined; - handlers?.delete(handler); - if (handlers && handlers.size === 0) { - delete handlersRef.current[eventName]; - } - if (options?.signal) { - options.signal.removeEventListener("abort", unsubscribe); - } - }; - - if (options?.signal) { - options.signal.addEventListener("abort", unsubscribe, { once: true }); - } - - return unsubscribe; - }, - [], - ); + }, [queryClient]); return { addEventListener }; } diff --git a/app/client/lib/auth-errors.ts b/app/client/lib/auth-errors.ts deleted file mode 100644 index a2f0f4c6..00000000 --- a/app/client/lib/auth-errors.ts +++ /dev/null @@ -1,45 +0,0 @@ -export type LoginErrorCode = - | "ACCOUNT_LINK_REQUIRED" - | "EMAIL_NOT_VERIFIED" - | "INVITE_REQUIRED" - | "BANNED_USER" - | "SSO_LOGIN_FAILED"; - -const VALID_ERROR_CODES: Set = new Set([ - "ACCOUNT_LINK_REQUIRED", - "EMAIL_NOT_VERIFIED", - "INVITE_REQUIRED", - "BANNED_USER", - "SSO_LOGIN_FAILED", -]); - -export function decodeLoginError(error?: string): LoginErrorCode | null { - if (!error) { - return null; - } - - const code = decodeURIComponent(error); - - if (VALID_ERROR_CODES.has(code as LoginErrorCode)) { - return code as LoginErrorCode; - } - - return null; -} - -export function getLoginErrorDescription(errorCode: LoginErrorCode | null): string | null { - switch (errorCode) { - case "ACCOUNT_LINK_REQUIRED": - return "Your account exists but is not linked to this SSO provider. Sign in with username/password first, then enable auto linking in your provider settings or contact your administrator."; - case "EMAIL_NOT_VERIFIED": - return "Your identity provider did not mark your email as verified."; - case "INVITE_REQUIRED": - return "Access is invite-only. Ask an organization admin to send you an invitation before signing in with SSO."; - case "BANNED_USER": - return "You have been banned from this application. Please contact support if you believe this is an error."; - case "SSO_LOGIN_FAILED": - return "SSO authentication failed. Please try again."; - default: - return null; - } -} diff --git a/app/client/lib/sso-errors.ts b/app/client/lib/sso-errors.ts new file mode 100644 index 00000000..a87b867d --- /dev/null +++ b/app/client/lib/sso-errors.ts @@ -0,0 +1,20 @@ +import { LOGIN_ERROR_CODES, type LoginErrorCode } from "~/lib/sso-errors"; + +export type { LoginErrorCode } from "~/lib/sso-errors"; +export { getLoginErrorDescription } from "~/lib/sso-errors"; + +const VALID_ERROR_CODES = new Set(LOGIN_ERROR_CODES); + +export function decodeLoginError(error?: string): LoginErrorCode | null { + if (!error) { + return null; + } + + const code = decodeURIComponent(error); + + if (VALID_ERROR_CODES.has(code as LoginErrorCode)) { + return code as LoginErrorCode; + } + + return null; +} diff --git a/app/client/modules/auth/routes/__tests__/login.test.tsx b/app/client/modules/auth/routes/__tests__/login.test.tsx index 88bf7faa..f8a6e7aa 100644 --- a/app/client/modules/auth/routes/__tests__/login.test.tsx +++ b/app/client/modules/auth/routes/__tests__/login.test.tsx @@ -69,7 +69,7 @@ describe("LoginPage", () => { expect( await screen.findByText( - "Your account exists but is not linked to this SSO provider. Sign in with username/password first, then enable auto linking in your provider settings or contact your administrator.", + "SSO sign-in was blocked because this email already belongs to another user in this instance. Contact your administrator to resolve the account conflict.", ), ).toBeTruthy(); }); diff --git a/app/client/modules/auth/routes/login.tsx b/app/client/modules/auth/routes/login.tsx index b4b01cd6..58663c1b 100644 --- a/app/client/modules/auth/routes/login.tsx +++ b/app/client/modules/auth/routes/login.tsx @@ -1,5 +1,4 @@ -import { arktypeResolver } from "@hookform/resolvers/arktype"; -import { type } from "arktype"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -10,20 +9,23 @@ import { Input } from "~/client/components/ui/input"; import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "~/client/components/ui/input-otp"; import { Label } from "~/client/components/ui/label"; import { authClient } from "~/client/lib/auth-client"; -import { decodeLoginError, getLoginErrorDescription } from "~/client/lib/auth-errors"; +import { decodeLoginError, getLoginErrorDescription } from "~/client/lib/sso-errors"; import { ResetPasswordDialog } from "../components/reset-password-dialog"; import { useNavigate } from "@tanstack/react-router"; import { normalizeUsername } from "~/lib/username"; -import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; -import { getPublicSsoProvidersOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { cn } from "~/client/lib/utils"; +import { SsoLoginSection } from "~/client/modules/sso/components/sso-login-section"; +import { z } from "zod"; -const loginSchema = type({ - username: "2<=string<=50", - password: "string>=1", +const loginSchema = z.object({ + username: z + .string() + .min(2, "Username must be at least 2 characters") + .max(50, "Username must be at most 50 characters"), + password: z.string().min(1, "Password is required"), }); -type LoginFormValues = typeof loginSchema.inferIn; +type LoginFormValues = z.input; type LoginPageProps = { error?: string; @@ -38,14 +40,10 @@ export function LoginPage({ error }: LoginPageProps = {}) { const [isVerifying2FA, setIsVerifying2FA] = useState(false); const [trustDevice, setTrustDevice] = useState(false); const errorCode = decodeLoginError(error); - const errorDescription = getLoginErrorDescription(errorCode); - - const { data: ssoProviders } = useSuspenseQuery({ - ...getPublicSsoProvidersOptions(), - }); + const errorDescription = errorCode ? getLoginErrorDescription(errorCode) : null; const form = useForm({ - resolver: arktypeResolver(loginSchema), + resolver: zodResolver(loginSchema), defaultValues: { username: "", password: "", @@ -129,27 +127,6 @@ export function LoginPage({ error }: LoginPageProps = {}) { form.reset(); }; - const ssoLoginMutation = useMutation({ - mutationFn: async (providerId: string) => { - const callbackPath = "/login"; - const { data, error } = await authClient.signIn.sso({ - providerId: providerId, - callbackURL: callbackPath, - errorCallbackURL: "/api/v1/auth/login-error", - }); - if (error) throw error; - - return data; - }, - onSuccess: (data) => { - window.location.href = data.url; - }, - onError: (error) => { - console.error(error); - toast.error("SSO Login failed", { description: error.message }); - }, - }); - if (requires2FA) { return ( @@ -265,26 +242,7 @@ export function LoginPage({ error }: LoginPageProps = {}) { - {ssoProviders.providers.length > 0 && ( -
-

Alternative Sign-in

-
- {ssoProviders.providers.map((provider) => ( - - ))} -
-
- )} +
diff --git a/app/client/modules/auth/routes/onboarding.tsx b/app/client/modules/auth/routes/onboarding.tsx index 8eddace4..e477837e 100644 --- a/app/client/modules/auth/routes/onboarding.tsx +++ b/app/client/modules/auth/routes/onboarding.tsx @@ -1,5 +1,4 @@ -import { arktypeResolver } from "@hookform/resolvers/arktype"; -import { type } from "arktype"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { @@ -19,24 +18,28 @@ import { authClient } from "~/client/lib/auth-client"; import { useState } from "react"; import { useNavigate } from "@tanstack/react-router"; import { normalizeUsername } from "~/lib/username"; +import { z } from "zod"; export const clientMiddleware = [authMiddleware]; -const onboardingSchema = type({ - username: type("2<=string<=30").pipe(normalizeUsername), - email: type("string.email").pipe((str) => str.trim().toLowerCase()), - password: "string>=8", - confirmPassword: "string>=1", +const onboardingSchema = z.object({ + username: z.string().min(2).max(30).transform(normalizeUsername), + email: z + .string() + .email() + .transform((str) => str.trim().toLowerCase()), + password: z.string().min(8), + confirmPassword: z.string().min(1), }); -type OnboardingFormValues = typeof onboardingSchema.inferIn; +type OnboardingFormValues = z.input; export function OnboardingPage() { const navigate = useNavigate(); const [submitting, setSubmitting] = useState(false); const form = useForm({ - resolver: arktypeResolver(onboardingSchema), + resolver: zodResolver(onboardingSchema), defaultValues: { username: "", password: "", diff --git a/app/client/modules/backups/components/backup-card.tsx b/app/client/modules/backups/components/backup-card.tsx index 2bf7b337..af583ea3 100644 --- a/app/client/modules/backups/components/backup-card.tsx +++ b/app/client/modules/backups/components/backup-card.tsx @@ -32,21 +32,21 @@ export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => {
- Schedule -
- + Schedule +
+ {schedule.cronExpression}
- Last backup -
- {formatTimeAgo(schedule.lastBackupAt)} + Last backup +
+ {formatTimeAgo(schedule.lastBackupAt)}
- Next backup -
- + Next backup +
+ {formatShortDateTime(schedule.nextBackupAt)}
diff --git a/app/client/modules/backups/components/backup-progress-card.tsx b/app/client/modules/backups/components/backup-progress-card.tsx index 053ac28d..8381d26c 100644 --- a/app/client/modules/backups/components/backup-progress-card.tsx +++ b/app/client/modules/backups/components/backup-progress-card.tsx @@ -46,6 +46,7 @@ export const BackupProgressCard = ({ scheduleShortId, initialProgress }: Props) const currentFile = progress?.current_files?.[0] || ""; const fileName = currentFile.split("/").pop() || currentFile; const speed = progress ? formatBytes(progress.bytes_done / progress.seconds_elapsed) : null; + const eta = progress?.seconds_remaining ? formatDuration(progress.seconds_remaining) : null; return ( @@ -96,6 +97,10 @@ export const BackupProgressCard = ({ scheduleShortId, initialProgress }: Props) {progress ? (progress.seconds_elapsed > 0 ? `${speed?.text} ${speed?.unit}/s` : "Calculating...") : "—"}

+
+

ETA

+

{progress ? (eta ?? "Calculating...") : "—"}

+
diff --git a/app/client/modules/backups/components/create-schedule-form/advanced-section.tsx b/app/client/modules/backups/components/create-schedule-form/advanced-section.tsx new file mode 100644 index 00000000..72d3f831 --- /dev/null +++ b/app/client/modules/backups/components/create-schedule-form/advanced-section.tsx @@ -0,0 +1,35 @@ +import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form"; +import { Textarea } from "~/client/components/ui/textarea"; +import type { UseFormReturn } from "react-hook-form"; +import type { InternalFormValues } from "./types"; + +type AdvancedSectionProps = { + form: UseFormReturn; +}; + +export const AdvancedSection = ({ form }: AdvancedSectionProps) => { + return ( + ( + + Custom restic parameters + +