From e7f0a2828db5cc5d18c30df4edc4dbfd2519c327 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:52:47 +0100 Subject: [PATCH 1/8] feat: mirror repositories (#95) * feat: mirror repositories feat: mirror backup repositories * chore: pr feedbacks --- .../api-client/@tanstack/react-query.gen.ts | 57 +- app/client/api-client/sdk.gen.ts | 36 +- app/client/api-client/types.gen.ts | 227 +++++ app/client/components/snapshots-table.tsx | 160 ++-- app/client/hooks/use-server-events.ts | 33 +- .../components/schedule-mirrors-config.tsx | 352 ++++++++ .../modules/backups/routes/backup-details.tsx | 13 +- .../modules/repositories/tabs/snapshots.tsx | 8 +- app/drizzle/0018_bizarre_zzzax.sql | 139 +++ app/drizzle/0019_heavy_shen.sql | 1 + app/drizzle/meta/0018_snapshot.json | 740 ++++++++++++++++ app/drizzle/meta/0019_snapshot.json | 792 ++++++++++++++++++ app/drizzle/meta/_journal.json | 276 +++--- app/server/core/events.ts | 8 + app/server/db/schema.ts | 39 +- .../modules/backups/backups.controller.ts | 28 +- app/server/modules/backups/backups.dto.ts | 85 ++ app/server/modules/backups/backups.service.ts | 190 ++++- .../modules/events/events.controller.ts | 24 + .../repositories/repositories.controller.ts | 2 + .../modules/repositories/repositories.dto.ts | 1 + app/server/utils/backend-compatibility.ts | 148 ++++ app/server/utils/restic.ts | 91 +- bun.lock | 1 + 24 files changed, 3220 insertions(+), 231 deletions(-) create mode 100644 app/client/modules/backups/components/schedule-mirrors-config.tsx create mode 100644 app/drizzle/0018_bizarre_zzzax.sql create mode 100644 app/drizzle/0019_heavy_shen.sql create mode 100644 app/drizzle/meta/0018_snapshot.json create mode 100644 app/drizzle/meta/0019_snapshot.json create mode 100644 app/server/utils/backend-compatibility.ts diff --git a/app/client/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts index 5beda44..411da6d 100644 --- a/app/client/api-client/@tanstack/react-query.gen.ts +++ b/app/client/api-client/@tanstack/react-query.gen.ts @@ -3,8 +3,8 @@ import { type DefaultError, queryOptions, type UseMutationOptions } from '@tanstack/react-query'; import { client } from '../client.gen'; -import { browseFilesystem, changePassword, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteVolume, doctorRepository, downloadResticPassword, getBackupSchedule, getBackupScheduleForVolume, getContainersUsingVolume, getMe, getNotificationDestination, getRepository, getScheduleNotifications, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, restoreSnapshot, runBackupNow, runForget, stopBackup, testConnection, testNotificationDestination, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleNotifications, updateVolume } from '../sdk.gen'; -import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetContainersUsingVolumeData, GetContainersUsingVolumeResponse, GetMeData, GetMeResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetRepositoryData, GetRepositoryResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, LoginData, LoginResponse, LogoutData, LogoutResponse, MountVolumeData, MountVolumeResponse, RegisterData, RegisterResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen'; +import { browseFilesystem, changePassword, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteVolume, doctorRepository, downloadResticPassword, getBackupSchedule, getBackupScheduleForVolume, getContainersUsingVolume, getMe, getMirrorCompatibility, getNotificationDestination, getRepository, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, restoreSnapshot, runBackupNow, runForget, stopBackup, testConnection, testNotificationDestination, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateVolume } from '../sdk.gen'; +import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetContainersUsingVolumeData, GetContainersUsingVolumeResponse, GetMeData, GetMeResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetRepositoryData, GetRepositoryResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, LoginData, LoginResponse, LogoutData, LogoutResponse, MountVolumeData, MountVolumeResponse, RegisterData, RegisterResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen'; /** * Register a new user @@ -755,6 +755,59 @@ export const updateScheduleNotificationsMutation = (options?: Partial) => createQueryKey("getScheduleMirrors", options); + +/** + * Get mirror repository assignments for a backup schedule + */ +export const getScheduleMirrorsOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getScheduleMirrors({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getScheduleMirrorsQueryKey(options) +}); + +/** + * Update mirror repository assignments for a backup schedule + */ +export const updateScheduleMirrorsMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await updateScheduleMirrors({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getMirrorCompatibilityQueryKey = (options: Options) => createQueryKey("getMirrorCompatibility", options); + +/** + * Get mirror compatibility info for all repositories relative to a backup schedule's primary repository + */ +export const getMirrorCompatibilityOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getMirrorCompatibility({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getMirrorCompatibilityQueryKey(options) +}); + export const listNotificationDestinationsQueryKey = (options?: Options) => createQueryKey("listNotificationDestinations", options); /** diff --git a/app/client/api-client/sdk.gen.ts b/app/client/api-client/sdk.gen.ts index 9f5afe4..1b8c032 100644 --- a/app/client/api-client/sdk.gen.ts +++ b/app/client/api-client/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetRepositoryData, GetRepositoryResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen'; +import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetRepositoryData, GetRepositoryResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen'; export type Options = Options2 & { /** @@ -476,6 +476,40 @@ export const updateScheduleNotifications = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/v1/backups/{scheduleId}/mirrors', + ...options + }); +}; + +/** + * Update mirror repository assignments for a backup schedule + */ +export const updateScheduleMirrors = (options: Options) => { + return (options.client ?? client).put({ + url: '/api/v1/backups/{scheduleId}/mirrors', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Get mirror compatibility info for all repositories relative to a backup schedule's primary repository + */ +export const getMirrorCompatibility = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/v1/backups/{scheduleId}/mirrors/compatibility', + ...options + }); +}; + /** * List all notification destinations */ diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 7e82ed3..bb15e40 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -1124,6 +1124,7 @@ export type ListSnapshotsResponses = { paths: Array; short_id: string; size: number; + tags: Array; time: number; }>; }; @@ -1170,6 +1171,7 @@ export type GetSnapshotDetailsResponses = { paths: Array; short_id: string; size: number; + tags: Array; time: number; }; }; @@ -2112,6 +2114,231 @@ export type UpdateScheduleNotificationsResponses = { export type UpdateScheduleNotificationsResponse = UpdateScheduleNotificationsResponses[keyof UpdateScheduleNotificationsResponses]; +export type GetScheduleMirrorsData = { + body?: never; + path: { + scheduleId: string; + }; + query?: never; + url: '/api/v1/backups/{scheduleId}/mirrors'; +}; + +export type GetScheduleMirrorsResponses = { + /** + * List of mirror repository assignments for the schedule + */ + 200: Array<{ + createdAt: number; + enabled: boolean; + lastCopyAt: number | null; + lastCopyError: string | null; + lastCopyStatus: 'error' | 'success' | null; + repository: { + compressionMode: 'auto' | 'max' | 'off' | null; + config: { + accessKeyId: string; + backend: 'r2'; + bucket: string; + endpoint: string; + secretAccessKey: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + accessKeyId: string; + backend: 's3'; + bucket: string; + endpoint: string; + secretAccessKey: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + accountKey: string; + accountName: string; + backend: 'azure'; + container: string; + customPassword?: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + } | { + backend: 'gcs'; + bucket: string; + credentialsJson: string; + projectId: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + backend: 'local'; + name: string; + customPassword?: string; + isExistingRepository?: boolean; + path?: string; + } | { + backend: 'rclone'; + path: string; + remote: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + backend: 'rest'; + url: string; + customPassword?: string; + isExistingRepository?: boolean; + password?: string; + path?: string; + username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + customPassword?: string; + isExistingRepository?: boolean; + }; + createdAt: number; + id: string; + lastChecked: number | null; + lastError: string | null; + name: string; + shortId: string; + status: 'error' | 'healthy' | 'unknown' | null; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + updatedAt: number; + }; + repositoryId: string; + scheduleId: number; + }>; +}; + +export type GetScheduleMirrorsResponse = GetScheduleMirrorsResponses[keyof GetScheduleMirrorsResponses]; + +export type UpdateScheduleMirrorsData = { + body?: { + mirrors: Array<{ + enabled: boolean; + repositoryId: string; + }>; + }; + path: { + scheduleId: string; + }; + query?: never; + url: '/api/v1/backups/{scheduleId}/mirrors'; +}; + +export type UpdateScheduleMirrorsResponses = { + /** + * Mirror assignments updated successfully + */ + 200: Array<{ + createdAt: number; + enabled: boolean; + lastCopyAt: number | null; + lastCopyError: string | null; + lastCopyStatus: 'error' | 'success' | null; + repository: { + compressionMode: 'auto' | 'max' | 'off' | null; + config: { + accessKeyId: string; + backend: 'r2'; + bucket: string; + endpoint: string; + secretAccessKey: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + accessKeyId: string; + backend: 's3'; + bucket: string; + endpoint: string; + secretAccessKey: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + accountKey: string; + accountName: string; + backend: 'azure'; + container: string; + customPassword?: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + } | { + backend: 'gcs'; + bucket: string; + credentialsJson: string; + projectId: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + backend: 'local'; + name: string; + customPassword?: string; + isExistingRepository?: boolean; + path?: string; + } | { + backend: 'rclone'; + path: string; + remote: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + backend: 'rest'; + url: string; + customPassword?: string; + isExistingRepository?: boolean; + password?: string; + path?: string; + username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + customPassword?: string; + isExistingRepository?: boolean; + }; + createdAt: number; + id: string; + lastChecked: number | null; + lastError: string | null; + name: string; + shortId: string; + status: 'error' | 'healthy' | 'unknown' | null; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + updatedAt: number; + }; + repositoryId: string; + scheduleId: number; + }>; +}; + +export type UpdateScheduleMirrorsResponse = UpdateScheduleMirrorsResponses[keyof UpdateScheduleMirrorsResponses]; + +export type GetMirrorCompatibilityData = { + body?: never; + path: { + scheduleId: string; + }; + query?: never; + url: '/api/v1/backups/{scheduleId}/mirrors/compatibility'; +}; + +export type GetMirrorCompatibilityResponses = { + /** + * List of repositories with their mirror compatibility status + */ + 200: Array<{ + compatible: boolean; + reason: string | null; + repositoryId: string; + }>; +}; + +export type GetMirrorCompatibilityResponse = GetMirrorCompatibilityResponses[keyof GetMirrorCompatibilityResponses]; + export type ListNotificationDestinationsData = { body?: never; path?: never; diff --git a/app/client/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx index c03059d..ebd80fb 100644 --- a/app/client/components/snapshots-table.tsx +++ b/app/client/components/snapshots-table.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Calendar, Clock, Database, FolderTree, HardDrive, Trash2 } from "lucide-react"; -import { useNavigate } from "react-router"; +import { Link, useNavigate } from "react-router"; import { toast } from "sonner"; import { ByteSize } from "~/client/components/bytes-size"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table"; @@ -18,18 +18,17 @@ import { AlertDialogTitle, } from "~/client/components/ui/alert-dialog"; import { formatDuration } from "~/utils/utils"; -import type { ListSnapshotsResponse } from "../api-client"; import { deleteSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen"; import { parseError } from "~/client/lib/errors"; - -type Snapshot = ListSnapshotsResponse[number]; +import type { BackupSchedule, Snapshot } from "../lib/types"; type Props = { snapshots: Snapshot[]; + backups: BackupSchedule[]; repositoryName: string; }; -export const SnapshotsTable = ({ snapshots, repositoryName }: Props) => { +export const SnapshotsTable = ({ snapshots, repositoryName, backups }: Props) => { const navigate = useNavigate(); const queryClient = useQueryClient(); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); @@ -76,6 +75,7 @@ export const SnapshotsTable = ({ snapshots, repositoryName }: Props) => { Snapshot ID + Schedule Date & Time Size Duration @@ -84,71 +84,91 @@ export const SnapshotsTable = ({ snapshots, repositoryName }: Props) => { - {snapshots.map((snapshot) => ( - handleRowClick(snapshot.short_id)} - > - -
- - {snapshot.short_id} -
-
- -
- - {new Date(snapshot.time).toLocaleString()} -
-
- -
- - - - -
-
- -
- - {formatDuration(snapshot.duration / 1000)} -
-
- -
- - - - - {snapshot.paths.length} {snapshot.paths.length === 1 ? "path" : "paths"} - - - -
- {snapshot.paths.map((path) => ( -
- {path} -
- ))} -
-
-
-
-
- - - -
- ))} + {snapshots.map((snapshot) => { + const backupIds = snapshot.tags.map(Number).filter((tag) => !Number.isNaN(tag)); + const backup = backups.find((b) => backupIds.includes(b.id)); + + return ( + handleRowClick(snapshot.short_id)} + > + +
+ + {snapshot.short_id} +
+
+ +
+ e.stopPropagation()} + className="hover:underline" + > + {backup ? backup.id : "-"} + + +
+
+ +
+ + {new Date(snapshot.time).toLocaleString()} +
+
+ +
+ + + + +
+
+ +
+ + {formatDuration(snapshot.duration / 1000)} +
+
+ +
+ + + + + {snapshot.paths.length} {snapshot.paths.length === 1 ? "path" : "paths"} + + + +
+ {snapshot.paths.map((path) => ( +
+ {path} +
+ ))} +
+
+
+
+
+ + + +
+ ); + })}
diff --git a/app/client/hooks/use-server-events.ts b/app/client/hooks/use-server-events.ts index 43c7205..d7c0377 100644 --- a/app/client/hooks/use-server-events.ts +++ b/app/client/hooks/use-server-events.ts @@ -9,7 +9,9 @@ type ServerEventType = | "backup:completed" | "volume:mounted" | "volume:unmounted" - | "volume:updated"; + | "volume:updated" + | "mirror:started" + | "mirror:completed"; export interface BackupEvent { scheduleId: number; @@ -35,6 +37,14 @@ export interface VolumeEvent { volumeName: string; } +export interface MirrorEvent { + scheduleId: number; + repositoryId: string; + repositoryName: string; + status?: "success" | "error"; + error?: string; +} + type EventHandler = (data: unknown) => void; /** @@ -125,6 +135,27 @@ export function useServerEvents() { }); }); + eventSource.addEventListener("mirror:started", (e) => { + const data = JSON.parse(e.data) as MirrorEvent; + console.log("[SSE] Mirror copy started:", data); + + handlersRef.current.get("mirror:started")?.forEach((handler) => { + handler(data); + }); + }); + + eventSource.addEventListener("mirror:completed", (e) => { + const data = JSON.parse(e.data) as MirrorEvent; + console.log("[SSE] Mirror copy completed:", data); + + // Invalidate queries to refresh mirror status in the UI + queryClient.invalidateQueries(); + + handlersRef.current.get("mirror:completed")?.forEach((handler) => { + handler(data); + }); + }); + eventSource.onerror = (error) => { console.error("[SSE] Connection error:", error); }; diff --git a/app/client/modules/backups/components/schedule-mirrors-config.tsx b/app/client/modules/backups/components/schedule-mirrors-config.tsx new file mode 100644 index 0000000..419f5e0 --- /dev/null +++ b/app/client/modules/backups/components/schedule-mirrors-config.tsx @@ -0,0 +1,352 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import { Copy, Plus, Trash2 } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; +import { Button } from "~/client/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; +import { Switch } from "~/client/components/ui/switch"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table"; +import { Badge } from "~/client/components/ui/badge"; +import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip"; +import { + getScheduleMirrorsOptions, + getMirrorCompatibilityOptions, + updateScheduleMirrorsMutation, +} from "~/client/api-client/@tanstack/react-query.gen"; +import { parseError } from "~/client/lib/errors"; +import type { Repository } from "~/client/lib/types"; +import { RepositoryIcon } from "~/client/components/repository-icon"; +import { StatusDot } from "~/client/components/status-dot"; +import { formatDistanceToNow } from "date-fns"; +import { Link } from "react-router"; +import { cn } from "~/client/lib/utils"; + +type Props = { + scheduleId: number; + primaryRepositoryId: string; + repositories: Repository[]; +}; + +type MirrorAssignment = { + repositoryId: string; + enabled: boolean; + lastCopyAt: number | null; + lastCopyStatus: "success" | "error" | null; + lastCopyError: string | null; +}; + +export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, repositories }: Props) => { + const [assignments, setAssignments] = useState>(new Map()); + const [hasChanges, setHasChanges] = useState(false); + const [isAddingNew, setIsAddingNew] = useState(false); + + const { data: currentMirrors } = useQuery({ + ...getScheduleMirrorsOptions({ path: { scheduleId: scheduleId.toString() } }), + }); + + const { data: compatibility } = useQuery({ + ...getMirrorCompatibilityOptions({ path: { scheduleId: scheduleId.toString() } }), + }); + + const updateMirrors = useMutation({ + ...updateScheduleMirrorsMutation(), + onSuccess: () => { + toast.success("Mirror settings saved successfully"); + setHasChanges(false); + }, + onError: (error) => { + toast.error("Failed to save mirror settings", { + description: parseError(error)?.message, + }); + }, + }); + + const compatibilityMap = useMemo(() => { + const map = new Map(); + if (compatibility) { + for (const item of compatibility) { + map.set(item.repositoryId, { compatible: item.compatible, reason: item.reason }); + } + } + return map; + }, [compatibility]); + + useEffect(() => { + if (currentMirrors && !hasChanges) { + const map = new Map(); + for (const mirror of currentMirrors) { + map.set(mirror.repositoryId, { + repositoryId: mirror.repositoryId, + enabled: mirror.enabled, + lastCopyAt: mirror.lastCopyAt, + lastCopyStatus: mirror.lastCopyStatus, + lastCopyError: mirror.lastCopyError, + }); + } + + setAssignments(map); + } + }, [currentMirrors, hasChanges]); + + const addRepository = (repositoryId: string) => { + const newAssignments = new Map(assignments); + newAssignments.set(repositoryId, { + repositoryId, + enabled: true, + lastCopyAt: null, + lastCopyStatus: null, + lastCopyError: null, + }); + + setAssignments(newAssignments); + setHasChanges(true); + setIsAddingNew(false); + }; + + const removeRepository = (repositoryId: string) => { + const newAssignments = new Map(assignments); + newAssignments.delete(repositoryId); + setAssignments(newAssignments); + setHasChanges(true); + }; + + const toggleEnabled = (repositoryId: string) => { + const assignment = assignments.get(repositoryId); + if (!assignment) return; + + const newAssignments = new Map(assignments); + newAssignments.set(repositoryId, { + ...assignment, + enabled: !assignment.enabled, + }); + + setAssignments(newAssignments); + setHasChanges(true); + }; + + const handleSave = () => { + const mirrorsList = Array.from(assignments.values()).map((a) => ({ + repositoryId: a.repositoryId, + enabled: a.enabled, + })); + updateMirrors.mutate({ + path: { scheduleId: scheduleId.toString() }, + body: { + mirrors: mirrorsList, + }, + }); + }; + + const handleReset = () => { + if (currentMirrors) { + const map = new Map(); + for (const mirror of currentMirrors) { + map.set(mirror.repositoryId, { + repositoryId: mirror.repositoryId, + enabled: mirror.enabled, + lastCopyAt: mirror.lastCopyAt, + lastCopyStatus: mirror.lastCopyStatus, + lastCopyError: mirror.lastCopyError, + }); + } + setAssignments(map); + setHasChanges(false); + } + }; + + const selectableRepositories = + repositories?.filter((r) => { + if (r.id === primaryRepositoryId) return false; + if (assignments.has(r.id)) return false; + return true; + }) || []; + + const hasAvailableRepositories = selectableRepositories.some((r) => { + const compat = compatibilityMap.get(r.id); + return compat?.compatible !== false; + }); + + const assignedRepositories = Array.from(assignments.keys()) + .map((id) => repositories?.find((r) => r.id === id)) + .filter((r) => r !== undefined); + + const getStatusVariant = (status: "success" | "error" | null) => { + if (status === "success") return "success"; + if (status === "error") return "error"; + return "neutral"; + }; + + const getStatusLabel = (assignment: MirrorAssignment) => { + if (assignment.lastCopyStatus === "error" && assignment.lastCopyError) { + return assignment.lastCopyError; + } + if (assignment.lastCopyStatus === "success") { + return "Last copy successful"; + } + return "Never copied"; + }; + + return ( + + +
+
+ + + Mirror Repositories + + + Configure secondary repositories where snapshots will be automatically copied after each backup + +
+ {!isAddingNew && selectableRepositories.length > 0 && ( + + )} +
+
+ + {isAddingNew && ( +
+ + +
+ )} + + {assignedRepositories.length === 0 ? ( +
+ +

No mirror repositories configured for this schedule.

+

Click "Add mirror" to replicate backups to additional repositories.

+
+ ) : ( +
+ + + + Repository + Enabled + Last Copy + + + + + {assignedRepositories.map((repository) => { + const assignment = assignments.get(repository.id); + if (!assignment) return null; + + return ( + + +
+ + + {repository.name} + + + {repository.type} + +
+
+ + toggleEnabled(repository.id)} + /> + + + {assignment.lastCopyAt ? ( +
+ + + {formatDistanceToNow(new Date(assignment.lastCopyAt), { addSuffix: true })} + +
+ ) : ( + Never + )} +
+ + + +
+ ); + })} +
+
+
+ )} + + {hasChanges && ( +
+ + +
+ )} +
+
+ ); +}; diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx index 727e939..be07779 100644 --- a/app/client/modules/backups/routes/backup-details.tsx +++ b/app/client/modules/backups/routes/backup-details.tsx @@ -29,8 +29,9 @@ import { ScheduleSummary } from "../components/schedule-summary"; import type { Route } from "./+types/backup-details"; import { SnapshotFileBrowser } from "../components/snapshot-file-browser"; import { SnapshotTimeline } from "../components/snapshot-timeline"; -import { getBackupSchedule, listNotificationDestinations } from "~/client/api-client"; +import { getBackupSchedule, listNotificationDestinations, listRepositories } from "~/client/api-client"; import { ScheduleNotificationsConfig } from "../components/schedule-notifications-config"; +import { ScheduleMirrorsConfig } from "../components/schedule-mirrors-config"; import { cn } from "~/client/lib/utils"; export const handle = { @@ -53,10 +54,11 @@ export function meta(_: Route.MetaArgs) { export const clientLoader = async ({ params }: Route.LoaderArgs) => { const schedule = await getBackupSchedule({ path: { scheduleId: params.id } }); const notifs = await listNotificationDestinations(); + const repos = await listRepositories(); if (!schedule.data) return redirect("/backups"); - return { schedule: schedule.data, notifs: notifs.data }; + return { schedule: schedule.data, notifs: notifs.data, repos: repos.data }; }; export default function ScheduleDetailsPage({ params, loaderData }: Route.ComponentProps) { @@ -226,6 +228,13 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
+
+ +
{ initialData: [], }); + const schedules = useQuery({ + ...listBackupSchedulesOptions(), + }); + const filteredSnapshots = data.filter((snapshot: Snapshot) => { if (!searchQuery) return true; const searchLower = searchQuery.toLowerCase(); @@ -132,7 +136,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => { ) : ( - + )}
diff --git a/app/drizzle/0018_bizarre_zzzax.sql b/app/drizzle/0018_bizarre_zzzax.sql new file mode 100644 index 0000000..0b72b21 --- /dev/null +++ b/app/drizzle/0018_bizarre_zzzax.sql @@ -0,0 +1,139 @@ +DROP TABLE IF EXISTS `backup_schedule_mirrors_table`;--> statement-breakpoint +CREATE TABLE `backup_schedule_mirrors_table` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `schedule_id` integer NOT NULL, + `repository_id` text NOT NULL, + `enabled` integer DEFAULT true NOT NULL, + `last_copy_at` integer, + `last_copy_status` text, + `last_copy_error` text, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + FOREIGN KEY (`schedule_id`) REFERENCES `backup_schedules_table`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`repository_id`) REFERENCES `repositories_table`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_app_metadata` ( + `key` text PRIMARY KEY NOT NULL, + `value` text NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL +); +--> statement-breakpoint +INSERT INTO `__new_app_metadata`("key", "value", "created_at", "updated_at") SELECT "key", "value", "created_at", "updated_at" FROM `app_metadata`;--> statement-breakpoint +DROP TABLE `app_metadata`;--> statement-breakpoint +ALTER TABLE `__new_app_metadata` RENAME TO `app_metadata`;--> statement-breakpoint +CREATE TABLE `__new_backup_schedule_notifications_table` ( + `schedule_id` integer NOT NULL, + `destination_id` integer NOT NULL, + `notify_on_start` integer DEFAULT false NOT NULL, + `notify_on_success` integer DEFAULT false NOT NULL, + `notify_on_failure` integer DEFAULT true NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + PRIMARY KEY(`schedule_id`, `destination_id`), + FOREIGN KEY (`schedule_id`) REFERENCES `backup_schedules_table`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`destination_id`) REFERENCES `notification_destinations_table`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_backup_schedule_notifications_table`("schedule_id", "destination_id", "notify_on_start", "notify_on_success", "notify_on_failure", "created_at") SELECT "schedule_id", "destination_id", "notify_on_start", "notify_on_success", "notify_on_failure", "created_at" FROM `backup_schedule_notifications_table`;--> statement-breakpoint +DROP TABLE `backup_schedule_notifications_table`;--> statement-breakpoint +ALTER TABLE `__new_backup_schedule_notifications_table` RENAME TO `backup_schedule_notifications_table`;--> statement-breakpoint +CREATE TABLE `__new_backup_schedules_table` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `volume_id` integer NOT NULL, + `repository_id` text NOT NULL, + `enabled` integer DEFAULT true NOT NULL, + `cron_expression` text NOT NULL, + `retention_policy` text, + `exclude_patterns` text DEFAULT '[]', + `include_patterns` text DEFAULT '[]', + `last_backup_at` integer, + `last_backup_status` text, + `last_backup_error` text, + `next_backup_at` integer, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + FOREIGN KEY (`volume_id`) REFERENCES `volumes_table`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`repository_id`) REFERENCES `repositories_table`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_backup_schedules_table`("id", "volume_id", "repository_id", "enabled", "cron_expression", "retention_policy", "exclude_patterns", "include_patterns", "last_backup_at", "last_backup_status", "last_backup_error", "next_backup_at", "created_at", "updated_at") SELECT "id", "volume_id", "repository_id", "enabled", "cron_expression", "retention_policy", "exclude_patterns", "include_patterns", "last_backup_at", "last_backup_status", "last_backup_error", "next_backup_at", "created_at", "updated_at" FROM `backup_schedules_table`;--> statement-breakpoint +DROP TABLE `backup_schedules_table`;--> statement-breakpoint +ALTER TABLE `__new_backup_schedules_table` RENAME TO `backup_schedules_table`;--> statement-breakpoint +CREATE TABLE `__new_notification_destinations_table` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `enabled` integer DEFAULT true NOT NULL, + `type` text NOT NULL, + `config` text NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL +); +--> statement-breakpoint +INSERT INTO `__new_notification_destinations_table`("id", "name", "enabled", "type", "config", "created_at", "updated_at") SELECT "id", "name", "enabled", "type", "config", "created_at", "updated_at" FROM `notification_destinations_table`;--> statement-breakpoint +DROP TABLE `notification_destinations_table`;--> statement-breakpoint +ALTER TABLE `__new_notification_destinations_table` RENAME TO `notification_destinations_table`;--> statement-breakpoint +CREATE UNIQUE INDEX `notification_destinations_table_name_unique` ON `notification_destinations_table` (`name`);--> statement-breakpoint +CREATE TABLE `__new_repositories_table` ( + `id` text PRIMARY KEY NOT NULL, + `short_id` text NOT NULL, + `name` text NOT NULL, + `type` text NOT NULL, + `config` text NOT NULL, + `compression_mode` text DEFAULT 'auto', + `status` text DEFAULT 'unknown', + `last_checked` integer, + `last_error` text, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL +); +--> statement-breakpoint +INSERT INTO `__new_repositories_table`("id", "short_id", "name", "type", "config", "compression_mode", "status", "last_checked", "last_error", "created_at", "updated_at") SELECT "id", "short_id", "name", "type", "config", "compression_mode", "status", "last_checked", "last_error", "created_at", "updated_at" FROM `repositories_table`;--> statement-breakpoint +DROP TABLE `repositories_table`;--> statement-breakpoint +ALTER TABLE `__new_repositories_table` RENAME TO `repositories_table`;--> statement-breakpoint +CREATE UNIQUE INDEX `repositories_table_short_id_unique` ON `repositories_table` (`short_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `repositories_table_name_unique` ON `repositories_table` (`name`);--> statement-breakpoint +CREATE TABLE `__new_sessions_table` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` integer NOT NULL, + `expires_at` integer NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users_table`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_sessions_table`("id", "user_id", "expires_at", "created_at") SELECT "id", "user_id", "expires_at", "created_at" FROM `sessions_table`;--> statement-breakpoint +DROP TABLE `sessions_table`;--> statement-breakpoint +ALTER TABLE `__new_sessions_table` RENAME TO `sessions_table`;--> statement-breakpoint +CREATE TABLE `__new_users_table` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `username` text NOT NULL, + `password_hash` text NOT NULL, + `has_downloaded_restic_password` integer DEFAULT false NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL +); +--> statement-breakpoint +INSERT INTO `__new_users_table`("id", "username", "password_hash", "has_downloaded_restic_password", "created_at", "updated_at") SELECT "id", "username", "password_hash", "has_downloaded_restic_password", "created_at", "updated_at" FROM `users_table`;--> statement-breakpoint +DROP TABLE `users_table`;--> statement-breakpoint +ALTER TABLE `__new_users_table` RENAME TO `users_table`;--> statement-breakpoint +CREATE UNIQUE INDEX `users_table_username_unique` ON `users_table` (`username`);--> statement-breakpoint +CREATE TABLE `__new_volumes_table` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `short_id` text NOT NULL, + `name` text NOT NULL, + `type` text NOT NULL, + `status` text DEFAULT 'unmounted' NOT NULL, + `last_error` text, + `last_health_check` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `config` text NOT NULL, + `auto_remount` integer DEFAULT true NOT NULL +); +--> statement-breakpoint +INSERT INTO `__new_volumes_table`("id", "short_id", "name", "type", "status", "last_error", "last_health_check", "created_at", "updated_at", "config", "auto_remount") SELECT "id", "short_id", "name", "type", "status", "last_error", "last_health_check", "created_at", "updated_at", "config", "auto_remount" FROM `volumes_table`;--> statement-breakpoint +DROP TABLE `volumes_table`;--> statement-breakpoint +ALTER TABLE `__new_volumes_table` RENAME TO `volumes_table`;--> statement-breakpoint +CREATE UNIQUE INDEX `volumes_table_short_id_unique` ON `volumes_table` (`short_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `volumes_table_name_unique` ON `volumes_table` (`name`); +PRAGMA foreign_keys=ON;--> statement-breakpoint diff --git a/app/drizzle/0019_heavy_shen.sql b/app/drizzle/0019_heavy_shen.sql new file mode 100644 index 0000000..98b747b --- /dev/null +++ b/app/drizzle/0019_heavy_shen.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `backup_schedule_mirrors_table_schedule_id_repository_id_unique` ON `backup_schedule_mirrors_table` (`schedule_id`,`repository_id`); \ No newline at end of file diff --git a/app/drizzle/meta/0018_snapshot.json b/app/drizzle/meta/0018_snapshot.json new file mode 100644 index 0000000..dcc5188 --- /dev/null +++ b/app/drizzle/meta/0018_snapshot.json @@ -0,0 +1,740 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "121ef03c-eb5a-4b97-b2f1-4add6adfb080", + "prevId": "d0bfd316-b8f5-459b-ab17-0ce679479321", + "tables": { + "app_metadata": { + "name": "app_metadata", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedule_mirrors_table": { + "name": "backup_schedule_mirrors_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_copy_at": { + "name": "last_copy_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_copy_status": { + "name": "last_copy_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_copy_error": { + "name": "last_copy_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": { + "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk", + "tableFrom": "backup_schedule_mirrors_table", + "tableTo": "backup_schedules_table", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedule_mirrors_table", + "tableTo": "repositories_table", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedule_notifications_table": { + "name": "backup_schedule_notifications_table", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "destination_id": { + "name": "destination_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notify_on_start": { + "name": "notify_on_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_success": { + "name": "notify_on_success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_failure": { + "name": "notify_on_failure", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": { + "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "tableTo": "backup_schedules_table", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": { + "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "tableTo": "notification_destinations_table", + "columnsFrom": ["destination_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "backup_schedule_notifications_table_schedule_id_destination_id_pk": { + "columns": ["schedule_id", "destination_id"], + "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedules_table": { + "name": "backup_schedules_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "volume_id": { + "name": "volume_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exclude_patterns": { + "name": "exclude_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "include_patterns": { + "name": "include_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "last_backup_at": { + "name": "last_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_status": { + "name": "last_backup_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_error": { + "name": "last_backup_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_backup_at": { + "name": "next_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedules_table_volume_id_volumes_table_id_fk": { + "name": "backup_schedules_table_volume_id_volumes_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "volumes_table", + "columnsFrom": ["volume_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedules_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "repositories_table", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_destinations_table": { + "name": "notification_destinations_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "notification_destinations_table_name_unique": { + "name": "notification_destinations_table_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories_table": { + "name": "repositories_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compression_mode": { + "name": "compression_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'auto'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'unknown'" + }, + "last_checked": { + "name": "last_checked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "repositories_table_short_id_unique": { + "name": "repositories_table_short_id_unique", + "columns": ["short_id"], + "isUnique": true + }, + "repositories_table_name_unique": { + "name": "repositories_table_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions_table": { + "name": "sessions_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_table_user_id_users_table_id_fk": { + "name": "sessions_table_user_id_users_table_id_fk", + "tableFrom": "sessions_table", + "tableTo": "users_table", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "has_downloaded_restic_password": { + "name": "has_downloaded_restic_password", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": ["username"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes_table": { + "name": "volumes_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unmounted'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_health_check": { + "name": "last_health_check", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto_remount": { + "name": "auto_remount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "volumes_table_short_id_unique": { + "name": "volumes_table_short_id_unique", + "columns": ["short_id"], + "isUnique": true + }, + "volumes_table_name_unique": { + "name": "volumes_table_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/app/drizzle/meta/0019_snapshot.json b/app/drizzle/meta/0019_snapshot.json new file mode 100644 index 0000000..ff1f037 --- /dev/null +++ b/app/drizzle/meta/0019_snapshot.json @@ -0,0 +1,792 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "dedfb246-68e7-4590-af52-6476eb2999d1", + "prevId": "121ef03c-eb5a-4b97-b2f1-4add6adfb080", + "tables": { + "app_metadata": { + "name": "app_metadata", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedule_mirrors_table": { + "name": "backup_schedule_mirrors_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_copy_at": { + "name": "last_copy_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_copy_status": { + "name": "last_copy_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_copy_error": { + "name": "last_copy_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "backup_schedule_mirrors_table_schedule_id_repository_id_unique": { + "name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique", + "columns": [ + "schedule_id", + "repository_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": { + "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk", + "tableFrom": "backup_schedule_mirrors_table", + "tableTo": "backup_schedules_table", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedule_mirrors_table", + "tableTo": "repositories_table", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedule_notifications_table": { + "name": "backup_schedule_notifications_table", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "destination_id": { + "name": "destination_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notify_on_start": { + "name": "notify_on_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_success": { + "name": "notify_on_success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_failure": { + "name": "notify_on_failure", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": { + "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "tableTo": "backup_schedules_table", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": { + "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "tableTo": "notification_destinations_table", + "columnsFrom": [ + "destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "backup_schedule_notifications_table_schedule_id_destination_id_pk": { + "columns": [ + "schedule_id", + "destination_id" + ], + "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedules_table": { + "name": "backup_schedules_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "volume_id": { + "name": "volume_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exclude_patterns": { + "name": "exclude_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "include_patterns": { + "name": "include_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "last_backup_at": { + "name": "last_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_status": { + "name": "last_backup_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_error": { + "name": "last_backup_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_backup_at": { + "name": "next_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedules_table_volume_id_volumes_table_id_fk": { + "name": "backup_schedules_table_volume_id_volumes_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "volumes_table", + "columnsFrom": [ + "volume_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedules_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "repositories_table", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_destinations_table": { + "name": "notification_destinations_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "notification_destinations_table_name_unique": { + "name": "notification_destinations_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories_table": { + "name": "repositories_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compression_mode": { + "name": "compression_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'auto'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'unknown'" + }, + "last_checked": { + "name": "last_checked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "repositories_table_short_id_unique": { + "name": "repositories_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "repositories_table_name_unique": { + "name": "repositories_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions_table": { + "name": "sessions_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_table_user_id_users_table_id_fk": { + "name": "sessions_table_user_id_users_table_id_fk", + "tableFrom": "sessions_table", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "has_downloaded_restic_password": { + "name": "has_downloaded_restic_password", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes_table": { + "name": "volumes_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unmounted'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_health_check": { + "name": "last_health_check", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto_remount": { + "name": "auto_remount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "volumes_table_short_id_unique": { + "name": "volumes_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "volumes_table_name_unique": { + "name": "volumes_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/app/drizzle/meta/_journal.json b/app/drizzle/meta/_journal.json index e11599d..4ae3a51 100644 --- a/app/drizzle/meta/_journal.json +++ b/app/drizzle/meta/_journal.json @@ -1,132 +1,146 @@ { - "version": "7", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1755765658194, - "tag": "0000_known_madelyne_pryor", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1755775437391, - "tag": "0001_far_frank_castle", - "breakpoints": true - }, - { - "idx": 2, - "version": "6", - "when": 1756930554198, - "tag": "0002_cheerful_randall", - "breakpoints": true - }, - { - "idx": 3, - "version": "6", - "when": 1758653407064, - "tag": "0003_mature_hellcat", - "breakpoints": true - }, - { - "idx": 4, - "version": "6", - "when": 1758961535488, - "tag": "0004_wealthy_tomas", - "breakpoints": true - }, - { - "idx": 5, - "version": "6", - "when": 1759416698274, - "tag": "0005_simple_alice", - "breakpoints": true - }, - { - "idx": 6, - "version": "6", - "when": 1760734377440, - "tag": "0006_secret_micromacro", - "breakpoints": true - }, - { - "idx": 7, - "version": "6", - "when": 1761224911352, - "tag": "0007_watery_sersi", - "breakpoints": true - }, - { - "idx": 8, - "version": "6", - "when": 1761414054481, - "tag": "0008_silent_lady_bullseye", - "breakpoints": true - }, - { - "idx": 9, - "version": "6", - "when": 1762095226041, - "tag": "0009_little_adam_warlock", - "breakpoints": true - }, - { - "idx": 10, - "version": "6", - "when": 1762610065889, - "tag": "0010_perfect_proemial_gods", - "breakpoints": true - }, - { - "idx": 11, - "version": "6", - "when": 1763644043601, - "tag": "0011_familiar_stone_men", - "breakpoints": true - }, - { - "idx": 12, - "version": "6", - "when": 1764100562084, - "tag": "0012_add_short_ids", - "breakpoints": true - }, - { - "idx": 13, - "version": "6", - "when": 1764182159797, - "tag": "0013_elite_sprite", - "breakpoints": true - }, - { - "idx": 14, - "version": "6", - "when": 1764182405089, - "tag": "0014_wild_echo", - "breakpoints": true - }, - { - "idx": 15, - "version": "6", - "when": 1764182465287, - "tag": "0015_jazzy_sersi", - "breakpoints": true - }, - { - "idx": 16, - "version": "6", - "when": 1764194697035, - "tag": "0016_fix-timestamps-to-ms", - "breakpoints": true - }, - { - "idx": 17, - "version": "6", - "when": 1764357897219, - "tag": "0017_fix-compression-modes", - "breakpoints": true - } - ] -} + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1755765658194, + "tag": "0000_known_madelyne_pryor", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1755775437391, + "tag": "0001_far_frank_castle", + "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1756930554198, + "tag": "0002_cheerful_randall", + "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1758653407064, + "tag": "0003_mature_hellcat", + "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1758961535488, + "tag": "0004_wealthy_tomas", + "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1759416698274, + "tag": "0005_simple_alice", + "breakpoints": true + }, + { + "idx": 6, + "version": "6", + "when": 1760734377440, + "tag": "0006_secret_micromacro", + "breakpoints": true + }, + { + "idx": 7, + "version": "6", + "when": 1761224911352, + "tag": "0007_watery_sersi", + "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1761414054481, + "tag": "0008_silent_lady_bullseye", + "breakpoints": true + }, + { + "idx": 9, + "version": "6", + "when": 1762095226041, + "tag": "0009_little_adam_warlock", + "breakpoints": true + }, + { + "idx": 10, + "version": "6", + "when": 1762610065889, + "tag": "0010_perfect_proemial_gods", + "breakpoints": true + }, + { + "idx": 11, + "version": "6", + "when": 1763644043601, + "tag": "0011_familiar_stone_men", + "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1764100562084, + "tag": "0012_add_short_ids", + "breakpoints": true + }, + { + "idx": 13, + "version": "6", + "when": 1764182159797, + "tag": "0013_elite_sprite", + "breakpoints": true + }, + { + "idx": 14, + "version": "6", + "when": 1764182405089, + "tag": "0014_wild_echo", + "breakpoints": true + }, + { + "idx": 15, + "version": "6", + "when": 1764182465287, + "tag": "0015_jazzy_sersi", + "breakpoints": true + }, + { + "idx": 16, + "version": "6", + "when": 1764194697035, + "tag": "0016_fix-timestamps-to-ms", + "breakpoints": true + }, + { + "idx": 17, + "version": "6", + "when": 1764357897219, + "tag": "0017_fix-compression-modes", + "breakpoints": true + }, + { + "idx": 18, + "version": "6", + "when": 1764619898949, + "tag": "0018_bizarre_zzzax", + "breakpoints": true + }, + { + "idx": 19, + "version": "6", + "when": 1764790151212, + "tag": "0019_heavy_shen", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/app/server/core/events.ts b/app/server/core/events.ts index 33fbb42..2e7f43d 100644 --- a/app/server/core/events.ts +++ b/app/server/core/events.ts @@ -24,6 +24,14 @@ interface ServerEvents { repositoryName: string; status: "success" | "error" | "stopped" | "warning"; }) => void; + "mirror:started": (data: { scheduleId: number; repositoryId: string; repositoryName: string }) => void; + "mirror:completed": (data: { + scheduleId: number; + repositoryId: string; + repositoryName: string; + status: "success" | "error"; + error?: string; + }) => void; "volume:mounted": (data: { volumeName: string }) => void; "volume:unmounted": (data: { volumeName: string }) => void; "volume:updated": (data: { volumeName: string }) => void; diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index 9b0ac0c..6ef9037 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -1,5 +1,5 @@ import { relations, sql } from "drizzle-orm"; -import { int, integer, sqliteTable, text, primaryKey } from "drizzle-orm/sqlite-core"; +import { int, integer, sqliteTable, text, primaryKey, uniqueIndex, unique } from "drizzle-orm/sqlite-core"; import type { CompressionMode, RepositoryBackend, repositoryConfigSchema, RepositoryStatus } from "~/schemas/restic"; import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes"; import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications"; @@ -93,6 +93,7 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", { createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`), updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`), }); + export const backupScheduleRelations = relations(backupSchedulesTable, ({ one, many }) => ({ volume: one(volumesTable, { fields: [backupSchedulesTable.volumeId], @@ -103,6 +104,7 @@ export const backupScheduleRelations = relations(backupSchedulesTable, ({ one, m references: [repositoriesTable.id], }), notifications: many(backupScheduleNotificationsTable), + mirrors: many(backupScheduleMirrorsTable), })); export type BackupSchedule = typeof backupSchedulesTable.$inferSelect; @@ -154,6 +156,41 @@ export const backupScheduleNotificationRelations = relations(backupScheduleNotif })); export type BackupScheduleNotification = typeof backupScheduleNotificationsTable.$inferSelect; +/** + * Backup Schedule Mirrors Junction Table (Many-to-Many) + * Allows copying snapshots to secondary repositories after backup completes + */ +export const backupScheduleMirrorsTable = sqliteTable( + "backup_schedule_mirrors_table", + { + id: int().primaryKey({ autoIncrement: true }), + scheduleId: int("schedule_id") + .notNull() + .references(() => backupSchedulesTable.id, { onDelete: "cascade" }), + repositoryId: text("repository_id") + .notNull() + .references(() => repositoriesTable.id, { onDelete: "cascade" }), + enabled: int("enabled", { mode: "boolean" }).notNull().default(true), + lastCopyAt: int("last_copy_at", { mode: "number" }), + lastCopyStatus: text("last_copy_status").$type<"success" | "error">(), + lastCopyError: text("last_copy_error"), + createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`), + }, + (table) => [unique().on(table.scheduleId, table.repositoryId)], +); + +export const backupScheduleMirrorRelations = relations(backupScheduleMirrorsTable, ({ one }) => ({ + schedule: one(backupSchedulesTable, { + fields: [backupScheduleMirrorsTable.scheduleId], + references: [backupSchedulesTable.id], + }), + repository: one(repositoriesTable, { + fields: [backupScheduleMirrorsTable.repositoryId], + references: [repositoriesTable.id], + }), +})); +export type BackupScheduleMirror = typeof backupScheduleMirrorsTable.$inferSelect; + /** * App Metadata Table * Used for storing key-value pairs like migration checkpoints diff --git a/app/server/modules/backups/backups.controller.ts b/app/server/modules/backups/backups.controller.ts index ed6fa4a..0bcc93e 100644 --- a/app/server/modules/backups/backups.controller.ts +++ b/app/server/modules/backups/backups.controller.ts @@ -12,6 +12,10 @@ import { stopBackupDto, updateBackupScheduleDto, updateBackupScheduleBody, + getScheduleMirrorsDto, + updateScheduleMirrorsDto, + updateScheduleMirrorsBody, + getMirrorCompatibilityDto, type CreateBackupScheduleDto, type DeleteBackupScheduleDto, type GetBackupScheduleDto, @@ -21,6 +25,9 @@ import { type RunForgetDto, type StopBackupDto, type UpdateBackupScheduleDto, + type GetScheduleMirrorsDto, + type UpdateScheduleMirrorsDto, + type GetMirrorCompatibilityDto, } from "./backups.dto"; import { backupsService } from "./backups.service"; import { @@ -113,4 +120,23 @@ export const backupScheduleController = new Hono() return c.json(assignments, 200); }, - ); + ) + .get("/:scheduleId/mirrors", getScheduleMirrorsDto, async (c) => { + const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10); + const mirrors = await backupsService.getMirrors(scheduleId); + + return c.json(mirrors, 200); + }) + .put("/:scheduleId/mirrors", updateScheduleMirrorsDto, validator("json", updateScheduleMirrorsBody), async (c) => { + const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10); + const body = c.req.valid("json"); + const mirrors = await backupsService.updateMirrors(scheduleId, body); + + return c.json(mirrors, 200); + }) + .get("/:scheduleId/mirrors/compatibility", getMirrorCompatibilityDto, async (c) => { + const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10); + const compatibility = await backupsService.getMirrorCompatibility(scheduleId); + + return c.json(compatibility, 200); + }); diff --git a/app/server/modules/backups/backups.dto.ts b/app/server/modules/backups/backups.dto.ts index 95c4d49..464891b 100644 --- a/app/server/modules/backups/backups.dto.ts +++ b/app/server/modules/backups/backups.dto.ts @@ -37,6 +37,19 @@ const backupScheduleSchema = type({ }), ); +const scheduleMirrorSchema = type({ + scheduleId: "number", + repositoryId: "string", + enabled: "boolean", + lastCopyAt: "number | null", + lastCopyStatus: "'success' | 'error' | null", + lastCopyError: "string | null", + createdAt: "number", + repository: repositorySchema, +}); + +export type ScheduleMirrorDto = typeof scheduleMirrorSchema.infer; + /** * List all backup schedules */ @@ -276,3 +289,75 @@ export const runForgetDto = describeRoute({ }, }, }); + +export const getScheduleMirrorsResponse = scheduleMirrorSchema.array(); +export type GetScheduleMirrorsDto = typeof getScheduleMirrorsResponse.infer; + +export const getScheduleMirrorsDto = describeRoute({ + description: "Get mirror repository assignments for a backup schedule", + operationId: "getScheduleMirrors", + tags: ["Backups"], + responses: { + 200: { + description: "List of mirror repository assignments for the schedule", + content: { + "application/json": { + schema: resolver(getScheduleMirrorsResponse), + }, + }, + }, + }, +}); + +export const updateScheduleMirrorsBody = type({ + mirrors: type({ + repositoryId: "string", + enabled: "boolean", + }).array(), +}); + +export type UpdateScheduleMirrorsBody = typeof updateScheduleMirrorsBody.infer; + +export const updateScheduleMirrorsResponse = scheduleMirrorSchema.array(); +export type UpdateScheduleMirrorsDto = typeof updateScheduleMirrorsResponse.infer; + +export const updateScheduleMirrorsDto = describeRoute({ + description: "Update mirror repository assignments for a backup schedule", + operationId: "updateScheduleMirrors", + tags: ["Backups"], + responses: { + 200: { + description: "Mirror assignments updated successfully", + content: { + "application/json": { + schema: resolver(updateScheduleMirrorsResponse), + }, + }, + }, + }, +}); + +const mirrorCompatibilitySchema = type({ + repositoryId: "string", + compatible: "boolean", + reason: "string | null", +}); + +export const getMirrorCompatibilityResponse = mirrorCompatibilitySchema.array(); +export type GetMirrorCompatibilityDto = typeof getMirrorCompatibilityResponse.infer; + +export const getMirrorCompatibilityDto = describeRoute({ + description: "Get mirror compatibility info for all repositories relative to a backup schedule's primary repository", + operationId: "getMirrorCompatibility", + tags: ["Backups"], + responses: { + 200: { + description: "List of repositories with their mirror compatibility status", + content: { + "application/json": { + schema: resolver(getMirrorCompatibilityResponse), + }, + }, + }, + }, +}); diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts index 8b627f3..df9e42b 100644 --- a/app/server/modules/backups/backups.service.ts +++ b/app/server/modules/backups/backups.service.ts @@ -3,15 +3,16 @@ import cron from "node-cron"; import { CronExpressionParser } from "cron-parser"; import { NotFoundError, BadRequestError, ConflictError } from "http-errors-enhanced"; import { db } from "../../db/db"; -import { backupSchedulesTable, repositoriesTable, volumesTable } from "../../db/schema"; +import { backupSchedulesTable, backupScheduleMirrorsTable, repositoriesTable, volumesTable } from "../../db/schema"; import { restic } from "../../utils/restic"; import { logger } from "../../utils/logger"; import { getVolumePath } from "../volumes/helpers"; -import type { CreateBackupScheduleBody, UpdateBackupScheduleBody } from "./backups.dto"; +import type { CreateBackupScheduleBody, UpdateBackupScheduleBody, UpdateScheduleMirrorsBody } from "./backups.dto"; import { toMessage } from "../../utils/errors"; import { serverEvents } from "../../core/events"; import { notificationsService } from "../notifications/notifications.service"; import { repoMutex } from "../../core/repository-mutex"; +import { checkMirrorCompatibility, getIncompatibleMirrorError } from "~/server/utils/backend-compatibility"; const runningBackups = new Map(); @@ -266,19 +267,25 @@ const executeBackup = async (scheduleId: number, manual = false) => { void runForget(schedule.id); } + copyToMirrors(scheduleId, repository, schedule.retentionPolicy).catch((error) => { + logger.error(`Background mirror copy failed for schedule ${scheduleId}: ${toMessage(error)}`); + }); + + const finalStatus = exitCode === 0 ? "success" : "warning"; + const nextBackupAt = calculateNextRun(schedule.cronExpression); await db .update(backupSchedulesTable) .set({ lastBackupAt: Date.now(), - lastBackupStatus: exitCode === 0 ? "success" : "warning", + lastBackupStatus: finalStatus, lastBackupError: null, nextBackupAt: nextBackupAt, updatedAt: Date.now(), }) .where(eq(backupSchedulesTable.id, scheduleId)); - if (exitCode !== 0) { + if (finalStatus === "warning") { logger.warn(`Backup completed with warnings for volume ${volume.name} to repository ${repository.name}`); } else { logger.info(`Backup completed successfully for volume ${volume.name} to repository ${repository.name}`); @@ -288,11 +295,11 @@ const executeBackup = async (scheduleId: number, manual = false) => { scheduleId, volumeName: volume.name, repositoryName: repository.name, - status: exitCode === 0 ? "success" : "warning", + status: finalStatus, }); notificationsService - .sendBackupNotification(scheduleId, exitCode === 0 ? "success" : "warning", { + .sendBackupNotification(scheduleId, finalStatus === "success" ? "success" : "warning", { volumeName: volume.name, repositoryName: repository.name, }) @@ -421,6 +428,174 @@ const runForget = async (scheduleId: number) => { logger.info(`Retention policy applied successfully for schedule ${scheduleId}`); }; +const getMirrors = async (scheduleId: number) => { + const schedule = await db.query.backupSchedulesTable.findFirst({ + where: eq(backupSchedulesTable.id, scheduleId), + }); + + if (!schedule) { + throw new NotFoundError("Backup schedule not found"); + } + + const mirrors = await db.query.backupScheduleMirrorsTable.findMany({ + where: eq(backupScheduleMirrorsTable.scheduleId, scheduleId), + with: { repository: true }, + }); + + return mirrors; +}; + +const updateMirrors = async (scheduleId: number, data: UpdateScheduleMirrorsBody) => { + const schedule = await db.query.backupSchedulesTable.findFirst({ + where: eq(backupSchedulesTable.id, scheduleId), + with: { repository: true }, + }); + + if (!schedule) { + throw new NotFoundError("Backup schedule not found"); + } + + for (const mirror of data.mirrors) { + if (mirror.repositoryId === schedule.repositoryId) { + throw new BadRequestError("Cannot add the primary repository as a mirror"); + } + + const repo = await db.query.repositoriesTable.findFirst({ + where: eq(repositoriesTable.id, mirror.repositoryId), + }); + + if (!repo) { + throw new NotFoundError(`Repository ${mirror.repositoryId} not found`); + } + + const compatibility = await checkMirrorCompatibility(schedule.repository.config, repo.config, repo.id); + + if (!compatibility.compatible) { + throw new BadRequestError( + getIncompatibleMirrorError(repo.name, schedule.repository.config.backend, repo.config.backend), + ); + } + } + + await db.delete(backupScheduleMirrorsTable).where(eq(backupScheduleMirrorsTable.scheduleId, scheduleId)); + + if (data.mirrors.length > 0) { + await db.insert(backupScheduleMirrorsTable).values( + data.mirrors.map((mirror) => ({ + scheduleId, + repositoryId: mirror.repositoryId, + enabled: mirror.enabled, + })), + ); + } + + return getMirrors(scheduleId); +}; + +const copyToMirrors = async ( + scheduleId: number, + sourceRepository: { id: string; config: (typeof repositoriesTable.$inferSelect)["config"] }, + retentionPolicy: (typeof backupSchedulesTable.$inferSelect)["retentionPolicy"], +) => { + const mirrors = await db.query.backupScheduleMirrorsTable.findMany({ + where: eq(backupScheduleMirrorsTable.scheduleId, scheduleId), + with: { repository: true }, + }); + + const enabledMirrors = mirrors.filter((m) => m.enabled); + + if (enabledMirrors.length === 0) { + return; + } + + logger.info( + `[Background] Copying snapshots to ${enabledMirrors.length} mirror repositories for schedule ${scheduleId}`, + ); + + for (const mirror of enabledMirrors) { + try { + logger.info(`[Background] Copying to mirror repository: ${mirror.repository.name}`); + + serverEvents.emit("mirror:started", { + scheduleId, + repositoryId: mirror.repositoryId, + repositoryName: mirror.repository.name, + }); + + const releaseSource = await repoMutex.acquireShared(sourceRepository.id, `mirror_source:${scheduleId}`); + const releaseMirror = await repoMutex.acquireShared(mirror.repository.id, `mirror:${scheduleId}`); + + try { + await restic.copy(sourceRepository.config, mirror.repository.config, { tag: scheduleId.toString() }); + } finally { + releaseSource(); + releaseMirror(); + } + + if (retentionPolicy) { + const releaseForget = await repoMutex.acquireExclusive(mirror.repository.id, `forget:mirror:${scheduleId}`); + + try { + logger.info(`[Background] Applying retention policy to mirror repository: ${mirror.repository.name}`); + await restic.forget(mirror.repository.config, retentionPolicy, { tag: scheduleId.toString() }); + } finally { + releaseForget(); + } + } + + await db + .update(backupScheduleMirrorsTable) + .set({ lastCopyAt: Date.now(), lastCopyStatus: "success", lastCopyError: null }) + .where(eq(backupScheduleMirrorsTable.id, mirror.id)); + + logger.info(`[Background] Successfully copied to mirror repository: ${mirror.repository.name}`); + + serverEvents.emit("mirror:completed", { + scheduleId, + repositoryId: mirror.repositoryId, + repositoryName: mirror.repository.name, + status: "success", + }); + } catch (error) { + const errorMessage = toMessage(error); + logger.error(`[Background] Failed to copy to mirror repository ${mirror.repository.name}: ${errorMessage}`); + + await db + .update(backupScheduleMirrorsTable) + .set({ lastCopyAt: Date.now(), lastCopyStatus: "error", lastCopyError: errorMessage }) + .where(eq(backupScheduleMirrorsTable.id, mirror.id)); + + serverEvents.emit("mirror:completed", { + scheduleId, + repositoryId: mirror.repositoryId, + repositoryName: mirror.repository.name, + status: "error", + error: errorMessage, + }); + } + } +}; + +const getMirrorCompatibility = async (scheduleId: number) => { + const schedule = await db.query.backupSchedulesTable.findFirst({ + where: eq(backupSchedulesTable.id, scheduleId), + with: { repository: true }, + }); + + if (!schedule) { + throw new NotFoundError("Backup schedule not found"); + } + + const allRepositories = await db.query.repositoriesTable.findMany(); + const repos = allRepositories.filter((repo) => repo.id !== schedule.repositoryId); + + const compatibility = await Promise.all( + repos.map((repo) => checkMirrorCompatibility(schedule.repository.config, repo.config, repo.id)), + ); + + return compatibility; +}; + export const backupsService = { listSchedules, getSchedule, @@ -432,4 +607,7 @@ export const backupsService = { getScheduleForVolume, stopBackup, runForget, + getMirrors, + updateMirrors, + getMirrorCompatibility, }; diff --git a/app/server/modules/events/events.controller.ts b/app/server/modules/events/events.controller.ts index a93dc27..cd40b84 100644 --- a/app/server/modules/events/events.controller.ts +++ b/app/server/modules/events/events.controller.ts @@ -70,12 +70,34 @@ export const eventsController = new Hono().get("/", (c) => { }); }; + const onMirrorStarted = (data: { scheduleId: number; repositoryId: string; repositoryName: string }) => { + stream.writeSSE({ + data: JSON.stringify(data), + event: "mirror:started", + }); + }; + + const onMirrorCompleted = (data: { + scheduleId: number; + repositoryId: string; + repositoryName: string; + status: "success" | "error"; + error?: string; + }) => { + stream.writeSSE({ + data: JSON.stringify(data), + event: "mirror:completed", + }); + }; + serverEvents.on("backup:started", onBackupStarted); serverEvents.on("backup:progress", onBackupProgress); serverEvents.on("backup:completed", onBackupCompleted); serverEvents.on("volume:mounted", onVolumeMounted); serverEvents.on("volume:unmounted", onVolumeUnmounted); serverEvents.on("volume:updated", onVolumeUpdated); + serverEvents.on("mirror:started", onMirrorStarted); + serverEvents.on("mirror:completed", onMirrorCompleted); let keepAlive = true; @@ -88,6 +110,8 @@ export const eventsController = new Hono().get("/", (c) => { serverEvents.off("volume:mounted", onVolumeMounted); serverEvents.off("volume:unmounted", onVolumeUnmounted); serverEvents.off("volume:updated", onVolumeUpdated); + serverEvents.off("mirror:started", onMirrorStarted); + serverEvents.off("mirror:completed", onMirrorCompleted); }); while (keepAlive) { diff --git a/app/server/modules/repositories/repositories.controller.ts b/app/server/modules/repositories/repositories.controller.ts index c629dec..a53a3a2 100644 --- a/app/server/modules/repositories/repositories.controller.ts +++ b/app/server/modules/repositories/repositories.controller.ts @@ -90,6 +90,7 @@ export const repositoriesController = new Hono() short_id: snapshot.short_id, duration, paths: snapshot.paths, + tags: snapshot.tags ?? [], size: summary?.total_bytes_processed || 0, time: new Date(snapshot.time).getTime(), }; @@ -113,6 +114,7 @@ export const repositoriesController = new Hono() time: new Date(snapshot.time).getTime(), paths: snapshot.paths, size: snapshot.summary?.total_bytes_processed || 0, + tags: snapshot.tags ?? [], summary: snapshot.summary, }; diff --git a/app/server/modules/repositories/repositories.dto.ts b/app/server/modules/repositories/repositories.dto.ts index aa77a62..c659b18 100644 --- a/app/server/modules/repositories/repositories.dto.ts +++ b/app/server/modules/repositories/repositories.dto.ts @@ -174,6 +174,7 @@ export const snapshotSchema = type({ paths: "string[]", size: "number", duration: "number", + tags: "string[]", }); const listSnapshotsResponse = snapshotSchema.array(); diff --git a/app/server/utils/backend-compatibility.ts b/app/server/utils/backend-compatibility.ts new file mode 100644 index 0000000..329db41 --- /dev/null +++ b/app/server/utils/backend-compatibility.ts @@ -0,0 +1,148 @@ +import type { RepositoryConfig } from "~/schemas/restic"; +import { cryptoUtils } from "./crypto"; + +type BackendConflictGroup = "s3" | "gcs" | "azure" | "rest" | "sftp" | null; + +export const getBackendConflictGroup = (backend: string): BackendConflictGroup => { + switch (backend) { + case "s3": + case "r2": + return "s3"; + case "gcs": + return "gcs"; + case "azure": + return "azure"; + case "rest": + return "rest"; + case "sftp": + return "sftp"; + case "local": + case "rclone": + return null; + default: + return null; + } +}; + +export const hasCompatibleCredentials = async ( + config1: RepositoryConfig, + config2: RepositoryConfig, +): Promise => { + const group1 = getBackendConflictGroup(config1.backend); + const group2 = getBackendConflictGroup(config2.backend); + + if (!group1 || !group2 || group1 !== group2) { + return true; + } + + switch (group1) { + case "s3": { + if ( + (config1.backend === "s3" || config1.backend === "r2") && + (config2.backend === "s3" || config2.backend === "r2") + ) { + const accessKey1 = await cryptoUtils.decrypt(config1.accessKeyId); + const secretKey1 = await cryptoUtils.decrypt(config1.secretAccessKey); + + const accessKey2 = await cryptoUtils.decrypt(config2.accessKeyId); + const secretKey2 = await cryptoUtils.decrypt(config2.secretAccessKey); + + return accessKey1 === accessKey2 && secretKey1 === secretKey2; + } + return false; + } + case "gcs": { + if (config1.backend === "gcs" && config2.backend === "gcs") { + const credentials1 = await cryptoUtils.decrypt(config1.credentialsJson); + const credentials2 = await cryptoUtils.decrypt(config2.credentialsJson); + + return credentials1 === credentials2 && config1.projectId === config2.projectId; + } + return false; + } + case "azure": { + if (config1.backend === "azure" && config2.backend === "azure") { + const config1Accountkey = await cryptoUtils.decrypt(config1.accountKey); + const config2Accountkey = await cryptoUtils.decrypt(config2.accountKey); + + return config1.accountName === config2.accountName && config1Accountkey === config2Accountkey; + } + return false; + } + case "rest": { + if (config1.backend === "rest" && config2.backend === "rest") { + if (!config1.username && !config2.username && !config1.password && !config2.password) { + return true; + } + + const config1Username = await cryptoUtils.decrypt(config1.username || ""); + const config1Password = await cryptoUtils.decrypt(config1.password || ""); + const config2Username = await cryptoUtils.decrypt(config2.username || ""); + const config2Password = await cryptoUtils.decrypt(config2.password || ""); + + return config1Username === config2Username && config1Password === config2Password; + } + return false; + } + case "sftp": { + return false; + } + default: + return false; + } +}; + +export interface CompatibilityResult { + repositoryId: string; + compatible: boolean; + reason: string | null; +} + +export const checkMirrorCompatibility = async ( + primaryConfig: RepositoryConfig, + mirrorConfig: RepositoryConfig, + mirrorRepositoryId: string, +): Promise => { + const primaryConflictGroup = getBackendConflictGroup(primaryConfig.backend); + const mirrorConflictGroup = getBackendConflictGroup(mirrorConfig.backend); + + if (!primaryConflictGroup || !mirrorConflictGroup) { + return { + repositoryId: mirrorRepositoryId, + compatible: true, + reason: null, + }; + } + + if (primaryConflictGroup !== mirrorConflictGroup) { + return { + repositoryId: mirrorRepositoryId, + compatible: true, + reason: null, + }; + } + + const compatible = await hasCompatibleCredentials(primaryConfig, mirrorConfig); + + if (compatible) { + return { + repositoryId: mirrorRepositoryId, + compatible: true, + reason: null, + }; + } + + return { + repositoryId: mirrorRepositoryId, + compatible: false, + reason: `Both use ${primaryConflictGroup.toUpperCase()} backends with different credentials`, + }; +}; + +export const getIncompatibleMirrorError = (mirrorRepoName: string, primaryBackend: string, mirrorBackend: string) => { + return ( + `Cannot mirror to ${mirrorRepoName}: both repositories use the same backend type (${primaryBackend}/${mirrorBackend}) with different credentials. ` + + "Restic cannot use different credentials for the same backend in a copy operation. " + + "Consider creating a new backup scheduler with the desired destination instead." + ); +}; diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index 317a365..84eaf08 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -40,6 +40,7 @@ const snapshotInfoSchema = type({ time: "string", uid: "number?", username: "string", + tags: "string[]?", summary: type({ backup_end: "string", backup_start: "string", @@ -201,7 +202,7 @@ const init = async (config: RepositoryConfig) => { const env = await buildEnv(config); const args = ["init", "--repo", repoUrl]; - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await $`restic ${args}`.env(env).nothrow(); await cleanupTemporaryKeys(config, env); @@ -277,7 +278,7 @@ const backup = async ( } } - addCommonArgs(args, config, env); + addCommonArgs(args, env); const logData = throttle((data: string) => { logger.info(data.trim()); @@ -403,7 +404,7 @@ const restore = async ( } } - addCommonArgs(args, config, env); + addCommonArgs(args, env); logger.debug(`Executing: restic ${args.join(" ")}`); const res = await $`restic ${args}`.env(env).nothrow(); @@ -466,7 +467,7 @@ const snapshots = async (config: RepositoryConfig, options: { tags?: string[] } } } - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await $`restic ${args}`.env(env).nothrow().quiet(); await cleanupTemporaryKeys(config, env); @@ -515,7 +516,7 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra: } args.push("--prune"); - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await $`restic ${args}`.env(env).nothrow(); await cleanupTemporaryKeys(config, env); @@ -533,7 +534,7 @@ const deleteSnapshot = async (config: RepositoryConfig, snapshotId: string) => { const env = await buildEnv(config); const args: string[] = ["--repo", repoUrl, "forget", snapshotId, "--prune"]; - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await $`restic ${args}`.env(env).nothrow(); await cleanupTemporaryKeys(config, env); @@ -583,7 +584,7 @@ const ls = async (config: RepositoryConfig, snapshotId: string, path?: string) = args.push(path); } - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(config, env); @@ -634,7 +635,7 @@ const unlock = async (config: RepositoryConfig) => { const env = await buildEnv(config); const args = ["unlock", "--repo", repoUrl, "--remove-all"]; - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await $`restic ${args}`.env(env).nothrow(); await cleanupTemporaryKeys(config, env); @@ -658,7 +659,7 @@ const check = async (config: RepositoryConfig, options?: { readData?: boolean }) args.push("--read-data"); } - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await $`restic ${args}`.env(env).nothrow(); await cleanupTemporaryKeys(config, env); @@ -692,7 +693,7 @@ const repairIndex = async (config: RepositoryConfig) => { const env = await buildEnv(config); const args = ["repair", "index", "--repo", repoUrl]; - addCommonArgs(args, config, env); + addCommonArgs(args, env); const res = await $`restic ${args}`.env(env).nothrow(); await cleanupTemporaryKeys(config, env); @@ -713,12 +714,65 @@ const repairIndex = async (config: RepositoryConfig) => { }; }; -const addCommonArgs = (args: string[], config: RepositoryConfig, env: Record) => { - args.push("--retry-lock", "1m", "--json"); +const copy = async ( + sourceConfig: RepositoryConfig, + destConfig: RepositoryConfig, + options: { + tag?: string; + snapshotId?: string; + }, +) => { + const sourceRepoUrl = buildRepoUrl(sourceConfig); + const destRepoUrl = buildRepoUrl(destConfig); - if (config.backend === "sftp" && env._SFTP_SSH_ARGS) { - args.push("-o", `sftp.args=${env._SFTP_SSH_ARGS}`); + const sourceEnv = await buildEnv(sourceConfig); + const destEnv = await buildEnv(destConfig); + + const env: Record = { + ...sourceEnv, + ...destEnv, + RESTIC_FROM_PASSWORD_FILE: sourceEnv.RESTIC_PASSWORD_FILE, + }; + + const args: string[] = ["--repo", destRepoUrl, "copy", "--from-repo", sourceRepoUrl]; + + if (options.tag) { + args.push("--tag", options.tag); } + + if (options.snapshotId) { + args.push(options.snapshotId); + } else { + args.push("latest"); + } + + addCommonArgs(args, env); + + if (sourceConfig.backend === "sftp" && sourceEnv._SFTP_SSH_ARGS) { + args.push("-o", `sftp.args=${sourceEnv._SFTP_SSH_ARGS}`); + } + + logger.info(`Copying snapshots from ${sourceRepoUrl} to ${destRepoUrl}...`); + logger.debug(`Executing: restic ${args.join(" ")}`); + + const res = await $`restic ${args}`.env(env).nothrow(); + + await cleanupTemporaryKeys(sourceConfig, sourceEnv); + await cleanupTemporaryKeys(destConfig, destEnv); + + const stdout = res.text(); + const stderr = res.stderr.toString(); + + if (res.exitCode !== 0) { + logger.error(`Restic copy failed: ${stderr}`); + throw new ResticError(res.exitCode, stderr); + } + + logger.info(`Restic copy completed from ${sourceRepoUrl} to ${destRepoUrl}`); + return { + success: true, + output: stdout, + }; }; const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record) => { @@ -731,6 +785,14 @@ const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record) => { + args.push("--retry-lock", "1m", "--json"); + + if (env._SFTP_SSH_ARGS) { + args.push("-o", `sftp.args=${env._SFTP_SSH_ARGS}`); + } +}; + export const restic = { ensurePassfile, init, @@ -743,4 +805,5 @@ export const restic = { ls, check, repairIndex, + copy, }; diff --git a/bun.lock b/bun.lock index 489487d..5d79fdb 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "dependencies": { From ce07c588ad853393df6c481badb4338f2407efc6 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Wed, 3 Dec 2025 20:55:38 +0100 Subject: [PATCH 2/8] chore: update dependencies --- Dockerfile | 2 +- app/server/db/schema.ts | 2 +- bun.lock | 288 +++++++++++++--------------------------- package.json | 70 +++++----- 4 files changed, 126 insertions(+), 236 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5267666..1b41373 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG BUN_VERSION="1.3.1" +ARG BUN_VERSION="1.3.3" FROM oven/bun:${BUN_VERSION}-alpine AS base diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index 6ef9037..6bfa699 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -1,5 +1,5 @@ import { relations, sql } from "drizzle-orm"; -import { int, integer, sqliteTable, text, primaryKey, uniqueIndex, unique } from "drizzle-orm/sqlite-core"; +import { int, integer, sqliteTable, text, primaryKey, unique } from "drizzle-orm/sqlite-core"; import type { CompressionMode, RepositoryBackend, repositoryConfigSchema, RepositoryStatus } from "~/schemas/restic"; import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes"; import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications"; diff --git a/bun.lock b/bun.lock index 5d79fdb..dd2432a 100644 --- a/bun.lock +++ b/bun.lock @@ -4,25 +4,25 @@ "workspaces": { "": { "dependencies": { - "@hono/standard-validator": "^0.1.5", + "@hono/standard-validator": "^0.2.0", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - "@react-router/node": "^7.9.3", - "@react-router/serve": "^7.9.3", - "@scalar/hono-api-reference": "^0.9.24", - "@tanstack/react-query": "^5.90.2", - "arktype": "^2.1.26", + "@react-router/node": "^7.10.0", + "@react-router/serve": "^7.10.0", + "@scalar/hono-api-reference": "^0.9.25", + "@tanstack/react-query": "^5.90.11", + "arktype": "^2.1.28", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cron-parser": "^5.4.0", @@ -31,54 +31,54 @@ "dockerode": "^4.0.9", "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", - "es-toolkit": "^1.41.0", - "hono": "^4.10.5", + "es-toolkit": "^1.42.0", + "hono": "^4.10.7", "hono-openapi": "^1.1.1", - "http-errors-enhanced": "^3.0.2", - "isbot": "^5.1.31", - "lucide-react": "^0.546.0", + "http-errors-enhanced": "^4.0.2", + "isbot": "^5.1.32", + "lucide-react": "^0.555.0", "next-themes": "^0.4.6", "node-cron": "^4.2.1", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-hook-form": "^7.63.0", - "react-router": "^7.9.3", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "react-hook-form": "^7.67.0", + "react-router": "^7.10.0", "react-router-hono-server": "^2.22.0", - "recharts": "3.2.1", + "recharts": "3.5.1", "slugify": "^1.6.6", "sonner": "^2.0.7", - "tailwind-merge": "^3.3.1", + "tailwind-merge": "^3.4.0", "tiny-typed-emitter": "^2.1.0", "winston": "^3.18.3", - "yaml": "^2.8.1", + "yaml": "^2.8.2", }, "devDependencies": { - "@biomejs/biome": "^2.3.5", - "@hey-api/openapi-ts": "^0.87.4", - "@react-router/dev": "^7.9.3", - "@tailwindcss/vite": "^4.1.14", - "@tanstack/react-query-devtools": "^5.90.2", - "@types/bun": "^1.3.2", - "@types/dockerode": "^3.3.45", - "@types/node": "^24.6.2", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", - "drizzle-kit": "^0.31.6", + "@biomejs/biome": "^2.3.8", + "@hey-api/openapi-ts": "^0.88.0", + "@react-router/dev": "^7.10.0", + "@tailwindcss/vite": "^4.1.17", + "@tanstack/react-query-devtools": "^5.91.1", + "@types/bun": "^1.3.3", + "@types/dockerode": "^3.3.47", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "drizzle-kit": "^0.31.7", "lightningcss": "^1.30.2", - "tailwindcss": "^4.1.14", + "tailwindcss": "^4.1.17", "tinyglobby": "^0.2.15", "tw-animate-css": "^1.4.0", "typescript": "^5.9.3", - "vite": "^7.1.9", + "vite": "^7.2.6", "vite-bundle-analyzer": "^1.2.3", "vite-tsconfig-paths": "^5.1.4", }, }, }, "packages": { - "@ark/schema": ["@ark/schema@0.54.0", "", { "dependencies": { "@ark/util": "0.54.0" } }, "sha512-QloFou+ODfb5qXgPxX1EbJyRqZEwoElzvJ6VuuFVvWJQGoigBEUW3L0HejXG/B9v8K3VvDikuULp5ELSwZn8hg=="], + "@ark/schema": ["@ark/schema@0.56.0", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA=="], - "@ark/util": ["@ark/util@0.54.0", "", {}, "sha512-ugTfpEDGA6d2uU2/3M7uRiuPNYckQMhg2wfNMw8zZHXjPhuVCbXWZzIcBojuXuCN8j4MrNWNqQ9z7f8W/JiE8w=="], + "@ark/util": ["@ark/util@0.56.0", "", {}, "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="], "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], @@ -138,23 +138,23 @@ "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], - "@biomejs/biome": ["@biomejs/biome@2.3.5", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.5", "@biomejs/cli-darwin-x64": "2.3.5", "@biomejs/cli-linux-arm64": "2.3.5", "@biomejs/cli-linux-arm64-musl": "2.3.5", "@biomejs/cli-linux-x64": "2.3.5", "@biomejs/cli-linux-x64-musl": "2.3.5", "@biomejs/cli-win32-arm64": "2.3.5", "@biomejs/cli-win32-x64": "2.3.5" }, "bin": { "biome": "bin/biome" } }, "sha512-HvLhNlIlBIbAV77VysRIBEwp55oM/QAjQEin74QQX9Xb259/XP/D5AGGnZMOyF1el4zcvlNYYR3AyTMUV3ILhg=="], + "@biomejs/biome": ["@biomejs/biome@2.3.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.8", "@biomejs/cli-darwin-x64": "2.3.8", "@biomejs/cli-linux-arm64": "2.3.8", "@biomejs/cli-linux-arm64-musl": "2.3.8", "@biomejs/cli-linux-x64": "2.3.8", "@biomejs/cli-linux-x64-musl": "2.3.8", "@biomejs/cli-win32-arm64": "2.3.8", "@biomejs/cli-win32-x64": "2.3.8" }, "bin": { "biome": "bin/biome" } }, "sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fLdTur8cJU33HxHUUsii3GLx/TR0BsfQx8FkeqIiW33cGMtUD56fAtrh+2Fx1uhiCsVZlFh6iLKUU3pniZREQw=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-qpT8XDqeUlzrOW8zb4k3tjhT7rmvVRumhi2657I2aGcY4B+Ft5fNwDdZGACzn8zj7/K1fdWjgwYE3i2mSZ+vOA=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-u/pybjTBPGBHB66ku4pK1gj+Dxgx7/+Z0jAriZISPX1ocTO8aHh8x8e7Kb1rB4Ms0nA/SzjtNOVJ4exVavQBCw=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-eGUG7+hcLgGnMNl1KHVZUYxahYAhC462jF/wQolqu4qso2MSk32Q+QrpN7eN4jAHAg7FUMIo897muIhK4hXhqg=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-XrIVi9YAW6ye0CGQ+yax0gLfx+BFOtKaNX74n+xHWla6Cl6huUmcKNO7HPx7BiKnJUzrxXY1qYlm7xMvi08X4g=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.5", "", { "os": "linux", "cpu": "x64" }, "sha512-awVuycTPpVTH/+WDVnEEYSf6nbCBHf/4wB3lquwT7puhNg8R4XvonWNZzUsfHZrCkjkLhFH/vCZK5jHatD9FEg=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-DlBiMlBZZ9eIq4H7RimDSGsYcOtfOIfZOaI5CqsWiSlbTfqbPVfWtCf92wNzx8GNMbu1s7/g3ZZESr6+GwM/SA=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.5", "", { "os": "win32", "cpu": "x64" }, "sha512-nUmR8gb6yvrKhtRgzwo/gDimPwnO5a4sCydf8ZS2kHIJhEmSmk+STsusr1LHTuM//wXppBawvSQi2xFXJCdgKQ=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w=="], "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], @@ -234,20 +234,18 @@ "@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.1", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0", "lodash": "^4.17.21" } }, "sha512-inPeksRLq+j3ArnuGOzQPQE//YrhezQG0+9Y9yizScBN2qatJ78fIByhEgKdNAbtguDCn4RPxmEhcrePwHxs4A=="], - "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.87.4", "", { "dependencies": { "@hey-api/codegen-core": "^0.3.3", "@hey-api/json-schema-ref-parser": "1.2.1", "ansi-colors": "4.1.3", "c12": "3.3.2", "color-support": "1.1.3", "commander": "14.0.1", "open": "10.2.0", "semver": "7.7.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-ogSjR7l+PCBoRvvJVrUFZ3Cv4GDnETpwAr5G9EwsbjKVJHZDsQn3EIOYJi2WqKl9VPZBT1CtycIxR/ZmCS/T4A=="], + "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.88.0", "", { "dependencies": { "@hey-api/codegen-core": "^0.3.3", "@hey-api/json-schema-ref-parser": "1.2.1", "ansi-colors": "4.1.3", "c12": "3.3.2", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.2" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-ZrvmDfmVf+N4ry786LAhS/DoH+xkIjIJIeDnj2aL1qnMTIDsdRIXXvr80EnAZVBgunzu1wTBbHb3H9OfyDQ2EQ=="], "@hono/node-server": ["@hono/node-server@1.19.6", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw=="], "@hono/node-ws": ["@hono/node-ws@1.2.0", "", { "dependencies": { "ws": "^8.17.0" }, "peerDependencies": { "@hono/node-server": "^1.11.1", "hono": "^4.6.0" } }, "sha512-OBPQ8OSHBw29mj00wT/xGYtB6HY54j0fNSdVZ7gZM3TUeq0So11GXaWtFf1xWxQNfumKIsj0wRuLKWfVsO5GgQ=="], - "@hono/standard-validator": ["@hono/standard-validator@0.1.5", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-EIyZPPwkyLn6XKwFj5NBEWHXhXbgmnVh2ceIFo5GO7gKI9WmzTjPDKnppQB0KrqKeAkq3kpoW4SIbu5X1dgx3w=="], + "@hono/standard-validator": ["@hono/standard-validator@0.2.0", "", { "peerDependencies": { "@standard-schema/spec": "1.0.0", "hono": ">=3.9.0" } }, "sha512-pFq0UVAnjzXcDAgqFpDeVL3MOUPrlIh/kPqBDvbCYoThVhhS+Vf37VcdsakdOFFGiqoiYVxp3LifXFhGhp/rgQ=="], "@hono/vite-dev-server": ["@hono/vite-dev-server@0.23.0", "", { "dependencies": { "@hono/node-server": "^1.14.2", "minimatch": "^9.0.3" }, "peerDependencies": { "hono": "*", "miniflare": "*", "wrangler": "*" }, "optionalPeers": ["miniflare", "wrangler"] }, "sha512-tHV86xToed9Up0j/dubQW2PDP4aYNFDSfQrjcV6Ra7bqCGrxhtg/zZBmbgSco3aTxKOEPzDXKK+6seAAfsbIXw=="], "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], - "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -264,14 +262,6 @@ "@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.2.0", "", {}, "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng=="], - "@npmcli/git": ["@npmcli/git@4.1.0", "", { "dependencies": { "@npmcli/promise-spawn": "^6.0.0", "lru-cache": "^7.4.4", "npm-pick-manifest": "^8.0.0", "proc-log": "^3.0.0", "promise-inflight": "^1.0.1", "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^3.0.0" } }, "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ=="], - - "@npmcli/package-json": ["@npmcli/package-json@4.0.1", "", { "dependencies": { "@npmcli/git": "^4.1.0", "glob": "^10.2.2", "hosted-git-info": "^6.1.1", "json-parse-even-better-errors": "^3.0.0", "normalize-package-data": "^5.0.0", "proc-log": "^3.0.0", "semver": "^7.5.3" } }, "sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q=="], - - "@npmcli/promise-spawn": ["@npmcli/promise-spawn@6.0.2", "", { "dependencies": { "which": "^3.0.0" } }, "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg=="], - - "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], @@ -368,13 +358,13 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - "@react-router/dev": ["@react-router/dev@7.9.5", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@npmcli/package-json": "^4.0.1", "@react-router/node": "7.9.5", "@remix-run/node-fetch-server": "^0.9.0", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "p-map": "^7.0.3", "pathe": "^1.1.2", "picocolors": "^1.1.1", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "tinyglobby": "^0.2.14", "valibot": "^1.1.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.9.5", "@vitejs/plugin-rsc": "*", "react-router": "^7.9.5", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@react-router/serve", "@vitejs/plugin-rsc", "typescript", "wrangler"], "bin": { "react-router": "bin.js" } }, "sha512-MkWI4zN7VbQ0tteuJtX5hmDINNS26IW236a8lM8+o1344xdnT/ZsBvcUh8AkzDdCRYEz1blgzgirpj0Wc1gmXg=="], + "@react-router/dev": ["@react-router/dev@7.10.0", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@react-router/node": "7.10.0", "@remix-run/node-fetch-server": "^0.9.0", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "p-map": "^7.0.3", "pathe": "^1.1.2", "picocolors": "^1.1.1", "pkg-types": "^2.3.0", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "tinyglobby": "^0.2.14", "valibot": "^1.1.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.10.0", "@vitejs/plugin-rsc": "*", "react-router": "^7.10.0", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@react-router/serve", "@vitejs/plugin-rsc", "typescript", "wrangler"], "bin": { "react-router": "bin.js" } }, "sha512-3UgkV0N5lp3+Ol3q64L4ZHgPXv2XA4KHJ59MVLSK2prokrOrPaYvqbdx40r602M+hRZp/u04ln2A6cOfBW6kxA=="], - "@react-router/express": ["@react-router/express@7.9.5", "", { "dependencies": { "@react-router/node": "7.9.5" }, "peerDependencies": { "express": "^4.17.1 || ^5", "react-router": "7.9.5", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-Mg94Tw9JSaRuwkvIC6PaODRzsLs6mo70ppz5qdIK/G3iotSxsH08TDNdzot7CaXXevk/pIiD/+Tbn0H/asHsYA=="], + "@react-router/express": ["@react-router/express@7.10.0", "", { "dependencies": { "@react-router/node": "7.10.0" }, "peerDependencies": { "express": "^4.17.1 || ^5", "react-router": "7.10.0", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-3cBJ2cyHn5J+wSNFn+XdNSpXVAlQ+nbj7CMa3OsiEpFb+d0GLthirvSESqRjX2Eid94xNHICqKpYS9bR4QqIxg=="], - "@react-router/node": ["@react-router/node@7.9.5", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.9.5", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-3mDd32mXh3gEkG0cLPnUaoLkY1pApsTPqn7O1j+P8aLf997uYz5lYDjt33vtMhaotlRM0x+5JziAKtz/76YBpQ=="], + "@react-router/node": ["@react-router/node@7.10.0", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.10.0", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-pff3Xz3gASrIUUX54QdlPzasdN9XRLnzoFEwUVsH5y2sZ6vijQdjZExLS6aQhPiuUr/uVPwN2WngO0Ryfrxulg=="], - "@react-router/serve": ["@react-router/serve@7.9.5", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.9.5", "@react-router/node": "7.9.5", "compression": "^1.7.4", "express": "^4.19.2", "get-port": "5.1.1", "morgan": "^1.10.0", "source-map-support": "^0.5.21" }, "peerDependencies": { "react-router": "7.9.5" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-sww8oDNqz8SgaXEQ3maqTuMlibCMpmWvLE0s5zyEyOQb1G99clYMcXceQ2HNU2jtXJkp+P5XI1CngpGpngyTnw=="], + "@react-router/serve": ["@react-router/serve@7.10.0", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.10.0", "@react-router/node": "7.10.0", "compression": "^1.7.4", "express": "^4.19.2", "get-port": "5.1.1", "morgan": "^1.10.0", "source-map-support": "^0.5.21" }, "peerDependencies": { "react-router": "7.10.0" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-tgdbw1lmDkzF3gCMj//iNklgUrYHUxz35rj0sbyLeti8K2gVsNxaZWyt5omanFgkeZ7WYfi0wzLHviqxl228eA=="], "@reduxjs/toolkit": ["@reduxjs/toolkit@2.10.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^10.2.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA=="], @@ -424,13 +414,11 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.2", "", { "os": "win32", "cpu": "x64" }, "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA=="], - "@scalar/core": ["@scalar/core@0.3.22", "", { "dependencies": { "@scalar/types": "0.4.0" } }, "sha512-6lzeRkvgkukSgge35kvxJKiJBny4rdGSaLTNzn/sF1F6JRfUo7I0AgqFxxSZWMD+EG4kGyNxAz0zciDSx2Cjvw=="], + "@scalar/core": ["@scalar/core@0.3.23", "", { "dependencies": { "@scalar/types": "0.5.0" } }, "sha512-hop7LVR3MKB2VpS8dly3gmmbB3lBGxQRtL0pBaC77zFMRHoBv1DuB2bj8l4gxd5grzitJ1LsYduvywLAMY9F6g=="], - "@scalar/hono-api-reference": ["@scalar/hono-api-reference@0.9.24", "", { "dependencies": { "@scalar/core": "0.3.22" }, "peerDependencies": { "hono": "^4.10.3" } }, "sha512-NjPY3iMm/FqYRXAgr6V7qBhJGbSUQ8hbijFUMuqZo4pIjGEUNLeB5L9U2Gh4cDIPPWeso8mlc16jaX7dV0FrPw=="], + "@scalar/hono-api-reference": ["@scalar/hono-api-reference@0.9.25", "", { "dependencies": { "@scalar/core": "0.3.23" }, "peerDependencies": { "hono": "^4.10.3" } }, "sha512-ZEQAhvVU/FXdJs8+rVXdfWjwzkE+M6Zr+4W+zNhy8DF17BIpxFXfVL7i3OxK1V/4EtkTplkETjYGTR4ju3RFZw=="], - "@scalar/openapi-types": ["@scalar/openapi-types@0.5.1", "", { "dependencies": { "zod": "4.1.11" } }, "sha512-8g7s9lPolyDFtijyh3Ob459tpezPuZbkXoFgJwBTHjPZ7ap+TvOJTvLk56CFwxVBVz2BxCzWJqxYyy3FUdeLoA=="], - - "@scalar/types": ["@scalar/types@0.4.0", "", { "dependencies": { "@scalar/openapi-types": "0.5.1", "nanoid": "5.1.5", "type-fest": "5.0.0", "zod": "4.1.11" } }, "sha512-vOD1GZez7kPdVA+UQit05QE9dbALfevhK9kqRTsqcPX7FvvZ9eQWSNl1GKmKtmRiAZGThv2agM5AvHRxkH2JSw=="], + "@scalar/types": ["@scalar/types@0.5.0", "", { "dependencies": { "nanoid": "5.1.5", "type-fest": "5.0.0", "zod": "4.1.11" } }, "sha512-imDMuTieOc5kHM9/Kt/1lmiI5ZtusuaYlzsXTP99IsWvD8mJ7ivF73lPBRj4PKtg4vY+ta5CO/vJpvnCYandRg=="], "@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="], @@ -472,15 +460,15 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.17", "", { "dependencies": { "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "tailwindcss": "4.1.17" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA=="], - "@tanstack/query-core": ["@tanstack/query-core@5.90.8", "", {}, "sha512-4E0RP/0GJCxSNiRF2kAqE/LQkTJVlL/QNU7gIJSptaseV9HP6kOuA+N11y4bZKZxa3QopK3ZuewwutHx6DqDXQ=="], + "@tanstack/query-core": ["@tanstack/query-core@5.90.11", "", {}, "sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A=="], - "@tanstack/query-devtools": ["@tanstack/query-devtools@5.90.1", "", {}, "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ=="], + "@tanstack/query-devtools": ["@tanstack/query-devtools@5.91.1", "", {}, "sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg=="], - "@tanstack/react-query": ["@tanstack/react-query@5.90.8", "", { "dependencies": { "@tanstack/query-core": "5.90.8" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/3b9QGzkf4rE5/miL6tyhldQRlLXzMHcySOm/2Tm2OLEFE9P1ImkH0+OviDBSvyAvtAOJocar5xhd7vxdLi3aQ=="], + "@tanstack/react-query": ["@tanstack/react-query@5.90.11", "", { "dependencies": { "@tanstack/query-core": "5.90.11" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA=="], - "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.90.2", "", { "dependencies": { "@tanstack/query-devtools": "5.90.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.2", "react": "^18 || ^19" } }, "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ=="], + "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.1", "", { "dependencies": { "@tanstack/query-devtools": "5.91.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.10", "react": "^18 || ^19" } }, "sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ=="], - "@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="], + "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], @@ -502,7 +490,7 @@ "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], - "@types/dockerode": ["@types/dockerode@3.3.45", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-iYpZF+xr5QLpIICejLdUF2r5gh8IXY1Gw3WLmt41dUbS3Vn/3hVgL+6lJBVbmrhYBWfbWPPstdr6+A0s95DTWA=="], + "@types/dockerode": ["@types/dockerode@3.3.47", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -510,7 +498,7 @@ "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], - "@types/react": ["@types/react@19.2.4", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A=="], + "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], @@ -534,9 +522,9 @@ "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], - "arkregex": ["arkregex@0.0.2", "", { "dependencies": { "@ark/util": "0.53.0" } }, "sha512-ttjDUICBVoXD/m8bf7eOjx8XMR6yIT2FmmW9vsN0FCcFOygEZvvIX8zK98tTdXkzi0LkRi5CmadB44jFEIyDNA=="], + "arkregex": ["arkregex@0.0.4", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-biS/FkvSwQq59TZ453piUp8bxMui11pgOMV9WHAnli1F8o0ayNCZzUwQadL/bGIUic5TkS/QlPcyMuI8ZIwedQ=="], - "arktype": ["arktype@2.1.26", "", { "dependencies": { "@ark/schema": "0.54.0", "@ark/util": "0.54.0", "arkregex": "0.0.2" } }, "sha512-zDwukKV6uTElKCAbIoQ9OU6shXE5ALjvZAqHErOSv6l0iLKlubELZ7AcevYLaWFYr5rmIN4Uv9+dIzInktSO1A=="], + "arktype": ["arktype@2.1.28", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.4" } }, "sha512-LVZqXl2zWRpNFnbITrtFmqeqNkPPo+KemuzbGSY6jvJwCb4v8NsDzrWOLHnQgWl26TkJeWWcUNUeBpq2Mst1/Q=="], "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], @@ -570,7 +558,7 @@ "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], - "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="], + "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -608,7 +596,7 @@ "color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="], - "commander": ["commander@14.0.1", "", {}, "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A=="], + "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], @@ -632,9 +620,7 @@ "cron-parser": ["cron-parser@5.4.0", "", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-HxYB8vTvnQFx4dLsZpGRa0uHp6X3qIzS3ZJgJ9v6l/5TJMgeWQbLkR5yiJ5hOxGbc9+jCADDnydIe15ReLZnJA=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], @@ -666,7 +652,7 @@ "dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="], - "default-browser": ["default-browser@5.3.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-Qq68+VkJlc8tjnPV1i7HtbIn7ohmjZa88qUvHMIK0ZKUXMCuV45cT7cEXALPUmeXCe0q1DWQkQTemHVaLIFSrg=="], + "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], "default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="], @@ -692,14 +678,12 @@ "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], - "drizzle-kit": ["drizzle-kit@0.31.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-/B4e/4pwnx25QwD5xXgdpo1S+077a2VZdosXbItE/oNmUgQwZydGDz9qJYmnQl/b+5IX0rLfwRhrPnroGtrg8Q=="], + "drizzle-kit": ["drizzle-kit@0.31.7", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-hOzRGSdyKIU4FcTSFYGKdXEjFsncVwHZ43gY3WU5Bz9j5Iadp6Rh6hxLSQ1IWXpKLBKt/d5y1cpSPcV+FcoQ1A=="], "drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "electron-to-chromium": ["electron-to-chromium@1.5.250", "", {}, "sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw=="], @@ -714,8 +698,6 @@ "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], - "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -724,7 +706,7 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - "es-toolkit": ["es-toolkit@1.41.0", "", {}, "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA=="], + "es-toolkit": ["es-toolkit@1.42.0", "", {}, "sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA=="], "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], @@ -752,8 +734,6 @@ "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], @@ -780,8 +760,6 @@ "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], - "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], - "globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], @@ -792,15 +770,13 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "hono": ["hono@4.10.5", "", {}, "sha512-h/MXuTkoAK8NG1EfDp0jI1YLf6yGdDnfkebRO2pwEh5+hE3RAJFXkCsnD0vamSiARK4ZrB6MY+o3E/hCnOyHrQ=="], + "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], "hono-openapi": ["hono-openapi@1.1.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.8", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-AC3HNhZYPHhnZdSy2Je7GDoTTNxPos6rKRQKVDBbSilY3cWJPqsxRnN6zA4pU7tfxmQEMTqkiLXbw6sAaemB8Q=="], - "hosted-git-info": ["hosted-git-info@6.1.3", "", { "dependencies": { "lru-cache": "^7.5.1" } }, "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw=="], - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], - "http-errors-enhanced": ["http-errors-enhanced@3.0.2", "", {}, "sha512-Pl8CVX7eu46fYSfEGCnOGD8aZkd/2dtr9214BpNRDiU1THT7vsu+T87SDQZgxjOGmsNXaTZe1sqIo/oZgJIfow=="], + "http-errors-enhanced": ["http-errors-enhanced@4.0.2", "", {}, "sha512-5EXN1gmhJVvuWpNfz+RclWvLnnENEXNMPfww3gm30H9mQzPF4QSBj/MD5FRkVDxGIUhO/cR2GSLCd/6C6xpBcw=="], "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], @@ -814,12 +790,12 @@ "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -828,10 +804,6 @@ "isbot": ["isbot@5.1.32", "", {}, "sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ=="], - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -840,8 +812,6 @@ "jsesc": ["jsesc@3.0.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="], - "json-parse-even-better-errors": ["json-parse-even-better-errors@3.0.2", "", {}, "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ=="], - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], @@ -880,7 +850,7 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="], + "lucide-react": ["lucide-react@0.555.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA=="], "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], @@ -902,8 +872,6 @@ "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], "morgan": ["morgan@1.10.1", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.1.0" } }, "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A=="], @@ -924,16 +892,6 @@ "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], - "normalize-package-data": ["normalize-package-data@5.0.0", "", { "dependencies": { "hosted-git-info": "^6.0.0", "is-core-module": "^2.8.1", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q=="], - - "npm-install-checks": ["npm-install-checks@6.3.0", "", { "dependencies": { "semver": "^7.1.1" } }, "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw=="], - - "npm-normalize-package-bin": ["npm-normalize-package-bin@3.0.1", "", {}, "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ=="], - - "npm-package-arg": ["npm-package-arg@10.1.0", "", { "dependencies": { "hosted-git-info": "^6.0.0", "proc-log": "^3.0.0", "semver": "^7.3.5", "validate-npm-package-name": "^5.0.0" } }, "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA=="], - - "npm-pick-manifest": ["npm-pick-manifest@8.0.2", "", { "dependencies": { "npm-install-checks": "^6.0.0", "npm-normalize-package-bin": "^3.0.0", "npm-package-arg": "^10.0.0", "semver": "^7.3.5" } }, "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg=="], - "nypm": ["nypm@0.6.2", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.2", "pathe": "^2.0.3", "pkg-types": "^2.3.0", "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -948,20 +906,14 @@ "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], - "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], @@ -976,14 +928,10 @@ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], - "proc-log": ["proc-log@3.0.0", "", {}, "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A=="], - - "promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="], - - "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], - "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -1000,11 +948,11 @@ "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], - "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], + "react": ["react@19.2.1", "", {}, "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw=="], - "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="], + "react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="], - "react-hook-form": ["react-hook-form@7.66.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw=="], + "react-hook-form": ["react-hook-form@7.67.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-E55EOwKJHHIT/I6J9DmQbCWToAYSw9nN5R57MZw9rMtjh+YQreMDxRLfdjfxQbiJ3/qbg3Z02wGzBX4M+5fMtQ=="], "react-is": ["react-is@19.2.0", "", {}, "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="], @@ -1016,7 +964,7 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-router": ["react-router@7.9.5", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A=="], + "react-router": ["react-router@7.10.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-FVyCOH4IZ0eDDRycODfUqoN8ZSR2LbTvtx6RPsBgzvJ8xAXlMZNCrOFpu+jb8QbtZnpAd/cEki2pwE848pNGxw=="], "react-router-hono-server": ["react-router-hono-server@2.22.0", "", { "dependencies": { "@drizzle-team/brocli": "^0.11.0", "@hono/node-server": "^1.19.5", "@hono/node-ws": "^1.2.0", "@hono/vite-dev-server": "^0.23.0", "hono": "^4.10.3" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250317.0", "@react-router/dev": "^7.9.0", "@types/react": "^19.0.0", "miniflare": "^3.20241205.0", "react-router": "^7.9.0", "vite": "^7.0.0", "wrangler": "^4.2.0" }, "optionalPeers": ["@cloudflare/workers-types", "miniflare", "wrangler"], "bin": { "react-router-hono-server": "dist/cli.js" } }, "sha512-XPJp1PQtkjHsFrneUdvmv22o7LPBGYEW11KtTDEmpXQINW5ViwpzUuwb0/eGjZ9E4RQh6AOKYsXA4+QIqXTkrQ=="], @@ -1026,7 +974,7 @@ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - "recharts": ["recharts@3.2.1", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw=="], + "recharts": ["recharts@3.5.1", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+v+HJojK7gnEgG6h+b2u7k8HH7FhyFUzAc4+cPrsjL4Otdgqr/ecXzAnHciqlzV1ko064eNcsdzrYOM78kankA=="], "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], @@ -1038,8 +986,6 @@ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], - "rollup": ["rollup@4.53.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.2", "@rollup/rollup-android-arm64": "4.53.2", "@rollup/rollup-darwin-arm64": "4.53.2", "@rollup/rollup-darwin-x64": "4.53.2", "@rollup/rollup-freebsd-arm64": "4.53.2", "@rollup/rollup-freebsd-x64": "4.53.2", "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", "@rollup/rollup-linux-arm-musleabihf": "4.53.2", "@rollup/rollup-linux-arm64-gnu": "4.53.2", "@rollup/rollup-linux-arm64-musl": "4.53.2", "@rollup/rollup-linux-loong64-gnu": "4.53.2", "@rollup/rollup-linux-ppc64-gnu": "4.53.2", "@rollup/rollup-linux-riscv64-gnu": "4.53.2", "@rollup/rollup-linux-riscv64-musl": "4.53.2", "@rollup/rollup-linux-s390x-gnu": "4.53.2", "@rollup/rollup-linux-x64-gnu": "4.53.2", "@rollup/rollup-linux-x64-musl": "4.53.2", "@rollup/rollup-openharmony-arm64": "4.53.2", "@rollup/rollup-win32-arm64-msvc": "4.53.2", "@rollup/rollup-win32-ia32-msvc": "4.53.2", "@rollup/rollup-win32-x64-gnu": "4.53.2", "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g=="], "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], @@ -1062,10 +1008,6 @@ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], @@ -1074,8 +1016,6 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="], "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], @@ -1086,14 +1026,6 @@ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], - - "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], - - "spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], - - "spdx-license-ids": ["spdx-license-ids@3.0.22", "", {}, "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ=="], - "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], "ssh2": ["ssh2@1.17.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.23.0" } }, "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ=="], @@ -1104,14 +1036,10 @@ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], @@ -1172,15 +1100,11 @@ "valibot": ["valibot@1.1.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw=="], - "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], - - "validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="], - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], - "vite": ["vite@7.2.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ=="], + "vite": ["vite@7.2.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ=="], "vite-bundle-analyzer": ["vite-bundle-analyzer@1.2.3", "", { "bin": { "analyze": "dist/bin.js" } }, "sha512-8nhwDGHWMKKgg6oegAOpDgTT7/yzTVzeYzLF4y8WBJoYu9gO7h29UpHiQnXD2rAvfQzDy5Wqe/Za5cgqhnxi5g=="], @@ -1188,27 +1112,23 @@ "vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="], - "which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="], - "winston": ["winston@3.18.3", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww=="], "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "wsl-utils": ["wsl-utils@0.3.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-3sFIGLiaDP7rTO4xh3g+b3AzhYDIUGGywE/WsmqzJWDxus5aJXVnPTNC/6L+r2WzrwXqVOdD262OaO+cEyPMSQ=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -1226,18 +1146,6 @@ "@grpc/grpc-js/@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - - "@npmcli/git/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - - "@npmcli/git/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "@npmcli/package-json/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -1280,8 +1188,6 @@ "ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "arkregex/@ark/util": ["@ark/util@0.53.0", "", {}, "sha512-TGn4gLlA6dJcQiqrtCtd88JhGb2XBHo6qIejsDre+nxpGuUVW4G3YZGVrwjNBTO0EyR+ykzIo4joHJzOj+/cpA=="], - "basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -1290,8 +1196,6 @@ "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "express/cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -1300,36 +1204,28 @@ "giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "hosted-git-info/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - "mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "morgan/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="], - "normalize-package-data/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "npm-install-checks/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "npm-package-arg/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "npm-pick-manifest/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "react-router-hono-server/@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], + "react-router-hono-server/hono": ["hono@4.10.5", "", {}, "sha512-h/MXuTkoAK8NG1EfDp0jI1YLf6yGdDnfkebRO2pwEh5+hE3RAJFXkCsnD0vamSiARK4ZrB6MY+o3E/hCnOyHrQ=="], + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], "vite-node/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "vite-node/vite": ["vite@7.2.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -1374,12 +1270,6 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], diff --git a/package.json b/package.json index 79bf831..a1f54ed 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "zerobyte", "private": true, "type": "module", - "packageManager": "bun@1.3.1", + "packageManager": "bun@1.3.3", "scripts": { "build": "react-router build", "dev": "bunx --bun vite", @@ -17,25 +17,25 @@ "studio": "drizzle-kit studio" }, "dependencies": { - "@hono/standard-validator": "^0.1.5", + "@hono/standard-validator": "^0.2.0", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.6", - "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", - "@react-router/node": "^7.9.3", - "@react-router/serve": "^7.9.3", - "@scalar/hono-api-reference": "^0.9.24", - "@tanstack/react-query": "^5.90.2", - "arktype": "^2.1.26", + "@react-router/node": "^7.10.0", + "@react-router/serve": "^7.10.0", + "@scalar/hono-api-reference": "^0.9.25", + "@tanstack/react-query": "^5.90.11", + "arktype": "^2.1.28", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cron-parser": "^5.4.0", @@ -44,45 +44,45 @@ "dockerode": "^4.0.9", "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", - "es-toolkit": "^1.41.0", - "hono": "^4.10.5", + "es-toolkit": "^1.42.0", + "hono": "^4.10.7", "hono-openapi": "^1.1.1", - "http-errors-enhanced": "^3.0.2", - "isbot": "^5.1.31", - "lucide-react": "^0.546.0", + "http-errors-enhanced": "^4.0.2", + "isbot": "^5.1.32", + "lucide-react": "^0.555.0", "next-themes": "^0.4.6", "node-cron": "^4.2.1", - "react": "^19.2.0", - "react-dom": "^19.2.0", - "react-hook-form": "^7.63.0", - "react-router": "^7.9.3", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "react-hook-form": "^7.67.0", + "react-router": "^7.10.0", "react-router-hono-server": "^2.22.0", - "recharts": "3.2.1", + "recharts": "3.5.1", "slugify": "^1.6.6", "sonner": "^2.0.7", - "tailwind-merge": "^3.3.1", + "tailwind-merge": "^3.4.0", "tiny-typed-emitter": "^2.1.0", "winston": "^3.18.3", - "yaml": "^2.8.1" + "yaml": "^2.8.2" }, "devDependencies": { - "@biomejs/biome": "^2.3.5", - "@hey-api/openapi-ts": "^0.87.4", - "@react-router/dev": "^7.9.3", - "@tailwindcss/vite": "^4.1.14", - "@tanstack/react-query-devtools": "^5.90.2", - "@types/bun": "^1.3.2", - "@types/dockerode": "^3.3.45", - "@types/node": "^24.6.2", - "@types/react": "^19.2.0", - "@types/react-dom": "^19.2.0", - "drizzle-kit": "^0.31.6", + "@biomejs/biome": "^2.3.8", + "@hey-api/openapi-ts": "^0.88.0", + "@react-router/dev": "^7.10.0", + "@tailwindcss/vite": "^4.1.17", + "@tanstack/react-query-devtools": "^5.91.1", + "@types/bun": "^1.3.3", + "@types/dockerode": "^3.3.47", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "drizzle-kit": "^0.31.7", "lightningcss": "^1.30.2", - "tailwindcss": "^4.1.14", + "tailwindcss": "^4.1.17", "tinyglobby": "^0.2.15", "tw-animate-css": "^1.4.0", "typescript": "^5.9.3", - "vite": "^7.1.9", + "vite": "^7.2.6", "vite-bundle-analyzer": "^1.2.3", "vite-tsconfig-paths": "^5.1.4" } From b8e30e298caf43e2c9779b369cfb66dfc397c042 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Wed, 3 Dec 2025 21:20:50 +0100 Subject: [PATCH 3/8] fix: broken migration --- ...rre_zzzax.sql => 0018_breezy_invaders.sql} | 6 +- app/drizzle/0019_heavy_shen.sql | 1 - app/drizzle/meta/0018_snapshot.json | 1530 +++++++++-------- app/drizzle/meta/0019_snapshot.json | 792 --------- app/drizzle/meta/_journal.json | 11 +- 5 files changed, 796 insertions(+), 1544 deletions(-) rename app/drizzle/{0018_bizarre_zzzax.sql => 0018_breezy_invaders.sql} (97%) delete mode 100644 app/drizzle/0019_heavy_shen.sql delete mode 100644 app/drizzle/meta/0019_snapshot.json diff --git a/app/drizzle/0018_bizarre_zzzax.sql b/app/drizzle/0018_breezy_invaders.sql similarity index 97% rename from app/drizzle/0018_bizarre_zzzax.sql rename to app/drizzle/0018_breezy_invaders.sql index 0b72b21..7a3e100 100644 --- a/app/drizzle/0018_bizarre_zzzax.sql +++ b/app/drizzle/0018_breezy_invaders.sql @@ -1,4 +1,3 @@ -DROP TABLE IF EXISTS `backup_schedule_mirrors_table`;--> statement-breakpoint CREATE TABLE `backup_schedule_mirrors_table` ( `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `schedule_id` integer NOT NULL, @@ -12,6 +11,7 @@ CREATE TABLE `backup_schedule_mirrors_table` ( FOREIGN KEY (`repository_id`) REFERENCES `repositories_table`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint +CREATE UNIQUE INDEX `backup_schedule_mirrors_table_schedule_id_repository_id_unique` ON `backup_schedule_mirrors_table` (`schedule_id`,`repository_id`);--> statement-breakpoint PRAGMA foreign_keys=OFF;--> statement-breakpoint CREATE TABLE `__new_app_metadata` ( `key` text PRIMARY KEY NOT NULL, @@ -23,6 +23,7 @@ CREATE TABLE `__new_app_metadata` ( INSERT INTO `__new_app_metadata`("key", "value", "created_at", "updated_at") SELECT "key", "value", "created_at", "updated_at" FROM `app_metadata`;--> statement-breakpoint DROP TABLE `app_metadata`;--> statement-breakpoint ALTER TABLE `__new_app_metadata` RENAME TO `app_metadata`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint CREATE TABLE `__new_backup_schedule_notifications_table` ( `schedule_id` integer NOT NULL, `destination_id` integer NOT NULL, @@ -135,5 +136,4 @@ INSERT INTO `__new_volumes_table`("id", "short_id", "name", "type", "status", "l DROP TABLE `volumes_table`;--> statement-breakpoint ALTER TABLE `__new_volumes_table` RENAME TO `volumes_table`;--> statement-breakpoint CREATE UNIQUE INDEX `volumes_table_short_id_unique` ON `volumes_table` (`short_id`);--> statement-breakpoint -CREATE UNIQUE INDEX `volumes_table_name_unique` ON `volumes_table` (`name`); -PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE UNIQUE INDEX `volumes_table_name_unique` ON `volumes_table` (`name`); \ No newline at end of file diff --git a/app/drizzle/0019_heavy_shen.sql b/app/drizzle/0019_heavy_shen.sql deleted file mode 100644 index 98b747b..0000000 --- a/app/drizzle/0019_heavy_shen.sql +++ /dev/null @@ -1 +0,0 @@ -CREATE UNIQUE INDEX `backup_schedule_mirrors_table_schedule_id_repository_id_unique` ON `backup_schedule_mirrors_table` (`schedule_id`,`repository_id`); \ No newline at end of file diff --git a/app/drizzle/meta/0018_snapshot.json b/app/drizzle/meta/0018_snapshot.json index dcc5188..27420d2 100644 --- a/app/drizzle/meta/0018_snapshot.json +++ b/app/drizzle/meta/0018_snapshot.json @@ -1,740 +1,792 @@ { - "version": "6", - "dialect": "sqlite", - "id": "121ef03c-eb5a-4b97-b2f1-4add6adfb080", - "prevId": "d0bfd316-b8f5-459b-ab17-0ce679479321", - "tables": { - "app_metadata": { - "name": "app_metadata", - "columns": { - "key": { - "name": "key", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "backup_schedule_mirrors_table": { - "name": "backup_schedule_mirrors_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "schedule_id": { - "name": "schedule_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "repository_id": { - "name": "repository_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "enabled": { - "name": "enabled", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "last_copy_at": { - "name": "last_copy_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_copy_status": { - "name": "last_copy_status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_copy_error": { - "name": "last_copy_error", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": {}, - "foreignKeys": { - "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": { - "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk", - "tableFrom": "backup_schedule_mirrors_table", - "tableTo": "backup_schedules_table", - "columnsFrom": ["schedule_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": { - "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk", - "tableFrom": "backup_schedule_mirrors_table", - "tableTo": "repositories_table", - "columnsFrom": ["repository_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "backup_schedule_notifications_table": { - "name": "backup_schedule_notifications_table", - "columns": { - "schedule_id": { - "name": "schedule_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "destination_id": { - "name": "destination_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "notify_on_start": { - "name": "notify_on_start", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "notify_on_success": { - "name": "notify_on_success", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "notify_on_failure": { - "name": "notify_on_failure", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": {}, - "foreignKeys": { - "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": { - "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk", - "tableFrom": "backup_schedule_notifications_table", - "tableTo": "backup_schedules_table", - "columnsFrom": ["schedule_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": { - "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk", - "tableFrom": "backup_schedule_notifications_table", - "tableTo": "notification_destinations_table", - "columnsFrom": ["destination_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "backup_schedule_notifications_table_schedule_id_destination_id_pk": { - "columns": ["schedule_id", "destination_id"], - "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk" - } - }, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "backup_schedules_table": { - "name": "backup_schedules_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "volume_id": { - "name": "volume_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "repository_id": { - "name": "repository_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "enabled": { - "name": "enabled", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "cron_expression": { - "name": "cron_expression", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "retention_policy": { - "name": "retention_policy", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "exclude_patterns": { - "name": "exclude_patterns", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'[]'" - }, - "include_patterns": { - "name": "include_patterns", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'[]'" - }, - "last_backup_at": { - "name": "last_backup_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_backup_status": { - "name": "last_backup_status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_backup_error": { - "name": "last_backup_error", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "next_backup_at": { - "name": "next_backup_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": {}, - "foreignKeys": { - "backup_schedules_table_volume_id_volumes_table_id_fk": { - "name": "backup_schedules_table_volume_id_volumes_table_id_fk", - "tableFrom": "backup_schedules_table", - "tableTo": "volumes_table", - "columnsFrom": ["volume_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "backup_schedules_table_repository_id_repositories_table_id_fk": { - "name": "backup_schedules_table_repository_id_repositories_table_id_fk", - "tableFrom": "backup_schedules_table", - "tableTo": "repositories_table", - "columnsFrom": ["repository_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "notification_destinations_table": { - "name": "notification_destinations_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "enabled": { - "name": "enabled", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "config": { - "name": "config", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": { - "notification_destinations_table_name_unique": { - "name": "notification_destinations_table_name_unique", - "columns": ["name"], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "repositories_table": { - "name": "repositories_table", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "short_id": { - "name": "short_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "config": { - "name": "config", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "compression_mode": { - "name": "compression_mode", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'auto'" - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'unknown'" - }, - "last_checked": { - "name": "last_checked", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": { - "repositories_table_short_id_unique": { - "name": "repositories_table_short_id_unique", - "columns": ["short_id"], - "isUnique": true - }, - "repositories_table_name_unique": { - "name": "repositories_table_name_unique", - "columns": ["name"], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "sessions_table": { - "name": "sessions_table", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "expires_at": { - "name": "expires_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": {}, - "foreignKeys": { - "sessions_table_user_id_users_table_id_fk": { - "name": "sessions_table_user_id_users_table_id_fk", - "tableFrom": "sessions_table", - "tableTo": "users_table", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "users_table": { - "name": "users_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "username": { - "name": "username", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password_hash": { - "name": "password_hash", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "has_downloaded_restic_password": { - "name": "has_downloaded_restic_password", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": { - "users_table_username_unique": { - "name": "users_table_username_unique", - "columns": ["username"], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "volumes_table": { - "name": "volumes_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "short_id": { - "name": "short_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'unmounted'" - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_health_check": { - "name": "last_health_check", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "config": { - "name": "config", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "auto_remount": { - "name": "auto_remount", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - } - }, - "indexes": { - "volumes_table_short_id_unique": { - "name": "volumes_table_short_id_unique", - "columns": ["short_id"], - "isUnique": true - }, - "volumes_table_name_unique": { - "name": "volumes_table_name_unique", - "columns": ["name"], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} + "version": "6", + "dialect": "sqlite", + "id": "d5a60aea-4490-423e-8725-6ace87a76c9b", + "prevId": "d0bfd316-b8f5-459b-ab17-0ce679479321", + "tables": { + "app_metadata": { + "name": "app_metadata", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedule_mirrors_table": { + "name": "backup_schedule_mirrors_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_copy_at": { + "name": "last_copy_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_copy_status": { + "name": "last_copy_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_copy_error": { + "name": "last_copy_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "backup_schedule_mirrors_table_schedule_id_repository_id_unique": { + "name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique", + "columns": [ + "schedule_id", + "repository_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": { + "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk", + "tableFrom": "backup_schedule_mirrors_table", + "tableTo": "backup_schedules_table", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedule_mirrors_table", + "tableTo": "repositories_table", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedule_notifications_table": { + "name": "backup_schedule_notifications_table", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "destination_id": { + "name": "destination_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notify_on_start": { + "name": "notify_on_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_success": { + "name": "notify_on_success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_failure": { + "name": "notify_on_failure", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": { + "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "tableTo": "backup_schedules_table", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": { + "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "tableTo": "notification_destinations_table", + "columnsFrom": [ + "destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "backup_schedule_notifications_table_schedule_id_destination_id_pk": { + "columns": [ + "schedule_id", + "destination_id" + ], + "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedules_table": { + "name": "backup_schedules_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "volume_id": { + "name": "volume_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exclude_patterns": { + "name": "exclude_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "include_patterns": { + "name": "include_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "last_backup_at": { + "name": "last_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_status": { + "name": "last_backup_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_error": { + "name": "last_backup_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_backup_at": { + "name": "next_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedules_table_volume_id_volumes_table_id_fk": { + "name": "backup_schedules_table_volume_id_volumes_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "volumes_table", + "columnsFrom": [ + "volume_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedules_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "repositories_table", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_destinations_table": { + "name": "notification_destinations_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "notification_destinations_table_name_unique": { + "name": "notification_destinations_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories_table": { + "name": "repositories_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compression_mode": { + "name": "compression_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'auto'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'unknown'" + }, + "last_checked": { + "name": "last_checked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "repositories_table_short_id_unique": { + "name": "repositories_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "repositories_table_name_unique": { + "name": "repositories_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions_table": { + "name": "sessions_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_table_user_id_users_table_id_fk": { + "name": "sessions_table_user_id_users_table_id_fk", + "tableFrom": "sessions_table", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "has_downloaded_restic_password": { + "name": "has_downloaded_restic_password", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes_table": { + "name": "volumes_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unmounted'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_health_check": { + "name": "last_health_check", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto_remount": { + "name": "auto_remount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "volumes_table_short_id_unique": { + "name": "volumes_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "volumes_table_name_unique": { + "name": "volumes_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/app/drizzle/meta/0019_snapshot.json b/app/drizzle/meta/0019_snapshot.json deleted file mode 100644 index ff1f037..0000000 --- a/app/drizzle/meta/0019_snapshot.json +++ /dev/null @@ -1,792 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "dedfb246-68e7-4590-af52-6476eb2999d1", - "prevId": "121ef03c-eb5a-4b97-b2f1-4add6adfb080", - "tables": { - "app_metadata": { - "name": "app_metadata", - "columns": { - "key": { - "name": "key", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "backup_schedule_mirrors_table": { - "name": "backup_schedule_mirrors_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "schedule_id": { - "name": "schedule_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "repository_id": { - "name": "repository_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "enabled": { - "name": "enabled", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "last_copy_at": { - "name": "last_copy_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_copy_status": { - "name": "last_copy_status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_copy_error": { - "name": "last_copy_error", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": { - "backup_schedule_mirrors_table_schedule_id_repository_id_unique": { - "name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique", - "columns": [ - "schedule_id", - "repository_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": { - "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk", - "tableFrom": "backup_schedule_mirrors_table", - "tableTo": "backup_schedules_table", - "columnsFrom": [ - "schedule_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": { - "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk", - "tableFrom": "backup_schedule_mirrors_table", - "tableTo": "repositories_table", - "columnsFrom": [ - "repository_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "backup_schedule_notifications_table": { - "name": "backup_schedule_notifications_table", - "columns": { - "schedule_id": { - "name": "schedule_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "destination_id": { - "name": "destination_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "notify_on_start": { - "name": "notify_on_start", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "notify_on_success": { - "name": "notify_on_success", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "notify_on_failure": { - "name": "notify_on_failure", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": {}, - "foreignKeys": { - "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": { - "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk", - "tableFrom": "backup_schedule_notifications_table", - "tableTo": "backup_schedules_table", - "columnsFrom": [ - "schedule_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": { - "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk", - "tableFrom": "backup_schedule_notifications_table", - "tableTo": "notification_destinations_table", - "columnsFrom": [ - "destination_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "backup_schedule_notifications_table_schedule_id_destination_id_pk": { - "columns": [ - "schedule_id", - "destination_id" - ], - "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk" - } - }, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "backup_schedules_table": { - "name": "backup_schedules_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "volume_id": { - "name": "volume_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "repository_id": { - "name": "repository_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "enabled": { - "name": "enabled", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "cron_expression": { - "name": "cron_expression", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "retention_policy": { - "name": "retention_policy", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "exclude_patterns": { - "name": "exclude_patterns", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'[]'" - }, - "include_patterns": { - "name": "include_patterns", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'[]'" - }, - "last_backup_at": { - "name": "last_backup_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_backup_status": { - "name": "last_backup_status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_backup_error": { - "name": "last_backup_error", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "next_backup_at": { - "name": "next_backup_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": {}, - "foreignKeys": { - "backup_schedules_table_volume_id_volumes_table_id_fk": { - "name": "backup_schedules_table_volume_id_volumes_table_id_fk", - "tableFrom": "backup_schedules_table", - "tableTo": "volumes_table", - "columnsFrom": [ - "volume_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "backup_schedules_table_repository_id_repositories_table_id_fk": { - "name": "backup_schedules_table_repository_id_repositories_table_id_fk", - "tableFrom": "backup_schedules_table", - "tableTo": "repositories_table", - "columnsFrom": [ - "repository_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "notification_destinations_table": { - "name": "notification_destinations_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "enabled": { - "name": "enabled", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "config": { - "name": "config", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": { - "notification_destinations_table_name_unique": { - "name": "notification_destinations_table_name_unique", - "columns": [ - "name" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "repositories_table": { - "name": "repositories_table", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "short_id": { - "name": "short_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "config": { - "name": "config", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "compression_mode": { - "name": "compression_mode", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'auto'" - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false, - "default": "'unknown'" - }, - "last_checked": { - "name": "last_checked", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": { - "repositories_table_short_id_unique": { - "name": "repositories_table_short_id_unique", - "columns": [ - "short_id" - ], - "isUnique": true - }, - "repositories_table_name_unique": { - "name": "repositories_table_name_unique", - "columns": [ - "name" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "sessions_table": { - "name": "sessions_table", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "expires_at": { - "name": "expires_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": {}, - "foreignKeys": { - "sessions_table_user_id_users_table_id_fk": { - "name": "sessions_table_user_id_users_table_id_fk", - "tableFrom": "sessions_table", - "tableTo": "users_table", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "users_table": { - "name": "users_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "username": { - "name": "username", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password_hash": { - "name": "password_hash", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "has_downloaded_restic_password": { - "name": "has_downloaded_restic_password", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": { - "users_table_username_unique": { - "name": "users_table_username_unique", - "columns": [ - "username" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "volumes_table": { - "name": "volumes_table", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "short_id": { - "name": "short_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'unmounted'" - }, - "last_error": { - "name": "last_error", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_health_check": { - "name": "last_health_check", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "config": { - "name": "config", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "auto_remount": { - "name": "auto_remount", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - } - }, - "indexes": { - "volumes_table_short_id_unique": { - "name": "volumes_table_short_id_unique", - "columns": [ - "short_id" - ], - "isUnique": true - }, - "volumes_table_name_unique": { - "name": "volumes_table_name_unique", - "columns": [ - "name" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/app/drizzle/meta/_journal.json b/app/drizzle/meta/_journal.json index 4ae3a51..46fb2dd 100644 --- a/app/drizzle/meta/_journal.json +++ b/app/drizzle/meta/_journal.json @@ -131,15 +131,8 @@ { "idx": 18, "version": "6", - "when": 1764619898949, - "tag": "0018_bizarre_zzzax", - "breakpoints": true - }, - { - "idx": 19, - "version": "6", - "when": 1764790151212, - "tag": "0019_heavy_shen", + "when": 1764794371040, + "tag": "0018_breezy_invaders", "breakpoints": true } ] From 1e20fb225e210c4d17bca348d28c3b230f82e3aa Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Thu, 4 Dec 2025 18:31:00 +0100 Subject: [PATCH 4/8] feat: naming backup schedules (#103) * docs: add agents instructions * feat: naming backup schedules * fix: wrong table for filtering --- .github/copilot-instructions.md | 1 + AGENTS.md | 267 ++++++ .../api-client/@tanstack/react-query.gen.ts | 48 +- app/client/api-client/client.gen.ts | 4 +- app/client/api-client/sdk.gen.ts | 540 ++++-------- app/client/api-client/types.gen.ts | 7 + .../components/create-schedule-form.tsx | 17 + .../backups/components/schedule-summary.tsx | 18 +- .../modules/backups/routes/backup-details.tsx | 9 +- app/client/modules/backups/routes/backups.tsx | 21 +- .../modules/backups/routes/create-backup.tsx | 1 + app/drizzle/0019_secret_nomad.sql | 24 + app/drizzle/meta/0019_snapshot.json | 807 ++++++++++++++++++ app/drizzle/meta/_journal.json | 7 + app/server/db/schema.ts | 1 + app/server/modules/backups/backups.dto.ts | 3 + app/server/modules/backups/backups.service.ts | 23 +- app/server/utils/restic.ts | 4 +- bun.lock | 10 +- package.json | 4 +- 20 files changed, 1382 insertions(+), 434 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 AGENTS.md create mode 100644 app/drizzle/0019_secret_nomad.sql create mode 100644 app/drizzle/meta/0019_snapshot.json diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..11d4b9c --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +- This project uses the AGENTS.md file to give detailed information about the repository structure and development commands. Make sure to read this file before starting development. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..136692d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,267 @@ +# AGENTS.md + +## Important instructions + +- Never create migration files manually. Always use the provided command to generate migrations +- If you realize an automated migration is incorrect, make sure to remove all the associated entries from the `_journal.json` and the newly created files located in `app/drizzle/` before re-generating the migration + +## Project Overview + +Zerobyte is a backup automation tool built on top of Restic that provides a web interface for scheduling, managing, and monitoring encrypted backups. It supports multiple volume backends (NFS, SMB, WebDAV, local directories) and repository backends (S3, Azure, GCS, local, and rclone-based storage). + +## Technology Stack + +- **Runtime**: Bun 1.3.1 +- **Server**: Hono (web framework) with Bun runtime +- **Client**: React Router v7 (SSR) with React 19 +- **Database**: SQLite with Drizzle ORM +- **Validation**: ArkType for runtime schema validation +- **Styling**: Tailwind CSS v4 + Radix UI components +- **Architecture**: Unified application structure (not a monorepo) +- **Code Quality**: Biome (formatter & linter) +- **Containerization**: Docker with multi-stage builds + +## Repository Structure + +This is a unified application with the following structure: + +- `app/server` - Bun-based API server with Hono +- `app/client` - React Router SSR frontend components and modules +- `app/schemas` - Shared ArkType schemas for validation +- `app/drizzle` - Database migrations + +### Type Checking + +```bash +# Run type checking and generate React Router types +bun run tsc +``` + +### Building + +```bash +# Build for production +bun run build +``` + +### Database Migrations + +```bash +# Generate new migration from schema changes +bun gen:migrations + +# Generate a custom empty migration +bunx drizzle-kit generate --custom --name=fix-timestamps-to-ms + +``` + +### API Client Generation + +```bash +# Generate TypeScript API client from OpenAPI spec +# Note: Server is always running don't need to start it separately +bun run gen:api-client +``` + +### Code Quality + +```bash +# Format and lint (Biome) +bunx biome check --write . + +# Format only +bunx biome format --write . + +# Lint only +bunx biome lint . +``` + +## Architecture + +### Server Architecture + +The server follows a modular service-oriented architecture: + +**Entry Point**: `app/server/index.ts` + +- Initializes servers using `react-router-hono-server`: + 1. Main API server on port 4096 (REST API + serves static frontend) + 2. Docker volume plugin server on Unix socket `/run/docker/plugins/zerobyte.sock` (optional, if Docker is available) + +**Modules** (`app/server/modules/`): +Each module follows a controller � service � database pattern: + +- `auth/` - User authentication and session management +- `volumes/` - Volume mounting/unmounting (NFS, SMB, WebDAV, directories) +- `repositories/` - Restic repository management (S3, Azure, GCS, local, rclone) +- `backups/` - Backup schedule management and execution +- `notifications/` - Notification system with multiple providers (Discord, email, Gotify, Ntfy, Slack, Pushover) +- `driver/` - Docker volume plugin implementation +- `events/` - Server-Sent Events for real-time updates +- `system/` - System information and capabilities +- `lifecycle/` - Application startup/shutdown hooks + +**Backends** (`app/server/modules/backends/`): +Each volume backend (NFS, SMB, WebDAV, directory) implements mounting logic using system tools (mount.nfs, mount.cifs, davfs2). + +**Jobs** (`app/server/jobs/`): +Cron-based background jobs managed by the Scheduler: + +- `backup-execution.ts` - Runs scheduled backups (every minute) +- `cleanup-dangling.ts` - Removes stale mounts (hourly) +- `healthchecks.ts` - Checks volume health (every 5 minutes) +- `repository-healthchecks.ts` - Validates repositories (every 10 minutes) +- `cleanup-sessions.ts` - Expires old sessions (daily) + +**Core** (`app/server/core/`): + +- `scheduler.ts` - Job scheduling system using node-cron +- `capabilities.ts` - Detects available system features (Docker support, etc.) +- `constants.ts` - Application-wide constants + +**Utils** (`app/server/utils/`): + +- `restic.ts` - Restic CLI wrapper with type-safe output parsing +- `spawn.ts` - Safe subprocess execution helpers +- `logger.ts` - Winston-based logging +- `crypto.ts` - Encryption utilities +- `errors.ts` - Error handling middleware + +**Database** (`app/server/db/`): + +- Uses Drizzle ORM with SQLite +- Schema in `schema.ts` defines: volumes, repositories, backup schedules, notifications, users, sessions +- Migrations: `app/drizzle/` + +### Client Architecture + +**Framework**: React Router v7 with SSR +**Entry Point**: `app/root.tsx` + +The client uses: + +- TanStack Query for server state management +- Auto-generated API client from OpenAPI spec (in `app/client/api-client/`) +- Radix UI primitives with custom Tailwind styling +- Server-Sent Events hook (`use-server-events.ts`) for real-time updates + +Routes are organized in feature modules at `app/client/modules/*/routes/`. + +### Shared Schemas + +`app/schemas/` contains ArkType schemas used by both client and server: + +- Volume configurations (NFS, SMB, WebDAV, directory) +- Repository configurations (S3, Azure, GCS, local, rclone) +- Restic command output parsing types +- Backend status types + +These schemas provide runtime validation and TypeScript types. + +## Restic Integration + +Zerobyte is a wrapper around Restic for backup operations. Key integration points: + +**Repository Management**: + +- Creates/initializes Restic repositories via `restic init` +- Supports multiple backends: local, S3, Azure Blob Storage, Google Cloud Storage, or any rclone-supported backend +- Stores single encryption password in `/var/lib/zerobyte/restic/password` (auto-generated on first run) + +**Backup Operations**: + +- Executes `restic backup` with user-defined schedules (cron expressions) +- Supports include/exclude patterns for selective backups +- Parses JSON output for progress tracking and statistics +- Implements retention policies via `restic forget --prune` + +**Repository Utilities** (`utils/restic.ts`): + +- `buildRepoUrl()` - Constructs repository URLs for different backends +- `buildEnv()` - Sets environment variables (credentials, cache dir) +- `ensurePassfile()` - Manages encryption password file +- Type-safe parsing of Restic JSON output using ArkType schemas + +**Rclone Integration** (`app/server/modules/repositories/`): + +- Allows using any rclone backend as a Restic repository +- Dynamically generates rclone config and passes via environment variables +- Supports backends like Dropbox, Google Drive, OneDrive, Backblaze B2, etc. + +## Docker Volume Plugin + +When Docker socket is available (`/var/run/docker.sock`), Zerobyte registers as a Docker volume plugin: + +**Plugin Location**: `/run/docker/plugins/zerobyte.sock` +**Implementation**: `app/server/modules/driver/driver.controller.ts` + +This allows other containers to mount Zerobyte volumes using Docker. + +The plugin implements the Docker Volume Plugin API v1. + +## Environment & Configuration + +**Runtime Environment Variables**: + +- Database path: `./data/zerobyte.db` (configurable via `drizzle.config.ts`) +- Restic cache: `/var/lib/zerobyte/restic/cache` +- Restic password: `/var/lib/zerobyte/restic/password` +- Volume mounts: `/var/lib/zerobyte/mounts/` +- Local repositories: `/var/lib/zerobyte/repositories/` + +**Capabilities Detection**: +On startup, the server detects available capabilities (see `core/capabilities.ts`): + +- **Docker**: Requires `/var/run/docker.sock` access +- System will gracefully degrade if capabilities are unavailable + +## Common Workflows + +### Adding a New Volume Backend + +1. Create backend implementation in `app/server/modules/backends//` +2. Implement `mount()` and `unmount()` methods +3. Add schema to `app/schemas/volumes.ts` +4. Update `volumeConfigSchema` discriminated union +5. Update backend factory in `app/server/modules/backends/backend.ts` + +### Adding a New Repository Backend + +1. Add backend type to `app/schemas/restic.ts` +2. Update `buildRepoUrl()` in `app/server/utils/restic.ts` +3. Update `buildEnv()` to handle credentials/configuration +4. Add DTO schemas in `app/server/modules/repositories/repositories.dto.ts` +5. Update repository service to handle new backend + +### Adding a New Scheduled Job + +1. Create job class in `app/server/jobs/.ts` extending `Job` +2. Implement `run()` method +3. Register in `app/server/modules/lifecycle/startup.ts` with cron expression: + ```typescript + Scheduler.build(YourJob).schedule("* * * * *"); + ``` + +## Important Notes + +- **Code Style**: Uses Biome with tabs (not spaces), 120 char line width, double quotes +- **Imports**: Organize imports is disabled in Biome - do not auto-organize +- **TypeScript**: Uses `"type": "module"` - all imports must include extensions when targeting Node/Bun +- **Validation**: Prefer ArkType over Zod - it's used throughout the codebase +- **Database**: Timestamps are stored as Unix epoch integers, not ISO strings +- **Security**: Restic password file has 0600 permissions - never expose it +- **Mounting**: Requires privileged container or CAP_SYS_ADMIN for FUSE mounts +- **API Documentation**: OpenAPI spec auto-generated at `/api/v1/openapi.json`, docs at `/api/v1/docs` + +## Docker Development Setup + +The `docker-compose.yml` defines two services: + +- `zerobyte-dev` - Development with hot reload (uses `development` stage) +- `zerobyte-prod` - Production build (uses `production` stage) + +Both mount: + +- `/var/lib/zerobyte` for persistent data +- `/dev/fuse` device for FUSE mounting +- Optionally `/var/run/docker.sock` for Docker plugin functionality diff --git a/app/client/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts index 411da6d..a4ac2a3 100644 --- a/app/client/api-client/@tanstack/react-query.gen.ts +++ b/app/client/api-client/@tanstack/react-query.gen.ts @@ -87,12 +87,10 @@ const createQueryKey = (id: string, options?: TOptions if (options?.query) { params.query = options.query; } - return [ - params - ]; + return [params]; }; -export const getMeQueryKey = (options?: Options) => createQueryKey("getMe", options); +export const getMeQueryKey = (options?: Options) => createQueryKey('getMe', options); /** * Get current authenticated user @@ -110,7 +108,7 @@ export const getMeOptions = (options?: Options) => queryOptions) => createQueryKey("getStatus", options); +export const getStatusQueryKey = (options?: Options) => createQueryKey('getStatus', options); /** * Get authentication system status @@ -145,7 +143,7 @@ export const changePasswordMutation = (options?: Partial) => createQueryKey("listVolumes", options); +export const listVolumesQueryKey = (options?: Options) => createQueryKey('listVolumes', options); /** * List all volumes @@ -214,7 +212,7 @@ export const deleteVolumeMutation = (options?: Partial return mutationOptions; }; -export const getVolumeQueryKey = (options: Options) => createQueryKey("getVolume", options); +export const getVolumeQueryKey = (options: Options) => createQueryKey('getVolume', options); /** * Get a volume by name @@ -249,7 +247,7 @@ export const updateVolumeMutation = (options?: Partial return mutationOptions; }; -export const getContainersUsingVolumeQueryKey = (options: Options) => createQueryKey("getContainersUsingVolume", options); +export const getContainersUsingVolumeQueryKey = (options: Options) => createQueryKey('getContainersUsingVolume', options); /** * Get containers using a volume by name @@ -318,7 +316,7 @@ export const healthCheckVolumeMutation = (options?: Partial) => createQueryKey("listFiles", options); +export const listFilesQueryKey = (options: Options) => createQueryKey('listFiles', options); /** * List files in a volume directory @@ -336,7 +334,7 @@ export const listFilesOptions = (options: Options) => queryOption queryKey: listFilesQueryKey(options) }); -export const browseFilesystemQueryKey = (options?: Options) => createQueryKey("browseFilesystem", options); +export const browseFilesystemQueryKey = (options?: Options) => createQueryKey('browseFilesystem', options); /** * Browse directories on the host filesystem @@ -354,7 +352,7 @@ export const browseFilesystemOptions = (options?: Options) queryKey: browseFilesystemQueryKey(options) }); -export const listRepositoriesQueryKey = (options?: Options) => createQueryKey("listRepositories", options); +export const listRepositoriesQueryKey = (options?: Options) => createQueryKey('listRepositories', options); /** * List all repositories @@ -389,7 +387,7 @@ export const createRepositoryMutation = (options?: Partial) => createQueryKey("listRcloneRemotes", options); +export const listRcloneRemotesQueryKey = (options?: Options) => createQueryKey('listRcloneRemotes', options); /** * List all configured rclone remotes on the host system @@ -424,7 +422,7 @@ export const deleteRepositoryMutation = (options?: Partial) => createQueryKey("getRepository", options); +export const getRepositoryQueryKey = (options: Options) => createQueryKey('getRepository', options); /** * Get a single repository by name @@ -459,7 +457,7 @@ export const updateRepositoryMutation = (options?: Partial) => createQueryKey("listSnapshots", options); +export const listSnapshotsQueryKey = (options: Options) => createQueryKey('listSnapshots', options); /** * List all snapshots in a repository @@ -494,7 +492,7 @@ export const deleteSnapshotMutation = (options?: Partial) => createQueryKey("getSnapshotDetails", options); +export const getSnapshotDetailsQueryKey = (options: Options) => createQueryKey('getSnapshotDetails', options); /** * Get details of a specific snapshot @@ -512,7 +510,7 @@ export const getSnapshotDetailsOptions = (options: Options) => createQueryKey("listSnapshotFiles", options); +export const listSnapshotFilesQueryKey = (options: Options) => createQueryKey('listSnapshotFiles', options); /** * List files and directories in a snapshot @@ -564,7 +562,7 @@ export const doctorRepositoryMutation = (options?: Partial) => createQueryKey("listBackupSchedules", options); +export const listBackupSchedulesQueryKey = (options?: Options) => createQueryKey('listBackupSchedules', options); /** * List all backup schedules @@ -616,7 +614,7 @@ export const deleteBackupScheduleMutation = (options?: Partial) => createQueryKey("getBackupSchedule", options); +export const getBackupScheduleQueryKey = (options: Options) => createQueryKey('getBackupSchedule', options); /** * Get a backup schedule by ID @@ -651,7 +649,7 @@ export const updateBackupScheduleMutation = (options?: Partial) => createQueryKey("getBackupScheduleForVolume", options); +export const getBackupScheduleForVolumeQueryKey = (options: Options) => createQueryKey('getBackupScheduleForVolume', options); /** * Get a backup schedule for a specific volume @@ -720,7 +718,7 @@ export const runForgetMutation = (options?: Partial>): Us return mutationOptions; }; -export const getScheduleNotificationsQueryKey = (options: Options) => createQueryKey("getScheduleNotifications", options); +export const getScheduleNotificationsQueryKey = (options: Options) => createQueryKey('getScheduleNotifications', options); /** * Get notification assignments for a backup schedule @@ -755,7 +753,7 @@ export const updateScheduleNotificationsMutation = (options?: Partial) => createQueryKey("getScheduleMirrors", options); +export const getScheduleMirrorsQueryKey = (options: Options) => createQueryKey('getScheduleMirrors', options); /** * Get mirror repository assignments for a backup schedule @@ -790,7 +788,7 @@ export const updateScheduleMirrorsMutation = (options?: Partial) => createQueryKey("getMirrorCompatibility", options); +export const getMirrorCompatibilityQueryKey = (options: Options) => createQueryKey('getMirrorCompatibility', options); /** * Get mirror compatibility info for all repositories relative to a backup schedule's primary repository @@ -808,7 +806,7 @@ export const getMirrorCompatibilityOptions = (options: Options) => createQueryKey("listNotificationDestinations", options); +export const listNotificationDestinationsQueryKey = (options?: Options) => createQueryKey('listNotificationDestinations', options); /** * List all notification destinations @@ -860,7 +858,7 @@ export const deleteNotificationDestinationMutation = (options?: Partial) => createQueryKey("getNotificationDestination", options); +export const getNotificationDestinationQueryKey = (options: Options) => createQueryKey('getNotificationDestination', options); /** * Get a notification destination by ID @@ -912,7 +910,7 @@ export const testNotificationDestinationMutation = (options?: Partial) => createQueryKey("getSystemInfo", options); +export const getSystemInfoQueryKey = (options?: Options) => createQueryKey('getSystemInfo', options); /** * Get system information including available capabilities diff --git a/app/client/api-client/client.gen.ts b/app/client/api-client/client.gen.ts index 50c3a3f..4dc9424 100644 --- a/app/client/api-client/client.gen.ts +++ b/app/client/api-client/client.gen.ts @@ -13,6 +13,4 @@ import type { ClientOptions as ClientOptions2 } from './types.gen'; */ export type CreateClientConfig = (override?: Config) => Config & T>; -export const client = createClient(createConfig({ - baseUrl: 'http://192.168.2.42:4096' -})); +export const client = createClient(createConfig({ baseUrl: 'http://192.168.2.42:4096' })); diff --git a/app/client/api-client/sdk.gen.ts b/app/client/api-client/sdk.gen.ts index 1b8c032..c27c269 100644 --- a/app/client/api-client/sdk.gen.ts +++ b/app/client/api-client/sdk.gen.ts @@ -21,583 +21,371 @@ export type Options(options?: Options) => { - return (options?.client ?? client).post({ - url: '/api/v1/auth/register', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options?.headers - } - }); -}; +export const register = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/auth/register', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * Login with username and password */ -export const login = (options?: Options) => { - return (options?.client ?? client).post({ - url: '/api/v1/auth/login', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options?.headers - } - }); -}; +export const login = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/auth/login', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * Logout current user */ -export const logout = (options?: Options) => { - return (options?.client ?? client).post({ - url: '/api/v1/auth/logout', - ...options - }); -}; +export const logout = (options?: Options) => (options?.client ?? client).post({ url: '/api/v1/auth/logout', ...options }); /** * Get current authenticated user */ -export const getMe = (options?: Options) => { - return (options?.client ?? client).get({ - url: '/api/v1/auth/me', - ...options - }); -}; +export const getMe = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/me', ...options }); /** * Get authentication system status */ -export const getStatus = (options?: Options) => { - return (options?.client ?? client).get({ - url: '/api/v1/auth/status', - ...options - }); -}; +export const getStatus = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/status', ...options }); /** * Change current user password */ -export const changePassword = (options?: Options) => { - return (options?.client ?? client).post({ - url: '/api/v1/auth/change-password', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options?.headers - } - }); -}; +export const changePassword = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/auth/change-password', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * List all volumes */ -export const listVolumes = (options?: Options) => { - return (options?.client ?? client).get({ - url: '/api/v1/volumes', - ...options - }); -}; +export const listVolumes = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/volumes', ...options }); /** * Create a new volume */ -export const createVolume = (options?: Options) => { - return (options?.client ?? client).post({ - url: '/api/v1/volumes', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options?.headers - } - }); -}; +export const createVolume = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/volumes', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * Test connection to backend */ -export const testConnection = (options?: Options) => { - return (options?.client ?? client).post({ - url: '/api/v1/volumes/test-connection', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options?.headers - } - }); -}; +export const testConnection = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/volumes/test-connection', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * Delete a volume */ -export const deleteVolume = (options: Options) => { - return (options.client ?? client).delete({ - url: '/api/v1/volumes/{name}', - ...options - }); -}; +export const deleteVolume = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/volumes/{name}', ...options }); /** * Get a volume by name */ -export const getVolume = (options: Options) => { - return (options.client ?? client).get({ - url: '/api/v1/volumes/{name}', - ...options - }); -}; +export const getVolume = (options: Options) => (options.client ?? client).get({ url: '/api/v1/volumes/{name}', ...options }); /** * Update a volume's configuration */ -export const updateVolume = (options: Options) => { - return (options.client ?? client).put({ - url: '/api/v1/volumes/{name}', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } - }); -}; +export const updateVolume = (options: Options) => (options.client ?? client).put({ + url: '/api/v1/volumes/{name}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * Get containers using a volume by name */ -export const getContainersUsingVolume = (options: Options) => { - return (options.client ?? client).get({ - url: '/api/v1/volumes/{name}/containers', - ...options - }); -}; +export const getContainersUsingVolume = (options: Options) => (options.client ?? client).get({ url: '/api/v1/volumes/{name}/containers', ...options }); /** * Mount a volume */ -export const mountVolume = (options: Options) => { - return (options.client ?? client).post({ - url: '/api/v1/volumes/{name}/mount', - ...options - }); -}; +export const mountVolume = (options: Options) => (options.client ?? client).post({ url: '/api/v1/volumes/{name}/mount', ...options }); /** * Unmount a volume */ -export const unmountVolume = (options: Options) => { - return (options.client ?? client).post({ - url: '/api/v1/volumes/{name}/unmount', - ...options - }); -}; +export const unmountVolume = (options: Options) => (options.client ?? client).post({ url: '/api/v1/volumes/{name}/unmount', ...options }); /** * Perform a health check on a volume */ -export const healthCheckVolume = (options: Options) => { - return (options.client ?? client).post({ - url: '/api/v1/volumes/{name}/health-check', - ...options - }); -}; +export const healthCheckVolume = (options: Options) => (options.client ?? client).post({ url: '/api/v1/volumes/{name}/health-check', ...options }); /** * List files in a volume directory */ -export const listFiles = (options: Options) => { - return (options.client ?? client).get({ - url: '/api/v1/volumes/{name}/files', - ...options - }); -}; +export const listFiles = (options: Options) => (options.client ?? client).get({ url: '/api/v1/volumes/{name}/files', ...options }); /** * Browse directories on the host filesystem */ -export const browseFilesystem = (options?: Options) => { - return (options?.client ?? client).get({ - url: '/api/v1/volumes/filesystem/browse', - ...options - }); -}; +export const browseFilesystem = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/volumes/filesystem/browse', ...options }); /** * List all repositories */ -export const listRepositories = (options?: Options) => { - return (options?.client ?? client).get({ - url: '/api/v1/repositories', - ...options - }); -}; +export const listRepositories = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/repositories', ...options }); /** * Create a new restic repository */ -export const createRepository = (options?: Options) => { - return (options?.client ?? client).post({ - url: '/api/v1/repositories', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options?.headers - } - }); -}; +export const createRepository = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/repositories', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * List all configured rclone remotes on the host system */ -export const listRcloneRemotes = (options?: Options) => { - return (options?.client ?? client).get({ - url: '/api/v1/repositories/rclone-remotes', - ...options - }); -}; +export const listRcloneRemotes = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/repositories/rclone-remotes', ...options }); /** * Delete a repository */ -export const deleteRepository = (options: Options) => { - return (options.client ?? client).delete({ - url: '/api/v1/repositories/{name}', - ...options - }); -}; +export const deleteRepository = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/repositories/{name}', ...options }); /** * Get a single repository by name */ -export const getRepository = (options: Options) => { - return (options.client ?? client).get({ - url: '/api/v1/repositories/{name}', - ...options - }); -}; +export const getRepository = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{name}', ...options }); /** * Update a repository's name or settings */ -export const updateRepository = (options: Options) => { - return (options.client ?? client).patch({ - url: '/api/v1/repositories/{name}', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } - }); -}; +export const updateRepository = (options: Options) => (options.client ?? client).patch({ + url: '/api/v1/repositories/{name}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * List all snapshots in a repository */ -export const listSnapshots = (options: Options) => { - return (options.client ?? client).get({ - url: '/api/v1/repositories/{name}/snapshots', - ...options - }); -}; +export const listSnapshots = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{name}/snapshots', ...options }); /** * Delete a specific snapshot from a repository */ -export const deleteSnapshot = (options: Options) => { - return (options.client ?? client).delete({ - url: '/api/v1/repositories/{name}/snapshots/{snapshotId}', - ...options - }); -}; +export const deleteSnapshot = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/repositories/{name}/snapshots/{snapshotId}', ...options }); /** * Get details of a specific snapshot */ -export const getSnapshotDetails = (options: Options) => { - return (options.client ?? client).get({ - url: '/api/v1/repositories/{name}/snapshots/{snapshotId}', - ...options - }); -}; +export const getSnapshotDetails = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{name}/snapshots/{snapshotId}', ...options }); /** * List files and directories in a snapshot */ -export const listSnapshotFiles = (options: Options) => { - return (options.client ?? client).get({ - url: '/api/v1/repositories/{name}/snapshots/{snapshotId}/files', - ...options - }); -}; +export const listSnapshotFiles = (options: Options) => (options.client ?? client).get({ url: '/api/v1/repositories/{name}/snapshots/{snapshotId}/files', ...options }); /** * Restore a snapshot to a target path on the filesystem */ -export const restoreSnapshot = (options: Options) => { - return (options.client ?? client).post({ - url: '/api/v1/repositories/{name}/restore', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } - }); -}; +export const restoreSnapshot = (options: Options) => (options.client ?? client).post({ + url: '/api/v1/repositories/{name}/restore', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * Run doctor operations on a repository to fix common issues (unlock, check, repair index). Use this when the repository is locked or has errors. */ -export const doctorRepository = (options: Options) => { - return (options.client ?? client).post({ - url: '/api/v1/repositories/{name}/doctor', - ...options - }); -}; +export const doctorRepository = (options: Options) => (options.client ?? client).post({ url: '/api/v1/repositories/{name}/doctor', ...options }); /** * List all backup schedules */ -export const listBackupSchedules = (options?: Options) => { - return (options?.client ?? client).get({ - url: '/api/v1/backups', - ...options - }); -}; +export const listBackupSchedules = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/backups', ...options }); /** * Create a new backup schedule for a volume */ -export const createBackupSchedule = (options?: Options) => { - return (options?.client ?? client).post({ - url: '/api/v1/backups', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options?.headers - } - }); -}; +export const createBackupSchedule = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/backups', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * Delete a backup schedule */ -export const deleteBackupSchedule = (options: Options) => { - return (options.client ?? client).delete({ - url: '/api/v1/backups/{scheduleId}', - ...options - }); -}; +export const deleteBackupSchedule = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/backups/{scheduleId}', ...options }); /** * Get a backup schedule by ID */ -export const getBackupSchedule = (options: Options) => { - return (options.client ?? client).get({ - url: '/api/v1/backups/{scheduleId}', - ...options - }); -}; +export const getBackupSchedule = (options: Options) => (options.client ?? client).get({ url: '/api/v1/backups/{scheduleId}', ...options }); /** * Update a backup schedule */ -export const updateBackupSchedule = (options: Options) => { - return (options.client ?? client).patch({ - url: '/api/v1/backups/{scheduleId}', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } - }); -}; +export const updateBackupSchedule = (options: Options) => (options.client ?? client).patch({ + url: '/api/v1/backups/{scheduleId}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * Get a backup schedule for a specific volume */ -export const getBackupScheduleForVolume = (options: Options) => { - return (options.client ?? client).get({ - url: '/api/v1/backups/volume/{volumeId}', - ...options - }); -}; +export const getBackupScheduleForVolume = (options: Options) => (options.client ?? client).get({ url: '/api/v1/backups/volume/{volumeId}', ...options }); /** * Trigger a backup immediately for a schedule */ -export const runBackupNow = (options: Options) => { - return (options.client ?? client).post({ - url: '/api/v1/backups/{scheduleId}/run', - ...options - }); -}; +export const runBackupNow = (options: Options) => (options.client ?? client).post({ url: '/api/v1/backups/{scheduleId}/run', ...options }); /** * Stop a backup that is currently in progress */ -export const stopBackup = (options: Options) => { - return (options.client ?? client).post({ - url: '/api/v1/backups/{scheduleId}/stop', - ...options - }); -}; +export const stopBackup = (options: Options) => (options.client ?? client).post({ url: '/api/v1/backups/{scheduleId}/stop', ...options }); /** * Manually apply retention policy to clean up old snapshots */ -export const runForget = (options: Options) => { - return (options.client ?? client).post({ - url: '/api/v1/backups/{scheduleId}/forget', - ...options - }); -}; +export const runForget = (options: Options) => (options.client ?? client).post({ url: '/api/v1/backups/{scheduleId}/forget', ...options }); /** * Get notification assignments for a backup schedule */ -export const getScheduleNotifications = (options: Options) => { - return (options.client ?? client).get({ - url: '/api/v1/backups/{scheduleId}/notifications', - ...options - }); -}; +export const getScheduleNotifications = (options: Options) => (options.client ?? client).get({ url: '/api/v1/backups/{scheduleId}/notifications', ...options }); /** * Update notification assignments for a backup schedule */ -export const updateScheduleNotifications = (options: Options) => { - return (options.client ?? client).put({ - url: '/api/v1/backups/{scheduleId}/notifications', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } - }); -}; +export const updateScheduleNotifications = (options: Options) => (options.client ?? client).put({ + url: '/api/v1/backups/{scheduleId}/notifications', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * Get mirror repository assignments for a backup schedule */ -export const getScheduleMirrors = (options: Options) => { - return (options.client ?? client).get({ - url: '/api/v1/backups/{scheduleId}/mirrors', - ...options - }); -}; +export const getScheduleMirrors = (options: Options) => (options.client ?? client).get({ url: '/api/v1/backups/{scheduleId}/mirrors', ...options }); /** * Update mirror repository assignments for a backup schedule */ -export const updateScheduleMirrors = (options: Options) => { - return (options.client ?? client).put({ - url: '/api/v1/backups/{scheduleId}/mirrors', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } - }); -}; +export const updateScheduleMirrors = (options: Options) => (options.client ?? client).put({ + url: '/api/v1/backups/{scheduleId}/mirrors', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * Get mirror compatibility info for all repositories relative to a backup schedule's primary repository */ -export const getMirrorCompatibility = (options: Options) => { - return (options.client ?? client).get({ - url: '/api/v1/backups/{scheduleId}/mirrors/compatibility', - ...options - }); -}; +export const getMirrorCompatibility = (options: Options) => (options.client ?? client).get({ url: '/api/v1/backups/{scheduleId}/mirrors/compatibility', ...options }); /** * List all notification destinations */ -export const listNotificationDestinations = (options?: Options) => { - return (options?.client ?? client).get({ - url: '/api/v1/notifications/destinations', - ...options - }); -}; +export const listNotificationDestinations = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/notifications/destinations', ...options }); /** * Create a new notification destination */ -export const createNotificationDestination = (options?: Options) => { - return (options?.client ?? client).post({ - url: '/api/v1/notifications/destinations', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options?.headers - } - }); -}; +export const createNotificationDestination = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/notifications/destinations', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); /** * Delete a notification destination */ -export const deleteNotificationDestination = (options: Options) => { - return (options.client ?? client).delete({ - url: '/api/v1/notifications/destinations/{id}', - ...options - }); -}; +export const deleteNotificationDestination = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/notifications/destinations/{id}', ...options }); /** * Get a notification destination by ID */ -export const getNotificationDestination = (options: Options) => { - return (options.client ?? client).get({ - url: '/api/v1/notifications/destinations/{id}', - ...options - }); -}; +export const getNotificationDestination = (options: Options) => (options.client ?? client).get({ url: '/api/v1/notifications/destinations/{id}', ...options }); /** * Update a notification destination */ -export const updateNotificationDestination = (options: Options) => { - return (options.client ?? client).patch({ - url: '/api/v1/notifications/destinations/{id}', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } - }); -}; +export const updateNotificationDestination = (options: Options) => (options.client ?? client).patch({ + url: '/api/v1/notifications/destinations/{id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); /** * Test a notification destination by sending a test message */ -export const testNotificationDestination = (options: Options) => { - return (options.client ?? client).post({ - url: '/api/v1/notifications/destinations/{id}/test', - ...options - }); -}; +export const testNotificationDestination = (options: Options) => (options.client ?? client).post({ url: '/api/v1/notifications/destinations/{id}/test', ...options }); /** * Get system information including available capabilities */ -export const getSystemInfo = (options?: Options) => { - return (options?.client ?? client).get({ - url: '/api/v1/system/info', - ...options - }); -}; +export const getSystemInfo = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/system/info', ...options }); /** * Download the Restic password file for backup recovery. Requires password re-authentication. */ -export const downloadResticPassword = (options?: Options) => { - return (options?.client ?? client).post({ - url: '/api/v1/system/restic-password', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options?.headers - } - }); -}; +export const downloadResticPassword = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/system/restic-password', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index bb15e40..6a8bc0d 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -1297,6 +1297,7 @@ export type ListBackupSchedulesResponses = { lastBackupAt: number | null; lastBackupError: string | null; lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; + name: string; nextBackupAt: number | null; repository: { compressionMode: 'auto' | 'max' | 'off' | null; @@ -1435,6 +1436,7 @@ export type CreateBackupScheduleData = { body?: { cronExpression: string; enabled: boolean; + name: string; repositoryId: string; volumeId: number; excludePatterns?: Array; @@ -1469,6 +1471,7 @@ export type CreateBackupScheduleResponses = { lastBackupAt: number | null; lastBackupError: string | null; lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; + name: string; nextBackupAt: number | null; repositoryId: string; retentionPolicy: { @@ -1530,6 +1533,7 @@ export type GetBackupScheduleResponses = { lastBackupAt: number | null; lastBackupError: string | null; lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; + name: string; nextBackupAt: number | null; repository: { compressionMode: 'auto' | 'max' | 'off' | null; @@ -1671,6 +1675,7 @@ export type UpdateBackupScheduleData = { enabled?: boolean; excludePatterns?: Array; includePatterns?: Array; + name?: string; retentionPolicy?: { keepDaily?: number; keepHourly?: number; @@ -1703,6 +1708,7 @@ export type UpdateBackupScheduleResponses = { lastBackupAt: number | null; lastBackupError: string | null; lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; + name: string; nextBackupAt: number | null; repositoryId: string; retentionPolicy: { @@ -1744,6 +1750,7 @@ export type GetBackupScheduleForVolumeResponses = { lastBackupAt: number | null; lastBackupError: string | null; lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; + name: string; nextBackupAt: number | null; repository: { compressionMode: 'auto' | 'max' | 'off' | null; diff --git a/app/client/modules/backups/components/create-schedule-form.tsx b/app/client/modules/backups/components/create-schedule-form.tsx index ee4d1d8..1fdede6 100644 --- a/app/client/modules/backups/components/create-schedule-form.tsx +++ b/app/client/modules/backups/components/create-schedule-form.tsx @@ -23,6 +23,7 @@ import type { BackupSchedule, Volume } from "~/client/lib/types"; import { deepClean } from "~/utils/object"; const internalFormSchema = type({ + name: "1 <= string <= 32", repositoryId: "string", excludePatternsText: "string?", includePatterns: "string[]?", @@ -80,6 +81,7 @@ const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalFormValu const weeklyDay = frequency === "weekly" ? dayOfWeekPart : undefined; return { + name: schedule.name, repositoryId: schedule.repositoryId, frequency, dailyTime, @@ -148,6 +150,21 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: + ( + + Backup name + + + + A unique name to identify this backup schedule. + + + )} + /> + {
- Backup schedule - - Automated backup configuration for volume  - {schedule.volume.name} + {schedule.name} + + + + {schedule.volume.name} + + + + + {schedule.repository.name} +
diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx index be07779..5a03609 100644 --- a/app/client/modules/backups/routes/backup-details.tsx +++ b/app/client/modules/backups/routes/backup-details.tsx @@ -35,10 +35,10 @@ import { ScheduleMirrorsConfig } from "../components/schedule-mirrors-config"; import { cn } from "~/client/lib/utils"; export const handle = { - breadcrumb: (match: Route.MetaArgs) => [ - { label: "Backups", href: "/backups" }, - { label: `Schedule #${match.params.id}` }, - ], + breadcrumb: (match: Route.MetaArgs) => { + const data = match.loaderData; + return [{ label: "Backups", href: "/backups" }, { label: data.schedule.name }]; + }, }; export function meta(_: Route.MetaArgs) { @@ -153,6 +153,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon updateSchedule.mutate({ path: { scheduleId: schedule.id.toString() }, body: { + name: formValues.name, repositoryId: formValues.repositoryId, enabled: schedule.enabled, cronExpression, diff --git a/app/client/modules/backups/routes/backups.tsx b/app/client/modules/backups/routes/backups.tsx index 7a547f0..b6ac602 100644 --- a/app/client/modules/backups/routes/backups.tsx +++ b/app/client/modules/backups/routes/backups.tsx @@ -67,13 +67,11 @@ export default function Backups({ loaderData }: Route.ComponentProps) { {schedules.map((schedule) => ( - -
-
- - - Volume {schedule.volume.name} - + +
+
+ + {schedule.name}
- - - {schedule.repository.name} + + + {schedule.volume.name} + + + {schedule.repository.name}
diff --git a/app/client/modules/backups/routes/create-backup.tsx b/app/client/modules/backups/routes/create-backup.tsx index f982086..400da91 100644 --- a/app/client/modules/backups/routes/create-backup.tsx +++ b/app/client/modules/backups/routes/create-backup.tsx @@ -83,6 +83,7 @@ export default function CreateBackup({ loaderData }: Route.ComponentProps) { createSchedule.mutate({ body: { + name: formValues.name, volumeId: selectedVolumeId, repositoryId: formValues.repositoryId, enabled: true, diff --git a/app/drizzle/0019_secret_nomad.sql b/app/drizzle/0019_secret_nomad.sql new file mode 100644 index 0000000..b8e3bbb --- /dev/null +++ b/app/drizzle/0019_secret_nomad.sql @@ -0,0 +1,24 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_backup_schedules_table` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `volume_id` integer NOT NULL REFERENCES `volumes_table`(`id`) ON DELETE CASCADE, + `repository_id` text NOT NULL REFERENCES `repositories_table`(`id`) ON DELETE CASCADE, + `enabled` integer DEFAULT true NOT NULL, + `cron_expression` text NOT NULL, + `retention_policy` text, + `exclude_patterns` text DEFAULT '[]', + `include_patterns` text DEFAULT '[]', + `last_backup_at` integer, + `last_backup_status` text, + `last_backup_error` text, + `next_backup_at` integer, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL +);--> statement-breakpoint +INSERT INTO `__new_backup_schedules_table`(`id`, `name`, `volume_id`, `repository_id`, `enabled`, `cron_expression`, `retention_policy`, `exclude_patterns`, `include_patterns`, `last_backup_at`, `last_backup_status`, `last_backup_error`, `next_backup_at`, `created_at`, `updated_at`) +SELECT `id`, lower(hex(randomblob(3))), `volume_id`, `repository_id`, `enabled`, `cron_expression`, `retention_policy`, `exclude_patterns`, `include_patterns`, `last_backup_at`, `last_backup_status`, `last_backup_error`, `next_backup_at`, `created_at`, `updated_at` FROM `backup_schedules_table`;--> statement-breakpoint +DROP TABLE `backup_schedules_table`;--> statement-breakpoint +ALTER TABLE `__new_backup_schedules_table` RENAME TO `backup_schedules_table`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE UNIQUE INDEX `backup_schedules_table_name_unique` ON `backup_schedules_table` (`name`); \ No newline at end of file diff --git a/app/drizzle/meta/0019_snapshot.json b/app/drizzle/meta/0019_snapshot.json new file mode 100644 index 0000000..3f2d178 --- /dev/null +++ b/app/drizzle/meta/0019_snapshot.json @@ -0,0 +1,807 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b5b3acff-51d7-45ae-b9d2-4b07a6286fc3", + "prevId": "d5a60aea-4490-423e-8725-6ace87a76c9b", + "tables": { + "app_metadata": { + "name": "app_metadata", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedule_mirrors_table": { + "name": "backup_schedule_mirrors_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_copy_at": { + "name": "last_copy_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_copy_status": { + "name": "last_copy_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_copy_error": { + "name": "last_copy_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "backup_schedule_mirrors_table_schedule_id_repository_id_unique": { + "name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique", + "columns": [ + "schedule_id", + "repository_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": { + "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk", + "tableFrom": "backup_schedule_mirrors_table", + "tableTo": "backup_schedules_table", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedule_mirrors_table", + "tableTo": "repositories_table", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedule_notifications_table": { + "name": "backup_schedule_notifications_table", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "destination_id": { + "name": "destination_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notify_on_start": { + "name": "notify_on_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_success": { + "name": "notify_on_success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_failure": { + "name": "notify_on_failure", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": { + "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "tableTo": "backup_schedules_table", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": { + "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "tableTo": "notification_destinations_table", + "columnsFrom": [ + "destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "backup_schedule_notifications_table_schedule_id_destination_id_pk": { + "columns": [ + "schedule_id", + "destination_id" + ], + "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedules_table": { + "name": "backup_schedules_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "volume_id": { + "name": "volume_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exclude_patterns": { + "name": "exclude_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "include_patterns": { + "name": "include_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "last_backup_at": { + "name": "last_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_status": { + "name": "last_backup_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_error": { + "name": "last_backup_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_backup_at": { + "name": "next_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "backup_schedules_table_name_unique": { + "name": "backup_schedules_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "backup_schedules_table_volume_id_volumes_table_id_fk": { + "name": "backup_schedules_table_volume_id_volumes_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "volumes_table", + "columnsFrom": [ + "volume_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedules_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "repositories_table", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_destinations_table": { + "name": "notification_destinations_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "notification_destinations_table_name_unique": { + "name": "notification_destinations_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories_table": { + "name": "repositories_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compression_mode": { + "name": "compression_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'auto'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'unknown'" + }, + "last_checked": { + "name": "last_checked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "repositories_table_short_id_unique": { + "name": "repositories_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "repositories_table_name_unique": { + "name": "repositories_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions_table": { + "name": "sessions_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_table_user_id_users_table_id_fk": { + "name": "sessions_table_user_id_users_table_id_fk", + "tableFrom": "sessions_table", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "has_downloaded_restic_password": { + "name": "has_downloaded_restic_password", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes_table": { + "name": "volumes_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unmounted'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_health_check": { + "name": "last_health_check", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto_remount": { + "name": "auto_remount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "volumes_table_short_id_unique": { + "name": "volumes_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "volumes_table_name_unique": { + "name": "volumes_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/app/drizzle/meta/_journal.json b/app/drizzle/meta/_journal.json index 46fb2dd..36145bb 100644 --- a/app/drizzle/meta/_journal.json +++ b/app/drizzle/meta/_journal.json @@ -134,6 +134,13 @@ "when": 1764794371040, "tag": "0018_breezy_invaders", "breakpoints": true + }, + { + "idx": 19, + "version": "6", + "when": 1764839917446, + "tag": "0019_secret_nomad", + "breakpoints": true } ] } \ No newline at end of file diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index 6bfa699..469732d 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -67,6 +67,7 @@ export type Repository = typeof repositoriesTable.$inferSelect; */ export const backupSchedulesTable = sqliteTable("backup_schedules_table", { id: int().primaryKey({ autoIncrement: true }), + name: text().notNull().unique(), volumeId: int("volume_id") .notNull() .references(() => volumesTable.id, { onDelete: "cascade" }), diff --git a/app/server/modules/backups/backups.dto.ts b/app/server/modules/backups/backups.dto.ts index 464891b..4cbc048 100644 --- a/app/server/modules/backups/backups.dto.ts +++ b/app/server/modules/backups/backups.dto.ts @@ -17,6 +17,7 @@ export type RetentionPolicy = typeof retentionPolicySchema.infer; const backupScheduleSchema = type({ id: "number", + name: "string", volumeId: "number", repositoryId: "string", enabled: "boolean", @@ -120,6 +121,7 @@ export const getBackupScheduleForVolumeDto = describeRoute({ * Create a new backup schedule */ export const createBackupScheduleBody = type({ + name: "1 <= string <= 32", volumeId: "number", repositoryId: "string", enabled: "boolean", @@ -156,6 +158,7 @@ export const createBackupScheduleDto = describeRoute({ * Update a backup schedule */ export const updateBackupScheduleBody = type({ + name: "(1 <= string <= 32)?", repositoryId: "string", enabled: "boolean?", cronExpression: "string", diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts index df9e42b..9dcb8be 100644 --- a/app/server/modules/backups/backups.service.ts +++ b/app/server/modules/backups/backups.service.ts @@ -1,4 +1,4 @@ -import { eq } from "drizzle-orm"; +import { and, eq, ne } from "drizzle-orm"; import cron from "node-cron"; import { CronExpressionParser } from "cron-parser"; import { NotFoundError, BadRequestError, ConflictError } from "http-errors-enhanced"; @@ -44,7 +44,7 @@ const listSchedules = async () => { const getSchedule = async (scheduleId: number) => { const schedule = await db.query.backupSchedulesTable.findFirst({ - where: eq(volumesTable.id, scheduleId), + where: eq(backupSchedulesTable.id, scheduleId), with: { volume: true, repository: true, @@ -63,6 +63,14 @@ const createSchedule = async (data: CreateBackupScheduleBody) => { throw new BadRequestError("Invalid cron expression"); } + const existingName = await db.query.backupSchedulesTable.findFirst({ + where: eq(backupSchedulesTable.name, data.name), + }); + + if (existingName) { + throw new ConflictError("A backup schedule with this name already exists"); + } + const volume = await db.query.volumesTable.findFirst({ where: eq(volumesTable.id, data.volumeId), }); @@ -84,6 +92,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => { const [newSchedule] = await db .insert(backupSchedulesTable) .values({ + name: data.name, volumeId: data.volumeId, repositoryId: data.repositoryId, enabled: data.enabled, @@ -115,6 +124,16 @@ const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody throw new BadRequestError("Invalid cron expression"); } + if (data.name) { + const existingName = await db.query.backupSchedulesTable.findFirst({ + where: and(eq(backupSchedulesTable.name, data.name), ne(backupSchedulesTable.id, scheduleId)), + }); + + if (existingName) { + throw new ConflictError("A backup schedule with this name already exists"); + } + } + const repository = await db.query.repositoriesTable.findFirst({ where: eq(repositoriesTable.id, data.repositoryId), }); diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index 84eaf08..55442f3 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -1,6 +1,7 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; +import os from "node:os"; import { throttle } from "es-toolkit"; import { type } from "arktype"; import { $ } from "bun"; @@ -261,8 +262,9 @@ const backup = async ( let includeFile: string | null = null; if (options?.include && options.include.length > 0) { - const tmp = await fs.mkdtemp("restic-include"); + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-restic-include-")); includeFile = path.join(tmp, `include.txt`); + const includePaths = options.include.map((p) => path.join(source, p)); await fs.writeFile(includeFile, includePaths.join("\n"), "utf-8"); diff --git a/bun.lock b/bun.lock index dd2432a..66bf1d8 100644 --- a/bun.lock +++ b/bun.lock @@ -32,7 +32,7 @@ "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", "es-toolkit": "^1.42.0", - "hono": "^4.10.7", + "hono": "4.10.5", "hono-openapi": "^1.1.1", "http-errors-enhanced": "^4.0.2", "isbot": "^5.1.32", @@ -41,7 +41,7 @@ "node-cron": "^4.2.1", "react": "^19.2.1", "react-dom": "^19.2.1", - "react-hook-form": "^7.67.0", + "react-hook-form": "^7.68.0", "react-router": "^7.10.0", "react-router-hono-server": "^2.22.0", "recharts": "3.5.1", @@ -770,7 +770,7 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], + "hono": ["hono@4.10.5", "", {}, "sha512-h/MXuTkoAK8NG1EfDp0jI1YLf6yGdDnfkebRO2pwEh5+hE3RAJFXkCsnD0vamSiARK4ZrB6MY+o3E/hCnOyHrQ=="], "hono-openapi": ["hono-openapi@1.1.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.8", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-AC3HNhZYPHhnZdSy2Je7GDoTTNxPos6rKRQKVDBbSilY3cWJPqsxRnN6zA4pU7tfxmQEMTqkiLXbw6sAaemB8Q=="], @@ -952,7 +952,7 @@ "react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="], - "react-hook-form": ["react-hook-form@7.67.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-E55EOwKJHHIT/I6J9DmQbCWToAYSw9nN5R57MZw9rMtjh+YQreMDxRLfdjfxQbiJ3/qbg3Z02wGzBX4M+5fMtQ=="], + "react-hook-form": ["react-hook-form@7.68.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q=="], "react-is": ["react-is@19.2.0", "", {}, "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="], @@ -1216,8 +1216,6 @@ "react-router-hono-server/@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="], - "react-router-hono-server/hono": ["hono@4.10.5", "", {}, "sha512-h/MXuTkoAK8NG1EfDp0jI1YLf6yGdDnfkebRO2pwEh5+hE3RAJFXkCsnD0vamSiARK4ZrB6MY+o3E/hCnOyHrQ=="], - "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], diff --git a/package.json b/package.json index a1f54ed..4ea70bd 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "dotenv": "^17.2.3", "drizzle-orm": "^0.44.7", "es-toolkit": "^1.42.0", - "hono": "^4.10.7", + "hono": "4.10.5", "hono-openapi": "^1.1.1", "http-errors-enhanced": "^4.0.2", "isbot": "^5.1.32", @@ -54,7 +54,7 @@ "node-cron": "^4.2.1", "react": "^19.2.1", "react-dom": "^19.2.1", - "react-hook-form": "^7.67.0", + "react-hook-form": "^7.68.0", "react-router": "^7.10.0", "react-router-hono-server": "^2.22.0", "recharts": "3.5.1", From 08d8a473522da0842f8f88ad650a58fec9ce3267 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Thu, 4 Dec 2025 18:44:34 +0100 Subject: [PATCH 5/8] feat: custom include patterns (#104) * feat: add custom include patterns * feat: add exclude-if-present option --- app/client/api-client/types.gen.ts | 7 + .../components/create-schedule-form.tsx | 120 ++- .../modules/backups/routes/backup-details.tsx | 6 +- .../modules/backups/routes/create-backup.tsx | 1 + app/drizzle/0020_even_dexter_bennett.sql | 1 + app/drizzle/meta/0020_snapshot.json | 815 ++++++++++++++++++ app/drizzle/meta/_journal.json | 7 + app/server/db/schema.ts | 1 + app/server/modules/backups/backups.dto.ts | 3 + app/server/modules/backups/backups.service.ts | 6 + app/server/utils/restic.ts | 7 + 11 files changed, 963 insertions(+), 11 deletions(-) create mode 100644 app/drizzle/0020_even_dexter_bennett.sql create mode 100644 app/drizzle/meta/0020_snapshot.json diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 6a8bc0d..2790e84 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -1291,6 +1291,7 @@ export type ListBackupSchedulesResponses = { createdAt: number; cronExpression: string; enabled: boolean; + excludeIfPresent: Array | null; excludePatterns: Array | null; id: number; includePatterns: Array | null; @@ -1439,6 +1440,7 @@ export type CreateBackupScheduleData = { name: string; repositoryId: string; volumeId: number; + excludeIfPresent?: Array; excludePatterns?: Array; includePatterns?: Array; retentionPolicy?: { @@ -1465,6 +1467,7 @@ export type CreateBackupScheduleResponses = { createdAt: number; cronExpression: string; enabled: boolean; + excludeIfPresent: Array | null; excludePatterns: Array | null; id: number; includePatterns: Array | null; @@ -1527,6 +1530,7 @@ export type GetBackupScheduleResponses = { createdAt: number; cronExpression: string; enabled: boolean; + excludeIfPresent: Array | null; excludePatterns: Array | null; id: number; includePatterns: Array | null; @@ -1673,6 +1677,7 @@ export type UpdateBackupScheduleData = { cronExpression: string; repositoryId: string; enabled?: boolean; + excludeIfPresent?: Array; excludePatterns?: Array; includePatterns?: Array; name?: string; @@ -1702,6 +1707,7 @@ export type UpdateBackupScheduleResponses = { createdAt: number; cronExpression: string; enabled: boolean; + excludeIfPresent: Array | null; excludePatterns: Array | null; id: number; includePatterns: Array | null; @@ -1744,6 +1750,7 @@ export type GetBackupScheduleForVolumeResponses = { createdAt: number; cronExpression: string; enabled: boolean; + excludeIfPresent: Array | null; excludePatterns: Array | null; id: number; includePatterns: Array | null; diff --git a/app/client/modules/backups/components/create-schedule-form.tsx b/app/client/modules/backups/components/create-schedule-form.tsx index 1fdede6..fd10c19 100644 --- a/app/client/modules/backups/components/create-schedule-form.tsx +++ b/app/client/modules/backups/components/create-schedule-form.tsx @@ -26,6 +26,8 @@ const internalFormSchema = type({ name: "1 <= string <= 32", repositoryId: "string", excludePatternsText: "string?", + excludeIfPresentText: "string?", + includePatternsText: "string?", includePatterns: "string[]?", frequency: "string", dailyTime: "string?", @@ -51,8 +53,12 @@ export const weeklyDays = [ type InternalFormValues = typeof internalFormSchema.infer; -export type BackupScheduleFormValues = Omit & { +export type BackupScheduleFormValues = Omit< + InternalFormValues, + "excludePatternsText" | "excludeIfPresentText" | "includePatternsText" +> & { excludePatterns?: string[]; + excludeIfPresent?: string[]; }; type Props = { @@ -80,14 +86,21 @@ const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalFormValu const weeklyDay = frequency === "weekly" ? dayOfWeekPart : undefined; + const patterns = schedule.includePatterns || []; + const isGlobPattern = (p: string) => /[*?[\]]/.test(p); + const fileBrowserPaths = patterns.filter((p) => !isGlobPattern(p)); + const textPatterns = patterns.filter(isGlobPattern); + return { name: schedule.name, repositoryId: schedule.repositoryId, frequency, dailyTime, weeklyDay, - includePatterns: schedule.includePatterns || undefined, + includePatterns: fileBrowserPaths.length > 0 ? fileBrowserPaths : undefined, + includePatternsText: textPatterns.length > 0 ? textPatterns.join("\n") : undefined, excludePatternsText: schedule.excludePatterns?.join("\n") || undefined, + excludeIfPresentText: schedule.excludeIfPresent?.join("\n") || undefined, ...schedule.retentionPolicy, }; }; @@ -100,18 +113,40 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: const handleSubmit = useCallback( (data: InternalFormValues) => { - // Convert excludePatternsText string to excludePatterns array - const { excludePatternsText, ...rest } = data; + const { + excludePatternsText, + excludeIfPresentText, + includePatternsText, + includePatterns: fileBrowserPatterns, + ...rest + } = data; const excludePatterns = excludePatternsText ? excludePatternsText .split("\n") .map((p) => p.trim()) .filter(Boolean) - : undefined; + : []; + + const excludeIfPresent = excludeIfPresentText + ? excludeIfPresentText + .split("\n") + .map((p) => p.trim()) + .filter(Boolean) + : []; + + const textPatterns = includePatternsText + ? includePatternsText + .split("\n") + .map((p) => p.trim()) + .filter(Boolean) + : []; + const includePatterns = [...(fileBrowserPatterns || []), ...textPatterns]; onSubmit({ ...rest, + includePatterns: includePatterns.length > 0 ? includePatterns : [], excludePatterns, + excludeIfPresent, }); }, [onSubmit], @@ -296,6 +331,27 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
)} + ( + + Additional include patterns + +