From db127afe5ab3e61be94b38e7b4dcfd0421137e85 Mon Sep 17 00:00:00 2001 From: Raj Dave Date: Sat, 10 Jan 2026 14:03:45 +0300 Subject: [PATCH 01/23] Adds bandwidth limiting to repositories Implements bandwidth limits for repository uploads and downloads. Adds UI components to configure bandwidth limits. Adds database fields to store bandwidth limit configuration. Integrates bandwidth limiting into restic commands. --- .gitignore | 2 + .../repository-forms/advanced-tls-form.tsx | 185 +++ app/drizzle/0032_outstanding_ultron.sql | 6 + app/drizzle/meta/0032_snapshot.json | 1243 +++++++++++++++++ app/drizzle/meta/_journal.json | 7 + app/schemas/restic.ts | 20 + app/server/db/schema.ts | 7 + app/server/utils/rclone-dummy.ts | 0 app/server/utils/restic.ts | 121 +- 9 files changed, 1561 insertions(+), 30 deletions(-) create mode 100644 app/drizzle/0032_outstanding_ultron.sql create mode 100644 app/drizzle/meta/0032_snapshot.json create mode 100644 app/server/utils/rclone-dummy.ts diff --git a/.gitignore b/.gitignore index c2b6cf20..887dcd56 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ data/ .env* !.env.example !.env.test + +.idea/ diff --git a/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx b/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx index d5f50256..11653990 100644 --- a/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx +++ b/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx @@ -7,12 +7,15 @@ import { FormLabel, FormMessage, } from "../../../../components/ui/form"; +import { Input } from "../../../../components/ui/input"; import { Textarea } from "../../../../components/ui/textarea"; import { Checkbox } from "../../../../components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../components/ui/select"; import { Tooltip, TooltipContent, TooltipTrigger } from "../../../../components/ui/tooltip"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../../components/ui/collapsible"; import type { RepositoryFormValues } from "../create-repository-form"; import { cn } from "~/client/lib/utils"; +import { BANDWIDTH_UNITS } from "~/schemas/restic"; type Props = { form: UseFormReturn; @@ -21,11 +24,193 @@ type Props = { export const AdvancedForm = ({ form }: Props) => { const insecureTls = form.watch("insecureTls"); const cacert = form.watch("cacert"); + const uploadEnabled = form.watch("uploadLimit.enabled"); + const downloadEnabled = form.watch("downloadLimit.enabled"); return ( Advanced Settings + {/* Bandwidth Limit Controls */} +
+
+
+

Bandwidth Limits

+

+ Control upload and download speeds to prevent saturating network bandwidth +

+
+
+ +
+ {/* Upload Limit */} +
+
+

Upload Limit

+
+ + ( + + + + +
+ Enable upload speed limit + + Limit upload speed to the repository + +
+
+ )} + /> + + {uploadEnabled && ( +
+
+ ( + + +
+ field.onChange(parseFloat(e.target.value) || 0)} + /> +
+
+
+
+ + + + )} + /> + ( + + + + + )} + /> +
+
+ )} +
+ + {/* Download Limit */} +
+
+

Download Limit

+
+ + ( + + + + +
+ Enable download speed limit + + Limit download speed from the repository + +
+
+ )} + /> + + {downloadEnabled && ( +
+
+ ( + + +
+ field.onChange(parseFloat(e.target.value) || 0)} + /> +
+
+
+
+ + + + )} + /> + ( + + + + + )} + /> +
+
+ )} +
+
+
+ statement-breakpoint +ALTER TABLE `repositories_table` ADD `upload_limit_value` integer DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE `repositories_table` ADD `upload_limit_unit` text DEFAULT 'MB/s' NOT NULL;--> statement-breakpoint +ALTER TABLE `repositories_table` ADD `download_limit_enabled` integer DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE `repositories_table` ADD `download_limit_value` integer DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE `repositories_table` ADD `download_limit_unit` text DEFAULT 'MB/s' NOT NULL; \ No newline at end of file diff --git a/app/drizzle/meta/0032_snapshot.json b/app/drizzle/meta/0032_snapshot.json new file mode 100644 index 00000000..3722f852 --- /dev/null +++ b/app/drizzle/meta/0032_snapshot.json @@ -0,0 +1,1243 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "2d6b79a4-2d75-4240-8943-1180372847ba", + "prevId": "ae0e3a39-ef4d-4d39-bb97-d67d49193bc7", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "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": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_users_table_id_fk": { + "name": "account_user_id_users_table_id_fk", + "tableFrom": "account", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "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_warning": { + "name": "notify_on_warning", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "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 + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "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": "'[]'" + }, + "exclude_if_present": { + "name": "exclude_if_present", + "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 + }, + "one_file_system": { + "name": "one_file_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "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_short_id_unique": { + "name": "backup_schedules_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "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 + }, + "upload_limit_enabled": { + "name": "upload_limit_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "upload_limit_value": { + "name": "upload_limit_value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "upload_limit_unit": { + "name": "upload_limit_unit", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'MB/s'" + }, + "download_limit_enabled": { + "name": "download_limit_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "download_limit_value": { + "name": "download_limit_value", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "download_limit_unit": { + "name": "download_limit_unit", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'MB/s'" + }, + "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 + } + }, + "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": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "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)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_table_token_unique": { + "name": "sessions_table_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "sessionsTable_userId_idx": { + "name": "sessionsTable_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "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": {} + }, + "two_factor": { + "name": "two_factor", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "twoFactor_secret_idx": { + "name": "twoFactor_secret_idx", + "columns": [ + "secret" + ], + "isUnique": false + }, + "twoFactor_userId_idx": { + "name": "twoFactor_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "two_factor_user_id_users_table_id_fk": { + "name": "two_factor_user_id_users_table_id_fk", + "tableFrom": "two_factor", + "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": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "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)" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": [ + "username" + ], + "isUnique": true + }, + "users_table_email_unique": { + "name": "users_table_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "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)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "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 f7fbfcbb..a7ced5d0 100644 --- a/app/drizzle/meta/_journal.json +++ b/app/drizzle/meta/_journal.json @@ -225,6 +225,13 @@ "when": 1767863951955, "tag": "0031_graceful_squadron_supreme", "breakpoints": true + }, + { + "idx": 32, + "version": "6", + "when": 1768040838612, + "tag": "0032_outstanding_ultron", + "breakpoints": true } ] } \ No newline at end of file diff --git a/app/schemas/restic.ts b/app/schemas/restic.ts index f6001fb1..7a8f2e7d 100644 --- a/app/schemas/restic.ts +++ b/app/schemas/restic.ts @@ -13,12 +13,32 @@ export const REPOSITORY_BACKENDS = { export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS; +export const BANDWIDTH_UNITS = { + "KB/s": "KB/s", + "MB/s": "MB/s", + "GB/s": "GB/s", +} as const; + +export type BandwidthUnit = keyof typeof BANDWIDTH_UNITS; + +// Bandwidth limit configuration +export const bandwidthLimitSchema = type({ + enabled: "boolean = false", + value: "number >= 0", + unit: type.valueOf(BANDWIDTH_UNITS).default("MB/s"), +}); + +export type BandwidthLimit = typeof bandwidthLimitSchema.infer; + // Common fields for all repository configs const baseRepositoryConfigSchema = type({ isExistingRepository: "boolean?", customPassword: "string?", cacert: "string?", insecureTls: "boolean?", + // Bandwidth controls + uploadLimit: bandwidthLimitSchema.optional(), + downloadLimit: bandwidthLimitSchema.optional(), }); export const s3RepositoryConfigSchema = type({ diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index 647a2f4e..4c5456ff 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -159,6 +159,13 @@ export const repositoriesTable = sqliteTable("repositories_table", { status: text().$type().default("unknown"), lastChecked: int("last_checked", { mode: "number" }), lastError: text("last_error"), + // Bandwidth limit fields + uploadLimitEnabled: int("upload_limit_enabled", { mode: "boolean" }).notNull().default(false), + uploadLimitValue: int("upload_limit_value").notNull().default(0), + uploadLimitUnit: text("upload_limit_unit").notNull().default("MB/s"), + downloadLimitEnabled: int("download_limit_enabled", { mode: "boolean" }).notNull().default(false), + downloadLimitValue: int("download_limit_value").notNull().default(0), + downloadLimitUnit: text("download_limit_unit").notNull().default("MB/s"), createdAt: int("created_at", { mode: "number" }) .notNull() .default(sql`(unixepoch() * 1000)`), diff --git a/app/server/utils/rclone-dummy.ts b/app/server/utils/rclone-dummy.ts new file mode 100644 index 00000000..e69de29b diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index 4eabbb49..97677084 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -10,7 +10,7 @@ import { logger } from "./logger"; import { cryptoUtils } from "./crypto"; import type { RetentionPolicy } from "../modules/backups/backups.dto"; import { safeSpawn } from "./spawn"; -import type { CompressionMode, RepositoryConfig, OverwriteMode } from "~/schemas/restic"; +import type { CompressionMode, RepositoryConfig, OverwriteMode, BandwidthLimit } from "~/schemas/restic"; import { ResticError } from "./errors"; const backupOutputSchema = type({ @@ -229,7 +229,7 @@ const init = async (config: RepositoryConfig) => { const env = await buildEnv(config); const args = ["init", "--repo", repoUrl]; - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -321,7 +321,7 @@ const backup = async ( } } - addCommonArgs(args, env); + addCommonArgs(args, env, config); const logData = throttle((data: string) => { logger.info(data.trim()); @@ -453,7 +453,7 @@ const restore = async ( } } - addCommonArgs(args, env); + addCommonArgs(args, env, config); logger.debug(`Executing: restic ${args.join(" ")}`); const res = await safeSpawn({ command: "restic", args, env }); @@ -516,7 +516,7 @@ const snapshots = async (config: RepositoryConfig, options: { tags?: string[] } } } - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -565,7 +565,7 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra: } args.push("--prune"); - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -587,7 +587,7 @@ const deleteSnapshots = async (config: RepositoryConfig, snapshotIds: string[]) } const args: string[] = ["--repo", repoUrl, "forget", ...snapshotIds, "--prune"]; - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -636,7 +636,7 @@ const tagSnapshots = async ( } } - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -686,7 +686,7 @@ const ls = async (config: RepositoryConfig, snapshotId: string, path?: string) = args.push(path); } - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -737,7 +737,7 @@ const unlock = async (config: RepositoryConfig) => { const env = await buildEnv(config); const args = ["unlock", "--repo", repoUrl, "--remove-all"]; - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -761,7 +761,7 @@ const check = async (config: RepositoryConfig, options?: { readData?: boolean }) args.push("--read-data"); } - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -794,7 +794,7 @@ const repairIndex = async (config: RepositoryConfig) => { const env = await buildEnv(config); const args = ["repair", "index", "--repo", repoUrl]; - addCommonArgs(args, env); + addCommonArgs(args, env, config); const res = await safeSpawn({ command: "restic", args, env }); await cleanupTemporaryKeys(env); @@ -846,7 +846,7 @@ const copy = async ( args.push("latest"); } - addCommonArgs(args, env); + addCommonArgs(args, env, destConfig); if (sourceConfig.backend === "sftp" && sourceEnv._SFTP_SSH_ARGS) { args.push("-o", `sftp.args=${sourceEnv._SFTP_SSH_ARGS}`); @@ -874,29 +874,32 @@ const copy = async ( }; }; -export const cleanupTemporaryKeys = async (env: Record) => { - if (env._SFTP_KEY_PATH) { - await fs.unlink(env._SFTP_KEY_PATH).catch(() => {}); +// Helper function to convert bandwidth limit to restic format +const formatBandwidthLimit = (limit: BandwidthLimit): string => { + if (!limit.enabled || limit.value <= 0) { + return ""; } - if (env._SFTP_KNOWN_HOSTS_PATH) { - await fs.unlink(env._SFTP_KNOWN_HOSTS_PATH).catch(() => {}); + // Convert to bytes per second for restic + let bytesPerSecond: number; + switch (limit.unit) { + case "KB/s": + bytesPerSecond = limit.value * 1024; + break; + case "MB/s": + bytesPerSecond = limit.value * 1024 * 1024; + break; + case "GB/s": + bytesPerSecond = limit.value * 1024 * 1024 * 1024; + break; + default: + return ""; } - if (env.RESTIC_PASSWORD_FILE && env.RESTIC_PASSWORD_FILE !== RESTIC_PASS_FILE) { - await fs.unlink(env.RESTIC_PASSWORD_FILE).catch(() => {}); - } - - if (env.GOOGLE_APPLICATION_CREDENTIALS) { - await fs.unlink(env.GOOGLE_APPLICATION_CREDENTIALS).catch(() => {}); - } - - if (env.RESTIC_CACERT) { - await fs.unlink(env.RESTIC_CACERT).catch(() => {}); - } + return `${Math.floor(bytesPerSecond)}`; }; -export const addCommonArgs = (args: string[], env: Record) => { +export const addCommonArgs = (args: string[], env: Record, config?: RepositoryConfig) => { args.push("--json"); if (env._SFTP_SSH_ARGS) { @@ -910,6 +913,40 @@ export const addCommonArgs = (args: string[], env: Record) => { if (env.RESTIC_CACERT) { args.push("--cacert", env.RESTIC_CACERT); } + + // Add bandwidth limits if configuration is provided + if (config) { + // Handle upload limit + if (config.uploadLimit?.enabled) { + const uploadLimit = formatBandwidthLimit(config.uploadLimit); + if (uploadLimit) { + if (config.backend === "rclone") { + // For rclone backends, use --bwlimit + args.push("-o", `rclone.bwlimit=${uploadLimit}`); + } else { + // For restic backends, use --limit-upload + args.push("--limit-upload", uploadLimit); + } + } + } + + // Handle download limit + if (config.downloadLimit?.enabled) { + const downloadLimit = formatBandwidthLimit(config.downloadLimit); + if (downloadLimit) { + if (config.backend === "rclone") { + // For rclone, bwlimit affects both upload and download + // If both limits are set, use the more restrictive one + const uploadLimit = config.uploadLimit?.enabled ? formatBandwidthLimit(config.uploadLimit) : ""; + const effectiveLimit = uploadLimit && parseInt(uploadLimit) < parseInt(downloadLimit) ? uploadLimit : downloadLimit; + args.push("-o", `rclone.bwlimit=${effectiveLimit}`); + } else { + // For restic backends, use --limit-download + args.push("--limit-download", downloadLimit); + } + } + } + } }; export const restic = { @@ -928,3 +965,27 @@ export const restic = { repairIndex, copy, }; + +// Helper function to clean up temporary files +const cleanupTemporaryKeys = async (env: Record) => { + const keysToClean = ['_SFTP_KEY_PATH', '_SFTP_KNOWN_HOSTS_PATH', 'RESTIC_CACERT', 'GOOGLE_APPLICATION_CREDENTIALS']; + + for (const key of keysToClean) { + if (env[key]) { + try { + await fs.unlink(env[key]); + } catch (error) { + // Ignore errors when cleaning up temporary files + } + } + } + + // Clean up custom password files + if (env.RESTIC_PASSWORD_FILE && env.RESTIC_PASSWORD_FILE !== RESTIC_PASS_FILE) { + try { + await fs.unlink(env.RESTIC_PASSWORD_FILE); + } catch (error) { + // Ignore errors when cleaning up temporary files + } + } +}; From a7a4ccef6b33f9bc94b83653238847101a5e3557 Mon Sep 17 00:00:00 2001 From: Raj Dave Date: Sat, 10 Jan 2026 14:23:39 +0300 Subject: [PATCH 02/23] fix a lot of the coderabbit warnings --- .../repository-forms/advanced-tls-form.tsx | 12 +++- app/schemas/restic.ts | 10 ++-- app/server/db/schema.ts | 12 ++-- app/server/utils/restic.ts | 57 +++++++++++++++---- 4 files changed, 66 insertions(+), 25 deletions(-) diff --git a/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx b/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx index 11653990..e042c4d9 100644 --- a/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx +++ b/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx @@ -10,7 +10,13 @@ import { import { Input } from "../../../../components/ui/input"; import { Textarea } from "../../../../components/ui/textarea"; import { Checkbox } from "../../../../components/ui/checkbox"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../components/ui/select"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "../../../../components/ui/select"; import { Tooltip, TooltipContent, TooltipTrigger } from "../../../../components/ui/tooltip"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../../components/ui/collapsible"; import type { RepositoryFormValues } from "../create-repository-form"; @@ -103,7 +109,7 @@ export const AdvancedForm = ({ form }: Props) => { name="uploadLimit.unit" render={({ field }) => ( - @@ -186,7 +192,7 @@ export const AdvancedForm = ({ form }: Props) => { name="downloadLimit.unit" render={({ field }) => ( - diff --git a/app/schemas/restic.ts b/app/schemas/restic.ts index 7a8f2e7d..f9b5123c 100644 --- a/app/schemas/restic.ts +++ b/app/schemas/restic.ts @@ -14,9 +14,9 @@ export const REPOSITORY_BACKENDS = { export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS; export const BANDWIDTH_UNITS = { - "KB/s": "KB/s", - "MB/s": "MB/s", - "GB/s": "GB/s", + "Kbps": "Kbps", + "Mbps": "Mbps", + "Gbps": "Gbps", } as const; export type BandwidthUnit = keyof typeof BANDWIDTH_UNITS; @@ -24,8 +24,8 @@ export type BandwidthUnit = keyof typeof BANDWIDTH_UNITS; // Bandwidth limit configuration export const bandwidthLimitSchema = type({ enabled: "boolean = false", - value: "number >= 0", - unit: type.valueOf(BANDWIDTH_UNITS).default("MB/s"), + value: "number >= 0 = 0", + unit: type.valueOf(BANDWIDTH_UNITS).default("Mbps"), }); export type BandwidthLimit = typeof bandwidthLimitSchema.infer; diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index 4c5456ff..09c8ee55 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -1,6 +1,6 @@ import { relations, sql } from "drizzle-orm"; -import { index, int, integer, sqliteTable, text, primaryKey, unique } from "drizzle-orm/sqlite-core"; -import type { CompressionMode, RepositoryBackend, repositoryConfigSchema, RepositoryStatus } from "~/schemas/restic"; +import { index, int, integer, sqliteTable, text, real, primaryKey, unique } from "drizzle-orm/sqlite-core"; +import type { CompressionMode, RepositoryBackend, repositoryConfigSchema, RepositoryStatus, BandwidthUnit } from "~/schemas/restic"; import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes"; import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications"; @@ -161,11 +161,11 @@ export const repositoriesTable = sqliteTable("repositories_table", { lastError: text("last_error"), // Bandwidth limit fields uploadLimitEnabled: int("upload_limit_enabled", { mode: "boolean" }).notNull().default(false), - uploadLimitValue: int("upload_limit_value").notNull().default(0), - uploadLimitUnit: text("upload_limit_unit").notNull().default("MB/s"), + uploadLimitValue: real("upload_limit_value").notNull().default(0), + uploadLimitUnit: text("upload_limit_unit").$type().notNull().default("Mbps"), downloadLimitEnabled: int("download_limit_enabled", { mode: "boolean" }).notNull().default(false), - downloadLimitValue: int("download_limit_value").notNull().default(0), - downloadLimitUnit: text("download_limit_unit").notNull().default("MB/s"), + downloadLimitValue: real("download_limit_value").notNull().default(0), + downloadLimitUnit: text("download_limit_unit").$type().notNull().default("Mbps"), createdAt: int("created_at", { mode: "number" }) .notNull() .default(sql`(unixepoch() * 1000)`), diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index 97677084..6d4ad9e1 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -846,8 +846,39 @@ const copy = async ( args.push("latest"); } + // Apply bandwidth limits from both source and destination configs + // First apply destination config limits (for the main operation) addCommonArgs(args, env, destConfig); + // Then explicitly apply source-side bandwidth limits for the from-repo operation + if (sourceConfig.uploadLimit?.enabled) { + const sourceUploadLimit = formatBandwidthLimit(sourceConfig.uploadLimit); + if (sourceUploadLimit) { + if (sourceConfig.backend === "rclone") { + // For rclone source backends, use rclone.bwlimit for the from-repo + args.push("-o", `rclone.from.bwlimit=${sourceUploadLimit}`); + } else { + // For restic source backends, the download from source becomes an upload limit + args.push("--limit-upload", sourceUploadLimit); + } + } + } + + if (sourceConfig.downloadLimit?.enabled) { + const sourceDownloadLimit = formatBandwidthLimit(sourceConfig.downloadLimit); + if (sourceDownloadLimit) { + if (sourceConfig.backend === "rclone") { + // For rclone source backends + const sourceUploadLimit = sourceConfig.uploadLimit?.enabled ? formatBandwidthLimit(sourceConfig.uploadLimit) : ""; + const effectiveLimit = sourceUploadLimit && parseInt(sourceUploadLimit) < parseInt(sourceDownloadLimit) ? sourceUploadLimit : sourceDownloadLimit; + args.push("-o", `rclone.from.bwlimit=${effectiveLimit}`); + } else { + // For restic source backends, this affects reading from the source repo + args.push("--limit-download", sourceDownloadLimit); + } + } + } + if (sourceConfig.backend === "sftp" && sourceEnv._SFTP_SSH_ARGS) { args.push("-o", `sftp.args=${sourceEnv._SFTP_SSH_ARGS}`); } @@ -874,29 +905,33 @@ const copy = async ( }; }; -// Helper function to convert bandwidth limit to restic format +// Helper function to convert bandwidth limit to restic/rclone format const formatBandwidthLimit = (limit: BandwidthLimit): string => { if (!limit.enabled || limit.value <= 0) { return ""; } - // Convert to bytes per second for restic - let bytesPerSecond: number; + // Convert to KiB/s for restic compatibility, or use suffixed format for rclone + let kibibytesPerSecond: number; switch (limit.unit) { - case "KB/s": - bytesPerSecond = limit.value * 1024; + case "Kbps": + // Kilobits per second to KiB/s: divide by 8 (bits to bytes), then by 1024 (bytes to KiB) + kibibytesPerSecond = limit.value / (8 * 1024); break; - case "MB/s": - bytesPerSecond = limit.value * 1024 * 1024; + case "Mbps": + // Megabits per second to KiB/s: multiply by 1000000 (Mb to bits), divide by 8 (bits to bytes), then by 1024 (bytes to KiB) + kibibytesPerSecond = (limit.value * 1000000) / (8 * 1024); break; - case "GB/s": - bytesPerSecond = limit.value * 1024 * 1024 * 1024; + case "Gbps": + // Gigabits per second to KiB/s: multiply by 1000000000 (Gb to bits), divide by 8 (bits to bytes), then by 1024 (bytes to KiB) + kibibytesPerSecond = (limit.value * 1000000000) / (8 * 1024); break; default: return ""; } - return `${Math.floor(bytesPerSecond)}`; + // Return as integer KiB/s for restic + return `${Math.floor(kibibytesPerSecond)}`; }; export const addCommonArgs = (args: string[], env: Record, config?: RepositoryConfig) => { @@ -968,7 +1003,7 @@ export const restic = { // Helper function to clean up temporary files const cleanupTemporaryKeys = async (env: Record) => { - const keysToClean = ['_SFTP_KEY_PATH', '_SFTP_KNOWN_HOSTS_PATH', 'RESTIC_CACERT', 'GOOGLE_APPLICATION_CREDENTIALS']; + const keysToClean = ["_SFTP_KEY_PATH", "_SFTP_KNOWN_HOSTS_PATH", "RESTIC_CACERT", "GOOGLE_APPLICATION_CREDENTIALS"]; for (const key of keysToClean) { if (env[key]) { From c91bce7814950118b6681e1a1a43f1455dec9a3e Mon Sep 17 00:00:00 2001 From: Raj Dave Date: Sat, 10 Jan 2026 14:47:58 +0300 Subject: [PATCH 03/23] fix more coderabbit issues --- app/server/utils/restic.ts | 100 +++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index 6d4ad9e1..c84a9027 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -851,31 +851,31 @@ const copy = async ( addCommonArgs(args, env, destConfig); // Then explicitly apply source-side bandwidth limits for the from-repo operation - if (sourceConfig.uploadLimit?.enabled) { - const sourceUploadLimit = formatBandwidthLimit(sourceConfig.uploadLimit); - if (sourceUploadLimit) { - if (sourceConfig.backend === "rclone") { - // For rclone source backends, use rclone.bwlimit for the from-repo - args.push("-o", `rclone.from.bwlimit=${sourceUploadLimit}`); - } else { - // For restic source backends, the download from source becomes an upload limit - args.push("--limit-upload", sourceUploadLimit); - } - } + const sourceUploadLimit = formatBandwidthLimit(sourceConfig.uploadLimit); + const sourceDownloadLimit = formatBandwidthLimit(sourceConfig.downloadLimit); + + // Determine effective limit for source (pick smaller numeric value if both exist) + let effectiveLimit = ""; + if (sourceUploadLimit && sourceDownloadLimit) { + effectiveLimit = parseInt(sourceUploadLimit) < parseInt(sourceDownloadLimit) ? sourceUploadLimit : sourceDownloadLimit; + } else if (sourceUploadLimit) { + effectiveLimit = sourceUploadLimit; + } else if (sourceDownloadLimit) { + effectiveLimit = sourceDownloadLimit; } - if (sourceConfig.downloadLimit?.enabled) { - const sourceDownloadLimit = formatBandwidthLimit(sourceConfig.downloadLimit); - if (sourceDownloadLimit) { - if (sourceConfig.backend === "rclone") { - // For rclone source backends - const sourceUploadLimit = sourceConfig.uploadLimit?.enabled ? formatBandwidthLimit(sourceConfig.uploadLimit) : ""; - const effectiveLimit = sourceUploadLimit && parseInt(sourceUploadLimit) < parseInt(sourceDownloadLimit) ? sourceUploadLimit : sourceDownloadLimit; - args.push("-o", `rclone.from.bwlimit=${effectiveLimit}`); - } else { - // For restic source backends, this affects reading from the source repo - args.push("--limit-download", sourceDownloadLimit); - } + if (sourceConfig.backend === "rclone") { + // For rclone source backends, use single consolidated bwlimit + if (effectiveLimit) { + args.push("-o", `rclone.from.bwlimit=${effectiveLimit}`); + } + } else { + // For restic source backends, apply individual limits + if (sourceConfig.uploadLimit?.enabled && sourceUploadLimit) { + args.push("--limit-upload", sourceUploadLimit); + } + if (sourceConfig.downloadLimit?.enabled && sourceDownloadLimit) { + args.push("--limit-download", sourceDownloadLimit); } } @@ -915,8 +915,8 @@ const formatBandwidthLimit = (limit: BandwidthLimit): string => { let kibibytesPerSecond: number; switch (limit.unit) { case "Kbps": - // Kilobits per second to KiB/s: divide by 8 (bits to bytes), then by 1024 (bytes to KiB) - kibibytesPerSecond = limit.value / (8 * 1024); + // Kilobits per second to KiB/s: kilobits→bits→bytes→KiB + kibibytesPerSecond = (limit.value * 1000) / 8 / 1024; break; case "Mbps": // Megabits per second to KiB/s: multiply by 1000000 (Mb to bits), divide by 8 (bits to bytes), then by 1024 (bytes to KiB) @@ -951,32 +951,36 @@ export const addCommonArgs = (args: string[], env: Record, confi // Add bandwidth limits if configuration is provided if (config) { - // Handle upload limit - if (config.uploadLimit?.enabled) { + if (config.backend === "rclone") { + // For rclone backends, consolidate both upload and download limits into one bwlimit const uploadLimit = formatBandwidthLimit(config.uploadLimit); - if (uploadLimit) { - if (config.backend === "rclone") { - // For rclone backends, use --bwlimit - args.push("-o", `rclone.bwlimit=${uploadLimit}`); - } else { - // For restic backends, use --limit-upload + const downloadLimit = formatBandwidthLimit(config.downloadLimit); + + // Determine effective limit (choose more restrictive when both exist) + let effectiveLimit = ""; + if (uploadLimit && downloadLimit) { + effectiveLimit = parseInt(uploadLimit) < parseInt(downloadLimit) ? uploadLimit : downloadLimit; + } else if (uploadLimit) { + effectiveLimit = uploadLimit; + } else if (downloadLimit) { + effectiveLimit = downloadLimit; + } + + if (effectiveLimit) { + args.push("-o", `rclone.bwlimit=${effectiveLimit}`); + } + } else { + // For restic backends, handle upload and download limits separately + if (config.uploadLimit?.enabled) { + const uploadLimit = formatBandwidthLimit(config.uploadLimit); + if (uploadLimit) { args.push("--limit-upload", uploadLimit); } } - } - // Handle download limit - if (config.downloadLimit?.enabled) { - const downloadLimit = formatBandwidthLimit(config.downloadLimit); - if (downloadLimit) { - if (config.backend === "rclone") { - // For rclone, bwlimit affects both upload and download - // If both limits are set, use the more restrictive one - const uploadLimit = config.uploadLimit?.enabled ? formatBandwidthLimit(config.uploadLimit) : ""; - const effectiveLimit = uploadLimit && parseInt(uploadLimit) < parseInt(downloadLimit) ? uploadLimit : downloadLimit; - args.push("-o", `rclone.bwlimit=${effectiveLimit}`); - } else { - // For restic backends, use --limit-download + if (config.downloadLimit?.enabled) { + const downloadLimit = formatBandwidthLimit(config.downloadLimit); + if (downloadLimit) { args.push("--limit-download", downloadLimit); } } @@ -1009,7 +1013,7 @@ const cleanupTemporaryKeys = async (env: Record) => { if (env[key]) { try { await fs.unlink(env[key]); - } catch (error) { + } catch (_error) { // Ignore errors when cleaning up temporary files } } @@ -1019,7 +1023,7 @@ const cleanupTemporaryKeys = async (env: Record) => { if (env.RESTIC_PASSWORD_FILE && env.RESTIC_PASSWORD_FILE !== RESTIC_PASS_FILE) { try { await fs.unlink(env.RESTIC_PASSWORD_FILE); - } catch (error) { + } catch (_error) { // Ignore errors when cleaning up temporary files } } From 51e0526622850059cf951df013060874565d614d Mon Sep 17 00:00:00 2001 From: Raj Dave Date: Sat, 10 Jan 2026 17:19:02 +0300 Subject: [PATCH 04/23] more fixed issues --- app/server/utils/restic.ts | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index c84a9027..cf098699 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -851,8 +851,9 @@ const copy = async ( addCommonArgs(args, env, destConfig); // Then explicitly apply source-side bandwidth limits for the from-repo operation - const sourceUploadLimit = formatBandwidthLimit(sourceConfig.uploadLimit); - const sourceDownloadLimit = formatBandwidthLimit(sourceConfig.downloadLimit); + // Guard the formatBandwidthLimit calls and compute values upfront + const sourceUploadLimit = sourceConfig.uploadLimit ? formatBandwidthLimit(sourceConfig.uploadLimit) : ""; + const sourceDownloadLimit = sourceConfig.downloadLimit ? formatBandwidthLimit(sourceConfig.downloadLimit) : ""; // Determine effective limit for source (pick smaller numeric value if both exist) let effectiveLimit = ""; @@ -870,11 +871,11 @@ const copy = async ( args.push("-o", `rclone.from.bwlimit=${effectiveLimit}`); } } else { - // For restic source backends, apply individual limits - if (sourceConfig.uploadLimit?.enabled && sourceUploadLimit) { + // For restic source backends, apply individual limits using the computed values + if (sourceUploadLimit) { args.push("--limit-upload", sourceUploadLimit); } - if (sourceConfig.downloadLimit?.enabled && sourceDownloadLimit) { + if (sourceDownloadLimit) { args.push("--limit-download", sourceDownloadLimit); } } @@ -906,8 +907,8 @@ const copy = async ( }; // Helper function to convert bandwidth limit to restic/rclone format -const formatBandwidthLimit = (limit: BandwidthLimit): string => { - if (!limit.enabled || limit.value <= 0) { +const formatBandwidthLimit = (limit?: BandwidthLimit): string => { + if (!limit || !limit.enabled || limit.value <= 0) { return ""; } @@ -971,18 +972,14 @@ export const addCommonArgs = (args: string[], env: Record, confi } } else { // For restic backends, handle upload and download limits separately - if (config.uploadLimit?.enabled) { - const uploadLimit = formatBandwidthLimit(config.uploadLimit); - if (uploadLimit) { - args.push("--limit-upload", uploadLimit); - } + const uploadLimit = formatBandwidthLimit(config.uploadLimit); + if (uploadLimit) { + args.push("--limit-upload", uploadLimit); } - if (config.downloadLimit?.enabled) { - const downloadLimit = formatBandwidthLimit(config.downloadLimit); - if (downloadLimit) { - args.push("--limit-download", downloadLimit); - } + const downloadLimit = formatBandwidthLimit(config.downloadLimit); + if (downloadLimit) { + args.push("--limit-download", downloadLimit); } } } From 07a925b9f3d7b5862dc8f0e6bc6bbffb65079de3 Mon Sep 17 00:00:00 2001 From: Raj Dave Date: Sat, 10 Jan 2026 17:33:46 +0300 Subject: [PATCH 05/23] final issue i promise --- app/server/utils/restic.ts | 58 +++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index cf098699..cb30421b 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -846,42 +846,42 @@ const copy = async ( args.push("latest"); } - // Apply bandwidth limits from both source and destination configs - // First apply destination config limits (for the main operation) - addCommonArgs(args, env, destConfig); + // Apply common args without automatic bandwidth limit injection to prevent duplication + addCommonArgs(args, env, destConfig, { skipBandwidth: true }); - // Then explicitly apply source-side bandwidth limits for the from-repo operation - // Guard the formatBandwidthLimit calls and compute values upfront - const sourceUploadLimit = sourceConfig.uploadLimit ? formatBandwidthLimit(sourceConfig.uploadLimit) : ""; - const sourceDownloadLimit = sourceConfig.downloadLimit ? formatBandwidthLimit(sourceConfig.downloadLimit) : ""; - - // Determine effective limit for source (pick smaller numeric value if both exist) - let effectiveLimit = ""; - if (sourceUploadLimit && sourceDownloadLimit) { - effectiveLimit = parseInt(sourceUploadLimit) < parseInt(sourceDownloadLimit) ? sourceUploadLimit : sourceDownloadLimit; - } else if (sourceUploadLimit) { - effectiveLimit = sourceUploadLimit; - } else if (sourceDownloadLimit) { - effectiveLimit = sourceDownloadLimit; - } + // Manually handle bandwidth limits with correct copy semantics: + // --limit-download uses sourceConfig.downloadLimit (limiting downloads from source repo) + // --limit-upload uses destConfig.uploadLimit (limiting uploads to destination repo) + const sourceDownloadLimit = formatBandwidthLimit(sourceConfig.downloadLimit); + const destUploadLimit = formatBandwidthLimit(destConfig.uploadLimit); if (sourceConfig.backend === "rclone") { - // For rclone source backends, use single consolidated bwlimit - if (effectiveLimit) { - args.push("-o", `rclone.from.bwlimit=${effectiveLimit}`); + // For rclone source backends, use rclone.from.bwlimit with effective source limit + const sourceUploadLimit = formatBandwidthLimit(sourceConfig.uploadLimit); + + // Determine effective limit for source (pick smaller numeric value if both exist) + let effectiveSourceLimit = ""; + if (sourceUploadLimit && sourceDownloadLimit) { + effectiveSourceLimit = parseInt(sourceUploadLimit) < parseInt(sourceDownloadLimit) ? sourceUploadLimit : sourceDownloadLimit; + } else if (sourceUploadLimit) { + effectiveSourceLimit = sourceUploadLimit; + } else if (sourceDownloadLimit) { + effectiveSourceLimit = sourceDownloadLimit; + } + + if (effectiveSourceLimit) { + args.push("-o", `rclone.from.bwlimit=${effectiveSourceLimit}`); } } else { - // For restic source backends, apply individual limits using the computed values - if (sourceUploadLimit) { - args.push("--limit-upload", sourceUploadLimit); - } + // For restic source backends, apply download limit from source if (sourceDownloadLimit) { args.push("--limit-download", sourceDownloadLimit); } } - if (sourceConfig.backend === "sftp" && sourceEnv._SFTP_SSH_ARGS) { - args.push("-o", `sftp.args=${sourceEnv._SFTP_SSH_ARGS}`); + // Apply upload limit to destination for all backends + if (destUploadLimit) { + args.push("--limit-upload", destUploadLimit); } logger.info(`Copying snapshots from ${sourceRepoUrl} to ${destRepoUrl}...`); @@ -935,7 +935,7 @@ const formatBandwidthLimit = (limit?: BandwidthLimit): string => { return `${Math.floor(kibibytesPerSecond)}`; }; -export const addCommonArgs = (args: string[], env: Record, config?: RepositoryConfig) => { +export const addCommonArgs = (args: string[], env: Record, config?: RepositoryConfig, options?: { skipBandwidth?: boolean }) => { args.push("--json"); if (env._SFTP_SSH_ARGS) { @@ -950,8 +950,8 @@ export const addCommonArgs = (args: string[], env: Record, confi args.push("--cacert", env.RESTIC_CACERT); } - // Add bandwidth limits if configuration is provided - if (config) { + // Add bandwidth limits if configuration is provided and not skipped + if (config && !options?.skipBandwidth) { if (config.backend === "rclone") { // For rclone backends, consolidate both upload and download limits into one bwlimit const uploadLimit = formatBandwidthLimit(config.uploadLimit); From 145c321fa1812e8c4529cc0012ca6a180075d7a7 Mon Sep 17 00:00:00 2001 From: Raj Dave Date: Sat, 10 Jan 2026 18:34:34 +0300 Subject: [PATCH 06/23] Refines bandwidth limit handling for rclone copy Simplifies bandwidth limit configuration for rclone source backends by considering only the source download limit. Ensures correct type casting when comparing upload and download limits, using base 10 for parsing. Makes the temporary keys cleanup function available for external use. --- app/server/utils/restic.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index cb30421b..21987af4 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -856,16 +856,9 @@ const copy = async ( const destUploadLimit = formatBandwidthLimit(destConfig.uploadLimit); if (sourceConfig.backend === "rclone") { - // For rclone source backends, use rclone.from.bwlimit with effective source limit - const sourceUploadLimit = formatBandwidthLimit(sourceConfig.uploadLimit); - - // Determine effective limit for source (pick smaller numeric value if both exist) + // For rclone source backends, use rclone.from.bwlimit with source download limit only let effectiveSourceLimit = ""; - if (sourceUploadLimit && sourceDownloadLimit) { - effectiveSourceLimit = parseInt(sourceUploadLimit) < parseInt(sourceDownloadLimit) ? sourceUploadLimit : sourceDownloadLimit; - } else if (sourceUploadLimit) { - effectiveSourceLimit = sourceUploadLimit; - } else if (sourceDownloadLimit) { + if (sourceDownloadLimit) { effectiveSourceLimit = sourceDownloadLimit; } @@ -960,7 +953,7 @@ export const addCommonArgs = (args: string[], env: Record, confi // Determine effective limit (choose more restrictive when both exist) let effectiveLimit = ""; if (uploadLimit && downloadLimit) { - effectiveLimit = parseInt(uploadLimit) < parseInt(downloadLimit) ? uploadLimit : downloadLimit; + effectiveLimit = parseInt(uploadLimit, 10) < parseInt(downloadLimit, 10) ? uploadLimit : downloadLimit; } else if (uploadLimit) { effectiveLimit = uploadLimit; } else if (downloadLimit) { @@ -1003,7 +996,7 @@ export const restic = { }; // Helper function to clean up temporary files -const cleanupTemporaryKeys = async (env: Record) => { +export const cleanupTemporaryKeys = async (env: Record) => { const keysToClean = ["_SFTP_KEY_PATH", "_SFTP_KNOWN_HOSTS_PATH", "RESTIC_CACERT", "GOOGLE_APPLICATION_CREDENTIALS"]; for (const key of keysToClean) { From 623b4b71167ccd3228fe4aeb04122724f95cfb39 Mon Sep 17 00:00:00 2001 From: Raj Dave Date: Tue, 13 Jan 2026 23:07:03 +0300 Subject: [PATCH 07/23] Remove 'Upload Limit' and 'Download Limit' text --- .../components/repository-forms/advanced-tls-form.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx b/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx index e042c4d9..336d4925 100644 --- a/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx +++ b/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx @@ -51,10 +51,6 @@ export const AdvancedForm = ({ form }: Props) => {
{/* Upload Limit */}
-
-

Upload Limit

-
- { {/* Download Limit */}
-
-

Download Limit

-
Date: Sat, 10 Jan 2026 11:15:55 +0100 Subject: [PATCH 08/23] docs: add troubleshooting readme file (#321) * docs: add troubleshooting section to README and create TROUBLESHOOTING.md * Update TROUBLESHOOTING.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update TROUBLESHOOTING.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update TROUBLESHOOTING.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 4 ++ TROUBLESHOOTING.md | 172 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 TROUBLESHOOTING.md diff --git a/README.md b/README.md index 4e3ffa7d..4f504ad7 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,10 @@ Zerobyte allows you to easily restore your data from backups. To restore data, n ![Preview](https://github.com/nicotsx/zerobyte/blob/main/screenshots/restoring.png?raw=true) +## Troubleshooting + +For troubleshooting common issues, please refer to the [TROUBLESHOOTING.md](TROUBLESHOOTING.md) file. + ## Third-Party Software This project includes the following third-party software components: diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 00000000..ad99d74e --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,172 @@ +# Troubleshooting + +If you encounter any issues while using Zerobyte, you can check the application logs for more information. +These logs can help you identify and resolve common problems; you should also check existing and closed issues on GitHub. +In case you need further assistance, feel free to open a new issue with detailed information about the problem you are facing and any relevant log entries. + +> [!WARNING] +> Make sure to never share sensitive information such as passwords, access keys, or personal data in public issues so remove them from logs before posting. + +To view the logs, run the command below: + +```bash +# replace 'zerobyte' with your container name if different +docker logs -f zerobyte +``` + +--- + +## Common issues + +### Permission denied errors when mounting remote shares + +Mounting remote filesystems (such as SMB/CIFS) requires kernel-level privileges. When Zerobyte attempts to perform mounts from inside a container, additional permissions may be required. + +Ensure that: + +* Remote share credentials are correct +* The host kernel supports the target filesystem (e.g. CIFS module is available) +* Docker is running in **rootful mode** (rootless Docker cannot perform kernel mounts) + +In some environments, Linux security mechanisms such as AppArmor or seccomp may block mount-related operations even when the required capabilities are present. + +--- + +### Security levels for mounting remote shares + +Zerobyte supports multiple deployment models depending on your security requirements and environment. + +--- + +#### **Secure** (recommended) + +Mount remote shares **outside of Zerobyte** (on the host) and point Zerobyte to an already mounted local path. + +This approach avoids granting additional privileges to the container and is the most portable and secure option. + +```yaml +services: + zerobyte: + volumes: + - /mnt/your-remote-share:/data +``` + +Remote mounts can be managed via `systemd`, `autofs`, or manual host mounts. + +--- + +#### **Advanced** (Zerobyte performs mounts) + +If Zerobyte must perform filesystem mounts itself, the container requires the `SYS_ADMIN` capability. + +```yaml +services: + zerobyte: + cap_add: + - SYS_ADMIN +``` + +> ⚠️ Granting `SYS_ADMIN` allows the container to perform mount operations and should be used only when strictly necessary. + +--- + +#### AppArmor-enabled systems (Ubuntu/Debian) + +On hosts using AppArmor, the default Docker profile (`docker-default`) may block mount operations even when `SYS_ADMIN` is present. + +If mount operations fail with permission errors, you may need to disable AppArmor confinement for the container. Check first if AppArmor is enabled on your system and the profile of the container: + +```bash +# check if AppArmor is enabled +sudo aa-status +# if next command returns 'docker-default', AppArmor is enabled on the container +docker inspect --format='{{.AppArmorProfile}}' zerobyte +``` + +If AppArmor is enabled, you can disable it for the Zerobyte container by adding the following to your `docker-compose.yml`: + +```yaml +services: + zerobyte: + cap_add: + - SYS_ADMIN + security_opt: + - apparmor:unconfined +``` + +--- + +#### Seccomp-restricted environments + +Docker's default seccomp profile may block mount-related syscalls required by filesystem operations. + +If mount operations continue to fail, you may need to disable seccomp filtering for the container: + +```yaml +services: + zerobyte: + cap_add: + - SYS_ADMIN + security_opt: + - seccomp:unconfined +``` + +--- + +#### SELinux-enabled systems (CentOS/Fedora) + +On hosts using SELinux, you may need to adjust the security context to allow mount operations. +If mount operations fail with permission errors, you can try adding the following label: + +```yaml +services: + zerobyte: + cap_add: + - SYS_ADMIN + security_opt: + - label:type:container_runtime_t +``` + +or disable SELinux enforcement for the container: + +```yaml +services: + zerobyte: + cap_add: + - SYS_ADMIN + security_opt: + - label:disable +``` + +--- + +#### **Not recommended** (When all else fails) + +Running the container in privileged mode disables most container isolation mechanisms and significantly increases the attack surface. + +This option should be used only as a last resort for troubleshooting. + +```yaml +services: + zerobyte: + privileged: true +``` + +--- + +### Notes on FUSE-based backends + +Access to `/dev/fuse` is required **only for FUSE-based filesystems** (such as `sshfs` or `rclone mount`). + +It is **not required** for SMB/CIFS mounts. + +### Rclone mount issues + +When using `rclone`, you may get errors like: + +```bash +bun: Failed to spawn process: EACCES +error > Failed to list rclone remotes: bun: Failed to spawn process: EACCES +``` + +Try to disable the AppArmor confinement for the container as described in the [AppArmor-enabled systems](#apparmor-enabled-systems-ubuntudebian) section above or [Seccomp-restricted environments](#seccomp-restricted-environments) section. From b9f92f7430ac46d965287e1bb236cbb040f29a5e Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Sat, 10 Jan 2026 11:57:24 +0100 Subject: [PATCH 09/23] chore: add a pre-commit hook to format files (#336) chore: add a pre-commit hook to format files --- .oxlintrc.json | 3 +- app/client/hooks/use-server-events.ts | 20 ++++++------- app/server/cli/commands/disable-2fa.ts | 26 +++++----------- app/server/cli/commands/reset-password.ts | 30 +++++-------------- app/server/core/capabilities.ts | 2 -- .../notifications/notifications.service.ts | 2 +- lefthook.yml | 7 +++++ 7 files changed, 35 insertions(+), 55 deletions(-) create mode 100644 lefthook.yml diff --git a/.oxlintrc.json b/.oxlintrc.json index 0caeb87a..cde16021 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -113,7 +113,8 @@ "unicorn/no-useless-spread": "warn", "unicorn/prefer-set-size": "warn", "unicorn/prefer-string-starts-ends-with": "warn", - "import/no-cycle": "error" + "import/no-cycle": "error", + "eslint/no-console": ["warn", { "allow": ["warn", "error", "info"] }] }, "settings": { "jsx-a11y": { diff --git a/app/client/hooks/use-server-events.ts b/app/client/hooks/use-server-events.ts index 211d33dc..fe3fc4bf 100644 --- a/app/client/hooks/use-server-events.ts +++ b/app/client/hooks/use-server-events.ts @@ -61,14 +61,14 @@ export function useServerEvents() { eventSourceRef.current = eventSource; eventSource.addEventListener("connected", () => { - console.log("[SSE] Connected to server events"); + console.info("[SSE] Connected to server events"); }); eventSource.addEventListener("heartbeat", () => {}); eventSource.addEventListener("backup:started", (e) => { const data = JSON.parse(e.data) as BackupEvent; - console.log("[SSE] Backup started:", data); + console.info("[SSE] Backup started:", data); handlersRef.current.get("backup:started")?.forEach((handler) => { handler(data); @@ -85,7 +85,7 @@ export function useServerEvents() { eventSource.addEventListener("backup:completed", (e) => { const data = JSON.parse(e.data) as BackupEvent; - console.log("[SSE] Backup completed:", data); + console.info("[SSE] Backup completed:", data); void queryClient.invalidateQueries(); void queryClient.refetchQueries(); @@ -97,7 +97,7 @@ export function useServerEvents() { eventSource.addEventListener("volume:mounted", (e) => { const data = JSON.parse(e.data) as VolumeEvent; - console.log("[SSE] Volume mounted:", data); + console.info("[SSE] Volume mounted:", data); handlersRef.current.get("volume:mounted")?.forEach((handler) => { handler(data); @@ -106,7 +106,7 @@ export function useServerEvents() { eventSource.addEventListener("volume:unmounted", (e) => { const data = JSON.parse(e.data) as VolumeEvent; - console.log("[SSE] Volume unmounted:", data); + console.info("[SSE] Volume unmounted:", data); handlersRef.current.get("volume:unmounted")?.forEach((handler) => { handler(data); @@ -115,7 +115,7 @@ export function useServerEvents() { eventSource.addEventListener("volume:updated", (e) => { const data = JSON.parse(e.data) as VolumeEvent; - console.log("[SSE] Volume updated:", data); + console.info("[SSE] Volume updated:", data); void queryClient.invalidateQueries(); @@ -126,7 +126,7 @@ export function useServerEvents() { eventSource.addEventListener("volume:status_updated", (e) => { const data = JSON.parse(e.data) as VolumeEvent; - console.log("[SSE] Volume status updated:", data); + console.info("[SSE] Volume status updated:", data); void queryClient.invalidateQueries(); @@ -137,7 +137,7 @@ export function useServerEvents() { eventSource.addEventListener("mirror:started", (e) => { const data = JSON.parse(e.data) as MirrorEvent; - console.log("[SSE] Mirror copy started:", data); + console.info("[SSE] Mirror copy started:", data); handlersRef.current.get("mirror:started")?.forEach((handler) => { handler(data); @@ -146,7 +146,7 @@ export function useServerEvents() { eventSource.addEventListener("mirror:completed", (e) => { const data = JSON.parse(e.data) as MirrorEvent; - console.log("[SSE] Mirror copy completed:", data); + console.info("[SSE] Mirror copy completed:", data); // Invalidate queries to refresh mirror status in the UI void queryClient.invalidateQueries(); @@ -161,7 +161,7 @@ export function useServerEvents() { }; return () => { - console.log("[SSE] Disconnecting from server events"); + console.info("[SSE] Disconnecting from server events"); eventSource.close(); eventSourceRef.current = null; }; diff --git a/app/server/cli/commands/disable-2fa.ts b/app/server/cli/commands/disable-2fa.ts index 1e0f1413..f4de9046 100644 --- a/app/server/cli/commands/disable-2fa.ts +++ b/app/server/cli/commands/disable-2fa.ts @@ -6,16 +6,11 @@ import { db } from "../../db/db"; import { twoFactor, usersTable } from "../../db/schema"; const listUsers = () => { - return db - .select({ id: usersTable.id, username: usersTable.username }) - .from(usersTable); + return db.select({ id: usersTable.id, username: usersTable.username }).from(usersTable); }; const disable2FA = async (username: string) => { - const [user] = await db - .select() - .from(usersTable) - .where(eq(usersTable.username, username)); + const [user] = await db.select().from(usersTable).where(eq(usersTable.username, username)); if (!user) { throw new Error(`User "${username}" not found`); @@ -26,10 +21,7 @@ const disable2FA = async (username: string) => { } await db.transaction(async (tx) => { - await tx - .update(usersTable) - .set({ twoFactorEnabled: false }) - .where(eq(usersTable.id, user.id)); + await tx.update(usersTable).set({ twoFactorEnabled: false }).where(eq(usersTable.id, user.id)); await tx.delete(twoFactor).where(eq(twoFactor.userId, user.id)); }); }; @@ -38,7 +30,7 @@ export const disable2FACommand = new Command("disable-2fa") .description("Disable two-factor authentication for a user") .option("-u, --username ", "Username of the account") .action(async (options) => { - console.log("\n🔐 Zerobyte 2FA Disable\n"); + console.info("\n🔐 Zerobyte 2FA Disable\n"); let username = options.username; @@ -47,9 +39,7 @@ export const disable2FACommand = new Command("disable-2fa") if (users.length === 0) { console.error("❌ No users found in the database."); - console.log( - " Please create a user first by starting the application.", - ); + console.info(" Please create a user first by starting the application."); process.exit(1); } @@ -61,10 +51,8 @@ export const disable2FACommand = new Command("disable-2fa") try { await disable2FA(username); - console.log( - `\n✅ Two-factor authentication has been disabled for user "${username}".`, - ); - console.log(" The user can re-enable 2FA from their account settings."); + console.info(`\n✅ Two-factor authentication has been disabled for user "${username}".`); + console.info(" The user can re-enable 2FA from their account settings."); } catch (error) { console.error(`\n❌ Failed to disable 2FA: ${toMessage(error)}`); process.exit(1); diff --git a/app/server/cli/commands/reset-password.ts b/app/server/cli/commands/reset-password.ts index ae482c6d..0306f802 100644 --- a/app/server/cli/commands/reset-password.ts +++ b/app/server/cli/commands/reset-password.ts @@ -7,16 +7,11 @@ import { db } from "../../db/db"; import { account, sessionsTable, usersTable } from "../../db/schema"; const listUsers = () => { - return db - .select({ id: usersTable.id, username: usersTable.username }) - .from(usersTable); + return db.select({ id: usersTable.id, username: usersTable.username }).from(usersTable); }; const resetPassword = async (username: string, newPassword: string) => { - const [user] = await db - .select() - .from(usersTable) - .where(eq(usersTable.username, username)); + const [user] = await db.select().from(usersTable).where(eq(usersTable.username, username)); if (!user) { throw new Error(`User "${username}" not found`); @@ -28,16 +23,11 @@ const resetPassword = async (username: string, newPassword: string) => { await tx .update(account) .set({ password: newPasswordHash }) - .where( - and(eq(account.userId, user.id), eq(account.providerId, "credential")), - ); + .where(and(eq(account.userId, user.id), eq(account.providerId, "credential"))); if (user.passwordHash) { const legacyHash = await Bun.password.hash(newPassword); - await tx - .update(usersTable) - .set({ passwordHash: legacyHash }) - .where(eq(usersTable.id, user.id)); + await tx.update(usersTable).set({ passwordHash: legacyHash }).where(eq(usersTable.id, user.id)); } await tx.delete(sessionsTable).where(eq(sessionsTable.userId, user.id)); @@ -49,7 +39,7 @@ export const resetPasswordCommand = new Command("reset-password") .option("-u, --username ", "Username of the account") .option("-p, --password ", "New password for the account") .action(async (options) => { - console.log("\n🔐 Zerobyte Password Reset\n"); + console.info("\n🔐 Zerobyte Password Reset\n"); let username = options.username; let newPassword = options.password; @@ -59,9 +49,7 @@ export const resetPasswordCommand = new Command("reset-password") if (users.length === 0) { console.error("❌ No users found in the database."); - console.log( - " Please create a user first by starting the application.", - ); + console.info(" Please create a user first by starting the application."); process.exit(1); } @@ -99,10 +87,8 @@ export const resetPasswordCommand = new Command("reset-password") try { await resetPassword(username, newPassword); - console.log( - `\n✅ Password for user "${username}" has been reset successfully.`, - ); - console.log(" All existing sessions have been invalidated."); + console.info(`\n✅ Password for user "${username}" has been reset successfully.`); + console.info(" All existing sessions have been invalidated."); } catch (error) { console.error(`\n❌ Failed to reset password: ${toMessage(error)}`); process.exit(1); diff --git a/app/server/core/capabilities.ts b/app/server/core/capabilities.ts index 41e75581..8f7dc029 100644 --- a/app/server/core/capabilities.ts +++ b/app/server/core/capabilities.ts @@ -1,6 +1,4 @@ import * as fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; import { logger } from "../utils/logger"; export type SystemCapabilities = { diff --git a/app/server/modules/notifications/notifications.service.ts b/app/server/modules/notifications/notifications.service.ts index e927e9ac..395056ff 100644 --- a/app/server/modules/notifications/notifications.service.ts +++ b/app/server/modules/notifications/notifications.service.ts @@ -229,7 +229,7 @@ const testDestination = async (id: number) => { const shoutrrrUrl = buildShoutrrrUrl(decryptedConfig); - console.log("Testing notification with Shoutrrr URL:", shoutrrrUrl); + logger.debug("Testing notification with Shoutrrr URL:", shoutrrrUrl); const result = await sendNotification({ shoutrrrUrl, diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 00000000..0af659fe --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,7 @@ +pre-commit: + commands: + oxfmt: + glob: '*.{js,jsx,ts,tsx,json,jsonc}' + run: bunx oxfmt --write {staged_files} + stage_fixed: true + From 9bff375217bfe6fc4f6727785ad7c1a4c1056709 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Sat, 10 Jan 2026 13:02:46 +0100 Subject: [PATCH 10/23] refactor: format dates in a human-friendly way (#334) --- app/client/components/cron-input.tsx | 8 +- app/client/components/onoff.tsx | 2 +- .../components/release-notes-dialog.tsx | 4 +- app/client/components/snapshots-table.tsx | 3 +- app/client/lib/datetime.ts | 92 +++++++++++++++++++ .../backups/components/backup-card.tsx | 9 +- .../backups/components/schedule-summary.tsx | 9 +- .../components/snapshot-file-browser.tsx | 3 +- .../backups/components/snapshot-timeline.tsx | 18 ++-- .../repositories/routes/snapshot-details.tsx | 3 +- app/client/modules/repositories/tabs/info.tsx | 7 +- .../volumes/components/healthchecks-card.tsx | 10 +- 12 files changed, 125 insertions(+), 43 deletions(-) create mode 100644 app/client/lib/datetime.ts diff --git a/app/client/components/cron-input.tsx b/app/client/components/cron-input.tsx index eb268fd7..c7e85e06 100644 --- a/app/client/components/cron-input.tsx +++ b/app/client/components/cron-input.tsx @@ -1,9 +1,9 @@ import { CronExpressionParser } from "cron-parser"; -import { format } from "date-fns"; import { AlertCircle, CheckCircle2 } from "lucide-react"; import { useMemo } from "react"; import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form"; import { Input } from "~/client/components/ui/input"; +import { formatDateTime } from "~/client/lib/datetime"; import { cn } from "~/client/lib/utils"; interface CronInputProps { @@ -51,7 +51,9 @@ export function CronInput({ value, onChange, error }: CronInputProps) { placeholder="* * * * *" value={value} onChange={(e) => onChange(e.target.value)} - className={cn("font-mono", { "border-destructive": error || (value && !isValid) })} + className={cn("font-mono", { + "border-destructive": error || (value && !isValid), + })} />
{value && ( @@ -77,7 +79,7 @@ export function CronInput({ value, onChange, error }: CronInputProps) { {nextRuns.map((date, i) => (
  • {i + 1}. - {format(date, "PPP p")} + {formatDateTime(date)}
  • ))} diff --git a/app/client/components/onoff.tsx b/app/client/components/onoff.tsx index 7fe95082..8161bc32 100644 --- a/app/client/components/onoff.tsx +++ b/app/client/components/onoff.tsx @@ -15,7 +15,7 @@ export const OnOff = ({ isOn, toggle, enabledLabel, disabledLabel, disabled }: P className={cn( "flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-semibold uppercase tracking-wide transition-colors", isOn - ? "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-500/40 dark:bg-emerald-500/10 dark:text-emerald-200" + ? "border-green-200 bg-green-50 text-green-700 dark:border-green-500/40 dark:bg-green-500/10 dark:text-green-200" : "border-muted bg-muted/40 text-muted-foreground dark:border-muted/60 dark:bg-muted/10", )} > diff --git a/app/client/components/release-notes-dialog.tsx b/app/client/components/release-notes-dialog.tsx index 81e1f765..f4f77f80 100644 --- a/app/client/components/release-notes-dialog.tsx +++ b/app/client/components/release-notes-dialog.tsx @@ -1,8 +1,8 @@ -import { format } from "date-fns"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/client/components/ui/dialog"; import { ScrollArea } from "~/client/components/ui/scroll-area"; +import { formatDate } from "~/client/lib/datetime"; import type { UpdateInfoDto } from "~/server/modules/system/system.dto"; interface ReleaseNotesDialogProps { @@ -29,7 +29,7 @@ export function ReleaseNotesDialog({ open, onOpenChange, updates }: ReleaseNotes

    {release.version}

    - {format(new Date(release.publishedAt), "PPP")} + {formatDate(release.publishedAt)}
    {release.body} diff --git a/app/client/components/snapshots-table.tsx b/app/client/components/snapshots-table.tsx index 9ff31b8f..613c54eb 100644 --- a/app/client/components/snapshots-table.tsx +++ b/app/client/components/snapshots-table.tsx @@ -27,6 +27,7 @@ import { } from "~/client/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; import { formatDuration } from "~/utils/utils"; +import { formatDateTime } from "~/client/lib/datetime"; import { deleteSnapshotsMutation, tagSnapshotsMutation } from "~/client/api-client/@tanstack/react-query.gen"; import { parseError } from "~/client/lib/errors"; import type { BackupSchedule, Snapshot } from "../lib/types"; @@ -185,7 +186,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
    - {new Date(snapshot.time).toLocaleString()} + {formatDateTime(snapshot.time)}
    diff --git a/app/client/lib/datetime.ts b/app/client/lib/datetime.ts new file mode 100644 index 00000000..1b107d48 --- /dev/null +++ b/app/client/lib/datetime.ts @@ -0,0 +1,92 @@ +import { formatDistanceToNow, isValid } from "date-fns"; + +// 1/10/2026, 2:30 PM +export function formatDateTime(date: Date | string | number | null | undefined): string { + if (!date) return "Never"; + const d = new Date(date); + if (!isValid(d)) return "Invalid Date"; + + return Intl.DateTimeFormat(navigator.languages, { + month: "numeric", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + }).format(d); +} + +// Jan 10, 2026 +export function formatDateWithMonth(date: Date | string | number | null | undefined): string { + if (!date) return "Never"; + const d = new Date(date); + if (!isValid(d)) return "Invalid Date"; + return Intl.DateTimeFormat(navigator.languages, { + month: "short", + day: "numeric", + year: "numeric", + }).format(d); +} + +// 1/10/2026 +export function formatDate(date: Date | string | number | null | undefined): string { + if (!date) return "Never"; + const d = new Date(date); + if (!isValid(d)) return "Invalid Date"; + + return Intl.DateTimeFormat(navigator.languages, { + month: "numeric", + day: "numeric", + year: "numeric", + }).format(d); +} + +// 1/10 +export function formatShortDate(date: Date | string | number | null | undefined): string { + if (!date) return "Never"; + const d = new Date(date); + if (!isValid(d)) return "Invalid Date"; + + return Intl.DateTimeFormat(navigator.languages, { + month: "numeric", + day: "numeric", + }).format(d); +} + +// 1/10, 2:30 PM +export function formatShortDateTime(date: Date | string | number | null | undefined): string { + if (!date) return "Never"; + const d = new Date(date); + if (!isValid(d)) return "Invalid Date"; + + return Intl.DateTimeFormat(navigator.languages, { + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + }).format(d); +} + +// 2:30 PM +export function formatTime(date: Date | string | number | null | undefined): string { + if (!date) return "Never"; + const d = new Date(date); + if (!isValid(d)) return "Invalid Date"; + + return Intl.DateTimeFormat(navigator.languages, { + hour: "numeric", + minute: "numeric", + }).format(d); +} + +// 5 minutes ago +export function formatTimeAgo(date: Date | string | number | null | undefined): string { + if (!date) return "Never"; + const d = new Date(date); + if (!isValid(d)) return "Invalid Date"; + + const timeAgo = formatDistanceToNow(d, { + addSuffix: true, + }); + + return timeAgo; +} diff --git a/app/client/modules/backups/components/backup-card.tsx b/app/client/modules/backups/components/backup-card.tsx index 85249704..074138e0 100644 --- a/app/client/modules/backups/components/backup-card.tsx +++ b/app/client/modules/backups/components/backup-card.tsx @@ -3,6 +3,7 @@ import { Link } from "react-router"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; import type { BackupSchedule } from "~/client/lib/types"; import { BackupStatusDot } from "./backup-status-dot"; +import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime"; export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => { return ( @@ -36,15 +37,11 @@ export const BackupCard = ({ schedule }: { schedule: BackupSchedule }) => {
    Last backup - - {schedule.lastBackupAt ? new Date(schedule.lastBackupAt).toLocaleDateString() : "Never"} - + {formatTimeAgo(schedule.lastBackupAt)}
    Next backup - - {schedule.nextBackupAt ? new Date(schedule.nextBackupAt).toLocaleDateString() : "N/A"} - + {formatShortDateTime(schedule.nextBackupAt)}
    diff --git a/app/client/modules/backups/components/schedule-summary.tsx b/app/client/modules/backups/components/schedule-summary.tsx index 16f1df03..1801f865 100644 --- a/app/client/modules/backups/components/schedule-summary.tsx +++ b/app/client/modules/backups/components/schedule-summary.tsx @@ -19,6 +19,7 @@ import { useMutation } from "@tanstack/react-query"; import { toast } from "sonner"; import { parseError } from "~/client/lib/errors"; import { Link } from "react-router"; +import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime"; type Props = { schedule: BackupSchedule; @@ -155,15 +156,11 @@ export const ScheduleSummary = (props: Props) => {

    Last backup

    -

    - {schedule.lastBackupAt ? new Date(schedule.lastBackupAt).toLocaleString() : "Never"} -

    +

    {formatTimeAgo(schedule.lastBackupAt)}

    Next backup

    -

    - {schedule.nextBackupAt ? new Date(schedule.nextBackupAt).toLocaleString() : "Never"} -

    +

    {formatShortDateTime(schedule.nextBackupAt)}

    diff --git a/app/client/modules/backups/components/snapshot-file-browser.tsx b/app/client/modules/backups/components/snapshot-file-browser.tsx index 185b0ed3..e2efbb37 100644 --- a/app/client/modules/backups/components/snapshot-file-browser.tsx +++ b/app/client/modules/backups/components/snapshot-file-browser.tsx @@ -7,6 +7,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/cli import { Button, buttonVariants } from "~/client/components/ui/button"; import type { Snapshot } from "~/client/lib/types"; import { listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen"; +import { formatDateTime } from "~/client/lib/datetime"; import { useFileBrowser } from "~/client/hooks/use-file-browser"; import { cn } from "~/client/lib/utils"; @@ -90,7 +91,7 @@ export const SnapshotFileBrowser = (props: Props) => { File Browser {`Viewing snapshot from ${new Date(snapshot?.time ?? 0).toLocaleString()}`} + >{`Viewing snapshot from ${formatDateTime(snapshot?.time)}`}
    { }, )} > -
    - {date.toLocaleDateString("en-US", { month: "short", day: "numeric" })} -
    -
    - {date.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" })} -
    +
    {formatShortDate(date)}
    +
    {formatTime(date)}
    @@ -98,8 +95,7 @@ export const SnapshotTimeline = (props: Props) => {
    {snapshots.length} snapshots - {new Date(snapshots[0].time).toLocaleDateString()} -{" "} - {new Date(snapshots.at(-1)?.time ?? 0).toLocaleDateString()} + {formatDateWithMonth(snapshots[0].time)} - {formatDateWithMonth(snapshots.at(-1)?.time)}
    diff --git a/app/client/modules/repositories/routes/snapshot-details.tsx b/app/client/modules/repositories/routes/snapshot-details.tsx index ea10fb6a..2d740d1b 100644 --- a/app/client/modules/repositories/routes/snapshot-details.tsx +++ b/app/client/modules/repositories/routes/snapshot-details.tsx @@ -3,6 +3,7 @@ import { redirect, useParams, Link, Await } from "react-router"; import { listBackupSchedulesOptions, listSnapshotFilesOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card"; import { SnapshotFileBrowser } from "~/client/modules/backups/components/snapshot-file-browser"; +import { formatDateTime } from "~/client/lib/datetime"; import { getRepository, getSnapshotDetails } from "~/client/api-client"; import type { Route } from "./+types/snapshot-details"; import { Suspense } from "react"; @@ -112,7 +113,7 @@ export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps
    Time: -

    {new Date(data.snapshot.time).toLocaleString()}

    +

    {formatDateTime(data.snapshot.time)}

    Loading...
    }> diff --git a/app/client/modules/repositories/tabs/info.tsx b/app/client/modules/repositories/tabs/info.tsx index e22b8c6a..6b63330d 100644 --- a/app/client/modules/repositories/tabs/info.tsx +++ b/app/client/modules/repositories/tabs/info.tsx @@ -19,6 +19,7 @@ import { } from "~/client/components/ui/alert-dialog"; import type { Repository } from "~/client/lib/types"; import { REPOSITORY_BASE } from "~/client/lib/constants"; +import { formatDateTime, formatTimeAgo } from "~/client/lib/datetime"; import { updateRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen"; import type { CompressionMode, RepositoryConfig } from "~/schemas/restic"; @@ -131,13 +132,11 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => { )}
    Created at
    -

    {new Date(repository.createdAt).toLocaleString()}

    +

    {formatDateTime(repository.createdAt)}

    Last checked
    -

    - {repository.lastChecked ? new Date(repository.lastChecked).toLocaleString() : "Never"} -

    +

    {formatTimeAgo(repository.lastChecked)}

    {config.cacert && (
    diff --git a/app/client/modules/volumes/components/healthchecks-card.tsx b/app/client/modules/volumes/components/healthchecks-card.tsx index 198efc46..86a3c88e 100644 --- a/app/client/modules/volumes/components/healthchecks-card.tsx +++ b/app/client/modules/volumes/components/healthchecks-card.tsx @@ -1,11 +1,11 @@ import { useMutation } from "@tanstack/react-query"; -import { formatDistanceToNow } from "date-fns"; import { Activity, HeartIcon } from "lucide-react"; import { toast } from "sonner"; import { healthCheckVolumeMutation, updateVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen"; import { OnOff } from "~/client/components/onoff"; import { Button } from "~/client/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; +import { formatTimeAgo } from "~/client/lib/datetime"; import type { Volume } from "~/client/lib/types"; type Props = { @@ -13,10 +13,6 @@ type Props = { }; export const HealthchecksCard = ({ volume }: Props) => { - const timeAgo = formatDistanceToNow(volume.lastHealthCheck, { - addSuffix: true, - }); - const healthcheck = useMutation({ ...healthCheckVolumeMutation(), onSuccess: (d) => { @@ -55,9 +51,9 @@ export const HealthchecksCard = ({ volume }: Props) => {
    {volume.lastError && {volume.lastError}} - {volume.status === "mounted" && Healthy} + {volume.status === "mounted" && Healthy} {volume.status !== "unmounted" && ( - Checked {timeAgo || "never"} + Checked {formatTimeAgo(volume.lastHealthCheck)} )} Remount on error From 4fda10d89d08328779d3971b0082539c132e8162 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sat, 10 Jan 2026 13:10:26 +0100 Subject: [PATCH 11/23] style(snapshots-timeline): ensure same width for all cards --- .../modules/backups/components/snapshot-timeline.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/client/modules/backups/components/snapshot-timeline.tsx b/app/client/modules/backups/components/snapshot-timeline.tsx index 0dbc801a..c8200643 100644 --- a/app/client/modules/backups/components/snapshot-timeline.tsx +++ b/app/client/modules/backups/components/snapshot-timeline.tsx @@ -82,9 +82,14 @@ export const SnapshotTimeline = (props: Props) => {
    - {isLatest && ( -
    Latest
    - )} +
    + Latest +
    ); })} From 881e8f8abe6b1f00bd2c31b205d3dcbd6bf7861f Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sat, 10 Jan 2026 13:22:57 +0100 Subject: [PATCH 12/23] feat: configurable trusted origins --- README.md | 3 ++- app/lib/auth.ts | 5 ++--- app/server/app.ts | 5 +++++ app/server/core/config.ts | 2 ++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4f504ad7..94c166ad 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,10 @@ Zerobyte can be customized using environment variables. Below are the available | :-------------------- | :----------------------------------------------------------------------------------------------------------------- | :--------- | | `PORT` | The port the web interface and API will listen on. | `4096` | | `RESTIC_HOSTNAME` | The hostname used by Restic when creating snapshots. Automatically detected if a custom hostname is set in Docker. | `zerobyte` | +| `TZ` | Timezone for the container (e.g., `Europe/Paris`). **Crucial for accurate backup scheduling.** | `UTC` | +| `TRUSTED_ORIGINS` | Comma-separated list of trusted origins for CORS (e.g., `http://localhost:3000,http://example.com`). | (none) | | `LOG_LEVEL` | Logging verbosity. Options: `debug`, `info`, `warn`, `error`. | `info` | | `SERVER_IDLE_TIMEOUT` | Idle timeout for the server in seconds. | `60` | -| `TZ` | Timezone for the container (e.g., `Europe/Paris`). **Crucial for accurate backup scheduling.** | `UTC` | ### Secret References diff --git a/app/lib/auth.ts b/app/lib/auth.ts index 20631681..3e4b3feb 100644 --- a/app/lib/auth.ts +++ b/app/lib/auth.ts @@ -11,12 +11,14 @@ import { convertLegacyUserOnFirstLogin } from "./auth-middlewares/convert-legacy import { cryptoUtils } from "~/server/utils/crypto"; import { db } from "~/server/db/db"; import { ensureOnlyOneUser } from "./auth-middlewares/only-one-user"; +import { config } from "~/server/core/config"; export type AuthMiddlewareContext = MiddlewareContext>; const createBetterAuth = (secret: string) => betterAuth({ secret, + trustedOrigins: config.trustedOrigins ?? ["*"], hooks: { before: createAuthMiddleware(async (ctx) => { await ensureOnlyOneUser(ctx); @@ -55,9 +57,6 @@ const createBetterAuth = (secret: string) => }, }), ], - advanced: { - disableOriginCheck: true, - }, }); type Auth = ReturnType; diff --git a/app/server/app.ts b/app/server/app.ts index e5bae37e..f127d431 100644 --- a/app/server/app.ts +++ b/app/server/app.ts @@ -1,5 +1,6 @@ import { Scalar } from "@scalar/hono-api-reference"; import { Hono } from "hono"; +import { cors } from "hono/cors"; import { logger as honoLogger } from "hono/logger"; import { secureHeaders } from "hono/secure-headers"; import { rateLimiter } from "hono-rate-limiter"; @@ -38,6 +39,10 @@ export const scalarDescriptor = Scalar({ export const createApp = () => { const app = new Hono(); + if (config.trustedOrigins) { + app.use(cors({ origin: config.trustedOrigins })); + } + if (config.environment !== "test") { app.use(honoLogger()); } diff --git a/app/server/core/config.ts b/app/server/core/config.ts index 38d09bec..7fb2b973 100644 --- a/app/server/core/config.ts +++ b/app/server/core/config.ts @@ -32,6 +32,7 @@ const envSchema = type({ PORT: 'string.integer.parse = "4096"', MIGRATIONS_PATH: "string?", APP_VERSION: "string = 'dev'", + TRUSTED_ORIGINS: "string?", }).pipe((s) => ({ __prod__: s.NODE_ENV === "production", environment: s.NODE_ENV, @@ -41,6 +42,7 @@ const envSchema = type({ port: s.PORT, migrationsPath: s.MIGRATIONS_PATH, appVersion: s.APP_VERSION, + trustedOrigins: s.TRUSTED_ORIGINS?.split(",").map((origin) => origin.trim()), })); const parseConfig = (env: unknown) => { From 1462250727bc6a24b3fdd7e59d9b1699ec8de749 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sat, 10 Jan 2026 13:32:23 +0100 Subject: [PATCH 13/23] chore: remove "about", "over", "almost" from timeago --- Dockerfile | 4 ++-- app/client/lib/datetime.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6423c386..159ba404 100644 --- a/Dockerfile +++ b/Dockerfile @@ -64,7 +64,7 @@ COPY --from=deps /deps/shoutrrr /usr/local/bin/shoutrrr COPY ./package.json ./bun.lock ./ -RUN bun install --frozen-lockfile --verbose --ignore-scripts +RUN bun install --frozen-lockfile --ignore-scripts COPY . . @@ -98,7 +98,7 @@ ENV NODE_ENV="production" WORKDIR /app COPY --from=builder /app/package.json ./ -RUN bun install --production --frozen-lockfile --verbose +RUN bun install --production --frozen-lockfile COPY --from=deps /deps/restic /usr/local/bin/restic COPY --from=deps /deps/rclone /usr/local/bin/rclone diff --git a/app/client/lib/datetime.ts b/app/client/lib/datetime.ts index 1b107d48..d183dd3f 100644 --- a/app/client/lib/datetime.ts +++ b/app/client/lib/datetime.ts @@ -86,7 +86,8 @@ export function formatTimeAgo(date: Date | string | number | null | undefined): const timeAgo = formatDistanceToNow(d, { addSuffix: true, + includeSeconds: true, }); - return timeAgo; + return timeAgo.replace("about ", "").replace("over ", "").replace("almost ", "").replace("less than ", ""); } From 60c0ce208d94e007fe09fae92918232d0feccb6e Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Sun, 11 Jan 2026 10:31:04 +0100 Subject: [PATCH 14/23] test(e2e): admin user registration (#338) * test(e2e): admin user registration * ci: e2e workflow * feat: disable rate limiting env var * test(e2e): fix order of execution in registration tests * ci: run e2e tests before release --- .env.example | 1 + .github/workflows/e2e.yml | 74 ++++++++++++++++++++++++++++++ .github/workflows/release.yml | 5 ++- .gitignore | 8 +++- app/server/app.ts | 12 ++--- app/server/core/config.ts | 2 + app/server/core/constants.ts | 2 +- app/server/db/db.ts | 6 +++ bun.lock | 72 ++++++++++++++++++++++++++++- docker-compose.yml | 19 ++++++++ e2e/0001-initial-setup.spec.ts | 82 ++++++++++++++++++++++++++++++++++ e2e/helpers/account.ts | 24 ++++++++++ e2e/helpers/db.ts | 20 +++++++++ package.json | 8 +++- playwright.config.ts | 51 +++++++++++++++++++++ 15 files changed, 376 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/e2e.yml create mode 100644 e2e/0001-initial-setup.spec.ts create mode 100644 e2e/helpers/account.ts create mode 100644 e2e/helpers/db.ts create mode 100644 playwright.config.ts diff --git a/.env.example b/.env.example index 81278aa5..7ce4dab1 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ +SERVER_IP=localhost DATABASE_URL=./data/zerobyte.db RESTIC_PASS_FILE=./data/restic.pass RESTIC_CACHE_DIR=./data/restic/cache diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..747d5888 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,74 @@ +name: E2E Tests +permissions: + contents: read + +on: + workflow_dispatch: + workflow_call: + +jobs: + playwright: + name: Playwright + timeout-minutes: 20 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + uses: "./.github/actions/install-dependencies" + + - name: Get Playwright version + id: playwright-version + shell: bash + run: | + playwright_version=$(bun -e "console.log((await Bun.file('./package.json').json()).devDependencies['@playwright/test'])") + echo "version=$playwright_version" >> $GITHUB_OUTPUT + + - name: Cache Playwright Browsers + id: playwright-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }} + restore-keys: | + ${{ runner.os }}-playwright- + + - name: Install Playwright Browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: bunx playwright install --with-deps + + - name: Install Playwright System Dependencies + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: bunx playwright install-deps + + - name: Prepare environment + run: | + mkdir -p data + touch .env.local + echo "SERVER_IP=localhost" >> .env.local + echo "DATABASE_URL=./data/zerobyte.db" >> .env.local + + - name: Start zerobyte-e2e service + run: bun run start:e2e -- -d + + - name: Wait for zerobyte to be ready + run: | + timeout 30s bash -c 'until curl -f http://localhost:4096/healthcheck; do echo "Waiting for server..." && sleep 2; done' + continue-on-error: false + + - name: Make data directory writable + run: sudo chmod -R 777 data + + - name: Run Playwright tests + run: bun run test:e2e + + - name: Stop Docker Compose + if: always() + run: docker compose down + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e87d09f..7ed6bc48 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,9 +34,12 @@ jobs: checks: uses: ./.github/workflows/checks.yml + e2e-tests: + uses: ./.github/workflows/e2e.yml + build-images: timeout-minutes: 15 - needs: [determine-release-type, checks] + needs: [determine-release-type, checks, e2e-tests] runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/.gitignore b/.gitignore index 887dcd56..329fb07b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,10 @@ data/ !.env.example !.env.test -.idea/ +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/app/server/app.ts b/app/server/app.ts index f127d431..9aed26ad 100644 --- a/app/server/app.ts +++ b/app/server/app.ts @@ -37,7 +37,7 @@ export const scalarDescriptor = Scalar({ }); export const createApp = () => { - const app = new Hono(); + const app = new Hono().use(secureHeaders()); if (config.trustedOrigins) { app.use(cors({ origin: config.trustedOrigins })); @@ -47,9 +47,8 @@ export const createApp = () => { app.use(honoLogger()); } - app - .use(secureHeaders()) - .use( + if (!config.disableRateLimiting) { + app.use( rateLimiter({ windowMs: 60 * 5 * 1000, limit: 1000, @@ -58,7 +57,10 @@ export const createApp = () => { return config.__prod__ === false; }, }), - ) + ); + } + + app .get("healthcheck", (c) => c.json({ status: "ok" })) .route("/api/v1/auth", authController) .route("/api/v1/volumes", volumeController) diff --git a/app/server/core/config.ts b/app/server/core/config.ts index 7fb2b973..70ed7e21 100644 --- a/app/server/core/config.ts +++ b/app/server/core/config.ts @@ -33,6 +33,7 @@ const envSchema = type({ MIGRATIONS_PATH: "string?", APP_VERSION: "string = 'dev'", TRUSTED_ORIGINS: "string?", + DISABLE_RATE_LIMITING: 'string = "false"', }).pipe((s) => ({ __prod__: s.NODE_ENV === "production", environment: s.NODE_ENV, @@ -43,6 +44,7 @@ const envSchema = type({ migrationsPath: s.MIGRATIONS_PATH, appVersion: s.APP_VERSION, trustedOrigins: s.TRUSTED_ORIGINS?.split(",").map((origin) => origin.trim()), + disableRateLimiting: s.DISABLE_RATE_LIMITING === "true", })); const parseConfig = (env: unknown) => { diff --git a/app/server/core/constants.ts b/app/server/core/constants.ts index 87e58f08..a98c54eb 100644 --- a/app/server/core/constants.ts +++ b/app/server/core/constants.ts @@ -5,7 +5,7 @@ export const REPOSITORY_BASE = process.env.ZEROBYTE_REPOSITORIES_DIR || "/var/li export const RESTIC_CACHE_DIR = process.env.RESTIC_CACHE_DIR || "/var/lib/zerobyte/restic/cache"; -export const DATABASE_URL = process.env.DATABASE_URL || "/var/lib/zerobyte/data/ironmount.db"; +export const DATABASE_URL = process.env.DATABASE_URL || "/var/lib/zerobyte/data/zerobyte.db"; export const RESTIC_PASS_FILE = process.env.RESTIC_PASS_FILE || "/var/lib/zerobyte/data/restic.pass"; export const DEFAULT_EXCLUDES = [DATABASE_URL, RESTIC_PASS_FILE, REPOSITORY_BASE]; diff --git a/app/server/db/db.ts b/app/server/db/db.ts index 10fbf264..ac57536d 100644 --- a/app/server/db/db.ts +++ b/app/server/db/db.ts @@ -27,7 +27,13 @@ const initDb = () => { if (!_schema) { throw new Error("Database schema not set. Call setSchema() before accessing the database."); } + fs.mkdirSync(path.dirname(DATABASE_URL), { recursive: true }); + + if (fs.existsSync(path.join(path.dirname(DATABASE_URL), "ironmount.db")) && !fs.existsSync(DATABASE_URL)) { + fs.renameSync(path.join(path.dirname(DATABASE_URL), "ironmount.db"), DATABASE_URL); + } + _sqlite = new Database(DATABASE_URL); return drizzle({ client: _sqlite, schema: _schema }); }; diff --git a/bun.lock b/bun.lock index 498b7f3a..2be3d8a8 100644 --- a/bun.lock +++ b/bun.lock @@ -70,6 +70,8 @@ "@faker-js/faker": "^10.2.0", "@happy-dom/global-registrator": "^20.1.0", "@hey-api/openapi-ts": "^0.90.2", + "@libsql/client": "^0.17.0", + "@playwright/test": "^1.57.0", "@react-router/dev": "^7.12.0", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.17", @@ -314,8 +316,36 @@ "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], + "@libsql/client": ["@libsql/client@0.17.0", "", { "dependencies": { "@libsql/core": "^0.17.0", "@libsql/hrana-client": "^0.9.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-TLjSU9Otdpq0SpKHl1tD1Nc9MKhrsZbCFGot3EbCxRa8m1E5R1mMwoOjKMMM31IyF7fr+hPNHLpYfwbMKNusmg=="], + + "@libsql/core": ["@libsql/core@0.17.0", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-hnZRnJHiS+nrhHKLGYPoJbc78FE903MSDrFJTbftxo+e52X+E0Y0fHOCVYsKWcg6XgB7BbJYUrz/xEkVTSaipw=="], + + "@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.22", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4B8ZlX3nIDPndfct7GNe0nI3Yw6ibocEicWdC4fvQbSs/jdq/RC2oCsoJxJ4NzXkvktX70C1J4FcmmoBy069UA=="], + + "@libsql/darwin-x64": ["@libsql/darwin-x64@0.5.22", "", { "os": "darwin", "cpu": "x64" }, "sha512-ny2HYWt6lFSIdNFzUFIJ04uiW6finXfMNJ7wypkAD8Pqdm6nAByO+Fdqu8t7sD0sqJGeUCiOg480icjyQ2/8VA=="], + + "@libsql/hrana-client": ["@libsql/hrana-client@0.9.0", "", { "dependencies": { "@libsql/isomorphic-ws": "^0.1.5", "cross-fetch": "^4.0.0", "js-base64": "^3.7.5", "node-fetch": "^3.3.2" } }, "sha512-pxQ1986AuWfPX4oXzBvLwBnfgKDE5OMhAdR/5cZmRaB4Ygz5MecQybvwZupnRz341r2CtFmbk/BhSu7k2Lm+Jw=="], + + "@libsql/isomorphic-ws": ["@libsql/isomorphic-ws@0.1.5", "", { "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" } }, "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg=="], + + "@libsql/linux-arm-gnueabihf": ["@libsql/linux-arm-gnueabihf@0.5.22", "", { "os": "linux", "cpu": "arm" }, "sha512-3Uo3SoDPJe/zBnyZKosziRGtszXaEtv57raWrZIahtQDsjxBVjuzYQinCm9LRCJCUT5t2r5Z5nLDPJi2CwZVoA=="], + + "@libsql/linux-arm-musleabihf": ["@libsql/linux-arm-musleabihf@0.5.22", "", { "os": "linux", "cpu": "arm" }, "sha512-LCsXh07jvSojTNJptT9CowOzwITznD+YFGGW+1XxUr7fS+7/ydUrpDfsMX7UqTqjm7xG17eq86VkWJgHJfvpNg=="], + + "@libsql/linux-arm64-gnu": ["@libsql/linux-arm64-gnu@0.5.22", "", { "os": "linux", "cpu": "arm64" }, "sha512-KSdnOMy88c9mpOFKUEzPskSaF3VLflfSUCBwas/pn1/sV3pEhtMF6H8VUCd2rsedwoukeeCSEONqX7LLnQwRMA=="], + + "@libsql/linux-arm64-musl": ["@libsql/linux-arm64-musl@0.5.22", "", { "os": "linux", "cpu": "arm64" }, "sha512-mCHSMAsDTLK5YH//lcV3eFEgiR23Ym0U9oEvgZA0667gqRZg/2px+7LshDvErEKv2XZ8ixzw3p1IrBzLQHGSsw=="], + + "@libsql/linux-x64-gnu": ["@libsql/linux-x64-gnu@0.5.22", "", { "os": "linux", "cpu": "x64" }, "sha512-kNBHaIkSg78Y4BqAdgjcR2mBilZXs4HYkAmi58J+4GRwDQZh5fIUWbnQvB9f95DkWUIGVeenqLRFY2pcTmlsew=="], + + "@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.5.22", "", { "os": "linux", "cpu": "x64" }, "sha512-UZ4Xdxm4pu3pQXjvfJiyCzZop/9j/eA2JjmhMaAhe3EVLH2g11Fy4fwyUp9sT1QJYR1kpc2JLuybPM0kuXv/Tg=="], + + "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.22", "", { "os": "win32", "cpu": "x64" }, "sha512-Fj0j8RnBpo43tVZUVoNK6BV/9AtDUM5S7DF3LB4qTYg1LMSZqi3yeCneUTLJD6XomQJlZzbI4mst89yspVSAnA=="], + "@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.2.0", "", {}, "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng=="], + "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], + "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], @@ -364,6 +394,8 @@ "@oxlint/win32-x64": ["@oxlint/win32-x64@1.38.0", "", { "os": "win32", "cpu": "x64" }, "sha512-7IuZMYiZiOcgg5zHvpJY6jRlEwh8EB/uq7GsoQJO9hANq96TIjyntGByhIjFSsL4asyZmhTEki+MO/u5Fb/WQA=="], + "@playwright/test": ["@playwright/test@1.57.0", "", { "dependencies": { "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" } }, "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA=="], + "@prisma/client": ["@prisma/client@5.22.0", "", { "peerDependencies": { "prisma": "*" }, "optionalPeers": ["prisma"] }, "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA=="], "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], @@ -566,6 +598,8 @@ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], + "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], @@ -660,7 +694,7 @@ "better-call": ["better-call@1.1.7", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-6gaJe1bBIEgVebQu/7q9saahVzvBsGaByEnE8aDVncZEDiJO7sdNB28ot9I6iXSbR25egGmmZ6aIURXyQHRraQ=="], - "better-sqlite3": ["better-sqlite3@12.5.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg=="], + "better-sqlite3": ["better-sqlite3@12.6.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-FXI191x+D6UPWSze5IzZjhz+i9MK9nsuHsmTX9bXVl52k06AfZ2xql0lrgIUuzsMsJ7Vgl5kIptvDgBLIV3ZSQ=="], "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], @@ -750,6 +784,8 @@ "cron-parser": ["cron-parser@5.4.0", "", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-HxYB8vTvnQFx4dLsZpGRa0uHp6X3qIzS3ZJgJ9v6l/5TJMgeWQbLkR5yiJ5hOxGbc9+jCADDnydIe15ReLZnJA=="], + "cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], @@ -778,6 +814,8 @@ "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -884,12 +922,16 @@ "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], @@ -992,6 +1034,8 @@ "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -1004,6 +1048,8 @@ "kysely": ["kysely@0.28.9", "", {}, "sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA=="], + "libsql": ["libsql@0.5.22", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.22", "@libsql/darwin-x64": "0.5.22", "@libsql/linux-arm-gnueabihf": "0.5.22", "@libsql/linux-arm-musleabihf": "0.5.22", "@libsql/linux-arm64-gnu": "0.5.22", "@libsql/linux-arm64-musl": "0.5.22", "@libsql/linux-x64-gnu": "0.5.22", "@libsql/linux-x64-musl": "0.5.22", "@libsql/win32-x64-msvc": "0.5.22" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-NscWthMQt7fpU8lqd7LXMvT9pi+KhhmTHAJWUB/Lj6MWa0MKFv0F2V4C6WKKpjCVZl0VwcDz4nOI3CyaT1DDiA=="], + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], @@ -1174,6 +1220,10 @@ "node-cron": ["node-cron@4.2.1", "", {}, "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -1238,6 +1288,10 @@ "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="], + + "playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="], + "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=="], "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], @@ -1258,6 +1312,8 @@ "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], + "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "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=="], @@ -1424,6 +1480,8 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], @@ -1490,8 +1548,14 @@ "vite-tsconfig-paths": ["vite-tsconfig-paths@6.0.3", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-7bL7FPX/DSviaZGYUKowWF1AiDVWjMjxNbE8lyaVGDezkedWqfGhlnQ4BZXre0ZN5P4kAgIJfAlgFDVyjrCIyg=="], + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "winston": ["winston@3.19.0", "", { "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-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA=="], @@ -1574,6 +1638,8 @@ "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], @@ -1586,6 +1652,8 @@ "happy-dom/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], + "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], + "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=="], @@ -1598,6 +1666,8 @@ "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], diff --git a/docker-compose.yml b/docker-compose.yml index b90b8518..fa3d629b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,3 +39,22 @@ services: - /etc/localtime:/etc/localtime:ro - /var/lib/zerobyte:/var/lib/zerobyte - ~/.config/rclone:/root/.config/rclone + + zerobyte-e2e: + build: + context: . + dockerfile: Dockerfile + target: production + container_name: zerobyte + restart: unless-stopped + environment: + - DISABLE_RATE_LIMITING=true + devices: + - /dev/fuse:/dev/fuse + cap_add: + - SYS_ADMIN + ports: + - "4096:4096" + volumes: + - /etc/localtime:/etc/localtime:ro + - ./data:/var/lib/zerobyte diff --git a/e2e/0001-initial-setup.spec.ts b/e2e/0001-initial-setup.spec.ts new file mode 100644 index 00000000..38251896 --- /dev/null +++ b/e2e/0001-initial-setup.spec.ts @@ -0,0 +1,82 @@ +import fs from "fs"; +import { test, expect } from "@playwright/test"; +import { resetDatabase } from "./helpers/db"; + +// TODO: Run these tests with different users once multi-user support is added + +// Run tests in serial mode to avoid conflicts during onboarding +test.describe.configure({ mode: "serial" }); + +test.beforeAll(async () => { + await resetDatabase(); +}); + +test("should redirect to onboarding", async ({ page }) => { + await page.goto("/"); + + await page.waitForURL(/onboarding/); + + await expect(page).toHaveTitle(/Zerobyte - Onboarding/); +}); + +test("user can register a new account", async ({ page }) => { + await page.goto("/onboarding"); + + await page.getByRole("textbox", { name: "Email" }).click(); + await page.getByRole("textbox", { name: "Email" }).fill("test@test.com"); + + await page.getByRole("textbox", { name: "Username" }).fill("test"); + + await page.getByRole("textbox", { name: "Password", exact: true }).fill("password"); + await page.getByRole("textbox", { name: "Confirm Password" }).fill("password"); + + await page.getByRole("button", { name: "Create admin user" }).click(); + + await expect(page.getByText("Download Your Recovery Key")).toBeVisible(); +}); + +test("user can download recovery key", async ({ page }) => { + await page.goto("/login"); + + await page.getByRole("textbox", { name: "Username" }).fill("test"); + await page.getByRole("textbox", { name: "Password" }).fill("password"); + await page.getByRole("button", { name: "Login" }).click(); + + await expect(page.getByText("Download Your Recovery Key")).toBeVisible(); + + await page.getByRole("textbox", { name: "Confirm Your Password" }).fill("test"); + await page.getByRole("button", { name: "Download Recovery Key" }).click(); + + // Should not be able to download with invalid confirm password + await expect(page.getByText("Invalid password")).toBeVisible(); + + await page.getByRole("textbox", { name: "Confirm Your Password" }).fill("password"); + + const downloadPromise = page.waitForEvent("download"); + await page.getByRole("button", { name: "Download Recovery Key" }).click(); + + const download = await downloadPromise; + + expect(download.suggestedFilename()).toBe("restic.pass"); + await download.saveAs("./data/restic.pass"); + + const fileContent = await fs.promises.readFile("./data/restic.pass", "utf8"); + + expect(fileContent).toHaveLength(64); +}); + +test("can't create another admin user after initial setup", async ({ page }) => { + await page.goto("/onboarding"); + + await page.getByRole("textbox", { name: "Email" }).click(); + await page.getByRole("textbox", { name: "Email" }).fill("test@test.com"); + + await page.getByRole("textbox", { name: "Username" }).fill("test"); + + await page.getByRole("textbox", { name: "Password", exact: true }).fill("password"); + await page.getByRole("textbox", { name: "Confirm Password" }).fill("password"); + + await page.getByRole("button", { name: "Create admin user" }).click(); + + await expect(page.getByText("Failed to create admin user")).toBeVisible(); +}); diff --git a/e2e/helpers/account.ts b/e2e/helpers/account.ts new file mode 100644 index 00000000..7e94d7f9 --- /dev/null +++ b/e2e/helpers/account.ts @@ -0,0 +1,24 @@ +import { expect, type Page } from "@playwright/test"; +import { db } from "./db"; + +export const createTestAccount = async (page: Page) => { + const existingUsers = await db.query.usersTable.findFirst(); + + if (existingUsers) { + return; + } + + await page.goto("/onboarding"); + + await page.getByRole("textbox", { name: "Email" }).click(); + await page.getByRole("textbox", { name: "Email" }).fill("test@test.com"); + + await page.getByRole("textbox", { name: "Username" }).fill("test"); + + await page.getByRole("textbox", { name: "Password", exact: true }).fill("password"); + await page.getByRole("textbox", { name: "Confirm Password" }).fill("password"); + + await page.getByRole("button", { name: "Create admin user" }).click(); + + await expect(page.getByText("Download Your Recovery Key")).toBeVisible(); +}; diff --git a/e2e/helpers/db.ts b/e2e/helpers/db.ts new file mode 100644 index 00000000..3c0068ac --- /dev/null +++ b/e2e/helpers/db.ts @@ -0,0 +1,20 @@ +import { createClient } from "@libsql/client"; +import { drizzle } from "drizzle-orm/libsql"; +import path from "node:path"; +import { DATABASE_URL } from "~/server/core/constants"; +import * as schema from "~/server/db/schema"; + +const sqlite = createClient({ url: `file:${path.join(process.cwd(), "data", DATABASE_URL)}` }); + +export const db = drizzle({ client: sqlite, schema: schema }); + +export const resetDatabase = async () => { + const cursor = await sqlite.execute("SELECT name FROM sqlite_master WHERE type='table'"); + const tables = cursor.rows + .map((row) => row.name) + .filter((name) => name !== "sqlite_sequence" && name !== "__drizzle_migrations") as string[]; + + for (const table of tables) { + await sqlite.execute(`DELETE FROM ${table}`); + } +}; diff --git a/package.json b/package.json index 4f20ca3d..e6cae0e8 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,16 @@ "tsc": "react-router typegen && tsc", "start:dev": "docker compose down && docker compose up --build zerobyte-dev", "start:prod": "docker compose down && docker compose up --build zerobyte-prod", + "start:e2e": "docker compose down && docker compose up --build zerobyte-e2e", "gen:api-client": "openapi-ts", "gen:migrations": "drizzle-kit generate", "studio": "drizzle-kit studio", "test:server": "dotenv -e .env.test -- bun test app/server --preload ./app/test/setup.ts", "test:client": "dotenv -e .env.test -- bun test app/client --preload ./app/test/setup-client.ts", - "test": "bun run test:server && bun run test:client" + "test": "bun run test:server && bun run test:client", + "test:e2e": "NODE_ENV=test dotenv -e .env.local -- playwright test", + "test:e2e:ui": "NODE_ENV=test dotenv -e .env.local -- playwright test --ui", + "test:codegen": "playwright codegen localhost:4096" }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -85,6 +89,8 @@ "@faker-js/faker": "^10.2.0", "@happy-dom/global-registrator": "^20.1.0", "@hey-api/openapi-ts": "^0.90.2", + "@libsql/client": "^0.17.0", + "@playwright/test": "^1.57.0", "@react-router/dev": "^7.12.0", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.17", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..1bb90c36 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,51 @@ +import "dotenv/config"; +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: "html", + use: { + baseURL: `http://${process.env.SERVER_IP}:4096`, + video: "retain-on-failure", + trace: "on-first-retry", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + // { + // name: "firefox", + // use: { ...devices["Desktop Firefox"] }, + // }, + // + // { + // name: "webkit", + // use: { ...devices["Desktop Safari"] }, + // }, + + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], +}); From 04e824c3329969e3db83e220b67205f934450087 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Sun, 11 Jan 2026 14:00:29 +0100 Subject: [PATCH 15/23] e2e: test backup & restore functions (#341) --- .gitignore | 3 + app/client/components/restore-form.tsx | 6 +- docker-compose.yml | 3 +- ...itial-setup.spec.ts => 0001-auth.setup.ts} | 16 ++++ e2e/0002-backup-restore.spec.ts | 73 +++++++++++++++++++ e2e/helpers/db.ts | 2 +- playwright.config.ts | 10 ++- 7 files changed, 106 insertions(+), 7 deletions(-) rename e2e/{0001-initial-setup.spec.ts => 0001-auth.setup.ts} (83%) create mode 100644 e2e/0002-backup-restore.spec.ts diff --git a/.gitignore b/.gitignore index 329fb07b..d0a5ab04 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ node_modules/ /blob-report/ /playwright/.cache/ /playwright/.auth/ + +playwright/.auth +playwright/temp diff --git a/app/client/components/restore-form.tsx b/app/client/components/restore-form.tsx index 37d6d55e..e2594232 100644 --- a/app/client/components/restore-form.tsx +++ b/app/client/components/restore-form.tsx @@ -98,10 +98,8 @@ export function RestoreForm({ snapshot, repository, snapshotId, returnPath }: Re const { mutate: restoreSnapshot, isPending: isRestoring } = useMutation({ ...restoreSnapshotMutation(), - onSuccess: (data) => { - toast.success("Restore completed", { - description: `Successfully restored ${data.filesRestored} file(s). ${data.filesSkipped} file(s) skipped.`, - }); + onSuccess: () => { + toast.success("Restore completed"); void navigate(returnPath); }, onError: (error) => { diff --git a/docker-compose.yml b/docker-compose.yml index fa3d629b..424f5d3a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,4 +57,5 @@ services: - "4096:4096" volumes: - /etc/localtime:/etc/localtime:ro - - ./data:/var/lib/zerobyte + - ./playwright/data:/var/lib/zerobyte/data + - ./playwright/temp:/test-data diff --git a/e2e/0001-initial-setup.spec.ts b/e2e/0001-auth.setup.ts similarity index 83% rename from e2e/0001-initial-setup.spec.ts rename to e2e/0001-auth.setup.ts index 38251896..72fb6626 100644 --- a/e2e/0001-initial-setup.spec.ts +++ b/e2e/0001-auth.setup.ts @@ -1,6 +1,9 @@ import fs from "fs"; import { test, expect } from "@playwright/test"; import { resetDatabase } from "./helpers/db"; +import path from "node:path"; + +const authFile = path.join(process.cwd(), "./playwright/.auth/user.json"); // TODO: Run these tests with different users once multi-user support is added @@ -80,3 +83,16 @@ test("can't create another admin user after initial setup", async ({ page }) => await expect(page.getByText("Failed to create admin user")).toBeVisible(); }); + +test("can login after initial setup", async ({ page }) => { + await page.goto("/login"); + + await page.getByRole("textbox", { name: "Username" }).fill("test"); + await page.getByRole("textbox", { name: "Password" }).fill("password"); + await page.getByRole("button", { name: "Login" }).click(); + + await expect(page).toHaveURL("/volumes"); + await expect(page.getByRole("heading", { name: "No volume" })).toBeVisible(); + + await page.context().storageState({ path: authFile }); +}); diff --git a/e2e/0002-backup-restore.spec.ts b/e2e/0002-backup-restore.spec.ts new file mode 100644 index 00000000..cedcc7ea --- /dev/null +++ b/e2e/0002-backup-restore.spec.ts @@ -0,0 +1,73 @@ +import { expect, test } from "@playwright/test"; +import path from "node:path"; +import fs from "node:fs"; + +test.beforeAll(() => { + const testDataPath = path.join(process.cwd(), "playwright", "temp"); + if (fs.existsSync(testDataPath)) { + fs.rmSync(testDataPath, { recursive: true, force: true }); + } +}); + +test("can backup & restore a file", async ({ page }) => { + await page.goto("/"); + await expect(page).toHaveURL("/volumes"); + + // 0. Create a test file in /test-data + const testDataPath = path.join(process.cwd(), "playwright", "temp"); + if (!fs.existsSync(testDataPath)) { + fs.mkdirSync(testDataPath); + } + const filePath = path.join(testDataPath, "test.json"); + fs.chmodSync(testDataPath, 0o777); + fs.writeFileSync(filePath, JSON.stringify({ data: "test file" })); + + // 1. Create a local volume on /test-data + await page.getByRole("button", { name: "Create Volume" }).click(); + await page.getByRole("textbox", { name: "Name" }).fill("Test Volume"); + await page.getByRole("button", { name: "Change" }).click(); + await page.getByRole("button", { name: "test-data" }).click(); + await page.getByRole("button", { name: "Create Volume" }).click(); + await expect(page.getByText("Volume created successfully")).toBeVisible(); + + // 2. Create a local repository on the default location + await page.getByRole("link", { name: "Repositories" }).click(); + await page.getByRole("button", { name: "Create repository" }).click(); + await page.getByRole("textbox", { name: "Name" }).fill("Test Repo"); + await page.getByRole("combobox", { name: "Backend" }).click(); + await page.getByRole("option", { name: "Local" }).click(); + await page.getByRole("button", { name: "Create repository" }).click(); + await expect(page.getByText("Repository created successfully")).toBeVisible(); + + // 3. Create a backup schedule + await page.getByRole("link", { name: "Backups" }).click(); + await page.getByRole("button", { name: "Create a backup job" }).click(); + await page.getByRole("combobox").filter({ hasText: "Choose a volume to backup" }).click(); + await page.getByRole("option", { name: "test-volume" }).click(); + await page.getByRole("textbox", { name: "Backup name" }).fill("Test Backup"); + await page.getByRole("combobox").filter({ hasText: "Select a repository" }).click(); + await page.getByRole("option", { name: "Test Repo" }).click(); + await page.getByRole("combobox").filter({ hasText: "Select frequency" }).click(); + await page.getByRole("option", { name: "Daily" }).click(); + await page.getByRole("textbox", { name: "Execution time" }).fill("00:00"); + await page.getByRole("button", { name: "Create" }).click(); + await expect(page.getByText("Backup job created successfully")).toBeVisible(); + + // 4. Runs that schedule once + await page.getByRole("button", { name: "Backup now" }).click(); + await expect(page.getByText("Backup started successfully")).toBeVisible(); + await expect(page.getByText("✓ Success")).toBeVisible({ timeout: 30000 }); + + // 5. Modify the json file after the backup + fs.writeFileSync(filePath, JSON.stringify({ data: "modified file" })); + + // 6. Restores the file from backup + await page.getByRole("link", { name: "Restore" }).click(); + await expect(page).toHaveURL(/\/restore/); + await page.getByRole("button", { name: "Restore All" }).click(); + await expect(page.getByText("Restore completed")).toBeVisible({ timeout: 30000 }); + + // 7. Ensures that the file is back to its previous state + const restoredContent = fs.readFileSync(filePath, "utf8"); + expect(JSON.parse(restoredContent)).toEqual({ data: "test file" }); +}); diff --git a/e2e/helpers/db.ts b/e2e/helpers/db.ts index 3c0068ac..bec09ce1 100644 --- a/e2e/helpers/db.ts +++ b/e2e/helpers/db.ts @@ -4,7 +4,7 @@ import path from "node:path"; import { DATABASE_URL } from "~/server/core/constants"; import * as schema from "~/server/db/schema"; -const sqlite = createClient({ url: `file:${path.join(process.cwd(), "data", DATABASE_URL)}` }); +const sqlite = createClient({ url: `file:${path.join(process.cwd(), "playwright", DATABASE_URL)}` }); export const db = drizzle({ client: sqlite, schema: schema }); diff --git a/playwright.config.ts b/playwright.config.ts index 1bb90c36..7b4797d9 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -14,9 +14,17 @@ export default defineConfig({ trace: "on-first-retry", }, projects: [ + { + name: "setup", + testMatch: /.*\.setup\.ts/, + }, { name: "chromium", - use: { ...devices["Desktop Chrome"] }, + use: { + ...devices["Desktop Chrome"], + storageState: "playwright/.auth/user.json", + }, + dependencies: ["setup"], }, // { From f0871ece52a789df2c7cf6e0b96ada03dc36dd1b Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sun, 11 Jan 2026 14:01:14 +0100 Subject: [PATCH 16/23] chore: hide email in settings (not yet changeable) --- app/client/modules/settings/routes/settings.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/client/modules/settings/routes/settings.tsx b/app/client/modules/settings/routes/settings.tsx index 4bcecd6d..f06054da 100644 --- a/app/client/modules/settings/routes/settings.tsx +++ b/app/client/modules/settings/routes/settings.tsx @@ -157,10 +157,10 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
    -
    - - -
    + {/*
    */} + {/* */} + {/* */} + {/*
    */}
    From 52280be3c2bc2b2f5a2c36601bec12961aa485ca Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sun, 11 Jan 2026 14:06:46 +0100 Subject: [PATCH 17/23] chore: lowercase all emails --- app/drizzle/0032_lowercase-email.sql | 2 + app/drizzle/meta/0032_snapshot.json | 2366 ++++++++++++-------------- app/drizzle/meta/_journal.json | 472 ++--- 3 files changed, 1362 insertions(+), 1478 deletions(-) create mode 100644 app/drizzle/0032_lowercase-email.sql diff --git a/app/drizzle/0032_lowercase-email.sql b/app/drizzle/0032_lowercase-email.sql new file mode 100644 index 00000000..bfbcd029 --- /dev/null +++ b/app/drizzle/0032_lowercase-email.sql @@ -0,0 +1,2 @@ +-- Custom SQL migration file, put your code below! -- +UPDATE users_table SET email = LOWER(TRIM(email)); diff --git a/app/drizzle/meta/0032_snapshot.json b/app/drizzle/meta/0032_snapshot.json index 3722f852..665f8e27 100644 --- a/app/drizzle/meta/0032_snapshot.json +++ b/app/drizzle/meta/0032_snapshot.json @@ -1,1243 +1,1125 @@ { - "version": "6", - "dialect": "sqlite", - "id": "2d6b79a4-2d75-4240-8943-1180372847ba", - "prevId": "ae0e3a39-ef4d-4d39-bb97-d67d49193bc7", - "tables": { - "account": { - "name": "account", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "access_token": { - "name": "access_token", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "refresh_token": { - "name": "refresh_token", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "id_token": { - "name": "id_token", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "access_token_expires_at": { - "name": "access_token_expires_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "refresh_token_expires_at": { - "name": "refresh_token_expires_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "password": { - "name": "password", - "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": { - "account_userId_idx": { - "name": "account_userId_idx", - "columns": [ - "user_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "account_user_id_users_table_id_fk": { - "name": "account_user_id_users_table_id_fk", - "tableFrom": "account", - "tableTo": "users_table", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "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_warning": { - "name": "notify_on_warning", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": true - }, - "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 - }, - "short_id": { - "name": "short_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "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": "'[]'" - }, - "exclude_if_present": { - "name": "exclude_if_present", - "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 - }, - "one_file_system": { - "name": "one_file_system", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "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_short_id_unique": { - "name": "backup_schedules_table_short_id_unique", - "columns": [ - "short_id" - ], - "isUnique": true - }, - "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 - }, - "upload_limit_enabled": { - "name": "upload_limit_enabled", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "upload_limit_value": { - "name": "upload_limit_value", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "upload_limit_unit": { - "name": "upload_limit_unit", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'MB/s'" - }, - "download_limit_enabled": { - "name": "download_limit_enabled", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "download_limit_value": { - "name": "download_limit_value", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "download_limit_unit": { - "name": "download_limit_unit", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'MB/s'" - }, - "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 - } - }, - "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": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "token": { - "name": "token", - "type": "text", - "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)" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - }, - "ip_address": { - "name": "ip_address", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "user_agent": { - "name": "user_agent", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "sessions_table_token_unique": { - "name": "sessions_table_token_unique", - "columns": [ - "token" - ], - "isUnique": true - }, - "sessionsTable_userId_idx": { - "name": "sessionsTable_userId_idx", - "columns": [ - "user_id" - ], - "isUnique": false - } - }, - "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": {} - }, - "two_factor": { - "name": "two_factor", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "secret": { - "name": "secret", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "backup_codes": { - "name": "backup_codes", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "twoFactor_secret_idx": { - "name": "twoFactor_secret_idx", - "columns": [ - "secret" - ], - "isUnique": false - }, - "twoFactor_userId_idx": { - "name": "twoFactor_userId_idx", - "columns": [ - "user_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "two_factor_user_id_users_table_id_fk": { - "name": "two_factor_user_id_users_table_id_fk", - "tableFrom": "two_factor", - "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": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "username": { - "name": "username", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password_hash": { - "name": "password_hash", - "type": "text", - "primaryKey": false, - "notNull": false, - "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)" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "email_verified": { - "name": "email_verified", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "display_username": { - "name": "display_username", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "two_factor_enabled": { - "name": "two_factor_enabled", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - } - }, - "indexes": { - "users_table_username_unique": { - "name": "users_table_username_unique", - "columns": [ - "username" - ], - "isUnique": true - }, - "users_table_email_unique": { - "name": "users_table_email_unique", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "verification": { - "name": "verification", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "value": { - "name": "value", - "type": "text", - "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)" - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "(unixepoch() * 1000)" - } - }, - "indexes": { - "verification_identifier_idx": { - "name": "verification_identifier_idx", - "columns": [ - "identifier" - ], - "isUnique": false - } - }, - "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 + "id": "ad86ee2a-231e-45d1-9910-e4b7cac6a8af", + "prevId": "ae0e3a39-ef4d-4d39-bb97-d67d49193bc7", + "version": "6", + "dialect": "sqlite", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "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": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_users_table_id_fk": { + "name": "account_user_id_users_table_id_fk", + "tableFrom": "account", + "columnsFrom": ["user_id"], + "tableTo": "users_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "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", + "columnsFrom": ["schedule_id"], + "tableTo": "backup_schedules_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": ["repository_id"], + "tableTo": "repositories_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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_warning": { + "name": "notify_on_warning", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "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", + "columnsFrom": ["schedule_id"], + "tableTo": "backup_schedules_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "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", + "columnsFrom": ["destination_id"], + "tableTo": "notification_destinations_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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 + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "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": "'[]'" + }, + "exclude_if_present": { + "name": "exclude_if_present", + "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 + }, + "one_file_system": { + "name": "one_file_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "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_short_id_unique": { + "name": "backup_schedules_table_short_id_unique", + "columns": ["short_id"], + "isUnique": true + }, + "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", + "columnsFrom": ["volume_id"], + "tableTo": "volumes_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + }, + "backup_schedules_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedules_table", + "columnsFrom": ["repository_id"], + "tableTo": "repositories_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "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 + } + }, + "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": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "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)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_table_token_unique": { + "name": "sessions_table_token_unique", + "columns": ["token"], + "isUnique": true + }, + "sessionsTable_userId_idx": { + "name": "sessionsTable_userId_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_table_user_id_users_table_id_fk": { + "name": "sessions_table_user_id_users_table_id_fk", + "tableFrom": "sessions_table", + "columnsFrom": ["user_id"], + "tableTo": "users_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "two_factor": { + "name": "two_factor", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "twoFactor_secret_idx": { + "name": "twoFactor_secret_idx", + "columns": ["secret"], + "isUnique": false + }, + "twoFactor_userId_idx": { + "name": "twoFactor_userId_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "two_factor_user_id_users_table_id_fk": { + "name": "two_factor_user_id_users_table_id_fk", + "tableFrom": "two_factor", + "columnsFrom": ["user_id"], + "tableTo": "users_table", + "columnsTo": ["id"], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "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)" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": ["username"], + "isUnique": true + }, + "users_table_email_unique": { + "name": "users_table_email_unique", + "columns": ["email"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "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)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": ["identifier"], + "isUnique": false + } + }, + "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": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/app/drizzle/meta/_journal.json b/app/drizzle/meta/_journal.json index a7ced5d0..8add8b31 100644 --- a/app/drizzle/meta/_journal.json +++ b/app/drizzle/meta/_journal.json @@ -1,237 +1,237 @@ { - "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": 1764794371040, - "tag": "0018_breezy_invaders", - "breakpoints": true - }, - { - "idx": 19, - "version": "6", - "when": 1764839917446, - "tag": "0019_secret_nomad", - "breakpoints": true - }, - { - "idx": 20, - "version": "6", - "when": 1764847918249, - "tag": "0020_even_dexter_bennett", - "breakpoints": true - }, - { - "idx": 21, - "version": "6", - "when": 1765307881092, - "tag": "0021_steady_viper", - "breakpoints": true - }, - { - "idx": 22, - "version": "6", - "when": 1765794552191, - "tag": "0022_woozy_shen", - "breakpoints": true - }, - { - "idx": 23, - "version": "6", - "when": 1766320570509, - "tag": "0023_special_thor", - "breakpoints": true - }, - { - "idx": 24, - "version": "6", - "when": 1766325504548, - "tag": "0024_schedules-one-fs", - "breakpoints": true - }, - { - "idx": 25, - "version": "6", - "when": 1766431021321, - "tag": "0025_remarkable_pete_wisdom", - "breakpoints": true - }, - { - "idx": 26, - "version": "6", - "when": 1766765013108, - "tag": "0026_migrate-local-repo-paths", - "breakpoints": true - }, - { - "idx": 27, - "version": "6", - "when": 1766778073418, - "tag": "0027_careful_cammi", - "breakpoints": true - }, - { - "idx": 28, - "version": "6", - "when": 1766778162985, - "tag": "0028_third_amazoness", - "breakpoints": true - }, - { - "idx": 29, - "version": "6", - "when": 1767819883495, - "tag": "0029_boring_luke_cage", - "breakpoints": true - }, - { - "idx": 30, - "version": "6", - "when": 1767821088612, - "tag": "0030_lower-trim-username", - "breakpoints": true - }, - { - "idx": 31, - "version": "6", - "when": 1767863951955, - "tag": "0031_graceful_squadron_supreme", - "breakpoints": true - }, - { - "idx": 32, - "version": "6", - "when": 1768040838612, - "tag": "0032_outstanding_ultron", - "breakpoints": true - } - ] -} \ No newline at end of file + "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": 1764794371040, + "tag": "0018_breezy_invaders", + "breakpoints": true + }, + { + "idx": 19, + "version": "6", + "when": 1764839917446, + "tag": "0019_secret_nomad", + "breakpoints": true + }, + { + "idx": 20, + "version": "6", + "when": 1764847918249, + "tag": "0020_even_dexter_bennett", + "breakpoints": true + }, + { + "idx": 21, + "version": "6", + "when": 1765307881092, + "tag": "0021_steady_viper", + "breakpoints": true + }, + { + "idx": 22, + "version": "6", + "when": 1765794552191, + "tag": "0022_woozy_shen", + "breakpoints": true + }, + { + "idx": 23, + "version": "6", + "when": 1766320570509, + "tag": "0023_special_thor", + "breakpoints": true + }, + { + "idx": 24, + "version": "6", + "when": 1766325504548, + "tag": "0024_schedules-one-fs", + "breakpoints": true + }, + { + "idx": 25, + "version": "6", + "when": 1766431021321, + "tag": "0025_remarkable_pete_wisdom", + "breakpoints": true + }, + { + "idx": 26, + "version": "6", + "when": 1766765013108, + "tag": "0026_migrate-local-repo-paths", + "breakpoints": true + }, + { + "idx": 27, + "version": "6", + "when": 1766778073418, + "tag": "0027_careful_cammi", + "breakpoints": true + }, + { + "idx": 28, + "version": "6", + "when": 1766778162985, + "tag": "0028_third_amazoness", + "breakpoints": true + }, + { + "idx": 29, + "version": "6", + "when": 1767819883495, + "tag": "0029_boring_luke_cage", + "breakpoints": true + }, + { + "idx": 30, + "version": "6", + "when": 1767821088612, + "tag": "0030_lower-trim-username", + "breakpoints": true + }, + { + "idx": 31, + "version": "6", + "when": 1767863951955, + "tag": "0031_graceful_squadron_supreme", + "breakpoints": true + }, + { + "idx": 32, + "version": "6", + "when": 1768136755563, + "tag": "0032_lowercase-email", + "breakpoints": true + } + ] +} From 9754e366c1b4e72db4dff7a6c40be5379cb87210 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Sun, 11 Jan 2026 14:53:57 +0100 Subject: [PATCH 18/23] feat: add CLI command to change username (#342) * feat: add CLI command to change username * ci: fix wrong folder chmod --- .github/workflows/e2e.yml | 4 +- app/server/cli/commands/change-username.ts | 86 ++++++++++++++++++++++ app/server/cli/index.ts | 2 + 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 app/server/cli/commands/change-username.ts diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 747d5888..9bb4f948 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -56,8 +56,8 @@ jobs: timeout 30s bash -c 'until curl -f http://localhost:4096/healthcheck; do echo "Waiting for server..." && sleep 2; done' continue-on-error: false - - name: Make data directory writable - run: sudo chmod -R 777 data + - name: Make playwright directory writable + run: sudo chmod -R 777 playwright - name: Run Playwright tests run: bun run test:e2e diff --git a/app/server/cli/commands/change-username.ts b/app/server/cli/commands/change-username.ts new file mode 100644 index 00000000..7e13bb61 --- /dev/null +++ b/app/server/cli/commands/change-username.ts @@ -0,0 +1,86 @@ +import { input, select } from "@inquirer/prompts"; +import { Command } from "commander"; +import { eq } from "drizzle-orm"; +import { toMessage } from "~/server/utils/errors"; +import { db } from "../../db/db"; +import { sessionsTable, usersTable } from "../../db/schema"; + +const listUsers = () => { + return db.select({ id: usersTable.id, username: usersTable.username }).from(usersTable); +}; + +const changeUsername = async (oldUsername: string, newUsername: string) => { + const [user] = await db.select().from(usersTable).where(eq(usersTable.username, oldUsername)); + + if (!user) { + throw new Error(`User "${oldUsername}" not found`); + } + + const normalizedUsername = newUsername.toLowerCase().trim(); + + const [existingUser] = await db.select().from(usersTable).where(eq(usersTable.username, normalizedUsername)); + if (existingUser) { + throw new Error(`Username "${newUsername}" is already taken`); + } + + const usernameRegex = /^[a-z0-9_]{3,30}$/; + if (!usernameRegex.test(normalizedUsername)) { + throw new Error( + `Invalid username "${newUsername}". Usernames must be 3-30 characters long and can only contain lowercase letters, numbers, and underscores.`, + ); + } + + await db.transaction(async (tx) => { + await tx.update(usersTable).set({ username: normalizedUsername }).where(eq(usersTable.id, user.id)); + await tx.delete(sessionsTable).where(eq(sessionsTable.userId, user.id)); + }); +}; + +export const changeUsernameCommand = new Command("change-username") + .description("Change username for a user") + .option("-u, --username ", "Current username of the account") + .option("-n, --new-username ", "New username for the account") + .action(async (options) => { + console.info("\n👤 Zerobyte Change Username\n"); + + let username = options.username; + let newUsername = options.newUsername; + + try { + if (!username) { + const users = await listUsers(); + + if (users.length === 0) { + console.error("No users found in the database."); + return; + } + + username = await select({ + message: "Select a user to change username for:", + choices: users.map((u) => ({ + name: u.username, + value: u.username, + })), + }); + } + + if (!newUsername) { + newUsername = await input({ + message: "Enter the new username:", + validate: (val) => { + const usernameRegex = /^[a-z0-9_]{3,30}$/; + return usernameRegex.test(val) + ? true + : "Username must be 3-30 characters and contain only lowercase letters, numbers, or underscores"; + }, + }); + newUsername = newUsername.toLowerCase().trim(); + } + + await changeUsername(username, newUsername); + console.info(`\n✅ Username for "${username}" has been changed to "${newUsername}" successfully.`); + } catch (error) { + console.error(`\n❌ Error: ${toMessage(error)}`); + process.exit(1); + } + }); diff --git a/app/server/cli/index.ts b/app/server/cli/index.ts index 74a6d074..fe54cd99 100644 --- a/app/server/cli/index.ts +++ b/app/server/cli/index.ts @@ -1,4 +1,5 @@ import { Command } from "commander"; +import { changeUsernameCommand } from "./commands/change-username"; import { disable2FACommand } from "./commands/disable-2fa"; import { resetPasswordCommand } from "./commands/reset-password"; @@ -7,6 +8,7 @@ const program = new Command(); program.name("zerobyte").description("Zerobyte CLI - Backup automation tool built on top of Restic").version("1.0.0"); program.addCommand(resetPasswordCommand); program.addCommand(disable2FACommand); +program.addCommand(changeUsernameCommand); export async function runCLI(argv: string[]): Promise { const args = argv.slice(2); From 583393eb47298477c91af027ea65bb654cccdea6 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:02:36 +0100 Subject: [PATCH 19/23] test(e2e): add container logs to report (#343) --- .github/workflows/e2e.yml | 25 +++++++++++++++++++------ e2e/0001-auth.setup.ts | 4 ++-- e2e/0002-backup-restore.spec.ts | 10 +++++----- playwright.config.ts | 2 +- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 9bb4f948..93e32ccb 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -44,6 +44,9 @@ jobs: - name: Prepare environment run: | mkdir -p data + mkdir -p playwright/temp + mkdir -p playwright/data + chmod -R 777 playwright/temp touch .env.local echo "SERVER_IP=localhost" >> .env.local echo "DATABASE_URL=./data/zerobyte.db" >> .env.local @@ -54,21 +57,31 @@ jobs: - name: Wait for zerobyte to be ready run: | timeout 30s bash -c 'until curl -f http://localhost:4096/healthcheck; do echo "Waiting for server..." && sleep 2; done' - continue-on-error: false - name: Make playwright directory writable - run: sudo chmod -R 777 playwright + run: sudo chmod 777 playwright/data/zerobyte.db - name: Run Playwright tests run: bun run test:e2e - - name: Stop Docker Compose + - name: Dump container logs in playwright-report folder if: always() - run: docker compose down + run: | + tree playwright + docker logs zerobyte > playwright-report/container-logs.txt || true - - uses: actions/upload-artifact@v4 + - name: Debug - print content of /test-data in container + if: failure() + run: | + docker exec zerobyte /bin/ash -c "ls -la /test-data" || true + + - uses: actions/upload-artifact@v5 if: always() with: name: playwright-report path: playwright-report/ - retention-days: 30 + retention-days: 5 + + - name: Stop Docker Compose + if: always() + run: docker compose down diff --git a/e2e/0001-auth.setup.ts b/e2e/0001-auth.setup.ts index 72fb6626..09088857 100644 --- a/e2e/0001-auth.setup.ts +++ b/e2e/0001-auth.setup.ts @@ -61,9 +61,9 @@ test("user can download recovery key", async ({ page }) => { const download = await downloadPromise; expect(download.suggestedFilename()).toBe("restic.pass"); - await download.saveAs("./data/restic.pass"); + await download.saveAs("./playwright/restic.pass"); - const fileContent = await fs.promises.readFile("./data/restic.pass", "utf8"); + const fileContent = await fs.promises.readFile("./playwright/restic.pass", "utf8"); expect(fileContent).toHaveLength(64); }); diff --git a/e2e/0002-backup-restore.spec.ts b/e2e/0002-backup-restore.spec.ts index cedcc7ea..41adf519 100644 --- a/e2e/0002-backup-restore.spec.ts +++ b/e2e/0002-backup-restore.spec.ts @@ -5,7 +5,11 @@ import fs from "node:fs"; test.beforeAll(() => { const testDataPath = path.join(process.cwd(), "playwright", "temp"); if (fs.existsSync(testDataPath)) { - fs.rmSync(testDataPath, { recursive: true, force: true }); + for (const file of fs.readdirSync(testDataPath)) { + fs.rmSync(path.join(testDataPath, file), { recursive: true, force: true }); + } + } else { + fs.mkdirSync(testDataPath, { recursive: true }); } }); @@ -15,11 +19,7 @@ test("can backup & restore a file", async ({ page }) => { // 0. Create a test file in /test-data const testDataPath = path.join(process.cwd(), "playwright", "temp"); - if (!fs.existsSync(testDataPath)) { - fs.mkdirSync(testDataPath); - } const filePath = path.join(testDataPath, "test.json"); - fs.chmodSync(testDataPath, 0o777); fs.writeFileSync(filePath, JSON.stringify({ data: "test file" })); // 1. Create a local volume on /test-data diff --git a/playwright.config.ts b/playwright.config.ts index 7b4797d9..af683998 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ testDir: "./e2e", fullyParallel: true, forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, + retries: 0, workers: process.env.CI ? 1 : undefined, reporter: "html", use: { From 02fe55b53394b3de4ee904c3085e3537a11b8b82 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sun, 11 Jan 2026 16:24:28 +0100 Subject: [PATCH 20/23] fix: properly catch before fallback mount --- app/client/api-client/client/client.gen.ts | 72 ++---- app/client/api-client/index.ts | 217 +++++++++++++++- app/client/api-client/types.gen.ts | 241 ++++-------------- app/schemas/volumes.ts | 2 +- .../modules/backends/nfs/nfs-backend.ts | 11 +- .../modules/backends/smb/smb-backend.ts | 24 +- .../modules/backends/utils/backend-utils.ts | 4 +- .../modules/backends/webdav/webdav-backend.ts | 10 +- 8 files changed, 325 insertions(+), 256 deletions(-) diff --git a/app/client/api-client/client/client.gen.ts b/app/client/api-client/client/client.gen.ts index a8a478dc..cc9b1f4e 100644 --- a/app/client/api-client/client/client.gen.ts +++ b/app/client/api-client/client/client.gen.ts @@ -3,12 +3,7 @@ import { createSseClient } from "../core/serverSentEvents.gen"; import type { HttpMethod } from "../core/types.gen"; import { getValidRequestBody } from "../core/utils.gen"; -import type { - Client, - Config, - RequestOptions, - ResolvedRequestOptions, -} from "./types.gen"; +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen"; import { buildUrl, createConfig, @@ -34,12 +29,7 @@ export const createClient = (config: Config = {}): Client => { return getConfig(); }; - const interceptors = createInterceptors< - Request, - Response, - unknown, - ResolvedRequestOptions - >(); + const interceptors = createInterceptors(); const beforeRequest = async (options: RequestOptions) => { const opts = { @@ -105,12 +95,7 @@ export const createClient = (config: Config = {}): Client => { for (const fn of interceptors.error.fns) { if (fn) { - finalError = (await fn( - error, - undefined as any, - request, - opts, - )) as unknown; + finalError = (await fn(error, undefined as any, request, opts)) as unknown; } } @@ -143,14 +128,9 @@ export const createClient = (config: Config = {}): Client => { if (response.ok) { const parseAs = - (opts.parseAs === "auto" - ? getParseAs(response.headers.get("Content-Type")) - : opts.parseAs) ?? "json"; + (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json"; - if ( - response.status === 204 || - response.headers.get("Content-Length") === "0" - ) { + if (response.status === 204 || response.headers.get("Content-Length") === "0") { let emptyData: any; switch (parseAs) { case "arrayBuffer": @@ -246,30 +226,28 @@ export const createClient = (config: Config = {}): Client => { }; }; - const makeMethodFn = - (method: Uppercase) => (options: RequestOptions) => - request({ ...options, method }); + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => request({ ...options, method }); - const makeSseFn = - (method: Uppercase) => async (options: RequestOptions) => { - const { opts, url } = await beforeRequest(options); - return createSseClient({ - ...opts, - body: opts.body as BodyInit | null | undefined, - headers: opts.headers as unknown as Record, - method, - onRequest: async (url, init) => { - let request = new Request(url, init); - for (const fn of interceptors.request.fns) { - if (fn) { - request = await fn(request, opts); - } + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); } - return request; - }, - url, - }); - }; + } + return request; + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }); + }; return { buildUrl, diff --git a/app/client/api-client/index.ts b/app/client/api-client/index.ts index 7a716924..bee3869e 100644 --- a/app/client/api-client/index.ts +++ b/app/client/api-client/index.ts @@ -1,4 +1,217 @@ // This file is auto-generated by @hey-api/openapi-ts -export type * from "./types.gen"; -export * from "./sdk.gen"; +export { + browseFilesystem, + createBackupSchedule, + createNotificationDestination, + createRepository, + createVolume, + deleteBackupSchedule, + deleteNotificationDestination, + deleteRepository, + deleteSnapshot, + deleteSnapshots, + deleteVolume, + doctorRepository, + downloadResticPassword, + getBackupSchedule, + getBackupScheduleForVolume, + getMirrorCompatibility, + getNotificationDestination, + getRepository, + getScheduleMirrors, + getScheduleNotifications, + getSnapshotDetails, + getStatus, + getSystemInfo, + getUpdates, + getVolume, + healthCheckVolume, + listBackupSchedules, + listFiles, + listNotificationDestinations, + listRcloneRemotes, + listRepositories, + listSnapshotFiles, + listSnapshots, + listVolumes, + mountVolume, + type Options, + reorderBackupSchedules, + restoreSnapshot, + runBackupNow, + runForget, + stopBackup, + tagSnapshots, + testConnection, + testNotificationDestination, + unmountVolume, + updateBackupSchedule, + updateNotificationDestination, + updateRepository, + updateScheduleMirrors, + updateScheduleNotifications, + updateVolume, +} from "./sdk.gen"; +export type { + BrowseFilesystemData, + BrowseFilesystemResponse, + BrowseFilesystemResponses, + ClientOptions, + CreateBackupScheduleData, + CreateBackupScheduleResponse, + CreateBackupScheduleResponses, + CreateNotificationDestinationData, + CreateNotificationDestinationResponse, + CreateNotificationDestinationResponses, + CreateRepositoryData, + CreateRepositoryResponse, + CreateRepositoryResponses, + CreateVolumeData, + CreateVolumeResponse, + CreateVolumeResponses, + DeleteBackupScheduleData, + DeleteBackupScheduleResponse, + DeleteBackupScheduleResponses, + DeleteNotificationDestinationData, + DeleteNotificationDestinationErrors, + DeleteNotificationDestinationResponse, + DeleteNotificationDestinationResponses, + DeleteRepositoryData, + DeleteRepositoryResponse, + DeleteRepositoryResponses, + DeleteSnapshotData, + DeleteSnapshotResponse, + DeleteSnapshotResponses, + DeleteSnapshotsData, + DeleteSnapshotsResponse, + DeleteSnapshotsResponses, + DeleteVolumeData, + DeleteVolumeResponse, + DeleteVolumeResponses, + DoctorRepositoryData, + DoctorRepositoryResponse, + DoctorRepositoryResponses, + DownloadResticPasswordData, + DownloadResticPasswordResponse, + DownloadResticPasswordResponses, + GetBackupScheduleData, + GetBackupScheduleForVolumeData, + GetBackupScheduleForVolumeResponse, + GetBackupScheduleForVolumeResponses, + GetBackupScheduleResponse, + GetBackupScheduleResponses, + GetMirrorCompatibilityData, + GetMirrorCompatibilityResponse, + GetMirrorCompatibilityResponses, + GetNotificationDestinationData, + GetNotificationDestinationErrors, + GetNotificationDestinationResponse, + GetNotificationDestinationResponses, + GetRepositoryData, + GetRepositoryResponse, + GetRepositoryResponses, + GetScheduleMirrorsData, + GetScheduleMirrorsResponse, + GetScheduleMirrorsResponses, + GetScheduleNotificationsData, + GetScheduleNotificationsResponse, + GetScheduleNotificationsResponses, + GetSnapshotDetailsData, + GetSnapshotDetailsResponse, + GetSnapshotDetailsResponses, + GetStatusData, + GetStatusResponse, + GetStatusResponses, + GetSystemInfoData, + GetSystemInfoResponse, + GetSystemInfoResponses, + GetUpdatesData, + GetUpdatesResponse, + GetUpdatesResponses, + GetVolumeData, + GetVolumeErrors, + GetVolumeResponse, + GetVolumeResponses, + HealthCheckVolumeData, + HealthCheckVolumeErrors, + HealthCheckVolumeResponse, + HealthCheckVolumeResponses, + ListBackupSchedulesData, + ListBackupSchedulesResponse, + ListBackupSchedulesResponses, + ListFilesData, + ListFilesResponse, + ListFilesResponses, + ListNotificationDestinationsData, + ListNotificationDestinationsResponse, + ListNotificationDestinationsResponses, + ListRcloneRemotesData, + ListRcloneRemotesResponse, + ListRcloneRemotesResponses, + ListRepositoriesData, + ListRepositoriesResponse, + ListRepositoriesResponses, + ListSnapshotFilesData, + ListSnapshotFilesResponse, + ListSnapshotFilesResponses, + ListSnapshotsData, + ListSnapshotsResponse, + ListSnapshotsResponses, + ListVolumesData, + ListVolumesResponse, + ListVolumesResponses, + MountVolumeData, + MountVolumeResponse, + MountVolumeResponses, + ReorderBackupSchedulesData, + ReorderBackupSchedulesResponse, + ReorderBackupSchedulesResponses, + RestoreSnapshotData, + RestoreSnapshotResponse, + RestoreSnapshotResponses, + RunBackupNowData, + RunBackupNowResponse, + RunBackupNowResponses, + RunForgetData, + RunForgetResponse, + RunForgetResponses, + StopBackupData, + StopBackupErrors, + StopBackupResponse, + StopBackupResponses, + TagSnapshotsData, + TagSnapshotsResponse, + TagSnapshotsResponses, + TestConnectionData, + TestConnectionResponse, + TestConnectionResponses, + TestNotificationDestinationData, + TestNotificationDestinationErrors, + TestNotificationDestinationResponse, + TestNotificationDestinationResponses, + UnmountVolumeData, + UnmountVolumeResponse, + UnmountVolumeResponses, + UpdateBackupScheduleData, + UpdateBackupScheduleResponse, + UpdateBackupScheduleResponses, + UpdateNotificationDestinationData, + UpdateNotificationDestinationErrors, + UpdateNotificationDestinationResponse, + UpdateNotificationDestinationResponses, + UpdateRepositoryData, + UpdateRepositoryErrors, + UpdateRepositoryResponse, + UpdateRepositoryResponses, + UpdateScheduleMirrorsData, + UpdateScheduleMirrorsResponse, + UpdateScheduleMirrorsResponses, + UpdateScheduleNotificationsData, + UpdateScheduleNotificationsResponse, + UpdateScheduleNotificationsResponses, + UpdateVolumeData, + UpdateVolumeErrors, + UpdateVolumeResponse, + UpdateVolumeResponses, +} from "./types.gen"; diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 7230304f..3cab252d 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -73,7 +73,7 @@ export type ListVolumesResponses = { server: string; share: string; username: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0"; + vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; port?: number; domain?: string; readOnly?: boolean; @@ -100,8 +100,7 @@ export type ListVolumesResponses = { }>; }; -export type ListVolumesResponse = - ListVolumesResponses[keyof ListVolumesResponses]; +export type ListVolumesResponse = ListVolumesResponses[keyof ListVolumesResponses]; export type CreateVolumeData = { body?: { @@ -143,7 +142,7 @@ export type CreateVolumeData = { server: string; share: string; username: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0"; + vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; port?: number; domain?: string; readOnly?: boolean; @@ -209,7 +208,7 @@ export type CreateVolumeResponses = { server: string; share: string; username: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0"; + vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; port?: number; domain?: string; readOnly?: boolean; @@ -236,8 +235,7 @@ export type CreateVolumeResponses = { }; }; -export type CreateVolumeResponse = - CreateVolumeResponses[keyof CreateVolumeResponses]; +export type CreateVolumeResponse = CreateVolumeResponses[keyof CreateVolumeResponses]; export type TestConnectionData = { body?: { @@ -279,7 +277,7 @@ export type TestConnectionData = { server: string; share: string; username: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0"; + vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; port?: number; domain?: string; readOnly?: boolean; @@ -310,8 +308,7 @@ export type TestConnectionResponses = { }; }; -export type TestConnectionResponse = - TestConnectionResponses[keyof TestConnectionResponses]; +export type TestConnectionResponse = TestConnectionResponses[keyof TestConnectionResponses]; export type DeleteVolumeData = { body?: never; @@ -331,8 +328,7 @@ export type DeleteVolumeResponses = { }; }; -export type DeleteVolumeResponse = - DeleteVolumeResponses[keyof DeleteVolumeResponses]; +export type DeleteVolumeResponse = DeleteVolumeResponses[keyof DeleteVolumeResponses]; export type GetVolumeData = { body?: never; @@ -400,7 +396,7 @@ export type GetVolumeResponses = { server: string; share: string; username: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0"; + vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; port?: number; domain?: string; readOnly?: boolean; @@ -471,7 +467,7 @@ export type UpdateVolumeData = { server: string; share: string; username: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0"; + vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; port?: number; domain?: string; readOnly?: boolean; @@ -546,7 +542,7 @@ export type UpdateVolumeResponses = { server: string; share: string; username: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0"; + vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; port?: number; domain?: string; readOnly?: boolean; @@ -573,8 +569,7 @@ export type UpdateVolumeResponses = { }; }; -export type UpdateVolumeResponse = - UpdateVolumeResponses[keyof UpdateVolumeResponses]; +export type UpdateVolumeResponse = UpdateVolumeResponses[keyof UpdateVolumeResponses]; export type MountVolumeData = { body?: never; @@ -595,8 +590,7 @@ export type MountVolumeResponses = { }; }; -export type MountVolumeResponse = - MountVolumeResponses[keyof MountVolumeResponses]; +export type MountVolumeResponse = MountVolumeResponses[keyof MountVolumeResponses]; export type UnmountVolumeData = { body?: never; @@ -617,8 +611,7 @@ export type UnmountVolumeResponses = { }; }; -export type UnmountVolumeResponse = - UnmountVolumeResponses[keyof UnmountVolumeResponses]; +export type UnmountVolumeResponse = UnmountVolumeResponses[keyof UnmountVolumeResponses]; export type HealthCheckVolumeData = { body?: never; @@ -646,8 +639,7 @@ export type HealthCheckVolumeResponses = { }; }; -export type HealthCheckVolumeResponse = - HealthCheckVolumeResponses[keyof HealthCheckVolumeResponses]; +export type HealthCheckVolumeResponse = HealthCheckVolumeResponses[keyof HealthCheckVolumeResponses]; export type ListFilesData = { body?: never; @@ -709,8 +701,7 @@ export type BrowseFilesystemResponses = { }; }; -export type BrowseFilesystemResponse = - BrowseFilesystemResponses[keyof BrowseFilesystemResponses]; +export type BrowseFilesystemResponse = BrowseFilesystemResponses[keyof BrowseFilesystemResponses]; export type ListRepositoriesData = { body?: never; @@ -824,8 +815,7 @@ export type ListRepositoriesResponses = { }>; }; -export type ListRepositoriesResponse = - ListRepositoriesResponses[keyof ListRepositoriesResponses]; +export type ListRepositoriesResponse = ListRepositoriesResponses[keyof ListRepositoriesResponses]; export type CreateRepositoryData = { body?: { @@ -938,8 +928,7 @@ export type CreateRepositoryResponses = { }; }; -export type CreateRepositoryResponse = - CreateRepositoryResponses[keyof CreateRepositoryResponses]; +export type CreateRepositoryResponse = CreateRepositoryResponses[keyof CreateRepositoryResponses]; export type ListRcloneRemotesData = { body?: never; @@ -958,8 +947,7 @@ export type ListRcloneRemotesResponses = { }>; }; -export type ListRcloneRemotesResponse = - ListRcloneRemotesResponses[keyof ListRcloneRemotesResponses]; +export type ListRcloneRemotesResponse = ListRcloneRemotesResponses[keyof ListRcloneRemotesResponses]; export type DeleteRepositoryData = { body?: never; @@ -979,8 +967,7 @@ export type DeleteRepositoryResponses = { }; }; -export type DeleteRepositoryResponse = - DeleteRepositoryResponses[keyof DeleteRepositoryResponses]; +export type DeleteRepositoryResponse = DeleteRepositoryResponses[keyof DeleteRepositoryResponses]; export type GetRepositoryData = { body?: never; @@ -1096,8 +1083,7 @@ export type GetRepositoryResponses = { }; }; -export type GetRepositoryResponse = - GetRepositoryResponses[keyof GetRepositoryResponses]; +export type GetRepositoryResponse = GetRepositoryResponses[keyof GetRepositoryResponses]; export type UpdateRepositoryData = { body?: { @@ -1227,8 +1213,7 @@ export type UpdateRepositoryResponses = { }; }; -export type UpdateRepositoryResponse = - UpdateRepositoryResponses[keyof UpdateRepositoryResponses]; +export type UpdateRepositoryResponse = UpdateRepositoryResponses[keyof UpdateRepositoryResponses]; export type DeleteSnapshotsData = { body?: { @@ -1250,8 +1235,7 @@ export type DeleteSnapshotsResponses = { }; }; -export type DeleteSnapshotsResponse = - DeleteSnapshotsResponses[keyof DeleteSnapshotsResponses]; +export type DeleteSnapshotsResponse = DeleteSnapshotsResponses[keyof DeleteSnapshotsResponses]; export type ListSnapshotsData = { body?: never; @@ -1278,8 +1262,7 @@ export type ListSnapshotsResponses = { }>; }; -export type ListSnapshotsResponse = - ListSnapshotsResponses[keyof ListSnapshotsResponses]; +export type ListSnapshotsResponse = ListSnapshotsResponses[keyof ListSnapshotsResponses]; export type DeleteSnapshotData = { body?: never; @@ -1300,8 +1283,7 @@ export type DeleteSnapshotResponses = { }; }; -export type DeleteSnapshotResponse = - DeleteSnapshotResponses[keyof DeleteSnapshotResponses]; +export type DeleteSnapshotResponse = DeleteSnapshotResponses[keyof DeleteSnapshotResponses]; export type GetSnapshotDetailsData = { body?: never; @@ -1327,8 +1309,7 @@ export type GetSnapshotDetailsResponses = { }; }; -export type GetSnapshotDetailsResponse = - GetSnapshotDetailsResponses[keyof GetSnapshotDetailsResponses]; +export type GetSnapshotDetailsResponse = GetSnapshotDetailsResponses[keyof GetSnapshotDetailsResponses]; export type ListSnapshotFilesData = { body?: never; @@ -1369,8 +1350,7 @@ export type ListSnapshotFilesResponses = { }; }; -export type ListSnapshotFilesResponse = - ListSnapshotFilesResponses[keyof ListSnapshotFilesResponses]; +export type ListSnapshotFilesResponse = ListSnapshotFilesResponses[keyof ListSnapshotFilesResponses]; export type RestoreSnapshotData = { body?: { @@ -1401,8 +1381,7 @@ export type RestoreSnapshotResponses = { }; }; -export type RestoreSnapshotResponse = - RestoreSnapshotResponses[keyof RestoreSnapshotResponses]; +export type RestoreSnapshotResponse = RestoreSnapshotResponses[keyof RestoreSnapshotResponses]; export type DoctorRepositoryData = { body?: never; @@ -1428,8 +1407,7 @@ export type DoctorRepositoryResponses = { }; }; -export type DoctorRepositoryResponse = - DoctorRepositoryResponses[keyof DoctorRepositoryResponses]; +export type DoctorRepositoryResponse = DoctorRepositoryResponses[keyof DoctorRepositoryResponses]; export type TagSnapshotsData = { body?: { @@ -1454,8 +1432,7 @@ export type TagSnapshotsResponses = { }; }; -export type TagSnapshotsResponse = - TagSnapshotsResponses[keyof TagSnapshotsResponses]; +export type TagSnapshotsResponse = TagSnapshotsResponses[keyof TagSnapshotsResponses]; export type ListBackupSchedulesData = { body?: never; @@ -1578,15 +1555,7 @@ export type ListBackupSchedulesResponses = { name: string; shortId: string; status: "error" | "healthy" | "unknown" | null; - type: - | "azure" - | "gcs" - | "local" - | "r2" - | "rclone" - | "rest" - | "s3" - | "sftp"; + type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; updatedAt: number; }; repositoryId: string; @@ -1641,7 +1610,7 @@ export type ListBackupSchedulesResponses = { server: string; share: string; username: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0"; + vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; port?: number; domain?: string; readOnly?: boolean; @@ -1670,8 +1639,7 @@ export type ListBackupSchedulesResponses = { }>; }; -export type ListBackupSchedulesResponse = - ListBackupSchedulesResponses[keyof ListBackupSchedulesResponses]; +export type ListBackupSchedulesResponse = ListBackupSchedulesResponses[keyof ListBackupSchedulesResponses]; export type CreateBackupScheduleData = { body?: { @@ -1734,8 +1702,7 @@ export type CreateBackupScheduleResponses = { }; }; -export type CreateBackupScheduleResponse = - CreateBackupScheduleResponses[keyof CreateBackupScheduleResponses]; +export type CreateBackupScheduleResponse = CreateBackupScheduleResponses[keyof CreateBackupScheduleResponses]; export type DeleteBackupScheduleData = { body?: never; @@ -1755,8 +1722,7 @@ export type DeleteBackupScheduleResponses = { }; }; -export type DeleteBackupScheduleResponse = - DeleteBackupScheduleResponses[keyof DeleteBackupScheduleResponses]; +export type DeleteBackupScheduleResponse = DeleteBackupScheduleResponses[keyof DeleteBackupScheduleResponses]; export type GetBackupScheduleData = { body?: never; @@ -1881,15 +1847,7 @@ export type GetBackupScheduleResponses = { name: string; shortId: string; status: "error" | "healthy" | "unknown" | null; - type: - | "azure" - | "gcs" - | "local" - | "r2" - | "rclone" - | "rest" - | "s3" - | "sftp"; + type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; updatedAt: number; }; repositoryId: string; @@ -1944,7 +1902,7 @@ export type GetBackupScheduleResponses = { server: string; share: string; username: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0"; + vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; port?: number; domain?: string; readOnly?: boolean; @@ -1973,8 +1931,7 @@ export type GetBackupScheduleResponses = { }; }; -export type GetBackupScheduleResponse = - GetBackupScheduleResponses[keyof GetBackupScheduleResponses]; +export type GetBackupScheduleResponse = GetBackupScheduleResponses[keyof GetBackupScheduleResponses]; export type UpdateBackupScheduleData = { body?: { @@ -2038,8 +1995,7 @@ export type UpdateBackupScheduleResponses = { }; }; -export type UpdateBackupScheduleResponse = - UpdateBackupScheduleResponses[keyof UpdateBackupScheduleResponses]; +export type UpdateBackupScheduleResponse = UpdateBackupScheduleResponses[keyof UpdateBackupScheduleResponses]; export type GetBackupScheduleForVolumeData = { body?: never; @@ -2164,15 +2120,7 @@ export type GetBackupScheduleForVolumeResponses = { name: string; shortId: string; status: "error" | "healthy" | "unknown" | null; - type: - | "azure" - | "gcs" - | "local" - | "r2" - | "rclone" - | "rest" - | "s3" - | "sftp"; + type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; updatedAt: number; }; repositoryId: string; @@ -2227,7 +2175,7 @@ export type GetBackupScheduleForVolumeResponses = { server: string; share: string; username: string; - vers?: "1.0" | "2.0" | "2.1" | "3.0"; + vers?: "1.0" | "2.0" | "2.1" | "3.0" | "auto"; port?: number; domain?: string; readOnly?: boolean; @@ -2277,8 +2225,7 @@ export type RunBackupNowResponses = { }; }; -export type RunBackupNowResponse = - RunBackupNowResponses[keyof RunBackupNowResponses]; +export type RunBackupNowResponse = RunBackupNowResponses[keyof RunBackupNowResponses]; export type StopBackupData = { body?: never; @@ -2414,16 +2361,7 @@ export type GetScheduleNotificationsResponses = { enabled: boolean; id: number; name: string; - type: - | "custom" - | "discord" - | "email" - | "generic" - | "gotify" - | "ntfy" - | "pushover" - | "slack" - | "telegram"; + type: "custom" | "discord" | "email" | "generic" | "gotify" | "ntfy" | "pushover" | "slack" | "telegram"; updatedAt: number; }; destinationId: number; @@ -2533,16 +2471,7 @@ export type UpdateScheduleNotificationsResponses = { enabled: boolean; id: number; name: string; - type: - | "custom" - | "discord" - | "email" - | "generic" - | "gotify" - | "ntfy" - | "pushover" - | "slack" - | "telegram"; + type: "custom" | "discord" | "email" | "generic" | "gotify" | "ntfy" | "pushover" | "slack" | "telegram"; updatedAt: number; }; destinationId: number; @@ -2672,15 +2601,7 @@ export type GetScheduleMirrorsResponses = { name: string; shortId: string; status: "error" | "healthy" | "unknown" | null; - type: - | "azure" - | "gcs" - | "local" - | "r2" - | "rclone" - | "rest" - | "s3" - | "sftp"; + type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; updatedAt: number; }; repositoryId: string; @@ -2688,8 +2609,7 @@ export type GetScheduleMirrorsResponses = { }>; }; -export type GetScheduleMirrorsResponse = - GetScheduleMirrorsResponses[keyof GetScheduleMirrorsResponses]; +export type GetScheduleMirrorsResponse = GetScheduleMirrorsResponses[keyof GetScheduleMirrorsResponses]; export type UpdateScheduleMirrorsData = { body?: { @@ -2811,15 +2731,7 @@ export type UpdateScheduleMirrorsResponses = { name: string; shortId: string; status: "error" | "healthy" | "unknown" | null; - type: - | "azure" - | "gcs" - | "local" - | "r2" - | "rclone" - | "rest" - | "s3" - | "sftp"; + type: "azure" | "gcs" | "local" | "r2" | "rclone" | "rest" | "s3" | "sftp"; updatedAt: number; }; repositoryId: string; @@ -2827,8 +2739,7 @@ export type UpdateScheduleMirrorsResponses = { }>; }; -export type UpdateScheduleMirrorsResponse = - UpdateScheduleMirrorsResponses[keyof UpdateScheduleMirrorsResponses]; +export type UpdateScheduleMirrorsResponse = UpdateScheduleMirrorsResponses[keyof UpdateScheduleMirrorsResponses]; export type GetMirrorCompatibilityData = { body?: never; @@ -2850,8 +2761,7 @@ export type GetMirrorCompatibilityResponses = { }>; }; -export type GetMirrorCompatibilityResponse = - GetMirrorCompatibilityResponses[keyof GetMirrorCompatibilityResponses]; +export type GetMirrorCompatibilityResponse = GetMirrorCompatibilityResponses[keyof GetMirrorCompatibilityResponses]; export type ReorderBackupSchedulesData = { body?: { @@ -2871,8 +2781,7 @@ export type ReorderBackupSchedulesResponses = { }; }; -export type ReorderBackupSchedulesResponse = - ReorderBackupSchedulesResponses[keyof ReorderBackupSchedulesResponses]; +export type ReorderBackupSchedulesResponse = ReorderBackupSchedulesResponses[keyof ReorderBackupSchedulesResponses]; export type ListNotificationDestinationsData = { body?: never; @@ -2957,16 +2866,7 @@ export type ListNotificationDestinationsResponses = { enabled: boolean; id: number; name: string; - type: - | "custom" - | "discord" - | "email" - | "generic" - | "gotify" - | "ntfy" - | "pushover" - | "slack" - | "telegram"; + type: "custom" | "discord" | "email" | "generic" | "gotify" | "ntfy" | "pushover" | "slack" | "telegram"; updatedAt: number; }>; }; @@ -3126,16 +3026,7 @@ export type CreateNotificationDestinationResponses = { enabled: boolean; id: number; name: string; - type: - | "custom" - | "discord" - | "email" - | "generic" - | "gotify" - | "ntfy" - | "pushover" - | "slack" - | "telegram"; + type: "custom" | "discord" | "email" | "generic" | "gotify" | "ntfy" | "pushover" | "slack" | "telegram"; updatedAt: number; }; }; @@ -3263,16 +3154,7 @@ export type GetNotificationDestinationResponses = { enabled: boolean; id: number; name: string; - type: - | "custom" - | "discord" - | "email" - | "generic" - | "gotify" - | "ntfy" - | "pushover" - | "slack" - | "telegram"; + type: "custom" | "discord" | "email" | "generic" | "gotify" | "ntfy" | "pushover" | "slack" | "telegram"; updatedAt: number; }; }; @@ -3442,16 +3324,7 @@ export type UpdateNotificationDestinationResponses = { enabled: boolean; id: number; name: string; - type: - | "custom" - | "discord" - | "email" - | "generic" - | "gotify" - | "ntfy" - | "pushover" - | "slack" - | "telegram"; + type: "custom" | "discord" | "email" | "generic" | "gotify" | "ntfy" | "pushover" | "slack" | "telegram"; updatedAt: number; }; }; @@ -3514,8 +3387,7 @@ export type GetSystemInfoResponses = { }; }; -export type GetSystemInfoResponse = - GetSystemInfoResponses[keyof GetSystemInfoResponses]; +export type GetSystemInfoResponse = GetSystemInfoResponses[keyof GetSystemInfoResponses]; export type GetUpdatesData = { body?: never; @@ -3559,5 +3431,4 @@ export type DownloadResticPasswordResponses = { 200: string; }; -export type DownloadResticPasswordResponse = - DownloadResticPasswordResponses[keyof DownloadResticPasswordResponses]; +export type DownloadResticPasswordResponse = DownloadResticPasswordResponses[keyof DownloadResticPasswordResponses]; diff --git a/app/schemas/volumes.ts b/app/schemas/volumes.ts index 5589935a..5fab2c51 100644 --- a/app/schemas/volumes.ts +++ b/app/schemas/volumes.ts @@ -26,7 +26,7 @@ export const smbConfigSchema = type({ share: "string", username: "string", password: "string", - vers: type("'1.0' | '2.0' | '2.1' | '3.0'").default("3.0"), + vers: type("'1.0' | '2.0' | '2.1' | '3.0' | 'auto'").default("auto"), domain: "string?", port: type("string.integer").or(type("number")).to("1 <= number <= 65535").default(445), readOnly: "boolean?", diff --git a/app/server/modules/backends/nfs/nfs-backend.ts b/app/server/modules/backends/nfs/nfs-backend.ts index 876c18b0..d41d2cfb 100644 --- a/app/server/modules/backends/nfs/nfs-backend.ts +++ b/app/server/modules/backends/nfs/nfs-backend.ts @@ -54,10 +54,13 @@ const mount = async (config: BackendConfig, path: string) => { logger.debug(`Mounting volume ${path}...`); logger.info(`Executing mount: mount ${args.join(" ")}`); - await executeMount(args); - - // Fallback with -i flag if the first mount fails using the mount helper - await executeMount(["-i", ...args]); + try { + await executeMount(args); + } catch (error) { + logger.warn(`Initial NFS mount failed, retrying with -i flag: ${toMessage(error)}`); + // Fallback with -i flag if the first mount fails using the mount helper + await executeMount(["-i", ...args]); + } logger.info(`NFS volume at ${path} mounted successfully.`); return { status: BACKEND_STATUS.mounted }; diff --git a/app/server/modules/backends/smb/smb-backend.ts b/app/server/modules/backends/smb/smb-backend.ts index 39d97a29..2f76ff3f 100644 --- a/app/server/modules/backends/smb/smb-backend.ts +++ b/app/server/modules/backends/smb/smb-backend.ts @@ -40,14 +40,11 @@ const mount = async (config: BackendConfig, path: string) => { const source = `//${config.server}/${config.share}`; const { uid, gid } = os.userInfo(); - const options = [ - `user=${config.username}`, - `pass=${password}`, - `vers=${config.vers}`, - `port=${config.port}`, - `uid=${uid}`, - `gid=${gid}`, - ]; + const options = [`user=${config.username}`, `pass=${password}`, `port=${config.port}`, `uid=${uid}`, `gid=${gid}`]; + + if (config.vers && config.vers !== "auto") { + options.push(`vers=${config.vers}`); + } if (config.domain) { options.push(`domain=${config.domain}`); @@ -62,10 +59,13 @@ const mount = async (config: BackendConfig, path: string) => { logger.debug(`Mounting SMB volume ${path}...`); logger.info(`Executing mount: mount ${args.join(" ")}`); - await executeMount(args); - - // Fallback with -i flag if the first mount fails using the mount helper - await executeMount(["-i", ...args]); + try { + await executeMount(args); + } catch (error) { + logger.warn(`Initial SMB mount failed, retrying with -i flag: ${toMessage(error)}`); + // Fallback with -i flag if the first mount fails using the mount helper + await executeMount(["-i", ...args]); + } logger.info(`SMB volume at ${path} mounted successfully.`); return { status: BACKEND_STATUS.mounted }; diff --git a/app/server/modules/backends/utils/backend-utils.ts b/app/server/modules/backends/utils/backend-utils.ts index 20153e59..044f4a7e 100644 --- a/app/server/modules/backends/utils/backend-utils.ts +++ b/app/server/modules/backends/utils/backend-utils.ts @@ -7,10 +7,10 @@ import { logger } from "../../../utils/logger"; export const executeMount = async (args: string[]): Promise => { const shouldBeVerbose = process.env.LOG_LEVEL === "debug" || process.env.NODE_ENV !== "production"; const hasVerboseFlag = args.some((arg) => arg === "-v" || arg.startsWith("-vv")); - const effectiveArgs = shouldBeVerbose && !hasVerboseFlag ? ["-vvv", ...args] : args; + const effectiveArgs = shouldBeVerbose && !hasVerboseFlag ? ["-v", ...args] : args; logger.debug(`Executing mount ${effectiveArgs.join(" ")}`); - const result = await $`mount ${effectiveArgs}`.nothrow(); + let result = await $`mount ${effectiveArgs}`.nothrow(); const stdout = result.stdout.toString().trim(); const stderr = result.stderr.toString().trim(); diff --git a/app/server/modules/backends/webdav/webdav-backend.ts b/app/server/modules/backends/webdav/webdav-backend.ts index 864da6ca..345a200b 100644 --- a/app/server/modules/backends/webdav/webdav-backend.ts +++ b/app/server/modules/backends/webdav/webdav-backend.ts @@ -58,10 +58,14 @@ const mount = async (config: BackendConfig, path: string) => { logger.debug(`Mounting WebDAV volume ${path}...`); const args = ["-t", "davfs", "-o", options.join(","), source, path]; - await executeMount(args); - // Fallback with -i flag if the first mount fails using the mount helper - await executeMount(["-i", ...args]); + try { + await executeMount(args); + } catch (error) { + logger.warn(`Initial WebDAV mount failed, retrying with -i flag: ${toMessage(error)}`); + // Fallback with -i flag if the first mount fails using the mount helper + await executeMount(["-i", ...args]); + } logger.info(`WebDAV volume at ${path} mounted successfully.`); return { status: BACKEND_STATUS.mounted }; From 9fe134b93a23775f129ffaddfbe30b55b2fc8c3e Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sun, 11 Jan 2026 16:48:19 +0100 Subject: [PATCH 21/23] docs(readme): update version --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 94c166ad..fd4004fb 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ In order to run Zerobyte, you need to have Docker and Docker Compose installed o ```yaml services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.21 + image: ghcr.io/nicotsx/zerobyte:v0.22 container_name: zerobyte restart: unless-stopped cap_add: @@ -102,7 +102,7 @@ If you only need to back up locally mounted folders and don't require remote sha ```yaml services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.21 + image: ghcr.io/nicotsx/zerobyte:v0.22 container_name: zerobyte restart: unless-stopped ports: @@ -139,7 +139,7 @@ If you want to track a local directory on the same server where Zerobyte is runn ```diff services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.21 + image: ghcr.io/nicotsx/zerobyte:v0.22 container_name: zerobyte restart: unless-stopped cap_add: @@ -212,7 +212,7 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov ```diff services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.21 + image: ghcr.io/nicotsx/zerobyte:v0.22 container_name: zerobyte restart: unless-stopped cap_add: From cb72e6c073add14965367173a3f624ee36a146cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 07:52:27 +0100 Subject: [PATCH 22/23] chore(deps): bump the minor-patch group with 9 updates (#355) * chore(deps): bump the minor-patch group with 9 updates Bumps the minor-patch group with 9 updates: | Package | From | To | | --- | --- | --- | | [@inquirer/prompts](https://github.com/SBoudrias/Inquirer.js) | `8.1.0` | `8.2.0` | | [better-auth](https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth) | `1.4.10` | `1.4.12` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.70.0` | `7.71.0` | | [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) | `0.90.2` | `0.90.3` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.0.3` | `25.0.7` | | [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.7` | `19.2.8` | | [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) | `0.23.0` | `0.24.0` | | [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) | `1.38.0` | `1.39.0` | | [vite-tsconfig-paths](https://github.com/aleclarson/vite-tsconfig-paths) | `6.0.3` | `6.0.4` | Updates `@inquirer/prompts` from 8.1.0 to 8.2.0 - [Release notes](https://github.com/SBoudrias/Inquirer.js/releases) - [Commits](https://github.com/SBoudrias/Inquirer.js/compare/@inquirer/prompts@8.1.0...@inquirer/prompts@8.2.0) Updates `better-auth` from 1.4.10 to 1.4.12 - [Release notes](https://github.com/better-auth/better-auth/releases) - [Commits](https://github.com/better-auth/better-auth/commits/v1.4.12/packages/better-auth) Updates `react-hook-form` from 7.70.0 to 7.71.0 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.70.0...v7.71.0) Updates `@hey-api/openapi-ts` from 0.90.2 to 0.90.3 - [Release notes](https://github.com/hey-api/openapi-ts/releases) - [Changelog](https://github.com/hey-api/openapi-ts/blob/main/docs/CHANGELOG.md) - [Commits](https://github.com/hey-api/openapi-ts/compare/@hey-api/openapi-ts@0.90.2...@hey-api/openapi-ts@0.90.3) Updates `@types/node` from 25.0.3 to 25.0.7 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/react` from 19.2.7 to 19.2.8 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `oxfmt` from 0.23.0 to 0.24.0 - [Release notes](https://github.com/oxc-project/oxc/releases) - [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md) - [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.24.0/npm/oxfmt) Updates `oxlint` from 1.38.0 to 1.39.0 - [Release notes](https://github.com/oxc-project/oxc/releases) - [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md) - [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.39.0/npm/oxlint) Updates `vite-tsconfig-paths` from 6.0.3 to 6.0.4 - [Release notes](https://github.com/aleclarson/vite-tsconfig-paths/releases) - [Commits](https://github.com/aleclarson/vite-tsconfig-paths/compare/v6.0.3...v6.0.4) --- updated-dependencies: - dependency-name: "@inquirer/prompts" dependency-version: 8.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: better-auth dependency-version: 1.4.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: react-hook-form dependency-version: 7.71.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: "@hey-api/openapi-ts" dependency-version: 0.90.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@types/node" dependency-version: 25.0.7 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: "@types/react" dependency-version: 19.2.8 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: oxfmt dependency-version: 0.24.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: oxlint dependency-version: 1.39.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: vite-tsconfig-paths dependency-version: 6.0.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch ... Signed-off-by: dependabot[bot] * chore: gen api client --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nicolas Meienberger --- .../api-client/@tanstack/react-query.gen.ts | 470 ++++-------------- app/client/api-client/client.gen.ts | 11 +- app/client/api-client/client/index.ts | 6 +- app/client/api-client/client/types.gen.ts | 68 +-- app/client/api-client/client/utils.gen.ts | 76 +-- app/client/api-client/core/auth.gen.ts | 3 +- .../api-client/core/bodySerializer.gen.ts | 30 +- app/client/api-client/core/params.gen.ts | 13 +- .../api-client/core/pathSerializer.gen.ts | 30 +- .../api-client/core/queryKeySerializer.gen.ts | 39 +- .../api-client/core/serverSentEvents.gen.ts | 45 +- app/client/api-client/core/types.gen.ts | 44 +- app/client/api-client/core/utils.gen.ts | 12 +- app/client/api-client/sdk.gen.ts | 422 ++++++---------- bun.lock | 110 ++-- openapi-ts.config.ts | 2 +- package.json | 18 +- 17 files changed, 392 insertions(+), 1007 deletions(-) diff --git a/app/client/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts index 8f40258f..342f4a36 100644 --- a/app/client/api-client/@tanstack/react-query.gen.ts +++ b/app/client/api-client/@tanstack/react-query.gen.ts @@ -1,10 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -import { - type DefaultError, - queryOptions, - type UseMutationOptions, -} from "@tanstack/react-query"; +import { type DefaultError, queryOptions, type UseMutationOptions } from "@tanstack/react-query"; import { client } from "../client.gen"; import { @@ -179,8 +175,7 @@ const createQueryKey = ( ): [QueryKey[0]] => { const params: QueryKey[0] = { _id: id, - baseUrl: - options?.baseUrl || (options?.client ?? client).getConfig().baseUrl, + baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl, } as QueryKey[0]; if (infinite) { params._infinite = infinite; @@ -203,19 +198,13 @@ const createQueryKey = ( return [params]; }; -export const getStatusQueryKey = (options?: Options) => - createQueryKey("getStatus", options); +export const getStatusQueryKey = (options?: Options) => createQueryKey("getStatus", options); /** * Get authentication system status */ export const getStatusOptions = (options?: Options) => - queryOptions< - GetStatusResponse, - DefaultError, - GetStatusResponse, - ReturnType - >({ + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await getStatus({ ...options, @@ -228,19 +217,13 @@ export const getStatusOptions = (options?: Options) => queryKey: getStatusQueryKey(options), }); -export const listVolumesQueryKey = (options?: Options) => - createQueryKey("listVolumes", options); +export const listVolumesQueryKey = (options?: Options) => createQueryKey("listVolumes", options); /** * List all volumes */ export const listVolumesOptions = (options?: Options) => - queryOptions< - ListVolumesResponse, - DefaultError, - ListVolumesResponse, - ReturnType - >({ + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await listVolumes({ ...options, @@ -258,16 +241,8 @@ export const listVolumesOptions = (options?: Options) => */ export const createVolumeMutation = ( options?: Partial>, -): UseMutationOptions< - CreateVolumeResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - CreateVolumeResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await createVolume({ ...options, @@ -285,16 +260,8 @@ export const createVolumeMutation = ( */ export const testConnectionMutation = ( options?: Partial>, -): UseMutationOptions< - TestConnectionResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - TestConnectionResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await testConnection({ ...options, @@ -312,16 +279,8 @@ export const testConnectionMutation = ( */ export const deleteVolumeMutation = ( options?: Partial>, -): UseMutationOptions< - DeleteVolumeResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - DeleteVolumeResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await deleteVolume({ ...options, @@ -334,19 +293,13 @@ export const deleteVolumeMutation = ( return mutationOptions; }; -export const getVolumeQueryKey = (options: Options) => - createQueryKey("getVolume", options); +export const getVolumeQueryKey = (options: Options) => createQueryKey("getVolume", options); /** * Get a volume by name */ export const getVolumeOptions = (options: Options) => - queryOptions< - GetVolumeResponse, - DefaultError, - GetVolumeResponse, - ReturnType - >({ + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await getVolume({ ...options, @@ -364,16 +317,8 @@ export const getVolumeOptions = (options: Options) => */ export const updateVolumeMutation = ( options?: Partial>, -): UseMutationOptions< - UpdateVolumeResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - UpdateVolumeResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await updateVolume({ ...options, @@ -391,16 +336,8 @@ export const updateVolumeMutation = ( */ export const mountVolumeMutation = ( options?: Partial>, -): UseMutationOptions< - MountVolumeResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - MountVolumeResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await mountVolume({ ...options, @@ -418,16 +355,8 @@ export const mountVolumeMutation = ( */ export const unmountVolumeMutation = ( options?: Partial>, -): UseMutationOptions< - UnmountVolumeResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - UnmountVolumeResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await unmountVolume({ ...options, @@ -445,16 +374,8 @@ export const unmountVolumeMutation = ( */ export const healthCheckVolumeMutation = ( options?: Partial>, -): UseMutationOptions< - HealthCheckVolumeResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - HealthCheckVolumeResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await healthCheckVolume({ ...options, @@ -467,19 +388,13 @@ export const healthCheckVolumeMutation = ( return mutationOptions; }; -export const listFilesQueryKey = (options: Options) => - createQueryKey("listFiles", options); +export const listFilesQueryKey = (options: Options) => createQueryKey("listFiles", options); /** * List files in a volume directory */ export const listFilesOptions = (options: Options) => - queryOptions< - ListFilesResponse, - DefaultError, - ListFilesResponse, - ReturnType - >({ + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await listFiles({ ...options, @@ -492,16 +407,13 @@ export const listFilesOptions = (options: Options) => 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 */ -export const browseFilesystemOptions = ( - options?: Options, -) => +export const browseFilesystemOptions = (options?: Options) => queryOptions< BrowseFilesystemResponse, DefaultError, @@ -520,16 +432,13 @@ export const browseFilesystemOptions = ( queryKey: browseFilesystemQueryKey(options), }); -export const listRepositoriesQueryKey = ( - options?: Options, -) => createQueryKey("listRepositories", options); +export const listRepositoriesQueryKey = (options?: Options) => + createQueryKey("listRepositories", options); /** * List all repositories */ -export const listRepositoriesOptions = ( - options?: Options, -) => +export const listRepositoriesOptions = (options?: Options) => queryOptions< ListRepositoriesResponse, DefaultError, @@ -553,16 +462,8 @@ export const listRepositoriesOptions = ( */ export const createRepositoryMutation = ( options?: Partial>, -): UseMutationOptions< - CreateRepositoryResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - CreateRepositoryResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await createRepository({ ...options, @@ -575,16 +476,13 @@ export const createRepositoryMutation = ( return mutationOptions; }; -export const listRcloneRemotesQueryKey = ( - options?: Options, -) => createQueryKey("listRcloneRemotes", options); +export const listRcloneRemotesQueryKey = (options?: Options) => + createQueryKey("listRcloneRemotes", options); /** * List all configured rclone remotes on the host system */ -export const listRcloneRemotesOptions = ( - options?: Options, -) => +export const listRcloneRemotesOptions = (options?: Options) => queryOptions< ListRcloneRemotesResponse, DefaultError, @@ -608,16 +506,8 @@ export const listRcloneRemotesOptions = ( */ export const deleteRepositoryMutation = ( options?: Partial>, -): UseMutationOptions< - DeleteRepositoryResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - DeleteRepositoryResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await deleteRepository({ ...options, @@ -630,19 +520,13 @@ export const deleteRepositoryMutation = ( return mutationOptions; }; -export const getRepositoryQueryKey = (options: Options) => - createQueryKey("getRepository", options); +export const getRepositoryQueryKey = (options: Options) => createQueryKey("getRepository", options); /** * Get a single repository by ID */ export const getRepositoryOptions = (options: Options) => - queryOptions< - GetRepositoryResponse, - DefaultError, - GetRepositoryResponse, - ReturnType - >({ + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await getRepository({ ...options, @@ -660,16 +544,8 @@ export const getRepositoryOptions = (options: Options) => */ export const updateRepositoryMutation = ( options?: Partial>, -): UseMutationOptions< - UpdateRepositoryResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - UpdateRepositoryResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await updateRepository({ ...options, @@ -687,16 +563,8 @@ export const updateRepositoryMutation = ( */ export const deleteSnapshotsMutation = ( options?: Partial>, -): UseMutationOptions< - DeleteSnapshotsResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - DeleteSnapshotsResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await deleteSnapshots({ ...options, @@ -709,19 +577,13 @@ export const deleteSnapshotsMutation = ( return mutationOptions; }; -export const listSnapshotsQueryKey = (options: Options) => - createQueryKey("listSnapshots", options); +export const listSnapshotsQueryKey = (options: Options) => createQueryKey("listSnapshots", options); /** * List all snapshots in a repository */ export const listSnapshotsOptions = (options: Options) => - queryOptions< - ListSnapshotsResponse, - DefaultError, - ListSnapshotsResponse, - ReturnType - >({ + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await listSnapshots({ ...options, @@ -739,16 +601,8 @@ export const listSnapshotsOptions = (options: Options) => */ export const deleteSnapshotMutation = ( options?: Partial>, -): UseMutationOptions< - DeleteSnapshotResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - DeleteSnapshotResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await deleteSnapshot({ ...options, @@ -761,16 +615,13 @@ export const deleteSnapshotMutation = ( return mutationOptions; }; -export const getSnapshotDetailsQueryKey = ( - options: Options, -) => createQueryKey("getSnapshotDetails", options); +export const getSnapshotDetailsQueryKey = (options: Options) => + createQueryKey("getSnapshotDetails", options); /** * Get details of a specific snapshot */ -export const getSnapshotDetailsOptions = ( - options: Options, -) => +export const getSnapshotDetailsOptions = (options: Options) => queryOptions< GetSnapshotDetailsResponse, DefaultError, @@ -789,16 +640,13 @@ export const getSnapshotDetailsOptions = ( queryKey: getSnapshotDetailsQueryKey(options), }); -export const listSnapshotFilesQueryKey = ( - options: Options, -) => createQueryKey("listSnapshotFiles", options); +export const listSnapshotFilesQueryKey = (options: Options) => + createQueryKey("listSnapshotFiles", options); /** * List files and directories in a snapshot */ -export const listSnapshotFilesOptions = ( - options: Options, -) => +export const listSnapshotFilesOptions = (options: Options) => queryOptions< ListSnapshotFilesResponse, DefaultError, @@ -822,16 +670,8 @@ export const listSnapshotFilesOptions = ( */ export const restoreSnapshotMutation = ( options?: Partial>, -): UseMutationOptions< - RestoreSnapshotResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - RestoreSnapshotResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await restoreSnapshot({ ...options, @@ -849,16 +689,8 @@ export const restoreSnapshotMutation = ( */ export const doctorRepositoryMutation = ( options?: Partial>, -): UseMutationOptions< - DoctorRepositoryResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - DoctorRepositoryResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await doctorRepository({ ...options, @@ -876,16 +708,8 @@ export const doctorRepositoryMutation = ( */ export const tagSnapshotsMutation = ( options?: Partial>, -): UseMutationOptions< - TagSnapshotsResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - TagSnapshotsResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await tagSnapshots({ ...options, @@ -898,16 +722,13 @@ export const tagSnapshotsMutation = ( return mutationOptions; }; -export const listBackupSchedulesQueryKey = ( - options?: Options, -) => createQueryKey("listBackupSchedules", options); +export const listBackupSchedulesQueryKey = (options?: Options) => + createQueryKey("listBackupSchedules", options); /** * List all backup schedules */ -export const listBackupSchedulesOptions = ( - options?: Options, -) => +export const listBackupSchedulesOptions = (options?: Options) => queryOptions< ListBackupSchedulesResponse, DefaultError, @@ -931,11 +752,7 @@ export const listBackupSchedulesOptions = ( */ export const createBackupScheduleMutation = ( options?: Partial>, -): UseMutationOptions< - CreateBackupScheduleResponse, - DefaultError, - Options -> => { +): UseMutationOptions> => { const mutationOptions: UseMutationOptions< CreateBackupScheduleResponse, DefaultError, @@ -958,11 +775,7 @@ export const createBackupScheduleMutation = ( */ export const deleteBackupScheduleMutation = ( options?: Partial>, -): UseMutationOptions< - DeleteBackupScheduleResponse, - DefaultError, - Options -> => { +): UseMutationOptions> => { const mutationOptions: UseMutationOptions< DeleteBackupScheduleResponse, DefaultError, @@ -980,16 +793,13 @@ export const deleteBackupScheduleMutation = ( return mutationOptions; }; -export const getBackupScheduleQueryKey = ( - options: Options, -) => createQueryKey("getBackupSchedule", options); +export const getBackupScheduleQueryKey = (options: Options) => + createQueryKey("getBackupSchedule", options); /** * Get a backup schedule by ID */ -export const getBackupScheduleOptions = ( - options: Options, -) => +export const getBackupScheduleOptions = (options: Options) => queryOptions< GetBackupScheduleResponse, DefaultError, @@ -1013,11 +823,7 @@ export const getBackupScheduleOptions = ( */ export const updateBackupScheduleMutation = ( options?: Partial>, -): UseMutationOptions< - UpdateBackupScheduleResponse, - DefaultError, - Options -> => { +): UseMutationOptions> => { const mutationOptions: UseMutationOptions< UpdateBackupScheduleResponse, DefaultError, @@ -1035,16 +841,13 @@ export const updateBackupScheduleMutation = ( return mutationOptions; }; -export const getBackupScheduleForVolumeQueryKey = ( - options: Options, -) => createQueryKey("getBackupScheduleForVolume", options); +export const getBackupScheduleForVolumeQueryKey = (options: Options) => + createQueryKey("getBackupScheduleForVolume", options); /** * Get a backup schedule for a specific volume */ -export const getBackupScheduleForVolumeOptions = ( - options: Options, -) => +export const getBackupScheduleForVolumeOptions = (options: Options) => queryOptions< GetBackupScheduleForVolumeResponse, DefaultError, @@ -1068,16 +871,8 @@ export const getBackupScheduleForVolumeOptions = ( */ export const runBackupNowMutation = ( options?: Partial>, -): UseMutationOptions< - RunBackupNowResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - RunBackupNowResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await runBackupNow({ ...options, @@ -1095,16 +890,8 @@ export const runBackupNowMutation = ( */ export const stopBackupMutation = ( options?: Partial>, -): UseMutationOptions< - StopBackupResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - StopBackupResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await stopBackup({ ...options, @@ -1122,16 +909,8 @@ export const stopBackupMutation = ( */ export const runForgetMutation = ( options?: Partial>, -): UseMutationOptions< - RunForgetResponse, - DefaultError, - Options -> => { - const mutationOptions: UseMutationOptions< - RunForgetResponse, - DefaultError, - Options - > = { +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { mutationFn: async (fnOptions) => { const { data } = await runForget({ ...options, @@ -1144,16 +923,13 @@ export const runForgetMutation = ( 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 */ -export const getScheduleNotificationsOptions = ( - options: Options, -) => +export const getScheduleNotificationsOptions = (options: Options) => queryOptions< GetScheduleNotificationsResponse, DefaultError, @@ -1177,11 +953,7 @@ export const getScheduleNotificationsOptions = ( */ export const updateScheduleNotificationsMutation = ( options?: Partial>, -): UseMutationOptions< - UpdateScheduleNotificationsResponse, - DefaultError, - Options -> => { +): UseMutationOptions> => { const mutationOptions: UseMutationOptions< UpdateScheduleNotificationsResponse, DefaultError, @@ -1199,16 +971,13 @@ export const updateScheduleNotificationsMutation = ( return mutationOptions; }; -export const getScheduleMirrorsQueryKey = ( - options: Options, -) => createQueryKey("getScheduleMirrors", options); +export const getScheduleMirrorsQueryKey = (options: Options) => + createQueryKey("getScheduleMirrors", options); /** * Get mirror repository assignments for a backup schedule */ -export const getScheduleMirrorsOptions = ( - options: Options, -) => +export const getScheduleMirrorsOptions = (options: Options) => queryOptions< GetScheduleMirrorsResponse, DefaultError, @@ -1232,11 +1001,7 @@ export const getScheduleMirrorsOptions = ( */ export const updateScheduleMirrorsMutation = ( options?: Partial>, -): UseMutationOptions< - UpdateScheduleMirrorsResponse, - DefaultError, - Options -> => { +): UseMutationOptions> => { const mutationOptions: UseMutationOptions< UpdateScheduleMirrorsResponse, DefaultError, @@ -1254,16 +1019,13 @@ export const updateScheduleMirrorsMutation = ( return mutationOptions; }; -export const getMirrorCompatibilityQueryKey = ( - options: Options, -) => 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 */ -export const getMirrorCompatibilityOptions = ( - options: Options, -) => +export const getMirrorCompatibilityOptions = (options: Options) => queryOptions< GetMirrorCompatibilityResponse, DefaultError, @@ -1287,11 +1049,7 @@ export const getMirrorCompatibilityOptions = ( */ export const reorderBackupSchedulesMutation = ( options?: Partial>, -): UseMutationOptions< - ReorderBackupSchedulesResponse, - DefaultError, - Options -> => { +): UseMutationOptions> => { const mutationOptions: UseMutationOptions< ReorderBackupSchedulesResponse, DefaultError, @@ -1309,16 +1067,13 @@ export const reorderBackupSchedulesMutation = ( return mutationOptions; }; -export const listNotificationDestinationsQueryKey = ( - options?: Options, -) => createQueryKey("listNotificationDestinations", options); +export const listNotificationDestinationsQueryKey = (options?: Options) => + createQueryKey("listNotificationDestinations", options); /** * List all notification destinations */ -export const listNotificationDestinationsOptions = ( - options?: Options, -) => +export const listNotificationDestinationsOptions = (options?: Options) => queryOptions< ListNotificationDestinationsResponse, DefaultError, @@ -1391,16 +1146,13 @@ export const deleteNotificationDestinationMutation = ( return mutationOptions; }; -export const getNotificationDestinationQueryKey = ( - options: Options, -) => createQueryKey("getNotificationDestination", options); +export const getNotificationDestinationQueryKey = (options: Options) => + createQueryKey("getNotificationDestination", options); /** * Get a notification destination by ID */ -export const getNotificationDestinationOptions = ( - options: Options, -) => +export const getNotificationDestinationOptions = (options: Options) => queryOptions< GetNotificationDestinationResponse, DefaultError, @@ -1451,11 +1203,7 @@ export const updateNotificationDestinationMutation = ( */ export const testNotificationDestinationMutation = ( options?: Partial>, -): UseMutationOptions< - TestNotificationDestinationResponse, - DefaultError, - Options -> => { +): UseMutationOptions> => { const mutationOptions: UseMutationOptions< TestNotificationDestinationResponse, DefaultError, @@ -1473,19 +1221,13 @@ export const testNotificationDestinationMutation = ( return mutationOptions; }; -export const getSystemInfoQueryKey = (options?: Options) => - createQueryKey("getSystemInfo", options); +export const getSystemInfoQueryKey = (options?: Options) => createQueryKey("getSystemInfo", options); /** * Get system information including available capabilities */ export const getSystemInfoOptions = (options?: Options) => - queryOptions< - GetSystemInfoResponse, - DefaultError, - GetSystemInfoResponse, - ReturnType - >({ + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await getSystemInfo({ ...options, @@ -1498,19 +1240,13 @@ export const getSystemInfoOptions = (options?: Options) => queryKey: getSystemInfoQueryKey(options), }); -export const getUpdatesQueryKey = (options?: Options) => - createQueryKey("getUpdates", options); +export const getUpdatesQueryKey = (options?: Options) => createQueryKey("getUpdates", options); /** * Check for application updates from GitHub */ export const getUpdatesOptions = (options?: Options) => - queryOptions< - GetUpdatesResponse, - DefaultError, - GetUpdatesResponse, - ReturnType - >({ + queryOptions>({ queryFn: async ({ queryKey, signal }) => { const { data } = await getUpdates({ ...options, @@ -1528,11 +1264,7 @@ export const getUpdatesOptions = (options?: Options) => */ export const downloadResticPasswordMutation = ( options?: Partial>, -): UseMutationOptions< - DownloadResticPasswordResponse, - DefaultError, - Options -> => { +): UseMutationOptions> => { const mutationOptions: UseMutationOptions< DownloadResticPasswordResponse, DefaultError, diff --git a/app/client/api-client/client.gen.ts b/app/client/api-client/client.gen.ts index 92f3451a..301c95c0 100644 --- a/app/client/api-client/client.gen.ts +++ b/app/client/api-client/client.gen.ts @@ -1,11 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -import { - type ClientOptions, - type Config, - createClient, - createConfig, -} from "./client"; +import { type ClientOptions, type Config, createClient, createConfig } from "./client"; import type { ClientOptions as ClientOptions2 } from "./types.gen"; /** @@ -20,6 +15,4 @@ export type CreateClientConfig = ( override?: Config, ) => Config & T>; -export const client = createClient( - createConfig({ baseUrl: "http://localhost:4096" }), -); +export const client = createClient(createConfig({ baseUrl: "http://localhost:4096" })); diff --git a/app/client/api-client/client/index.ts b/app/client/api-client/client/index.ts index 99ab72d0..88229e0a 100644 --- a/app/client/api-client/client/index.ts +++ b/app/client/api-client/client/index.ts @@ -2,11 +2,7 @@ export type { Auth } from "../core/auth.gen"; export type { QuerySerializerOptions } from "../core/bodySerializer.gen"; -export { - formDataBodySerializer, - jsonBodySerializer, - urlSearchParamsBodySerializer, -} from "../core/bodySerializer.gen"; +export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer } from "../core/bodySerializer.gen"; export { buildClientParams } from "../core/params.gen"; export { serializeQueryKeyValue } from "../core/queryKeySerializer.gen"; export { createClient } from "./client.gen"; diff --git a/app/client/api-client/client/types.gen.ts b/app/client/api-client/client/types.gen.ts index 68310f36..b5495c11 100644 --- a/app/client/api-client/client/types.gen.ts +++ b/app/client/api-client/client/types.gen.ts @@ -1,21 +1,14 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Auth } from "../core/auth.gen"; -import type { - ServerSentEventsOptions, - ServerSentEventsResult, -} from "../core/serverSentEvents.gen"; -import type { - Client as CoreClient, - Config as CoreConfig, -} from "../core/types.gen"; +import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen"; +import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen"; import type { Middleware } from "./utils.gen"; export type ResponseStyle = "data" | "fields"; export interface Config - extends Omit, - CoreConfig { + extends Omit, CoreConfig { /** * Base URL for all requests made by this client. */ @@ -42,14 +35,7 @@ export interface Config * * @default 'auto' */ - parseAs?: - | "arrayBuffer" - | "auto" - | "blob" - | "formData" - | "json" - | "stream" - | "text"; + parseAs?: "arrayBuffer" | "auto" | "blob" | "formData" | "json" | "stream" | "text"; /** * Should we return only data or multiple fields (data, error, response, etc.)? * @@ -69,17 +55,15 @@ export interface RequestOptions< TResponseStyle extends ResponseStyle = "fields", ThrowOnError extends boolean = boolean, Url extends string = string, -> extends Config<{ +> + extends + Config<{ responseStyle: TResponseStyle; throwOnError: ThrowOnError; }>, Pick< ServerSentEventsOptions, - | "onSseError" - | "onSseEvent" - | "sseDefaultRetryDelay" - | "sseMaxRetryAttempts" - | "sseMaxRetryDelay" + "onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay" > { /** * Any body that you want to add to your request. @@ -116,32 +100,22 @@ export type RequestResult< ? TData[keyof TData] : TData : { - data: TData extends Record - ? TData[keyof TData] - : TData; + data: TData extends Record ? TData[keyof TData] : TData; request: Request; response: Response; } > : Promise< TResponseStyle extends "data" - ? - | (TData extends Record - ? TData[keyof TData] - : TData) - | undefined + ? (TData extends Record ? TData[keyof TData] : TData) | undefined : ( | { - data: TData extends Record - ? TData[keyof TData] - : TData; + data: TData extends Record ? TData[keyof TData] : TData; error: undefined; } | { data: undefined; - error: TError extends Record - ? TError[keyof TError] - : TError; + error: TError extends Record ? TError[keyof TError] : TError; } ) & { request: Request; @@ -180,10 +154,7 @@ type RequestFn = < TResponseStyle extends ResponseStyle = "fields", >( options: Omit, "method"> & - Pick< - Required>, - "method" - >, + Pick>, "method">, ) => RequestResult; type BuildUrlFn = < @@ -197,13 +168,7 @@ type BuildUrlFn = < options: TData & Options, ) => string; -export type Client = CoreClient< - RequestFn, - Config, - MethodFn, - BuildUrlFn, - SseFn -> & { +export type Client = CoreClient & { interceptors: Middleware; }; @@ -234,8 +199,5 @@ export type Options< ThrowOnError extends boolean = boolean, TResponse = unknown, TResponseStyle extends ResponseStyle = "fields", -> = OmitKeys< - RequestOptions, - "body" | "path" | "query" | "url" -> & +> = OmitKeys, "body" | "path" | "query" | "url"> & ([TData] extends [never] ? unknown : Omit); diff --git a/app/client/api-client/client/utils.gen.ts b/app/client/api-client/client/utils.gen.ts index 82b63a89..fb460443 100644 --- a/app/client/api-client/client/utils.gen.ts +++ b/app/client/api-client/client/utils.gen.ts @@ -3,23 +3,11 @@ import { getAuthToken } from "../core/auth.gen"; import type { QuerySerializerOptions } from "../core/bodySerializer.gen"; import { jsonBodySerializer } from "../core/bodySerializer.gen"; -import { - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from "../core/pathSerializer.gen"; +import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen"; import { getUrl } from "../core/utils.gen"; -import type { - Client, - ClientOptions, - Config, - RequestOptions, -} from "./types.gen"; +import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen"; -export const createQuerySerializer = ({ - parameters = {}, - ...args -}: QuerySerializerOptions = {}) => { +export const createQuerySerializer = ({ parameters = {}, ...args }: QuerySerializerOptions = {}) => { const querySerializer = (queryParams: T) => { const search: string[] = []; if (queryParams && typeof queryParams === "object") { @@ -70,9 +58,7 @@ export const createQuerySerializer = ({ /** * Infers parseAs value from provided Content-Type header. */ -export const getParseAs = ( - contentType: string | null, -): Exclude => { +export const getParseAs = (contentType: string | null): Exclude => { if (!contentType) { // If no Content-Type header is provided, the best we can do is return the raw response body, // which is effectively the same as the 'stream' option. @@ -85,10 +71,7 @@ export const getParseAs = ( return; } - if ( - cleanContent.startsWith("application/json") || - cleanContent.endsWith("+json") - ) { + if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) { return "json"; } @@ -96,11 +79,7 @@ export const getParseAs = ( return "formData"; } - if ( - ["application/", "audio/", "image/", "video/"].some((type) => - cleanContent.startsWith(type), - ) - ) { + if (["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))) { return "blob"; } @@ -120,11 +99,7 @@ const checkForExistence = ( if (!name) { return false; } - if ( - options.headers.has(name) || - options.query?.[name] || - options.headers.get("Cookie")?.includes(`${name}=`) - ) { + if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) { return true; } return false; @@ -197,19 +172,14 @@ const headersEntries = (headers: Headers): Array<[string, string]> => { return entries; }; -export const mergeHeaders = ( - ...headers: Array["headers"] | undefined> -): Headers => { +export const mergeHeaders = (...headers: Array["headers"] | undefined>): Headers => { const mergedHeaders = new Headers(); for (const header of headers) { if (!header) { continue; } - const iterator = - header instanceof Headers - ? headersEntries(header) - : Object.entries(header); + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -221,10 +191,7 @@ export const mergeHeaders = ( } else if (value !== undefined) { // assume object headers are meant to be JSON stringified, i.e. their // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === "object" ? JSON.stringify(value) : (value as string), - ); + mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : (value as string)); } } } @@ -238,16 +205,9 @@ type ErrInterceptor = ( options: Options, ) => Err | Promise; -type ReqInterceptor = ( - request: Req, - options: Options, -) => Req | Promise; +type ReqInterceptor = (request: Req, options: Options) => Req | Promise; -type ResInterceptor = ( - response: Res, - request: Req, - options: Options, -) => Res | Promise; +type ResInterceptor = (response: Res, request: Req, options: Options) => Res | Promise; class Interceptors { fns: Array = []; @@ -275,10 +235,7 @@ class Interceptors { return this.fns.indexOf(id); } - update( - id: number | Interceptor, - fn: Interceptor, - ): number | Interceptor | false { + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { const index = this.getInterceptorIndex(id); if (this.fns[index]) { this.fns[index] = fn; @@ -299,12 +256,7 @@ export interface Middleware { response: Interceptors>; } -export const createInterceptors = (): Middleware< - Req, - Res, - Err, - Options -> => ({ +export const createInterceptors = (): Middleware => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/app/client/api-client/core/auth.gen.ts b/app/client/api-client/core/auth.gen.ts index 48eb5109..9d804052 100644 --- a/app/client/api-client/core/auth.gen.ts +++ b/app/client/api-client/core/auth.gen.ts @@ -23,8 +23,7 @@ export const getAuthToken = async ( auth: Auth, callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, ): Promise => { - const token = - typeof callback === "function" ? await callback(auth) : callback; + const token = typeof callback === "function" ? await callback(auth) : callback; if (!token) { return; diff --git a/app/client/api-client/core/bodySerializer.gen.ts b/app/client/api-client/core/bodySerializer.gen.ts index 80771d70..10e7b3a6 100644 --- a/app/client/api-client/core/bodySerializer.gen.ts +++ b/app/client/api-client/core/bodySerializer.gen.ts @@ -1,10 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { - ArrayStyle, - ObjectStyle, - SerializerOptions, -} from "./pathSerializer.gen"; +import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.gen"; export type QuerySerializer = (query: Record) => string; @@ -24,11 +20,7 @@ export type QuerySerializerOptions = QuerySerializerOptionsObject & { parameters?: Record; }; -const serializeFormDataPair = ( - data: FormData, - key: string, - value: unknown, -): void => { +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { if (typeof value === "string" || value instanceof Blob) { data.append(key, value); } else if (value instanceof Date) { @@ -38,11 +30,7 @@ const serializeFormDataPair = ( } }; -const serializeUrlSearchParamsPair = ( - data: URLSearchParams, - key: string, - value: unknown, -): void => { +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { if (typeof value === "string") { data.append(key, value); } else { @@ -51,9 +39,7 @@ const serializeUrlSearchParamsPair = ( }; export const formDataBodySerializer = { - bodySerializer: | Array>>( - body: T, - ): FormData => { + bodySerializer: | Array>>(body: T): FormData => { const data = new FormData(); Object.entries(body).forEach(([key, value]) => { @@ -73,15 +59,11 @@ export const formDataBodySerializer = { export const jsonBodySerializer = { bodySerializer: (body: T): string => - JSON.stringify(body, (_key, value) => - typeof value === "bigint" ? value.toString() : value, - ), + JSON.stringify(body, (_key, value) => (typeof value === "bigint" ? value.toString() : value)), }; export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>( - body: T, - ): string => { + bodySerializer: | Array>>(body: T): string => { const data = new URLSearchParams(); Object.entries(body).forEach(([key, value]) => { diff --git a/app/client/api-client/core/params.gen.ts b/app/client/api-client/core/params.gen.ts index 8cfe5a59..c1fb15c5 100644 --- a/app/client/api-client/core/params.gen.ts +++ b/app/client/api-client/core/params.gen.ts @@ -102,10 +102,7 @@ const stripEmptySlots = (params: Params) => { } }; -export const buildClientParams = ( - args: ReadonlyArray, - fields: FieldsConfig, -) => { +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { const params: Params = { body: {}, headers: {}, @@ -148,15 +145,11 @@ export const buildClientParams = ( params[field.map] = value; } } else { - const extra = extraPrefixes.find(([prefix]) => - key.startsWith(prefix), - ); + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); if (extra) { const [prefix, slot] = extra; - (params[slot] as Record)[ - key.slice(prefix.length) - ] = value; + (params[slot] as Record)[key.slice(prefix.length)] = value; } else if ("allowExtra" in config && config.allowExtra) { for (const [slot, allowed] of Object.entries(config.allowExtra)) { if (allowed) { diff --git a/app/client/api-client/core/pathSerializer.gen.ts b/app/client/api-client/core/pathSerializer.gen.ts index 2fe930d9..9e3cccde 100644 --- a/app/client/api-client/core/pathSerializer.gen.ts +++ b/app/client/api-client/core/pathSerializer.gen.ts @@ -1,8 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -interface SerializeOptions - extends SerializePrimitiveOptions, - SerializerOptions {} +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} interface SerializePrimitiveOptions { allowReserved?: boolean; @@ -76,9 +74,9 @@ export const serializeArrayParam = ({ value: unknown[]; }) => { if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); + const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v as string))).join( + separatorArrayNoExplode(style), + ); switch (style) { case "label": return `.${joinedValues}`; @@ -105,16 +103,10 @@ export const serializeArrayParam = ({ }); }) .join(separator); - return style === "label" || style === "matrix" - ? separator + joinedValues - : joinedValues; + return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues; }; -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { +export const serializePrimitiveParam = ({ allowReserved, name, value }: SerializePrimitiveParam) => { if (value === undefined || value === null) { return ""; } @@ -146,11 +138,7 @@ export const serializeObjectParam = ({ if (style !== "deepObject" && !explode) { let values: string[] = []; Object.entries(value).forEach(([key, v]) => { - values = [ - ...values, - key, - allowReserved ? (v as string) : encodeURIComponent(v as string), - ]; + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; }); const joinedValues = values.join(","); switch (style) { @@ -175,7 +163,5 @@ export const serializeObjectParam = ({ }), ) .join(separator); - return style === "label" || style === "matrix" - ? separator + joinedValues - : joinedValues; + return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues; }; diff --git a/app/client/api-client/core/queryKeySerializer.gen.ts b/app/client/api-client/core/queryKeySerializer.gen.ts index 2e8e7aab..b3e87676 100644 --- a/app/client/api-client/core/queryKeySerializer.gen.ts +++ b/app/client/api-client/core/queryKeySerializer.gen.ts @@ -3,23 +3,13 @@ /** * JSON-friendly union that mirrors what Pinia Colada can hash. */ -export type JsonValue = - | null - | string - | number - | boolean - | JsonValue[] - | { [key: string]: JsonValue }; +export type JsonValue = null | string | number | boolean | JsonValue[] | { [key: string]: JsonValue }; /** * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. */ export const queryKeyJsonReplacer = (_key: string, value: unknown) => { - if ( - value === undefined || - typeof value === "function" || - typeof value === "symbol" - ) { + if (value === undefined || typeof value === "function" || typeof value === "symbol") { return undefined; } if (typeof value === "bigint") { @@ -61,9 +51,7 @@ const isPlainObject = (value: unknown): value is Record => { * Turns URLSearchParams into a sorted JSON object for deterministic keys. */ const serializeSearchParams = (params: URLSearchParams): JsonValue => { - const entries = Array.from(params.entries()).sort(([a], [b]) => - a.localeCompare(b), - ); + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); const result: Record = {}; for (const [key, value] of entries) { @@ -86,26 +74,16 @@ const serializeSearchParams = (params: URLSearchParams): JsonValue => { /** * Normalizes any accepted value into a JSON-friendly shape for query keys. */ -export const serializeQueryKeyValue = ( - value: unknown, -): JsonValue | undefined => { +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { if (value === null) { return null; } - if ( - typeof value === "string" || - typeof value === "number" || - typeof value === "boolean" - ) { + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { return value; } - if ( - value === undefined || - typeof value === "function" || - typeof value === "symbol" - ) { + if (value === undefined || typeof value === "function" || typeof value === "symbol") { return undefined; } @@ -121,10 +99,7 @@ export const serializeQueryKeyValue = ( return stringifyToJsonValue(value); } - if ( - typeof URLSearchParams !== "undefined" && - value instanceof URLSearchParams - ) { + if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) { return serializeSearchParams(value); } diff --git a/app/client/api-client/core/serverSentEvents.gen.ts b/app/client/api-client/core/serverSentEvents.gen.ts index 8dfe6a5c..479546e8 100644 --- a/app/client/api-client/core/serverSentEvents.gen.ts +++ b/app/client/api-client/core/serverSentEvents.gen.ts @@ -2,10 +2,7 @@ import type { Config } from "./types.gen"; -export type ServerSentEventsOptions = Omit< - RequestInit, - "method" -> & +export type ServerSentEventsOptions = Omit & Pick & { /** * Fetch API implementation. You can use this option to provide a custom @@ -74,16 +71,8 @@ export interface StreamEvent { retry?: number; } -export type ServerSentEventsResult< - TData = unknown, - TReturn = void, - TNext = unknown, -> = { - stream: AsyncGenerator< - TData extends Record ? TData[keyof TData] : TData, - TReturn, - TNext - >; +export type ServerSentEventsResult = { + stream: AsyncGenerator ? TData[keyof TData] : TData, TReturn, TNext>; }; export const createSseClient = ({ @@ -101,9 +90,7 @@ export const createSseClient = ({ }: ServerSentEventsOptions): ServerSentEventsResult => { let lastEventId: string | undefined; - const sleep = - sseSleepFn ?? - ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); const createStream = async function* () { let retryDelay: number = sseDefaultRetryDelay ?? 3000; @@ -141,16 +128,11 @@ export const createSseClient = ({ const _fetch = options.fetch ?? globalThis.fetch; const response = await _fetch(request); - if (!response.ok) - throw new Error( - `SSE failed: ${response.status} ${response.statusText}`, - ); + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); if (!response.body) throw new Error("No body in SSE response"); - const reader = response.body - .pipeThrough(new TextDecoderStream()) - .getReader(); + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); let buffer = ""; @@ -188,10 +170,7 @@ export const createSseClient = ({ } else if (line.startsWith("id:")) { lastEventId = line.replace(/^id:\s*/, ""); } else if (line.startsWith("retry:")) { - const parsed = Number.parseInt( - line.replace(/^retry:\s*/, ""), - 10, - ); + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10); if (!Number.isNaN(parsed)) { retryDelay = parsed; } @@ -243,18 +222,12 @@ export const createSseClient = ({ // connection failed or aborted; retry after delay onSseError?.(error); - if ( - sseMaxRetryAttempts !== undefined && - attempt >= sseMaxRetryAttempts - ) { + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { break; // stop after firing error } // exponential backoff: double retry each attempt, cap at 30s - const backoff = Math.min( - retryDelay * 2 ** (attempt - 1), - sseMaxRetryDelay ?? 30000, - ); + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); await sleep(backoff); } } diff --git a/app/client/api-client/core/types.gen.ts b/app/client/api-client/core/types.gen.ts index 8db8c934..62252e53 100644 --- a/app/client/api-client/core/types.gen.ts +++ b/app/client/api-client/core/types.gen.ts @@ -1,30 +1,11 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Auth, AuthToken } from "./auth.gen"; -import type { - BodySerializer, - QuerySerializer, - QuerySerializerOptions, -} from "./bodySerializer.gen"; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen"; -export type HttpMethod = - | "connect" - | "delete" - | "get" - | "head" - | "options" - | "patch" - | "post" - | "put" - | "trace"; +export type HttpMethod = "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace"; -export type Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, - SseFn = never, -> = { +export type Client = { /** * Returns the final request URL. */ @@ -34,9 +15,7 @@ export type Client< setConfig: (config: Config) => Config; } & { [K in HttpMethod]: MethodFn; -} & ([SseFn] extends [never] - ? { sse?: never } - : { sse: { [K in HttpMethod]: SseFn } }); +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -57,16 +36,7 @@ export interface Config { */ headers?: | RequestInit["headers"] - | Record< - string, - | string - | number - | boolean - | (string | number | boolean)[] - | null - | undefined - | unknown - >; + | Record; /** * The request method. * @@ -112,7 +82,5 @@ type IsExactlyNeverOrNeverUndefined = [T] extends [never] : false; export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true - ? never - : K]: T[K]; + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; }; diff --git a/app/client/api-client/core/utils.gen.ts b/app/client/api-client/core/utils.gen.ts index 999c4a36..7e48839f 100644 --- a/app/client/api-client/core/utils.gen.ts +++ b/app/client/api-client/core/utils.gen.ts @@ -44,10 +44,7 @@ export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { } if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); continue; } @@ -76,9 +73,7 @@ export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { continue; } - const replaceValue = encodeURIComponent( - style === "label" ? `.${value as string}` : (value as string), - ); + const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string)); url = url.replace(match, replaceValue); } } @@ -123,8 +118,7 @@ export function getValidRequestBody(options: { if (isSerializedBody) { if ("serializedBody" in options) { - const hasSerializedBody = - options.serializedBody !== undefined && options.serializedBody !== ""; + const hasSerializedBody = options.serializedBody !== undefined && options.serializedBody !== ""; return hasSerializedBody ? options.serializedBody : null; } diff --git a/app/client/api-client/sdk.gen.ts b/app/client/api-client/sdk.gen.ts index 5d28a09c..835080bd 100644 --- a/app/client/api-client/sdk.gen.ts +++ b/app/client/api-client/sdk.gen.ts @@ -114,10 +114,10 @@ import type { UpdateVolumeResponses, } from "./types.gen"; -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, -> = Options2 & { +export type Options = Options2< + TData, + ThrowOnError +> & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -134,9 +134,7 @@ export type Options< /** * Get authentication system status */ -export const getStatus = ( - options?: Options, -) => +export const getStatus = (options?: Options) => (options?.client ?? client).get({ url: "/api/v1/auth/status", ...options, @@ -145,25 +143,14 @@ export const getStatus = ( /** * List all volumes */ -export const listVolumes = ( - options?: Options, -) => - (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, -) => - (options?.client ?? client).post< - CreateVolumeResponses, - unknown, - ThrowOnError - >({ +export const createVolume = (options?: Options) => + (options?.client ?? client).post({ url: "/api/v1/volumes", ...options, headers: { @@ -178,11 +165,7 @@ export const createVolume = ( export const testConnection = ( options?: Options, ) => - (options?.client ?? client).post< - TestConnectionResponses, - unknown, - ThrowOnError - >({ + (options?.client ?? client).post({ url: "/api/v1/volumes/test-connection", ...options, headers: { @@ -194,38 +177,26 @@ export const testConnection = ( /** * Delete a volume */ -export const deleteVolume = ( - options: Options, -) => - (options.client ?? client).delete< - DeleteVolumeResponses, - unknown, - ThrowOnError - >({ 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, -) => - (options.client ?? client).get< - GetVolumeResponses, - GetVolumeErrors, - ThrowOnError - >({ 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, -) => - (options.client ?? client).put< - UpdateVolumeResponses, - UpdateVolumeErrors, - ThrowOnError - >({ +export const updateVolume = (options: Options) => + (options.client ?? client).put({ url: "/api/v1/volumes/{name}", ...options, headers: { @@ -237,9 +208,7 @@ export const updateVolume = ( /** * Mount a volume */ -export const mountVolume = ( - options: Options, -) => +export const mountVolume = (options: Options) => (options.client ?? client).post({ url: "/api/v1/volumes/{name}/mount", ...options, @@ -251,11 +220,10 @@ export const mountVolume = ( export const unmountVolume = ( options: Options, ) => - (options.client ?? client).post< - UnmountVolumeResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/volumes/{name}/unmount", ...options }); + (options.client ?? client).post({ + url: "/api/v1/volumes/{name}/unmount", + ...options, + }); /** * Perform a health check on a volume @@ -263,18 +231,15 @@ export const unmountVolume = ( export const healthCheckVolume = ( options: Options, ) => - (options.client ?? client).post< - HealthCheckVolumeResponses, - HealthCheckVolumeErrors, - ThrowOnError - >({ url: "/api/v1/volumes/{name}/health-check", ...options }); + (options.client ?? client).post({ + url: "/api/v1/volumes/{name}/health-check", + ...options, + }); /** * List files in a volume directory */ -export const listFiles = ( - options: Options, -) => +export const listFiles = (options: Options) => (options.client ?? client).get({ url: "/api/v1/volumes/{name}/files", ...options, @@ -286,11 +251,10 @@ export const listFiles = ( export const browseFilesystem = ( options?: Options, ) => - (options?.client ?? client).get< - BrowseFilesystemResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/volumes/filesystem/browse", ...options }); + (options?.client ?? client).get({ + url: "/api/v1/volumes/filesystem/browse", + ...options, + }); /** * List all repositories @@ -298,11 +262,10 @@ export const browseFilesystem = ( export const listRepositories = ( options?: Options, ) => - (options?.client ?? client).get< - ListRepositoriesResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/repositories", ...options }); + (options?.client ?? client).get({ + url: "/api/v1/repositories", + ...options, + }); /** * Create a new restic repository @@ -310,11 +273,7 @@ export const listRepositories = ( export const createRepository = ( options?: Options, ) => - (options?.client ?? client).post< - CreateRepositoryResponses, - unknown, - ThrowOnError - >({ + (options?.client ?? client).post({ url: "/api/v1/repositories", ...options, headers: { @@ -329,11 +288,10 @@ export const createRepository = ( export const listRcloneRemotes = ( options?: Options, ) => - (options?.client ?? client).get< - ListRcloneRemotesResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/repositories/rclone-remotes", ...options }); + (options?.client ?? client).get({ + url: "/api/v1/repositories/rclone-remotes", + ...options, + }); /** * Delete a repository @@ -341,11 +299,10 @@ export const listRcloneRemotes = ( export const deleteRepository = ( options: Options, ) => - (options.client ?? client).delete< - DeleteRepositoryResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/repositories/{id}", ...options }); + (options.client ?? client).delete({ + url: "/api/v1/repositories/{id}", + ...options, + }); /** * Get a single repository by ID @@ -353,9 +310,10 @@ export const deleteRepository = ( export const getRepository = ( options: Options, ) => - (options.client ?? client).get( - { url: "/api/v1/repositories/{id}", ...options }, - ); + (options.client ?? client).get({ + url: "/api/v1/repositories/{id}", + ...options, + }); /** * Update a repository's name or settings @@ -363,11 +321,7 @@ export const getRepository = ( export const updateRepository = ( options: Options, ) => - (options.client ?? client).patch< - UpdateRepositoryResponses, - UpdateRepositoryErrors, - ThrowOnError - >({ + (options.client ?? client).patch({ url: "/api/v1/repositories/{id}", ...options, headers: { @@ -382,11 +336,7 @@ export const updateRepository = ( export const deleteSnapshots = ( options: Options, ) => - (options.client ?? client).delete< - DeleteSnapshotsResponses, - unknown, - ThrowOnError - >({ + (options.client ?? client).delete({ url: "/api/v1/repositories/{id}/snapshots", ...options, headers: { @@ -401,9 +351,10 @@ export const deleteSnapshots = ( export const listSnapshots = ( options: Options, ) => - (options.client ?? client).get( - { url: "/api/v1/repositories/{id}/snapshots", ...options }, - ); + (options.client ?? client).get({ + url: "/api/v1/repositories/{id}/snapshots", + ...options, + }); /** * Delete a specific snapshot from a repository @@ -411,11 +362,10 @@ export const listSnapshots = ( export const deleteSnapshot = ( options: Options, ) => - (options.client ?? client).delete< - DeleteSnapshotResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/repositories/{id}/snapshots/{snapshotId}", ...options }); + (options.client ?? client).delete({ + url: "/api/v1/repositories/{id}/snapshots/{snapshotId}", + ...options, + }); /** * Get details of a specific snapshot @@ -423,11 +373,10 @@ export const deleteSnapshot = ( export const getSnapshotDetails = ( options: Options, ) => - (options.client ?? client).get< - GetSnapshotDetailsResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/repositories/{id}/snapshots/{snapshotId}", ...options }); + (options.client ?? client).get({ + url: "/api/v1/repositories/{id}/snapshots/{snapshotId}", + ...options, + }); /** * List files and directories in a snapshot @@ -435,11 +384,7 @@ export const getSnapshotDetails = ( export const listSnapshotFiles = ( options: Options, ) => - (options.client ?? client).get< - ListSnapshotFilesResponses, - unknown, - ThrowOnError - >({ + (options.client ?? client).get({ url: "/api/v1/repositories/{id}/snapshots/{snapshotId}/files", ...options, }); @@ -450,11 +395,7 @@ export const listSnapshotFiles = ( export const restoreSnapshot = ( options: Options, ) => - (options.client ?? client).post< - RestoreSnapshotResponses, - unknown, - ThrowOnError - >({ + (options.client ?? client).post({ url: "/api/v1/repositories/{id}/restore", ...options, headers: { @@ -469,28 +410,23 @@ export const restoreSnapshot = ( export const doctorRepository = ( options: Options, ) => - (options.client ?? client).post< - DoctorRepositoryResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/repositories/{id}/doctor", ...options }); + (options.client ?? client).post({ + url: "/api/v1/repositories/{id}/doctor", + ...options, + }); /** * Tag multiple snapshots in a repository */ -export const tagSnapshots = ( - options: Options, -) => - (options.client ?? client).post( - { - url: "/api/v1/repositories/{id}/snapshots/tag", - ...options, - headers: { - "Content-Type": "application/json", - ...options.headers, - }, +export const tagSnapshots = (options: Options) => + (options.client ?? client).post({ + url: "/api/v1/repositories/{id}/snapshots/tag", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, }, - ); + }); /** * List all backup schedules @@ -498,11 +434,10 @@ export const tagSnapshots = ( export const listBackupSchedules = ( options?: Options, ) => - (options?.client ?? client).get< - ListBackupSchedulesResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/backups", ...options }); + (options?.client ?? client).get({ + url: "/api/v1/backups", + ...options, + }); /** * Create a new backup schedule for a volume @@ -510,11 +445,7 @@ export const listBackupSchedules = ( export const createBackupSchedule = ( options?: Options, ) => - (options?.client ?? client).post< - CreateBackupScheduleResponses, - unknown, - ThrowOnError - >({ + (options?.client ?? client).post({ url: "/api/v1/backups", ...options, headers: { @@ -529,11 +460,10 @@ export const createBackupSchedule = ( export const deleteBackupSchedule = ( options: Options, ) => - (options.client ?? client).delete< - DeleteBackupScheduleResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/backups/{scheduleId}", ...options }); + (options.client ?? client).delete({ + url: "/api/v1/backups/{scheduleId}", + ...options, + }); /** * Get a backup schedule by ID @@ -541,11 +471,10 @@ export const deleteBackupSchedule = ( export const getBackupSchedule = ( options: Options, ) => - (options.client ?? client).get< - GetBackupScheduleResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/backups/{scheduleId}", ...options }); + (options.client ?? client).get({ + url: "/api/v1/backups/{scheduleId}", + ...options, + }); /** * Update a backup schedule @@ -553,11 +482,7 @@ export const getBackupSchedule = ( export const updateBackupSchedule = ( options: Options, ) => - (options.client ?? client).patch< - UpdateBackupScheduleResponses, - unknown, - ThrowOnError - >({ + (options.client ?? client).patch({ url: "/api/v1/backups/{scheduleId}", ...options, headers: { @@ -569,45 +494,36 @@ export const updateBackupSchedule = ( /** * Get a backup schedule for a specific volume */ -export const getBackupScheduleForVolume = < - ThrowOnError extends boolean = false, ->( +export const getBackupScheduleForVolume = ( options: Options, ) => - (options.client ?? client).get< - GetBackupScheduleForVolumeResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/backups/volume/{volumeId}", ...options }); + (options.client ?? client).get({ + url: "/api/v1/backups/volume/{volumeId}", + ...options, + }); /** * Trigger a backup immediately for a schedule */ -export const runBackupNow = ( - options: Options, -) => - (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, -) => - (options.client ?? client).post< - StopBackupResponses, - StopBackupErrors, - ThrowOnError - >({ 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, -) => +export const runForget = (options: Options) => (options.client ?? client).post({ url: "/api/v1/backups/{scheduleId}/forget", ...options, @@ -619,25 +535,18 @@ export const runForget = ( export const getScheduleNotifications = ( options: Options, ) => - (options.client ?? client).get< - GetScheduleNotificationsResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/backups/{scheduleId}/notifications", ...options }); + (options.client ?? client).get({ + url: "/api/v1/backups/{scheduleId}/notifications", + ...options, + }); /** * Update notification assignments for a backup schedule */ -export const updateScheduleNotifications = < - ThrowOnError extends boolean = false, ->( +export const updateScheduleNotifications = ( options: Options, ) => - (options.client ?? client).put< - UpdateScheduleNotificationsResponses, - unknown, - ThrowOnError - >({ + (options.client ?? client).put({ url: "/api/v1/backups/{scheduleId}/notifications", ...options, headers: { @@ -652,11 +561,10 @@ export const updateScheduleNotifications = < export const getScheduleMirrors = ( options: Options, ) => - (options.client ?? client).get< - GetScheduleMirrorsResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/backups/{scheduleId}/mirrors", ...options }); + (options.client ?? client).get({ + url: "/api/v1/backups/{scheduleId}/mirrors", + ...options, + }); /** * Update mirror repository assignments for a backup schedule @@ -664,11 +572,7 @@ export const getScheduleMirrors = ( export const updateScheduleMirrors = ( options: Options, ) => - (options.client ?? client).put< - UpdateScheduleMirrorsResponses, - unknown, - ThrowOnError - >({ + (options.client ?? client).put({ url: "/api/v1/backups/{scheduleId}/mirrors", ...options, headers: { @@ -683,11 +587,10 @@ export const updateScheduleMirrors = ( export const getMirrorCompatibility = ( options: Options, ) => - (options.client ?? client).get< - GetMirrorCompatibilityResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/backups/{scheduleId}/mirrors/compatibility", ...options }); + (options.client ?? client).get({ + url: "/api/v1/backups/{scheduleId}/mirrors/compatibility", + ...options, + }); /** * Reorder backup schedules by providing an array of schedule IDs in the desired order @@ -695,11 +598,7 @@ export const getMirrorCompatibility = ( export const reorderBackupSchedules = ( options?: Options, ) => - (options?.client ?? client).post< - ReorderBackupSchedulesResponses, - unknown, - ThrowOnError - >({ + (options?.client ?? client).post({ url: "/api/v1/backups/reorder", ...options, headers: { @@ -711,30 +610,21 @@ export const reorderBackupSchedules = ( /** * List all notification destinations */ -export const listNotificationDestinations = < - ThrowOnError extends boolean = false, ->( +export const listNotificationDestinations = ( options?: Options, ) => - (options?.client ?? client).get< - ListNotificationDestinationsResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/notifications/destinations", ...options }); + (options?.client ?? client).get({ + url: "/api/v1/notifications/destinations", + ...options, + }); /** * Create a new notification destination */ -export const createNotificationDestination = < - ThrowOnError extends boolean = false, ->( +export const createNotificationDestination = ( options?: Options, ) => - (options?.client ?? client).post< - CreateNotificationDestinationResponses, - unknown, - ThrowOnError - >({ + (options?.client ?? client).post({ url: "/api/v1/notifications/destinations", ...options, headers: { @@ -746,9 +636,7 @@ export const createNotificationDestination = < /** * Delete a notification destination */ -export const deleteNotificationDestination = < - ThrowOnError extends boolean = false, ->( +export const deleteNotificationDestination = ( options: Options, ) => (options.client ?? client).delete< @@ -760,23 +648,18 @@ export const deleteNotificationDestination = < /** * Get a notification destination by ID */ -export const getNotificationDestination = < - ThrowOnError extends boolean = false, ->( +export const getNotificationDestination = ( options: Options, ) => - (options.client ?? client).get< - GetNotificationDestinationResponses, - GetNotificationDestinationErrors, - ThrowOnError - >({ url: "/api/v1/notifications/destinations/{id}", ...options }); + (options.client ?? client).get({ + url: "/api/v1/notifications/destinations/{id}", + ...options, + }); /** * Update a notification destination */ -export const updateNotificationDestination = < - ThrowOnError extends boolean = false, ->( +export const updateNotificationDestination = ( options: Options, ) => (options.client ?? client).patch< @@ -795,9 +678,7 @@ export const updateNotificationDestination = < /** * Test a notification destination by sending a test message */ -export const testNotificationDestination = < - ThrowOnError extends boolean = false, ->( +export const testNotificationDestination = ( options: Options, ) => (options.client ?? client).post< @@ -812,18 +693,15 @@ export const testNotificationDestination = < export const getSystemInfo = ( options?: Options, ) => - (options?.client ?? client).get< - GetSystemInfoResponses, - unknown, - ThrowOnError - >({ url: "/api/v1/system/info", ...options }); + (options?.client ?? client).get({ + url: "/api/v1/system/info", + ...options, + }); /** * Check for application updates from GitHub */ -export const getUpdates = ( - options?: Options, -) => +export const getUpdates = (options?: Options) => (options?.client ?? client).get({ url: "/api/v1/system/updates", ...options, @@ -835,11 +713,7 @@ export const getUpdates = ( export const downloadResticPassword = ( options?: Options, ) => - (options?.client ?? client).post< - DownloadResticPasswordResponses, - unknown, - ThrowOnError - >({ + (options?.client ?? client).post({ url: "/api/v1/system/restic-password", ...options, headers: { diff --git a/bun.lock b/bun.lock index 2be3d8a8..ccb8aad2 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "@dnd-kit/utilities": "^3.2.2", "@hono/standard-validator": "^0.2.0", "@hookform/resolvers": "^5.2.2", - "@inquirer/prompts": "^8.0.2", + "@inquirer/prompts": "^8.2.0", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", @@ -30,7 +30,7 @@ "@scalar/hono-api-reference": "^0.9.32", "@tanstack/react-query": "^5.90.16", "arktype": "^2.1.28", - "better-auth": "^1.4.10", + "better-auth": "^1.4.12", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "commander": "^14.0.2", @@ -52,7 +52,7 @@ "qrcode.react": "^4.2.0", "react": "^19.2.1", "react-dom": "^19.2.1", - "react-hook-form": "^7.70.0", + "react-hook-form": "^7.71.0", "react-markdown": "^10.1.0", "react-router": "^7.12.0", "react-router-hono-server": "^2.22.0", @@ -69,7 +69,7 @@ "devDependencies": { "@faker-js/faker": "^10.2.0", "@happy-dom/global-registrator": "^20.1.0", - "@hey-api/openapi-ts": "^0.90.2", + "@hey-api/openapi-ts": "^0.90.3", "@libsql/client": "^0.17.0", "@playwright/test": "^1.57.0", "@react-router/dev": "^7.12.0", @@ -79,15 +79,15 @@ "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.1", "@types/bun": "^1.3.4", - "@types/node": "^25.0.3", - "@types/react": "^19.2.7", + "@types/node": "^25.0.7", + "@types/react": "^19.2.8", "@types/react-dom": "^19.2.3", "@types/semver": "^7.7.1", "dotenv-cli": "^11.0.0", "drizzle-kit": "^0.31.7", "lightningcss": "^1.30.2", - "oxfmt": "^0.23.0", - "oxlint": "^1.38.0", + "oxfmt": "^0.24.0", + "oxlint": "^1.39.0", "oxlint-tsgolint": "^0.11.0", "tailwindcss": "^4.1.17", "tinyglobby": "^0.2.15", @@ -95,7 +95,7 @@ "typescript": "^5.9.3", "vite": "^7.3.1", "vite-bundle-analyzer": "^1.2.3", - "vite-tsconfig-paths": "^6.0.3", + "vite-tsconfig-paths": "^6.0.4", }, }, }, @@ -166,9 +166,9 @@ "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - "@better-auth/core": ["@better-auth/core@1.4.10", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-AThrfb6CpG80wqkanfrbN2/fGOYzhGladHFf3JhaWt/3/Vtf4h084T6PJLrDE7M/vCCGYvDI1DkvP3P1OB2HAg=="], + "@better-auth/core": ["@better-auth/core@1.4.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-VfqZwMAEl9rnGx092BIZ2Q5z8rt7jjN2OAbvPqehufSKZGmh8JsdtZRBMl/CHQir9bwi2Ev0UF4+7TQp+DXEMg=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.4.10", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.10" } }, "sha512-Dq4XJX6EKsUu0h3jpRagX739p/VMOTcnJYWRrLtDYkqtZFg+sFiFsSWVcfapZoWpRSUGYX9iKwl6nDHn6Ju2oQ=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.4.12", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.12" } }, "sha512-4q504Og42PzkUbZjXDt+FyeYaS0WZmAlEOC3nbBCZDObTVCRUnGgJW52B2maJ7BCVvAQgBGLEeQmQzU5+63J0A=="], "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], @@ -260,7 +260,7 @@ "@hey-api/json-schema-ref-parser": ["@hey-api/json-schema-ref-parser@1.2.2", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.1", "lodash": "^4.17.21" } }, "sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA=="], - "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.90.2", "", { "dependencies": { "@hey-api/codegen-core": "^0.5.2", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-wfqLHxlRkyowHa88X1+iD46SIgf8HEUH7v+nec00Yaya4fzzBCkOI30f7nlI+/4/EQtVmjzjnr1bthOzYHF8BQ=="], + "@hey-api/openapi-ts": ["@hey-api/openapi-ts@0.90.3", "", { "dependencies": { "@hey-api/codegen-core": "^0.5.2", "@hey-api/json-schema-ref-parser": "1.2.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3", "commander": "14.0.2", "open": "11.0.0", "semver": "7.7.3" }, "peerDependencies": { "typescript": ">=5.5.3" }, "bin": { "openapi-ts": "bin/run.js" } }, "sha512-9UT347hVQaph/jwAJJI2l+istWDLw540PTIHUevq9xzRYnHAxC1Aprtv4CYoWkeFv9aMcMrmAbNH7ANPoxEK2A=="], "@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="], @@ -272,37 +272,37 @@ "@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=="], - "@inquirer/ansi": ["@inquirer/ansi@2.0.2", "", {}, "sha512-SYLX05PwJVnW+WVegZt1T4Ip1qba1ik+pNJPDiqvk6zS5Y/i8PhRzLpGEtVd7sW0G8cMtkD8t4AZYhQwm8vnww=="], + "@inquirer/ansi": ["@inquirer/ansi@2.0.3", "", {}, "sha512-g44zhR3NIKVs0zUesa4iMzExmZpLUdTLRMCStqX3GE5NT6VkPcxQGJ+uC8tDgBUC/vB1rUhUd55cOf++4NZcmw=="], - "@inquirer/checkbox": ["@inquirer/checkbox@5.0.3", "", { "dependencies": { "@inquirer/ansi": "^2.0.2", "@inquirer/core": "^11.1.0", "@inquirer/figures": "^2.0.2", "@inquirer/type": "^4.0.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-xtQP2eXMFlOcAhZ4ReKP2KZvDIBb1AnCfZ81wWXG3DXLVH0f0g4obE0XDPH+ukAEMRcZT0kdX2AS1jrWGXbpxw=="], + "@inquirer/checkbox": ["@inquirer/checkbox@5.0.4", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/core": "^11.1.1", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-DrAMU3YBGMUAp6ArwTIp/25CNDtDbxk7UjIrrtM25JVVrlVYlVzHh5HR1BDFu9JMyUoZ4ZanzeaHqNDttf3gVg=="], - "@inquirer/confirm": ["@inquirer/confirm@6.0.3", "", { "dependencies": { "@inquirer/core": "^11.1.0", "@inquirer/type": "^4.0.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-lyEvibDFL+NA5R4xl8FUmNhmu81B+LDL9L/MpKkZlQDJZXzG8InxiqYxiAlQYa9cqLLhYqKLQwZqXmSTqCLjyw=="], + "@inquirer/confirm": ["@inquirer/confirm@6.0.4", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-WdaPe7foUnoGYvXzH4jp4wH/3l+dBhZ3uwhKjXjwdrq5tEIFaANxj6zrGHxLdsIA0yKM0kFPVcEalOZXBB5ISA=="], - "@inquirer/core": ["@inquirer/core@11.1.0", "", { "dependencies": { "@inquirer/ansi": "^2.0.2", "@inquirer/figures": "^2.0.2", "@inquirer/type": "^4.0.2", "cli-width": "^4.1.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^9.0.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-+jD/34T1pK8M5QmZD/ENhOfXdl9Zr+BrQAUc5h2anWgi7gggRq15ZbiBeLoObj0TLbdgW7TAIQRU2boMc9uOKQ=="], + "@inquirer/core": ["@inquirer/core@11.1.1", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3", "cli-width": "^4.1.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^9.0.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-hV9o15UxX46OyQAtaoMqAOxGR8RVl1aZtDx1jHbCtSJy1tBdTfKxLPKf7utsE4cRy4tcmCQ4+vdV+ca+oNxqNA=="], - "@inquirer/editor": ["@inquirer/editor@5.0.3", "", { "dependencies": { "@inquirer/core": "^11.1.0", "@inquirer/external-editor": "^2.0.2", "@inquirer/type": "^4.0.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-wYyQo96TsAqIciP/r5D3cFeV8h4WqKQ/YOvTg5yOfP2sqEbVVpbxPpfV3LM5D0EP4zUI3EZVHyIUIllnoIa8OQ=="], + "@inquirer/editor": ["@inquirer/editor@5.0.4", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/external-editor": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QI3Jfqcv6UO2/VJaEFONH8Im1ll++Xn/AJTBn9Xf+qx2M+H8KZAdQ5sAe2vtYlo+mLW+d7JaMJB4qWtK4BG3pw=="], - "@inquirer/expand": ["@inquirer/expand@5.0.3", "", { "dependencies": { "@inquirer/core": "^11.1.0", "@inquirer/type": "^4.0.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-2oINvuL27ujjxd95f6K2K909uZOU2x1WiAl7Wb1X/xOtL8CgQ1kSxzykIr7u4xTkXkXOAkCuF45T588/YKee7w=="], + "@inquirer/expand": ["@inquirer/expand@5.0.4", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-0I/16YwPPP0Co7a5MsomlZLpch48NzYfToyqYAOWtBmaXSB80RiNQ1J+0xx2eG+Wfxt0nHtpEWSRr6CzNVnOGg=="], - "@inquirer/external-editor": ["@inquirer/external-editor@2.0.2", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-X/fMXK7vXomRWEex1j8mnj7s1mpnTeP4CO/h2gysJhHLT2WjBnLv4ZQEGpm/kcYI8QfLZ2fgW+9kTKD+jeopLg=="], + "@inquirer/external-editor": ["@inquirer/external-editor@2.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-LgyI7Agbda74/cL5MvA88iDpvdXI2KuMBCGRkbCl2Dg1vzHeOgs+s0SDcXV7b+WZJrv2+ERpWSM65Fpi9VfY3w=="], - "@inquirer/figures": ["@inquirer/figures@2.0.2", "", {}, "sha512-qXm6EVvQx/FmnSrCWCIGtMHwqeLgxABP8XgcaAoywsL0NFga9gD5kfG0gXiv80GjK9Hsoz4pgGwF/+CjygyV9A=="], + "@inquirer/figures": ["@inquirer/figures@2.0.3", "", {}, "sha512-y09iGt3JKoOCBQ3w4YrSJdokcD8ciSlMIWsD+auPu+OZpfxLuyz+gICAQ6GCBOmJJt4KEQGHuZSVff2jiNOy7g=="], - "@inquirer/input": ["@inquirer/input@5.0.3", "", { "dependencies": { "@inquirer/core": "^11.1.0", "@inquirer/type": "^4.0.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-4R0TdWl53dtp79Vs6Df2OHAtA2FVNqya1hND1f5wjHWxZJxwDMSNB1X5ADZJSsQKYAJ5JHCTO+GpJZ42mK0Otw=="], + "@inquirer/input": ["@inquirer/input@5.0.4", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-4B3s3jvTREDFvXWit92Yc6jF1RJMDy2VpSqKtm4We2oVU65YOh2szY5/G14h4fHlyQdpUmazU5MPCFZPRJ0AOw=="], - "@inquirer/number": ["@inquirer/number@4.0.3", "", { "dependencies": { "@inquirer/core": "^11.1.0", "@inquirer/type": "^4.0.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-TjQLe93GGo5snRlu83JxE38ZPqj5ZVggL+QqqAF2oBA5JOJoxx25GG3EGH/XN/Os5WOmKfO8iLVdCXQxXRZIMQ=="], + "@inquirer/number": ["@inquirer/number@4.0.4", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-CmMp9LF5HwE+G/xWsC333TlCzYYbXMkcADkKzcawh49fg2a1ryLc7JL1NJYYt1lJ+8f4slikNjJM9TEL/AljYQ=="], - "@inquirer/password": ["@inquirer/password@5.0.3", "", { "dependencies": { "@inquirer/ansi": "^2.0.2", "@inquirer/core": "^11.1.0", "@inquirer/type": "^4.0.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-rCozGbUMAHedTeYWEN8sgZH4lRCdgG/WinFkit6ZPsp8JaNg2T0g3QslPBS5XbpORyKP/I+xyBO81kFEvhBmjA=="], + "@inquirer/password": ["@inquirer/password@5.0.4", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-ZCEPyVYvHK4W4p2Gy6sTp9nqsdHQCfiPXIP9LbJVW4yCinnxL/dDDmPaEZVysGrj8vxVReRnpfS2fOeODe9zjg=="], - "@inquirer/prompts": ["@inquirer/prompts@8.1.0", "", { "dependencies": { "@inquirer/checkbox": "^5.0.3", "@inquirer/confirm": "^6.0.3", "@inquirer/editor": "^5.0.3", "@inquirer/expand": "^5.0.3", "@inquirer/input": "^5.0.3", "@inquirer/number": "^4.0.3", "@inquirer/password": "^5.0.3", "@inquirer/rawlist": "^5.1.0", "@inquirer/search": "^4.0.3", "@inquirer/select": "^5.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-LsZMdKcmRNF5LyTRuZE5nWeOjganzmN3zwbtNfcs6GPh3I2TsTtF1UYZlbxVfhxd+EuUqLGs/Lm3Xt4v6Az1wA=="], + "@inquirer/prompts": ["@inquirer/prompts@8.2.0", "", { "dependencies": { "@inquirer/checkbox": "^5.0.4", "@inquirer/confirm": "^6.0.4", "@inquirer/editor": "^5.0.4", "@inquirer/expand": "^5.0.4", "@inquirer/input": "^5.0.4", "@inquirer/number": "^4.0.4", "@inquirer/password": "^5.0.4", "@inquirer/rawlist": "^5.2.0", "@inquirer/search": "^4.1.0", "@inquirer/select": "^5.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-rqTzOprAj55a27jctS3vhvDDJzYXsr33WXTjODgVOru21NvBo9yIgLIAf7SBdSV0WERVly3dR6TWyp7ZHkvKFA=="], - "@inquirer/rawlist": ["@inquirer/rawlist@5.1.0", "", { "dependencies": { "@inquirer/core": "^11.1.0", "@inquirer/type": "^4.0.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yUCuVh0jW026Gr2tZlG3kHignxcrLKDR3KBp+eUgNz+BAdSeZk0e18yt2gyBr+giYhj/WSIHCmPDOgp1mT2niQ=="], + "@inquirer/rawlist": ["@inquirer/rawlist@5.2.0", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-CciqGoOUMrFo6HxvOtU5uL8fkjCmzyeB6fG7O1vdVAZVSopUBYECOwevDBlqNLyyYmzpm2Gsn/7nLrpruy9RFg=="], - "@inquirer/search": ["@inquirer/search@4.0.3", "", { "dependencies": { "@inquirer/core": "^11.1.0", "@inquirer/figures": "^2.0.2", "@inquirer/type": "^4.0.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-lzqVw0YwuKYetk5VwJ81Ba+dyVlhseHPx9YnRKQgwXdFS0kEavCz2gngnNhnMIxg8+j1N/rUl1t5s1npwa7bqg=="], + "@inquirer/search": ["@inquirer/search@4.1.0", "", { "dependencies": { "@inquirer/core": "^11.1.1", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-EAzemfiP4IFvIuWnrHpgZs9lAhWDA0GM3l9F4t4mTQ22IFtzfrk8xbkMLcAN7gmVML9O/i+Hzu8yOUyAaL6BKA=="], - "@inquirer/select": ["@inquirer/select@5.0.3", "", { "dependencies": { "@inquirer/ansi": "^2.0.2", "@inquirer/core": "^11.1.0", "@inquirer/figures": "^2.0.2", "@inquirer/type": "^4.0.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-M+ynbwS0ecQFDYMFrQrybA0qL8DV0snpc4kKevCCNaTpfghsRowRY7SlQBeIYNzHqXtiiz4RG9vTOeb/udew7w=="], + "@inquirer/select": ["@inquirer/select@5.0.4", "", { "dependencies": { "@inquirer/ansi": "^2.0.3", "@inquirer/core": "^11.1.1", "@inquirer/figures": "^2.0.3", "@inquirer/type": "^4.0.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-s8KoGpPYMEQ6WXc0dT9blX2NtIulMdLOO3LA1UKOiv7KFWzlJ6eLkEYTDBIi+JkyKXyn8t/CD6TinxGjyLt57g=="], - "@inquirer/type": ["@inquirer/type@4.0.2", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-cae7mzluplsjSdgFA6ACLygb5jC8alO0UUnFPyu0E7tNRPrL+q/f8VcSXp+cjZQ7l5CMpDpi2G1+IQvkOiL1Lw=="], + "@inquirer/type": ["@inquirer/type@4.0.3", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-cKZN7qcXOpj1h+1eTTcGDVLaBIHNMT1Rz9JqJP5MnEJ0JhgVWllx7H/tahUp5YEK1qaByH2Itb8wLG/iScD5kw=="], "@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=="], @@ -350,21 +350,21 @@ "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], - "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.23.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-shGng2EjBspvuqtFtcjcKf0WoZ9QCdL8iLYgdOoKSiSQ9pPyLJ4jQf62yhm4b2PpZNVcV/20gV6d8SyKzg6SZQ=="], + "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A=="], - "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.23.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-DxQ7Hm7B+6JiIkiRU3CSJmM15nTJDDezyaAv+x9NN8BfU0C49O8JuZIFu1Lr9AKEPV+ECIYM2X4HU0xm6IdiMQ=="], + "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg=="], - "@oxfmt/linux-arm64-gnu": ["@oxfmt/linux-arm64-gnu@0.23.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-7qTXPpENi45sEKsaYFit4VRywPVkX+ZJc5JVA17KW1coJ/SLUuRAdLjRipU+QTZsr1TF93HCmGFSlUjB7lmEVQ=="], + "@oxfmt/linux-arm64-gnu": ["@oxfmt/linux-arm64-gnu@0.24.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ItPDOPoQ0wLj/s8osc5ch57uUcA1Wk8r0YdO8vLRpXA3UNg7KPOm1vdbkIZRRiSUphZcuX5ioOEetEK8H7RlTw=="], - "@oxfmt/linux-arm64-musl": ["@oxfmt/linux-arm64-musl@0.23.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qkFXbf+K01B++j69o9mLvvyfhmmL4+qX7hGPA2PRDkE5xxuUTWdqboQQc1FgGI0teUlIYYyxjamq9UztL2A7NA=="], + "@oxfmt/linux-arm64-musl": ["@oxfmt/linux-arm64-musl@0.24.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-JkQO3WnQjQTJONx8nxdgVBfl6BBFfpp9bKhChYhWeakwJdr7QPOAWJ/v3FGZfr0TbqINwnNR74aVZayDDRyXEA=="], - "@oxfmt/linux-x64-gnu": ["@oxfmt/linux-x64-gnu@0.23.0", "", { "os": "linux", "cpu": "x64" }, "sha512-J7Q13Ujyn8IgjHD96urA377GOy8HerxC13OrEyYaM8iwH3gc/EoboK9AKu0bxp9qai4btPFDhnkRnpCwJE9pAw=="], + "@oxfmt/linux-x64-gnu": ["@oxfmt/linux-x64-gnu@0.24.0", "", { "os": "linux", "cpu": "x64" }, "sha512-N/SXlFO+2kak5gMt0oxApi0WXQDhwA0PShR0UbkY0PwtHjfSiDqJSOumyNqgQVoroKr1GNnoRmUqjZIz6DKIcw=="], - "@oxfmt/linux-x64-musl": ["@oxfmt/linux-x64-musl@0.23.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3gb25Zk2/y4An8fi399KdpLkDYFTJEB5Nq/sSHmeXG0pZlR/jnKoXEFHsjU+9nqF2wsuZ+tmkoi/swcaGG8+Qg=="], + "@oxfmt/linux-x64-musl": ["@oxfmt/linux-x64-musl@0.24.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WM0pek5YDCQf50XQ7GLCE9sMBCMPW/NPAEPH/Hx6Qyir37lEsP4rUmSECo/QFNTU6KBc9NnsviAyJruWPpCMXw=="], - "@oxfmt/win32-arm64": ["@oxfmt/win32-arm64@0.23.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-JKfRP2ENWwRZ73rMZFyChvRi/+oDEW+3obp1XIwecot8gvDHgGZ4nX3hTp4VPiBFL89JORMpWSKzJvjRDucJIw=="], + "@oxfmt/win32-arm64": ["@oxfmt/win32-arm64@0.24.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-vFCseli1KWtwdHrVlT/jWfZ8jP8oYpnPPEjI23mPLW8K/6GEJmmvy0PZP5NpWUFNTzX0lqie58XnrATJYAe9Xw=="], - "@oxfmt/win32-x64": ["@oxfmt/win32-x64@0.23.0", "", { "os": "win32", "cpu": "x64" }, "sha512-vgqtYK1X1n/KexCNQKWXao3hyOnmWuCzk2sQyCSpkLhjSNIDPm7dmnEkvOXhf1t0O5RjCwHpk2VB6Fuaq3GULg=="], + "@oxfmt/win32-x64": ["@oxfmt/win32-x64@0.24.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0tmlNzcyewAnauNeBCq0xmAkmiKzl+H09p0IdHy+QKrTQdtixtf+AOjDAADbRfihkS+heF15Pjc4IyJMdAAJjw=="], "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@0.11.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-F67T8dXgYIrgv6wpd52fKQFdmieSOHaxBkscgso64YdtEHrV3s52ASiZGNzw62TKihn9Ox9ek3PYx9XsxIJDUw=="], @@ -378,21 +378,21 @@ "@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@0.11.0", "", { "os": "win32", "cpu": "x64" }, "sha512-TsK4C61+mjmbkUJ3Q3E9Ev3VFbeI6prPEAm9FAOq8VsfUGEiIUBBjrZ8ysGoQXNiU3dCKpmu012ptVUZTk5/eg=="], - "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.38.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9rN3047QTyA4i73FKikDUBdczRcLtOsIwZ5TsEx5Q7jr5nBjolhYQOFQf9QdhBLdInxw1iX4+lgdMCf1g74zjg=="], + "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.39.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lT3hNhIa02xCujI6YGgjmYGg3Ht/X9ag5ipUVETaMpx5Rd4BbTNWUPif1WN1YZHxt3KLCIqaAe7zVhatv83HOQ=="], - "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.38.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Y1UHW4KOlg5NvyrSn/bVBQP8/LRuid7Pnu+BWGbAVVsFcK0b565YgMSO3Eu9nU3w8ke91dr7NFpUmS+bVkdkbw=="], + "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.39.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-UT+rfTWd+Yr7iJeSLd/7nF8X4gTYssKh+n77hxl6Oilp3NnG1CKRHxZDy3o3lIBnwgzJkdyUAiYWO1bTMXQ1lA=="], - "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.38.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZiVxPZizlXSnAMdkEFWX/mAj7U3bNiku8p6I9UgLrXzgGSSAhFobx8CaFGwVoKyWOd+gQgZ/ogCrunvx2k0CFg=="], + "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qocBkvS2V6rH0t9AT3DfQunMnj3xkM7srs5/Ycj2j5ZqMoaWd/FxHNVJDFP++35roKSvsRJoS0mtA8/77jqm6Q=="], - "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.38.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ELtlCIGZ72A65ATZZHFxHMFrkRtY+DYDCKiNKg6v7u5PdeOFey+OlqRXgXtXlxWjCL+g7nivwI2FPVsWqf05Qw=="], + "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-arZzAc1PPcz9epvGBBCMHICeyQloKtHX3eoOe62B3Dskn7gf6Q14wnDHr1r9Vp4vtcBATNq6HlKV14smdlC/qA=="], - "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.38.0", "", { "os": "linux", "cpu": "x64" }, "sha512-E1OcDh30qyng1m0EIlsOuapYkqk5QB6o6IMBjvDKqIoo6IrjlVAasoJfS/CmSH998gXRL3BcAJa6Qg9IxPFZnQ=="], + "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZVt5qsECpuNprdWxAPpDBwoixr1VTcZ4qAEQA2l/wmFyVPDYFD3oBY/SWACNnWBddMrswjTg9O8ALxYWoEpmXw=="], - "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.38.0", "", { "os": "linux", "cpu": "x64" }, "sha512-4AfpbM/4sQnr6S1dMijEPfsq4stQbN5vJ2jsahSy/QTcvIVbFkgY+RIhrA5UWlC6eb0rD5CdaPQoKGMJGeXpYw=="], + "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pB0hlGyKPbxr9NMIV783lD6cWL3MpaqnZRM9MWni4yBdHPTKyFNYdg5hGD0Bwg+UP4S2rOevq/+OO9x9Bi7E6g=="], - "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.38.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-OvUVYdI68OwXh3d1RjH9N/okCxb6PrOGtEtzXyqGA7Gk+IxyZcX0/QCTBwV8FNbSSzDePSSEHOKpoIB+VXdtvg=="], + "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.39.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gg2SFaJohI9+tIQVKXlPw3FsPQFi/eCSWiCgwPtPn5uzQxHRTeQEZKuluz1fuzR5U70TXubb2liZi4Dgl8LJQA=="], - "@oxlint/win32-x64": ["@oxlint/win32-x64@1.38.0", "", { "os": "win32", "cpu": "x64" }, "sha512-7IuZMYiZiOcgg5zHvpJY6jRlEwh8EB/uq7GsoQJO9hANq96TIjyntGByhIjFSsL4asyZmhTEki+MO/u5Fb/WQA=="], + "@oxlint/win32-x64": ["@oxlint/win32-x64@1.39.0", "", { "os": "win32", "cpu": "x64" }, "sha512-sbi25lfj74hH+6qQtb7s1wEvd1j8OQbTaH8v3xTcDjrwm579Cyh0HBv1YSZ2+gsnVwfVDiCTL1D0JsNqYXszVA=="], "@playwright/test": ["@playwright/test@1.57.0", "", { "dependencies": { "playwright": "1.57.0" }, "bin": { "playwright": "cli.js" } }, "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA=="], @@ -634,9 +634,9 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], + "@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], @@ -690,7 +690,7 @@ "basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="], - "better-auth": ["better-auth@1.4.10", "", { "dependencies": { "@better-auth/core": "1.4.10", "@better-auth/telemetry": "1.4.10", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.7", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-0kqwEBJLe8eyFzbUspRG/htOriCf9uMLlnpe34dlIJGdmDfPuQISd4shShvUrvIVhPxsY1dSTXdXPLpqISYOYg=="], + "better-auth": ["better-auth@1.4.12", "", { "dependencies": { "@better-auth/core": "1.4.12", "@better-auth/telemetry": "1.4.12", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.7", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-FsFMnWgk+AGrxsIGbpWLCibgYcbm6uNhPHln3ohXFDXSRa0gk39Beuh54Q+x6ml2qYodF0snxf/tPtDpBI/JiA=="], "better-call": ["better-call@1.1.7", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-6gaJe1bBIEgVebQu/7q9saahVzvBsGaByEnE8aDVncZEDiJO7sdNB28ot9I6iXSbR25egGmmZ6aIURXyQHRraQ=="], @@ -988,7 +988,7 @@ "http-errors-enhanced": ["http-errors-enhanced@4.0.2", "", {}, "sha512-5EXN1gmhJVvuWpNfz+RclWvLnnENEXNMPfww3gm30H9mQzPF4QSBj/MD5FRkVDxGIUhO/cR2GSLCd/6C6xpBcw=="], - "iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -1246,9 +1246,9 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], - "oxfmt": ["oxfmt@0.23.0", "", { "dependencies": { "tinypool": "2.0.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.23.0", "@oxfmt/darwin-x64": "0.23.0", "@oxfmt/linux-arm64-gnu": "0.23.0", "@oxfmt/linux-arm64-musl": "0.23.0", "@oxfmt/linux-x64-gnu": "0.23.0", "@oxfmt/linux-x64-musl": "0.23.0", "@oxfmt/win32-arm64": "0.23.0", "@oxfmt/win32-x64": "0.23.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-dh4rlNBua93aVf2ZaDecbQxVLMnUUTvDi1K1fdvBdontQeEf6K22Z1KQg5QKl2D9aNFeFph+wOVwcjjYUIO6Mw=="], + "oxfmt": ["oxfmt@0.24.0", "", { "dependencies": { "tinypool": "2.0.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.24.0", "@oxfmt/darwin-x64": "0.24.0", "@oxfmt/linux-arm64-gnu": "0.24.0", "@oxfmt/linux-arm64-musl": "0.24.0", "@oxfmt/linux-x64-gnu": "0.24.0", "@oxfmt/linux-x64-musl": "0.24.0", "@oxfmt/win32-arm64": "0.24.0", "@oxfmt/win32-x64": "0.24.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-UjeM3Peez8Tl7IJ9s5UwAoZSiDRMww7BEc21gDYxLq3S3/KqJnM3mjNxsoSHgmBvSeX6RBhoVc2MfC/+96RdSw=="], - "oxlint": ["oxlint@1.38.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.38.0", "@oxlint/darwin-x64": "1.38.0", "@oxlint/linux-arm64-gnu": "1.38.0", "@oxlint/linux-arm64-musl": "1.38.0", "@oxlint/linux-x64-gnu": "1.38.0", "@oxlint/linux-x64-musl": "1.38.0", "@oxlint/win32-arm64": "1.38.0", "@oxlint/win32-x64": "1.38.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.10.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-XT7tBinQS+hVLxtfJOnokJ9qVBiQvZqng40tDgR6qEJMRMnpVq/JwYfbYyGntSq8MO+Y+N9M1NG4bAMFUtCJiw=="], + "oxlint": ["oxlint@1.39.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.39.0", "@oxlint/darwin-x64": "1.39.0", "@oxlint/linux-arm64-gnu": "1.39.0", "@oxlint/linux-arm64-musl": "1.39.0", "@oxlint/linux-x64-gnu": "1.39.0", "@oxlint/linux-x64-musl": "1.39.0", "@oxlint/win32-arm64": "1.39.0", "@oxlint/win32-x64": "1.39.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.10.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-wSiLr0wjG+KTU6c1LpVoQk7JZ7l8HCKlAkVDVTJKWmCGazsNxexxnOXl7dsar92mQcRnzko5g077ggP3RINSjA=="], "oxlint-tsgolint": ["oxlint-tsgolint@0.11.0", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.11.0", "@oxlint-tsgolint/darwin-x64": "0.11.0", "@oxlint-tsgolint/linux-arm64": "0.11.0", "@oxlint-tsgolint/linux-x64": "0.11.0", "@oxlint-tsgolint/win32-arm64": "0.11.0", "@oxlint-tsgolint/win32-x64": "0.11.0" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-fGYb7z/cljC0Rjtbxh7mIe8vtF/M9TShLvniwc2rdcqNG3Z9g3nM01cr2kWRb1DZdbY4/kItvIsrV4uhaMifyQ=="], @@ -1338,7 +1338,7 @@ "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], - "react-hook-form": ["react-hook-form@7.70.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-COOMajS4FI3Wuwrs3GPpi/Jeef/5W1DRR84Yl5/ShlT3dKVFUfoGiEZ/QE6Uw8P4T2/CLJdcTVYKvWBMQTEpvw=="], + "react-hook-form": ["react-hook-form@7.71.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-oFDt/iIFMV9ZfV52waONXzg4xuSlbwKUPvXVH2jumL1me5qFhBMc4knZxuXiZ2+j6h546sYe3ZKJcg/900/iHw=="], "react-is": ["react-is@19.2.3", "", {}, "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA=="], @@ -1546,7 +1546,7 @@ "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], - "vite-tsconfig-paths": ["vite-tsconfig-paths@6.0.3", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-7bL7FPX/DSviaZGYUKowWF1AiDVWjMjxNbE8lyaVGDezkedWqfGhlnQ4BZXre0ZN5P4kAgIJfAlgFDVyjrCIyg=="], + "vite-tsconfig-paths": ["vite-tsconfig-paths@6.0.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3", "vite": "*" } }, "sha512-iIsEJ+ek5KqRTK17pmxtgIxXtqr3qDdE6OxrP9mVeGhVDNXRJTKN/l9oMbujTQNzMLe6XZ8qmpztfbkPu2TiFQ=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], @@ -1624,6 +1624,10 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@types/better-sqlite3/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/ws/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], @@ -1632,6 +1636,8 @@ "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "bun-types/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "c12/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], diff --git a/openapi-ts.config.ts b/openapi-ts.config.ts index 1cde65df..8a691832 100644 --- a/openapi-ts.config.ts +++ b/openapi-ts.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ input: `http://${config.serverIp}:4096/api/v1/openapi.json`, output: { path: "./app/client/api-client", - format: "biome", + postProcess: ["oxfmt"], }, plugins: [...defaultPlugins, "@tanstack/react-query", "@hey-api/client-fetch"], }); diff --git a/package.json b/package.json index e6cae0e8..3373ac8d 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@dnd-kit/utilities": "^3.2.2", "@hono/standard-validator": "^0.2.0", "@hookform/resolvers": "^5.2.2", - "@inquirer/prompts": "^8.0.2", + "@inquirer/prompts": "^8.2.0", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-collapsible": "^1.1.12", @@ -49,7 +49,7 @@ "@scalar/hono-api-reference": "^0.9.32", "@tanstack/react-query": "^5.90.16", "arktype": "^2.1.28", - "better-auth": "^1.4.10", + "better-auth": "^1.4.12", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "commander": "^14.0.2", @@ -71,7 +71,7 @@ "qrcode.react": "^4.2.0", "react": "^19.2.1", "react-dom": "^19.2.1", - "react-hook-form": "^7.70.0", + "react-hook-form": "^7.71.0", "react-markdown": "^10.1.0", "react-router": "^7.12.0", "react-router-hono-server": "^2.22.0", @@ -88,7 +88,7 @@ "devDependencies": { "@faker-js/faker": "^10.2.0", "@happy-dom/global-registrator": "^20.1.0", - "@hey-api/openapi-ts": "^0.90.2", + "@hey-api/openapi-ts": "^0.90.3", "@libsql/client": "^0.17.0", "@playwright/test": "^1.57.0", "@react-router/dev": "^7.12.0", @@ -98,15 +98,15 @@ "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.1", "@types/bun": "^1.3.4", - "@types/node": "^25.0.3", - "@types/react": "^19.2.7", + "@types/node": "^25.0.7", + "@types/react": "^19.2.8", "@types/react-dom": "^19.2.3", "@types/semver": "^7.7.1", "dotenv-cli": "^11.0.0", "drizzle-kit": "^0.31.7", "lightningcss": "^1.30.2", - "oxfmt": "^0.23.0", - "oxlint": "^1.38.0", + "oxfmt": "^0.24.0", + "oxlint": "^1.39.0", "oxlint-tsgolint": "^0.11.0", "tailwindcss": "^4.1.17", "tinyglobby": "^0.2.15", @@ -114,7 +114,7 @@ "typescript": "^5.9.3", "vite": "^7.3.1", "vite-bundle-analyzer": "^1.2.3", - "vite-tsconfig-paths": "^6.0.3" + "vite-tsconfig-paths": "^6.0.4" }, "overrides": { "esbuild": "^0.27.2", From 1622e34b0d99a8c8c58020db7d91f275cd9eae7e Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Thu, 15 Jan 2026 17:57:38 +0100 Subject: [PATCH 23/23] refactoring(bw-limit): stylistic refactoring --- .gitignore | 2 + .../repository-forms/advanced-tls-form.tsx | 297 ++-- ...ing_ultron.sql => 0033_curved_warhawk.sql} | 8 +- app/drizzle/meta/0033_snapshot.json | 1243 +++++++++++++++++ app/drizzle/meta/_journal.json | 479 +++---- app/schemas/restic.ts | 9 +- app/server/db/schema.ts | 9 +- app/server/utils/rclone-dummy.ts | 0 app/server/utils/restic.ts | 80 +- 9 files changed, 1657 insertions(+), 470 deletions(-) rename app/drizzle/{0032_outstanding_ultron.sql => 0033_curved_warhawk.sql} (55%) create mode 100644 app/drizzle/meta/0033_snapshot.json delete mode 100644 app/server/utils/rclone-dummy.ts diff --git a/.gitignore b/.gitignore index d0a5ab04..cbe33bc5 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ node_modules/ playwright/.auth playwright/temp + +.idea/ diff --git a/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx b/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx index 336d4925..eabacd0c 100644 --- a/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx +++ b/app/client/modules/repositories/components/repository-forms/advanced-tls-form.tsx @@ -10,13 +10,7 @@ import { import { Input } from "../../../../components/ui/input"; import { Textarea } from "../../../../components/ui/textarea"; import { Checkbox } from "../../../../components/ui/checkbox"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "../../../../components/ui/select"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../../components/ui/select"; import { Tooltip, TooltipContent, TooltipTrigger } from "../../../../components/ui/tooltip"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../../../components/ui/collapsible"; import type { RepositoryFormValues } from "../create-repository-form"; @@ -30,26 +24,15 @@ type Props = { export const AdvancedForm = ({ form }: Props) => { const insecureTls = form.watch("insecureTls"); const cacert = form.watch("cacert"); - const uploadEnabled = form.watch("uploadLimit.enabled"); - const downloadEnabled = form.watch("downloadLimit.enabled"); + const uploadLimitEnabled = form.watch("uploadLimit.enabled"); + const downloadLimitEnabled = form.watch("downloadLimit.enabled"); return ( Advanced Settings - {/* Bandwidth Limit Controls */} -
    -
    -
    -

    Bandwidth Limits

    -

    - Control upload and download speeds to prevent saturating network bandwidth -

    -
    -
    - -
    - {/* Upload Limit */} +
    +
    { render={({ field }) => ( - + -
    +
    Enable upload speed limit - - Limit upload speed to the repository - + Limit upload speed to the repository
    )} /> - - {uploadEnabled && ( -
    -
    - ( - - -
    - field.onChange(parseFloat(e.target.value) || 0)} - /> -
    -
    -
    -
    - +
    + ( + + +
    + field.onChange(parseInt(e.target.value, 10) || 1)} + /> +
    +
    +
    - - )} - /> - ( - - - - - )} - /> -
    -
    - )} +
    + + + )} + /> + ( + + + + + )} + /> +
    +
    - {/* Download Limit */} -
    - - ( - - - - -
    - Enable download speed limit - - Limit download speed from the repository - -
    -
    - )} - /> - - {downloadEnabled && ( -
    -
    - ( - - -
    - field.onChange(parseFloat(e.target.value) || 0)} - /> -
    -
    -
    -
    - - - - )} - /> - ( - - - - - )} - /> +
    + ( + + + + +
    + Enable download speed limit + Limit download speed from the repository
    -
    + )} + /> + +
    +
    + ( + + +
    + field.onChange(parseInt(e.target.value, 10) || 1)} + /> +
    +
    +
    +
    + + + + )} + /> + ( + + + + + )} + /> +
    diff --git a/app/drizzle/0032_outstanding_ultron.sql b/app/drizzle/0033_curved_warhawk.sql similarity index 55% rename from app/drizzle/0032_outstanding_ultron.sql rename to app/drizzle/0033_curved_warhawk.sql index 669c156f..763c6f9d 100644 --- a/app/drizzle/0032_outstanding_ultron.sql +++ b/app/drizzle/0033_curved_warhawk.sql @@ -1,6 +1,6 @@ ALTER TABLE `repositories_table` ADD `upload_limit_enabled` integer DEFAULT false NOT NULL;--> statement-breakpoint -ALTER TABLE `repositories_table` ADD `upload_limit_value` integer DEFAULT 0 NOT NULL;--> statement-breakpoint -ALTER TABLE `repositories_table` ADD `upload_limit_unit` text DEFAULT 'MB/s' NOT NULL;--> statement-breakpoint +ALTER TABLE `repositories_table` ADD `upload_limit_value` real DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE `repositories_table` ADD `upload_limit_unit` text DEFAULT 'Mbps' NOT NULL;--> statement-breakpoint ALTER TABLE `repositories_table` ADD `download_limit_enabled` integer DEFAULT false NOT NULL;--> statement-breakpoint -ALTER TABLE `repositories_table` ADD `download_limit_value` integer DEFAULT 0 NOT NULL;--> statement-breakpoint -ALTER TABLE `repositories_table` ADD `download_limit_unit` text DEFAULT 'MB/s' NOT NULL; \ No newline at end of file +ALTER TABLE `repositories_table` ADD `download_limit_value` real DEFAULT 0 NOT NULL;--> statement-breakpoint +ALTER TABLE `repositories_table` ADD `download_limit_unit` text DEFAULT 'Mbps' NOT NULL; \ No newline at end of file diff --git a/app/drizzle/meta/0033_snapshot.json b/app/drizzle/meta/0033_snapshot.json new file mode 100644 index 00000000..c9a2b486 --- /dev/null +++ b/app/drizzle/meta/0033_snapshot.json @@ -0,0 +1,1243 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "2d9b50c4-e0dd-4422-abd8-71fb1cfb18c6", + "prevId": "ad86ee2a-231e-45d1-9910-e4b7cac6a8af", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "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": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_users_table_id_fk": { + "name": "account_user_id_users_table_id_fk", + "tableFrom": "account", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "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_warning": { + "name": "notify_on_warning", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "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 + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "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": "'[]'" + }, + "exclude_if_present": { + "name": "exclude_if_present", + "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 + }, + "one_file_system": { + "name": "one_file_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "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_short_id_unique": { + "name": "backup_schedules_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "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 + }, + "upload_limit_enabled": { + "name": "upload_limit_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "upload_limit_value": { + "name": "upload_limit_value", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "upload_limit_unit": { + "name": "upload_limit_unit", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Mbps'" + }, + "download_limit_enabled": { + "name": "download_limit_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "download_limit_value": { + "name": "download_limit_value", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "download_limit_unit": { + "name": "download_limit_unit", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Mbps'" + }, + "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 + } + }, + "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": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "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)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_table_token_unique": { + "name": "sessions_table_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "sessionsTable_userId_idx": { + "name": "sessionsTable_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "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": {} + }, + "two_factor": { + "name": "two_factor", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "twoFactor_secret_idx": { + "name": "twoFactor_secret_idx", + "columns": [ + "secret" + ], + "isUnique": false + }, + "twoFactor_userId_idx": { + "name": "twoFactor_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "two_factor_user_id_users_table_id_fk": { + "name": "two_factor_user_id_users_table_id_fk", + "tableFrom": "two_factor", + "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": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "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)" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": [ + "username" + ], + "isUnique": true + }, + "users_table_email_unique": { + "name": "users_table_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "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)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "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 8add8b31..3e4b1781 100644 --- a/app/drizzle/meta/_journal.json +++ b/app/drizzle/meta/_journal.json @@ -1,237 +1,244 @@ { - "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": 1764794371040, - "tag": "0018_breezy_invaders", - "breakpoints": true - }, - { - "idx": 19, - "version": "6", - "when": 1764839917446, - "tag": "0019_secret_nomad", - "breakpoints": true - }, - { - "idx": 20, - "version": "6", - "when": 1764847918249, - "tag": "0020_even_dexter_bennett", - "breakpoints": true - }, - { - "idx": 21, - "version": "6", - "when": 1765307881092, - "tag": "0021_steady_viper", - "breakpoints": true - }, - { - "idx": 22, - "version": "6", - "when": 1765794552191, - "tag": "0022_woozy_shen", - "breakpoints": true - }, - { - "idx": 23, - "version": "6", - "when": 1766320570509, - "tag": "0023_special_thor", - "breakpoints": true - }, - { - "idx": 24, - "version": "6", - "when": 1766325504548, - "tag": "0024_schedules-one-fs", - "breakpoints": true - }, - { - "idx": 25, - "version": "6", - "when": 1766431021321, - "tag": "0025_remarkable_pete_wisdom", - "breakpoints": true - }, - { - "idx": 26, - "version": "6", - "when": 1766765013108, - "tag": "0026_migrate-local-repo-paths", - "breakpoints": true - }, - { - "idx": 27, - "version": "6", - "when": 1766778073418, - "tag": "0027_careful_cammi", - "breakpoints": true - }, - { - "idx": 28, - "version": "6", - "when": 1766778162985, - "tag": "0028_third_amazoness", - "breakpoints": true - }, - { - "idx": 29, - "version": "6", - "when": 1767819883495, - "tag": "0029_boring_luke_cage", - "breakpoints": true - }, - { - "idx": 30, - "version": "6", - "when": 1767821088612, - "tag": "0030_lower-trim-username", - "breakpoints": true - }, - { - "idx": 31, - "version": "6", - "when": 1767863951955, - "tag": "0031_graceful_squadron_supreme", - "breakpoints": true - }, - { - "idx": 32, - "version": "6", - "when": 1768136755563, - "tag": "0032_lowercase-email", - "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": 1764794371040, + "tag": "0018_breezy_invaders", + "breakpoints": true + }, + { + "idx": 19, + "version": "6", + "when": 1764839917446, + "tag": "0019_secret_nomad", + "breakpoints": true + }, + { + "idx": 20, + "version": "6", + "when": 1764847918249, + "tag": "0020_even_dexter_bennett", + "breakpoints": true + }, + { + "idx": 21, + "version": "6", + "when": 1765307881092, + "tag": "0021_steady_viper", + "breakpoints": true + }, + { + "idx": 22, + "version": "6", + "when": 1765794552191, + "tag": "0022_woozy_shen", + "breakpoints": true + }, + { + "idx": 23, + "version": "6", + "when": 1766320570509, + "tag": "0023_special_thor", + "breakpoints": true + }, + { + "idx": 24, + "version": "6", + "when": 1766325504548, + "tag": "0024_schedules-one-fs", + "breakpoints": true + }, + { + "idx": 25, + "version": "6", + "when": 1766431021321, + "tag": "0025_remarkable_pete_wisdom", + "breakpoints": true + }, + { + "idx": 26, + "version": "6", + "when": 1766765013108, + "tag": "0026_migrate-local-repo-paths", + "breakpoints": true + }, + { + "idx": 27, + "version": "6", + "when": 1766778073418, + "tag": "0027_careful_cammi", + "breakpoints": true + }, + { + "idx": 28, + "version": "6", + "when": 1766778162985, + "tag": "0028_third_amazoness", + "breakpoints": true + }, + { + "idx": 29, + "version": "6", + "when": 1767819883495, + "tag": "0029_boring_luke_cage", + "breakpoints": true + }, + { + "idx": 30, + "version": "6", + "when": 1767821088612, + "tag": "0030_lower-trim-username", + "breakpoints": true + }, + { + "idx": 31, + "version": "6", + "when": 1767863951955, + "tag": "0031_graceful_squadron_supreme", + "breakpoints": true + }, + { + "idx": 32, + "version": "6", + "when": 1768136755563, + "tag": "0032_lowercase-email", + "breakpoints": true + }, + { + "idx": 33, + "version": "6", + "when": 1768490085610, + "tag": "0033_curved_warhawk", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/app/schemas/restic.ts b/app/schemas/restic.ts index f9b5123c..846fc0c7 100644 --- a/app/schemas/restic.ts +++ b/app/schemas/restic.ts @@ -14,17 +14,16 @@ export const REPOSITORY_BACKENDS = { export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS; export const BANDWIDTH_UNITS = { - "Kbps": "Kbps", - "Mbps": "Mbps", - "Gbps": "Gbps", + Kbps: "Kbps", + Mbps: "Mbps", + Gbps: "Gbps", } as const; export type BandwidthUnit = keyof typeof BANDWIDTH_UNITS; -// Bandwidth limit configuration export const bandwidthLimitSchema = type({ enabled: "boolean = false", - value: "number >= 0 = 0", + value: "number > 0 = 1", unit: type.valueOf(BANDWIDTH_UNITS).default("Mbps"), }); diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index 09c8ee55..23ad9da9 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -1,6 +1,12 @@ import { relations, sql } from "drizzle-orm"; import { index, int, integer, sqliteTable, text, real, primaryKey, unique } from "drizzle-orm/sqlite-core"; -import type { CompressionMode, RepositoryBackend, repositoryConfigSchema, RepositoryStatus, BandwidthUnit } from "~/schemas/restic"; +import type { + CompressionMode, + RepositoryBackend, + repositoryConfigSchema, + RepositoryStatus, + BandwidthUnit, +} from "~/schemas/restic"; import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes"; import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications"; @@ -159,7 +165,6 @@ export const repositoriesTable = sqliteTable("repositories_table", { status: text().$type().default("unknown"), lastChecked: int("last_checked", { mode: "number" }), lastError: text("last_error"), - // Bandwidth limit fields uploadLimitEnabled: int("upload_limit_enabled", { mode: "boolean" }).notNull().default(false), uploadLimitValue: real("upload_limit_value").notNull().default(0), uploadLimitUnit: text("upload_limit_unit").$type().notNull().default("Mbps"), diff --git a/app/server/utils/rclone-dummy.ts b/app/server/utils/rclone-dummy.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/app/server/utils/restic.ts b/app/server/utils/restic.ts index 21987af4..733c2e1d 100644 --- a/app/server/utils/restic.ts +++ b/app/server/utils/restic.ts @@ -846,30 +846,13 @@ const copy = async ( args.push("latest"); } - // Apply common args without automatic bandwidth limit injection to prevent duplication addCommonArgs(args, env, destConfig, { skipBandwidth: true }); - // Manually handle bandwidth limits with correct copy semantics: - // --limit-download uses sourceConfig.downloadLimit (limiting downloads from source repo) - // --limit-upload uses destConfig.uploadLimit (limiting uploads to destination repo) const sourceDownloadLimit = formatBandwidthLimit(sourceConfig.downloadLimit); const destUploadLimit = formatBandwidthLimit(destConfig.uploadLimit); - if (sourceConfig.backend === "rclone") { - // For rclone source backends, use rclone.from.bwlimit with source download limit only - let effectiveSourceLimit = ""; - if (sourceDownloadLimit) { - effectiveSourceLimit = sourceDownloadLimit; - } - - if (effectiveSourceLimit) { - args.push("-o", `rclone.from.bwlimit=${effectiveSourceLimit}`); - } - } else { - // For restic source backends, apply download limit from source - if (sourceDownloadLimit) { - args.push("--limit-download", sourceDownloadLimit); - } + if (sourceDownloadLimit) { + args.push("--limit-download", sourceDownloadLimit); } // Apply upload limit to destination for all backends @@ -899,13 +882,11 @@ const copy = async ( }; }; -// Helper function to convert bandwidth limit to restic/rclone format const formatBandwidthLimit = (limit?: BandwidthLimit): string => { if (!limit || !limit.enabled || limit.value <= 0) { return ""; } - // Convert to KiB/s for restic compatibility, or use suffixed format for rclone let kibibytesPerSecond: number; switch (limit.unit) { case "Kbps": @@ -924,11 +905,15 @@ const formatBandwidthLimit = (limit?: BandwidthLimit): string => { return ""; } - // Return as integer KiB/s for restic return `${Math.floor(kibibytesPerSecond)}`; }; -export const addCommonArgs = (args: string[], env: Record, config?: RepositoryConfig, options?: { skipBandwidth?: boolean }) => { +export const addCommonArgs = ( + args: string[], + env: Record, + config?: RepositoryConfig, + options?: { skipBandwidth?: boolean }, +) => { args.push("--json"); if (env._SFTP_SSH_ARGS) { @@ -943,37 +928,15 @@ export const addCommonArgs = (args: string[], env: Record, confi args.push("--cacert", env.RESTIC_CACERT); } - // Add bandwidth limits if configuration is provided and not skipped if (config && !options?.skipBandwidth) { - if (config.backend === "rclone") { - // For rclone backends, consolidate both upload and download limits into one bwlimit - const uploadLimit = formatBandwidthLimit(config.uploadLimit); - const downloadLimit = formatBandwidthLimit(config.downloadLimit); + const uploadLimit = formatBandwidthLimit(config.uploadLimit); + if (uploadLimit) { + args.push("--limit-upload", uploadLimit); + } - // Determine effective limit (choose more restrictive when both exist) - let effectiveLimit = ""; - if (uploadLimit && downloadLimit) { - effectiveLimit = parseInt(uploadLimit, 10) < parseInt(downloadLimit, 10) ? uploadLimit : downloadLimit; - } else if (uploadLimit) { - effectiveLimit = uploadLimit; - } else if (downloadLimit) { - effectiveLimit = downloadLimit; - } - - if (effectiveLimit) { - args.push("-o", `rclone.bwlimit=${effectiveLimit}`); - } - } else { - // For restic backends, handle upload and download limits separately - const uploadLimit = formatBandwidthLimit(config.uploadLimit); - if (uploadLimit) { - args.push("--limit-upload", uploadLimit); - } - - const downloadLimit = formatBandwidthLimit(config.downloadLimit); - if (downloadLimit) { - args.push("--limit-download", downloadLimit); - } + const downloadLimit = formatBandwidthLimit(config.downloadLimit); + if (downloadLimit) { + args.push("--limit-download", downloadLimit); } } }; @@ -995,26 +958,17 @@ export const restic = { copy, }; -// Helper function to clean up temporary files export const cleanupTemporaryKeys = async (env: Record) => { const keysToClean = ["_SFTP_KEY_PATH", "_SFTP_KNOWN_HOSTS_PATH", "RESTIC_CACERT", "GOOGLE_APPLICATION_CREDENTIALS"]; for (const key of keysToClean) { if (env[key]) { - try { - await fs.unlink(env[key]); - } catch (_error) { - // Ignore errors when cleaning up temporary files - } + await fs.unlink(env[key]).catch(() => {}); } } // Clean up custom password files if (env.RESTIC_PASSWORD_FILE && env.RESTIC_PASSWORD_FILE !== RESTIC_PASS_FILE) { - try { - await fs.unlink(env.RESTIC_PASSWORD_FILE); - } catch (_error) { - // Ignore errors when cleaning up temporary files - } + await fs.unlink(env.RESTIC_PASSWORD_FILE).catch(() => {}); } };